Files
benilla-octo/crates/benilla/assets/ui/PartyFrame.xml
T
SamandClaude Fable 5 94de741cb1 /target, /assist and /follow, and right click opens a clam
- /target and /assist find units by name like the original, and /follow walks you behind your target
- right click opens clams and lockboxes, and the tooltip promises it
- the freeze thaws clean, poses release instead of sticking, and you drop to neutral standing before it grips
- unit frame art layers over the bars like the original

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 18:02:44 +07:00

394 lines
21 KiB
XML

<Ui>
<!--
The party member frames + the group-invite popup (decision 0434 phase 3). Geometry, art
paths, behavior laws, and the popup entry are QUOTED from the reference client's own
FrameXML, extracted through the full MPQ patch chain (decision 0246; survey 2026-07-17):
PartyFrame.xml, PartyFrameTemplates.xml, PartyMemberFrame.lua, StaticPopup.lua ("PARTY_INVITE"),
UIParent.lua (the PARTY_INVITE_REQUEST wiring). Data flow is ours: the app feeds
party1..party4 unit snapshots + the PartyState roster (crates/benilla/src/ui_party.rs) and
fires the PARTY_* events; the popup's Accept/Decline queue PartyRequests the app drains.
THE CASCADE. The ref anchors frames 2-4 to the PREVIOUS frame's PetFrame bottom-left at
(-23,-10) — spacing rides the pet frame, which sits at (23,-27) when hidden (26 tall), so
the no-pet stride is TOPLEFT->BOTTOMLEFT (0,-10). Pet frames land in phase 4; until then
the cascade uses that no-pet stride directly, which is byte-identical to the ref's layout
for petless parties (the only kind phase 3 renders).
DEVIATIONS / DEFERRED (phase 4+, decision 0434):
- Pet frames, the 4 debuff buttons (48,-32 +2px chain), the 16/8 buff hover panel, and the
dispellable-debuff Status ring: they need the party aura/pet feed (phase 4).
(The PVP icon landed with decision 0646 — UnitFactionGroup exists now.)
- HIDE_PARTY_INTERFACE (hide-in-raid uvar): waits for the raid UI slice.
- Portrait: our high-res model bake (decision 0105), same as every unit frame.
- The ref's low-health portrait pulse flips alpha 127/255<->255/255 on 0.5 s half-cycles
(PartyMemberFrame.lua's sign-flip counter); transcribed as the same triangle wave.
DOUBLE-DECLINE, KEPT: the ref's popup calls DeclineGroup() in OnCancel AND again in OnHide
(only OnAccept sets the inviteAccepted guard), so a clicked Decline sends two
CMSG_GROUP_DECLINE. vmangos silently ignores the second (no pending invite -> early
return). Transcribed verbatim rather than "fixed" - the wire tolerates it and the guard's
real job is the timeout/override/escape paths.
-->
<Script><![CDATA[
-- StaticPopup entry, quoted from the ref's StaticPopup.lua (INVITATION =
-- "%s invites you to a group.", GlobalStrings:2586-area; sound + 60 s silent timeout +
-- whileDead + hideOnEscape are the ref's own fields).
StaticPopupDialogs["PARTY_INVITE"] = {
text = INVITATION,
button1 = ACCEPT,
button2 = DECLINE,
sound = "igPlayerInvite",
OnShow = function()
StaticPopupDialogs["PARTY_INVITE"].inviteAccepted = nil
end,
OnAccept = function()
AcceptGroup()
StaticPopupDialogs["PARTY_INVITE"].inviteAccepted = 1
end,
OnCancel = function()
DeclineGroup()
end,
OnHide = function()
if not StaticPopupDialogs["PARTY_INVITE"].inviteAccepted then
DeclineGroup()
end
end,
timeout = 60,
whileDead = 1,
hideOnEscape = 1,
}
-- ref PartyMemberFrame.lua's OnLoad: cache children, register, first paint.
function BenillaPartyFrame_OnLoad(frame)
local id = frame:GetID()
local name = frame:GetName()
local tf = name .. "TextureFrame"
frame.unit = "party" .. id
frame.portrait = getglobal(name .. "Portrait")
frame.healthBar = getglobal(name .. "HealthBar")
frame.manaBar = getglobal(name .. "ManaBar")
-- The recessed-bar look, the reference's own mechanism (ref-TargetFrame.lua l.32-34):
-- push the bars one frame level BELOW the 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
-- the BACKGROUND party art over the ARTWORK bar fills. See the LAYERING note below.
local textureFrameLevel = getglobal(tf):GetFrameLevel()
frame.healthBar:SetFrameLevel(textureFrameLevel - 1)
frame.manaBar:SetFrameLevel(textureFrameLevel - 1)
frame.nameText = getglobal(tf .. "Name")
frame.leaderIcon = getglobal(tf .. "LeaderIcon")
frame.masterIcon = getglobal(tf .. "MasterIcon")
frame.disconnect = getglobal(tf .. "Disconnect")
frame.pvpIcon = getglobal(tf .. "PVPIcon")
frame.flashTimer = 0
SetPortraitTexture(frame.portrait, frame.unit)
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("PARTY_MEMBERS_CHANGED")
frame:RegisterEvent("PARTY_LEADER_CHANGED")
frame:RegisterEvent("PARTY_LOOT_METHOD_CHANGED")
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("UNIT_FACTION")
frame:EnableMouse(true)
BenillaPartyFrame_Update(frame)
end
function BenillaPartyFrame_OnEvent(frame)
-- UNIT_* events name their unit in arg1; other units' updates are not ours.
if string.sub(event, 1, 5) == "UNIT_" and arg1 ~= frame.unit then
return
end
BenillaPartyFrame_Update(frame)
end
-- ref PartyMemberHealthCheck: the portrait recolor law + the disconnect override.
function BenillaPartyFrame_HealthCheck(frame)
local unit = frame.unit
if not UnitIsConnected(unit) then
-- Disconnected: bar forced full and grey, portrait desaturated, icon on.
frame.healthBar:SetMinMaxValues(0, 1)
frame.healthBar:SetValue(1)
frame.healthBar:SetStatusBarColor(0.5, 0.5, 0.5)
SetDesaturation(frame.portrait, 1)
frame.disconnect:Show()
return
end
SetDesaturation(frame.portrait, nil)
frame.disconnect:Hide()
frame.healthBar:SetStatusBarColor(0.0, 1.0, 0.0)
local hp, hpMax = UnitHealth(unit), UnitHealthMax(unit)
if UnitIsDead(unit) then
frame.portrait:SetVertexColor(0.35, 0.35, 0.35, 1.0)
elseif UnitIsGhost(unit) then
frame.portrait:SetVertexColor(0.2, 0.2, 0.75, 1.0)
elseif hpMax > 0 and hp / hpMax <= 0.2 then
frame.portrait:SetVertexColor(1.0, 0.0, 0.0, 1.0)
else
frame.portrait:SetVertexColor(1.0, 1.0, 1.0, 1.0)
end
end
function BenillaPartyFrame_Update(frame)
local unit = frame.unit
if not UnitExists(unit) then
frame:Hide()
return
end
frame:Show()
frame.nameText:SetText(UnitName(unit) or "Unknown")
frame.healthBar:SetMinMaxValues(0, UnitHealthMax(unit))
frame.healthBar:SetValue(UnitHealth(unit))
local maxPower = UnitPowerMax(unit)
if maxPower > 0 then
frame.manaBar:Show()
local _, token = UnitPowerType(unit)
local c = BENILLA_POWER_COLORS[token] or BENILLA_POWER_COLORS.MANA
frame.manaBar:SetStatusBarColor(c.r, c.g, c.b)
frame.manaBar:SetMinMaxValues(0, maxPower)
frame.manaBar:SetValue(UnitPower(unit))
else
frame.manaBar:Hide()
end
-- Leader + master-looter icons (ref: GetPartyLeaderIndex / GetLootMethod vs own id).
local id = frame:GetID()
if GetPartyLeaderIndex() == id then
frame.leaderIcon:Show()
else
frame.leaderIcon:Hide()
end
local lootMethod, masterId = GetLootMethod()
if lootMethod == "master" and masterId == id then
frame.masterIcon:Show()
else
frame.masterIcon:Hide()
end
BenillaPartyFrame_HealthCheck(frame)
BenillaPartyFrame_UpdatePvP(frame)
end
-- ref PartyMemberFrame_UpdatePvPStatus (PartyMemberFrame.lua l.116-130), verbatim: the
-- same three branches the unit frames run, but the faction leg draws the GroupFrame art
-- (UI-Group-PVP-<group>) — only the FFA skull is shared with the big frames, because no
-- UI-Group-PVP-FFA ships. Decision 0646 §4; closes 0434's named deferral (the icon waited
-- on UnitFactionGroup, which the app now feeds).
function BenillaPartyFrame_UpdatePvP(frame)
local unit = frame.unit
local factionGroup = UnitFactionGroup(unit)
if UnitIsPVPFreeForAll(unit) then
frame.pvpIcon:SetTexture("Interface\\TargetingFrame\\UI-PVP-FFA")
frame.pvpIcon:Show()
elseif factionGroup and UnitIsPVP(unit) then
frame.pvpIcon:SetTexture("Interface\\GroupFrame\\UI-Group-PVP-" .. factionGroup)
frame.pvpIcon:Show()
else
frame.pvpIcon:Hide()
end
end
-- The ref's <=20% portrait alpha pulse: a 0.5 s-per-leg triangle between 127/255 and 1.
function BenillaPartyFrame_OnUpdate(frame, elapsed)
local unit = frame.unit
if not UnitExists(unit) or not UnitIsConnected(unit) then
return
end
local hp, hpMax = UnitHealth(unit), UnitHealthMax(unit)
if hpMax > 0 and hp > 0 and hp / hpMax <= 0.2 then
local t = math.fmod(frame.flashTimer + elapsed, 1.0)
frame.flashTimer = t
local a
if t < 0.5 then
a = 1.0 - t * (1.0 - 127 / 255) * 2
else
a = 127 / 255 + (t - 0.5) * (1.0 - 127 / 255) * 2
end
frame.portrait:SetAlpha(a)
elseif frame.flashTimer ~= 0 then
frame.flashTimer = 0
frame.portrait:SetAlpha(1.0)
end
end
-- The hover tooltip: same law as the unit frames (default corner anchor + SetUnit).
function BenillaPartyFrame_OnEnter(frame)
if not UnitExists(frame.unit) then return end
GameTooltip_SetDefaultAnchor(GameTooltip, frame)
if GameTooltip:SetUnit(frame.unit) then
getglobal("GameTooltipTextLeft1"):SetTextColor(GameTooltip_UnitColor(frame.unit))
end
end
-- The invite driver (ref UIParent.lua): PARTY_INVITE_REQUEST shows the popup with the
-- inviter's name; PARTY_INVITE_CANCEL hides it.
function BenillaPartyInviteDriver_OnEvent()
if event == "PARTY_INVITE_REQUEST" then
StaticPopup_Show("PARTY_INVITE", arg1)
elseif event == "PARTY_INVITE_CANCEL" then
StaticPopup_Hide("PARTY_INVITE")
end
end
-- The right-click dropdown (decision 0434 phase 5; ref PartyMemberFrame.lua l.369-381)
-- + the ref's click law (l.226-237): left targets the member, right opens the PARTY
-- menu seated at the frame's (47, 15).
function PartyFrameDropDown_OnLoad()
UIDropDownMenu_Initialize(this, PartyFrameDropDown_Initialize, "MENU")
end
function PartyFrameDropDown_Initialize()
local dropdown
if UIDROPDOWNMENU_OPEN_MENU then
dropdown = getglobal(UIDROPDOWNMENU_OPEN_MENU)
else
dropdown = this
end
UnitPopup_ShowMenu(dropdown, "PARTY", "party" .. dropdown:GetParent():GetID())
end
function BenillaPartyFrame_OnClick(frame, button)
if button == "LeftButton" then
TargetUnit(frame.unit)
elseif button == "RightButton" then
ToggleDropDownMenu(1, nil, getglobal(frame:GetName() .. "DropDown"), frame:GetName(), 47, 15)
end
end
]]></Script>
<!-- The shared member-frame template — ref PartyFrameTemplates.xml, 128x53 over the 128x64
UI-PartyFrame art at (0,-2) (the art hangs 13 px below the frame rect, as authored).
LAYERING (the ref's law): the ref nests the frame art two anonymous child frames deep so it
draws OVER the status bars — the bars show through the art's semi-transparent tube windows,
inset, their ends clipped by the rails. Ours is the UnitFrames.xml idiom, which reaches the
same place by the ref's OTHER route (its TargetFrame's): a $parentTextureFrame child, with
OnLoad pushing the bars one frame level BELOW it. Both spend a frame LEVEL, which is the
point — level is the one key term above the draw layer (decision 0884). Until then this file
instead leaned on declaring the TextureFrame after the bars, and the old draw key's
insertion-order tie-break; 0884 pinned the layer as bucket-wide and above the frame, which
put the bars' ARTWORK fills straight over the BACKGROUND art. Do not go back to declaration
order. The name and the leader/master/disconnect icons ride in the TextureFrame too (the ref
keeps them in that nested frame; left below the art, the ring would swallow the leader
crown). -->
<!-- frameStrata LOW is the ref's own (`PartyFrameTemplates.xml` l.194) and it is load-bearing,
not decoration. The panel windows (loot, character, merchant, quest log, …) are MEDIUM, and
the party frames share the top-left corner with the left-slot panel. Left in the default
MEDIUM, a party frame's nested HealthBar/ManaBar/TextureFrame children land one frame LEVEL
above a panel's background region — and level outranks insertion order in the draw key, so
the party frame draws straight *through* the loot window no matter which was shown last.
Nothing about raising could have saved it: a raise only reorders within one stratum (wow-re
`system/ui/scratch/toplevel-raise.md`, dispatched for exactly this). Decision 0597. -->
<Frame name="BenillaPartyMemberTemplate" virtual="true" hidden="true" frameStrata="LOW">
<Size><AbsDimension x="128" y="53"/></Size>
<Layers>
<Layer level="BACKGROUND">
<!-- Portrait at the bottom of the stack; the art's round window shows it through. -->
<Texture name="$parentPortrait">
<Size><AbsDimension x="37" y="37"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="7" y="-6"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<StatusBar name="$parentHealthBar">
<Size><AbsDimension x="70" y="8"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="47" y="-12"/></Offset></Anchor></Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0" g="1.0" b="0"/>
</StatusBar>
<StatusBar name="$parentManaBar">
<Size><AbsDimension x="70" y="8"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="47" y="-21"/></Offset></Anchor></Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0" g="0" b="1.0"/>
</StatusBar>
<!-- The PARTY-menu dropdown host (ref $parentDropDown): hidden state-holder only —
the list anchors to the member frame by name. -->
<Frame name="$parentDropDown" 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>PartyFrameDropDown_OnLoad()</OnLoad>
</Scripts>
</Frame>
<Frame name="$parentTextureFrame">
<Size><AbsDimension x="128" y="53"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentTexture" file="Interface\TargetingFrame\UI-PartyFrame">
<Size><AbsDimension x="128" y="64"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-2"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentName" inherits="GameFontNormalSmall" justifyH="LEFT">
<Anchors><Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="50" y="43"/></Offset></Anchor></Anchors>
</FontString>
<Texture name="$parentLeaderIcon" file="Interface\GroupFrame\UI-Group-LeaderIcon" hidden="true">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
</Texture>
<Texture name="$parentMasterIcon" file="Interface\GroupFrame\UI-Group-MasterLooter" hidden="true">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="32" y="0"/></Offset></Anchor></Anchors>
</Texture>
<!-- The group PvP icon, 32x32 TOPLEFT (-9,-15) (ref PartyFrameTemplates.xml
l.298-309). Texture chosen at runtime — decision 0646 §4. -->
<Texture name="$parentPVPIcon" hidden="true">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="-9" y="-15"/></Offset></Anchor></Anchors>
</Texture>
<Texture name="$parentDisconnect" file="Interface\CharacterFrame\Disconnect-Icon" hidden="true">
<Size><AbsDimension x="64" y="64"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="-7" y="-1"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
</Layers>
</Frame>
</Frames>
<Scripts>
<OnLoad>BenillaPartyFrame_OnLoad(this)</OnLoad>
<OnEvent>BenillaPartyFrame_OnEvent(this)</OnEvent>
<OnUpdate>BenillaPartyFrame_OnUpdate(this, arg1)</OnUpdate>
<OnEnter>BenillaPartyFrame_OnEnter(this)</OnEnter>
<OnLeave>GameTooltip:FadeOut()</OnLeave>
<OnClick>BenillaPartyFrame_OnClick(this, arg1)</OnClick>
</Scripts>
</Frame>
<!-- The four instances — frame 1 at the ref's (10,-128); 2-4 cascade at the no-pet stride
(see the header note; the pet-frame anchor law returns with phase 4). -->
<Frame name="PartyMemberFrame1" inherits="BenillaPartyMemberTemplate" parent="UIParent" id="1">
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="10" y="-128"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="PartyMemberFrame2" inherits="BenillaPartyMemberTemplate" parent="UIParent" id="2">
<Anchors><Anchor point="TOPLEFT" relativeTo="PartyMemberFrame1" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="PartyMemberFrame3" inherits="BenillaPartyMemberTemplate" parent="UIParent" id="3">
<Anchors><Anchor point="TOPLEFT" relativeTo="PartyMemberFrame2" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</Frame>
<Frame name="PartyMemberFrame4" inherits="BenillaPartyMemberTemplate" parent="UIParent" id="4">
<Anchors><Anchor point="TOPLEFT" relativeTo="PartyMemberFrame3" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-10"/></Offset></Anchor></Anchors>
</Frame>
<!-- The invite event driver (hidden; the DeathFrame driver pattern). -->
<Frame name="BenillaPartyInviteDriver" parent="UIParent" hidden="true">
<Size><AbsDimension x="1" y="1"/></Size>
<Anchors><Anchor point="TOPLEFT"/></Anchors>
<Scripts>
<OnLoad>
this:RegisterEvent("PARTY_INVITE_REQUEST")
this:RegisterEvent("PARTY_INVITE_CANCEL")
</OnLoad>
<OnEvent>BenillaPartyInviteDriver_OnEvent()</OnEvent>
</Scripts>
</Frame>
</Ui>