Files
moon-well-ui/data/addon/ForeverUI/PlayerFrame.lua
T
Adrestie ca2b78e217 Remove every debug command
/fui keeps edit mode, reset and list. The thirty diagnostic and tuning
functions behind the other subcommands (bar, bags, character, tabs, close,
model, minimap, mouse, map, questlog, chat, chatlines, tabard...) go with
them, with Panes.Report, the bags read-back and the 205 texts they alone used.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 13:39:35 +02:00

575 lines
19 KiB
Lua

-- Player unit frame. Geometry from Blizzard_UnitFrame/mainline/PlayerFrame.xml, as camelot
-- PlayerFrame.lua overrides it since 1.60.1.70450 (PlayerFrame_Update*Visuals).
-- The art sits above the square portrait and its ring covers the corners: 3.3.5 has no
-- MaskTexture to make the portrait round.
-- Position is decided and saved by ForeverUI.Layout (see Layout.lua).
local FRAME_WIDTH, FRAME_HEIGHT = 232, 100
local L = ForeverUI.L
local ART_NORMAL = "ui-hud-unitframe-player-portraiton"
-- Threat glow, not an alternate frame art: both clients pass this image to UnitFrame_Initialize
-- as threatIndicator (modern FrameFlash, 3.3.5 PlayerFrameFlash).
local THREAT_GLOW = "ui-hud-unitframe-player-portraiton-incombat"
local HEALTH_FILL = "ui-hud-unitframe-player-portraiton-bar-health"
-- The atlas has one fill per power type: it gives the color, nothing is tinted by hand.
local POWER_FILL = {
MANA = "ui-hud-unitframe-player-portraiton-bar-mana",
RAGE = "ui-hud-unitframe-player-portraiton-bar-rage",
FOCUS = "ui-hud-unitframe-player-portraiton-bar-focus",
ENERGY = "ui-hud-unitframe-player-portraiton-bar-energy",
RUNIC_POWER = "ui-hud-unitframe-player-portraiton-bar-runicpower",
}
local frame = CreateFrame("Button", "ForeverUIPlayerFrame", UIParent, "SecureUnitButtonTemplate")
frame:SetWidth(FRAME_WIDTH)
frame:SetHeight(FRAME_HEIGHT)
frame:SetFrameStrata("LOW")
-- The clickable area follows the art, not the frame: (232-198)/2 on each side.
frame:SetHitRectInsets(17, 17, 14, 15)
frame.unit = "player"
frame:SetAttribute("unit", "player")
frame:SetAttribute("*type1", "target")
-- 3.3.5 uses "menu", not "togglemenu" as recent clients do:
-- SecureActionButton_OnClick then calls self.menu.
frame:SetAttribute("*type2", "menu")
frame:RegisterForClicks("AnyUp")
frame.menu = function(self)
ForeverUI.UnitMenu.open(ToggleDropDownMenu, 1, nil, PlayerFrameDropDown, self, 106, 27)
end
local portrait = frame:CreateTexture(nil, "BACKGROUND")
portrait:SetWidth(60)
portrait:SetHeight(60)
portrait:SetPoint("TOPLEFT", 24, -19)
-- camelot: health container 124 x 15 at (87, -46), the 124 x 20 bar 3 px above it; mana bar at
-- (87, -61). The mana bar is created after the health bar, so it covers the 2 px the health art
-- reaches below -61 (camelot's mask cuts them; 3.3.5 has none).
-- OVERLAY, above the art: camelot's bars are StatusBars, child frames drawn over the frame
-- texture, whose bar slots are a dark veil (74 % opaque since 70450, 40 % before).
local healthFill = frame:CreateTexture(nil, "OVERLAY")
healthFill:SetPoint("TOPLEFT", 87, -43)
local powerFill = frame:CreateTexture(nil, "OVERLAY")
powerFill:SetPoint("TOPLEFT", 87, -61)
local art = frame:CreateTexture(nil, "ARTWORK")
art:SetPoint("CENTER", 0, 0)
ForeverUI.SetAtlas(art, ART_NORMAL)
-- camelot PlayerFrame_UpdatePlayerNameTextAnchor: centered, 116 wide, center at (149, -37)
local nameText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
nameText:SetWidth(116)
nameText:SetHeight(12)
nameText:SetJustifyH("CENTER")
nameText:SetPoint("CENTER", frame, "TOPLEFT", 149, -37)
-- The level lives in a child frame one level up, not in a layer of the main frame. Textures
-- in one layer are ordered only by creation, and the art is re-set during play, which put it
-- in front of the circle. A child frame does not depend on that order.
local overlayHolder = CreateFrame("Frame", nil, frame)
overlayHolder:SetAllPoints(frame)
overlayHolder:SetFrameLevel(frame:GetFrameLevel() + 1)
local levelCircle = overlayHolder:CreateTexture(nil, "ARTWORK")
levelCircle:SetPoint("BOTTOMLEFT", 13, 7)
ForeverUI.SetAtlas(levelCircle, "ui-hud-unitframe-smallcircle")
local levelText = overlayHolder:CreateFontString(nil, "OVERLAY", "GameFontNormal")
levelText:SetPoint("CENTER", levelCircle, "CENTER", 0, 0)
-- ------------------------------------------------------------- Player states
-- Everything below lives in overlayHolder, above the art, sorted by layer: status veil at the
-- back, icons above, texts last. These textures are never re-set in play (only color and
-- alpha change), so their order cannot drift.
local STATUS_ATLAS = "ui-hud-unitframe-player-portraiton-status"
local COMBAT_ICON = "ui-hud-unitframe-player-combaticon"
local CORNER_ATLAS = "ui-hud-unitframe-player-portraiton-cornerembellishment"
local LEADER_ICON = "ui-hud-unitframe-player-group-leadericon"
-- camelot has no assistant nor master looter icon: the 3.3.5 ones (16 x 16)
local GROUP_ICONS = "Interface" .. string.char(92) .. "GroupFrame" .. string.char(92)
local ASSISTANT_ICON = GROUP_ICONS .. "UI-Group-AssistantIcon"
local MASTER_ICON = GROUP_ICONS .. "UI-Group-MasterLooter"
local REST_ATLAS = "ui-hud-unitframe-player-rest-flipbook"
-- Rest sprite sheet: 7 rows of 6 cells, 42 frames played in 1.5 s. 3.3.5 has no FlipBook
-- animation, so OnUpdate moves the tex coords by hand.
local REST_COLUMNS, REST_ROWS, REST_FRAMES, REST_DURATION = 6, 7, 42, 1.5
-- ADD blend, as the 3.3.5 PlayerStatusTexture: the light adds up. A normal blend with a red
-- tint at half alpha washes the whole frame pink.
local statusTexture = overlayHolder:CreateTexture(nil, "BACKGROUND")
statusTexture:SetPoint("TOPLEFT", 17, -14)
ForeverUI.SetAtlas(statusTexture, STATUS_ATLAS)
statusTexture:SetBlendMode("ADD")
statusTexture:Hide()
local threatGlow = overlayHolder:CreateTexture(nil, "BORDER")
threatGlow:SetPoint("CENTER", frame, "CENTER", 0, 0)
ForeverUI.SetAtlas(threatGlow, THREAT_GLOW)
threatGlow:Hide()
local cornerIcon = overlayHolder:CreateTexture(nil, "ARTWORK")
cornerIcon:SetPoint("TOPLEFT", 58, -53)
ForeverUI.SetAtlas(cornerIcon, CORNER_ATLAS)
local combatIcon = overlayHolder:CreateTexture(nil, "OVERLAY")
combatIcon:SetPoint("TOPLEFT", 64, -62)
ForeverUI.SetAtlas(combatIcon, COMBAT_ICON)
combatIcon:Hide()
local leaderIcon = overlayHolder:CreateTexture(nil, "OVERLAY")
leaderIcon:SetPoint("TOPLEFT", 86, -10)
ForeverUI.SetAtlas(leaderIcon, LEADER_ICON)
leaderIcon:Hide()
-- Master looter, 40 right and 2 up from the leader icon, as PlayerFrame_ToPlayerArt places
-- PlayerMasterIcon next to PlayerLeaderIcon
local masterIcon = overlayHolder:CreateTexture(nil, "OVERLAY")
masterIcon:SetTexture(MASTER_ICON)
masterIcon:SetWidth(16)
masterIcon:SetHeight(16)
masterIcon:SetPoint("TOPLEFT", leaderIcon, "TOPLEFT", 40, 2)
masterIcon:Hide()
local restTexture = overlayHolder:CreateTexture(nil, "OVERLAY")
restTexture:SetPoint("TOPLEFT", 59, -1)
restTexture:SetWidth(30)
restTexture:SetHeight(30)
restTexture:Hide()
local healthText = overlayHolder:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
healthText:SetPoint("CENTER", frame, "TOPLEFT", 87 + 62, -43 - 10)
healthText:Hide()
local powerText = overlayHolder:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
powerText:SetPoint("CENTER", frame, "TOPLEFT", 87 + 62, -61 - 5)
powerText:Hide()
frame.overlayHolder = overlayHolder
frame.portrait = portrait
frame.healthFill = healthFill
frame.powerFill = powerFill
frame.art = art
frame.threatGlow = threatGlow
frame.nameText = nameText
frame.levelText = levelText
frame.statusTexture = statusTexture
frame.combatIcon = combatIcon
frame.cornerIcon = cornerIcon
frame.leaderIcon = leaderIcon
frame.masterIcon = masterIcon
frame.restTexture = restTexture
frame.healthText = healthText
frame.powerText = powerText
local VEHICLE_ART = "ui-hud-unitframe-player-portraiton-vehicle"
local VEHICLE_HEALTH_WIDTH = 118
-- Unit shown by the frame: the vehicle while in one, as PlayerFrame_ToVehicleArt in 3.3.5.
-- The modern client also swaps the art for the vehicle variant, a few pixels wider.
local function displayedUnit()
return frame.displayUnit or "player"
end
local function updateVehicleArt()
local inVehicle = (UnitHasVehicleUI and UnitHasVehicleUI("player") and UnitExists("vehicle")) and true or false
frame.inVehicle = inVehicle
frame.displayUnit = inVehicle and "vehicle" or "player"
art:ClearAllPoints()
if inVehicle then
ForeverUI.SetAtlas(art, VEHICLE_ART)
art:SetPoint("CENTER", -2, 0)
else
ForeverUI.SetAtlas(art, ART_NORMAL)
art:SetPoint("CENTER", 0, 0)
end
end
local function updateHealth()
local unit = displayedUnit()
local maximum = UnitHealthMax(unit)
local fraction = 0
if maximum and maximum > 0 then
fraction = UnitHealth(unit) / maximum
end
ForeverUI.SetAtlasFill(healthFill, HEALTH_FILL, fraction,
frame.inVehicle and VEHICLE_HEALTH_WIDTH or nil)
end
local function updatePower()
local unit = displayedUnit()
local powerType, powerToken = UnitPowerType(unit)
local atlas = POWER_FILL[powerToken or ""] or POWER_FILL.MANA
local maximum = UnitPowerMax(unit, powerType)
local fraction = 0
if maximum and maximum > 0 then
fraction = UnitPower(unit, powerType) / maximum
end
ForeverUI.SetAtlasFill(powerFill, atlas, fraction,
frame.inVehicle and VEHICLE_HEALTH_WIDTH or nil)
end
local function updateName()
nameText:SetText(UnitName(displayedUnit()))
end
local function updateLevel()
levelText:SetText(UnitLevel("player"))
end
local function updatePortrait()
SetPortraitTexture(portrait, displayedUnit())
end
-- The glow follows camelot's observed behavior, not UnitFrame_UpdateThreatIndicator (lit above
-- zero, tinted by GetThreatStatusColor): it is always red and only its alpha changes, faint
-- once engaged, full when an enemy targets you.
local THREAT_ALPHA_ENGAGED, THREAT_ALPHA_TARGETED = 0.45, 1.0
-- Being targeted cannot rely on the threat table alone: on a private server
-- UnitThreatSituation("player") often returns nil and UNIT_THREAT_SITUATION_UPDATE never
-- fires. So also check whether the hostile target targets us.
local function playerIsTargeted()
local status = UnitThreatSituation and UnitThreatSituation("player")
if status and status >= 2 then
return true
end
if UnitExists("target") and UnitCanAttack("player", "target")
and UnitIsUnit("targettarget", "player") then
return true
end
return false
end
local updateThreat
updateThreat = function()
if UnitIsDeadOrGhost and UnitIsDeadOrGhost("player") then
threatGlow:Hide()
return
end
local status = UnitThreatSituation and UnitThreatSituation("player")
local targeted = playerIsTargeted()
local engaged = frame.inCombat or frame.onHateList or (status and status > 0)
if targeted or engaged then
threatGlow:SetVertexColor(1.0, 0.0, 0.0)
threatGlow:SetAlpha(targeted and THREAT_ALPHA_TARGETED or THREAT_ALPHA_ENGAGED)
threatGlow:Show()
else
threatGlow:Hide()
end
end
-- Player state, in the order of the modern PlayerFrame_UpdateStatus (camelot 70450): nothing in
-- a vehicle, otherwise combat, then the hate list, then rest. 3.3.5 put rest first: in an inn,
-- the veil was gold even in combat.
local restElapsed = 0
local function updateStatus()
if UnitHasVehicleUI and UnitHasVehicleUI("player") then
statusTexture:Hide()
combatIcon:Hide()
restTexture:Hide()
frame.resting = false
return
end
-- The client separates two states that UnitAffectingCombat merges:
-- inCombat -- PLAYER_ENTER_COMBAT: you attack (right-click on an enemy).
-- Red veil and combat icon.
-- onHateList -- PLAYER_REGEN_DISABLED: you are on someone's hate list. Icon only.
-- The onHateList branch leaves the veil alone, as both clients do: it keeps its color until
-- combat really ends.
if frame.inCombat then
statusTexture:SetVertexColor(1.0, 0.0, 0.0)
statusTexture:Show()
combatIcon:Show()
cornerIcon:Hide()
restTexture:Hide()
frame.resting = false
elseif frame.onHateList then
combatIcon:Show()
cornerIcon:Hide()
restTexture:Hide()
frame.resting = false
elseif IsResting() then
statusTexture:SetVertexColor(1.0, 0.88, 0.25)
statusTexture:Show()
restTexture:Show()
combatIcon:Hide()
cornerIcon:Show()
frame.resting = true
restElapsed = 0
else
statusTexture:Hide()
combatIcon:Hide()
cornerIcon:Show()
restTexture:Hide()
frame.resting = false
end
end
-- Leader, or raid assistant in its place (the raid list's rank icon); master looter as
-- PlayerFrame_UpdatePartyLeader shows it
local function updateLeader()
if IsPartyLeader and IsPartyLeader() then
ForeverUI.SetAtlas(leaderIcon, LEADER_ICON)
leaderIcon:Show()
elseif GetNumRaidMembers() > 0 and IsRaidOfficer() then
leaderIcon:SetTexture(ASSISTANT_ICON)
leaderIcon:SetTexCoord(0, 1, 0, 1)
leaderIcon:SetWidth(16)
leaderIcon:SetHeight(16)
leaderIcon:Show()
else
leaderIcon:Hide()
end
local _, master = GetLootMethod()
if master == 0 and (GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0) then
masterIcon:Show()
else
masterIcon:Hide()
end
end
-- Bar texts follow the game settings: playerStatusText "1" means always shown, and
-- statusTextPercentage picks a percentage or raw values. On hover they always show.
local function formatValue(value, maximum)
if not maximum or maximum <= 0 then
return ""
end
if GetCVarBool and GetCVarBool("statusTextPercentage") then
return tostring(math.ceil(value / maximum * 100)) .. "%"
end
return value .. " / " .. maximum
end
local function updateTexts()
local always = GetCVar and GetCVar("playerStatusText") == "1"
if not (always or frame.hovered) then
healthText:Hide()
powerText:Hide()
return
end
-- the values of the bars: the vehicle's while in one
local unit = displayedUnit()
local powerType = UnitPowerType(unit)
healthText:SetText(formatValue(UnitHealth(unit), UnitHealthMax(unit)))
powerText:SetText(formatValue(UnitPower(unit, powerType), UnitPowerMax(unit, powerType)))
healthText:Show()
powerText:Show()
end
ForeverUI.PlayerFrameUpdateThreat = function() updateThreat() end
-- Some classes change the frame art: camelot switches to the "ClassResource" variant when a
-- class resource shows under the bars. normalAtlas: atlas used outside a vehicle.
ForeverUI.PlayerFrameSetArt = function(normalAtlas)
if normalAtlas then
ART_NORMAL = normalAtlas
updateVehicleArt()
end
end
frame:SetScript("OnEnter", function(self)
self.hovered = true
updateTexts()
end)
frame:SetScript("OnLeave", function(self)
self.hovered = false
updateTexts()
end)
-- One OnUpdate for two animations: the rest sprite sheet, and the status veil that pulses
-- like the original frame's.
ForeverUI.SetAtlas(restTexture, REST_ATLAS, true)
frame:SetScript("OnUpdate", function(self, elapsed)
if self.resting then
restElapsed = restElapsed + elapsed
local entry = ForeverUI.AtlasEntry(REST_ATLAS)
if entry then
local index = math.floor(restElapsed / (REST_DURATION / REST_FRAMES)) % REST_FRAMES
local column = index - math.floor(index / REST_COLUMNS) * REST_COLUMNS
local row = math.floor(index / REST_COLUMNS)
local du = (entry[3] - entry[2]) / REST_COLUMNS
local dv = (entry[5] - entry[4]) / REST_ROWS
restTexture:SetTexCoord(entry[2] + column * du, entry[2] + (column + 1) * du,
entry[4] + row * dv, entry[4] + (row + 1) * dv)
end
end
if statusTexture:IsShown() then
local pulse = 0.35 + 0.25 * math.sin(GetTime() * 3)
statusTexture:SetAlpha(pulse)
end
-- "Targeted" is read from the target, not from an event: re-check it three times per second
-- while engaged.
self.threatElapsed = (self.threatElapsed or 0) + elapsed
if self.threatElapsed > 0.3 then
self.threatElapsed = 0
if frame.inCombat or frame.onHateList or threatGlow:IsShown() then
ForeverUI.PlayerFrameUpdateThreat()
end
end
end)
-- The default frame is disabled, not just hidden: its own events (mounts, vehicles, group)
-- would show it again.
local function hideDefaultPlayerFrame()
if InCombatLockdown() or not PlayerFrame then
return
end
ForeverUI.Suppress(PlayerFrame)
end
local function updateAll()
updateVehicleArt()
updateHealth()
updatePower()
updateName()
updateLevel()
updatePortrait()
updateThreat()
updateStatus()
updateLeader()
updateTexts()
end
frame:SetScript("OnEvent", function(self, event, unit)
if event == "PLAYER_ENTERING_WORLD" then
self.onHateList = UnitAffectingCombat("player")
hideDefaultPlayerFrame()
updateAll()
return
end
if event == "PLAYER_ENTER_COMBAT" then
self.inCombat = true
updateStatus()
updateThreat()
return
end
if event == "PLAYER_LEAVE_COMBAT" then
self.inCombat = false
updateStatus()
updateThreat()
return
end
if event == "PLAYER_REGEN_DISABLED" or event == "PLAYER_REGEN_ENABLED" then
self.onHateList = (event == "PLAYER_REGEN_DISABLED")
-- Skipped when PLAYER_ENTERING_WORLD came in combat (reload, reconnect): done now
if event == "PLAYER_REGEN_ENABLED" then
hideDefaultPlayerFrame()
end
updateStatus()
updateThreat()
return
end
if event == "UNIT_THREAT_SITUATION_UPDATE" or event == "PLAYER_TARGET_CHANGED"
or event == "UNIT_TARGET" then
updateThreat()
return
end
if event == "PLAYER_UPDATE_RESTING" then
updateStatus()
return
end
if event == "PARTY_LEADER_CHANGED" or event == "PARTY_MEMBERS_CHANGED"
or event == "PARTY_LOOT_METHOD_CHANGED" or event == "RAID_ROSTER_UPDATE" then
updateLeader()
return
end
if event == "CVAR_UPDATE" then
updateTexts()
return
end
if event == "PLAYER_LEVEL_UP" then
updateLevel()
return
end
-- In a vehicle the bars show "vehicle": its health and power events carry that token
if unit ~= "player" and unit ~= displayedUnit() then
return
end
if event == "UNIT_ENTERED_VEHICLE" or event == "UNIT_EXITED_VEHICLE" then
updateVehicleArt()
updateHealth()
updatePower()
updateName()
updatePortrait()
updateStatus()
elseif event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH" then
updateHealth()
updateTexts()
elseif event == "UNIT_NAME_UPDATE" then
updateName()
elseif event == "UNIT_PORTRAIT_UPDATE" then
updatePortrait()
elseif event == "UNIT_LEVEL" then
updateLevel()
else
updatePower()
updateTexts()
end
end)
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("PLAYER_REGEN_DISABLED")
frame:RegisterEvent("PLAYER_ENTER_COMBAT")
frame:RegisterEvent("PLAYER_LEAVE_COMBAT")
frame:RegisterEvent("PLAYER_REGEN_ENABLED")
frame:RegisterEvent("PLAYER_LEVEL_UP")
frame:RegisterEvent("UNIT_HEALTH")
frame:RegisterEvent("UNIT_MAXHEALTH")
frame:RegisterEvent("UNIT_NAME_UPDATE")
frame:RegisterEvent("UNIT_PORTRAIT_UPDATE")
frame:RegisterEvent("UNIT_LEVEL")
frame:RegisterEvent("UNIT_DISPLAYPOWER")
-- 3.3.5 has no UNIT_POWER: each power type has its own event.
frame:RegisterEvent("UNIT_MANA")
frame:RegisterEvent("UNIT_RAGE")
frame:RegisterEvent("UNIT_FOCUS")
frame:RegisterEvent("UNIT_ENERGY")
frame:RegisterEvent("UNIT_RUNIC_POWER")
frame:RegisterEvent("UNIT_MAXMANA")
frame:RegisterEvent("UNIT_MAXRAGE")
frame:RegisterEvent("UNIT_MAXFOCUS")
frame:RegisterEvent("UNIT_MAXENERGY")
frame:RegisterEvent("UNIT_MAXRUNIC_POWER")
frame:RegisterEvent("PLAYER_UPDATE_RESTING")
frame:RegisterEvent("PARTY_LEADER_CHANGED")
frame:RegisterEvent("PARTY_MEMBERS_CHANGED")
frame:RegisterEvent("PARTY_LOOT_METHOD_CHANGED")
frame:RegisterEvent("RAID_ROSTER_UPDATE")
frame:RegisterEvent("UNIT_ENTERED_VEHICLE")
frame:RegisterEvent("UNIT_EXITED_VEHICLE")
frame:RegisterEvent("CVAR_UPDATE")
frame:RegisterEvent("UNIT_THREAT_SITUATION_UPDATE")
frame:RegisterEvent("PLAYER_TARGET_CHANGED")
frame:RegisterEvent("UNIT_TARGET")
ForeverUI.PlayerFrame = frame
ForeverUI.Layout.Register(frame, "playerframe", L.PLAYERFRAME_EDIT_LABEL, "TOPLEFT", "TOPLEFT", 10, -10)