Files
moon-well-ui/data/addon/ForeverUI/StatisticsTab.lua
T
Adrestie 5842eb7a2b ForeverUI : code en anglais, commentaires resserres, code mort retire
Traduction
- Identifiants en anglais dans tout le module : addon, ecrans d'accueil,
  outils Python, banc d'essai et patcheur C#.
- Fichiers au nom francais renommes (Gabarits.lua -> Templates.lua,
  Textes_*.lua -> Texts_*.lua, deployer.py -> deploy.py,
  tools/patcheur -> tools/patcher, etc.).
- Commandes /fui et options de deploy.py en anglais (--check, --server,
  --remove-dbc).
- Cles des variables sauvegardees et de data/dbc/statistics.json
  traduites, sans reprise des anciennes cles.
- API de la micro-barre : ForeverUI.AddMicroButton(def) avec les champs
  name, atlasSet, after, tooltip, onClick, ready, et
  ForeverUI.UpdateMicro(name, isOpen).
- Commentaires reecrits en anglais, courts, sans dates ni historique ;
  les fonctions au nom peu parlant et leurs parametres sont expliques.

Code mort
- Exports, fonctions et variables jamais lus retires.
- Retires a la demande des modules : ForeverUI.PvPJauge, les exports
  FondEntete / TuilerEntete / Polices de TradeSkill, l'art legacy-up-c60,
  honorfill(half)blue et inv_misc_book_09, leurs entrees d'atlas et les
  parties d'outils correspondantes.

Verifications : liaisons des noms comparees avant et apres, banc vert,
chemins de textures inchanges, generateurs reproduisant les fichiers,
patcheur identique octet pour octet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:15:52 +02:00

537 lines
16 KiB
Lua

-- ForeverUI: the character Statistics tab, after Camelot's blizzard_statistics (StatisticsFrame).
-- 3.3.5 has no ScrollBox or tree view, so the list is built by hand with recycled rows and the
-- ScrollBar.lua bar. The collapsed state survives CRITERIA_UPDATE (Camelot rebuilds its tree and
-- expands everything). Values are shown as the client returns them, as AchievementFrameStats_Update
-- does (Camelot shows only positive numbers), so the value column widens up to half the row.
local ForeverUI = ForeverUI or {}
_G.ForeverUI = ForeverUI
local ROOT = -1 -- ROOT_CATEGORY_ID
local LIST_X, LIST_Y = 10, -40
local LIST_X2, LIST_Y2 = -25, 15
local INDENT = 20 -- CreateScrollBoxListTreeListView
local MARGIN = 10
local GAP = 3
local HEADER_H = 26 -- StatisticsHeaderTemplate
local SUBHEADER_H = 22 -- StatisticsSubHeaderTemplate
local ENTRY_H = 24 -- StatisticsEntryTemplate
local HEADER_NAME_X = 10
local ARROW_X, ARROW_Y = -8, -1
local ARROW_SPACE = 16
local NAME_H = 15
local NAME_X = 2
local NAME_GAP = -10
local VALUE_W, VALUE_X = 50, -12
local BUTTON_W, BUTTON_X = 20, 2
local SUBNAME_GAP = 4
local SUB_NAME_X2 = -24
local SIDE = 6
local PLATE_CORNER = 12 -- as in the Skills and Reputation tabs
local HOVER_ALPHA = 0.10
local HEADER_HOVER_ALPHA = 0.3
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_CLOSED = "campaign_headericon_closed"
local ATLAS_CLOSED_PRESSED = "campaign_headericon_closedpressed"
local ATLAS_OPEN = "campaign_headericon_open"
local ATLAS_OPEN_PRESSED = "campaign_headericon_openpressed"
local BUTTON_HOVER = "Interface\\Buttons\\UI-PlusButton-Hilight"
-- Pane size, fixed by construction (as the Skills tab).
local PANE_W, PANE_H = 398, 464
local S = {}
ForeverUI.StatisticsTab = S
local rows = {}
local panel, measure, offset = nil, nil, 0
local visibleCount = 0
local collapsedSet = {} -- [category] = true
local elements = {} -- flattened list, as shown
-- --------------------------------------------------------------- Data
-- BuildOrderedCategoryIDs: ids in the order of their keys
local function orderedCategories(source)
local entries = {}
for key, value in next, source or {} do
if value and type(key) == "number" then
entries[#entries + 1] = { index = key, id = value }
end
end
table.sort(entries, function(a, b) return a.index < b.index end)
local ids = {}
for _, e in ipairs(entries) do
ids[#ids + 1] = e.id
end
return ids
end
-- statistic value as the client returns it; -- when empty (AchievementFrameStats_Update)
local function valueText(quantity)
if quantity == nil or quantity == "" then
return "--"
end
return tostring(quantity)
end
-- tree: { id, name, children = { categories }, stats = { { name, value } } }
-- (BuildCategoryEntriesFromSource, AppendStatisticRows). In 3.3.5 GetStatistic takes the id
-- from GetAchievementInfo(category, rank), as AchievementFrameStats_Update does.
local function readTree()
local order = orderedCategories(GetStatisticsCategoryList and GetStatisticsCategoryList())
local roots, childrenOf = {}, {}
for _, id in ipairs(order) do
local _, parent = GetCategoryInfo(id)
if parent == ROOT then
roots[#roots + 1] = id
else
childrenOf[parent] = childrenOf[parent] or {}
table.insert(childrenOf[parent], id)
end
end
local seen = {}
local function node(id)
if seen[id] then
return nil
end
seen[id] = true
local n = { id = id, name = GetCategoryInfo(id) or UNKNOWN, stats = {}, children = {} }
for rank = 1, (GetCategoryNumAchievements(id)) or 0 do
local statID = GetAchievementInfo(id, rank)
if statID then
local quantity, skip = GetStatistic(statID)
if not skip then
local _, name = GetAchievementInfo(statID)
n.stats[#n.stats + 1] = { name = name or UNKNOWN, value = valueText(quantity) }
end
end
end
for _, child in ipairs(childrenOf[id] or {}) do
local e = node(child)
if e then
n.children[#n.children + 1] = e
end
end
return n
end
local tree = {}
for _, id in ipairs(roots) do
local n = node(id)
if n then
tree[#tree + 1] = n
end
end
return tree
end
-- Flattened list in TreeDataProvider order: a category, its statistics, then its
-- subcategories; nothing under a collapsed category.
local function flatten(tree)
local list = {}
local function descend(n, depth)
list[#list + 1] = { kind = (depth == 1) and "header" or "sub", id = n.id,
name = n.name, depth = depth }
if collapsedSet[n.id] then
return
end
for _, s in ipairs(n.stats) do
list[#list + 1] = { kind = "stat", name = s.name, value = s.value, depth = depth + 1 }
end
for _, e in ipairs(n.children) do
descend(e, depth + 1)
end
end
for _, n in ipairs(tree) do
descend(n, 1)
end
return list
end
-- --------------------------------------------------------------- Row
-- is the name cut? 3.3.5 has no IsTruncated: compares the full width, measured on a separate
-- unbounded text, with the box
local function isTruncated(text, font)
local t = text:GetText()
if not t or t == "" then
return false
end
measure:SetFontObject(font)
measure:SetText(t)
return measure:GetStringWidth() > (text:GetWidth() or 0) + 0.5
end
local function tooltip(row)
local name = row.name
local font = (row.kind == "header") and (GameFontNormalLeft or GameFontNormal) or (GameFontHighlight or GameFontNormal)
if isTruncated(name, font) then
GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
GameTooltip:SetText(name:GetText())
GameTooltip:Show()
end
end
local function hideTooltip(row)
if GameTooltip:GetOwner() == row then
GameTooltip:Hide()
end
end
local function createRow(index)
local row = CreateFrame("Button", "ForeverUIStatisticsRow" .. index, panel)
row:SetHeight(ENTRY_H)
-- header: the plate, and the same in ADD on hover
row.plate = ForeverUI.CreateNineSlice(row, ATLAS_HEADER, PLATE_CORNER,
{ 0, 0, 0, 0 }, "BACKGROUND") or {}
row.glow = ForeverUI.CreateNineSlice(row, ATLAS_HEADER, PLATE_CORNER,
{ 0, 0, 0, 0 }, "HIGHLIGHT") or {}
for _, t in ipairs(row.glow) do
t:SetBlendMode("ADD")
t:SetAlpha(HEADER_HOVER_ALPHA)
end
-- content of an entry or subheader: shifts when pressed
local content = CreateFrame("Frame", nil, row)
content:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
content:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
row.content = content
local hover = CreateFrame("Frame", nil, content)
hover:SetAllPoints(content)
hover:SetAlpha(0)
hover:SetFrameLevel(math.max(0, row:GetFrameLevel() - 1))
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)
for _, t in ipairs({ left, right, middle }) do
t:SetVertexColor(1, 1, 1)
end
local value = content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
value:SetJustifyH("RIGHT")
value:SetWidth(VALUE_W)
value:SetHeight(NAME_H)
value:SetPoint("RIGHT", content, "RIGHT", VALUE_X, 0)
row.value = value
local name = content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
name:SetHeight(NAME_H)
name:SetJustifyH("LEFT")
row.name = name
-- header arrow, on the right
local arrow = row:CreateTexture(nil, "BORDER")
arrow:SetPoint("RIGHT", row, "RIGHT", ARROW_X, ARROW_Y)
arrow:Hide()
row.arrow = arrow
-- subheader button, on the left
local button = CreateFrame("Button", nil, content)
button:SetWidth(BUTTON_W)
button:SetHeight(BUTTON_W)
button:SetPoint("LEFT", content, "LEFT", BUTTON_X, 0)
button:SetNormalTexture(ForeverUI.AtlasEntry(ATLAS_CLOSED)[1])
button:SetPushedTexture(ForeverUI.AtlasEntry(ATLAS_CLOSED_PRESSED)[1])
button:SetHighlightTexture(BUTTON_HOVER, "ADD")
button:Hide()
row.button = button
row:RegisterForClicks("LeftButtonUp")
row:SetScript("OnMouseDown", function(self)
self.down = true
S.ApplyOffset(self)
end)
row:SetScript("OnMouseUp", function(self)
self.down = nil
S.ApplyOffset(self)
end)
row:SetScript("OnEnter", function(self)
S.ApplyHover(self)
tooltip(self)
end)
row:SetScript("OnLeave", function(self)
S.ApplyHover(self)
hideTooltip(self)
end)
row:SetScript("OnClick", function(self)
if self.category then
S.Toggle(self.category)
end
end)
button:SetScript("OnClick", function()
if row.category then
S.Toggle(row.category)
end
end)
return row
end
-- atlas image at its size, centered in a button texture
local function placeImage(texture, atlas)
if texture then
ForeverUI.SetAtlas(texture, atlas)
texture:ClearAllPoints()
texture:SetPoint("CENTER", texture:GetParent(), "CENTER", 0, 0)
end
end
-- RefreshBackgroundHighlightOpacity: 0.10 under the mouse; never for a header, which has its glow
function S.ApplyHover(row)
if row.kind == "header" then
row.hover:SetAlpha(0)
return
end
row.hover:SetAlpha(row:IsMouseOver() and HOVER_ALPHA or 0)
end
-- pressed: a header's name or an entry's content shifts by (1, -1)
function S.ApplyOffset(row)
local dx, dy = row.down and 1 or 0, row.down and -1 or 0
if row.kind == "header" then
row.content:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
row.content:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
row.name:SetPoint("LEFT", row, "LEFT", HEADER_NAME_X + dx, dy)
row.name:SetPoint("RIGHT", row, "RIGHT", ARROW_X - ARROW_SPACE + dx, dy)
else
row.content:SetPoint("TOPLEFT", row, "TOPLEFT", dx, dy)
row.content:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", dx, dy)
end
end
-- Fills row with list element e. SetFontObject clears the justification, so it is set again
-- after it (see SkillsTab.lua).
local function populateRow(row, e)
row.kind = e.kind
row.category = (e.kind ~= "stat") and e.id or nil
row.down = nil
row.name:SetText(e.name or "")
row.name:ClearAllPoints()
local header = e.kind == "header"
for _, t in ipairs(row.plate) do
if header then t:Show() else t:Hide() end
end
for _, t in ipairs(row.glow) do
if header then t:Show() else t:Hide() end
end
if header then
row:SetHeight(HEADER_H)
row.name:SetFontObject(GameFontNormalLeft or GameFontNormal)
row.name:SetJustifyH("LEFT")
row.value:SetText("")
row.button:Hide()
ForeverUI.SetAtlas(row.arrow, collapsedSet[e.id] and ATLAS_PLUS or ATLAS_MINUS)
row.arrow:Show()
elseif e.kind == "sub" then
row:SetHeight(SUBHEADER_H)
row.name:SetFontObject(GameFontHighlight or GameFontNormal)
row.name:SetJustifyH("LEFT")
-- the name anchors to the content in place of the button (anchored to a sibling it drifts,
-- see ReputationTab.lua)
row.name:SetPoint("LEFT", row.content, "LEFT", BUTTON_X + BUTTON_W + SUBNAME_GAP, 0)
row.name:SetPoint("RIGHT", row.content, "RIGHT", SUB_NAME_X2, 0)
row.value:SetText("")
row.arrow:Hide()
local closed = collapsedSet[e.id]
placeImage(row.button:GetNormalTexture(), closed and ATLAS_CLOSED or ATLAS_OPEN)
placeImage(row.button:GetPushedTexture(), closed and ATLAS_CLOSED_PRESSED or ATLAS_OPEN_PRESSED)
row.button:Show()
else
row:SetHeight(ENTRY_H)
row.name:SetFontObject(GameFontHighlight or GameFontNormal)
row.name:SetJustifyH("LEFT")
row.name:SetPoint("LEFT", row.content, "LEFT", NAME_X, 0)
row.name:SetPoint("RIGHT", row.value, "LEFT", NAME_GAP, 0)
row.value:SetText(e.value or "--")
-- value column: 50, or the value's width, up to half the row
measure:SetFontObject(GameFontHighlight or GameFontNormal)
measure:SetText(e.value or "--")
local cap = math.floor((row:GetWidth() or 0) / 2)
row.value:SetWidth(math.max(VALUE_W, math.min(math.ceil(measure:GetStringWidth()), cap)))
row.arrow:Hide()
row.button:Hide()
end
S.ApplyOffset(row)
S.ApplyHover(row)
row:Show()
end
-- ------------------------------------------------------------- Layout
local function heightOf(e)
return (e.kind == "header" and HEADER_H) or (e.kind == "sub" and SUBHEADER_H) or ENTRY_H
end
-- places the rows from offset; returns how many fit
local function layout()
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
for rank, row in ipairs(rows) do
local e = elements[offset + rank]
local height = e and heightOf(e) or 0
if e and y + height <= usableHeight - MARGIN then
local indent = (e.depth - 1) * INDENT
row:SetWidth(usableWidth - 2 * MARGIN - indent)
populateRow(row, e)
row:ClearAllPoints()
row:SetPoint("TOPLEFT", panel, "TOPLEFT", MARGIN + indent, -y)
y = y + height + GAP
placedCount = placedCount + 1
else
row:Hide()
end
end
return placedCount
end
-- lays out the list, clamps the offset and updates the bar
local function layoutList()
if not panel then
return
end
local total = #elements
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
end
-- Update: re-reads the tree and lays out the list at the same offset
function S.Update()
if not panel then
return
end
elements = flatten(readTree())
layoutList()
end
-- ToggleCollapsed of a category
function S.Toggle(category)
collapsedSet[category] = not collapsedSet[category] or nil
S.Update()
end
-- --------------------------------------------------------- Build
-- builds the tab once in pane host
local function build(host)
if panel then
return panel:GetParent(), {}
end
local root = CreateFrame("Frame", "ForeverUIStatisticsFrame", host)
root:SetAllPoints(host)
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", "ForeverUIStatisticsList", root)
panel:SetPoint("TOPLEFT", host, "TOPLEFT", LIST_X, LIST_Y)
panel:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", LIST_X2, LIST_Y2)
panel:SetWidth(width)
measure = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
measure:SetAlpha(0)
local position = math.floor((height + LIST_Y - LIST_Y2) / (SUBHEADER_H + GAP))
if position < 1 then
position = 1
end
for index = 1, position do
rows[index] = createRow(index)
end
-- the two lines, centered on the list's top and bottom
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 scroll bar; without it, the list takes its room
panel.bar = ForeverUI.CreateScrollBar("ForeverUIStatisticsScrollBar", root, panel)
panel.bar.onScroll = function(new)
offset = new
layoutList()
end
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:EnableMouseWheel(true)
panel:SetScript("OnMouseWheel", function(_, direction)
offset = math.max(0, math.min(offset - direction, #elements - visibleCount))
layoutList()
end)
-- listens to CRITERIA_UPDATE while the tab is shown
root:SetScript("OnShow", function(self)
self:RegisterEvent("CRITERIA_UPDATE")
S.Update()
end)
root:SetScript("OnHide", function(self)
self:UnregisterEvent("CRITERIA_UPDATE")
end)
root:SetScript("OnEvent", function(_, ev)
if ev == "CRITERIA_UPDATE" then
S.Update()
end
end)
root:RegisterEvent("CRITERIA_UPDATE")
S.Update()
return root, {}
end
S.Build = build
S.Rows = rows