10ce15481b
Reprise du checkFit de Camelot (marges 200 x 140 de PlayerSpellsFrame) : la fenêtre est réduite jusqu'à tenir dans l'écran, position par défaut bornée comme ClampUIPanelY. Positions enregistrées en unités d'écran. Hors combat seulement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1756 lines
68 KiB
Lua
1756 lines
68 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
|
|
StaticPopup_Show("FOREVERUI_TALENTS_CONFIRM_CLOSE")
|
|
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()
|
|
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 T.book and T.book:IsVisible() then T.update() end
|
|
end)
|
|
if PlayerTalentFrame then T.build() end
|