Files
Nanami-UI/Units/TalentTree.lua
2026-03-16 13:48:46 +08:00

1066 lines
42 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

SFrames.TalentTree = {}
local ICON_SIZE = 36
local ICON_SPACING_X = 14
local ICON_SPACING_Y = 14
local TAB_WIDTH = 220
local FRAME_WIDTH = (TAB_WIDTH * 3) + 40
local FRAME_HEIGHT = 520
--------------------------------------------------------------------------------
-- Theme: Pink Cat-Paw
--------------------------------------------------------------------------------
local T = SFrames.Theme:Extend()
local function GetHex()
return (SFrames.Theme and SFrames.Theme:GetAccentHex()) or "ffffb3d9"
end
--------------------------------------------------------------------------------
-- Class definitions
--------------------------------------------------------------------------------
local CLASS_LIST = {
{ key = "WARRIOR", name = "战士", color = { 0.78, 0.61, 0.43 } },
{ key = "PALADIN", name = "圣骑士", color = { 0.96, 0.55, 0.73 } },
{ key = "HUNTER", name = "猎人", color = { 0.67, 0.83, 0.45 } },
{ key = "ROGUE", name = "盗贼", color = { 1.00, 0.96, 0.41 } },
{ key = "PRIEST", name = "牧师", color = { 1.00, 1.00, 1.00 } },
{ key = "SHAMAN", name = "萨满", color = { 0.00, 0.44, 0.87 } },
{ key = "MAGE", name = "法师", color = { 0.41, 0.80, 0.94 } },
{ key = "WARLOCK", name = "术士", color = { 0.58, 0.51, 0.79 } },
{ key = "DRUID", name = "德鲁伊", color = { 1.00, 0.49, 0.04 } },
}
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function GetFont()
if SFrames and SFrames.GetFont then return SFrames:GetFont() end
return "Fonts\\ARIALN.TTF"
end
local function SetRoundBackdrop(frame, bgColor, borderColor)
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
local bg = bgColor or T.panelBg
local bd = borderColor or T.panelBorder
frame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 1)
frame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 1)
end
local function SetPixelBackdrop(frame, bgColor, borderColor)
frame:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 }
})
if bgColor then frame:SetBackdropColor(bgColor[1], bgColor[2], bgColor[3], bgColor[4] or 1) end
if borderColor then frame:SetBackdropBorderColor(borderColor[1], borderColor[2], borderColor[3], borderColor[4] or 1) end
end
local function CreateShadow(parent, size)
local s = CreateFrame("Frame", nil, parent)
local sz = size or 4
s:SetPoint("TOPLEFT", parent, "TOPLEFT", -sz, sz)
s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", sz, -sz)
s:SetFrameLevel(math.max(parent:GetFrameLevel() - 1, 0))
s:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
s:SetBackdropColor(0, 0, 0, 0.55)
s:SetBackdropBorderColor(0, 0, 0, 0.4)
return s
end
local function MakeFS(parent, size, justifyH, color)
local fs = parent:CreateFontString(nil, "OVERLAY")
fs:SetFont(GetFont(), size or 11, "OUTLINE")
fs:SetJustifyH(justifyH or "LEFT")
local c = color or T.valueText
fs:SetTextColor(c[1], c[2], c[3])
return fs
end
local function StyleButton(btn, label)
SetRoundBackdrop(btn, T.btnBg, T.btnBorder)
local fs = MakeFS(btn, 12, "CENTER", T.btnText)
fs:SetPoint("CENTER", btn, "CENTER", 0, 0)
if label then fs:SetText(label) end
btn.nanamiLabel = fs
btn:SetScript("OnEnter", function()
this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4])
if this.nanamiLabel then
this.nanamiLabel:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3])
end
if this.nanamiTooltip then
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:AddLine(this.nanamiTooltip[1], 1, 1, 1)
if this.nanamiTooltip[2] then
GameTooltip:AddLine(this.nanamiTooltip[2], 0.7, 0.7, 0.7)
end
GameTooltip:Show()
end
end)
btn:SetScript("OnLeave", function()
this:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4])
this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
if this.nanamiLabel then
this.nanamiLabel:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3])
end
GameTooltip:Hide()
end)
btn:SetScript("OnMouseDown", function()
this:SetBackdropColor(T.btnDownBg[1], T.btnDownBg[2], T.btnDownBg[3], T.btnDownBg[4])
end)
btn:SetScript("OnMouseUp", function()
this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
end)
return fs
end
--------------------------------------------------------------------------------
-- Talent data cache (stores talent trees per class in SFramesGlobalDB)
--------------------------------------------------------------------------------
local function GetCache()
if not SFramesGlobalDB then SFramesGlobalDB = {} end
if not SFramesGlobalDB.talentCache then SFramesGlobalDB.talentCache = {} end
return SFramesGlobalDB.talentCache
end
local function CacheCurrentClassData()
local _, classEn = UnitClass("player")
if not classEn then return end
local numTabs = GetNumTalentTabs()
if numTabs == 0 then return end
local cache = GetCache()
local classData = {}
for t = 1, numTabs do
local tabName, tabIcon, pointsSpent, background = GetTalentTabInfo(t)
local treeData = { name = tabName, icon = tabIcon, background = background or "", talents = {}, numTalents = 0 }
local numTalents = GetNumTalents(t)
treeData.numTalents = numTalents
for i = 1, numTalents do
local tName, tIcon, tier, column, rank, maxRank = GetTalentInfo(t, i)
local prereqTier, prereqCol = GetTalentPrereqs(t, i)
local descLines = {}
local scanTip = _G["NanamiTalentScanTip"]
if not scanTip then
scanTip = CreateFrame("GameTooltip", "NanamiTalentScanTip", UIParent, "GameTooltipTemplate")
scanTip:SetOwner(UIParent, "ANCHOR_NONE")
end
scanTip:ClearLines()
scanTip:SetTalent(t, i)
local numLines = scanTip:NumLines()
for li = 2, numLines do
local lineObj = _G["NanamiTalentScanTipTextLeft" .. li]
if lineObj then
local txt = lineObj:GetText()
if txt and txt ~= "" then
table.insert(descLines, txt)
end
end
end
treeData.talents[i] = {
name = tName or "",
icon = tIcon or "",
tier = tier or 1,
column = column or 1,
maxRank = maxRank or 1,
prereqTier = prereqTier,
prereqColumn = prereqCol,
desc = descLines,
}
end
classData[t] = treeData
end
classData.numTabs = numTabs
cache[classEn] = classData
end
--------------------------------------------------------------------------------
-- Data access wrappers (API for own class, cache for others)
--------------------------------------------------------------------------------
local TT = SFrames.TalentTree
local function IsViewingOwnClass(self)
return not self.viewingClass or self.viewingClass == self.playerClass
end
local function TT_GetNumTabs(self)
if IsViewingOwnClass(self) then return GetNumTalentTabs() end
local cache = GetCache()
local cd = cache[self.viewingClass]
return cd and cd.numTabs or 0
end
local function TT_GetTabInfo(self, tab)
if IsViewingOwnClass(self) then return GetTalentTabInfo(tab) end
local cache = GetCache()
local cd = cache[self.viewingClass]
if cd and cd[tab] then
return cd[tab].name, cd[tab].icon, 0, cd[tab].background
end
return "", "", 0, ""
end
local function TT_GetNumTalents(self, tab)
if IsViewingOwnClass(self) then return GetNumTalents(tab) end
local cache = GetCache()
local cd = cache[self.viewingClass]
if cd and cd[tab] then return cd[tab].numTalents or 0 end
return 0
end
local function TT_GetTalentInfo(self, tab, index)
if IsViewingOwnClass(self) then return GetTalentInfo(tab, index) end
local cache = GetCache()
local cd = cache[self.viewingClass]
if cd and cd[tab] and cd[tab].talents[index] then
local t = cd[tab].talents[index]
return t.name, t.icon, t.tier, t.column, 0, t.maxRank, nil, true
end
return nil
end
local function TT_GetTalentPrereqs(self, tab, index)
if IsViewingOwnClass(self) then return GetTalentPrereqs(tab, index) end
local cache = GetCache()
local cd = cache[self.viewingClass]
if cd and cd[tab] and cd[tab].talents[index] then
local t = cd[tab].talents[index]
return t.prereqTier, t.prereqColumn
end
return nil, nil
end
--------------------------------------------------------------------------------
-- Initialize
--------------------------------------------------------------------------------
function SFrames.TalentTree:Initialize()
local _, classEn = UnitClass("player")
self.playerClass = classEn
self:CreateMainFrame()
self:HookVanillaUI()
SFrames:RegisterEvent("CHARACTER_POINTS_CHANGED", function()
if self.frame and self.frame:IsShown() then self:Update() end
end)
SFrames:RegisterEvent("SPELLS_CHANGED", function()
if self.frame and self.frame:IsShown() then self:Update() end
end)
end
--------------------------------------------------------------------------------
-- Main Frame
--------------------------------------------------------------------------------
function SFrames.TalentTree:CreateMainFrame()
local f = CreateFrame("Frame", "SFramesTalentFrame", UIParent)
f:SetWidth(FRAME_WIDTH)
f:SetHeight(FRAME_HEIGHT)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
SetRoundBackdrop(f, T.panelBg, T.panelBorder)
CreateShadow(f, 5)
f:EnableMouse(true)
f:SetMovable(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
f:SetFrameStrata("HIGH")
f:Hide()
local titleIcoSize = 14
local titleGap = 4
f.title = MakeFS(f, 14, "CENTER", T.titleColor)
f.title:SetPoint("TOP", f, "TOP", (titleIcoSize + titleGap) / 2, -10)
f.title:SetText("|c" .. GetHex() .. "Nanami|r 天赋系统")
local titleIco = SFrames:CreateIcon(f, "talent", titleIcoSize)
titleIco:SetDrawLayer("OVERLAY")
titleIco:SetVertexColor(T.titleColor[1], T.titleColor[2], T.titleColor[3])
titleIco:SetPoint("RIGHT", f.title, "LEFT", -titleGap, 0)
-- Close button
local closeBtn = CreateFrame("Button", nil, f)
closeBtn:SetWidth(18)
closeBtn:SetHeight(18)
closeBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8)
closeBtn:SetFrameLevel(f:GetFrameLevel() + 3)
SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder)
local closeTxt = MakeFS(closeBtn, 10, "CENTER", T.title)
closeTxt:SetPoint("CENTER", closeBtn, "CENTER", 0, 0)
closeTxt:SetText("x")
closeBtn:SetScript("OnClick", function() f:Hide() end)
closeBtn:SetScript("OnEnter", function()
this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
this:SetBackdropBorderColor(T.btnHoverBd[1], T.btnHoverBd[2], T.btnHoverBd[3], T.btnHoverBd[4])
end)
closeBtn:SetScript("OnLeave", function()
this:SetBackdropColor(T.buttonDownBg[1], T.buttonDownBg[2], T.buttonDownBg[3], T.buttonDownBg[4])
this:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
end)
f.close = closeBtn
-- Class selector bar (shown in sim mode)
local classBar = CreateFrame("Frame", nil, f)
classBar:SetHeight(20)
classBar:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -28)
classBar:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -28)
classBar:SetFrameLevel(f:GetFrameLevel() + 2)
classBar:Hide()
f.classBar = classBar
local numClasses = table.getn(CLASS_LIST)
local gap = 2
local barW = FRAME_WIDTH - 20
local cbW = math.floor((barW - (numClasses - 1) * gap) / numClasses)
f.classBtns = {}
for ci, cinfo in ipairs(CLASS_LIST) do
local cb = CreateFrame("Button", nil, classBar)
cb:SetWidth(cbW)
cb:SetHeight(18)
cb:SetPoint("TOPLEFT", classBar, "TOPLEFT", (ci - 1) * (cbW + gap), 0)
SetPixelBackdrop(cb, T.slotBg, T.slotBorder)
local cIcon = SFrames:CreateClassIcon(cb, 14)
cIcon.overlay:SetPoint("LEFT", cb, "LEFT", 3, 0)
SFrames:SetClassIcon(cIcon, cinfo.key)
cb.classIconTex = cIcon
local cbt = MakeFS(cb, 9, "CENTER", cinfo.color)
cbt:SetPoint("LEFT", cIcon.overlay, "RIGHT", 1, 0)
cbt:SetPoint("RIGHT", cb, "RIGHT", -2, 0)
cbt:SetText(cinfo.name)
cb.label = cbt
cb.classKey = cinfo.key
cb.classColor = cinfo.color
cb:SetScript("OnClick", function()
SFrames.TalentTree:SwitchViewClass(this.classKey)
end)
cb:SetScript("OnEnter", function()
this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4])
local cache = GetCache()
if not cache[this.classKey] and this.classKey ~= SFrames.TalentTree.playerClass then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
GameTooltip:AddLine("需先用该职业角色打开天赋面板以缓存数据", 1, 0.5, 0.5)
GameTooltip:Show()
end
end)
cb:SetScript("OnLeave", function()
this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
GameTooltip:Hide()
end)
f.classBtns[ci] = cb
end
-- Overlay for text on top of children
f.overlay = CreateFrame("Frame", nil, f)
f.overlay:SetAllPoints(f)
f.overlay:SetFrameLevel(f:GetFrameLevel() + 10)
f.pointsText = MakeFS(f.overlay, 13, "LEFT", T.titleColor)
f.pointsText:SetPoint("BOTTOMLEFT", f.overlay, "BOTTOMLEFT", 14, 25)
f.simLabel = MakeFS(f.overlay, 11, "LEFT", T.titleColor)
f.simLabel:SetPoint("LEFT", f.pointsText, "LEFT", 0, -18)
f.simLabel:SetText("")
-- Bottom buttons
f.btnApply = CreateFrame("Button", nil, f)
f.btnApply:SetWidth(100)
f.btnApply:SetHeight(26)
f.btnApply:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 12)
StyleButton(f.btnApply, "应用天赋")
f.btnApply.nanamiTooltip = { "应用天赋", "将所有模拟点数提交至服务器。" }
f.btnApply:SetScript("OnClick", function() SFrames.TalentTree:ApplyVirtualPoints() end)
f.btnReset = CreateFrame("Button", nil, f)
f.btnReset:SetWidth(100)
f.btnReset:SetHeight(26)
f.btnReset:SetPoint("RIGHT", f.btnApply, "LEFT", -8, 0)
StyleButton(f.btnReset, "重置预览")
f.btnReset:SetScript("OnClick", function() SFrames.TalentTree:ResetVirtualPoints() end)
f.btnSimMode = CreateFrame("Button", nil, f)
f.btnSimMode:SetWidth(110)
f.btnSimMode:SetHeight(26)
f.btnSimMode:SetPoint("RIGHT", f.btnReset, "LEFT", -8, 0)
StyleButton(f.btnSimMode, "|cff888888模拟加点: 关|r")
f.simModeText = f.btnSimMode.nanamiLabel
f.btnSimMode.nanamiTooltip = { "模拟加点模式", "开启后可用左键虚拟加点、右键取消,\n确认后点「应用天赋」才会实际扣分。\n可切换职业模拟其他职业加点。" }
f.btnSimMode:SetScript("OnClick", function()
SFrames.TalentTree.simMode = not SFrames.TalentTree.simMode
SFrames.TalentTree:UpdateSimModeLabel()
end)
self.frame = f
self.tabs = {}
self.virtualPoints = {}
self.simMode = false
self.viewingClass = nil
tinsert(UISpecialFrames, "SFramesTalentFrame")
end
--------------------------------------------------------------------------------
-- Class bar update
--------------------------------------------------------------------------------
function SFrames.TalentTree:UpdateClassBar()
if not self.frame or not self.frame.classBtns then return end
local viewing = self.viewingClass or self.playerClass
local cache = GetCache()
for ci, cinfo in ipairs(CLASS_LIST) do
local cb = self.frame.classBtns[ci]
if not cb then break end
local hasData = (cinfo.key == self.playerClass) or (cache[cinfo.key] ~= nil)
if cinfo.key == viewing then
SetPixelBackdrop(cb, T.tabActiveBg, cinfo.color)
cb.label:SetTextColor(cinfo.color[1], cinfo.color[2], cinfo.color[3])
elseif hasData then
SetPixelBackdrop(cb, T.slotBg, T.slotBorder)
cb.label:SetTextColor(cinfo.color[1] * 0.8, cinfo.color[2] * 0.8, cinfo.color[3] * 0.8)
else
SetPixelBackdrop(cb, T.emptySlotBg, T.emptySlotBd)
cb.label:SetTextColor(T.passive[1], T.passive[2], T.passive[3])
end
end
end
function SFrames.TalentTree:UpdateSimModeLabel()
if self.simMode then
local viewing = self.viewingClass or self.playerClass
local viewName = viewing or "?"
for _, c in ipairs(CLASS_LIST) do
if c.key == viewing then viewName = c.name; break end
end
self.frame.simModeText:SetText("|c" .. GetHex() .. "模拟: " .. viewName .. "|r")
self.frame.simLabel:SetText("|c" .. GetHex() .. "[模拟] 总点数: 51 左键: 加点 右键: 撤销|r")
self.frame.classBar:Show()
self:UpdateClassBar()
if not IsViewingOwnClass(self) then
self.frame.btnApply:Hide()
else
self.frame.btnApply:Show()
end
else
self.frame.simModeText:SetText("|cff888888模拟加点: 关|r")
self.frame.simLabel:SetText("")
self.frame.classBar:Hide()
self.frame.btnApply:Show()
if self.viewingClass and self.viewingClass ~= self.playerClass then
self.viewingClass = self.playerClass
self.virtualPoints = {}
self:DestroyTrees()
self:BuildTrees()
self:Update()
return
end
end
self:ResetVirtualPoints()
end
--------------------------------------------------------------------------------
-- Switch class
--------------------------------------------------------------------------------
function SFrames.TalentTree:SwitchViewClass(classKey)
if not self.simMode then return end
if classKey == (self.viewingClass or self.playerClass) then return end
if classKey ~= self.playerClass then
local cache = GetCache()
if not cache[classKey] then
UIErrorsFrame:AddMessage("该职业天赋数据尚未缓存,请先用该职业角色打开天赋面板", 1, 0.5, 0.5, 1)
return
end
end
self.viewingClass = classKey
self.virtualPoints = {}
self:DestroyTrees()
self:BuildTrees()
self:UpdateSimModeLabel()
self:Update()
end
--------------------------------------------------------------------------------
-- Destroy / Rebuild trees
--------------------------------------------------------------------------------
function SFrames.TalentTree:DestroyTrees()
if self.tabs then
for _, tabData in pairs(self.tabs) do
if type(tabData) == "table" and tabData.frame then
tabData.frame:Hide()
tabData.frame:ClearAllPoints()
tabData.frame:SetParent(nil)
end
end
end
self.tabs = {}
self.treesBuilt = false
end
--------------------------------------------------------------------------------
-- Build trees (from API or cache)
--------------------------------------------------------------------------------
function SFrames.TalentTree:BuildTrees()
if self.treesBuilt then return end
self.treesBuilt = true
if IsViewingOwnClass(self) then
CacheCurrentClassData()
end
local treeTop = -38
if self.simMode then treeTop = -52 end
local numTabs = TT_GetNumTabs(self)
for t = 1, numTabs do
local name, icon, pointsSpent, background = TT_GetTabInfo(self, t)
local tabFrame = CreateFrame("Frame", nil, self.frame)
tabFrame:SetWidth(TAB_WIDTH)
tabFrame:SetHeight(FRAME_HEIGHT - 90)
local offsetX = 10 + ((t - 1) * (TAB_WIDTH + 5))
tabFrame:SetPoint("TOPLEFT", self.frame, "TOPLEFT", offsetX, treeTop)
SetRoundBackdrop(tabFrame, T.tabBg, T.tabBorder)
if background and background ~= "" then
local bg = tabFrame:CreateTexture(nil, "BACKGROUND")
bg:SetTexture("Interface\\TalentFrame\\" .. background)
bg:SetPoint("TOPLEFT", tabFrame, "TOPLEFT", 3, -3)
bg:SetPoint("BOTTOMRIGHT", tabFrame, "BOTTOMRIGHT", -3, 3)
bg:SetAlpha(0.35)
end
local tTitle = MakeFS(tabFrame, 14, "CENTER", T.titleColor)
tTitle:SetPoint("TOP", tabFrame, "TOP", 0, -10)
tTitle:SetText("|c" .. GetHex() .. (name or "") .. "|r")
local tPoints = MakeFS(tabFrame, 12, "CENTER", T.dimText)
tPoints:SetPoint("TOP", tTitle, "BOTTOM", 0, -2)
tabFrame.pointsText = tPoints
self.tabs[t] = { frame = tabFrame, talents = {}, grid = {} }
local GRID_PAD_X = math.floor((TAB_WIDTH - (4 * ICON_SIZE + 3 * ICON_SPACING_X)) / 2)
local numTalents = TT_GetNumTalents(self, t)
for i = 1, numTalents do
local tName, tIcon, tier, column, rank, maxRank = TT_GetTalentInfo(self, t, i)
if not tName then break end
if not self.tabs[t].grid[tier] then self.tabs[t].grid[tier] = {} end
local btn = CreateFrame("Button", "SFramesTalent_" .. (self.viewingClass or "own") .. "_" .. t .. "_" .. i, tabFrame)
btn:SetWidth(ICON_SIZE)
btn:SetHeight(ICON_SIZE)
local x = (column - 1) * (ICON_SIZE + ICON_SPACING_X) + GRID_PAD_X
local y = -(tier - 1) * (ICON_SIZE + ICON_SPACING_Y) - 44
btn:SetPoint("TOPLEFT", tabFrame, "TOPLEFT", x, y)
btn.icon = btn:CreateTexture(nil, "ARTWORK")
btn.icon:SetTexture(tIcon)
btn.icon:SetAllPoints()
btn.borderTex = btn:CreateTexture(nil, "OVERLAY")
btn.borderTex:SetTexture("Interface\\Buttons\\UI-Quickslot2")
btn.borderTex:SetWidth(ICON_SIZE * 1.5)
btn.borderTex:SetHeight(ICON_SIZE * 1.5)
btn.borderTex:SetPoint("CENTER", btn, "CENTER", 0, 0)
local rankFrame = CreateFrame("Frame", nil, btn)
rankFrame:SetAllPoints(btn)
rankFrame:SetFrameLevel(btn:GetFrameLevel() + 2)
btn.rankBg = rankFrame:CreateTexture(nil, "BACKGROUND")
btn.rankBg:SetTexture(0, 0, 0, 0.75)
btn.rankBg:SetWidth(20)
btn.rankBg:SetHeight(12)
btn.rankBg:SetPoint("BOTTOMRIGHT", rankFrame, "BOTTOMRIGHT", -1, 1)
btn.rankBg:Hide()
btn.rankText = MakeFS(rankFrame, 10, "RIGHT")
btn.rankText:SetPoint("BOTTOMRIGHT", rankFrame, "BOTTOMRIGHT", -2, 2)
btn.rankText:SetShadowColor(0, 0, 0, 1)
btn.rankText:SetShadowOffset(1, -1)
btn.rankText:SetText("")
btn.tab = t
btn.index = i
btn.tier = tier
btn.maxRank = maxRank
btn.talentName = tName
local cachedDesc = nil
if not IsViewingOwnClass(self) then
local cd = GetCache()[self.viewingClass]
if cd and cd[t] and cd[t].talents[i] then
cachedDesc = cd[t].talents[i].desc
end
end
btn.cachedDesc = cachedDesc
if IsViewingOwnClass(self) then
btn:SetScript("OnEnter", function()
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetTalent(this.tab, this.index)
GameTooltip:Show()
end)
else
btn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:AddLine(this.talentName or "?", 1, 1, 1)
local vr = SFrames.TalentTree:GetVirtualRank(this.tab, this.index)
GameTooltip:AddLine("等级 " .. vr .. "/" .. (this.maxRank or "?"), 0.7, 0.7, 0.7)
if this.cachedDesc then
GameTooltip:AddLine(" ")
for _, line in ipairs(this.cachedDesc) do
GameTooltip:AddLine(line, 1, 0.82, 0, 1)
end
end
GameTooltip:Show()
end)
end
btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:SetScript("OnClick", function()
SFrames.TalentTree:OnTalentClick(this, arg1)
end)
self.tabs[t].talents[i] = btn
self.tabs[t].grid[tier][column] = btn
end
-- Dependency lines
for i = 1, numTalents do
local prereqTier, prereqColumn = TT_GetTalentPrereqs(self, t, i)
if prereqTier and prereqColumn then
local btn = self.tabs[t].talents[i]
local pBtn = self.tabs[t].grid[prereqTier] and self.tabs[t].grid[prereqTier][prereqColumn]
if pBtn then
btn.prereqIndex = pBtn.index
btn.prereqLines = {}
local _, _, tier, column = TT_GetTalentInfo(self, t, i)
local pX = (prereqColumn - 1) * (ICON_SIZE + ICON_SPACING_X) + GRID_PAD_X
local pY = -(prereqTier - 1) * (ICON_SIZE + ICON_SPACING_Y) - 44
local cX = (column - 1) * (ICON_SIZE + ICON_SPACING_X) + GRID_PAD_X
local cY = -(tier - 1) * (ICON_SIZE + ICON_SPACING_Y) - 44
local pCenterX = pX + (ICON_SIZE / 2)
local pCenterY = pY - (ICON_SIZE / 2)
local pBottomY = pY - ICON_SIZE
local pRightX = pX + ICON_SIZE
local tCenterX = cX + (ICON_SIZE / 2)
local tTopY = cY
local tLeftX = cX
local tRightX = cX + ICON_SIZE
local function CreateLine(x1, y1, x2, y2)
local line = tabFrame:CreateTexture(nil, "BACKGROUND")
line:SetTexture(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
if math.abs(x1 - x2) < 0.1 then
line:SetWidth(2)
line:SetHeight(math.abs(y1 - y2))
line:SetPoint("TOP", tabFrame, "TOPLEFT", x1, math.max(y1, y2))
else
line:SetHeight(2)
line:SetWidth(math.abs(x1 - x2))
line:SetPoint("LEFT", tabFrame, "TOPLEFT", math.min(x1, x2), y1)
end
table.insert(btn.prereqLines, line)
end
if prereqTier == tier then
if prereqColumn < column then
CreateLine(pRightX, pCenterY, tLeftX, pCenterY)
else
CreateLine(pX, pCenterY, tRightX, pCenterY)
end
elseif prereqColumn == column then
CreateLine(pCenterX, pBottomY, tCenterX, tTopY)
else
local midY = pBottomY - (ICON_SPACING_Y / 2) + 2
CreateLine(pCenterX, pBottomY, pCenterX, midY)
CreateLine(pCenterX, midY, tCenterX, midY)
CreateLine(tCenterX, midY, tCenterX, tTopY)
end
end
end
end
end
end
--------------------------------------------------------------------------------
-- Hooks
--------------------------------------------------------------------------------
function SFrames.TalentTree:HookVanillaUI()
local self = SFrames.TalentTree
local function ShowCustomTalentFrame()
if not self.frame then return end
if not self.frame:IsShown() then
self:BuildTrees()
self.frame:Show()
self:Update()
end
end
local function ToggleCustomTalentFrame()
if not self.frame then return end
if self.frame:IsShown() then
self.frame:Hide()
else
ShowCustomTalentFrame()
end
end
ToggleTalentFrame = ToggleCustomTalentFrame
if not self.hookedShowUIPanel then
self.hookedShowUIPanel = true
local orig_ShowUIPanel = ShowUIPanel
ShowUIPanel = function(frame)
if frame and frame.GetName then
local fName = frame:GetName()
if fName == "TalentFrame" or fName == "TalentMicroButtonAlert" then
ShowCustomTalentFrame()
return
end
end
if orig_ShowUIPanel then
orig_ShowUIPanel(frame)
end
end
local orig_HideUIPanel = HideUIPanel
HideUIPanel = function(frame)
if frame and frame.GetName and frame:GetName() == "TalentFrame" then
if self.frame then self.frame:Hide() end
return
end
if orig_HideUIPanel then
orig_HideUIPanel(frame)
end
end
end
local orig_TalentFrame_LoadUI = TalentFrame_LoadUI
if orig_TalentFrame_LoadUI then
TalentFrame_LoadUI = function()
orig_TalentFrame_LoadUI()
end
end
end
--------------------------------------------------------------------------------
-- Virtual point helpers
--------------------------------------------------------------------------------
function SFrames.TalentTree:GetVirtualRank(tab, index)
if self.virtualPoints[tab] and self.virtualPoints[tab][index] then
return self.virtualPoints[tab][index]
end
if self.simMode then return 0 end
if IsViewingOwnClass(self) then
local name, icon, tier, column, rank, maxRank = GetTalentInfo(tab, index)
return rank or 0
end
return 0
end
function SFrames.TalentTree:GetVirtualTreePoints(tab)
local total = 0
local n = TT_GetNumTalents(self, tab)
for i = 1, n do
total = total + self:GetVirtualRank(tab, i)
end
return total
end
function SFrames.TalentTree:GetRemainingUnspent()
if self.simMode then
local unspent = 51
local numTabs = TT_GetNumTabs(self)
for tb = 1, numTabs do
unspent = unspent - self:GetVirtualTreePoints(tb)
end
return unspent
else
local unspent = UnitCharacterPoints("player")
for tb = 1, GetNumTalentTabs() do
for idx = 1, GetNumTalents(tb) do
local name, icon, tier, column, realRank = GetTalentInfo(tb, idx)
local virtRank = self:GetVirtualRank(tb, idx)
unspent = unspent - (virtRank - (realRank or 0))
end
end
return unspent
end
end
--------------------------------------------------------------------------------
-- Talent click
--------------------------------------------------------------------------------
function SFrames.TalentTree:OnTalentClick(btn, buttonType)
if not self.simMode and IsViewingOwnClass(self) then
if buttonType == "LeftButton" then
local tName, tIcon, tier, column, rank, maxRank, isExceptional, meetsPrereq = GetTalentInfo(btn.tab, btn.index)
local unspent = UnitCharacterPoints("player")
if rank < maxRank and meetsPrereq and unspent > 0 then
StaticPopupDialogs["NANAMI_CONFIRM_TALENT"] = {
text = "确定学习天赋 [" .. tName .. "] ?\n(等级 " .. rank .. "" .. (rank+1) .. " / " .. maxRank .. "",
button1 = "确认",
button2 = "取消",
OnAccept = function()
LearnTalent(btn.tab, btn.index)
end,
timeout = 0,
whileDead = true,
hideOnEscape = true,
}
StaticPopup_Show("NANAMI_CONFIRM_TALENT")
end
end
return
end
if not self.simMode then return end
local _, _, tier, _, _, maxRank = TT_GetTalentInfo(self, btn.tab, btn.index)
local virtRank = self:GetVirtualRank(btn.tab, btn.index)
maxRank = maxRank or btn.maxRank
if not self.virtualPoints[btn.tab] then self.virtualPoints[btn.tab] = {} end
if not self.virtualPoints[btn.tab][btn.index] then
self.virtualPoints[btn.tab][btn.index] = self:GetVirtualRank(btn.tab, btn.index)
end
if buttonType == "LeftButton" then
local unspent = self:GetRemainingUnspent()
if unspent > 0 and virtRank < maxRank then
local treePts = self:GetVirtualTreePoints(btn.tab)
local prereqMet = true
if btn.prereqIndex then
local pVirtRank = self:GetVirtualRank(btn.tab, btn.prereqIndex)
local pMaxRank = self.tabs[btn.tab].talents[btn.prereqIndex].maxRank
if pVirtRank < pMaxRank then prereqMet = false end
end
if treePts >= (tier - 1) * 5 and prereqMet then
self.virtualPoints[btn.tab][btn.index] = virtRank + 1
end
end
elseif buttonType == "RightButton" then
if virtRank > 0 then
local canRevert = true
local treePts = self:GetVirtualTreePoints(btn.tab)
local numT = TT_GetNumTalents(self, btn.tab)
for i = 1, numT do
local childVirtRank = self:GetVirtualRank(btn.tab, i)
if childVirtRank > 0 then
local _, _, qTier = TT_GetTalentInfo(self, btn.tab, i)
if qTier and qTier > tier and (treePts - 1) < (qTier - 1) * 5 then
canRevert = false
end
local cBtn = self.tabs[btn.tab].talents[i]
if cBtn and cBtn.prereqIndex == btn.index then
if childVirtRank > 0 and (virtRank - 1) < maxRank then
canRevert = false
end
end
end
end
if canRevert then
self.virtualPoints[btn.tab][btn.index] = virtRank - 1
else
UIErrorsFrame:AddMessage("无法取消点数:其他已点天赋依赖于此天赋", 1, 0, 0, 1)
end
end
end
self:Update()
end
function SFrames.TalentTree:ResetVirtualPoints()
self.virtualPoints = {}
self:Update()
end
function SFrames.TalentTree:ApplyVirtualPoints()
if not self.simMode then return end
if not IsViewingOwnClass(self) then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[错误]|r 只能应用本职业的天赋。")
return
end
local canApply = true
for tb = 1, GetNumTalentTabs() do
for idx = 1, GetNumTalents(tb) do
local name, icon, tier, column, realRank = GetTalentInfo(tb, idx)
local virtRank = self:GetVirtualRank(tb, idx)
if realRank > virtRank then
canApply = false
end
end
end
if not canApply then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[错误]|r 模拟的点数未包含您已学习的天赋,无法直接应用。")
return
end
if not self.applyQueue then self.applyQueue = {} end
self.applyQueue = {}
for tb = 1, GetNumTalentTabs() do
for idx = 1, GetNumTalents(tb) do
local name, icon, tier, column, realRank = GetTalentInfo(tb, idx)
local virtRank = self:GetVirtualRank(tb, idx)
local diff = virtRank - realRank
if diff > 0 then
for i = 1, diff do
table.insert(self.applyQueue, {tab = tb, index = idx})
end
end
end
end
if table.getn(self.applyQueue) > 0 then
self.frame:SetScript("OnUpdate", function()
if table.getn(SFrames.TalentTree.applyQueue) > 0 then
local t = table.remove(SFrames.TalentTree.applyQueue, 1)
LearnTalent(t.tab, t.index)
else
this:SetScript("OnUpdate", nil)
SFrames.TalentTree.simMode = false
SFrames.TalentTree:UpdateSimModeLabel()
end
end)
else
DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 没有新的天赋点数需要应用。")
end
end
--------------------------------------------------------------------------------
-- Update display
--------------------------------------------------------------------------------
function SFrames.TalentTree:Update()
if not self.frame or not self.frame:IsShown() then return end
if not self.tabs or not self.tabs[1] then return end
local unspent = self:GetRemainingUnspent()
local realUnspent = UnitCharacterPoints("player")
local numTabs = TT_GetNumTabs(self)
for tb = 1, numTabs do
if not self.tabs[tb] then break end
local treePts = self:GetVirtualTreePoints(tb)
self.tabs[tb].frame.pointsText:SetText("已用: |c" .. GetHex() .. treePts .. "|r")
local numT = TT_GetNumTalents(self, tb)
for idx = 1, numT do
local btn = self.tabs[tb].talents[idx]
if btn then
local _, _, tier, column, rank, maxRank = TT_GetTalentInfo(self, tb, idx)
if not IsViewingOwnClass(self) then rank = 0 end
local virtRank = self:GetVirtualRank(tb, idx)
if virtRank > 0 or (not self.simMode and rank and rank > 0) then
local displayRank = virtRank
if not self.simMode and displayRank == 0 and rank and rank > 0 then
displayRank = rank
end
btn.rankText:SetText(displayRank .. "/" .. maxRank)
btn.rankBg:Show()
btn.rankText:Show()
else
btn.rankText:SetText("")
btn.rankText:Hide()
btn.rankBg:Hide()
end
local prereqMet = true
if btn.prereqIndex then
local pVirtRank = self:GetVirtualRank(tb, btn.prereqIndex)
local pMaxRank = self.tabs[tb].talents[btn.prereqIndex].maxRank
if pVirtRank < pMaxRank then
prereqMet = false
end
end
local isLearnable = (unspent > 0 and treePts >= (tier - 1) * 5 and virtRank < maxRank and prereqMet)
if btn.prereqLines then
for _, line in ipairs(btn.prereqLines) do
if prereqMet then
line:SetTexture(T.accent[1], T.accent[2], T.accent[3], 0.8)
else
line:SetTexture(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4])
end
end
end
local isVirtual = (not self.simMode and virtRank > (rank or 0)) or (self.simMode and virtRank > 0)
if (not self.simMode and isVirtual) or (self.simMode and isVirtual and virtRank < maxRank) then
btn.rankText:SetTextColor(T.title[1], T.title[2], T.title[3])
btn.icon:SetVertexColor(T.accentLight[1], T.accentLight[2], T.accentLight[3])
btn.borderTex:SetVertexColor(T.accent[1], T.accent[2], T.accent[3])
elseif virtRank >= maxRank then
btn.rankText:SetTextColor(T.title[1], T.title[2], T.title[3])
btn.icon:SetVertexColor(1, 1, 1)
btn.borderTex:SetVertexColor(T.accent[1], T.accent[2], T.accent[3])
elseif virtRank > 0 then
btn.rankText:SetTextColor(0, 1, 0)
btn.icon:SetVertexColor(1, 1, 1)
btn.borderTex:SetVertexColor(0, 0.7, 0)
elseif isLearnable then
btn.rankText:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
btn.icon:SetVertexColor(1, 1, 1)
btn.borderTex:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
else
btn.rankText:SetTextColor(T.passive[1], T.passive[2], T.passive[3])
btn.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
btn.borderTex:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
end
end
end
end
if self.simMode then
if IsViewingOwnClass(self) then
self.frame.pointsText:SetText("剩余天赋点: |c" .. GetHex() .. unspent .. " |cff888888(总 " .. realUnspent .. ")|r")
else
self.frame.pointsText:SetText("剩余天赋点: |c" .. GetHex() .. unspent .. "|r")
end
else
self.frame.pointsText:SetText("剩余天赋点: |c" .. GetHex() .. realUnspent .. "|r")
end
end