Files
moon-well-ui/data/addon/ForeverUI/SkillsTab.lua
T
Claude afdd314370 ForeverUI : feuille de personnage, lignes des listes, statistiques, ensembles par personnage
Listes de réputation, compétences et statistiques : une ligne qui ne tenait
plus était cachée, mais la boucle continuait, et une ligne plus courte plus
loin (un en-tête après une entrée) prenait sa place, hors d'ordre. Rien
n'est plus placé après la première ligne qui ne tient pas.

Statistiques : la page possédait les douze lignes et les réaffichait toutes
après chaque mise à jour, y compris la sixième que le client cache pour la
catégorie Distance (les chasseurs par défaut), avec le contenu de la
catégorie d'avant. Elle possède désormais leur parent, CharacterAttributesFrame.

Ensembles : le panneau était créé visible, et GearManagerDialog_Update
(ensemble porté depuis une barre, /equipset) pouvait le construire avant
la première ouverture de sa page, par-dessus les statistiques. Il naît
caché, et sa page le connaît même ouverte en combat.

L'ordre d'affichage et le dernier ensemble porté étaient au compte, alors
que les ensembles sont à un personnage : ouvrir la page sur un autre
personnage élaguait l'ordre du premier. Ils sont rangés par personnage, et
le premier usage reprend les anciennes valeurs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxRZNHkq453dyUMcWsyUf1
2026-10-01 23:35:58 +00:00

632 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 }
-- Debug output: /fui skills.
function ForeverUI.SkillsDebug()
local say = function(text)
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. text)
end
local total = (GetNumSkillLines and GetNumSkillLines()) or 0
say(string.format(L.SKILLSTAB_DEBUG_SUMMARY,
total, offset, visibleCount, tostring(selectedItem)))
for rank = 1, total do
local d = readSkill(rank)
if d then
DEFAULT_CHAT_FRAME:AddMessage(string.format(
L.SKILLSTAB_DEBUG_ROW,
rank, d.name, tostring(d.header), tostring(d.collapsed), d.text))
end
end
end
-- 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