Files
Claude 9c96c28590 ForeverUI : talents, pas de reconstruction à chaque sort, question de fermeture juste
Fenêtre ouverte, chaque début ou fin d'incantation (CURRENT_SPELL_CAST_CHANGED)
relisait tout l'arbre, quelque 85 talents et leurs tables ; seul le bouton
Activer en dépend, comme PlayerTalentFrameActivateButton_Update.

Points en attente : la question de fermeture partait aussi quand la carte
plein écran refusait la réouverture (elle apparaissait ensuite pour une
fenêtre fermée), et quand seul UIParent se masquait (barbier), qui effaçait
de plus le mode inspection.

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

1772 lines
69 KiB
Lua

-- ForeverUI: camelot's talent screen (blizzard_playerspells, blizzard_sharedtalentui): three
-- trees, nodes, arrows, gates, unspent points, spec and pet side tabs, glyphs and inspection.
-- PlayerTalentFrame (Blizzard_TalentUI, loaded on demand) stays the panel that N, the micro
-- button and ToggleTalentFrame open and close; its art is hidden and this screen sits inside it.
-- Pending changes use the WotLK talent preview in place of C_Traits; there is no Reset, since
-- WotLK only unlearns at a trainer. The tooltip is WotLK's (SetTalent).
ForeverUI = ForeverUI or {}
local T = {}
ForeverUI.Talents = T
local G = {
width = 1218, height = 708, top = -116,
-- margins of the fit to the screen (blizzard_playerspellsregistration: checkFitExtraWidth,
-- checkFitExtraHeight)
fitExtraW = 200, fitExtraH = 140,
pageW = 1212, pageH = 681, pageBottom = 4,
backgroundW = 605, backgroundH = 701, backgroundTop = -70, backgroundBottom = 36,
frameLeft = -2, frameTop = 4, frameCorner = 24,
-- Horizontal bar 46 higher than camelot's -4 (its line lands on the top of the art). Vertical
-- separators 60 below the bars' center (camelot) but exactly on the art transitions, at thirds
-- (x 404 and 808; camelot: +-95 from the bars' centers). Headers 5 px under the bar line
-- (camelot: -100), their small separator 5 px under the ring (camelot: BOTTOM (60, -20)).
-- Unspent points 26 higher (camelot: -6), box scaled 0.8, digits 24 instead of 32.
separatorTop = 42, verticalY = -60, transitionsX = { 404, 808 },
headerX = 140, headerGap = 400, headerY = -38.5, headerSide = 48, smallSeparatorY = -8.5,
pointsX = -20, pointsY = 20, pointsScale = 0.8, pointsSize = 24,
-- Nodes fill the page height (page coordinates): first row 10 px under the small separator
-- line, last row 40 from the bottom (camelot's bottomPadding); each tree centered in its
-- column, between the vertical separators; steps keep camelot's ratios (columns 1.5 nodes,
-- rows 1.4 nodes).
firstTop = -123, lastBottom = -641, tiers = 11,
columnsX = { 202, 606, 1010 },
columnRatio = 1.5, rowRatio = 1.4,
node = 40, icon = 36, squareShadow = 39, roundShadow = 38,
rankX = 11, rankY = 4, rankSize = 16,
line = 6, arrowHeadW = 14, arrowHeadH = 12,
gateW = 124, gateH = 40, gateIconW = 84, gateIconH = 14, gateX = -12,
closeX = -2, closeY = 1, redSide = 24,
portraitX = -5, portraitY = 7, portraitSide = 62,
titleX1 = 58, titleX2 = -24, titleY = -1, titleH = 20,
pointsPerTier = 5,
petPointsPerTier = 3,
-- Window reduced to one tree (pet, glyphs): the first column
narrowPage = 404,
-- Glyphs: center of the socket star in GlyphFrame (Blizzard_GlyphUI.xml: sockets 121 from
-- (178, -238.5)), placed at the center of the art frame; center of the drawn circle in its
-- piece. The parchment covers the whole art frame; the gold corners sit inside the inner
-- frame's line (talents-inner-frame-c60: the line ends 7 px in at the sides, 5 at top/bottom).
glyphCenterX = 178, glyphCenterY = -238.5,
circleCenterX = 179.65, circleCenterY = 202.41,
cornerIndentX = 7, cornerIndentY = 5,
-- Glow pulse (GlyphFramePulse: 0.1 s rise, 1.5 s fall); the sparkle is enlarged, since the
-- star has rays
glowRise = 0.1, glowFall = 1.5, sparkScale = 2.5,
-- Concentric circles: faint, slowly counter-rotating, lit by filled sockets; max alpha, and
-- fade speed per second
circlesAlpha = 0.35, circlesFade = 0.5,
-- Side tabs (same build as CharacterFrame.lua)
tabsX = 1, tabsY = -30, tabSide = 55, tabGap = -2, tabIcon = 50,
tabIconX = -3, tabCrop = 0.03125, checkMarkW = 20, checkMarkH = 15, lockW = 10, lockH = 14,
applyW = 164, applyH = 22, applyY = 8, cancelSide = 25, cancelX = 14,
cancelIconW = 21, cancelIconH = 20, glowW = 25, glowH = 49, glowX = 12,
-- Inspection is more compact: 260-wide columns instead of 404 (nodes fit within +-95 of the
-- center). The small separator is cut 5 from its column's edge; each tree keeps its own third
-- of the art, cropped by the same amount on each side; no bottom strip (Apply / Undo), no
-- gates.
inspectionColumn = 260, smallSeparatorMargin = 5,
}
-- Scale that fits the tiers: 10 steps + one node = the available height
G.scale = (G.firstTop - G.lastBottom) / (40 * ((G.tiers - 1) * G.rowRatio + 1))
G.rowStep = G.rowRatio * 40 * G.scale
G.columnStep = G.columnRatio * 40 * G.scale
-- Vertical separators: 60 below the bars' center (camelot)
G.verticalTop = G.separatorTop - 28 + G.verticalY
-- Inspection: centers of the three columns, and their transitions
G.inspectionColumns = { G.inspectionColumn / 2, 1.5 * G.inspectionColumn, 2.5 * G.inspectionColumn }
G.inspectionTransitions = { G.inspectionColumn, 2 * G.inspectionColumn }
local METAL = {
{ key = "topLeft", name = "ui-frame-portraitmetal-cornertopleft", point = "TOPLEFT", x = -13, y = 16 },
{ key = "topRight", name = "ui-frame-metal-cornertopright", point = "TOPRIGHT", x = 2, y = 16 },
{ key = "bottomLeft", name = "ui-frame-metal-cornerbottomleft", point = "BOTTOMLEFT", x = -13, y = -8 },
{ key = "bottomRight", name = "ui-frame-metal-cornerbottomright", point = "BOTTOMRIGHT", x = 2, y = -8 },
}
local SEP = string.char(92)
local FONT = "Fonts" .. SEP .. "FRIZQT__.TTF"
local ROCK = "interface" .. SEP .. "ForeverUI" .. SEP .. "framegeneral" .. SEP .. "ui-background-rock"
local PORTRAIT = "Interface" .. SEP .. "ForeverUI" .. SEP .. "talents" .. SEP .. "portrait_"
local L = ForeverUI.L
local TEXT = {
title = TALENTS,
unspent = L.TALENTS_UNSPENT_POINTS, -- UNSPENT_POINTS
gate = L.TALENTS_GATE_TOOLTIP, -- TALENT_FRAME_GATE_TOOLTIP_FORMAT
apply = L.TALENTS_APPLY_CHANGES, -- TALENT_FRAME_APPLY_BUTTON_TEXT
cancel = L.TALENTS_UNDO_PENDING_CHANGES, -- TALENT_FRAME_DISCARD_CHANGES_BUTTON_TOOLTIP
activate = ACTIVATE, -- TALENT_SPEC_ACTIVATE
confirmClose = L.TALENTS_CONFIRM_CLOSE, -- TALENT_FRAME_CONFIRM_CLOSE
active = L.TALENTS_SPEC_ACTIVE, -- TALENT_SPEC_ACTIVE
locked = LOCKED, -- TALENT_SPEC_LOCKED
primary = L.TALENTS_SPEC_PRIMARY, -- DUAL_SPEC_PRIMARY
secondary = L.TALENTS_SPEC_SECONDARY, -- DUAL_SPEC_SECONDARY
pet = PET,
glyphs = GLYPHS, -- GLYPHS
primaryGlyphs = TALENT_SPEC_PRIMARY_GLYPH,
secondaryGlyphs = TALENT_SPEC_SECONDARY_GLYPH,
inspection = L.TALENTS_INSPECT_TITLE, -- TALENTS_INSPECT_FORMAT
}
-- Spec icon (TalentFrame_UpdateSpecInfoCache): the main tree's, the hybrid one, or the
-- default one; baked (tools/bake_masks.py)
local TAB_ICONS = "Interface" .. SEP .. "ForeverUI" .. SEP .. "TabIcons" .. SEP
local HYBRID_ICON = "ability_dualwieldspecialization"
local DEFAULT_ICON = "ability_marksmanship"
local GLYPHS_ICON = "inv_inscription_tradeskill01"
-- Glyphs: the two baked sheets (tools/bake_glyphs.py) and their pieces: sheet, x, y, width,
-- height in pixels; displayed width and height
local GLYPHS_FOLDER = "Interface" .. SEP .. "ForeverUI" .. SEP .. "glyphes" .. SEP
local GLYPHS_RECTS = {
["parchment"] = { "glyphes-fond", 0, 0, 539, 793, 404.00, 595.00 },
["circle"] = { "glyphes-fond", 543, 0, 481, 550, 360.84, 412.52 },
["coin-hg"] = { "glyphes-fond", 0, 801, 89, 94, 66.62, 70.25 },
["coin-hd"] = { "glyphes-fond", 93, 801, 89, 94, 66.62, 70.25 },
["coin-bg"] = { "glyphes-fond", 186, 801, 88, 90, 65.75, 67.62 },
["coin-bd"] = { "glyphes-fond", 278, 801, 87, 90, 65.50, 67.62 },
["anneau-runique"] = { "glyphes-lueurs", 94, 94, 444, 444, 332.72, 332.72 },
["anneau-epines"] = { "glyphes-lueurs", 692, 56, 256, 256, 191.95, 191.95 },
["anneau-double"] = { "glyphes-lueurs", 669, 405, 145, 145, 108.57, 108.57 },
["anneau-orange"] = { "glyphes-lueurs", 851, 372, 145, 145, 108.87, 108.87 },
["star"] = { "glyphes-lueurs", 851, 521, 64, 64, 48.00, 48.00 },
["rayon-0"] = { "glyphes-lueurs", 0, 636, 92, 376, 68.80, 282.00 },
["rayon-1"] = { "glyphes-lueurs", 100, 636, 372, 268, 279.00, 201.00 },
["rayon-2"] = { "glyphes-lueurs", 480, 636, 372, 268, 279.00, 201.00 },
}
local GLYPHS_SHEET = 1024
-- Shows a piece of the glyph sheets on texture t; size: also apply its displayed size
local function applyGlyph(t, key, size)
local r = GLYPHS_RECTS[key]
t:SetTexture(GLYPHS_FOLDER .. r[1])
t:SetTexCoord(r[2] / GLYPHS_SHEET, (r[2] + r[4]) / GLYPHS_SHEET,
r[3] / GLYPHS_SHEET, (r[3] + r[5]) / GLYPHS_SHEET)
if size then
t:SetWidth(r[6])
t:SetHeight(r[7])
end
end
-- Concentric circles: one ring per socket pair, from center to edge, in the order WotLK opens
-- them (levels 15/15, 30/50, 70/80); each filled glyph of the pair lights half the ring.
-- loop: seconds per turn, the sign gives the direction.
local CIRCLES = {
{ key = "anneau-double", sockets = { 1, 2 }, loop = 90 },
{ key = "anneau-epines", sockets = { 3, 4 }, loop = -120 },
{ key = "anneau-runique", sockets = { 5, 6 }, loop = 180 },
}
-- Rotates a ring texture: its coords turn around the center of its piece (the sheet leaves
-- room around each ring for the corners of the rotated square: tools/bake_glyphs.py)
local function rotateRing(t, key, angle)
local r = GLYPHS_RECTS[key]
local cx, cy = (r[2] + r[4] / 2) / GLYPHS_SHEET, (r[3] + r[5] / 2) / GLYPHS_SHEET
local h = r[4] / 2 / GLYPHS_SHEET
local c, s = math.cos(angle), math.sin(angle)
local function p(ox, oy) return cx + ox * c - oy * s, cy + ox * s + oy * c end
local ulx, uly = p(-h, -h)
local llx, lly = p(-h, h)
local urx, ury = p(h, -h)
local lrx, lry = p(h, h)
t:SetTexCoord(ulx, uly, llx, lly, urx, ury, lrx, lry)
end
-- Activation spells (TALENT_ACTIVATION_SPELLS, Constants.lua)
local ACTIVATION_SPELLS = { 63645, 63644 }
local COLOR = {
gray = { 0.5, 0.5, 0.5 }, -- DISABLED_FONT_COLOR
green = { 0.1, 1, 0.1 }, -- GREEN_FONT_COLOR
yellow = { 1, 0.82, 0 }, -- NORMAL_FONT_COLOR
gate = { 1, 0.64, 0.56 },
}
-- Backgrounds: one per class; death knights get one per tree
local BACKGROUND_DK = { "talents-background-deathknight-blood", "talents-background-deathknight-frost",
"talents-background-deathknight-unholy" }
local function atlas(t, name, keep)
return ForeverUI.SetAtlas(t, name, keep)
end
-- Sets an atlas at a given size (the LOGICAL sizes of UiTextureAtlasMember, which the atlas
-- table lacks for talents.blp); l: width; h: height (default l)
local function sizedAtlas(t, name, l, h)
ForeverUI.SetAtlas(t, name, true)
t:SetWidth(l)
t:SetHeight(h or l)
end
local function color(fs, c)
fs:SetTextColor(c[1], c[2], c[3])
end
-- Silences a WotLK region or frame: texture, alpha, visibility, mouse. The WotLK code shows
-- again what is only hidden.
local function suppress(f)
if not f then return end
if f.SetTexture then f:SetTexture(nil) end
f:SetAlpha(0)
if f.EnableMouse then f:EnableMouse(false) end
f:Hide()
end
-- ------------------------------------------------------------ Frame
-- Camelot's red close button on WotLK's PlayerTalentFrameCloseButton
local function skinCloseButton(book)
local close = PlayerTalentFrameCloseButton
if not close then return end
close:SetWidth(G.redSide)
close:SetHeight(G.redSide)
close:SetHitRectInsets(0, 0, 0, 0)
for _, state in ipairs({ { "Normal", "redbutton-exit" }, { "Pushed", "redbutton-exit-pressed" },
{ "Highlight", "redbutton-highlight" } }) do
local e = ForeverUI.AtlasEntry(state[2])
local t = close["Get" .. state[1] .. "Texture"](close)
if not t and e then
close["Set" .. state[1] .. "Texture"](close, e[1])
t = close["Get" .. state[1] .. "Texture"](close)
end
if t then
atlas(t, state[2], true)
t:ClearAllPoints()
t:SetAllPoints(close)
end
end
close:ClearAllPoints()
close:SetPoint("TOPRIGHT", book, "TOPRIGHT", G.closeX, G.closeY)
end
-- Highest frame level of a frame and all its descendants
local function highestLevel(frame)
local n = frame:GetFrameLevel()
for _, child in ipairs({ frame:GetChildren() }) do
local m = highestLevel(child)
if m > n then n = m end
end
return n
end
-- The close button is a sibling of the book, not a child, so its level is reset on every open.
-- Blizzard_GlyphUI puts it just above GlyphFrame on load (GlyphFrame_OnEvent, ADDON_LOADED),
-- which is under the book: it is raised above both.
function T.raiseCloseButton()
local close, book = PlayerTalentFrameCloseButton, T.book
if not close or not book then return end
close:SetFrameStrata(book:GetFrameStrata())
local top = highestLevel(book)
if GlyphFrame then top = math.max(top, highestLevel(GlyphFrame)) end
close:SetFrameLevel(top + 1)
end
-- Window frame: rock background, metal border, class portrait, title banner, close button
local function buildFrame(book)
local rock = book:CreateTexture(nil, "BACKGROUND")
rock:SetTexture(ROCK, true)
if rock.SetHorizTile then rock:SetHorizTile(true) rock:SetVertTile(true) end
rock:SetPoint("TOPLEFT", book, "TOPLEFT", 2, -21)
rock:SetPoint("BOTTOMRIGHT", book, "BOTTOMRIGHT", -2, 2)
local metal = CreateFrame("Frame", nil, book)
metal:SetAllPoints(book)
metal:SetFrameLevel(book:GetFrameLevel() + 20)
local p = {}
for _, corner in ipairs(METAL) do
local t = metal:CreateTexture(nil, "OVERLAY")
atlas(t, corner.name)
t:SetPoint(corner.point, metal, corner.point, corner.x, corner.y)
p[corner.key] = t
end
local function edge(name, a1, c1, r1, a2, c2, r2)
local t = metal:CreateTexture(nil, "OVERLAY")
atlas(t, name)
t:SetPoint(a1, c1, r1)
t:SetPoint(a2, c2, r2)
end
edge("_ui-frame-metal-edgetop", "TOPLEFT", p.topLeft, "TOPRIGHT", "TOPRIGHT", p.topRight, "TOPLEFT")
edge("_ui-frame-metal-edgebottom", "BOTTOMLEFT", p.bottomLeft, "BOTTOMRIGHT", "BOTTOMRIGHT", p.bottomRight, "BOTTOMLEFT")
edge("!ui-frame-metal-edgeleft", "TOPLEFT", p.topLeft, "BOTTOMLEFT", "BOTTOMLEFT", p.bottomLeft, "TOPLEFT")
edge("!ui-frame-metal-edgeright", "TOPRIGHT", p.topRight, "BOTTOMRIGHT", "BOTTOMRIGHT", p.bottomRight, "TOPRIGHT")
-- Portrait: the class icon, baked round (tools/bake_masks.py)
local portraitFrame = CreateFrame("Frame", nil, book)
portraitFrame:SetAllPoints(book)
portraitFrame:SetFrameLevel(book:GetFrameLevel() + 19)
local portrait = portraitFrame:CreateTexture(nil, "OVERLAY")
portrait:SetWidth(G.portraitSide)
portrait:SetHeight(G.portraitSide)
portrait:SetPoint("TOPLEFT", book, "TOPLEFT", G.portraitX, G.portraitY)
local _, className = UnitClass("player")
portrait:SetTexture(PORTRAIT .. string.lower(className or "warrior"))
book.portrait = portrait
local banner = CreateFrame("Frame", nil, book)
banner:SetFrameLevel(book:GetFrameLevel() + 21)
banner:SetHeight(G.titleH)
banner:SetPoint("TOPLEFT", book, "TOPLEFT", G.titleX1, G.titleY)
banner:SetPoint("TOPRIGHT", book, "TOPRIGHT", G.titleX2, G.titleY)
local title = banner:CreateFontString(nil, "OVERLAY", "GameFontNormal")
title:SetPoint("TOP", banner, "TOP", 0, -5)
title:SetText(TEXT.title)
book.title = title
book.banner = banner
skinCloseButton(book)
end
-- Camelot's UIPanelButtonTemplate: three pieces of UI-Panel-Button-* (as in QuestLog.lua)
local PANEL = "Interface" .. SEP .. "Buttons" .. SEP .. "UI-Panel-Button-"
function T.panelButton(parent, name, text, width)
local b = CreateFrame("Button", name, parent)
b:SetWidth(width)
b:SetHeight(G.applyH)
local function piece(u1, u2)
local t = b:CreateTexture(nil, "BACKGROUND")
t:SetTexCoord(u1, u2, 0, 0.6875)
return t
end
local g = piece(0, 0.09375)
g:SetWidth(12)
g:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0)
g:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", 0, 0)
local d = piece(0.53125, 0.625)
d:SetWidth(12)
d:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0)
d:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0)
local m = piece(0.09375, 0.53125)
m:SetPoint("TOPLEFT", g, "TOPRIGHT", 0, 0)
m:SetPoint("BOTTOMRIGHT", d, "BOTTOMLEFT", 0, 0)
b.pieces = { g, m, d }
local function state(suffix)
for _, t in ipairs(b.pieces) do t:SetTexture(PANEL .. suffix) end
end
state("Up")
local fs = b:CreateFontString(nil, "ARTWORK")
fs:SetFontObject(GameFontNormal)
fs:SetPoint("CENTER", b, "CENTER", 0, 0)
b:SetFontString(fs)
b:SetNormalFontObject(GameFontNormal)
b:SetHighlightFontObject(GameFontHighlight)
b:SetDisabledFontObject(GameFontDisable)
b:SetText(text)
b:SetHighlightTexture(PANEL .. "Highlight")
local s = b:GetHighlightTexture()
if s then
s:SetTexCoord(0, 0.625, 0, 0.6875)
s:SetBlendMode("ADD")
end
b.active = true
b:SetScript("OnMouseDown", function(self) if self.active then state("Down") end end)
b:SetScript("OnMouseUp", function(self) if self.active then state("Up") end end)
function b:Activate(yes)
self.active = yes and true or false
if yes then
self:Enable()
state("Up")
else
self:Disable()
state("Disabled")
end
end
return b
end
-- Talents-Background-c60 tiled across the page width; when shorter (inspection, no bottom
-- strip) it is cropped at the bottom, not squashed
local function placeStone(width, height)
local background = T.background
local e = ForeverUI.AtlasEntry("talents-background-c60")
local down = e and (e[4] + (e[5] - e[4]) * (height or G.backgroundH) / G.backgroundH)
local x, n = 0, 0
while x < width do
n = n + 1
local t = background.stones[n] or background:CreateTexture(nil, "BACKGROUND")
background.stones[n] = t
local l = math.min(G.backgroundW, width - x)
if e then
t:SetTexture(e[1])
t:SetTexCoord(e[2], e[2] + (e[3] - e[2]) * l / G.backgroundW, e[4], down)
end
t:SetWidth(l)
t:ClearAllPoints()
t:SetPoint("TOPLEFT", background, "TOPLEFT", x, 0)
t:SetPoint("BOTTOMLEFT", background, "BOTTOMLEFT", x, 0)
t:Show()
x = x + l
end
for i = n + 1, #background.stones do background.stones[i]:Hide() end
end
-- ------------------------------------------------------------ Page
local function buildPage(book)
local page = CreateFrame("Frame", "ForeverUITalentsPage", book)
page:SetWidth(G.pageW)
page:SetHeight(G.pageH)
page:SetPoint("BOTTOM", book, "BOTTOM", 0, G.pageBottom)
T.page = page
-- Talents-Background-c60 repeats horizontally (horizTile): 3.3.5 cannot tile an atlas
-- region, so copies are placed side by side
local background = CreateFrame("Frame", nil, page)
background:SetHeight(G.backgroundH)
background:SetPoint("BOTTOMLEFT", page, "BOTTOMLEFT", 0, 0)
background:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 0)
background.stones = {}
T.background = background
-- Class background ONE LEVEL ABOVE the stone: two sibling frames at the same level have no
-- guaranteed draw order, and the opaque stone could cover the art
background:SetFrameLevel(page:GetFrameLevel() + 1)
local className = CreateFrame("Frame", nil, page)
className:SetFrameLevel(background:GetFrameLevel() + 1)
className:SetPoint("TOPLEFT", background, "TOPLEFT", 0, G.backgroundTop)
className:SetPoint("BOTTOMRIGHT", background, "BOTTOMRIGHT", 0, G.backgroundBottom)
className.textures = {}
T.className = className
-- Inner frame, nine-slice (its corners are ornate)
local frame = CreateFrame("Frame", nil, page)
frame:SetPoint("TOPLEFT", className, "TOPLEFT", G.frameLeft, G.frameTop)
frame:SetPoint("BOTTOMRIGHT", className, "BOTTOMRIGHT", -G.frameLeft, -G.frameTop)
frame:SetFrameLevel(page:GetFrameLevel() + 2)
ForeverUI.CreateNineSlice(frame, "talents-inner-frame-c60", G.frameCorner, { 0, 0, 0, 0 }, "OVERLAY")
T.frame = frame
-- Separators
local left = frame:CreateTexture(nil, "OVERLAY")
atlas(left, "talents-divider-left-c60")
T.barLeft = left
left:SetPoint("RIGHT", frame, "CENTER", 0, 0)
left:SetPoint("TOP", frame, "TOP", 0, G.separatorTop)
local right = frame:CreateTexture(nil, "OVERLAY")
atlas(right, "talents-divider-right-c60")
T.barRight = right
right:SetPoint("LEFT", frame, "CENTER", 0, 0)
right:SetPoint("TOP", frame, "TOP", 0, G.separatorTop)
-- Vertical separators: 60 below the bars' center (camelot), on the art transitions (the frame
-- starts at -2 from the page)
local verticals = {}
for i, x in ipairs(G.transitionsX) do
local v = frame:CreateTexture(nil, "OVERLAY")
atlas(v, "talents-divider-vertical-c60")
v:SetPoint("TOP", frame, "TOPLEFT", x - G.frameLeft, G.verticalTop)
verticals[i] = v
end
T.verticals = verticals
-- Unspent points
local points = CreateFrame("Frame", nil, frame)
points:SetWidth(1)
points:SetHeight(1)
points:SetPoint("TOPRIGHT", frame, "TOPRIGHT", G.pointsX, G.pointsY)
local box = points:CreateTexture(nil, "ARTWORK")
atlas(box, "talents-square-box-c60")
local k = G.pointsScale
box:SetWidth(box:GetWidth() * k)
box:SetHeight(box:GetHeight() * k)
box:SetPoint("RIGHT", points, "RIGHT", 0, 0)
local caption = points:CreateFontString(nil, "ARTWORK", "SystemFont_Shadow_Med1")
caption:SetJustifyH("RIGHT")
caption:SetPoint("RIGHT", box, "LEFT", 60 * k, 0)
caption:SetText(TEXT.unspent)
-- The search aligns on this label: its right edge, from the frame's right edge
points.caption = caption
points.captionRight = G.pointsX - box:GetWidth() + 60 * k
local count = points:CreateFontString(nil, "ARTWORK")
count:SetFont(FONT, G.pointsSize)
count:SetShadowOffset(2, -2)
count:SetShadowColor(0, 0, 0, 1)
count:SetPoint("CENTER", box, "RIGHT", (-4 - 24) * k, 0)
points.count = count
T.points = points
-- The three headers
T.headers = {}
for i = 1, 3 do
local h = CreateFrame("Frame", "ForeverUITalentsHeader" .. i, frame)
h:SetWidth(G.headerSide)
h:SetHeight(G.headerSide)
h:SetPoint("CENTER", frame, "TOPLEFT", G.headerX + (i - 1) * G.headerGap, G.headerY)
h:SetFrameLevel(frame:GetFrameLevel() + 5)
local icon = h:CreateTexture(nil, "BORDER")
icon:SetWidth(36)
icon:SetHeight(36)
icon:SetPoint("CENTER", h, "CENTER", -3, 3)
local ring = h:CreateTexture(nil, "OVERLAY")
atlas(ring, "talents-main-ring-c60")
ring:SetPoint("CENTER", icon, "CENTER", 0, 0)
local name = h:CreateFontString(nil, "OVERLAY")
name:SetFont(FONT, 16, "OUTLINE")
name:SetTextColor(1, 1, 1)
name:SetPoint("LEFT", h, "LEFT", 54, 2)
local textBox = h:CreateTexture(nil, "OVERLAY")
atlas(textBox, "talents-main-ring-box-c60")
textBox:SetPoint("CENTER", h, "CENTER", 14, -12)
local spent = h:CreateFontString(nil, "OVERLAY", "GameFontNormal")
spent:SetPoint("CENTER", textBox, "CENTER", 0, 0)
local line = h:CreateTexture(nil, "OVERLAY")
atlas(line, "talents-small-divider-c60")
line:SetPoint("BOTTOM", h, "BOTTOM", 60, G.smallSeparatorY)
h.icon, h.name, h.spent, h.line = icon, name, spent, line
T.headers[i] = h
end
-- Apply and Undo, at the bottom of the background
local apply = T.panelButton(page, "ForeverUITalentsApplyButton", TEXT.apply, G.applyW)
apply:SetPoint("BOTTOM", background, "BOTTOM", 0, G.applyY)
apply:SetFrameLevel(frame:GetFrameLevel() + 6)
apply:SetScript("OnClick", function() T.apply() end)
local glow = CreateFrame("Frame", nil, apply)
glow:SetHeight(1)
glow:SetPoint("LEFT", apply, "LEFT", -G.glowX, 0)
glow:SetPoint("RIGHT", apply, "RIGHT", G.glowX, 0)
local lg = glow:CreateTexture(nil, "ARTWORK")
sizedAtlas(lg, "newplayertutorial-yellowglow-redbutton-left", G.glowW, G.glowH)
lg:SetBlendMode("ADD")
lg:SetPoint("LEFT", glow, "LEFT", 0, 0)
local ld = glow:CreateTexture(nil, "ARTWORK")
sizedAtlas(ld, "newplayertutorial-yellowglow-redbutton-right", G.glowW, G.glowH)
ld:SetBlendMode("ADD")
ld:SetPoint("RIGHT", glow, "RIGHT", 0, 0)
local lm = glow:CreateTexture(nil, "ARTWORK")
atlas(lm, "newplayertutorial-yellowglow-redbutton-middle", true)
lm:SetBlendMode("ADD")
lm:SetPoint("TOPLEFT", lg, "TOPRIGHT", 0, 0)
lm:SetPoint("BOTTOMRIGHT", ld, "BOTTOMLEFT", 0, 0)
glow:Hide()
apply.glow = glow
T.applyButton = apply
local cancel = CreateFrame("Button", "ForeverUITalentsUndoButton", page)
cancel:SetWidth(G.cancelSide)
cancel:SetHeight(G.cancelSide)
cancel:SetPoint("CENTER", apply, "RIGHT", G.cancelX + G.cancelSide / 2, 0)
cancel:SetFrameLevel(frame:GetFrameLevel() + 6)
local icon = cancel:CreateTexture(nil, "ARTWORK")
sizedAtlas(icon, "talents-button-undo", G.cancelIconW, G.cancelIconH)
icon:SetPoint("CENTER", cancel, "CENTER", 0, 0)
-- useIconAsHighlight: the same icon, in ADD, on hover
local hover = cancel:CreateTexture(nil, "HIGHLIGHT")
sizedAtlas(hover, "talents-button-undo", G.cancelIconW, G.cancelIconH)
hover:SetPoint("CENTER", cancel, "CENTER", 0, 0)
hover:SetBlendMode("ADD")
cancel:SetScript("OnClick", function() T.cancel() end)
cancel:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(TEXT.cancel, 1, 1, 1)
GameTooltip:Show()
end)
cancel:SetScript("OnLeave", function() GameTooltip:Hide() end)
cancel:Hide()
T.cancelButton = cancel
-- Activate, in place of Apply on an inactive spec
local activate = T.panelButton(page, "ForeverUITalentsActivateButton", TEXT.activate, G.applyW)
activate:SetPoint("BOTTOM", background, "BOTTOM", 0, G.applyY)
activate:SetFrameLevel(frame:GetFrameLevel() + 6)
activate:SetScript("OnClick", function() T.activate() end)
activate:Hide()
T.activateButton = activate
-- The tree: nodes, arrows and gates, scaled
local tree = CreateFrame("Frame", "ForeverUITalentsTree", page)
tree:SetScale(G.scale)
tree:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0)
tree:SetWidth(G.pageW / G.scale)
tree:SetHeight(G.pageH / G.scale)
tree:SetFrameLevel(frame:GetFrameLevel() + 3)
tree.nodes, tree.lines, tree.arrowHeads, tree.gates = {}, {}, {}, {}
T.tree = tree
end
-- ------------------------------------------------------------ Nodes
-- Column centers and transitions: the narrower inspection ones, or the page ones
local function columnsX()
if T.inspection then return G.inspectionColumns, G.inspectionTransitions end
return G.columnsX, G.transitionsX
end
-- Center of a node in tree coordinates (divided by the scale)
local function center(tab, tier, column)
local x = columnsX()[tab] + (column - 2.5) * G.columnStep
-- The pet tree (reduced window) is tab 1, so it lands in the only column shown
local y = G.firstTop - 20 * G.scale - (tier - 1) * G.rowStep
return x / G.scale, y / G.scale
end
T.center = center
local function createNode(n)
local tree = T.tree
local b = CreateFrame("Button", "ForeverUITalentsNode" .. n, tree)
b:SetWidth(G.node)
b:SetHeight(G.node)
b:SetFrameLevel(tree:GetFrameLevel() + 2)
local shadow = b:CreateTexture(nil, "BACKGROUND")
shadow:SetPoint("CENTER", b, "CENTER", 0, 0)
local icon = b:CreateTexture(nil, "BORDER")
icon:SetWidth(G.icon)
icon:SetHeight(G.icon)
icon:SetPoint("CENTER", b, "CENTER", 0, 0)
local border = b:CreateTexture(nil, "ARTWORK")
border:SetPoint("CENTER", b, "CENTER", 0, 0)
-- Rank: SystemFont16_Shadow_ThickOutline and its four shadows
local rank = b:CreateFontString(nil, "OVERLAY")
rank:SetFont(FONT, G.rankSize, "THICKOUTLINE")
rank:SetPoint("BOTTOM", b, "BOTTOM", G.rankX, G.rankY)
local shadows = {}
for i, d in ipairs({ { -1, 1 }, { 1, 1 }, { -1, -1 }, { 1, -1 } }) do
local o = b:CreateFontString(nil, "ARTWORK")
o:SetFont(FONT, G.rankSize, "THICKOUTLINE")
o:SetTextColor(0, 0, 0)
o:SetPoint("CENTER", rank, "CENTER", d[1], d[2])
shadows[i] = o
end
b.shadow, b.icon, b.border, b.rank, b.rankShadows = shadow, icon, border, rank, shadows
b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
b:SetScript("OnEnter", function(self)
if not self.talent then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
-- Pending points count (WotLK preview)
GameTooltip:SetTalent(self.talent.tab, self.talent.index, T.inspection ~= nil, T.pet, T.group, T.inspection == nil)
GameTooltip:Show()
end)
b:SetScript("OnClick", function(self, button) T.click(self, button) end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
-- Drag a learned active spell to a bar (camelot's TalentButtonSpendMixin:OnDragStart; WotLK
-- has none)
b:RegisterForDrag("LeftButton")
b:SetScript("OnDragStart", function(self) T.pickupSpell(self) end)
return b
end
local function writeRank(b, text, c)
b.rank:SetText(text)
color(b.rank, c)
for _, o in ipairs(b.rankShadows) do o:SetText(text) end
end
-- Talent state (camelot override, blizzard_sharedtalentoverrides): "maxed" yellow,
-- "selectable" green (can be bought, or partly ranked), "locked" behind a gate, "disabled"
-- gray otherwise. isOpen: its tier is reached; points: unspent points.
local function stateOf(t, isOpen, points)
if t.rank > 0 and t.rank >= t.max then return "maxed" end
if not isOpen then return "locked" end
if t.rank > 0 then return "selectable" end
if t.prereq and points > 0 then return "selectable" end
return "disabled"
end
T.stateOf = stateOf
local BORDER = { maxed = "yellow", selectable = "green", locked = "locked", disabled = "gray" }
local RANK_TEXT = { maxed = COLOR.yellow, selectable = COLOR.green, locked = COLOR.gray, disabled = COLOR.gray }
local function fillNode(b, t, state)
b.talent = t
local shape = t.square and "square" or "circle"
sizedAtlas(b.shadow, "talents-node-" .. shape .. "-shadow", t.square and G.squareShadow or G.roundShadow)
sizedAtlas(b.border, "talents-node-" .. shape .. "-" .. BORDER[state], G.node)
if t.square then
b.icon:SetTexture(t.icon)
b.icon:SetTexCoord(0, 1, 0, 1)
else
SetPortraitToTexture(b.icon, t.icon)
b.icon:SetTexCoord(0, 1, 0, 1)
end
-- DisabledOverlay: black at 0.7 behind a gate, 0.25 when gray; 3.3.5 has no mask for the
-- round shape, so the icon is darkened instead
local v = (state == "locked") and 0.3 or ((state == "disabled") and 0.75 or 1)
b.icon:SetVertexColor(v, v, v)
-- Current rank only; nothing at zero unless it can be bought
if t.rank == 0 and state ~= "selectable" then
writeRank(b, "", RANK_TEXT[state])
else
writeRank(b, tostring(t.rank), RANK_TEXT[state])
end
local x, y = center(t.tab, t.tier, t.column)
b:ClearAllPoints()
b:SetPoint("CENTER", T.tree, "TOPLEFT", x, y)
b:Show()
end
-- ------------------------------------------------------------ Arrows
-- SetClampedTextureRotation at 90 / 270, as the eight-argument SetTexCoord
local function rotate(t, name, degrees)
local e = ForeverUI.AtlasEntry(name)
if not e then return end
t:SetTexture(e[1])
local u1, u2, v1, v2 = e[2], e[3], e[4], e[5]
if degrees == 90 then
t:SetTexCoord(u1, v2, u2, v2, u1, v1, u2, v1)
elseif degrees == 270 then
t:SetTexCoord(u2, v1, u1, v1, u2, v2, u1, v2)
else
t:SetTexCoord(u1, u2, v1, v2)
end
end
-- Reuses or creates texture n of a pool; overlay: its draw layer
local function acquire(list, n, overlay)
local t = list[n]
if not t then
t = T.tree:CreateTexture(nil, overlay)
list[n] = t
end
t:Show()
return t
end
-- Straight line, horizontal or vertical, from (x1, y1) to (x2, y2); n: pool index
local function line(n, x1, y1, x2, y2, state)
local t = acquire(T.tree.lines, n, "BACKGROUND")
local name = "talents-arrow-line-" .. state
t:ClearAllPoints()
if x1 == x2 then
-- The strip is horizontal in the image: rotated for a vertical line
rotate(t, name, 90)
t:SetWidth(G.line)
t:SetHeight(math.abs(y2 - y1))
t:SetPoint("TOP", T.tree, "TOPLEFT", x1, math.max(y1, y2))
else
rotate(t, name, 0)
t:SetHeight(G.line)
t:SetWidth(math.abs(x2 - x1))
t:SetPoint("LEFT", T.tree, "TOPLEFT", math.min(x1, x2), y1)
end
end
-- Arrow head against the target node; it points down in the image
local function arrowHead(n, x, y, direction, state)
local t = acquire(T.tree.arrowHeads, n, "BORDER")
local name = "talents-arrow-head-" .. state
t:ClearAllPoints()
if direction == "down" then
rotate(t, name, 0)
t:SetWidth(G.arrowHeadW) t:SetHeight(G.arrowHeadH)
elseif direction == "right" then
rotate(t, name, 270)
t:SetWidth(G.arrowHeadH) t:SetHeight(G.arrowHeadW)
else
rotate(t, name, 90)
t:SetWidth(G.arrowHeadH) t:SetHeight(G.arrowHeadW)
end
t:SetPoint("CENTER", T.tree, "TOPLEFT", x, y)
end
-- Prerequisite arrow: straight within a column or tier, otherwise down then sideways (as in
-- WotLK: 3.3.5 cannot rotate a texture freely). Returns the next line and head indexes.
local function arrow(nLine, nArrowHead, source, target, state)
local x1, y1 = center(source.tab, source.tier, source.column)
local x2, y2 = center(target.tab, target.tier, target.column)
local r = G.node / 2
if x1 == x2 then
local tip = y2 + r + G.arrowHeadH / 2
line(nLine, x1, y1, x1, tip, state)
arrowHead(nArrowHead, x2, tip, "down", state)
return nLine + 1, nArrowHead + 1
end
local direction = (x2 > x1) and "right" or "left"
local tip = (x2 > x1) and (x2 - r - G.arrowHeadH / 2) or (x2 + r + G.arrowHeadH / 2)
if y1 == y2 then
line(nLine, x1, y1, tip, y1, state)
arrowHead(nArrowHead, tip, y2, direction, state)
return nLine + 1, nArrowHead + 1
end
line(nLine, x1, y1, x1, y2, state)
line(nLine + 1, x1, y2, tip, y2, state)
arrowHead(nArrowHead, tip, y2, direction, state)
return nLine + 2, nArrowHead + 1
end
-- ------------------------------------------------------------ Gates
local function createGate(n)
local p = CreateFrame("Frame", "ForeverUITalentsGate" .. n, T.tree)
p:SetWidth(G.gateW)
p:SetHeight(G.gateH)
p:SetFrameLevel(T.tree:GetFrameLevel() + 3)
local icon = p:CreateTexture(nil, "ARTWORK")
sizedAtlas(icon, "talents-gate", G.gateIconW, G.gateIconH)
icon:SetPoint("RIGHT", p, "RIGHT", 25, 2)
local text = p:CreateFontString(nil, "OVERLAY")
text:SetFont(FONT, 24)
color(text, COLOR.gate)
text:SetPoint("RIGHT", icon, "LEFT", -10, -1)
p.text = text
p:EnableMouse(true)
p:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_LEFT", 4, -4)
GameTooltip:SetText(string.format(TEXT.gate, self.rest or 0), 1, 0.125, 0.125)
GameTooltip:Show()
end)
p:SetScript("OnLeave", function() GameTooltip:Hide() end)
return p
end
-- ------------------------------------------------------------ Filling
-- View: the spec shown (group nil = the active one) or the pet
T.view = { group = nil, pet = false }
-- Inspection: camelot opens its talent window on the inspected player
-- (PlayerSpellsFrameMixin:SetInspecting): TALENTS_INSPECT_FORMAT title, their class, no spec
-- tabs, Apply / Undo hidden, nothing to buy. T.inspection holds the unit (nil otherwise); the
-- 3.3.5 talent functions read it through their inspect argument, active spec only. The window
-- is more compact: no gates, narrower columns, no bottom strip, no search, no unspent points.
T.inspection = nil
-- Reads the talents of the viewed spec or pet, by tab; returns tabs and the points left
function T.read()
-- Glyphs: the state is WotLK's (GlyphFrame shown, and the spec PlayerSpecTab_OnClick chose)
local inspecting = T.inspection ~= nil
T.glyphs = (not inspecting and GlyphFrame and GlyphFrame:IsShown()) and true or false
if inspecting then T.view.pet = false end
if T.glyphs then
T.view.pet = false
T.view.group = PlayerTalentFrame.talentGroup or T.view.group
end
-- Pet gone (or without talents): the view goes back to the player
if T.view.pet and (GetNumTalentTabs(false, true) or 0) == 0 then T.view.pet = false end
local pet = T.view.pet and true or false
local active = GetActiveTalentGroup and GetActiveTalentGroup(inspecting, pet) or 1
local group = (not pet and not inspecting and T.view.group) or active
T.group, T.pet, T.active = group, pet, (group == active)
local tabs = {}
for o = 1, math.min(GetNumTalentTabs(inspecting, pet) or 0, 3) do
local name, icon, spent, background, pending = GetTalentTabInfo(o, inspecting, pet, group)
local tab = { name = name, icon = icon, spent = (spent or 0) + (pending or 0), background = background, talents = {} }
local squares = ForeverUI.TalentsSquares and ForeverUI.TalentsSquares[background or ""] or {}
for i = 1, (GetNumTalents(o, inspecting, pet) or 0) do
local n, ic, tier, column, rank, max, _, prereq, previewRank, previewPrereq =
GetTalentInfo(o, i, inspecting, pet, group)
if n then
-- The rank SHOWN is the preview rank: learned + pending
table.insert(tab.talents, { tab = o, index = i, name = n, icon = ic, tier = tier,
column = column, rank = previewRank or rank or 0, learned = rank or 0, max = max or 1,
prereq = (previewPrereq ~= nil and previewPrereq or prereq) and true or false,
square = squares[tier .. ":" .. column] and true or false })
end
end
tabs[o] = tab
end
-- Points left: minus the pending ones
local pending = (not inspecting and GetGroupPreviewTalentPointsSpent) and GetGroupPreviewTalentPointsSpent(pet, group) or 0
T.pending = pending
return tabs, (GetUnspentTalentPoints(inspecting, pet, group) or 0) - pending
end
-- ------------------------------------------------------------ Drag
-- Book slot of talent t's spell (player or pet book), at its highest known rank. Talent spells
-- come from the DBC (TalentsData.lua, ForeverUI.TalentsSpells); the name is the fallback.
-- Only a learned (not just pending) square talent of the active spec can be dragged.
function T.talentSpell(t)
local book = T.pet and BOOKTYPE_PET or BOOKTYPE_SPELL
local total = 0
if T.pet then
total = (HasPetSpells and HasPetSpells()) or 0
else
for i = 1, (GetNumSpellTabs() or 0) do
local _, _, start, count = GetSpellTabInfo(i)
total = math.max(total, (start or 0) + (count or 0))
end
end
local o = T.tabs and T.tabs[t.tab]
local spells = o and ForeverUI.TalentsSpells and ForeverUI.TalentsSpells[o.background or ""]
spells = spells and spells[t.tier .. ":" .. t.column]
local wantedIds = {}
for _, id in ipairs(spells or {}) do wantedIds[id] = true end
local match, byName
for slot = 1, total do
local link = GetSpellLink(slot, book)
local id = link and tonumber(string.match(link, "spell:(%d+)"))
if id and wantedIds[id] then match = slot end
if GetSpellName(slot, book) == t.name then byName = slot end
end
return match or byName, book
end
function T.pickupSpell(b)
local t = b.talent
if T.inspection or not t or not t.square or (t.learned or 0) == 0 or not T.active then return end
local slot, book = T.talentSpell(t)
if slot then PickupSpell(slot, book) end
end
-- ------------------------------------------------------------ Pending changes
-- Left click adds a point, right click removes a pending point, modified click links;
-- the client refuses what is not allowed
function T.click(b, button)
local t = b.talent
if not t then return end
if IsModifiedClick("CHATLINK") then
local link = GetTalentLink(t.tab, t.index, T.inspection ~= nil, T.pet, T.group)
if link then ChatEdit_InsertLink(link) end
return
end
-- An inactive spec, or an inspected one, is read-only (IsLocked)
if not T.active or T.inspection then return end
if button == "RightButton" then
if t.rank > t.learned then
AddPreviewTalentPoints(t.tab, t.index, -1, T.pet, T.group)
end
elseif t.state == "selectable" and t.rank < t.max then
AddPreviewTalentPoints(t.tab, t.index, 1, T.pet, T.group)
end
T.update()
if GameTooltip.IsOwned and GameTooltip:IsOwned(b) then b:GetScript("OnEnter")(b) end
end
function T.apply()
if (T.pending or 0) <= 0 then return end
LearnPreviewTalents(T.pet)
T.update()
end
function T.cancel()
ResetGroupPreviewTalentPoints(T.pet, T.group)
T.update()
end
-- ActivateSpec: SetActiveTalentGroup casts a spell (63645 / 63644)
function T.activate()
if T.pet or T.active then return end
SetActiveTalentGroup(T.group)
T.update()
end
-- ------------------------------------------------------------ Tabs
-- Tab icon of spec group: its main tree's if it clearly leads, else the hybrid icon;
-- the default one with no points
local function specializationIcon(group)
local points = {}
for o = 1, math.min(GetNumTalentTabs(false, false) or 0, 3) do
local _, icon, spent = GetTalentTabInfo(o, false, false, group)
table.insert(points, { n = spent or 0, icon = icon })
end
table.sort(points, function(a, b) return a.n > b.n end)
local top, middle, down = points[1], points[2], points[3]
if not top or top.n == 0 then return TAB_ICONS .. DEFAULT_ICON end
local m, b = middle and middle.n or 0, down and down.n or 0
if 3 * (m - b) < 2 * (top.n - b) then
local name = string.match(top.icon or "", "([^" .. SEP .. "/]+)$") or DEFAULT_ICON
return TAB_ICONS .. string.lower(name)
end
return TAB_ICONS .. HYBRID_ICON
end
T.specializationIcon = specializationIcon
-- Side tab button n; key: spec1, spec2, glyphs or pet
local function createTab(key, n)
local bar = T.tabBar
local b = CreateFrame("Button", "ForeverUITalentsTab" .. n, bar)
b:SetFrameLevel(bar:GetFrameLevel() + 1)
b:SetWidth(G.tabSide)
b:SetHeight(G.tabSide)
local tabBackground = b:CreateTexture(nil, "BACKGROUND")
atlas(tabBackground, "common-sidetab", true)
tabBackground:SetAllPoints(b)
local icon = b:CreateTexture(nil, "ARTWORK")
icon:SetWidth(G.tabIcon)
icon:SetHeight(G.tabIcon)
icon:SetPoint("CENTER", b, "CENTER", G.tabIconX, 0)
icon:SetTexCoord(G.tabCrop, 1 - G.tabCrop, G.tabCrop, 1 - G.tabCrop)
local selected = b:CreateTexture(nil, "OVERLAY")
atlas(selected, "common-sidetab-selected", true)
selected:SetAllPoints(b)
selected:Hide()
local hover = b:CreateTexture(nil, "HIGHLIGHT")
atlas(hover, "common-sidetab-hover", true)
hover:SetAllPoints(b)
-- Check mark on the active spec, lock on the locked one
local checkMark = b:CreateTexture(nil, "OVERLAY")
sizedAtlas(checkMark, "talents-checkmark-c60", G.checkMarkW, G.checkMarkH)
checkMark:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -2, 2)
checkMark:Hide()
local lock = b:CreateTexture(nil, "OVERLAY")
sizedAtlas(lock, "talents-lock-c60", G.lockW, G.lockH)
lock:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -4, 4)
lock:Hide()
b.icon, b.selected, b.checkMark, b.lock, b.key = icon, selected, checkMark, lock, key
b:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(self.title or "")
if self.locked then
GameTooltip:AddLine(TEXT.locked, 1, 0.125, 0.125)
elseif self.isActive then
GameTooltip:AddLine(TEXT.active, COLOR.green[1], COLOR.green[2], COLOR.green[3])
end
if self.distribution then GameTooltip:AddLine(self.distribution, 1, 1, 1) end
GameTooltip:Show()
end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
b:SetScript("OnClick", function(self)
if self.locked then return end
PlaySound("igCharacterInfoTab")
if self.key == "glyphs" then
T.openGlyphs(not T.view.pet and T.view.group or nil)
elseif self.key == "pet" then
T.closeGlyphs()
T.view.pet = true
else
T.closeGlyphs()
T.view.pet = false
T.view.group = self.group
end
T.update()
end)
return b
end
-- Tab stack: first spec, second spec, glyphs (from the inscription level), pet (if it has
-- talents)
function T.layoutTabs()
local bar = T.tabBar
if not bar then return end
bar.tabs = bar.tabs or {}
-- Inspection has no tabs (camelot's IsTabAvailable)
if T.inspection then
for _, b in ipairs(bar.tabs) do b:Hide() end
return
end
local active = GetActiveTalentGroup and GetActiveTalentGroup(false, false) or 1
local numGroups = GetNumTalentGroups and GetNumTalentGroups(false, false) or 1
local defs = {
{ key = "spec1", group = 1, title = TEXT.primary },
{ key = "spec2", group = 2, title = TEXT.secondary },
}
-- PlayerTalentFrame_UpdateTabs: the tab exists from SHOW_INSCRIPTION_LEVEL; its title is
-- GlyphFrameTitleText's
if (UnitLevel("player") or 0) >= (SHOW_INSCRIPTION_LEVEL or 15) then
local found = (not T.view.pet and T.view.group) or active
local title = TEXT.glyphs
if numGroups > 1 then
title = found == 2 and TEXT.secondaryGlyphs or TEXT.primaryGlyphs
end
table.insert(defs, { key = "glyphs", title = title })
end
if (GetNumTalentTabs(false, true) or 0) > 0 then
table.insert(defs, { key = "pet", title = TEXT.pet })
end
local previous
for i, d in ipairs(defs) do
local b = bar.tabs[i] or createTab(d.key, i)
bar.tabs[i] = b
b.key, b.group, b.title = d.key, d.group, d.title
b.locked = (d.group == 2 and numGroups < 2) or nil
b.isActive = (d.group ~= nil and d.group == active) or nil
if d.key == "pet" then
SetPortraitTexture(b.icon, "pet")
b.distribution = nil
elseif d.key == "glyphs" then
b.icon:SetTexture(TAB_ICONS .. GLYPHS_ICON)
b.distribution = nil
else
b.icon:SetTexture(specializationIcon(d.group))
local r = {}
for o = 1, math.min(GetNumTalentTabs(false, false) or 0, 3) do
local _, _, spent = GetTalentTabInfo(o, false, false, d.group)
table.insert(r, tostring(spent or 0))
end
b.distribution = table.concat(r, " / ")
end
b.icon:SetDesaturated(b.locked and true or false)
if b.isActive then b.checkMark:Show() else b.checkMark:Hide() end
if b.locked then b.lock:Show() else b.lock:Hide() end
local selected
if d.key == "glyphs" then
selected = T.glyphs
elseif T.glyphs then
selected = false
elseif d.key == "pet" then
selected = T.view.pet
else
selected = not T.view.pet and d.group == (T.view.group or active)
end
if selected then b.selected:Show() else b.selected:Hide() end
b:ClearAllPoints()
if previous then
b:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, G.tabGap)
else
b:SetPoint("TOPLEFT", bar, "TOPLEFT", 0, 0)
end
b:Show()
previous = b
end
for i = #defs + 1, #bar.tabs do bar.tabs[i]:Hide() end
end
-- ------------------------------------------------------------ Glyphs
-- WotLK keeps control: its own hidden tabs are clicked
local function specializationTab(key)
for i = 1, 4 do
local b = _G["PlayerSpecTab" .. i]
if b and b.specIndex == key then return b end
end
end
function T.openGlyphs(group)
group = group or (GetActiveTalentGroup and GetActiveTalentGroup(false, false)) or 1
T.view.pet, T.view.group = false, group
local spec = specializationTab("spec" .. group)
if spec and PlayerSpecTab_OnClick then PlayerSpecTab_OnClick(spec) end
local tab = _G["PlayerTalentFrameTab" .. (GLYPH_TALENT_TAB or 4)]
if tab and PlayerTalentTab_OnClick then PlayerTalentTab_OnClick(tab) end
end
function T.closeGlyphs()
if not (GlyphFrame and GlyphFrame:IsShown()) then return end
local tab = _G["PlayerTalentFrameTab1"]
if tab and PlayerTalentTab_OnClick then
PlayerTalentTab_OnClick(tab)
else
GlyphFrame:Hide()
end
end
-- Places GlyphFrame on the reduced page: parchment centered, at its size. GlyphFrame keeps
-- its WotLK parent (PlayerTalentFrame): reparenting it to our frames crashes the client at
-- logout (ERROR #132, frame destruction hits a freed child). It is only anchored to the page
-- and raised above it.
function T.placeGlyphs()
local g = GlyphFrame
if not g or not T.page then return end
T.buildGlyphScenery()
g:ClearAllPoints()
g:SetPoint("TOPLEFT", T.className, "CENTER", -G.glyphCenterX, -G.glyphCenterY)
-- Scenery (on the inner frame), its glows (frame + 2), then the sockets
g:SetFrameLevel(T.frame:GetFrameLevel() + 3)
-- Hide WotLK's scenery: parchment, glow and title
if _G.GlyphFrameBackground then _G.GlyphFrameBackground:Hide() end
if g.glow then g.glow:Hide() end
if _G.GlyphFrameTitleText then _G.GlyphFrameTitleText:Hide() end
T.raiseCloseButton()
end
-- Scenery: ours (never GlyphFrame itself, see above). Its textures sit ON THE INNER FRAME, in
-- its background layers, so the frame's border (OVERLAY) draws over them. One frame orders
-- its layers; two frames at the same level have no guaranteed order.
function T.buildGlyphScenery()
if T.scenery then return T.scenery end
local className, frame = T.className, T.frame
local scenery = { textures = {} }
local parchment = frame:CreateTexture("ForeverUIGlyphParchment", "BACKGROUND")
applyGlyph(parchment, "parchment")
parchment:SetAllPoints(className)
local circle = frame:CreateTexture("ForeverUIGlyphCircle", "BORDER")
applyGlyph(circle, "circle", true)
circle:SetPoint("TOPLEFT", className, "CENTER", -G.circleCenterX, G.circleCenterY)
scenery.textures = { parchment, circle }
for _, c in ipairs({ { "coin-hg", "TOPLEFT", 1, -1 }, { "coin-hd", "TOPRIGHT", -1, -1 },
{ "coin-bg", "BOTTOMLEFT", 1, 1 }, { "coin-bd", "BOTTOMRIGHT", -1, 1 } }) do
local t = frame:CreateTexture("ForeverUIGlyphCorner" .. c[1]:sub(-2), "ARTWORK")
applyGlyph(t, c[1], true)
t:SetPoint(c[2], className, c[2], c[3] * G.cornerIndentX, c[4] * G.cornerIndentY)
table.insert(scenery.textures, t)
end
function scenery:Show()
for _, t in ipairs(self.textures) do t:Show() end
if self.circles then self.circles:Show() end
end
function scenery:Hide()
for _, t in ipairs(self.textures) do t:Hide() end
if self.circles then self.circles:Hide() end
end
-- Concentric circles: over the parchment, under the glows and sockets, in ADD; they only
-- rotate while the page is shown
local circles = CreateFrame("Frame", "ForeverUIGlyphRings", T.page)
circles:SetAllPoints(className)
circles:SetFrameLevel(frame:GetFrameLevel() + 1)
circles.rings = {}
for i, def in ipairs(CIRCLES) do
local t = circles:CreateTexture(nil, "ARTWORK")
applyGlyph(t, def.key, true)
t:SetBlendMode("ADD")
t:SetPoint("CENTER", className, "CENTER", 0, 0)
t:SetAlpha(0)
circles.rings[i] = { texture = t, def = def, angle = 0, alpha = 0, target = 0 }
end
circles:SetScript("OnUpdate", function(self, elapsed)
for _, a in ipairs(self.rings) do
a.angle = (a.angle + 2 * math.pi * elapsed / a.def.loop) % (2 * math.pi)
rotateRing(a.texture, a.def.key, a.angle)
if a.alpha ~= a.target then
local step = G.circlesFade * elapsed
if a.alpha < a.target then
a.alpha = math.min(a.target, a.alpha + step)
else
a.alpha = math.max(a.target, a.alpha - step)
end
a.texture:SetAlpha(a.alpha)
end
end
end)
circles:Hide()
scenery.circles = circles
-- Glows: above the frame, under the sockets, in ADD
local glows = CreateFrame("Frame", "ForeverUIGlyphGlow", T.page)
glows:SetAllPoints(className)
glows:SetFrameLevel(frame:GetFrameLevel() + 2)
glows.textures = {}
for _, key in ipairs({ "anneau-runique", "anneau-epines", "anneau-double", "rayon-0", "rayon-1", "rayon-2" }) do
local t = glows:CreateTexture(nil, "OVERLAY")
applyGlyph(t, key, true)
t:SetBlendMode("ADD")
t:SetPoint("CENTER", className, "CENTER", 0, 0)
glows.textures[key] = t
end
glows:SetAlpha(0)
glows:Hide()
glows:SetScript("OnUpdate", function(self, elapsed)
self.clock = (self.clock or 0) + elapsed
local t = self.clock
if t < G.glowRise then
self:SetAlpha(t / G.glowRise)
elseif t < G.glowRise + G.glowFall then
self:SetAlpha(1 - (t - G.glowRise) / G.glowFall)
else
self:SetAlpha(0)
self:Hide()
end
end)
scenery.glows = glows
scenery:Hide()
T.scenery = scenery
return scenery
end
-- GlyphFrame_PulseGlow: our glows replace UI-GlyphFrame-Glow
function T.pulseGlyphs()
local glows = T.scenery and T.scenery.glows
if not glows then return end
glows.clock = 0
glows:SetAlpha(0)
glows:Show()
if GlyphFrame and GlyphFrame.glow then GlyphFrame.glow:Hide() end
end
-- Socket highlight: the orange ring, at the size WotLK gives it (the mount's, 108 or 86);
-- GlyphFrameGlyph_SetGlyphType resets its coords to UI-GlyphFrame on every update
local function highlight(button)
if button and button.highlight then applyGlyph(button.highlight, "anneau-orange") end
end
-- Filled sockets of the spec shown set each circle's target alpha (GetGlyphSocketInfo returns
-- the spell of the inscribed glyph)
function T.countGlyphs()
local circles = T.scenery and T.scenery.circles
if not circles then return end
local group = PlayerTalentFrame and PlayerTalentFrame.talentGroup
for _, a in ipairs(circles.rings) do
local n = 0
for _, id in ipairs(a.def.sockets) do
local _, _, spell = GetGlyphSocketInfo(id, group)
if spell then n = n + 1 end
end
a.target = G.circlesAlpha * n / #a.def.sockets
end
end
-- Inactive spec: the scenery is grayed, as WotLK grays its background
function T.grayOutGlyphs()
if not T.scenery then return end
local active = PlayerTalentFrame and not PlayerTalentFrame.pet
and PlayerTalentFrame.talentGroup == GetActiveTalentGroup(false, false)
for _, t in ipairs(T.scenery.textures) do t:SetDesaturated(not active) end
end
-- Before the client destroys the UI, GlyphFrame gets back the anchor WotLK gives it
-- (ADDON_LOADED: SetAllPoints on PlayerTalentFrame); it returns to the page on its next open
function T.restoreGlyphs()
local g = GlyphFrame
if not g or not PlayerTalentFrame then return end
g:ClearAllPoints()
g:SetAllPoints(PlayerTalentFrame)
end
-- Once, when Blizzard_GlyphUI is loaded
function T.hookGlyphs()
local g = GlyphFrame
if not g or g.foreverHooked then return end
g.foreverHooked = true
g:HookScript("OnShow", function()
T.placeGlyphs()
if T.book and T.book:IsVisible() then T.update() end
end)
-- At logout the client destroys the UI and hides GlyphFrame if shown; resizing and
-- re-anchoring the window during that destruction crashes the client (ERROR #132).
-- Nothing moves after PLAYER_LOGOUT / PLAYER_LEAVING_WORLD (T.finished).
g:HookScript("OnHide", function()
if not T.finished and T.book and T.book:IsVisible() then T.update() end
end)
-- Hooks on WotLK's code: glow, highlight, sparkles, gray-out
if type(GlyphFrame_PulseGlow) == "function" then
hooksecurefunc("GlyphFrame_PulseGlow", T.pulseGlyphs)
end
if type(GlyphFrameGlyph_SetGlyphType) == "function" then
hooksecurefunc("GlyphFrameGlyph_SetGlyphType", highlight)
end
for i = 1, 6 do highlight(_G["GlyphFrameGlyph" .. i]) end
if type(GlyphFrame_StartSlotAnimation) == "function" then
hooksecurefunc("GlyphFrame_StartSlotAnimation", function(id)
local e = _G["GlyphFrameSparkle" .. id]
if not e then return end
local l, h = e:GetWidth(), e:GetHeight()
applyGlyph(e, "star")
e:SetWidth(l * G.sparkScale)
e:SetHeight(h * G.sparkScale)
end)
end
if type(GlyphFrame_Update) == "function" then
hooksecurefunc("GlyphFrame_Update", function()
T.grayOutGlyphs()
T.countGlyphs()
end)
end
-- A glyph inscribed or removed (GLYPH_ADDED / _REMOVED / _UPDATED) calls
-- GlyphFrameGlyph_UpdateSlot, not GlyphFrame_Update
if type(GlyphFrameGlyph_UpdateSlot) == "function" then
hooksecurefunc("GlyphFrameGlyph_UpdateSlot", T.countGlyphs)
end
T.placeGlyphs()
end
-- Width: three trees, one (narrow: pet, glyphs) or three narrower columns (inspection);
-- height: no bottom strip during inspection
function T.applyWidth(narrow)
local pageW = narrow and G.narrowPage or (T.inspection and 3 * G.inspectionColumn) or G.pageW
local indent = T.inspection and G.backgroundBottom or 0
T.book:SetHeight(G.height - indent)
T.page:SetHeight(G.pageH - indent)
T.background:SetHeight(G.backgroundH - indent)
T.className:ClearAllPoints()
T.className:SetPoint("TOPLEFT", T.background, "TOPLEFT", 0, G.backgroundTop)
T.className:SetPoint("BOTTOMRIGHT", T.background, "BOTTOMRIGHT", 0, G.backgroundBottom - indent)
-- Headers keep the same offset from their column's center; during inspection, the small
-- separator is cropped to its column
local centers, transitions = columnsX()
local e = ForeverUI.AtlasEntry("talents-small-divider-c60")
for i, h in ipairs(T.headers) do
local gap = G.headerX + (i - 1) * G.headerGap - (G.columnsX[i] - G.frameLeft)
h:ClearAllPoints()
h:SetPoint("CENTER", T.frame, "TOPLEFT", centers[i] - G.frameLeft + gap, G.headerY)
local t = h.line
atlas(t, "talents-small-divider-c60")
local g, d = 0, e and e[6] or t:GetWidth()
if T.inspection and e then
-- Separator center from the column center; the kept window, in separator coordinates
local lineCenter = gap + 60
local a = G.inspectionColumn / 2 - G.smallSeparatorMargin
g = math.max(0, -a - lineCenter + e[6] / 2)
d = math.min(e[6], a - lineCenter + e[6] / 2)
local du = (e[3] - e[2]) / e[6]
t:SetTexCoord(e[2] + g * du, e[2] + d * du, e[4], e[5])
t:SetWidth(d - g)
end
t:ClearAllPoints()
t:SetPoint("BOTTOM", h, "BOTTOM", 60 + (g + d) / 2 - (e and e[6] or d) / 2, G.smallSeparatorY)
end
T.book:SetWidth(G.width - G.pageW + pageW)
T.page:SetWidth(pageW)
placeStone(pageW, G.backgroundH - indent)
-- Horizontal bars cropped to half the width: the end ornament stays, the line reaches the
-- center
local half = (pageW - 2 * G.frameLeft) / 2
for _, d in ipairs({ { T.barLeft, "talents-divider-left-c60", false },
{ T.barRight, "talents-divider-right-c60", true } }) do
local t, e = d[1], ForeverUI.AtlasEntry(d[2])
if e then
local l = math.min(e[6], half)
local du = (e[3] - e[2]) * l / e[6]
if d[3] then
t:SetTexCoord(e[3] - du, e[3], e[4], e[5])
else
t:SetTexCoord(e[2], e[2] + du, e[4], e[5])
end
t:SetWidth(l)
end
end
for i, v in ipairs(T.verticals) do
v:ClearAllPoints()
v:SetPoint("TOP", T.frame, "TOPLEFT", transitions[i] - G.frameLeft, G.verticalTop)
if narrow then v:Hide() else v:Show() end
end
T.narrow = narrow
end
-- Class art behind the trees, and the portrait
local function placeBackground(tabs)
local className = T.className
for _, t in ipairs(className.textures) do t:Hide() end
local _, token = UnitClass(T.inspection or "player")
-- Portrait: the class shown (the inspected unit's during inspection)
T.book.portrait:SetTexture(PORTRAIT .. string.lower(token or "warrior"))
-- One background per tree: death knights (the middle third of each modern image), and
-- inspection (the tree's own third of the class art, cropped on each side like its column)
local dk = token == "DEATHKNIGHT"
if (dk or T.inspection) and not T.narrow then
local l = T.inspection and G.inspectionColumn or G.pageW / 3
local keep = l / (G.pageW / 3)
for i = 1, 3 do
local t = className.textures[i] or className:CreateTexture(nil, "BACKGROUND")
className.textures[i] = t
local e = ForeverUI.AtlasEntry(dk and BACKGROUND_DK[i] or ("talent-background-" .. string.lower(token or "warrior")))
if e then
t:SetTexture(e[1])
local du = (e[3] - e[2]) / 3
local start = e[2] + du * (dk and 1 or (i - 1))
local margin = du * (1 - keep) / 2
t:SetTexCoord(start + margin, start + du - margin, e[4], e[5])
end
t:ClearAllPoints()
t:SetPoint("TOPLEFT", className, "TOPLEFT", (i - 1) * l, 0)
t:SetPoint("BOTTOMLEFT", className, "BOTTOMLEFT", (i - 1) * l, 0)
t:SetWidth(l)
t:Show()
end
return
end
local t = className.textures[1] or className:CreateTexture(nil, "BACKGROUND")
className.textures[1] = t
local name = "talent-background-" .. string.lower(token or "warrior")
atlas(t, name, true)
-- The reduced window shows the first third of the art: the one of the column it keeps
if T.narrow then
local e = ForeverUI.AtlasEntry(name)
if e then t:SetTexCoord(e[2], e[2] + (e[3] - e[2]) / 3, e[4], e[5]) end
end
t:ClearAllPoints()
t:SetAllPoints(className)
t:Show()
end
function T.update()
if not T.book or T.finished then return end
local tabs, points = T.read()
T.tabs = tabs
T.applyWidth(T.pet or T.glyphs)
placeBackground(tabs)
T.layoutTabs()
-- Glyphs: the page keeps only the stone and its frame
local glyphs = T.glyphs
for _, f in ipairs({ T.className, T.tree, T.points, T.barLeft, T.barRight }) do
if glyphs then f:Hide() else f:Show() end
end
-- No unspent points during inspection, nor search (TalentsSearch.lua)
if T.inspection then T.points:Hide() end
local title = TEXT.title
if T.inspection then title = string.format(TEXT.inspection, UnitName(T.inspection) or "") end
T.book.title:SetText(glyphs and _G.GlyphFrameTitleText and _G.GlyphFrameTitleText:GetText() or title)
if T.scenery then
if glyphs then T.scenery:Show() else T.scenery:Hide() end
end
local search = ForeverUI.TalentsSearch
if glyphs then
if search then search.update() end
for _, h in ipairs(T.headers) do h:Hide() end
T.placeGlyphs()
T.scenery:Show()
T.grayOutGlyphs()
T.countGlyphs()
T.applyButton:Hide()
T.cancelButton:Hide()
if T.active then
T.activateButton:Hide()
else
T.activateButton:Show()
local spell = ACTIVATION_SPELLS[T.group]
T.activateButton:Activate(not (spell and IsCurrentSpell and IsCurrentSpell(spell)))
end
return
end
local perTier = T.pet and G.petPointsPerTier or G.pointsPerTier
-- Headers
for i, h in ipairs(T.headers) do
local o = tabs[i]
if o then
SetPortraitToTexture(h.icon, o.icon)
h.name:SetText(o.name)
h.spent:SetText(tostring(o.spent))
h:Show()
else
h:Hide()
end
end
-- Apply and Undo only with pending changes; on an inactive spec, Activate instead
local pending = (T.pending or 0) > 0
if T.inspection then
T.applyButton:Hide()
T.cancelButton:Hide()
T.activateButton:Hide()
elseif T.active then
T.activateButton:Hide()
T.applyButton:Show()
T.applyButton:Activate(pending)
if pending then T.applyButton.glow:Show() T.cancelButton:Show()
else T.applyButton.glow:Hide() T.cancelButton:Hide() end
else
T.applyButton:Hide()
T.cancelButton:Hide()
T.activateButton:Show()
-- While the activation spell casts, the button is disabled
-- (PlayerTalentFrameActivateButton)
local spell = ACTIVATION_SPELLS[T.group]
T.activateButton:Activate(not (spell and IsCurrentSpell and IsCurrentSpell(spell)))
end
-- Unspent points: green if any are left
T.points.count:SetText(tostring(points))
color(T.points.count, points > 0 and COLOR.green or COLOR.gray)
-- Nodes
local tree = T.tree
local n = 0
local bySlot = {}
for _, o in ipairs(tabs) do
for _, t in ipairs(o.talents) do
n = n + 1
local b = tree.nodes[n] or createNode(n)
tree.nodes[n] = b
local isOpen = (t.tier - 1) * perTier <= o.spent
t.state = stateOf(t, isOpen, T.inspection and 0 or points)
fillNode(b, t, t.state)
bySlot[t.tab .. ":" .. t.tier .. ":" .. t.column] = t
end
end
for i = n + 1, #tree.nodes do
tree.nodes[i].talent = nil
tree.nodes[i]:Hide()
end
-- Arrows: prerequisites of each talent (tier, column, met)
local nW, nH = 1, 1
for _, o in ipairs(tabs) do
for _, t in ipairs(o.talents) do
local p = { GetTalentPrereqs(t.tab, t.index, T.inspection ~= nil, T.pet, T.group) }
for k = 1, #p, 4 do
local source = bySlot[t.tab .. ":" .. p[k] .. ":" .. p[k + 1]]
if source then
local filled = p[k + 3]
if filled == nil then filled = p[k + 2] end
local state = (t.state == "locked") and "locked" or (filled and "yellow" or "gray")
nW, nH = arrow(nW, nH, source, t, state)
end
end
end
end
for i = nW, #tree.lines do tree.lines[i]:Hide() end
for i = nH, #tree.arrowHeads do tree.arrowHeads[i]:Hide() end
-- Gates: one per tree, at the first locked tier holding a talent, left of its first node;
-- none during inspection
local nGate = 0
for ti, o in ipairs(tabs) do
local first
for _, t in ipairs(o.talents) do
if t.state == "locked" and (not first or t.tier < first.tier
or (t.tier == first.tier and t.column < first.column)) then
first = t
end
end
if first and not T.inspection then
nGate = nGate + 1
local p = tree.gates[nGate] or createGate(nGate)
tree.gates[nGate] = p
p.rest = (first.tier - 1) * perTier - o.spent
p.text:SetText(tostring(p.rest))
local x, y = center(first.tab, first.tier, first.column)
p:ClearAllPoints()
p:SetPoint("RIGHT", tree, "TOPLEFT", x - G.node / 2 + G.gateX, y)
p:Show()
end
end
for i = nGate + 1, #tree.gates do tree.gates[i]:Hide() end
-- Search: its width, and the node marks
if search then
search.place(T.page:GetWidth())
search.update()
end
end
-- ------------------------------------------------------------ Inspection
-- Talents button of the inspect frame (Inspect.lua): opens the window on the inspected unit,
-- or switches to it if already open. Closing brings back the player's screen (OnHide below).
function T.inspect(unit)
if not unit then return end
if not PlayerTalentFrame and TalentFrame_LoadUI then TalentFrame_LoadUI() end
if not PlayerTalentFrame then return end
T.build()
T.closeGlyphs()
T.inspection = unit
if PlayerTalentFrame:IsShown() then
T.view.group, T.view.pet = nil, false
T.update()
else
ShowUIPanel(PlayerTalentFrame)
end
end
-- ------------------------------------------------------------ Assembly
-- Silences PlayerTalentFrame's own art and children, except our book, the close button and
-- GlyphFrame
function T.suppressWotLK()
local f = PlayerTalentFrame
if not f then return end
for _, region in ipairs({ f:GetRegions() }) do suppress(region) end
for _, child in ipairs({ f:GetChildren() }) do
if child ~= T.book and child ~= PlayerTalentFrameCloseButton and child ~= GlyphFrame then
suppress(child)
end
end
end
-- ------------------------------------------------------------ Close confirmation
-- Pending changes: the player's (active spec, the only one that takes points) or the pet's
local function hasPet()
return (GetNumTalentTabs(false, true) or 0) > 0
end
function T.queued()
local n = GetGroupPreviewTalentPointsSpent(false, GetActiveTalentGroup(false, false)) or 0
if hasPet() then
n = n + (GetGroupPreviewTalentPointsSpent(true, GetActiveTalentGroup(false, true)) or 0)
end
return n > 0
end
-- CONTINUE: drop the pending changes and really close
function T.closeNow()
ResetGroupPreviewTalentPoints(false, GetActiveTalentGroup(false, false))
if hasPet() then ResetGroupPreviewTalentPoints(true, GetActiveTalentGroup(false, true)) end
T.confirmed = true
HideUIPanel(PlayerTalentFrame)
T.confirmed = nil
end
-- StaticPopupDialogs always exists (StaticPopup.lua): NEVER reassign it. The whole global
-- would become addon-owned and taint every client popup (BindEnchant blocked). Only our key
-- is written.
StaticPopupDialogs["FOREVERUI_TALENTS_CONFIRM_CLOSE"] = {
text = TEXT.confirmClose,
button1 = CONTINUE,
button2 = CANCEL,
OnAccept = function() T.closeNow() end,
timeout = 0,
whileDead = 1,
hideOnEscape = 1,
}
-- Closed with pending changes: 3.3.5 cannot cancel a close without tainting HideUIPanel, so
-- the window reopens on the next frame, as it was, and asks
local reopener = CreateFrame("Frame")
reopener:Hide()
reopener:SetScript("OnUpdate", function(self)
self:Hide()
if T.finished or not T.queued() then return end
T.keepView = true
ShowUIPanel(PlayerTalentFrame)
T.keepView = nil
-- A full-screen panel (WotLK's map) refuses the reopening: no question about an absent
-- window; the pending points stay, as WotLK keeps them
if PlayerTalentFrame:IsShown() then
StaticPopup_Show("FOREVERUI_TALENTS_CONFIRM_CLOSE")
end
end)
T.reopener = reopener
function T.build()
if T.book or not PlayerTalentFrame then return T.book end
PlayerTalentFrame:EnableMouse(false)
local book = CreateFrame("Frame", "ForeverUITalentsFrame", PlayerTalentFrame)
book:SetPoint("TOP", UIParent, "TOP", 0, G.top)
book:SetWidth(G.width)
book:SetHeight(G.height)
book:EnableMouse(true)
T.book = book
buildFrame(book)
buildPage(book)
-- Side tab bar, outside on the right (CharacterFrameModeTabs)
local bar = CreateFrame("Frame", "ForeverUITalentsTabs", book)
bar:SetWidth(64)
bar:SetHeight(384)
bar:SetPoint("TOPLEFT", book, "TOPRIGHT", G.tabsX, G.tabsY)
bar:SetFrameLevel(book:GetFrameLevel() + 1)
T.tabBar = bar
T.suppressWotLK()
T.hookGlyphs()
-- Search (TalentsSearch.lua loads after this file; if not loaded yet, it builds itself)
if ForeverUI.TalentsSearch then ForeverUI.TalentsSearch.build(T) end
-- Movable by its title; brought to front when opened or clicked
ForeverUI.WindowStack.makeMovable(book, book.banner, "talents")
ForeverUI.WindowStack.fitToScreen("talents", PlayerTalentFrame, book, 0, G.top, G.fitExtraW, G.fitExtraH)
ForeverUI.WindowStack.register("talents", PlayerTalentFrame, function()
return { T.book, T.tabBar, _G.ForeverUITalentsSearchPreview, _G.ForeverUITalentsSearchOptionsList }
end)
PlayerTalentFrame:HookScript("OnShow", function()
T.suppressWotLK()
T.raiseCloseButton()
-- On every open, the active spec (SetTab(GetActiveTab())); not when it reopens to confirm
if not T.keepView then T.view.group, T.view.pet = nil, false end
T.update()
end)
PlayerTalentFrame:HookScript("OnHide", function(self)
-- OnHide also fires when only UIParent hides (barber shop): the window is not closed
if self:IsShown() then return end
T.inspection = nil
if T.finished or T.confirmed or not T.queued() then return end
reopener:Show()
end)
for _, name in ipairs({ "PlayerTalentFrame_Refresh", "PlayerTalentFrame_Update" }) do
if type(_G[name]) == "function" then
hooksecurefunc(name, function()
T.suppressWotLK()
if T.book:IsVisible() then T.update() end
end)
end
end
return book
end
-- Blizzard_TalentUI loads on demand: the screen is built when it arrives (or now if already
-- loaded)
local watcher = CreateFrame("Frame")
watcher:RegisterEvent("ADDON_LOADED")
watcher:RegisterEvent("PLAYER_TALENT_UPDATE")
watcher:RegisterEvent("CHARACTER_POINTS_CHANGED")
watcher:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED")
watcher:RegisterEvent("PREVIEW_TALENT_POINTS_CHANGED")
watcher:RegisterEvent("PREVIEW_PET_TALENT_POINTS_CHANGED")
watcher:RegisterEvent("PET_TALENT_UPDATE")
watcher:RegisterEvent("UNIT_PET")
watcher:RegisterEvent("CURRENT_SPELL_CAST_CHANGED")
watcher:RegisterEvent("PLAYER_LEVEL_UP")
watcher:RegisterEvent("PLAYER_LOGOUT")
watcher:RegisterEvent("PLAYER_LEAVING_WORLD")
watcher:RegisterEvent("PLAYER_ENTERING_WORLD")
watcher:RegisterEvent("INSPECT_TALENT_READY")
watcher:SetScript("OnEvent", function(_, ev, arg1)
if ev == "ADDON_LOADED" then
if arg1 == "Blizzard_TalentUI" then T.build() end
if arg1 == "Blizzard_GlyphUI" and T.book then T.hookGlyphs() end
return
end
if ev == "PLAYER_LOGOUT" or ev == "PLAYER_LEAVING_WORLD" then
T.finished = true
T.restoreGlyphs()
return
end
if ev == "PLAYER_ENTERING_WORLD" then T.finished = false return end
if ev == "UNIT_PET" and arg1 ~= "player" then return end
if not (T.book and T.book:IsVisible()) then return end
-- Each cast start or stop: only the Activate button depends on it
-- (PlayerTalentFrameActivateButton_Update), not the whole tree (some 85 talents read)
if ev == "CURRENT_SPELL_CAST_CHANGED" then
if T.activateButton and T.activateButton:IsShown() then
local spell = ACTIVATION_SPELLS[T.group]
T.activateButton:Activate(not (spell and IsCurrentSpell and IsCurrentSpell(spell)))
end
return
end
T.update()
end)
if PlayerTalentFrame then T.build() end