Files
moon-well-ui/data/addon/ForeverUI/CharacterFrame.lua
T
Adrestie 8372286532 ForeverUI : flèches du gestionnaire d'équipement à la camelot
Les boutons popout de 3.3.5 (PaperDollFrameItemPopoutButton : même liste
d'objets, mêmes entrées « ignorer l'emplacement », montrés avec
GearManagerDialog, donc sur la page Équipement seulement) prennent l'art
et la place de camelot 1.60.1.70450 (EquipmentFlyoutPopoutButtonTemplate) :
UI-Character-Info-Button-PullSide 20 x 43 à 5 px de l'emplacement,
tournée vers l'extérieur (colonne gauche vers la droite, colonne droite
vers la gauche), PullUp 43 x 20 au-dessus des armes ; -Open quand la
liste est ouverte, -Pressed enfoncé, survol en ADD. Le demi-tour se fait
par les coordonnées de texture (pas de SetRotation en 3.3.5).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 00:06:24 +02:00

2376 lines
83 KiB
Lua

-- ForeverUI: the character sheet in Camelot's layout: metal frame, left pane (model, gear
-- slots), right pane (stats, gear manager, titles) and a column of side tabs on the right.
-- Client frames (slots, tabs, model, stat rows) are resized and moved, never recreated, and
-- never in combat. Debug commands: /fui character, /fui tabs, /fui close, /fui model.
-- camelot: CHARACTER_FRAME_WIDTH/HEIGHT; LeftPaneHost 398 (the collapsed width), RightPaneHost 233
local WIDTH, HEIGHT = 631, 484
local LEFT_PANE, RIGHT_PANE = 398, 233
local L = ForeverUI.L
-- Right pane collapse button (camelot $parentRightPaneToggleButton: 28 x 28 at TOPRIGHT
-- (-6, -6) of the left pane). Collapsed, the window is the left pane's width; the side tabs
-- are anchored to the window edge and stay visible. 3.3.5 has no CVar for the state (kept in
-- ForeverUIDB) and no SOUNDKIT (sounds are named by string).
local COLLAPSE_SIZE = 28
local COLLAPSE_X, COLLAPSE_Y = -6, -6
local COLLAPSE_ART = {
expanded = { "Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Up",
"Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Down" },
collapsed = { "Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up",
"Interface\\Buttons\\UI-SpellbookIcon-NextPage-Down" },
}
local COLLAPSE_HOVER = "Interface\\Buttons\\UI-Common-MouseHilight"
local OVERHEAD = 20 -- panes start below the title bar
local ART_LEVEL = 5 -- frame level offset of the metal art, above the panes
-- Metal frame corner offsets. The camelot layout (PortraitFrameTemplate plus
-- NineSliceLayoutOverrides.lua) places the image's outer edge, which leaves the inner rim
-- 6 px inside the window. Each offset here is the rim-to-edge distance measured on the art
-- (uiframemetal2xc60, double density): left 18.5, right 8.5, bottom 14. The top keeps 16:
-- the title bar is inside by design. PANEL_RISE lifts the whole frame by 1 px.
local PANEL_RISE = 1
local PANEL_CORNERS = {
topLeftCorner = { x = -18.5, y = 16 + PANEL_RISE },
topRightCorner = { x = 8.5, y = 16 + PANEL_RISE },
bottomLeftCorner = { x = -18.5, y = -14 + PANEL_RISE },
bottomRightCorner = { x = 8.5, y = -14 + PANEL_RISE },
}
local SLOT_SIZE = 40
-- Slot layout (camelot/PaperDollFrame.xml): columns at (24, -60) and (-20, -60), main hand at
-- BOTTOM (-60, 30), the variant used when a ranged slot is shown (always, in 3.3.5).
-- The source gap is 6 both ways; the columns use 2 px less, the weapon row keeps 6.
local GAP = 6
local VERTICAL_GAP = GAP - 2
local LEFT_X, LEFT_Y = 24, -60
local RIGHT_X, RIGHT_Y = -20, -60
local WEAPON_X, WEAPON_Y = -60, 30
local SMALL_SLOT_SIZE = 27 -- ammo slot only; camelot: 27, AMMO_GAP 19 after the ranged slot
local AMMO_GAP = 19
-- The ranged slot is full size, unlike camelot's 27 px GearSlotSmall: it holds a ranged weapon
-- or a relic, items of the same rank as the melee weapons.
-- Right pane tabs (camelot PaperDollSidebarTabs: 42 x 42 buttons, bar at TOP (0, -4) of the
-- pane, tabs at (0, -5) of the bar): stats, gear manager and titles (from mainline; camelot
-- has no titles tab). 3.3.5 lacks PaperDollSidebarTabs.blp and the PAPERDOLL_SIDEBAR_* strings:
-- the gear tab uses UI-GearManager-Button, the tooltips CHARACTER_INFO and EQUIPMENT_MANAGER.
local PANE_TAB = 42
local PANE_TAB_Y = -9 -- bar at -4, first tab at -5 inside it
-- Each visible side tab sits at BOTTOMLEFT (0, -2) of the previous visible one
-- (camelot CharacterFrame.lua, UpdateTabLayout).
local TAB_GAP = -2
-- Side tab icons (camelot CHARACTER_MODE_TAB_ICONS and SetupModeTabs), baked with the tab mask.
-- Indexes are the 3.3.5 ones: 1 character, 2 pet, 3 reputation, 4 skills, 5 currency; pvp and
-- stats are tabs we create (CHARACTERFRAME_SUBFRAMES has only five). The PvP icon follows the
-- faction. Some files exist only by FileDataID: tools/simple_files.txt maps them to names.
local TAB_ICONS = {
[3] = "Interface\\ForeverUI\\TabIcons\\Inv_SideTab_Reputation2_c60",
[4] = "Interface\\ForeverUI\\TabIcons\\Ability_Racial_JackofAllTrades",
[5] = "Interface\\ForeverUI\\TabIcons\\Inv_SideTab_Currency_c60",
pvp = {
Alliance = "Interface\\ForeverUI\\TabIcons\\Inv_SideTab_Honor_Alliance_c60",
Horde = "Interface\\ForeverUI\\TabIcons\\Inv_SideTab_Honor_Horde_c60",
},
stats = "Interface\\ForeverUI\\TabIcons\\Inv_SideTab_Stats_c60",
mounts = "Interface\\ForeverUI\\TabIcons\\MountJournalPortrait",
}
-- Pet tab icon by class (camelot has no pet tab); the hunter one otherwise, e.g. for a mage
-- with the water elemental glyph
local PET_TAB = {
HUNTER = "Ability_Hunter_BeastTaming",
WARLOCK = "Spell_Shadow_SummonImp",
DEATHKNIGHT = "Spell_Shadow_AnimateDead",
}
TAB_ICONS[2] = "Interface\\ForeverUI\\TabIcons\\"
.. (PET_TAB[select(2, UnitClass("player")) or ""] or PET_TAB.HUNTER)
-- Column order: camelot's (character, reputation, skills, PvP, currency, stats) with the 3.3.5
-- pet tab second and the mounts under it. A number is a client tab, a string one of ours.
local TAB_ORDER = { 1, 2, "mounts", 3, 4, "pvp", 5, "stats" }
-- Character tab icon: the class icon (camelot uses the player portrait), one baked file per
-- class, named after UnitClass's token (WARRIOR, DEATHKNIGHT). MPQ lookups ignore case.
local CLASS_TAB = "Interface\\ForeverUI\\TabIcons\\ClassIcon_"
-- Screen opened by each client tab (CharacterFrameTab_OnClick); tells which tab is active
local TAB_SCREEN = {
[1] = "PaperDollFrame",
[2] = "PetPaperDollFrame",
[3] = "ReputationFrame",
[4] = "SkillFrame",
[5] = "TokenFrame",
}
-- Our screens, used as ForeverUI.Panes group names; they are not client subframes
local PVP_SCREEN = "ForeverUIPvPPane"
local STATS_SCREEN = "ForeverUIStatsPane"
local MOUNTS_SCREEN = "ForeverUIMountsPane"
local TAB_CREATED_SCREENS = { pvp = PVP_SCREEN, stats = STATS_SCREEN, mounts = MOUNTS_SCREEN }
-- Right pane tab icon: 28 px, the opening of UI-Character-Info-StatTab (metal at 3..6 and
-- 35..38 of 42). A portrait fills its texture and would overflow the rounded frame.
local PANE_TAB_ICON = 28
local ATLAS_PANE_TAB = "ui-character-info-stattab"
local ATLAS_PANE_TAB_SELECTED = "ui-character-info-stattab-selected"
local TAB_PORTRAIT_COORD = { 0.109375, 0.890625, 0.09375, 0.90625 }
local GEAR_MANAGER_ICON = "Interface\\PaperDollInfoFrame\\UI-GearManager-Button"
-- UI-GearManager-Button is opaque only in x 6..58 of 64: crop the central 52 x 52 square
local GEAR_MANAGER_ICON_COORD = { 6 / 64, 58 / 64, 6 / 64, 58 / 64 }
-- Titles tab icon: mainline PAPERDOLL_SIDEBARTAB_TITLES (camelot has no titles tab). The sheet
-- is not in 3.3.5 and ships with ForeverUI; the crop is the sealed scroll.
local TITLES_ICON =
"Interface\\ForeverUI\\PaperDollInfoFrame\\paperdollsidebartabs"
local TITLES_ICON_COORD = { 0.015625, 0.53125, 0.32421875, 0.46093750 }
-- camelot 70450: UI-Character-Info-Stat-StoneBG3, 233 x 60 (the level line left the band for the
-- left pane). StoneBG3 is StoneBG (233 x 85) with 25 px less in the middle: drawn from StoneBG's
-- top and bottom 30 px, so no new art is needed.
local STONE_HEIGHT = 60
local STONE_SOURCE_HEIGHT = 85
local STONE_PART = 30
local SEPARATOR_WIDTH = 11
local SEPARATOR_CAP = 4 -- cap height; the art is 11 x 50 with a cap at each end
local TABS_W, TABS_H = 64, 384 -- CharacterFrameModeTabs
local TABS_X = 1 -- 1 px right of the source
local TABS_Y = -30
local TAB_W, TAB_H = 55, 55 -- 55 x 60 minus 5 transparent px
-- Side tab icon (camelot SidePanelTabButtonMixin:UpdateIconInterior, fillToInterior): 50 px,
-- cropped by 0.03125, offset (-3, 0) since the tab art is transparent on the right.
-- 3.3.5 has no MaskTexture: tools/bake_masks.py bakes common-sidetab-mask into the icons
-- (TabIcons/). The bake assumes this size and crop.
local SIDE_TAB_ICON = 50 -- interiorExtent
local TAB_ICON_X = -3 -- GetIconAnchorOffsetsForTabArt
local TAB_PORTRAIT_INSET = 0.03125 -- UpdateIconInterior crop
-- Side tab layers (LargeSideTabButtonTemplate): BACKGROUND common-sidetab, ARTWORK icon,
-- OVERLAY common-sidetab-selected for the active tab, HIGHLIGHT common-sidetab-hover.
local PORTRAIT = 48 -- see placePortrait
-- The portrait follows the top-left corner image, which carries the ring: the hole's center is
-- at (38, -38.5) from the image's top-left (measured on the double-density art).
local PORTRAIT_HOLE_X, PORTRAIT_HOLE_Y = 38, -38.5
local PORTRAIT_X = PANEL_CORNERS.topLeftCorner.x + PORTRAIT_HOLE_X
local PORTRAIT_Y = PANEL_CORNERS.topLeftCorner.y + PORTRAIT_HOLE_Y
-- Title (TitledPanelMixin:SetTitleOffsets defaults, which CharacterFrame keeps): container at
-- TOPLEFT (58, -1) and TOPRIGHT (-24, -1), text at TOP (0, -5). It is centered between the
-- portrait and the close button, not on the window; one line.
local TITLE_LEFT, TITLE_RIGHT = 58, -24
local TITLE_CONTAINER, TITLE_TEXT = -1, -5
local TITLE_STRIP_HEIGHT = 20
-- The title always shows the character's name and title (camelot shows the open panel's name).
-- Level and class line (camelot 70450 paperdollframe.xml): TopBackgroundStripHost, 36 high across
-- the top of the left pane, UI-Background-Rock tiled with _UI-Frame-InnerTopTile along its bottom;
-- PaperDollLevelInfo 220 x 20 at CENTER (0, -2) of the strip, GameFontNormalMed2Outline (14,
-- outlined; 3.3.5 has no such font object), "Level %d |c<class color>Class|r"
-- (PLAYER_LEVEL_NO_SPEC).
local STRIP_HEIGHT = 36
local STRIP_EDGE = 3
local LEVEL_Y = -2
local LEVEL_WIDTH, LEVEL_HEIGHT = 220, 20
local LEVEL_FONT_SIZE = 14
-- UI-Background-Rock is 1024 x 1024: the strip shows a crop at its own size (3.3.5 textures
-- cannot tile)
local ROCK_FILE = "Interface" .. string.char(92) .. "ForeverUI" .. string.char(92) .. "framegeneral"
.. string.char(92) .. "ui-background-rock"
local ROCK_SIZE = 1024
-- Model rotation arrows, side by side, centered at the top of the left pane, under the strip
-- (camelot 70450: the model scene's ControlFrame at TOP (0, -40) of the left pane).
local ROTATION_Y = -40
local ROTATION_GAP = 4
-- The model is raised in its pane: camelot's scene fills the pane, but the 3.3.5 camera frames
-- the character lower. Tuned by eye.
local MODEL_Y = 24
-- Camelot frames the character with a model scene; 3.3.5 has none. This client's Model has
-- SetModelScale, SetPosition, SetCamera and SetFacing, but no SetCameraDistance,
-- SetCamDistanceScale or SetPortraitZoom. SetModelScale shrinks the model around its origin
-- (it slides down); SetPosition(depth, side, height) moves it away without reframing.
-- Values tuned by eye (/fui model); a nil scale leaves the client's scale untouched.
local MODEL_SCALE = nil
local MODEL_POSITION = { -6.5, 0, 0 } -- depth, side, height
-- Resistances panel offset to the right, added to its original anchor
local RESISTANCES_X = 30
-- Statistics: CharacterAttributesFrame holds two groups of six StatFrameTemplate rows (104 x 13),
-- PlayerStatFrameLeft1..6 and Right1..6, each under a category dropdown. They stay the client's
-- (values, tooltips, menus) and are stacked in one column like camelot, widened to the pane.
-- The client's FrameXML is in patch-enUS-2/3 (patch-enUS-9 is disabled).
local STAT_STEP = 13 -- StatFrameTemplate height
local STAT_MARGIN = 20 -- on each side of the pane
local STAT_TOP = 2 -- below the stone band (tightened by the user, 2026-10-17)
-- 12 (was 16): three groups under the 60 px stone take 2 + 3 x (35 + 78) + 2 x 12 = 365 of 404 px
local STAT_GROUP_GAP = 12
local STAT_PER_GROUP = 6
-- Category selectors (UIDropDownMenuTemplate): camelot's dropdown control,
-- WowStyle1DropdownTemplate (blizzard_menu/mainline/MenuTemplates.xml, 25 high), laid over the
-- client's menu by Tpl.MenuStyle1: common-dropdown-textholder background, common-dropdown-a-button
-- arrow and its states, GameFontHighlight text on the left. The menu and its categories stay the
-- client's. The control is as wide as the rows; its background reaches 8 px past them on each
-- side (TOPLEFT (-8, 7) / BOTTOMRIGHT (8, -9)): STAT_HEADER leaves room for it.
local STAT_SELECTOR_HEIGHT = 25
local STAT_SELECTOR_TOP = 7 -- the background starts 7 px above the control
-- rows start 3 px under the control, inside the shadow its background casts (tightened by the
-- user, 2026-10-17: was the whole 9 px shadow + 2)
local STAT_HEADER = STAT_SELECTOR_TOP + STAT_SELECTOR_HEIGHT + 3
-- Alternating stat row bands (neither camelot nor 3.3.5 has them), from paperdollinfopart1c60:
-- dark UI-Character-Info-ItemLevel-Bounce first, then light UI-Character-Info-Line-Bounce;
-- the count restarts at each category. StatFrameTemplate's label is on BACKGROUND and 3.3.5
-- has no sublevels, so the band would draw over it: the label moves up to ARTWORK.
local ATLAS_STAT_DARK = "ui-character-info-itemlevel-bounce"
local ATLAS_STAT_LIGHT = "ui-character-info-line-bounce"
local STAT_GROUPS = {
{
selector = "PlayerStatFrameLeftDropDown",
prefix = "PlayerStatFrameLeft",
cvar = "playerStatLeftDropdown",
},
{
selector = "PlayerStatFrameRightDropDown",
prefix = "PlayerStatFrameRight",
cvar = "playerStatRightDropdown",
},
-- A third group, ForeverUI's own (not in camelot nor in 3.3.5), in the 25 px the stone gave
-- back: same dropdown and rows, its choice kept in ForeverUIDB.statThirdCategory (3.3.5 cannot
-- create a CVar); by default a category the two others do not show.
{
selector = "ForeverUIStatFrameThirdDropDown",
prefix = "ForeverUIStatFrameThird",
third = true,
},
}
-- The client's category list (PaperDollFrame.lua PLAYERSTAT_DROPDOWN_OPTIONS), in its order
local STAT_CATEGORIES = PLAYERSTAT_DROPDOWN_OPTIONS or { "PLAYERSTAT_BASE_STATS",
"PLAYERSTAT_MELEE_COMBAT", "PLAYERSTAT_RANGED_COMBAT", "PLAYERSTAT_SPELL_COMBAT",
"PLAYERSTAT_DEFENSES" }
local CLOSE_SIZE = 24
local CLOSE_X, CLOSE_Y = 1, 0
local CLOSE_ATLAS = {
{ atlas = "redbutton-exit", method = "GetNormalTexture" },
{ atlas = "redbutton-exit-pressed", method = "GetPushedTexture" },
{ atlas = "redbutton-exit-disabled", method = "GetDisabledTexture" },
{ atlas = "redbutton-highlight", method = "GetHighlightTexture" },
}
local PORTRAIT_CORNER = "ui-frame-portraitmetal-cornertopleft"
local ATLAS = {
backgroundLeft = "ui-character-info-general-bg",
backgroundRight = "ui-character-info-stat-bg",
stone = "ui-character-info-stat-stonebg",
separator = "common-framedivider",
slot = "ui-character-info-gearslot",
smallSlot = "ui-character-info-gearslotsmall",
tab = "common-sidetab",
tabHover = "common-sidetab-hover",
tabActive = "common-sidetab-selected",
}
local LEFT_COLUMN = {
"Head", "Neck", "Shoulder", "Back", "Chest", "Shirt", "Tabard", "Wrist",
}
local RIGHT_COLUMN = {
"Hands", "Waist", "Legs", "Feet", "Finger0", "Finger1", "Trinket0", "Trinket1",
}
local WEAPON_ROW = { "MainHand", "SecondaryHand", "Ranged" }
-- Weapon row: extra gap per slot, relative to the left neighbor and cumulative along the chain
-- (main hand 0, off hand -2, ranged -4; the ammo slot follows the ranged one). The ammo arrow
-- is a region of CharacterAmmoSlot and moves with it.
local ROW_OFFSET = { 0, -2, -2 }
-- Frames whose old art is hidden: 3.3.5 spreads it over child frames, which a sweep of
-- CharacterFrame alone misses.
local OLD_FRAMES = {
"CharacterFrame", "PaperDollFrame", "PetPaperDollFrame", "SkillFrame",
"ReputationFrame", "HonorFrame", "TokenFrame", "PaperDollItemsFrame",
"CharacterAttributesFrame", "CharacterAttributesFrameer",
"CharacterResistanceFrame",
}
-- Saved window positions (ForeverUIDB). The client's panel manager re-places the sheet on each
-- show, so the position saved by dragging is re-applied after it.
local function savedPositions()
ForeverUIDB = ForeverUIDB or {}
ForeverUIDB.positions = ForeverUIDB.positions or {}
return ForeverUIDB.positions
end
local leftPane, rightPane, tabBar
local tabs = {}
-- Hides the texture regions of a frame (not those of its children)
local function sweepTextures(frame)
if not frame or not frame.GetRegions then
return
end
local regions = { frame:GetRegions() }
for _, region in ipairs(regions) do
if region and region.GetObjectType and region:GetObjectType() == "Texture" then
region:SetAlpha(0)
end
end
end
local function clearLegacyArt()
for _, name in ipairs(OLD_FRAMES) do
sweepTextures(_G[name])
end
end
-- A file added to the .toc loads only after a client restart (/reload does not find new
-- files). If Panes.lua is missing, say so in the chat instead of failing.
local missingReported = false
local function libraryPresent()
if ForeverUI.Panes then
return true
end
if not missingReported then
missingReported = true
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r : "
.. L.CHARACTERFRAME_PANES_MISSING)
end
return false
end
-- Creates the left and right panes (backgrounds, stone band, divider) as ForeverUI.Panes hosts
local function setupPanes(frame)
if leftPane then
return
end
leftPane = CreateFrame("Frame", "ForeverUICharacterLeftPane", frame)
leftPane:SetWidth(LEFT_PANE)
leftPane:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, -OVERHEAD)
leftPane:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0)
local backgroundLeft = leftPane:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(backgroundLeft, ATLAS.backgroundLeft)
backgroundLeft:SetPoint("TOPLEFT", leftPane, "TOPLEFT", 0, 0)
rightPane = CreateFrame("Frame", "ForeverUICharacterRightPane", frame)
rightPane:SetWidth(RIGHT_PANE)
rightPane:SetPoint("TOPLEFT", leftPane, "TOPRIGHT", 0, 0)
rightPane:SetPoint("BOTTOMLEFT", leftPane, "BOTTOMRIGHT", 0, 0)
-- The source sets no anchor: the background fills the pane (its atlas is 233 x 383).
local backgroundRight = rightPane:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(backgroundRight, ATLAS.backgroundRight, true)
backgroundRight:SetAllPoints(rightPane)
-- StoneBG3: StoneBG's top and bottom STONE_PART px, one under the other, in a 233 x 60 frame
-- that is the stone band for whatever anchors under it (equipment and titles pages)
local stone = CreateFrame("Frame", "ForeverUICharacterStone", rightPane)
stone:SetWidth(RIGHT_PANE)
stone:SetHeight(STONE_HEIGHT)
stone:SetPoint("TOPLEFT", rightPane, "TOPLEFT", 0, 0)
stone:SetFrameLevel(rightPane:GetFrameLevel())
local upper = stone:CreateTexture(nil, "ARTWORK")
local lower = stone:CreateTexture(nil, "ARTWORK")
local e = ForeverUI.AtlasEntry(ATLAS.stone)
if e then
local cut = (e[5] - e[4]) * STONE_PART / STONE_SOURCE_HEIGHT
upper:SetTexture(e[1])
upper:SetTexCoord(e[2], e[3], e[4], e[4] + cut)
lower:SetTexture(e[1])
lower:SetTexCoord(e[2], e[3], e[5] - cut, e[5])
end
upper:SetPoint("TOPLEFT", stone, "TOPLEFT", 0, 0)
upper:SetPoint("TOPRIGHT", stone, "TOPRIGHT", 0, 0)
upper:SetHeight(STONE_PART)
lower:SetPoint("TOPLEFT", upper, "BOTTOMLEFT", 0, 0)
lower:SetPoint("BOTTOMRIGHT", stone, "BOTTOMRIGHT", 0, 0)
stone.upper, stone.lower = upper, lower
rightPane.stone = stone
-- The divider has a cap at each end, so it is drawn in three slices. It sits one level above
-- the skin layer (ART_LEVEL), else the metal covers it; that layer does not exist yet here,
-- hence the constant.
local separator = ForeverUI.CreateVerticalDivider(rightPane, ATLAS.separator,
SEPARATOR_CAP, frame:GetFrameLevel() + ART_LEVEL + 1)
separator:SetWidth(SEPARATOR_WIDTH)
separator:SetPoint("TOPLEFT", rightPane, "TOPLEFT", -6, -1)
separator:SetPoint("BOTTOMLEFT", rightPane, "BOTTOMLEFT", -6, 0)
rightPane.separator = separator
ForeverUI.CharacterPanes = { left = leftPane, right = rightPane }
-- Both panes are ForeverUI.Panes hosts: what they show is decided in Panes.lua.
ForeverUI.Panes.NewHost("left", leftPane)
ForeverUI.Panes.NewHost("right", rightPane)
end
-- Portrait: the ring's hole crops it, as on the bags (camelot masks a 62 px portrait).
-- Ring profile by radius: 0-12 hole, 13-25 gradient, 26-29 opaque, 30-32 fade. A square of
-- side C reaches C/2 on its axes and C/2 x 1.41 in its corners: 48 reaches opaque metal on the
-- axes (24), with corners at 33.9, just past the ring; 46 would keep the corners inside.
local function placePortrait(frame)
local host = frame.foreverSkinLayer or frame
if not frame.foreverPortrait then
local portrait = host:CreateTexture(nil, "BACKGROUND")
portrait:SetWidth(PORTRAIT)
portrait:SetHeight(PORTRAIT)
portrait:SetPoint("CENTER", host, "TOPLEFT", PORTRAIT_X, PORTRAIT_Y)
frame.foreverPortrait = portrait
end
if SetPortraitTexture then
SetPortraitTexture(frame.foreverPortrait, "player")
end
end
-- Title in a child frame above the metal (camelot: TitleContainer at level 510, NineSlice at
-- 400): the metal is OVERLAY on its own frame.
local function placeTitle(frame)
if not frame.foreverTitleStrip then
local old = _G["CharacterNameText"]
if old then
old:Hide()
end
local strip = CreateFrame("Frame", "ForeverUICharacterTitle", frame)
strip:SetFrameLevel(frame:GetFrameLevel() + ART_LEVEL + 2)
strip:SetHeight(TITLE_STRIP_HEIGHT)
strip:SetPoint("TOPLEFT", frame, "TOPLEFT", TITLE_LEFT, TITLE_CONTAINER)
strip:SetPoint("TOPRIGHT", frame, "TOPRIGHT", TITLE_RIGHT, TITLE_CONTAINER)
local text = strip:CreateFontString(nil, "OVERLAY", "GameFontNormal")
text:SetPoint("TOP", strip, "TOP", 0, TITLE_TEXT)
text:SetPoint("LEFT", strip, "LEFT")
text:SetPoint("RIGHT", strip, "RIGHT")
text:SetJustifyH("CENTER")
frame.foreverTitleStrip = strip
frame.foreverTitle = text
-- The title bar is the drag handle
frame:SetMovable(true)
frame:SetClampedToScreen(true)
strip:EnableMouse(true)
strip:RegisterForDrag("LeftButton")
strip:SetScript("OnDragStart", function()
if not InCombatLockdown() then
frame:StartMoving()
end
end)
strip:SetScript("OnDragStop", function()
frame:StopMovingOrSizing()
local point, _, relativePoint, x, y = frame:GetPoint(1)
if point then
savedPositions()["sheet"] = {
point = point, relativePoint = relativePoint, x = x, y = y,
}
end
end)
end
-- UnitPVPName gives the name with the current title, if any
local text = frame.foreverTitle
local name = (UnitPVPName and UnitPVPName("player")) or UnitName("player")
text:SetText(name or "")
if HIGHLIGHT_FONT_COLOR then
text:SetTextColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g,
HIGHLIGHT_FONT_COLOR.b)
end
end
-- Close button: the client's, with the modern red X, at the top right corner
local function placeCloseButton(frame)
local close = _G["CharacterFrameCloseButton"]
if not close then
return
end
close:SetWidth(CLOSE_SIZE)
close:SetHeight(CLOSE_SIZE)
close:ClearAllPoints()
close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", CLOSE_X, CLOSE_Y)
close:SetFrameLevel(frame:GetFrameLevel() + ART_LEVEL + 2)
if frame.foreverCloseSkinned then
return
end
for _, entry in ipairs(CLOSE_ATLAS) do
local texture = close[entry.method] and close[entry.method](close)
if texture then
ForeverUI.SetAtlas(texture, entry.atlas, true)
texture:ClearAllPoints()
texture:SetAllPoints(close)
if entry.atlas == "redbutton-highlight" then
texture:SetBlendMode("ADD")
end
end
end
frame.foreverCloseSkinned = true
end
-- Resizes a client gear slot and gives it camelot's slot frame.
-- name: slot name part (Head, Ammo); side: size; frameAtlas: frame art
local function skinSlot(name, side, frameAtlas)
local button = _G["Character" .. name .. "Slot"]
if not button then
return nil
end
button:SetWidth(side)
button:SetHeight(side)
-- Camelot lays the slots over a model that fills the left pane. The model takes the mouse and
-- shares the slots' frame level (all PaperDollFrame children); at equal levels the model gets
-- the click, so the slot goes one level above. Raised only when needed: this runs on each pass.
local model = _G["CharacterModelFrame"]
if model and model.GetFrameLevel
and button:GetFrameLevel() <= model:GetFrameLevel() then
button:SetFrameLevel(model:GetFrameLevel() + 1)
end
if not button.foreverFrame then
local normalFont = button:GetNormalTexture()
if normalFont then
normalFont:SetAlpha(0)
end
local frame = button:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(frame, frameAtlas)
frame:SetPoint("CENTER", button, "CENTER", 0, 0)
button.foreverFrame = frame
end
local icon = _G["Character" .. name .. "SlotIconTexture"]
if icon then
icon:ClearAllPoints()
icon:SetAllPoints(button)
icon:SetTexCoord(0, 1, 0, 1)
end
return button
end
-- Equipment flyout arrows (camelot EquipmentFlyoutPopoutButtonTemplate, 1.60.1.70450): the 3.3.5
-- popout button of each slot (PaperDollFrameItemPopoutButton: same flyout, same "ignore this
-- slot" entries; shown while GearManagerDialog is, so on the equipment page only) gets camelot's
-- art and place. Left column: points right, 20 x 43 at LEFT on the slot's RIGHT (5, 0); right
-- column: points left, at RIGHT on its LEFT (-5, 0); weapons: points up, 43 x 20 at BOTTOM on
-- its TOP (0, 5). UI-Character-Info-Button-PullSide / PullUp, -Open while the flyout is locked
-- open, -Pressed, highlight = the image in ADD. camelot rotates the side image for the right
-- direction; 3.3.5 has no SetRotation: the half turn is done by swapping the tex coords.
local POPOUT = {
side = { w = 20, h = 43, atlas = "ui-character-info-button-pullside" },
up = { w = 43, h = 20, atlas = "ui-character-info-button-pullup" },
gap = 5,
}
-- Sets atlas name on texture, turned half a turn when flip is set
local function popoutTexture(texture, name, flip)
local e = texture and ForeverUI.AtlasEntry(name)
if not e then
return
end
texture:SetTexture(e[1])
if flip then
texture:SetTexCoord(e[3], e[2], e[5], e[4])
else
texture:SetTexCoord(e[2], e[3], e[4], e[5])
end
texture:ClearAllPoints()
texture:SetAllPoints(texture:GetParent())
end
-- direction: "LEFT", "RIGHT" or "UP"; open: the flyout is locked open
local function paintPopout(popout, direction, open)
local set = (direction == "UP") and POPOUT.up or POPOUT.side
-- the side image points left (camelot's rotation for LEFT is 0)
local flip = direction == "RIGHT"
local normal = set.atlas .. (open and "-open" or "")
popout:SetWidth(set.w)
popout:SetHeight(set.h)
popoutTexture(popout:GetNormalTexture(), normal, flip)
popoutTexture(popout:GetPushedTexture(), set.atlas .. "-pressed", flip)
local highlight = popout:GetHighlightTexture()
popoutTexture(highlight, normal, flip)
if highlight then
highlight:SetBlendMode("ADD")
end
end
local function skinPopout(name, direction)
local slot = _G["Character" .. name .. "Slot"]
local popout = slot and (slot.popoutButton or _G["Character" .. name .. "SlotPopoutButton"])
if not popout then
return
end
-- the Pushed state does not exist in the client's template
if not popout:GetPushedTexture() and popout.SetPushedTexture then
local e = ForeverUI.AtlasEntry(POPOUT.side.atlas)
if e then
popout:SetPushedTexture(e[1])
end
end
popout.foreverDirection = direction
paintPopout(popout, direction, popout.flyoutLocked)
popout:ClearAllPoints()
if direction == "RIGHT" then
popout:SetPoint("LEFT", slot, "RIGHT", POPOUT.gap, 0)
elseif direction == "LEFT" then
popout:SetPoint("RIGHT", slot, "LEFT", -POPOUT.gap, 0)
else
popout:SetPoint("BOTTOM", slot, "TOP", 0, POPOUT.gap)
end
end
local function skinPopouts()
for _, name in ipairs(LEFT_COLUMN) do skinPopout(name, "RIGHT") end
for _, name in ipairs(RIGHT_COLUMN) do skinPopout(name, "LEFT") end
for _, name in ipairs(WEAPON_ROW) do skinPopout(name, "UP") end
end
-- The client turns its arrow when the flyout opens or closes: our image follows, open or not
if hooksecurefunc and type(PaperDollFrameItemPopoutButton_SetReversed) == "function" then
hooksecurefunc("PaperDollFrameItemPopoutButton_SetReversed", function(popout, reversed)
if popout and popout.foreverDirection then
paintPopout(popout, popout.foreverDirection, reversed)
end
end)
end
local function placeSlots()
local previous
for index, name in ipairs(LEFT_COLUMN) do
local button = skinSlot(name, SLOT_SIZE, ATLAS.slot)
if button then
button:ClearAllPoints()
if index == 1 then
button:SetPoint("TOPLEFT", leftPane, "TOPLEFT", LEFT_X, LEFT_Y)
else
button:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, -VERTICAL_GAP)
end
previous = button
end
end
previous = nil
for index, name in ipairs(RIGHT_COLUMN) do
local button = skinSlot(name, SLOT_SIZE, ATLAS.slot)
if button then
button:ClearAllPoints()
if index == 1 then
button:SetPoint("TOPRIGHT", leftPane, "TOPRIGHT", RIGHT_X, RIGHT_Y)
else
button:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, -VERTICAL_GAP)
end
previous = button
end
end
previous = nil
for index, name in ipairs(WEAPON_ROW) do
local button = skinSlot(name, SLOT_SIZE, ATLAS.slot)
if button then
button:ClearAllPoints()
if index == 1 then
button:SetPoint("BOTTOM", leftPane, "BOTTOM", WEAPON_X, WEAPON_Y)
else
button:SetPoint("TOPLEFT", previous, "TOPRIGHT",
GAP + (ROW_OFFSET[index] or 0), 0)
end
previous = button
end
end
local ammo = skinSlot("Ammo", SMALL_SLOT_SIZE, ATLAS.smallSlot)
if ammo and previous then
ammo:ClearAllPoints()
ammo:SetPoint("LEFT", previous, "RIGHT", AMMO_GAP, 0)
end
skinPopouts()
end
ForeverUI.ModelSetting = { scale = MODEL_SCALE, position = MODEL_POSITION }
-- Mouse drag rotation for the character and pet models (3.3.5 only rotates them with the arrow
-- buttons). Camelot's orbit camera: 0.008 rad per UI unit of horizontal drag, eased at 0.15
-- per frame (OrbitCameraMixin). Here the model turns through model.rotation and SetRotation.
local MOUSE_ROTATION, ROTATION_SMOOTHING = 0.008, 0.15
local function rotateWithMouse(model)
if not model or model.foreverMouseHooked then
return
end
model.foreverMouseHooked = true
model:EnableMouse(true)
model:HookScript("OnMouseDown", function(self, button)
if button == "LeftButton" then
self.foreverCursor = GetCursorPosition()
self.foreverTarget = self.foreverTarget or self.rotation or 0
end
end)
model:HookScript("OnMouseUp", function(self, button)
if button == "LeftButton" then
self.foreverCursor = nil
end
end)
model:HookScript("OnHide", function(self)
self.foreverCursor, self.foreverTarget = nil, nil
end)
model:HookScript("OnUpdate", function(self, elapsed)
if self.foreverCursor then
local x = GetCursorPosition()
local d = (x - self.foreverCursor) / UIParent:GetEffectiveScale()
self.foreverCursor = x
self.foreverTarget = self.foreverTarget + d * MOUSE_ROTATION
end
if not self.foreverTarget then
return
end
local r = self.rotation or 0
local k = 1 - (1 - ROTATION_SMOOTHING) ^ ((elapsed or 0) * 60)
r = r + (self.foreverTarget - r) * k
if math.abs(self.foreverTarget - r) < 0.0005 then
r = self.foreverTarget
if not self.foreverCursor then
self.foreverTarget = nil
end
end
self.rotation = r
self:SetRotation(r)
end)
end
ForeverUI.RotateWithMouse = rotateWithMouse
rotateWithMouse(_G["CharacterModelFrame"])
rotateWithMouse(_G["PetModelFrame"])
-- The model loads asynchronously after PaperDollFrame_OnShow's SetUnit, and the load resets its
-- transform. The setting is re-applied every frame for 1.5 s after each pass, without
-- comparing: GetPosition may not match what is drawn.
local MODEL_RECHECK_TIME = 1.5
local modelRecheck = CreateFrame("Frame", "ForeverUICharacterModelRecheck")
modelRecheck:Hide()
modelRecheck.rest = 0
local function applyModelSetting()
local model = _G["CharacterModelFrame"]
if not model then
return
end
local setting = ForeverUI.ModelSetting
if model.SetModelScale and setting.scale then
model:SetModelScale(setting.scale)
end
if model.SetPosition and setting.position then
model:SetPosition(setting.position[1], setting.position[2], setting.position[3])
end
end
modelRecheck:SetScript("OnUpdate", function(self, elapsed)
applyModelSetting()
self.rest = self.rest - (elapsed or 0)
if self.rest <= 0 then
self:Hide()
end
end)
local function placeModel()
local model = CharacterModelFrame
if not model then
return
end
-- camelot 70450: the model scene starts under the level strip
model:ClearAllPoints()
model:SetPoint("TOPLEFT", leftPane, "TOPLEFT", 0, MODEL_Y - STRIP_HEIGHT)
model:SetPoint("BOTTOMRIGHT", leftPane, "BOTTOMRIGHT", 0, MODEL_Y)
-- Now, then every frame until the model has loaded
applyModelSetting()
modelRecheck.rest = MODEL_RECHECK_TIME
modelRecheck:Show()
-- Rotation arrows, side by side, centered on the pane
local left = _G["CharacterModelFrameRotateLeftButton"]
local right = _G["CharacterModelFrameRotateRightButton"]
if left and right then
local half = left:GetWidth() / 2 + ROTATION_GAP / 2
left:ClearAllPoints()
left:SetPoint("TOP", leftPane, "TOP", -half, ROTATION_Y)
left:SetFrameLevel(model:GetFrameLevel() + 2)
right:ClearAllPoints()
right:SetPoint("TOP", leftPane, "TOP", half, ROTATION_Y)
right:SetFrameLevel(model:GetFrameLevel() + 2)
end
end
-- Resistances panel: always offset from its original anchor, kept aside, so it does not drift
local function placeResistances()
local frame = _G["CharacterResistanceFrame"]
if not frame or not frame.GetPoint or frame:GetNumPoints() == 0 then
return
end
if not frame.foreverAnchor then
local point, target, targetPoint, x, y = frame:GetPoint(1)
frame.foreverAnchor = { point, target, targetPoint, x or 0, y or 0 }
end
local a = frame.foreverAnchor
frame:ClearAllPoints()
frame:SetPoint(a[1], a[2], a[3], a[4] + RESISTANCES_X, a[5])
end
-- Forward declaration: skinSelector hooks it before its definition below; without it the name
-- would resolve to a nil global.
local updateSelectorStates
-- Third group: its category, from ForeverUIDB, else the first category the two client groups do
-- not show, casters' spell and hunters' ranged categories first
local THIRD_PREFERRED = {
HUNTER = "PLAYERSTAT_RANGED_COMBAT", MAGE = "PLAYERSTAT_SPELL_COMBAT",
PRIEST = "PLAYERSTAT_SPELL_COMBAT", WARLOCK = "PLAYERSTAT_SPELL_COMBAT",
DRUID = "PLAYERSTAT_SPELL_COMBAT", SHAMAN = "PLAYERSTAT_SPELL_COMBAT",
PALADIN = "PLAYERSTAT_SPELL_COMBAT",
}
local function thirdCategory()
ForeverUIDB = ForeverUIDB or {}
local saved = ForeverUIDB.statThirdCategory
if saved and _G[saved] then
return saved
end
local shown = {}
shown[GetCVar and GetCVar("playerStatLeftDropdown") or ""] = true
shown[GetCVar and GetCVar("playerStatRightDropdown") or ""] = true
local _, classToken = UnitClass("player")
local preferred = classToken and THIRD_PREFERRED[classToken]
if preferred and _G[preferred] and not shown[preferred] then
return preferred
end
for _, key in ipairs(STAT_CATEGORIES) do
if _G[key] and not shown[key] then
return key
end
end
return STAT_CATEGORIES[1]
end
-- PlayerStatFrameLeftDropDown_Initialize / _OnClick for the third group
local function initializeThird(self)
local current = thirdCategory()
for _, key in ipairs(STAT_CATEGORIES) do
if _G[key] then
local info = UIDropDownMenu_CreateInfo()
info.text = _G[key]
info.value = key
info.checked = (key == current) and 1 or nil
info.func = function(button)
ForeverUIDB = ForeverUIDB or {}
ForeverUIDB.statThirdCategory = button.value
UIDropDownMenu_SetSelectedValue(_G["ForeverUIStatFrameThirdDropDown"], button.value)
UpdatePaperdollStats("ForeverUIStatFrameThird", button.value)
end
UIDropDownMenu_AddButton(info)
end
end
end
-- Creates the third group once, in CharacterAttributesFrame like the client's two (the stats page
-- shows and hides that frame)
local function createThirdGroup()
local parent = _G["CharacterAttributesFrame"]
if not parent or _G["ForeverUIStatFrameThirdDropDown"] then
return
end
for i = 1, STAT_PER_GROUP do
CreateFrame("Frame", "ForeverUIStatFrameThird" .. i, parent, "StatFrameTemplate")
end
local dropdown = CreateFrame("Frame", "ForeverUIStatFrameThirdDropDown", parent, "UIDropDownMenuTemplate")
UIDropDownMenu_Initialize(dropdown, initializeThird)
UIDropDownMenu_SetSelectedValue(dropdown, thirdCategory())
if type(UpdatePaperdollStats) == "function" then
UpdatePaperdollStats("ForeverUIStatFrameThird", thirdCategory())
end
end
-- Gives a category selector camelot's dropdown look (Tpl.MenuStyle1); the menu stays the client's
-- Templates.lua loads after this file: the first pass, at load, comes too early; the skin is
-- applied again each time the sheet opens.
local function skinSelector(selector, width)
if selector.foreverButton or not (ForeverUI.Templates and ForeverUI.Templates.MenuStyle1) then
return
end
ForeverUI.Templates.MenuStyle1(selector, width, STAT_SELECTOR_HEIGHT)
end
-- Pressed state of the selectors while their list is open. The list closes through
-- ToggleDropDownMenu or CloseDropDownMenus, so the list's own OnShow and OnHide are hooked.
-- Only selectors with a foreverPress function react.
function updateSelectorStates()
local list = _G["DropDownList1"]
for _, group in ipairs(STAT_GROUPS) do
local selector = _G[group.selector]
if selector and selector.foreverPress then
selector.foreverPress(list and list:IsShown()
and UIDROPDOWNMENU_OPEN_MENU == selector)
end
end
end
ForeverUI.CharacterStatTabsState = updateSelectorStates
-- UIDropDownMenu_InitializeHelper sets the selector height to UIDROPDOWNMENU_BUTTON_HEIGHT * 2
-- on each open (40, as ForeverUI sets the constant to 20). The saved size is re-applied after
-- UIDropDownMenu_Initialize, which calls it through securecall.
local function reapplySelectorSize(frame)
for _, group in ipairs(STAT_GROUPS) do
local selector = _G[group.selector]
if selector == frame and selector.foreverSize then
selector:SetWidth(selector.foreverSize[1])
selector:SetHeight(selector.foreverSize[2])
return
end
end
end
ForeverUI.CharacterStatTabsSize = reapplySelectorSize
if hooksecurefunc and type(UIDropDownMenu_Initialize) == "function" then
hooksecurefunc("UIDropDownMenu_Initialize", reapplySelectorSize)
end
-- Category label: the CVar holds a key (PLAYERSTAT_BASE_STATS); the global of that name is the
-- text.
local function writeCategory(selector, key)
if selector and key and _G[key] and UIDropDownMenu_SetText then
UIDropDownMenu_SetText(selector, _G[key])
end
end
-- Background band of a stat row, created once. A child of the row, so it follows its
-- visibility.
local function applyRowBackground(row)
if row.foreverBackground then
return row.foreverBackground
end
local background = row:CreateTexture(nil, "BACKGROUND")
background:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
background:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
row.foreverBackground = background
-- The label is on the same layer and created first, and 3.3.5 has no sublevels: move it up so
-- the band stays behind.
local name = row:GetName()
local label = name and _G[name .. "Label"]
if label and label.SetDrawLayer then
label:SetDrawLayer("ARTWORK")
end
return background
end
-- Stripes the rows of each category, counting only the rows the client shows (it hides some
-- depending on the category).
local function stripeStatistics()
for _, group in ipairs(STAT_GROUPS) do
local rank = 0
for index = 1, STAT_PER_GROUP do
local row = _G[group.prefix .. index]
if row and row.foreverBackground and row:IsShown() then
rank = rank + 1
ForeverUI.SetAtlas(row.foreverBackground,
(rank % 2 == 1) and ATLAS_STAT_DARK or ATLAS_STAT_LIGHT,
true)
end
end
end
end
ForeverUI.CharacterStatStripes = stripeStatistics
-- Moves and widens the client's stat rows and selectors into the right pane. They are not
-- recreated: they carry the tooltips and category menus. Geometry only: ForeverUI.Panes shows
-- and hides them.
local function placeStatistics()
if not rightPane then
return
end
local width = RIGHT_PANE - 2 * STAT_MARGIN
local level = rightPane:GetFrameLevel() + 3
local y = -(STONE_HEIGHT + STAT_TOP)
createThirdGroup()
for _, group in ipairs(STAT_GROUPS) do
local selector = _G[group.selector]
if selector then
skinSelector(selector, width)
writeCategory(selector, group.third and thirdCategory() or (GetCVar and GetCVar(group.cvar)))
-- Size kept: the client resets it each time the menu opens
selector.foreverSize = { width, STAT_SELECTOR_HEIGHT }
selector:SetFrameLevel(level)
selector:SetWidth(selector.foreverSize[1])
selector:SetHeight(selector.foreverSize[2])
selector:ClearAllPoints()
selector:SetPoint("TOPLEFT", rightPane, "TOPLEFT", STAT_MARGIN, y - STAT_SELECTOR_TOP)
y = y - STAT_HEADER
end
for index = 1, STAT_PER_GROUP do
local row = _G[group.prefix .. index]
if row then
row:SetFrameLevel(level)
row:SetWidth(width)
row:ClearAllPoints()
row:SetPoint("TOPLEFT", rightPane, "TOPLEFT", STAT_MARGIN, y)
applyRowBackground(row)
y = y - STAT_STEP
end
end
y = y - STAT_GROUP_GAP
end
stripeStatistics()
end
-- Level and class line. 3.3.5 FontStrings have no SetParent, and the client's line belongs to
-- CharacterFrame, so it would draw under the panes (child frames). It is hidden and our own
-- line, in the rock strip at the top of the left pane, shows the text.
local function buildLevelStrip()
if leftPane.levelStrip then
return leftPane.levelStrip
end
local strip = CreateFrame("Frame", "ForeverUICharacterLevelStrip", leftPane)
strip:SetPoint("TOPLEFT", leftPane, "TOPLEFT", 0, 0)
strip:SetPoint("TOPRIGHT", leftPane, "TOPRIGHT", 0, 0)
strip:SetHeight(STRIP_HEIGHT)
-- Above the model (camelot: frameLevel 50 against the scene's 50, declared later), below the
-- collapse button and the rotation arrows (model + 2)
local model = _G["CharacterModelFrame"]
strip:SetFrameLevel(((model and model:GetFrameLevel()) or leftPane:GetFrameLevel()) + 1)
local rock = strip:CreateTexture(nil, "ARTWORK")
rock:SetTexture(ROCK_FILE)
rock:SetTexCoord(0, LEFT_PANE / ROCK_SIZE, 0, STRIP_HEIGHT / ROCK_SIZE)
rock:SetAllPoints(strip)
local edge = strip:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(edge, "_ui-frame-innertoptile", true)
edge:SetHeight(STRIP_EDGE)
edge:SetPoint("BOTTOMLEFT", strip, "BOTTOMLEFT", 0, 0)
edge:SetPoint("BOTTOMRIGHT", strip, "BOTTOMRIGHT", 0, 0)
local line = strip:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
local fontFile = line:GetFont()
line:SetFont(fontFile, LEVEL_FONT_SIZE, "OUTLINE")
line:SetPoint("CENTER", strip, "CENTER", 0, LEVEL_Y)
line:SetWidth(LEVEL_WIDTH)
line:SetHeight(LEVEL_HEIGHT)
line:SetJustifyH("CENTER")
line:SetJustifyV("MIDDLE")
strip.rock, strip.edge, strip.text = rock, edge, line
leftPane.levelStrip = strip
return strip
end
local function placeLevel()
local source = _G["CharacterLevelText"]
local strip = buildLevelStrip()
if source then
source:Hide()
end
-- camelot PLAYER_LEVEL_NO_SPEC: "Level %d", then the class in its color. 3.3.5 has neither that
-- string nor colorStr: built from UNIT_LEVEL_TEMPLATE and RAID_CLASS_COLORS, so it stays
-- localized.
local level = UnitLevel and UnitLevel("player")
-- not "UnitClass and UnitClass(...)": "and" keeps only the first value, the token is lost
local className, classToken
if UnitClass then
className, classToken = UnitClass("player")
end
local text = ""
if level and className then
local color = RAID_CLASS_COLORS and classToken and RAID_CLASS_COLORS[classToken]
local name = className
if color then
local function byte(v) return math.floor(v * 255 + 0.5) end
name = string.format("|cff%02x%02x%02x%s|r", byte(color.r), byte(color.g), byte(color.b), className)
end
text = string.format(UNIT_LEVEL_TEMPLATE, level) .. " " .. name
elseif source then
text = source:GetText() or ""
end
strip.text:SetText(text)
end
-- Creates a right pane tab: camelot's frame art, an icon under it and a selection mark.
-- SetCheckedTexture only takes a file path in 3.3.5, not an atlas, so the mark is our own
-- texture.
local function createPaneTab(name, tooltip, onClick)
local tab = CreateFrame("Button", name, rightPane)
tab:SetWidth(PANE_TAB)
tab:SetHeight(PANE_TAB)
tab:SetFrameLevel(rightPane:GetFrameLevel() + 3)
local icon = tab:CreateTexture(nil, "BACKGROUND")
icon:SetWidth(PANE_TAB_ICON)
icon:SetHeight(PANE_TAB_ICON)
icon:SetPoint("CENTER", tab, "CENTER", 0, 0)
tab.icon = icon
local selected = tab:CreateTexture(nil, "BORDER")
ForeverUI.SetAtlas(selected, ATLAS_PANE_TAB_SELECTED)
selected:SetPoint("CENTER", tab, "CENTER", 0, 0)
selected:Hide()
tab.selected = selected
local frame = tab:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(frame, ATLAS_PANE_TAB)
frame:SetPoint("CENTER", tab, "CENTER", 0, 0)
tab:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(tooltip)
GameTooltip:Show()
end)
tab:SetScript("OnLeave", function() GameTooltip:Hide() end)
if onClick then
tab:SetScript("OnClick", onClick)
end
return tab
end
-- Creates the three right pane tabs once and refreshes the stats tab portrait. ForeverUI.Panes
-- remembers each host's open page, so collapsing and expanding keeps it.
local function placePaneTabs()
if not rightPane then
return
end
if not rightPane.statsTab then
-- The tabs only select the right host's page and move the mark; ForeverUI.Panes shows and
-- hides. The gear manager stays the client's.
local function choose(page)
-- Legacy callers pass a boolean: true is stats, false is equipment
if page == true then
page = "stats"
elseif page == false then
page = "equipment"
end
local markers = {
stats = rightPane.statsTab,
equipment = rightPane.equipmentTab,
titles = rightPane.titlesTab,
}
for name, tab in pairs(markers) do
if tab then
if name == page then
tab.selected:Show()
else
tab.selected:Hide()
end
end
end
ForeverUI.Panes.ShowPage("right", page)
end
rightPane.selectPage = choose
rightPane.statsTab = createPaneTab("ForeverUICharacterStatsTab",
CHARACTER_INFO,
function() choose("stats") end)
rightPane.statsTab.selected:Show()
rightPane.equipmentTab = createPaneTab(
"ForeverUICharacterGearTab", EQUIPMENT_MANAGER,
function() choose("equipment") end)
rightPane.equipmentTab.icon:SetTexture(GEAR_MANAGER_ICON)
rightPane.equipmentTab.icon:SetTexCoord(
GEAR_MANAGER_ICON_COORD[1], GEAR_MANAGER_ICON_COORD[2],
GEAR_MANAGER_ICON_COORD[3], GEAR_MANAGER_ICON_COORD[4])
rightPane.titlesTab = createPaneTab(
"ForeverUICharacterTitlesTab", L.CHARACTERFRAME_TITLES,
function() choose("titles") end)
rightPane.titlesTab.icon:SetTexture(TITLES_ICON)
rightPane.titlesTab.icon:SetTexCoord(
TITLES_ICON_COORD[1], TITLES_ICON_COORD[2],
TITLES_ICON_COORD[3], TITLES_ICON_COORD[4])
-- camelot PaperDollFrame_UpdateSidebarTabLayout: the middle tab holds the anchor
-- at TOP (0, -5) of the bar, the others touch its sides. Stats left, gear manager
-- middle, titles right.
rightPane.equipmentTab:SetPoint("TOP", rightPane, "TOP",
0, PANE_TAB_Y)
rightPane.statsTab:SetPoint("RIGHT", rightPane.equipmentTab,
"LEFT", 0, 0)
rightPane.titlesTab:SetPoint("LEFT", rightPane.equipmentTab,
"RIGHT", 0, 0)
end
-- Portrait cropped as in camelot; re-applied since the client redraws it on appearance changes
if SetPortraitTexture then
SetPortraitTexture(rightPane.statsTab.icon, "player")
rightPane.statsTab.icon:SetTexCoord(TAB_PORTRAIT_COORD[1],
TAB_PORTRAIT_COORD[2], TAB_PORTRAIT_COORD[3], TAB_PORTRAIT_COORD[4])
end
-- The client's gear manager button is hidden: the tab opens the panel
local old = _G["GearManagerToggleButton"]
if old then
old:Hide()
end
end
-- Side tab screens. 3.3.5 lists its five screens in CHARACTERFRAME_SUBFRAMES and
-- CharacterFrame_ShowSubFrame shows one of them; the screen name is the ForeverUI.Panes group.
-- The right pane stays open on every tab, like camelot (UpdateRightPaneHeader hides only the
-- stone band); the character's stone band, page tabs and level line go with its group.
local CHARACTER_SCREEN = "PaperDollFrame"
local SIMPLE_SCREENS = {
{ group = "PetPaperDollFrame", id = "pet", module = "PetTab" },
{ group = "ReputationFrame", id = "reputation", module = "ReputationTab" },
{ group = "SkillFrame", id = "skills", module = "SkillsTab" },
{ group = "TokenFrame", id = "currency", module = "TokensTab" },
}
local contentsDeclared = false
-- Registers every screen's left and right content with ForeverUI.Panes, once
local function declareContents()
if contentsDeclared or not rightPane then
return
end
contentsDeclared = true
local Panes = ForeverUI.Panes
-- Left pane, character group: the model, slots and resistances are PaperDollFrame children the
-- client shows and hides; this entry only marks the left host as used.
Panes.Register({
host = "left", group = CHARACTER_SCREEN, id = "character",
build = function() return nil, {} end,
})
-- The four other client screens are sized for the narrower original window: they are anchored
-- to the left host when built, unless their module lays them out.
for _, screen in ipairs(SIMPLE_SCREENS) do
Panes.Register({
host = "left", group = screen.group, id = screen.id,
build = function(host)
-- A screen with its own module (ForeverUI[screen.module]) builds its own content
local module = ForeverUI[screen.module or ""]
if module and module.Build then
return module.Build(host)
end
local frame = _G[screen.group]
if not frame then
return nil, {}
end
frame:ClearAllPoints()
frame:SetPoint("TOPLEFT", host, "TOPLEFT", 0, 0)
return nil, { frame }
end,
})
end
-- PvP: 3.3.5 has a separate toplevel window (PVPParentFrame, 384 x 512) where camelot has a
-- sheet tab; ForeverUI.PvPTab moves it into the left pane. Reparenting is safe since it carries
-- none of our level settings, and needed since a toplevel frame stays above everything.
Panes.Register({
host = "left", group = PVP_SCREEN, id = "pvp",
build = function(host)
return ForeverUI.PvPTab.Build(host)
end,
})
-- Statistics: camelot's StatisticsFrame (StatisticsTab.lua) on the client's achievement
-- statistics
Panes.Register({
host = "left", group = STATS_SCREEN, id = "stats",
build = function(host)
return ForeverUI.StatisticsTab.Build(host)
end,
})
-- Mounts: the chosen one in 3D on the left, the list on the right (MountsTab.lua)
Panes.Register({
host = "left", group = MOUNTS_SCREEN, id = "mounts",
build = function(host)
return ForeverUI.MountsTab.Build(host)
end,
})
Panes.Register({
host = "right", group = MOUNTS_SCREEN, id = MOUNTS_SCREEN .. ".droit",
build = function(host)
return ForeverUI.MountsTab.BuildRight(host)
end,
})
-- The PvP tab shows its rank details on the right; statistics leave the right pane empty
Panes.Register({
host = "right", group = PVP_SCREEN, id = PVP_SCREEN .. ".droit",
build = function(host)
return ForeverUI.PvPTab.BuildRight(host)
end,
})
Panes.Register({
host = "right", group = STATS_SCREEN, id = STATS_SCREEN .. ".droit",
build = function() return nil, {} end,
})
-- Right pane furniture of the character group: shown whatever the page, hidden on other tabs
-- (camelot: UpdateRightPaneHeader, HidePaperDollRightPane).
Panes.Furniture("right", CHARACTER_SCREEN, {
rightPane.stone,
rightPane.statsTab,
rightPane.equipmentTab,
rightPane.titlesTab,
})
-- The level strip belongs to the character group too (camelot: PaperDollFrame's own child)
Panes.Furniture("left", CHARACTER_SCREEN, { buildLevelStrip() })
-- The four other client screens keep the right pane: a module may fill it (reputation shows the
-- selected faction); an empty page still keeps the host open and the window wide.
for _, screen in ipairs(SIMPLE_SCREENS) do
Panes.Register({
host = "right", group = screen.group, id = screen.id .. ".droit",
build = function(host)
local module = ForeverUI[screen.module or ""]
if module and module.BuildRight then
return module.BuildRight(host)
end
return nil, {}
end,
})
end
-- Right pane pages of the character group; the first registered opens by default
Panes.Register({
host = "right", group = CHARACTER_SCREEN, id = "stats",
build = function()
-- The page owns CharacterAttributesFrame, parent of the two selectors and the twelve
-- rows, not the rows: showing each row brought back those the client hides
-- (UpdatePaperdollStats hides the sixth row of the ranged category), with the content
-- of the category shown before. Hidden, it also hides the rows the client shows over
-- the gear or titles page.
return nil, { _G["CharacterAttributesFrame"] }
end,
})
Panes.Register({
host = "right", group = CHARACTER_SCREEN, id = "equipment",
build = function()
if ForeverUI.EquipmentPane then
ForeverUI.EquipmentPane.Apply()
end
local panel = ForeverUI.EquipmentPane and ForeverUI.EquipmentPane.Frame()
-- GearManagerDialog is a child of the panel but hidden by default: the page shows it
return panel, { _G["GearManagerDialog"] }
end,
})
-- Titles page (camelot has no titles tab, see Titles.lua)
Panes.Register({
host = "right", group = CHARACTER_SCREEN, id = "titles",
build = function(host)
if ForeverUI.TitlesPane and ForeverUI.TitlesPane.Build then
return ForeverUI.TitlesPane.Build(host)
end
return nil, {}
end,
})
-- Open the group of the screen the client currently shows
local isOpen = CHARACTER_SCREEN
for _, name in ipairs(CHARACTERFRAME_SUBFRAMES or { CHARACTER_SCREEN }) do
local frame = _G[name]
if frame and frame:IsShown() then
isOpen = name
break
end
end
Panes.ShowGroup(isOpen)
end
-- Follows the client's tab switch (CharacterFrame_ShowSubFrame): ForeverUI.Panes hides what
-- belongs to the previous screen, including our right pane, which is not a PaperDollFrame child.
local function trackScreen(name)
if not contentsDeclared then
return
end
ForeverUI.Panes.ShowGroup(name)
if ForeverUI.CharacterUpdateActiveTab then
ForeverUI.CharacterUpdateActiveTab()
end
if ForeverUI.CharacterApplyPanes then
ForeverUI.CharacterApplyPanes(CharacterFrame)
end
end
if hooksecurefunc and type(_G["CharacterFrame_ShowSubFrame"]) == "function" then
hooksecurefunc("CharacterFrame_ShowSubFrame", trackScreen)
end
-- Shows the active marker on the tab of the open ForeverUI.Panes group; call it whenever the
-- group changes.
local function updateActiveTab()
local isOpen = ForeverUI.Panes.CurrentGroup()
for key, tab in pairs(tabs) do
if tab.foreverActive then
local ownScreen = TAB_SCREEN[key] or TAB_CREATED_SCREENS[key]
if ownScreen and ownScreen == isOpen then
tab.foreverActive:Show()
else
tab.foreverActive:Hide()
end
end
end
end
ForeverUI.CharacterUpdateActiveTab = updateActiveTab
-- Opens one of our screens. CharacterFrame_ShowSubFrame only knows its five frames (an unknown
-- name hides them all), so the five are hidden here and our group is shown.
local function openOwnScreen(group)
-- PetPaperDollFrame is protected (secure companion buttons): the addon cannot hide it in
-- combat, so while it is the screen shown our screens open only out of combat
for _, name in ipairs(CHARACTERFRAME_SUBFRAMES or {}) do
local frame = _G[name]
if frame and frame:IsShown() and InCombatLockdown() and frame:IsProtected() then
return
end
end
for _, name in ipairs(CHARACTERFRAME_SUBFRAMES or {}) do
local frame = _G[name]
if frame and frame:IsShown() then
frame:Hide()
end
end
-- PanelTemplates_SelectTab disables the selected client tab and only PanelTemplates_DeselectTab
-- (from ToggleCharacter) enables it again. Our screens use neither, so the client tabs are
-- enabled here; otherwise the previous one stays unclickable.
for index = 1, (CharacterFrame and CharacterFrame.numTabs) or 5 do
local tab = _G["CharacterFrameTab" .. index]
if tab and tab.Enable and not tab.isDisabled then
tab:Enable()
end
end
ForeverUI.Panes.ShowGroup(group)
updateActiveTab()
if ForeverUI.CharacterApplyPanes then
ForeverUI.CharacterApplyPanes(CharacterFrame)
end
end
-- The PvP window is a tab now: its key binding and micro button call TogglePVPFrame, whose
-- ToggleFrame(PVPParentFrame) no longer shows anything. Open the sheet on the PvP tab instead.
if hooksecurefunc and type(_G["TogglePVPFrame"]) == "function" then
hooksecurefunc("TogglePVPFrame", function()
if not CharacterFrame then
return
end
if not CharacterFrame:IsShown() and ShowUIPanel then
ShowUIPanel(CharacterFrame)
end
openOwnScreen(PVP_SCREEN)
end)
end
-- Right pane collapse, kept across sessions in ForeverUIDB (camelot uses a CVar).
-- Forward declaration: reapplyPosition is defined below; without it the name would resolve to
-- a nil global when the button is clicked.
local reapplyPosition
local function isPaneCollapsed()
ForeverUIDB = ForeverUIDB or {}
return ForeverUIDB.rightPaneCollapsed == true
end
local function updateCollapseButton(frame)
local button = frame and frame.foreverCollapse
if not button then
return
end
local collapsed = isPaneCollapsed()
local art = collapsed and COLLAPSE_ART.collapsed or COLLAPSE_ART.expanded
button:SetNormalTexture(art[1])
button:SetPushedTexture(art[2])
if collapsed then
button.tooltip = L.CHARACTERFRAME_SHOW_DETAILS
else
button.tooltip = L.CHARACTERFRAME_HIDE_DETAILS
end
end
-- Applies the right pane state and the window width. The right host is shown only when it has
-- a page for the open group and is not collapsed; the window is then WIDTH wide, else
-- LEFT_PANE. The side tabs are anchored to the window's right edge and follow it.
local function applyCollapse(frame)
if not frame then
return
end
local Panes = ForeverUI.Panes
local hasContentToShow = Panes.HasContent("right", Panes.CurrentGroup())
local isOpen = hasContentToShow and not isPaneCollapsed()
Panes.SetHostShown("right", isOpen)
-- CharacterFrame is protected (secure companion buttons in its pet screen): resized out of
-- combat only, and only when the width changes. PLAYER_REGEN_ENABLED runs applySkin again.
local width = isOpen and WIDTH or LEFT_PANE
if not (InCombatLockdown() and frame:IsProtected()) and math.abs((frame:GetWidth() or 0) - width) > 0.5 then
frame:SetWidth(width)
end
-- No collapse button on a tab without a right pane
local button = frame.foreverCollapse
if button then
if hasContentToShow then button:Show() else button:Hide() end
end
updateCollapseButton(frame)
if ForeverUI.UpdatePanelCorners then
ForeverUI.UpdatePanelCorners(frame)
end
end
ForeverUI.CharacterApplyPanes = applyCollapse
local function togglePane()
local frame = CharacterFrame
if not frame or InCombatLockdown() then
return
end
ForeverUIDB = ForeverUIDB or {}
ForeverUIDB.rightPaneCollapsed = not isPaneCollapsed()
if PlaySound then
PlaySound(isPaneCollapsed() and "igCharacterInfoClose" or "igCharacterInfoOpen")
end
applyCollapse(frame)
reapplyPosition(frame)
-- The tooltip text changed under a still cursor: show it again, like camelot
local button = frame.foreverCollapse
if button and GameTooltip:GetOwner() == button then
GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
GameTooltip:SetText(button.tooltip)
GameTooltip:Show()
end
end
local function placeCollapseButton(frame)
if not leftPane then
return
end
local button = frame.foreverCollapse
if not button then
button = CreateFrame("Button", "ForeverUICharacterRightPaneToggle", frame)
button:SetWidth(COLLAPSE_SIZE)
button:SetHeight(COLLAPSE_SIZE)
button:SetPoint("TOPRIGHT", leftPane, "TOPRIGHT", COLLAPSE_X, COLLAPSE_Y)
button:SetHighlightTexture(COLLAPSE_HOVER, "ADD")
button:SetScript("OnClick", togglePane)
button:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(self.tooltip)
GameTooltip:Show()
end)
button:SetScript("OnLeave", function() GameTooltip:Hide() end)
frame.foreverCollapse = button
end
-- Above the metal and the model: the button sits in the corner the model covers, and at an
-- equal level the model takes the mouse. Two levels above the model, like the rotation
-- arrows, and never below the skin layer.
local level = frame:GetFrameLevel() + ART_LEVEL + 2
local model = _G["CharacterModelFrame"]
if model and model.GetFrameLevel then
local wanted = model:GetFrameLevel() + 2
if wanted > level then
level = wanted
end
end
button:SetFrameLevel(level)
updateCollapseButton(frame)
end
-- Side tabs: a 64 x 384 column right of the frame, 30 px down (camelot CharacterFrameModeTabs).
-- Only visible tabs are stacked, each on the previous visible one: 3.3.5 hides the pet tab
-- without a pet (PetPaperDollFrame_UpdateIsAvailable), which would leave a gap in a column.
-- Restacked each time the client changes that state.
local function stackTabs()
if not tabBar then
return
end
local previous
for _, key in ipairs(TAB_ORDER) do
local tab = tabs[key]
if tab and tab:IsShown() then
tab:ClearAllPoints()
if previous then
tab:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, TAB_GAP)
else
tab:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 0, 0)
end
previous = tab
end
end
end
-- The pet tab is for the pet alone: 3.3.5 also offers it for companions and mounts. Without a
-- pet it is hidden as the client hides it when there is nothing to show, then the column closes up.
local function updatePetTab()
local tab, screen = _G["CharacterFrameTab2"], _G["PetPaperDollFrame"]
if tab and screen and not HasPetUI() then
screen.hidden = true
tab:Hide()
-- ToggleCharacter hides the protected pet screen: not from here in combat
if screen:IsVisible() and not InCombatLockdown() then
ToggleCharacter("PaperDollFrame")
end
end
stackTabs()
end
if hooksecurefunc and type(_G["PetPaperDollFrame_UpdateIsAvailable"]) == "function" then
hooksecurefunc("PetPaperDollFrame_UpdateIsAvailable", updatePetTab)
end
-- A client tab shown again runs CharacterFrame_TabBoundsCheck, which resizes the five client
-- tabs to their text, empty here: they get the side tab width back.
local function keepTabWidth()
for key, tab in pairs(tabs) do
if type(key) == "number" and tab.foreverSkinned then
tab:SetWidth(TAB_W)
end
end
end
if hooksecurefunc and type(_G["CharacterFrame_TabBoundsCheck"]) == "function" then
hooksecurefunc("CharacterFrame_TabBoundsCheck", keepTabWidth)
end
-- Creates one of our side tabs, skinned like the client's.
-- key: tabs table key; name: frame name; group: ForeverUI.Panes group it opens
local function createSideTab(key, name, icon, tooltip, group)
if tabs[key] then
return tabs[key]
end
local tab = CreateFrame("Button", name, tabBar)
tab:SetWidth(TAB_W)
tab:SetHeight(TAB_H)
local background = tab:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(background, ATLAS.tab, true)
background:SetAllPoints(tab)
tab.foreverBackground = background
local image = tab:CreateTexture(nil, "ARTWORK")
image:SetWidth(SIDE_TAB_ICON)
image:SetHeight(SIDE_TAB_ICON)
image:SetPoint("CENTER", tab, "CENTER", TAB_ICON_X, 0)
image:SetTexCoord(TAB_PORTRAIT_INSET, 1 - TAB_PORTRAIT_INSET,
TAB_PORTRAIT_INSET, 1 - TAB_PORTRAIT_INSET)
image:SetTexture(icon)
tab.foreverIcon = image
-- Active marker over the icon (camelot OVERLAY); the hover stays on its own HIGHLIGHT layer
local active = tab:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(active, ATLAS.tabActive, true)
active:SetAllPoints(tab)
active:Hide()
tab.foreverActive = active
local hover = tab:CreateTexture(nil, "HIGHLIGHT")
ForeverUI.SetAtlas(hover, ATLAS.tabHover, true)
hover:SetAllPoints(tab)
tab:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(tooltip)
GameTooltip:Show()
end)
tab:SetScript("OnLeave", function() GameTooltip:Hide() end)
tab:SetScript("OnClick", function()
if PlaySound then
PlaySound("igCharacterInfoTab")
end
openOwnScreen(group)
end)
tab.foreverSkinned = true
tabs[key] = tab
return tab
end
-- The mounts tab shows only with mounts, as the pet tab only with a pet. Losing the last one
-- while its screen is open brings back the character page.
local function updateMountsTab()
local tab = tabs.mounts
if not tab then
return
end
if GetNumCompanions("MOUNT") > 0 then
tab:Show()
else
tab:Hide()
if ForeverUI.Panes.CurrentGroup() == MOUNTS_SCREEN and CharacterFrame:IsShown() then
ToggleCharacter("PaperDollFrame")
end
end
end
-- Creates our PvP, statistics and mounts side tabs, sets their level and the faction icon
local function placeCreatedTabs()
if not tabBar then
return
end
local faction = (UnitFactionGroup and UnitFactionGroup("player")) or "Alliance"
createSideTab("pvp", "ForeverUICharacterTabPvP",
TAB_ICONS.pvp[faction] or TAB_ICONS.pvp.Alliance,
PVP, PVP_SCREEN)
createSideTab("stats", "ForeverUICharacterTabStats",
TAB_ICONS.stats, STATISTICS, STATS_SCREEN)
createSideTab("mounts", "ForeverUICharacterTabMounts",
TAB_ICONS.mounts, MOUNTS, MOUNTS_SCREEN)
updateMountsTab()
-- Same level rule as the client tabs: below the metal
local tabsLevel = tabBar:GetFrameLevel()
for _, key in ipairs({ "pvp", "stats", "mounts" }) do
if tabs[key] then
tabs[key]:SetFrameLevel(tabsLevel + 1)
end
end
-- The tab may be created before the client knows the faction: set the icon on each pass
if tabs.pvp and faction then
tabs.pvp.foreverIcon:SetTexture(
TAB_ICONS.pvp[faction] or TAB_ICONS.pvp.Alliance)
end
end
-- A mount learned or forgotten, or the list arriving after login
local mountsWatcher = CreateFrame("Frame")
for _, event in ipairs({ "PLAYER_ENTERING_WORLD", "COMPANION_LEARNED", "COMPANION_UNLEARNED",
"COMPANION_UPDATE" }) do
mountsWatcher:RegisterEvent(event)
end
mountsWatcher:SetScript("OnEvent", function()
updateMountsTab()
stackTabs()
end)
-- Skins the client side tabs and stacks all side tabs in the column right of the frame
local function layoutTabs(frame)
if not tabBar then
tabBar = CreateFrame("Frame", "ForeverUICharacterModeTabs", frame)
tabBar:SetWidth(TABS_W)
tabBar:SetHeight(TABS_H)
tabBar:SetPoint("TOPLEFT", frame, "TOPRIGHT", TABS_X, TABS_Y)
end
-- The tab art has a tongue on its left that slides under the window; its hover and active
-- marker would overlap the right pane's metal. The bar sits at the frame level and the tabs
-- one level above: over the panel background, below the skin layer (ART_LEVEL).
local tabsLevel = frame:GetFrameLevel()
tabBar:SetFrameLevel(tabsLevel)
local previous
local index = 1
while true do
local tab = _G["CharacterFrameTab" .. index]
if not tab then
break
end
if not tab.foreverSkinned then
sweepTextures(tab)
for _, method in ipairs({ "GetNormalTexture", "GetPushedTexture",
"GetDisabledTexture", "GetHighlightTexture" }) do
local texture = tab[method] and tab[method](tab)
if texture then
texture:SetAlpha(0)
end
end
local background = tab:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(background, ATLAS.tab, true)
background:SetAllPoints(tab)
tab.foreverBackground = background
local icon = tab:CreateTexture(nil, "ARTWORK")
icon:SetWidth(SIDE_TAB_ICON)
icon:SetHeight(SIDE_TAB_ICON)
icon:SetPoint("CENTER", tab, "CENTER", TAB_ICON_X, 0)
icon:SetTexCoord(TAB_PORTRAIT_INSET, 1 - TAB_PORTRAIT_INSET,
TAB_PORTRAIT_INSET, 1 - TAB_PORTRAIT_INSET)
icon:Hide()
tab.foreverIcon = icon
local active = tab:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(active, ATLAS.tabActive, true)
active:SetAllPoints(tab)
active:Hide()
tab.foreverActive = active
local hover = tab:CreateTexture(nil, "HIGHLIGHT")
ForeverUI.SetAtlas(hover, ATLAS.tabHover, true)
hover:SetAllPoints(tab)
-- The icon replaces the text when there is one; otherwise the client text
-- stays, centered like the icon
local text = _G["CharacterFrameTab" .. index .. "Text"]
if text then
text:ClearAllPoints()
text:SetPoint("CENTER", tab, "CENTER", TAB_ICON_X, 0)
text:SetWidth(TAB_W - 10)
end
if TAB_ICONS[index] then
icon:SetTexture(TAB_ICONS[index])
icon:Show()
if text then
text:Hide()
text:SetText("")
end
end
tab.foreverSkinned = true
end
tab:SetWidth(TAB_W)
tab:SetHeight(TAB_H)
tab:SetFrameLevel(tabsLevel + 1)
tabs[index] = tab
index = index + 1
end
placeCreatedTabs()
stackTabs()
updateActiveTab()
-- The character tab shows the class icon
local first = tabs[1]
local _, token = UnitClass("player")
if first and first.foreverIcon and token then
first.foreverIcon:SetTexture(CLASS_TAB .. token)
-- Keep the crop: the mask was baked for it
first.foreverIcon:SetTexCoord(TAB_PORTRAIT_INSET, 1 - TAB_PORTRAIT_INSET,
TAB_PORTRAIT_INSET, 1 - TAB_PORTRAIT_INSET)
first.foreverIcon:Show()
local text = _G["CharacterFrameTab1Text"]
if text then
text:Hide()
text:SetText("")
end
end
end
-- The client's panel manager re-places the window on each show: re-apply the saved position
-- after it
function reapplyPosition(frame)
local position = savedPositions()["sheet"]
if not position or InCombatLockdown() then
return
end
frame:ClearAllPoints()
frame:SetPoint(position.point, UIParent, position.relativePoint or position.point,
position.x or 0, position.y or 0)
end
-- GearManagerDialog's OnShow and OnHide call UpdateUIPanelPositions, which puts the sheet back
-- at its default place. Hook UpdateUIPanelPositions itself, which covers every caller.
if hooksecurefunc and type(UpdateUIPanelPositions) == "function" then
hooksecurefunc("UpdateUIPanelPositions", function()
if CharacterFrame and CharacterFrame:IsShown() then
reapplyPosition(CharacterFrame)
end
end)
end
-- Skins and lays out the whole sheet; runs on each show and client update, out of combat
local function applySkin()
local frame = CharacterFrame
if not frame or InCombatLockdown() or not libraryPresent() then
return
end
-- The width is set by applyCollapse at the end, after the stats and gear panel are placed;
-- otherwise they would show again.
frame:SetHeight(HEIGHT)
if not frame.foreverSkinned then
-- Order matters: the panes are child frames and cover their parent's regions, so the panel
-- art lives in a higher child frame, like camelot's NineSlice.
clearLegacyArt()
setupPanes(frame)
ForeverUI.SetPanelArt(frame, {
topLeftCorner = PORTRAIT_CORNER,
corners = PANEL_CORNERS,
level = ART_LEVEL,
})
frame.foreverSkinned = true
-- Brought to the front when shown or clicked (WindowStack.lua)
ForeverUI.WindowStack.register("sheet", frame, function()
-- The arena team details open next to the sheet (PvPArena.lua): a click on
-- them counts as a click on the sheet
return { frame, tabBar, _G["ForeverUIArenaTeamDetails"] }
end)
end
clearLegacyArt()
if ForeverUI.UpdatePanelCorners then
ForeverUI.UpdatePanelCorners(frame)
end
placePortrait(frame)
placeTitle(frame)
reapplyPosition(frame)
placeCloseButton(frame)
placeModel()
placeResistances()
placeStatistics()
placeLevel()
placePaneTabs()
-- The gear manager panel is built with its page by ForeverUI.Panes, not on each pass; its
-- list updates itself.
placeSlots()
layoutTabs(frame)
placeCollapseButton(frame)
declareContents()
applyCollapse(frame)
end
-- The client calls UpdatePaperdollStats(prefix, key) when a category changes: update the
-- matching header label
local refreshingThird = false
local function trackCategory(prefix, key)
-- The client refreshes its two groups on every stat change (PaperDollFrame_UpdateStats): the
-- third follows, from the same call
if not refreshingThird and (prefix == "PlayerStatFrameLeft" or prefix == "PlayerStatFrameRight")
and _G["ForeverUIStatFrameThirdDropDown"] then
refreshingThird = true
UpdatePaperdollStats("ForeverUIStatFrameThird", thirdCategory())
refreshingThird = false
end
for _, group in ipairs(STAT_GROUPS) do
if group.prefix == prefix then
writeCategory(_G[group.selector], key)
-- The client has just shown and hidden rows: restripe the visible ones. The rows it
-- shows while another page is open stay invisible: the stats page hides their parent.
stripeStatistics()
return
end
end
end
ForeverUI.CharacterSheet = { Apply = applySkin, Tabs = tabs }
if hooksecurefunc and type(_G["UpdatePaperdollStats"]) == "function" then
hooksecurefunc("UpdatePaperdollStats", trackCategory)
end
if hooksecurefunc then
for _, funcName in ipairs({ "CharacterFrame_ShowSubFrame", "PaperDollFrame_OnShow",
"PaperDollFrame_SetLevel", "CharacterFrame_Collapse", "CharacterFrame_Expand" }) do
if type(_G[funcName]) == "function" then
hooksecurefunc(funcName, applySkin)
end
end
end
local listener = CreateFrame("Frame", "ForeverUICharacterWatcher")
listener:RegisterEvent("PLAYER_ENTERING_WORLD")
listener:RegisterEvent("PLAYER_REGEN_ENABLED")
listener:RegisterEvent("UNIT_INVENTORY_CHANGED")
listener:RegisterEvent("UNIT_PORTRAIT_UPDATE")
listener:RegisterEvent("UNIT_MODEL_CHANGED")
-- A title change reaches UnitPVPName only after the server answers (UNIT_NAME_UPDATE). The
-- event fires for every unit: only the title strip is redrawn, and only for the player.
listener:RegisterEvent("UNIT_NAME_UPDATE")
listener:SetScript("OnEvent", function(_self, event, unit)
if event == "UNIT_NAME_UPDATE" then
if unit == "player" and CharacterFrame then
placeTitle(CharacterFrame)
end
return
end
-- The UNIT_ events fire for every unit (party, raid, target...): only the player's concern
-- the sheet, and only while it is shown (its OnShow hook lays it out again anyway)
if unit and (unit ~= "player" or not (CharacterFrame and CharacterFrame:IsShown())) then
return
end
applySkin()
end)
if CharacterFrame then
CharacterFrame:HookScript("OnShow", applySkin)
end
applySkin()
-- Debug helpers: find the frames that take the mouse over a point
local function nameOf(frame)
if not frame then
return "?"
end
return (frame.GetName and frame:GetName()) or L.CHARACTERFRAME_DEBUG_UNNAMED
end
local function containsPoint(frame, x, y)
if not frame.GetLeft then
return false
end
local g, d, h, b = frame:GetLeft(), frame:GetRight(), frame:GetTop(), frame:GetBottom()
return g and d and h and b and x >= g and x <= d and y >= b and y <= h
end
-- Collects the visible, mouse-enabled frames containing (x, y) in the subtree of frame
local function browse(frame, x, y, matches, depth)
if not frame or depth > 6 then
return
end
if frame.IsVisible and frame:IsVisible() and frame.IsMouseEnabled
and frame:IsMouseEnabled() and containsPoint(frame, x, y) then
matches[#matches + 1] = frame
end
if frame.GetChildren then
local children = { frame:GetChildren() }
for _, child in ipairs(children) do
browse(child, x, y, matches, depth + 1)
end
end
end
-- Whether a tab is enabled: IsEnabled returns a number in 3.3.5, and 0 is true in Lua.
local function isActive(tab)
if not tab or not tab.IsEnabled then
return true
end
local state = tab:IsEnabled()
return state ~= nil and state ~= false and state ~= 0
end
-- /fui tabs: why a side tab ignores clicks. Prints the client screens shown, then each tab's
-- state and the mouse-enabled frames covering it, then the frame under the cursor for 5 s.
function ForeverUI.CharacterTabsDebug()
local say = function(text)
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. text)
end
if not CharacterFrame or not CharacterFrame:IsShown() then
say(L.CHARACTERFRAME_DEBUG_TABS_OPEN_FIRST)
return
end
-- Client screens still shown: if one is while our screen is open, ToggleCharacter closes the
-- window instead of switching tabs.
local shownItems = {}
for _, name in ipairs(CHARACTERFRAME_SUBFRAMES or {}) do
local frame = _G[name]
if frame and frame:IsShown() then
shownItems[#shownItems + 1] = name
end
end
say(string.format(L.CHARACTERFRAME_DEBUG_SHOWN_SCREENS,
(#shownItems > 0) and table.concat(shownItems, ", ") or L.CHARACTERFRAME_DEBUG_NONE,
tostring(ForeverUI.Panes and ForeverUI.Panes.CurrentGroup
and ForeverUI.Panes.CurrentGroup())))
local column = {}
for _, key in ipairs(TAB_ORDER) do
local tab
if type(key) == "number" then
tab = _G["CharacterFrameTab" .. key]
else
tab = tabs[key]
end
if tab then
column[#column + 1] = tab
end
end
for _, tab in ipairs(column) do
local left = tab.GetLeft and tab:GetLeft()
say(string.format(L.CHARACTERFRAME_DEBUG_TAB_STATE,
nameOf(tab), tostring(tab:IsVisible()),
tostring(tab:IsMouseEnabled()),
tostring(isActive(tab)),
tostring(tab:GetFrameStrata()), tab:GetFrameLevel(),
tostring(tab:GetScript("OnClick") ~= nil)))
if left then
local x = (tab:GetLeft() + tab:GetRight()) / 2
local y = (tab:GetTop() + tab:GetBottom()) / 2
local matches = {}
-- From UIParent: a covering frame need not be related to the tab
browse(UIParent, x, y, matches, 0)
local names = {}
for _, frame in ipairs(matches) do
if frame ~= tab then
names[#names + 1] = string.format("%s(%d)", nameOf(frame),
frame:GetFrameLevel())
end
end
if #names > 0 then
DEFAULT_CHAT_FRAME:AddMessage(L.CHARACTERFRAME_DEBUG_COVERED_BY
.. table.concat(names, ", "))
end
end
end
local lookout = ForeverUI.mouseLookout
if lookout and lookout.SetScript then
lookout.rest = 5
lookout.last = nil
lookout:Show()
end
say(L.CHARACTERFRAME_DEBUG_HOVER_TABS)
end
-- Close button probe helpers. Edges are compared in screen pixels (edge x effective scale)
-- since frames have different scales. Visibility counts, not the mouse: the metal draws over
-- the button without taking the mouse.
local function containsScreenPoint(frame, x, y)
if not frame.GetLeft or not frame.GetEffectiveScale then return false end
local g, d, h, b = frame:GetLeft(), frame:GetRight(), frame:GetTop(), frame:GetBottom()
if not (g and d and h and b) then return false end
local s = frame:GetEffectiveScale()
return x >= g * s and x <= d * s and y >= b * s and y <= h * s
end
local function visibleUnder(frame, x, y, matches, depth)
if not frame or depth > 10 then return end
if frame.IsVisible and frame:IsVisible() and containsScreenPoint(frame, x, y) then
matches[#matches + 1] = frame
end
if frame.GetChildren then
for _, child in ipairs({ frame:GetChildren() }) do
visibleUnder(child, x, y, matches, depth + 1)
end
end
end
-- /fui close: records the close button, its textures, the skin and title levels, the highest
-- accepted frame level and every visible frame over its center into
-- ForeverUIDB.closeButtonProbe (written to disk on the next /reload).
function ForeverUI.CharacterCloseDebug()
local r = {}
local function note(...)
local t = {}
for i = 1, select("#", ...) do t[i] = tostring((select(i, ...))) end
r[#r + 1] = table.concat(t, " ")
end
local c, b = CharacterFrame, _G["CharacterFrameCloseButton"]
note("feuille shown", c:IsShown(), "level", c:GetFrameLevel(), "strata", c:GetFrameStrata(),
"alpha", c:GetAlpha())
if b then
note("croix shown", b:IsShown(), "visible", b:IsVisible(), "alpha", b:GetAlpha(),
"level", b:GetFrameLevel(), "strata", b:GetFrameStrata(), "parent", nameOf(b:GetParent()),
"w", b:GetWidth(), "h", b:GetHeight(), "left", b:GetLeft(), "top", b:GetTop(),
"scale", b:GetEffectiveScale(), "mouse", b:IsMouseEnabled())
for _, state in ipairs({ "Normal", "Pushed", "Disabled", "Highlight" }) do
local read = b["Get" .. state .. "Texture"]
local t = read and read(b)
if t then
note(state, "tex", t:GetTexture(), "coords", table.concat({ t:GetTexCoord() }, ","),
"shown", t:IsShown(), "alpha", t:GetAlpha(), "layer", t.GetDrawLayer and t:GetDrawLayer(),
"w", t:GetWidth(), "h", t:GetHeight(), "left", t:GetLeft(), "top", t:GetTop())
else
note(state, "nil")
end
end
end
local h, title = c.foreverSkinLayer, c.foreverTitleStrip
note("habillage level", h and h:GetFrameLevel(), "visible", h and h:IsVisible())
note("titre level", title and title:GetFrameLevel())
-- highest accepted frame level: probe with a very high value
local probe = ForeverUI.levelProbe or CreateFrame("Frame")
ForeverUI.levelProbe = probe
local ok = pcall(probe.SetFrameLevel, probe, 100000)
note("niveau-max", ok, probe:GetFrameLevel())
if b and b:GetLeft() then
local s = b:GetEffectiveScale()
local x = (b:GetLeft() + b:GetRight()) / 2 * s
local y = (b:GetTop() + b:GetBottom()) / 2 * s
local matches = {}
visibleUnder(UIParent, x, y, matches, 0)
for _, f in ipairs(matches) do
note("containsPoint", nameOf(f), "parent", nameOf(f:GetParent()), "strata", f:GetFrameStrata(),
"level", f:GetFrameLevel())
end
end
ForeverUIDB = ForeverUIDB or {}
ForeverUIDB.closeButtonProbe = r
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. L.CHARACTERFRAME_DEBUG_CLOSE_SAVED)
end
-- /fui character: prints the head slot state and every mouse-enabled frame over its center
-- (strata and level decide the click), then the frame under the cursor for 5 s.
function ForeverUI.CharacterSheetDebug()
-- Each host's content first: most display faults of this window show there
if ForeverUI.Panes and ForeverUI.Panes.Report then
for _, row in ipairs(ForeverUI.Panes.Report()) do
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. L.CHARACTERFRAME_DEBUG_PANES .. row)
end
end
local say = function(text)
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. text)
end
local button = _G["CharacterHeadSlot"]
if not button or not button:GetLeft() then
say(L.CHARACTERFRAME_DEBUG_SHEET_OPEN_FIRST)
return
end
say(string.format(L.CHARACTERFRAME_DEBUG_HEAD_STATE,
button:GetWidth(), button:GetHeight(), tostring(button:GetFrameStrata()),
button:GetFrameLevel(), tostring(button:IsMouseEnabled()),
tostring(button:IsVisible())))
-- A button may also have lost its scripts or been disabled
DEFAULT_CHAT_FRAME:AddMessage(string.format(
L.CHARACTERFRAME_DEBUG_HEAD_SCRIPTS,
tostring(button:GetScript("OnClick") ~= nil),
tostring(button:GetScript("OnDragStart") ~= nil),
tostring(button:GetScript("OnReceiveDrag") ~= nil),
tostring(not button.IsEnabled or button:IsEnabled() and true or false)))
local x = (button:GetLeft() + button:GetRight()) / 2
local y = (button:GetTop() + button:GetBottom()) / 2
local matches = {}
browse(CharacterFrame, x, y, matches, 0)
DEFAULT_CHAT_FRAME:AddMessage(L.CHARACTERFRAME_DEBUG_COVERING)
for _, frame in ipairs(matches) do
DEFAULT_CHAT_FRAME:AddMessage(string.format(L.CHARACTERFRAME_DEBUG_COVERING_ROW,
nameOf(frame), tostring(frame:GetFrameStrata()), frame:GetFrameLevel()))
end
local lookout = ForeverUI.mouseLookout
if not lookout then
lookout = CreateFrame("Frame", "ForeverUICharacterMouseWatch")
lookout:Hide()
ForeverUI.mouseLookout = lookout
end
lookout.rest = 5
lookout.last = nil
lookout:SetScript("OnUpdate", function(self, elapsed)
self.rest = self.rest - elapsed
if self.rest <= 0 then
self:Hide()
self:SetScript("OnUpdate", nil)
DEFAULT_CHAT_FRAME:AddMessage(L.CHARACTERFRAME_DEBUG_WATCH_END)
return
end
local sub = GetMouseFocus and GetMouseFocus()
local name = nameOf(sub)
if name ~= self.last then
self.last = name
DEFAULT_CHAT_FRAME:AddMessage(L.CHARACTERFRAME_DEBUG_UNDER_CURSOR .. name)
end
end)
lookout:Show()
say(L.CHARACTERFRAME_DEBUG_HOVER_SLOT)
end
-- /fui model: in-game tuning of the model scale and position (camelot uses a scene, so there
-- is no source value). Sets the values and prints the result, without a reload.
-- /fui model current values
-- /fui model scale 0.8 SetModelScale
-- /fui model position -5 0 0 SetPosition(depth, side, height)
-- /fui model position default back to the client's position
function ForeverUI.CharacterModelTune(argument)
local say = function(text)
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. text)
end
local model = _G["CharacterModelFrame"]
if not model then
return
end
local key, rest = string.match(argument or "", "^(%S*)%s*(.*)$")
key = string.lower(key or "")
local setting = ForeverUI.ModelSetting
if key == "scale" then
setting.scale = tonumber(rest) or setting.scale
elseif key == "position" then
if string.lower(rest) == "default" then
setting.position = nil
model:RefreshUnit()
else
local x, y, z = string.match(rest, "^(%-?[%d%.]+)%s+(%-?[%d%.]+)%s+(%-?[%d%.]+)$")
if x then
setting.position = { tonumber(x), tonumber(y), tonumber(z) }
else
say(L.CHARACTERFRAME_MODEL_POSITION_USAGE)
return
end
end
elseif key ~= "" then
say(L.CHARACTERFRAME_MODEL_USAGE)
return
end
if ForeverUI.CharacterSheet then
ForeverUI.CharacterSheet.Apply()
end
local scale = model.GetModelScale and model:GetModelScale()
local x, y, z = nil, nil, nil
if model.GetPosition then
x, y, z = model:GetPosition()
end
say(string.format(L.CHARACTERFRAME_MODEL_STATE,
tostring(scale), tostring(x), tostring(y), tostring(z)))
end