Files
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

519 lines
17 KiB
Lua

-- ForeverUI: the pet tab, built on 3.3.5's PetPaperDollFrame (camelot shows the pet as a
-- right-pane sidebar instead). Left pane: the client's pet model. Right pane: the level, then
-- General (health, armor, damage, attack power, crit) and Resistances, laid out like the
-- character stats. Values are read from the client's own frames, which keep their formatting.
-- 3.3.5 has no pet crit API: the crit shown is only GetCritChanceFromAgility("pet").
local ForeverUI = ForeverUI or {}
_G.ForeverUI = ForeverUI
local L = ForeverUI.L
-- Same measures as the character stats (CharacterFrame.lua), so both pages share one edge.
local MARGIN = 20 -- STAT_MARGIN
local STEP = 13 -- STAT_STEP, one row's height
local HEADER_H = 34 -- STAT_HEADER
local HEADER_OVERHANG = 5 -- STAT_HEADER_OVERHANG
local GROUP_GAP = 16 -- STAT_GROUP_GAP
local TOP = 14 -- below the pane's top
local LEVEL_H = 20
local LEVEL_GAP = -8
-- Rotation arrows, placed like the character's (placeModel in CharacterFrame.lua): centered
-- on the pane's top, side by side, above the model so they get the click. Both screens use
-- 35 x 35 buttons (client XML), so only the position changes.
local ROTATION_Y = -12
local ROTATION_GAP = 4
local ATLAS_HEADER = "ui-character-info-title"
local ATLAS_LEVEL = "ui-character-info-itemlevel-bounce"
local ATLAS_DARK_BACKGROUND = "ui-character-info-itemlevel-bounce"
local ATLAS_LIGHT_BACKGROUND = "ui-character-info-line-bounce"
-- Default pane sizes: a content may be built on first open, before the window is laid out.
local PANE_W = 233
local LEFT_PANE_W, LEFT_PANE_H = 398, 464
local panel, detail
-- ---------------------------------------------------------------- Data
local function hasPet()
return (HasPetUI and HasPetUI()) and UnitExists and UnitExists("pet")
end
-- Reads a stat's text from the client's frame after letting the client compute it,
-- so the client keeps its formatting. place: the client's PaperDollFrame_Set* function.
local function readFromClient(frameName, place)
local frame = _G[frameName]
if not frame then
return nil
end
if place then
place(frame, "Pet")
end
local text = _G[frameName .. "StatText"]
return text and text:GetText()
end
-- Rows of the General category: { name, value }.
local function readGeneral()
local rows = {}
rows[#rows + 1] = {
name = HEALTH,
value = tostring((UnitHealthMax and UnitHealthMax("pet")) or 0),
}
rows[#rows + 1] = {
name = ARMOR,
value = readFromClient("PetArmorFrame", PaperDollFrame_SetArmor) or "",
}
rows[#rows + 1] = {
name = DAMAGE,
value = readFromClient("PetDamageFrame", PaperDollFrame_SetDamage) or "",
}
-- ATTACK_POWER is "Power" in this client; ATTACK_POWER_TOOLTIP is "Attack Power".
rows[#rows + 1] = {
name = ATTACK_POWER_TOOLTIP or ATTACK_POWER,
value = readFromClient("PetAttackPowerFrame",
PaperDollFrame_SetAttackPower) or "",
}
-- Only the agility part of crit: 3.3.5 has no pet crit API.
local critChance = (GetCritChanceFromAgility and GetCritChanceFromAgility("pet")) or 0
rows[#rows + 1] = {
name = MELEE_CRIT_CHANCE,
value = string.format("%.2f%%", critChance),
}
return rows
end
-- Resistance rows { name, value }, for the debug report.
local function readResistances()
local rows = {}
local numResistances = NUM_PET_RESISTANCE_TYPES or 5
for rank = 1, numResistances do
local frame = _G["PetMagicResFrame" .. rank]
local school = frame and frame.GetID and frame:GetID()
if school then
local _, value = UnitResistance("pet", school)
rows[#rows + 1] = {
name = _G["RESISTANCE" .. school .. "_NAME"] or tostring(school),
value = tostring(value or 0),
}
end
end
return rows
end
-- --------------------------------------------------------------- Right pane
local groups = {}
local rowLines = {}
-- One stat row like StatFrameTemplate: label left, value right, alternating background.
local function createRow(rank, width)
local row = CreateFrame("Frame", "ForeverUIPetStat" .. rank, detail)
row:SetWidth(width)
row:SetHeight(STEP)
local background = row:CreateTexture(nil, "BACKGROUND")
background:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
background:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
row.background = background
local label = row:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
label:SetJustifyH("LEFT")
label:SetPoint("LEFT", row, "LEFT", 0, 0)
row.label = label
local value = row:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
value:SetJustifyH("RIGHT")
value:SetPoint("RIGHT", row, "RIGHT", 0, 0)
value:SetPoint("LEFT", label, "RIGHT", 4, 0)
row.value = value
rowLines[rank] = row
return row
end
local function createHeader(rank, width)
local header = CreateFrame("Frame", "ForeverUIPetCategory" .. rank, detail)
header:SetWidth(width + 2 * HEADER_OVERHANG)
header:SetHeight(HEADER_H)
-- camelot's header frame, stretched, as on the character stat category selectors.
local background = header:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(background, ATLAS_HEADER, true)
background:SetAllPoints(header)
local label = header:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
label:SetPoint("CENTER", header, "CENTER", 0, 1)
header.label = label
groups[rank] = header
return header
end
-- Resistances are the client's own icons (PetMagicResFrame1-5), placed in a row, so the
-- client keeps their value, color and tooltip. They are reparented to the right pane
-- because the sweep hides their parent screen. placeResistances returns the height used.
local RES_W, RES_H = 32, 29 -- MagicResistanceFrameTemplate
local function placeResistances(width, y)
if PetPaperDollFrame_SetResistances then
PetPaperDollFrame_SetResistances()
end
local numResistances = NUM_PET_RESISTANCE_TYPES or 5
-- Spread evenly over the row width.
local gap = 0
if numResistances > 1 then
gap = (width - numResistances * RES_W) / (numResistances - 1)
if gap < 0 then
gap = 0
end
end
for rank = 1, numResistances do
local frame = _G["PetMagicResFrame" .. rank]
if frame then
frame:SetParent(detail)
frame:SetWidth(RES_W)
frame:SetHeight(RES_H)
frame:ClearAllPoints()
frame:SetPoint("TOPLEFT", detail, "TOPLEFT",
MARGIN + (rank - 1) * (RES_W + gap), -y)
frame:Show()
for _, region in ipairs({ frame:GetRegions() }) do
if region.Show then
region:Show()
end
end
end
end
return RES_H
end
-- Stacks each category header and its rows.
local function layout()
if not detail then
return
end
local width = detail:GetWidth() or 0
if width < 50 then
width = PANE_W
end
width = width - 2 * MARGIN
local blocks = {
{ name = GENERAL, rows = readGeneral() },
{ name = L.PETTAB_RESISTANCES, icons = true },
}
local y = TOP
-- "Level X <name>" at the top of the pane.
local level = (UnitLevel and UnitLevel("pet")) or 0
local name = (UnitName and UnitName("pet")) or ""
detail.level:SetWidth(width)
detail.level:ClearAllPoints()
detail.level:SetPoint("TOP", detail, "TOP", 0, -y)
detail.level:SetText(string.format("%s %s",
string.format(UNIT_LEVEL_TEMPLATE, level), name))
y = y + LEVEL_H - LEVEL_GAP
local headerIndex, rowIndex = 0, 0
for _, block in ipairs(blocks) do
headerIndex = headerIndex + 1
local header = groups[headerIndex] or createHeader(headerIndex, width)
header:SetWidth(width + 2 * HEADER_OVERHANG)
header:ClearAllPoints()
header:SetPoint("TOPLEFT", detail, "TOPLEFT", MARGIN - HEADER_OVERHANG, -y)
header.label:SetText(block.name)
header:Show()
y = y + HEADER_H
if block.icons then
y = y + placeResistances(width, y)
else
for number, data in ipairs(block.rows) do
rowIndex = rowIndex + 1
local row = rowLines[rowIndex] or createRow(rowIndex, width)
row:SetWidth(width)
row:ClearAllPoints()
row:SetPoint("TOPLEFT", detail, "TOPLEFT", MARGIN, -y)
row.label:SetText(string.format(STAT_FORMAT, data.name))
row.value:SetText(data.value)
-- Dark first, restarting at each category, as in the character stats.
ForeverUI.SetAtlas(row.background,
(number % 2 == 1) and ATLAS_DARK_BACKGROUND or ATLAS_LIGHT_BACKGROUND,
true)
row:Show()
y = y + STEP
end
end
y = y + GROUP_GAP
end
for rank = headerIndex + 1, #groups do
groups[rank]:Hide()
end
for rank = rowIndex + 1, #rowLines do
rowLines[rank]:Hide()
end
end
-- Fills the right pane, or empties it when there is no pet.
local function updateDetail()
if not detail then
return
end
if not hasPet() then
detail.level:SetText("")
for _, header in ipairs(groups) do
header:Hide()
end
for _, row in ipairs(rowLines) do
row:Hide()
end
for rank = 1, (NUM_PET_RESISTANCE_TYPES or 5) do
local frame = _G["PetMagicResFrame" .. rank]
if frame then
frame:Hide()
end
end
return
end
layout()
end
ForeverUI.PetDetail = updateDetail
-- ------------------------------------------------------------- Left pane
-- Hides a client frame's art, text and children, except the spared ones.
-- PetModelFrame is a grandchild of PetPaperDollFrame (inside PetPaperDollFramePetFrame),
-- so both levels are swept, each sparing what holds the model.
-- spared: set of child frames to leave shown.
local function sweep(frame, spared)
if not frame then
return
end
if frame.SetBackdrop then
frame:SetBackdrop(nil)
end
for _, region in ipairs({ frame:GetRegions() }) do
local objectType = region.GetObjectType and region:GetObjectType()
if objectType == "Texture" then
region:SetAlpha(0)
elseif objectType == "FontString" and region.Hide then
region:Hide()
end
end
if frame.GetChildren then
-- PetPaperDollFrameCompanionFrame holds the secure CompanionButtons, so the client
-- protects it: in combat an insecure Hide on it is blocked (ADDON_ACTION_BLOCKED)
local locked = InCombatLockdown()
for _, childFrame in ipairs({ frame:GetChildren() }) do
if not spared[childFrame] and childFrame.Hide
and not (locked and childFrame:IsProtected()) then
childFrame:Hide()
end
end
end
end
-- Hides the client pet screen, except the model and its carrier.
local function suppressClientScreen()
local screen = _G["PetPaperDollFrame"]
local carrier = _G["PetPaperDollFramePetFrame"]
local model = _G["PetModelFrame"]
if not screen then
return
end
sweep(screen, { [panel or false] = true, [carrier or false] = true,
[model or false] = true })
sweep(carrier, { [model or false] = true })
-- PetPaperDollFrame_SetTab hides it when another client tab is chosen; it holds the model.
if carrier then
carrier:Show()
end
end
-- Puts the pet model and its rotation arrows in the left pane.
local function updatePreview()
local model = _G["PetModelFrame"]
if not model or not panel then
return
end
suppressClientScreen()
model:ClearAllPoints()
model:SetPoint("TOPLEFT", panel, "TOPLEFT", 0, 0)
model:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", 0, 0)
-- The arrows are children of the model (PetPaperDollFrame.xml), so the sweep misses them;
-- they only need to move where the character's are.
local left = _G["PetModelFrameRotateLeftButton"]
local right = _G["PetModelFrameRotateRightButton"]
if left and right then
local half = (left:GetWidth() or 0) / 2 + ROTATION_GAP / 2
local level = (model:GetFrameLevel() or 0) + 2
left:ClearAllPoints()
left:SetPoint("TOP", panel, "TOP", -half, ROTATION_Y)
left:SetFrameLevel(level)
right:ClearAllPoints()
right:SetPoint("TOP", panel, "TOP", half, ROTATION_Y)
right:SetFrameLevel(level)
end
if hasPet() then
if model.SetUnit then
model:SetUnit("pet")
end
model:Show()
if left then left:Show() end
if right then right:Show() end
else
model:Hide()
if left then left:Hide() end
if right then right:Hide() end
end
end
ForeverUI.PetPreview = updatePreview
-- ---------------------------------------------------------- Build
-- Left pane content; host: the left pane frame.
-- Returns no root and the client frames it owns (see Panes.Register).
local function build(host)
local frame = _G["PetPaperDollFrame"]
if not frame or not host then
return nil, {}
end
-- Protected through its secure CompanionButtons: moved out of combat only (its XML
-- setAllPoints on CharacterFrame stays meanwhile; the model hangs on our panel)
if not InCombatLockdown() then
frame:ClearAllPoints()
frame:SetPoint("TOPLEFT", host, "TOPLEFT", 0, 0)
frame:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", 0, 0)
end
if panel then
updatePreview()
updateDetail()
return nil, { frame }
end
panel = CreateFrame("Frame", "ForeverUIPetPane", frame)
panel:SetPoint("TOPLEFT", host, "TOPLEFT", 0, 0)
panel:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", 0, 0)
local width = host:GetWidth() or 0
if width < 100 then
width = LEFT_PANE_W
end
panel:SetWidth(width)
local height = host:GetHeight() or 0
if height < 100 then
height = LEFT_PANE_H
end
panel:SetHeight(height)
updatePreview()
updateDetail()
return nil, { frame }
end
-- Right pane content; host: the right pane frame. Returns its root (see Panes.Register).
local function buildDetail(host)
if detail then
updateDetail()
return detail, {}
end
if not host then
return nil, {}
end
detail = CreateFrame("Frame", "ForeverUIPetStats", host)
detail:SetPoint("TOPLEFT", host, "TOPLEFT", 0, 0)
detail:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", 0, 0)
local width = host:GetWidth() or 0
if width < 50 then
width = PANE_W
end
detail:SetWidth(width)
-- "Level X <name>", on the same background as the character's level line.
local background = detail:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(background, ATLAS_LEVEL, true)
local level = detail:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
level:SetHeight(LEVEL_H)
level:SetJustifyH("CENTER")
level:SetJustifyV("MIDDLE")
detail.level = level
background:SetPoint("TOP", level, "TOP", 0, 0)
background:SetPoint("BOTTOM", level, "BOTTOM", 0, 0)
background:SetPoint("LEFT", level, "LEFT", 0, 0)
background:SetPoint("RIGHT", level, "RIGHT", 0, 0)
updateDetail()
return detail, {}
end
ForeverUI.PetTab = { Build = build, BuildRight = buildDetail }
-- The client redraws its screen in PetPaperDollFrame_Update: re-apply both panes after it.
if hooksecurefunc and type(_G["PetPaperDollFrame_Update"]) == "function" then
hooksecurefunc("PetPaperDollFrame_Update", function()
updatePreview()
updateDetail()
end)
end
-- Mounts have no place on this tab: their sub-tab goes, and the pet's with it when no other is
-- left (the client hides a lone sub-tab). A mount page left selected goes back to the pet.
if hooksecurefunc and type(_G["PetPaperDollFrame_UpdateTabs"]) == "function" then
hooksecurefunc("PetPaperDollFrame_UpdateTabs", function()
local pet, critters, mounts = PetPaperDollFrameTab1, PetPaperDollFrameTab2, PetPaperDollFrameTab3
if not (pet and critters and mounts) then
return
end
mounts:Hide()
if not critters:IsShown() then
pet:Hide()
end
-- PetPaperDollFrame_SetTab shows and hides the protected companion frame
if PetPaperDollFrame.selectedTab == 3 and HasPetUI() and not InCombatLockdown() then
PetPaperDollFrame_SetTab(1)
end
end)
end
-- UNIT_PET: the pet changes; the other three: its numbers change.
local listener = CreateFrame("Frame")
listener:RegisterEvent("UNIT_PET")
listener:RegisterEvent("UNIT_STATS")
listener:RegisterEvent("UNIT_ATTACK_POWER")
listener:RegisterEvent("UNIT_RESISTANCES")
listener:SetScript("OnEvent", function(_, event, unit)
-- Only the player's pet: UNIT_PET names the owner, the other three the unit itself (they
-- fire for every group member, and each refresh reloads the model)
if unit ~= ((event == "UNIT_PET") and "player" or "pet") then
return
end
updatePreview()
updateDetail()
end)