ca2b78e217
/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>
519 lines
17 KiB
Lua
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)
|