9c96c28590
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
1772 lines
69 KiB
Lua
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
|