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

613 lines
18 KiB
Lua

-- Skills tab of the character frame (camelot SkillsFrame.xml / skillsframe.lua), the same
-- screen as the reputation tab. Headers are 26 high (not 28), entries 30. The entry bar
-- (SkillsBarTemplate 160 x 29) is blue through its sprite, not a tint, and always shows
-- the progress. Right pane: SkillDetailFrameMixin with a 180 x 29 RankBar.
-- 3.3.5 differences: GetSkillLineInfo has one level, so there are no sub-headers; weapon
-- lines get no hit / crit detail (no WEAPON_SKILL_DETAIL_* strings); the description does
-- not scroll (no ScrollingFontTemplate); the scroll bar is MinimalScrollBar from ScrollBar.lua.
local ForeverUI = ForeverUI or {}
_G.ForeverUI = ForeverUI
local L = ForeverUI.L
local LIST_X, LIST_Y = 10, -40
local LIST_X2, LIST_Y2 = -25, 15
local ENTRY_H = 30 -- SkillsEntryTemplate
local HEADER_H = 26 -- SkillsHeaderTemplate
local MARGIN = 10 -- SetPadding
local GAP = 3 -- elementSpacing
local HEADER_INDENT = 0
local OTHER_INDENT = 2
local BAR_W, BAR_H = 160, 29
local BAR_X = -3
local FILL_H = 15
local NAME_H = 15
local NAME_X = 2 -- SkillsEntryTemplate
local NAME_GAP = -10
local HEADER_NAME_X = 10
local ARROW_X, ARROW_Y = -8, -1
local ARROW_SPACE = 16
local SIDE = 6
local PLATE_CORNER = 12 -- same as the reputation tab
local GAUGE_CORNER = 10
local ATLAS_BAR_BACKGROUND = "common-stat-bar-bg"
local ATLAS_HEADER = "common-button-list-collapseexpand"
local ATLAS_PLUS = "common-button-list-plus"
local ATLAS_MINUS = "common-button-list-minus"
local ATLAS_LINE = "ui-character-info-scrollline-long"
local ATLAS_HOVER_SIDE = "charactercreate-customize-dropdown-linemouseover-side"
local ATLAS_HOVER_MIDDLE = "charactercreate-customize-dropdown-linemouseover-middle"
local ATLAS_SEPARATOR = "ui-character-info-scrollline"
-- Blue fill, baked with the mask like the reputation's white one: same slicing as the
-- background, so the outlines overlap.
local FILL_PATH = "Interface\\ForeverUI\\Bars\\statbarfillblue"
-- Right pane: CharacterFrameSidePaneTemplate.
local RIGHT_PANE_X, RIGHT_PANE_Y = 16, -14
local RIGHT_PANE_X2, RIGHT_PANE_Y2 = -12, 14
local TITLE_W = 195
local SUBTITLE_Y = -3
local SEPARATOR_Y = -4
local RANK_W, RANK_H = 180, 29
local RANK_Y = -6
local DESCRIPTION_Y = -8 -- not -6: SkillDetailFrameMixin
local DESCRIPTION_X2 = -14
local DESCRIPTION_Y2 = 6
-- Pane size, used while the host has no size yet.
local PANE_W, PANE_H = 398, 464
local rows = {}
local panel, offset = nil, 0
local visibleCount = 0
local selectedItem -- the NAME, the only stable key
local detail, lastData
-- --------------------------------------------------------------- Data
-- GetSkillLineInfo returns, in order: name, header, expanded, rank, temporary points, bonus,
-- max rank, abandonable, step cost, rank cost, min level, cost type, description.
local function readSkill(rank)
local name, header, expanded, value, temporary, bonus, maximum, _, _, _,
_, _, description = GetSkillLineInfo(rank)
if not name or name == "" then
return nil
end
value = (value or 0) + (temporary or 0)
bonus = bonus or 0
maximum = maximum or 0
-- InitializeBarForStandardSkill: "rank / max", or "rank (+bonus) / max" with the bonus
-- in green.
local text
if bonus == 0 then
text = tostring(value) .. " / " .. tostring(maximum)
else
text = tostring(value) .. " |cff00ff00(+" .. tostring(bonus)
.. ")|r / " .. tostring(maximum)
end
return {
index = rank,
name = name,
header = header,
collapsed = (header and not expanded) or false,
value = value,
maximum = maximum,
text = text,
description = description or "",
}
end
-- Skill line index of a skill name, or nil
local function indexOf(name)
if not name then
return nil
end
for rank = 1, (GetNumSkillLines and GetNumSkillLines()) or 0 do
if GetSkillLineInfo(rank) == name then
return rank
end
end
return nil
end
-- --------------------------------------------------------------- Row
local function createRow(index, width)
local row = CreateFrame("Button", "ForeverUISkillRow" .. index, panel)
row:SetWidth(width)
row:SetHeight(ENTRY_H)
row.plate = ForeverUI.CreateNineSlice(row, ATLAS_HEADER, PLATE_CORNER,
{ 0, 0, 0, 0 }, "BACKGROUND") or {}
for _, slice in ipairs(row.plate) do
slice:Hide()
end
local hover = CreateFrame("Frame", nil, row)
hover:SetAllPoints(row)
hover:SetAlpha(0)
row.hover = hover
local left = hover:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(left, ATLAS_HOVER_SIDE, true)
left:SetWidth(SIDE)
left:SetPoint("TOPLEFT", hover, "TOPLEFT", 0, 0)
left:SetPoint("BOTTOMLEFT", hover, "BOTTOMLEFT", 0, 0)
local right = hover:CreateTexture(nil, "BACKGROUND")
if ForeverUI.SetAtlas(right, ATLAS_HOVER_SIDE, true) then
local e = ForeverUI.AtlasEntry(ATLAS_HOVER_SIDE)
right:SetTexCoord(e[3], e[2], e[4], e[5])
end
right:SetWidth(SIDE)
right:SetPoint("TOPRIGHT", hover, "TOPRIGHT", 0, 0)
right:SetPoint("BOTTOMRIGHT", hover, "BOTTOMRIGHT", 0, 0)
local middle = hover:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(middle, ATLAS_HOVER_MIDDLE, true)
middle:SetPoint("TOPLEFT", left, "TOPRIGHT", 0, 0)
middle:SetPoint("BOTTOMRIGHT", right, "BOTTOMLEFT", 0, 0)
local bar = CreateFrame("Frame", nil, row)
bar:SetWidth(BAR_W)
bar:SetHeight(BAR_H)
bar:SetPoint("RIGHT", row, "RIGHT", BAR_X, 0)
row.bar = bar
ForeverUI.CreateNineSlice(bar, ATLAS_BAR_BACKGROUND, GAUGE_CORNER,
{ 0, 0, 0, 0 }, "BACKGROUND")
bar.fill = bar:CreateTexture(nil, "BORDER")
bar.fill:SetPoint("LEFT", bar, "LEFT", 0, 0)
bar.text = bar:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
bar.text:SetPoint("LEFT", bar, "LEFT", 0, 0)
bar.text:SetPoint("RIGHT", bar, "RIGHT", 0, 0)
bar.text:SetJustifyH("CENTER")
local name = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
name:SetHeight(NAME_H)
name:SetJustifyH("LEFT")
row.name = name
local arrow = row:CreateTexture(nil, "OVERLAY")
arrow:SetPoint("RIGHT", row, "RIGHT", ARROW_X, ARROW_Y)
arrow:Hide()
row.arrow = arrow
row:RegisterForClicks("LeftButtonUp")
return row
end
-- ----------------------------------------------------------- Fill
-- SetFillPercent: width = fraction x bar width, texture cropped the same.
local function placeBarIn(bar, data, width)
local fraction = 0
if data.maximum and data.maximum > 0 then
fraction = data.value / data.maximum
end
if fraction < 0 then
fraction = 0
elseif fraction > 1 then
fraction = 1
end
if fraction * width < 1 then
bar.fill:Hide()
else
bar.fill:SetTexture(FILL_PATH)
bar.fill:SetTexCoord(0, fraction, 0, 1)
bar.fill:SetWidth(width * fraction)
bar.fill:SetHeight(FILL_H)
bar.fill:Show()
end
bar.text:SetText(data.text or "")
end
-- RefreshBackgroundHighlightOpacity, without the "at war" case; the tint is always white.
local function placeHover(row)
if row.header then
row.hover:SetAlpha(0)
return
end
local hovered = row:IsMouseOver()
row.hover:SetAlpha((row.selectedItem and 0.20) or (hovered and 0.10) or 0)
end
-- SetFontObject resets the justification to the font's own (GameFontHighlight is centered
-- in the client's FontStyles.xml), so SetJustifyH must come after it, as camelot sets
-- justifyH="LEFT" on top of the font object.
local function populateRow(row, data)
row.skillIndex = data.index
row.skillName = data.name
row.header = data.header
row.collapsed = data.collapsed
row.name:SetText(data.name or "")
row.name:ClearAllPoints()
for _, slice in ipairs(row.plate) do
if data.header then slice:Show() else slice:Hide() end
end
if data.header then
row:SetHeight(HEADER_H)
row.bar:Hide()
row.name:SetFontObject(GameFontNormalLeft or GameFontNormal)
row.name:SetJustifyH("LEFT")
row.name:SetPoint("LEFT", row, "LEFT", HEADER_NAME_X, 0)
row.name:SetPoint("RIGHT", row, "RIGHT", ARROW_X - ARROW_SPACE, 0)
ForeverUI.SetAtlas(row.arrow, data.collapsed and ATLAS_PLUS or ATLAS_MINUS)
row.arrow:Show()
else
row:SetHeight(ENTRY_H)
row.bar:Show()
row.arrow:Hide()
row.name:SetFontObject(GameFontHighlight or GameFontNormal)
row.name:SetJustifyH("LEFT")
row.name:SetPoint("LEFT", row, "LEFT", NAME_X, 0)
row.name:SetPoint("RIGHT", row.bar, "LEFT", NAME_GAP, 0)
placeBarIn(row.bar, data, BAR_W)
end
row.selectedItem = (selectedItem ~= nil and selectedItem == data.name)
placeHover(row)
row:Show()
end
-- ------------------------------------------------------------- Layout
local function indentOf(data)
return data.header and HEADER_INDENT or OTHER_INDENT
end
local function heightOf(data)
return data.header and HEADER_H or ENTRY_H
end
-- Hides everything the client's SkillFrame holds except our panel: its regions
-- (GetRegions) and child frames (GetChildren). Runs on every pass, since
-- SkillFrame_UpdateSkills shows them again.
local function suppressClientScreen()
local frame = _G["SkillFrame"]
if not frame then
return
end
for _, region in ipairs({ frame:GetRegions() }) do
if region.Hide then
region:Hide()
end
end
if frame.GetChildren then
for _, childFrame in ipairs({ frame:GetChildren() }) do
if childFrame ~= panel and childFrame.Hide then
childFrame:Hide()
end
end
end
end
local function layout()
local total = (GetNumSkillLines and GetNumSkillLines()) or 0
local usableHeight = (panel:GetHeight() or 0)
if usableHeight < 50 then
usableHeight = PANE_H + LIST_Y - LIST_Y2
end
local usableWidth = (panel:GetWidth() or 0)
if usableWidth < 50 then
usableWidth = PANE_W + LIST_X2 - LIST_X
end
local y = MARGIN
local placedCount = 0
local full = false
for rank, row in ipairs(rows) do
local index = offset + rank
local data = (not full and index <= total) and readSkill(index) or nil
local height = data and heightOf(data) or 0
if data and y + height <= usableHeight - MARGIN then
local indent = indentOf(data)
row:SetWidth(usableWidth - 2 * MARGIN - indent)
populateRow(row, data)
row:ClearAllPoints()
row:SetPoint("TOPLEFT", panel, "TOPLEFT", MARGIN + indent, -y)
y = y + height + GAP
placedCount = placedCount + 1
else
-- Past the first row that does not fit nothing is placed: a shorter row further down
-- (a header after an entry) would otherwise take its slot, out of order
full = full or data ~= nil
row:Hide()
end
end
return placedCount
end
local function layoutList()
if not panel then
return
end
suppressClientScreen()
local total = (GetNumSkillLines and GetNumSkillLines()) or 0
local placedCount = layout()
if offset > 0 and offset + placedCount > total then
offset = math.max(0, total - placedCount)
placedCount = layout()
end
visibleCount = placedCount
if panel.bar then
panel.bar:Configure(total, placedCount, offset)
end
if ForeverUI.SkillsDetail then
ForeverUI.SkillsDetail()
end
end
ForeverUI.SkillsLayout = layoutList
-- --------------------------------------------------------------- Detail
local function clearDetail()
detail.title:SetText("")
detail.subtitle:SetText("")
-- SKILL_DETAIL_SELECT_PROMPT does not exist in 3.3.5: left empty.
detail.description:SetText("")
detail.separator:Hide()
detail.gauge:Hide()
end
local function updateDetail()
if not detail then
return
end
if not selectedItem then
lastData = nil
clearDetail()
return
end
local index = indexOf(selectedItem)
local data
if index then
if SetSelectedSkill and GetSelectedSkill and GetSelectedSkill() ~= index then
SetSelectedSkill(index)
end
data = readSkill(index)
lastData = data
else
data = lastData
end
if not data or data.header then
clearDetail()
return
end
detail.title:SetText(data.name or "")
detail.subtitle:SetText("")
detail.separator:Show()
detail.gauge:Show()
placeBarIn(detail.gauge, data, RANK_W)
detail.description:SetText(data.description or "")
end
ForeverUI.SkillsDetail = updateDetail
local function choose(name)
selectedItem = name
local index = indexOf(name)
if index and SetSelectedSkill then
SetSelectedSkill(index)
end
layoutList()
end
ForeverUI.SkillsSelect = choose
-- SelectFirstSkillIfNoneSelected: on opening, select the first non-header skill so the
-- right pane is not empty.
local function selectFirst()
if selectedItem then
return
end
for rank = 1, (GetNumSkillLines and GetNumSkillLines()) or 0 do
local data = readSkill(rank)
if data and not data.header then
choose(data.name)
return
end
end
end
-- Builds the right pane (skill detail) in host
local function buildDetail(host)
if detail then
return detail, {}
end
local frame = CreateFrame("Frame", "ForeverUISkillDetail", host)
frame:SetPoint("TOPLEFT", host, "TOPLEFT", RIGHT_PANE_X, RIGHT_PANE_Y)
frame:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", RIGHT_PANE_X2, RIGHT_PANE_Y2)
detail = frame
detail.title = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
detail.title:SetWidth(TITLE_W)
detail.title:SetJustifyH("CENTER")
detail.title:SetPoint("TOP", frame, "TOP", 0, 0)
detail.subtitle = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
detail.subtitle:SetWidth(TITLE_W)
detail.subtitle:SetJustifyH("CENTER")
detail.subtitle:SetPoint("TOP", detail.title, "BOTTOM", 0, SUBTITLE_Y)
detail.separator = frame:CreateTexture(nil, "BORDER")
ForeverUI.SetAtlas(detail.separator, ATLAS_SEPARATOR)
detail.separator:SetPoint("TOP", detail.subtitle, "BOTTOM", 0, SEPARATOR_Y)
local gauge = CreateFrame("Frame", "ForeverUISkillRank", frame)
gauge:SetWidth(RANK_W)
gauge:SetHeight(RANK_H)
gauge:SetPoint("TOP", detail.separator, "BOTTOM", 0, RANK_Y)
ForeverUI.CreateNineSlice(gauge, ATLAS_BAR_BACKGROUND, GAUGE_CORNER,
{ 0, 0, 0, 0 }, "BACKGROUND")
gauge.fill = gauge:CreateTexture(nil, "BORDER")
gauge.fill:SetPoint("LEFT", gauge, "LEFT", 0, 0)
gauge.text = gauge:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
gauge.text:SetPoint("LEFT", gauge, "LEFT", 0, 0)
gauge.text:SetPoint("RIGHT", gauge, "RIGHT", 0, 0)
gauge.text:SetJustifyH("CENTER")
detail.gauge = gauge
-- Description in a box: without a bottom anchor, a long text disappears.
detail.description = frame:CreateFontString(nil, "ARTWORK", "GameFontHighlight")
detail.description:SetPoint("TOPLEFT", gauge, "BOTTOMLEFT", 0, DESCRIPTION_Y)
detail.description:SetPoint("TOPRIGHT", gauge, "BOTTOMRIGHT", 0, DESCRIPTION_Y)
detail.description:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, DESCRIPTION_Y2)
detail.description:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT",
DESCRIPTION_X2, DESCRIPTION_Y2)
detail.description:SetJustifyH("LEFT")
detail.description:SetJustifyV("TOP")
if detail.description.SetWordWrap then
detail.description:SetWordWrap(true)
end
updateDetail()
return frame, {}
end
-- --------------------------------------------------------- Build
local function trackHover()
for _, row in ipairs(rows) do
if row:IsShown() and not row.header then
placeHover(row)
end
end
end
-- Builds the skill list in host over the client's SkillFrame
local function build(host)
local frame = _G["SkillFrame"]
if not frame or not host then
return nil, {}
end
frame:ClearAllPoints()
frame:SetPoint("TOPLEFT", host, "TOPLEFT", 0, 0)
frame:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", 0, 0)
if panel then
selectFirst()
return nil, { frame }
end
-- The 3.3.5 art and rows go away; only the frame is kept.
if frame.SetBackdrop then
frame:SetBackdrop(nil)
end
local height = host:GetHeight() or 0
if height < 100 then
height = PANE_H
end
local width = host:GetWidth() or 0
if width < 100 then
width = PANE_W
end
width = width + LIST_X2 - LIST_X
panel = CreateFrame("Frame", "ForeverUISkillList", frame)
panel:SetPoint("TOPLEFT", host, "TOPLEFT", LIST_X, LIST_Y)
panel:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", LIST_X2, LIST_Y2)
panel:SetWidth(width)
local position = math.floor((height + LIST_Y - LIST_Y2) / (HEADER_H + GAP))
if position < 1 then
position = 1
end
for index = 1, position do
local row = createRow(index, width)
row:SetScript("OnClick", function(self)
if not self.skillIndex then
return
end
if self.header then
if self.collapsed then
ExpandSkillHeader(self.skillIndex)
else
CollapseSkillHeader(self.skillIndex)
end
layoutList()
else
ForeverUI.SkillsSelect(self.skillName)
end
end)
rows[index] = row
end
-- Both lines live on our panel, not on the client's frame: its regions are all hidden
-- on every pass.
local top = panel:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(top, ATLAS_LINE)
top:SetPoint("CENTER", panel, "TOP", 0, 0)
local down = panel:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(down, ATLAS_LINE)
down:SetPoint("CENTER", panel, "BOTTOM", 0, 0)
-- Camelot's scroll bar, right of the list, as in the reputation tab. It does not know the
-- list: it gets three numbers and returns the new offset.
panel.bar = ForeverUI.CreateScrollBar("ForeverUISkillsScrollBar",
frame, panel)
panel.bar.onScroll = function(new)
offset = new
layoutList()
end
-- Without a bar the list takes its place, as in the reputation tab: right edge from -25
-- to -10, rows laid out again.
panel.bar.onVisibility = function(hasBar)
local x2 = hasBar and LIST_X2 or -LIST_X
panel:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", x2, LIST_Y2)
panel:SetWidth(width + x2 - LIST_X2)
layout()
end
panel:SetScript("OnUpdate", trackHover)
panel:EnableMouseWheel(true)
panel:SetScript("OnMouseWheel", function(self, direction)
local total = (GetNumSkillLines and GetNumSkillLines()) or 0
offset = math.max(0, math.min(offset - direction, total - visibleCount))
layoutList()
end)
layoutList()
selectFirst()
return nil, { frame }
end
ForeverUI.SkillsTab = { Build = build, BuildRight = buildDetail, Rows = rows }
-- The client rebuilds its list in SkillFrame_UpdateSkills: run after it.
if hooksecurefunc and type(_G["SkillFrame_UpdateSkills"]) == "function" then
hooksecurefunc("SkillFrame_UpdateSkills", layoutList)
end