Files
Nanami-UI/Units/TalentTree.lua
rucky 40d37dc8c4 天赋修改更直接展示预览时天赋变更信息
世界地图揭示迷雾全修复
2026-03-23 18:13:03 +08:00

2380 lines
90 KiB
Lua
Raw Permalink 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 = 550
--------------------------------------------------------------------------------
-- 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 } },
}
local CLASS_KEY_LOOKUP = {}
for _, c in ipairs(CLASS_LIST) do
CLASS_KEY_LOOKUP[string.lower(c.key)] = c.key
CLASS_KEY_LOOKUP[c.key] = c.key
end
--------------------------------------------------------------------------------
-- 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 code encode / decode (turtlecraft.gg compatible)
--------------------------------------------------------------------------------
local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
local B64_LOOKUP = {}
for i = 1, 64 do B64_LOOKUP[string.sub(B64, i, i)] = i - 1 end
local function EncodeTreeGrid(grid)
local lastNonZero = 0
for i = 1, 28 do
if (grid[i] or 0) > 0 then lastNonZero = i end
end
if lastNonZero == 0 then return "" end
local bits = {}
for i = 1, lastNonZero do
local v = grid[i] or 0
table.insert(bits, math.mod(math.floor(v / 4), 2))
table.insert(bits, math.mod(math.floor(v / 2), 2))
table.insert(bits, math.mod(v, 2))
end
while math.mod(table.getn(bits), 6) ~= 0 do
table.insert(bits, 0)
end
local out = ""
for i = 1, table.getn(bits), 6 do
local val = bits[i]*32 + bits[i+1]*16 + bits[i+2]*8 + bits[i+3]*4 + bits[i+4]*2 + bits[i+5]
out = out .. string.sub(B64, val + 1, val + 1)
end
return out
end
local function DecodeTreeGrid(str)
local grid = {}
if not str or str == "" then
for i = 1, 28 do grid[i] = 0 end
return grid
end
local bits = {}
for ci = 1, string.len(str) do
local ch = string.sub(str, ci, ci)
local val = B64_LOOKUP[ch]
if not val then
for i = 1, 28 do grid[i] = 0 end
return grid
end
table.insert(bits, math.mod(math.floor(val / 32), 2))
table.insert(bits, math.mod(math.floor(val / 16), 2))
table.insert(bits, math.mod(math.floor(val / 8), 2))
table.insert(bits, math.mod(math.floor(val / 4), 2))
table.insert(bits, math.mod(math.floor(val / 2), 2))
table.insert(bits, math.mod(val, 2))
end
local idx = 1
for i = 1, table.getn(bits) - 2, 3 do
grid[idx] = bits[i]*4 + bits[i+1]*2 + bits[i+2]
idx = idx + 1
if idx > 28 then break end
end
while idx <= 28 do grid[idx] = 0; idx = idx + 1 end
return grid
end
--------------------------------------------------------------------------------
-- Chat link: rewrite [NUI:...] tokens in chat into clickable hyperlinks
--------------------------------------------------------------------------------
local function FilterNanamiTalentLink(msg)
if not msg or not string.find(msg, "[NUI:", 1, true) then return msg end
return (string.gsub(msg, "%[NUI:([^:]+):([^:]+):(%u+):([^%]]+)%]", function(name, pts, ck, code)
return "|cffFFB3D9|Hnanami:talent:" .. ck .. ":" .. code .. "|h[Nanami天赋: " .. name .. " " .. pts .. "]|h|r"
end))
end
--------------------------------------------------------------------------------
-- Builds storage
--------------------------------------------------------------------------------
local function GetBuildsStore()
if not SFramesGlobalDB then SFramesGlobalDB = {} end
if not SFramesGlobalDB.talentBuilds then SFramesGlobalDB.talentBuilds = {} end
return SFramesGlobalDB.talentBuilds
end
--------------------------------------------------------------------------------
-- Talent data cache (stores talent trees per class in SFramesGlobalDB)
-- Falls back to NanamiTalentDefaultDB for classes not yet cached locally
--------------------------------------------------------------------------------
local mergedCacheProxy = nil
local function GetCache()
if not SFramesGlobalDB then SFramesGlobalDB = {} end
if not SFramesGlobalDB.talentCache then SFramesGlobalDB.talentCache = {} end
if not mergedCacheProxy then
mergedCacheProxy = setmetatable({}, {
__index = function(_, k)
local v = SFramesGlobalDB.talentCache[k]
if v ~= nil then return v end
if NanamiTalentDefaultDB then return NanamiTalentDefaultDB[k] end
return nil
end,
__newindex = function(_, k, v)
SFramesGlobalDB.talentCache[k] = v
end,
})
end
return mergedCacheProxy
end
local function HasCacheData(classKey)
if SFramesGlobalDB and SFramesGlobalDB.talentCache and SFramesGlobalDB.talentCache[classKey] then
return true, "local"
end
if NanamiTalentDefaultDB and NanamiTalentDefaultDB[classKey] then
return true, "default"
end
return false, nil
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()
CacheCurrentClassData()
SFrames:RegisterEvent("CHARACTER_POINTS_CHANGED", function()
CacheCurrentClassData()
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)
self:HookChatTalentLinks()
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])
if this.classKey ~= SFrames.TalentTree.playerClass then
local hasData, source = HasCacheData(this.classKey)
if not hasData then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
GameTooltip:AddLine("该职业天赋数据不可用", 1, 0.5, 0.5)
GameTooltip:AddLine("需先用该职业角色登录以缓存数据", 0.7, 0.7, 0.7)
GameTooltip:Show()
elseif source == "default" then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
GameTooltip:AddLine("使用内置默认数据", 0.7, 0.7, 0.7)
GameTooltip:AddLine("登录该职业可获取最新数据", 0.5, 0.5, 0.5)
GameTooltip:Show()
end
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, 42)
f.pointsText:SetWidth(FRAME_WIDTH - 220)
f.simLabel = MakeFS(f.overlay, 11, "LEFT", T.titleColor)
f.simLabel:SetPoint("BOTTOMLEFT", f.overlay, "BOTTOMLEFT", 14, 18)
f.simLabel:SetWidth(FRAME_WIDTH - 120)
f.simLabel:SetText("")
local btnLevel = f:GetFrameLevel() + 11
-- Bottom buttons - Row 1 (lower): preview mode / reset / apply
f.btnApply = CreateFrame("Button", nil, f)
f.btnApply:SetWidth(100)
f.btnApply:SetHeight(26)
f.btnApply:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 12)
f.btnApply:SetFrameLevel(btnLevel)
StyleButton(f.btnApply, "应用天赋")
f.btnApply.nanamiTooltip = { "应用天赋", "将所有预览点数提交至服务器。" }
f.btnApply:SetScript("OnClick", function() SFrames.TalentTree:ApplyVirtualPoints() end)
f.btnApply:Hide()
f.btnReset = CreateFrame("Button", nil, f)
f.btnReset:SetWidth(100)
f.btnReset:SetHeight(26)
f.btnReset:SetPoint("RIGHT", f.btnApply, "LEFT", -8, 0)
f.btnReset:SetFrameLevel(btnLevel)
StyleButton(f.btnReset, "重置预览")
f.btnReset:SetScript("OnClick", function() SFrames.TalentTree:ResetVirtualPoints() end)
f.btnReset:Hide()
f.btnSimMode = CreateFrame("Button", nil, f)
f.btnSimMode:SetWidth(110)
f.btnSimMode:SetHeight(26)
f.btnSimMode:SetPoint("RIGHT", f.btnReset, "LEFT", -8, 0)
f.btnSimMode:SetFrameLevel(btnLevel)
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)
-- Bottom buttons - Row 2 (upper): builds / save / import / export
f.btnExport = CreateFrame("Button", nil, f)
f.btnExport:SetWidth(60)
f.btnExport:SetHeight(26)
f.btnExport:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 42)
f.btnExport:SetFrameLevel(btnLevel)
StyleButton(f.btnExport, "导出")
f.btnExport.nanamiTooltip = { "导出天赋", "导出天赋代码 (含职业标识)\n可发送到聊天或分享给其他玩家。" }
f.btnExport:SetScript("OnClick", function() SFrames.TalentTree:ShowExportDialog() end)
f.btnImport = CreateFrame("Button", nil, f)
f.btnImport:SetWidth(60)
f.btnImport:SetHeight(26)
f.btnImport:SetPoint("RIGHT", f.btnExport, "LEFT", -6, 0)
f.btnImport:SetFrameLevel(btnLevel)
StyleButton(f.btnImport, "导入")
f.btnImport.nanamiTooltip = { "导入天赋", "粘贴天赋代码 (含职业标识)\n或完整 URL自动切换对应职业预览。" }
f.btnImport:SetScript("OnClick", function() SFrames.TalentTree:ShowImportDialog() end)
f.btnSave = CreateFrame("Button", nil, f)
f.btnSave:SetWidth(60)
f.btnSave:SetHeight(26)
f.btnSave:SetPoint("RIGHT", f.btnImport, "LEFT", -6, 0)
f.btnSave:SetFrameLevel(btnLevel)
StyleButton(f.btnSave, "保存")
f.btnSave.nanamiTooltip = { "保存方案", "将当前天赋配置保存为命名方案。" }
f.btnSave:SetScript("OnClick", function() SFrames.TalentTree:ShowSaveDialog() end)
f.btnBuilds = CreateFrame("Button", nil, f)
f.btnBuilds:SetWidth(80)
f.btnBuilds:SetHeight(26)
f.btnBuilds:SetPoint("RIGHT", f.btnSave, "LEFT", -6, 0)
f.btnBuilds:SetFrameLevel(btnLevel)
StyleButton(f.btnBuilds, "方案管理")
f.btnBuilds.nanamiTooltip = { "方案管理", "查看、加载、删除已保存的天赋方案。\n可浏览所有职业的方案。" }
f.btnBuilds:SetScript("OnClick", function() SFrames.TalentTree:ToggleBuildsPanel() 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
for ci, cinfo in ipairs(CLASS_LIST) do
local cb = self.frame.classBtns[ci]
if not cb then break end
local hasData, source = HasCacheData(cinfo.key)
if cinfo.key == self.playerClass then hasData = true; source = "local" end
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 and source == "local" 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)
elseif hasData and source == "default" then
SetPixelBackdrop(cb, T.slotBg, T.slotBorder)
cb.label:SetTextColor(cinfo.color[1] * 0.6, cinfo.color[2] * 0.6, cinfo.color[3] * 0.6)
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()
self.frame.btnReset:Show()
if IsViewingOwnClass(self) then
self.frame.btnApply:Show()
else
self.frame.btnApply:Hide()
end
else
self.frame.simModeText:SetText("|cff888888预览模式: 关|r")
self.frame.simLabel:SetText("")
self.frame.classBar:Hide()
self.frame.btnApply:Hide()
self.frame.btnReset:Hide()
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 hasData = HasCacheData(classKey)
if not hasData 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 - 120)
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, -14)
tTitle:SetText("|c" .. GetHex() .. (name or "") .. "|r")
local tPoints = MakeFS(tabFrame, 12, "CENTER", T.dimText)
tPoints:SetPoint("TOP", tTitle, "BOTTOM", 0, -4)
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 GRID_TOP = 50
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) - GRID_TOP
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
btn:SetScript("OnEnter", function()
SFrames.TalentTree:ShowTalentTooltip(this)
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) - GRID_TOP
local cX = (column - 1) * (ICON_SIZE + ICON_SPACING_X) + GRID_PAD_X
local cY = -(tier - 1) * (ICON_SIZE + ICON_SPACING_Y) - GRID_TOP
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
--------------------------------------------------------------------------------
-- Talent tooltip (shared by OnEnter and post-click refresh)
--------------------------------------------------------------------------------
function SFrames.TalentTree:ShowTalentTooltip(btn)
if IsViewingOwnClass(self) and not self.simMode then
GameTooltip_SetDefaultAnchor(GameTooltip, btn)
GameTooltip:SetTalent(btn.tab, btn.index)
GameTooltip:Show()
return
end
GameTooltip:SetOwner(btn, "ANCHOR_RIGHT")
GameTooltip:AddLine(btn.talentName or "?", 1, 1, 1)
local vr = self:GetVirtualRank(btn.tab, btn.index)
local mr = btn.maxRank or 1
GameTooltip:AddLine("等级 " .. vr .. "/" .. mr, 0.7, 0.7, 0.7)
local desc = nil
local classKey = self.viewingClass or self.playerClass
if NanamiTalentDefaultDB and NanamiTalentDefaultDB[classKey] then
local tabData = NanamiTalentDefaultDB[classKey][btn.tab]
if tabData and tabData.talents and tabData.talents[btn.index] then
desc = tabData.talents[btn.index].desc
end
end
if not desc then
local cd = GetCache()[classKey]
if cd and cd[btn.tab] and cd[btn.tab].talents[btn.index] then
desc = cd[btn.tab].talents[btn.index].desc
end
end
if desc then
local n = table.getn(desc)
if n == mr and desc[1] then
GameTooltip:AddLine(" ")
if vr == 0 then
GameTooltip:AddLine(desc[1], 1, 0.82, 0, 1)
else
GameTooltip:AddLine(desc[vr], 1, 0.82, 0, 1)
if vr < mr and desc[vr + 1] then
GameTooltip:AddLine(" ")
GameTooltip:AddLine("下一级:", 0, 1, 0)
GameTooltip:AddLine(desc[vr + 1], 1, 0.82, 0, 1)
end
end
else
GameTooltip:AddLine(" ")
for _, line in ipairs(desc) do
GameTooltip:AddLine(line, 1, 0.82, 0, 1)
end
end
end
GameTooltip:Show()
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()
if GameTooltip:IsVisible() then
self:ShowTalentTooltip(btn)
end
end
function SFrames.TalentTree:ResetVirtualPoints()
self.virtualPoints = {}
self:Update()
end
function SFrames.TalentTree:SetBottomButtonsEnabled(enabled)
local btns = { self.frame.btnApply, self.frame.btnReset, self.frame.btnSimMode,
self.frame.btnExport, self.frame.btnImport, self.frame.btnSave, self.frame.btnBuilds }
for _, b in ipairs(btns) do
if enabled then
b:Enable()
if b.nanamiLabel then b.nanamiLabel:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) end
else
b:Disable()
if b.nanamiLabel then b.nanamiLabel:SetTextColor(T.passive[1], T.passive[2], T.passive[3]) end
end
end
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
self.applyQueue = {}
for tb = 1, GetNumTalentTabs() do
local treeTalents = {}
for idx = 1, GetNumTalents(tb) do
local name, icon, tier, column, realRank, maxRank = GetTalentInfo(tb, idx)
local virtRank = self:GetVirtualRank(tb, idx)
local diff = virtRank - realRank
if diff > 0 then
for i = 1, diff do
table.insert(treeTalents, {
tab = tb, index = idx, tier = tier or 1,
name = name or "?", toRank = realRank + i, maxRank = maxRank or 1,
})
end
end
end
table.sort(treeTalents, function(a, b) return a.tier < b.tier end)
for _, entry in ipairs(treeTalents) do
table.insert(self.applyQueue, entry)
end
end
if table.getn(self.applyQueue) == 0 then
DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 没有新的天赋点数需要应用。")
return
end
local total = table.getn(self.applyQueue)
self.applyTotal = total
self.applyDone = 0
DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 正在应用天赋,请等待完成... (共 " .. total .. " 点)")
self.frame.simLabel:SetText("|cffff6600正在应用天赋请勿操作... 0/" .. total .. "|r")
self:SetBottomButtonsEnabled(false)
if not self.applyEventFrame then
self.applyEventFrame = CreateFrame("Frame", "NanamiTalentApplyFrame")
end
self.applyEventFrame:UnregisterAllEvents()
self.applyStallTimer = 0
self.applyWaiting = false
local function FinishApply()
self.applyEventFrame:UnregisterAllEvents()
self.applyEventFrame:SetScript("OnUpdate", nil)
self.applyQueue = {}
self.applyWaiting = false
self:SetBottomButtonsEnabled(true)
self.simMode = false
self:UpdateSimModeLabel()
DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 天赋应用完成!共应用 " .. (self.applyTotal or 0) .. " 个天赋点。")
self.frame.simLabel:SetText("|cff00ff00天赋应用完成|r")
end
local function TryNextTalent()
if table.getn(self.applyQueue) == 0 then
FinishApply()
return
end
local t = table.remove(self.applyQueue, 1)
self.applyDone = (self.applyDone or 0) + 1
self.frame.simLabel:SetText("|cffff6600应用中 " .. self.applyDone .. "/" .. total .. ": " .. t.name .. "|r")
DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. " > " .. t.name .. " (等级 " .. t.toRank .. "/" .. t.maxRank .. ")|r")
LearnTalent(t.tab, t.index)
self.applyWaiting = true
self.applyStallTimer = 0
end
self.applyEventFrame:RegisterEvent("CHARACTER_POINTS_CHANGED")
self.applyEventFrame:SetScript("OnEvent", function()
if not SFrames.TalentTree.applyWaiting then return end
SFrames.TalentTree.applyWaiting = false
SFrames.TalentTree.applyStallTimer = 0
TryNextTalent()
end)
self.applyEventFrame:SetScript("OnUpdate", function()
if not SFrames.TalentTree.applyWaiting then return end
SFrames.TalentTree.applyStallTimer = (SFrames.TalentTree.applyStallTimer or 0) + (arg1 or 0)
if SFrames.TalentTree.applyStallTimer >= 2.0 then
SFrames.TalentTree.applyStallTimer = 0
SFrames.TalentTree.applyWaiting = false
if table.getn(SFrames.TalentTree.applyQueue) > 0 then
TryNextTalent()
else
FinishApply()
end
end
end)
TryNextTalent()
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
--------------------------------------------------------------------------------
-- Encode current talents to turtlecraft-compatible string
--------------------------------------------------------------------------------
function SFrames.TalentTree:EncodeTalents()
local numTabs = TT_GetNumTabs(self)
local parts = {}
for t = 1, 3 do
local grid = {}
for pos = 1, 28 do grid[pos] = 0 end
if t <= numTabs then
local numT = TT_GetNumTalents(self, t)
for i = 1, numT do
local _, _, tier, column = TT_GetTalentInfo(self, t, i)
local rank = self:GetVirtualRank(t, i)
if tier and column then
grid[(tier - 1) * 4 + column] = rank
end
end
end
parts[t] = EncodeTreeGrid(grid)
end
return parts[1] .. "-" .. parts[2] .. "-" .. parts[3]
end
--------------------------------------------------------------------------------
-- Decode turtlecraft string into virtualPoints
--------------------------------------------------------------------------------
function SFrames.TalentTree:DecodeTalents(codeStr)
if not codeStr or codeStr == "" then return nil end
local s = codeStr
local pIdx = string.find(s, "points=", 1, true)
if pIdx then s = string.sub(s, pIdx + 7) end
local amp = string.find(s, "&", 1, true)
if amp then s = string.sub(s, 1, amp - 1) end
local treeParts = {}
local rest = s
while true do
local dash = string.find(rest, "-", 1, true)
if dash then
table.insert(treeParts, string.sub(rest, 1, dash - 1))
rest = string.sub(rest, dash + 1)
else
table.insert(treeParts, rest)
break
end
end
local grids = {}
for i = 1, 3 do
grids[i] = DecodeTreeGrid(treeParts[i] or "")
end
return grids
end
--------------------------------------------------------------------------------
-- Get points summary string (e.g. "31/20/0")
--------------------------------------------------------------------------------
function SFrames.TalentTree:GetPointsSummary()
local numTabs = TT_GetNumTabs(self)
local pts = {}
for t = 1, 3 do
if t <= numTabs then
pts[t] = self:GetVirtualTreePoints(t)
else
pts[t] = 0
end
end
return pts[1] .. "/" .. pts[2] .. "/" .. pts[3]
end
--------------------------------------------------------------------------------
-- Export dialog
--------------------------------------------------------------------------------
function SFrames.TalentTree:ShowExportDialog()
local code = self:EncodeTalents()
local classKey = self.viewingClass or self.playerClass
local className = classKey
for _, c in ipairs(CLASS_LIST) do
if c.key == classKey then className = c.name; break end
end
local urlBase = "https://talents.turtlecraft.gg/" .. string.lower(classKey) .. "?points="
if not self.exportFrame then
local ef = CreateFrame("Frame", "NanamiTalentExportFrame", UIParent)
ef:SetWidth(440)
ef:SetHeight(180)
ef:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
SetRoundBackdrop(ef, T.panelBg, T.panelBorder)
CreateShadow(ef, 4)
ef:EnableMouse(true)
ef:SetMovable(true)
ef:RegisterForDrag("LeftButton")
ef:SetScript("OnDragStart", function() this:StartMoving() end)
ef:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
ef:SetFrameStrata("DIALOG")
ef:Hide()
ef.title = MakeFS(ef, 13, "CENTER", T.titleColor)
ef.title:SetPoint("TOP", ef, "TOP", 0, -12)
ef.desc = MakeFS(ef, 10, "CENTER", T.dimText)
ef.desc:SetPoint("TOP", ef.title, "BOTTOM", 0, -4)
ef.desc:SetText("Ctrl+A 全选Ctrl+C 复制")
local codeLabel = MakeFS(ef, 10, "LEFT", T.dimText)
codeLabel:SetPoint("TOPLEFT", ef, "TOPLEFT", 20, -48)
codeLabel:SetText("天赋代码:")
local eb = CreateFrame("EditBox", "NanamiTalentExportEB", ef, "InputBoxTemplate")
eb:SetWidth(340)
eb:SetHeight(22)
eb:SetPoint("LEFT", codeLabel, "RIGHT", 6, 0)
eb:SetFont(GetFont(), 11)
eb:SetAutoFocus(true)
eb:SetScript("OnEscapePressed", function() ef:Hide() end)
eb:SetScript("OnEnterPressed", function() ef:Hide() end)
ef.editBox = eb
local urlLabel = MakeFS(ef, 10, "LEFT", T.dimText)
urlLabel:SetPoint("TOPLEFT", codeLabel, "BOTTOMLEFT", 0, -10)
urlLabel:SetText("完整链接:")
local urlEB = CreateFrame("EditBox", "NanamiTalentExportUrlEB", ef, "InputBoxTemplate")
urlEB:SetWidth(286)
urlEB:SetHeight(22)
urlEB:SetPoint("LEFT", urlLabel, "RIGHT", 6, 0)
urlEB:SetFont(GetFont(), 10)
urlEB:SetAutoFocus(false)
urlEB:SetScript("OnEscapePressed", function() ef:Hide() end)
urlEB:SetScript("OnEnterPressed", function() ef:Hide() end)
ef.urlEditBox = urlEB
local copyUrlBtn = CreateFrame("Button", nil, ef)
copyUrlBtn:SetWidth(50)
copyUrlBtn:SetHeight(22)
copyUrlBtn:SetPoint("LEFT", urlEB, "RIGHT", 4, 0)
StyleButton(copyUrlBtn, "选中")
copyUrlBtn.nanamiTooltip = { "选中链接", "选中链接文本以便 Ctrl+C 复制" }
copyUrlBtn:SetScript("OnClick", function()
ef.urlEditBox:SetFocus()
ef.urlEditBox:HighlightText()
end)
local chatBtn = CreateFrame("Button", nil, ef)
chatBtn:SetWidth(80)
chatBtn:SetHeight(24)
chatBtn:SetPoint("BOTTOMRIGHT", ef, "BOTTOM", -4, 10)
StyleButton(chatBtn, "发送到聊天")
chatBtn.nanamiTooltip = { "发送到聊天", "将天赋方案发送到聊天频道,\n同插件用户可点击链接直接导入。" }
chatBtn:SetScript("OnClick", function()
local eFrame = SFrames.TalentTree.exportFrame
SFrames.TalentTree:SendTalentToChat(eFrame.currentCode, eFrame.currentClassKey, eFrame.currentPoints)
eFrame:Hide()
end)
ef.chatBtn = chatBtn
local closeBtn = CreateFrame("Button", nil, ef)
closeBtn:SetWidth(60)
closeBtn:SetHeight(24)
closeBtn:SetPoint("BOTTOMLEFT", ef, "BOTTOM", 4, 10)
StyleButton(closeBtn, "关闭")
closeBtn:SetScript("OnClick", function() ef:Hide() end)
self.exportFrame = ef
end
local shareCode = classKey .. ":" .. code
local pointsSummary = self:GetPointsSummary()
self.exportFrame.currentCode = code
self.exportFrame.currentClassKey = classKey
self.exportFrame.currentPoints = pointsSummary
self.exportFrame.title:SetText("|c" .. GetHex() .. "导出天赋 - " .. className .. " (" .. pointsSummary .. ")|r")
self.exportFrame.editBox:SetText(shareCode)
self.exportFrame.urlEditBox:SetText(urlBase .. code)
self.exportFrame:Show()
self.exportFrame.editBox:HighlightText()
self.exportFrame.editBox:SetFocus()
end
--------------------------------------------------------------------------------
-- Import dialog
--------------------------------------------------------------------------------
function SFrames.TalentTree:ShowImportDialog()
if not self.importFrame then
local imf = CreateFrame("Frame", "NanamiTalentImportFrame", UIParent)
imf:SetWidth(440)
imf:SetHeight(130)
imf:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
SetRoundBackdrop(imf, T.panelBg, T.panelBorder)
CreateShadow(imf, 4)
imf:EnableMouse(true)
imf:SetMovable(true)
imf:RegisterForDrag("LeftButton")
imf:SetScript("OnDragStart", function() this:StartMoving() end)
imf:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
imf:SetFrameStrata("DIALOG")
imf:Hide()
imf.title = MakeFS(imf, 13, "CENTER", T.titleColor)
imf.title:SetPoint("TOP", imf, "TOP", 0, -12)
imf.title:SetText("|c" .. GetHex() .. "导入天赋方案|r")
imf.desc = MakeFS(imf, 10, "CENTER", T.dimText)
imf.desc:SetPoint("TOP", imf.title, "BOTTOM", 0, -4)
imf.desc:SetText("粘贴天赋代码 (如 HUNTER:Eo--)、完整 URL 或聊天链接")
local eb = CreateFrame("EditBox", "NanamiTalentImportEB", imf, "InputBoxTemplate")
eb:SetWidth(400)
eb:SetHeight(24)
eb:SetPoint("TOP", imf.desc, "BOTTOM", 0, -8)
eb:SetFont(GetFont(), 12)
eb:SetAutoFocus(true)
eb:SetScript("OnEscapePressed", function() imf:Hide() end)
eb:SetScript("OnEnterPressed", function()
SFrames.TalentTree:ImportTalentCode(this:GetText())
imf:Hide()
end)
imf.editBox = eb
local okBtn = CreateFrame("Button", nil, imf)
okBtn:SetWidth(60)
okBtn:SetHeight(24)
okBtn:SetPoint("BOTTOMRIGHT", imf, "BOTTOM", -4, 10)
StyleButton(okBtn, "导入")
okBtn:SetScript("OnClick", function()
SFrames.TalentTree:ImportTalentCode(imf.editBox:GetText())
imf:Hide()
end)
local cancelBtn = CreateFrame("Button", nil, imf)
cancelBtn:SetWidth(60)
cancelBtn:SetHeight(24)
cancelBtn:SetPoint("BOTTOMLEFT", imf, "BOTTOM", 4, 10)
StyleButton(cancelBtn, "取消")
cancelBtn:SetScript("OnClick", function() imf:Hide() end)
self.importFrame = imf
end
self.importFrame.editBox:SetText("")
self.importFrame:Show()
self.importFrame.editBox:SetFocus()
end
function SFrames.TalentTree:ImportTalentCode(codeStr)
if not codeStr or codeStr == "" then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[错误]|r 未输入天赋代码。")
return
end
local detectedClass = nil
local cleanCode = codeStr
local _, _, urlClassStr = string.find(codeStr, "turtlecraft%.gg/(%a+)")
if urlClassStr then
local mapped = CLASS_KEY_LOOKUP[string.lower(urlClassStr)]
if mapped then detectedClass = mapped end
end
if not detectedClass then
local _, _, prefix, rest = string.find(codeStr, "^(%u+):(.+)$")
if prefix and CLASS_KEY_LOOKUP[prefix] then
detectedClass = CLASS_KEY_LOOKUP[prefix]
cleanCode = rest
end
end
if detectedClass and detectedClass ~= (self.viewingClass or self.playerClass) then
if detectedClass ~= self.playerClass then
local hasData = HasCacheData(detectedClass)
if not hasData then
UIErrorsFrame:AddMessage("该职业天赋数据不可用,请先用该职业角色登录", 1, 0.5, 0.5, 1)
return
end
end
if not self.simMode then self.simMode = true end
self.viewingClass = detectedClass
self.virtualPoints = {}
self:DestroyTrees()
self:BuildTrees()
self:UpdateSimModeLabel()
end
local grids = self:DecodeTalents(cleanCode)
if not grids then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[错误]|r 天赋代码解析失败。")
return
end
if not self.simMode then
self.simMode = true
end
self:UpdateSimModeLabel()
self.virtualPoints = {}
local numTabs = TT_GetNumTabs(self)
for t = 1, numTabs do
self.virtualPoints[t] = {}
local numT = TT_GetNumTalents(self, t)
for idx = 1, numT do
local _, _, tier, column, _, maxRank = TT_GetTalentInfo(self, t, idx)
if tier and column and grids[t] then
local gridPos = (tier - 1) * 4 + column
local rank = grids[t][gridPos] or 0
if rank > (maxRank or 5) then rank = maxRank or 5 end
self.virtualPoints[t][idx] = rank
else
self.virtualPoints[t][idx] = 0
end
end
end
self:Update()
local impClassName = ""
if detectedClass then
for _, c in ipairs(CLASS_LIST) do
if c.key == detectedClass then impClassName = c.name; break end
end
DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 天赋方案已导入到预览模式。(职业: " .. impClassName .. ")")
else
DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 天赋方案已导入到预览模式。")
end
end
--------------------------------------------------------------------------------
-- Chat talent link: send / receive
--------------------------------------------------------------------------------
function SFrames.TalentTree:SendTalentToChat(code, classKey, points)
local className = classKey or ""
for _, c in ipairs(CLASS_LIST) do
if c.key == classKey then className = c.name; break end
end
local msg = "[NUI:" .. className .. ":" .. (points or "0/0/0") .. ":" .. (classKey or "") .. ":" .. (code or "") .. "]"
ChatFrame_OpenChat(msg, DEFAULT_CHAT_FRAME)
end
function SFrames.TalentTree:HookChatTalentLinks()
local origSetItemRef = SetItemRef
SetItemRef = function(link, text, button)
local _, _, classKey, code = string.find(link, "^nanami:talent:(%u+):(.+)$")
if classKey and code then
local TT = SFrames.TalentTree
if TT.frame and not TT.frame:IsShown() then
TT:BuildTrees()
TT.frame:Show()
end
TT:ImportTalentCode(classKey .. ":" .. code)
return
end
if origSetItemRef then
return origSetItemRef(link, text, button)
end
end
local origChatFrame_OnEvent = ChatFrame_OnEvent
ChatFrame_OnEvent = function(event)
if arg1 and type(arg1) == "string" and string.find(arg1, "[NUI:", 1, true) then
arg1 = FilterNanamiTalentLink(arg1)
end
origChatFrame_OnEvent(event)
end
end
--------------------------------------------------------------------------------
-- Save dialog
--------------------------------------------------------------------------------
function SFrames.TalentTree:ShowSaveDialog()
if not self.saveFrame then
local sf = CreateFrame("Frame", "NanamiTalentSaveFrame", UIParent)
sf:SetWidth(340)
sf:SetHeight(120)
sf:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
SetRoundBackdrop(sf, T.panelBg, T.panelBorder)
CreateShadow(sf, 4)
sf:EnableMouse(true)
sf:SetMovable(true)
sf:RegisterForDrag("LeftButton")
sf:SetScript("OnDragStart", function() this:StartMoving() end)
sf:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
sf:SetFrameStrata("DIALOG")
sf:Hide()
sf.title = MakeFS(sf, 13, "CENTER", T.titleColor)
sf.title:SetPoint("TOP", sf, "TOP", 0, -12)
sf.title:SetText("|c" .. GetHex() .. "保存天赋方案|r")
sf.desc = MakeFS(sf, 10, "CENTER", T.dimText)
sf.desc:SetPoint("TOP", sf.title, "BOTTOM", 0, -4)
sf.desc:SetText("输入方案名称")
local eb = CreateFrame("EditBox", "NanamiTalentSaveEB", sf, "InputBoxTemplate")
eb:SetWidth(300)
eb:SetHeight(24)
eb:SetPoint("TOP", sf.desc, "BOTTOM", 0, -6)
eb:SetFont(GetFont(), 12)
eb:SetAutoFocus(true)
eb:SetMaxLetters(32)
eb:SetScript("OnEscapePressed", function() sf:Hide() end)
eb:SetScript("OnEnterPressed", function()
SFrames.TalentTree:SaveCurrentBuild(this:GetText())
sf:Hide()
end)
sf.editBox = eb
local okBtn = CreateFrame("Button", nil, sf)
okBtn:SetWidth(60)
okBtn:SetHeight(24)
okBtn:SetPoint("BOTTOMRIGHT", sf, "BOTTOM", -4, 10)
StyleButton(okBtn, "保存")
okBtn:SetScript("OnClick", function()
SFrames.TalentTree:SaveCurrentBuild(sf.editBox:GetText())
sf:Hide()
end)
local cancelBtn = CreateFrame("Button", nil, sf)
cancelBtn:SetWidth(60)
cancelBtn:SetHeight(24)
cancelBtn:SetPoint("BOTTOMLEFT", sf, "BOTTOM", 4, 10)
StyleButton(cancelBtn, "取消")
cancelBtn:SetScript("OnClick", function() sf:Hide() end)
self.saveFrame = sf
end
local classKey = self.viewingClass or self.playerClass
local className = classKey
for _, c in ipairs(CLASS_LIST) do
if c.key == classKey then className = c.name; break end
end
self.saveFrame.desc:SetText(className .. " - " .. self:GetPointsSummary())
self.saveFrame.editBox:SetText("")
self.saveFrame:Show()
self.saveFrame.editBox:SetFocus()
end
function SFrames.TalentTree:SaveCurrentBuild(name)
if not name or name == "" then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[错误]|r 请输入方案名称。")
return
end
local classKey = self.viewingClass or self.playerClass
local code = self:EncodeTalents()
local points = self:GetPointsSummary()
local store = GetBuildsStore()
if not store[classKey] then store[classKey] = {} end
table.insert(store[classKey], { name = name, code = code, points = points })
DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 方案「" .. name .. "」已保存。(" .. points .. ")")
end
--------------------------------------------------------------------------------
-- Builds management panel
--------------------------------------------------------------------------------
function SFrames.TalentTree:CreateBuildsPanel()
if self.buildsPanel then return end
local f = self.frame
local PANEL_W = 380
local PANEL_H = 370
local ENTRIES_PER_PAGE = 8
local ENTRY_H = 30
local CLASS_BAR_H = 24
local p = CreateFrame("Frame", nil, f)
p:SetWidth(PANEL_W)
p:SetHeight(PANEL_H)
p:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 6, 56)
SetRoundBackdrop(p, { 0.08, 0.08, 0.12, 0.97 }, T.panelBorder)
CreateShadow(p, 3)
p:SetFrameLevel(f:GetFrameLevel() + 12)
p:EnableMouse(true)
p:Hide()
p.title = MakeFS(p, 13, "CENTER", T.titleColor)
p.title:SetPoint("TOP", p, "TOP", 0, -8)
p.title:SetText("|c" .. GetHex() .. "方案管理|r")
local closeBtn = CreateFrame("Button", nil, p)
closeBtn:SetWidth(16)
closeBtn:SetHeight(16)
closeBtn:SetPoint("TOPRIGHT", p, "TOPRIGHT", -6, -6)
closeBtn:SetFrameLevel(p:GetFrameLevel() + 1)
SetRoundBackdrop(closeBtn, T.buttonDownBg, T.btnBorder)
local closeTxt = MakeFS(closeBtn, 9, "CENTER", T.title)
closeTxt:SetPoint("CENTER", closeBtn, "CENTER", 0, 0)
closeTxt:SetText("x")
closeBtn:SetScript("OnClick", function() p:Hide() end)
closeBtn:SetScript("OnEnter", function()
this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4])
end)
closeBtn:SetScript("OnLeave", function()
this:SetBackdropColor(T.buttonDownBg[1], T.buttonDownBg[2], T.buttonDownBg[3], T.buttonDownBg[4])
end)
-- Class selector bar
local classBarFrame = CreateFrame("Frame", nil, p)
classBarFrame:SetHeight(CLASS_BAR_H)
classBarFrame:SetPoint("TOPLEFT", p, "TOPLEFT", 8, -24)
classBarFrame:SetPoint("TOPRIGHT", p, "TOPRIGHT", -8, -24)
classBarFrame:SetFrameLevel(p:GetFrameLevel() + 1)
p.classBarFrame = classBarFrame
local numClasses = table.getn(CLASS_LIST)
local cbGap = 2
local cbW = math.floor((PANEL_W - 16 - (numClasses - 1) * cbGap) / numClasses)
p.classBtns = {}
for ci, cinfo in ipairs(CLASS_LIST) do
local cb = CreateFrame("Button", nil, classBarFrame)
cb:SetWidth(cbW)
cb:SetHeight(CLASS_BAR_H - 2)
cb:SetPoint("TOPLEFT", classBarFrame, "TOPLEFT", (ci - 1) * (cbW + cbGap), 0)
SetPixelBackdrop(cb, T.slotBg, T.slotBorder)
cb:SetFrameLevel(classBarFrame:GetFrameLevel() + 1)
local cIcon = SFrames:CreateClassIcon(cb, 16)
cIcon.overlay:SetPoint("CENTER", cb, "CENTER", 0, 0)
SFrames:SetClassIcon(cIcon, cinfo.key)
cb.classIconTex = cIcon
cb.classKey = cinfo.key
cb.classColor = cinfo.color
cb:SetScript("OnClick", function()
p.selectedClass = this.classKey
p.page = 0
SFrames.TalentTree:RefreshBuildsPanel()
end)
cb:SetScript("OnEnter", function()
this:SetBackdropColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4])
GameTooltip:SetOwner(this, "ANCHOR_TOP")
local cn = this.classKey
for _, c in ipairs(CLASS_LIST) do
if c.key == cn then cn = c.name; break end
end
GameTooltip:AddLine(cn, this.classColor[1], this.classColor[2], this.classColor[3])
local st = GetBuildsStore()
local bl = st[this.classKey]
local cnt = bl and table.getn(bl) or 0
GameTooltip:AddLine(cnt .. " 个方案", 0.7, 0.7, 0.7)
GameTooltip:Show()
end)
cb:SetScript("OnLeave", function()
this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
GameTooltip:Hide()
end)
p.classBtns[ci] = cb
end
-- List area
local listTop = -24 - CLASS_BAR_H - 4
p.emptyText = MakeFS(p, 11, "CENTER", T.dimText)
p.emptyText:SetPoint("CENTER", p, "CENTER", 0, -10)
p.emptyText:SetText("暂无已保存的方案")
p.entries = {}
p.page = 0
for i = 1, ENTRIES_PER_PAGE do
local entry = CreateFrame("Frame", nil, p)
entry:SetHeight(ENTRY_H)
entry:SetPoint("TOPLEFT", p, "TOPLEFT", 8, listTop - (i - 1) * (ENTRY_H + 2))
entry:SetPoint("RIGHT", p, "RIGHT", -8, 0)
SetPixelBackdrop(entry, T.slotBg, T.slotBorder)
entry:SetFrameLevel(p:GetFrameLevel() + 1)
local classIcon = SFrames:CreateClassIcon(entry, 16)
classIcon.overlay:SetPoint("LEFT", entry, "LEFT", 5, 0)
entry.classIcon = classIcon
entry.nameText = MakeFS(entry, 10, "LEFT", T.valueText)
entry.nameText:SetPoint("LEFT", classIcon.overlay, "RIGHT", 4, 0)
entry.ptsText = MakeFS(entry, 9, "LEFT", T.dimText)
entry.ptsText:SetPoint("LEFT", entry.nameText, "RIGHT", 4, 0)
local delBtn = CreateFrame("Button", nil, entry)
delBtn:SetWidth(36)
delBtn:SetHeight(20)
delBtn:SetPoint("RIGHT", entry, "RIGHT", -3, 0)
delBtn:SetFrameLevel(entry:GetFrameLevel() + 1)
StyleButton(delBtn, "删除")
entry.delBtn = delBtn
local loadBtn = CreateFrame("Button", nil, entry)
loadBtn:SetWidth(36)
loadBtn:SetHeight(20)
loadBtn:SetPoint("RIGHT", delBtn, "LEFT", -2, 0)
loadBtn:SetFrameLevel(entry:GetFrameLevel() + 1)
StyleButton(loadBtn, "加载")
entry.loadBtn = loadBtn
local exportBtn = CreateFrame("Button", nil, entry)
exportBtn:SetWidth(36)
exportBtn:SetHeight(20)
exportBtn:SetPoint("RIGHT", loadBtn, "LEFT", -2, 0)
exportBtn:SetFrameLevel(entry:GetFrameLevel() + 1)
StyleButton(exportBtn, "导出")
entry.exportBtn = exportBtn
entry.nameText:SetPoint("RIGHT", exportBtn, "LEFT", -4, 0)
entry:Hide()
p.entries[i] = entry
end
-- Page nav
local pageFrame = CreateFrame("Frame", nil, p)
pageFrame:SetHeight(22)
pageFrame:SetPoint("BOTTOMLEFT", p, "BOTTOMLEFT", 8, 6)
pageFrame:SetPoint("BOTTOMRIGHT", p, "BOTTOMRIGHT", -8, 6)
p.pageFrame = pageFrame
p.pageText = MakeFS(pageFrame, 9, "CENTER", T.dimText)
p.pageText:SetPoint("CENTER", pageFrame, "CENTER", 0, 0)
local prevBtn = CreateFrame("Button", nil, pageFrame)
prevBtn:SetWidth(46)
prevBtn:SetHeight(18)
prevBtn:SetPoint("LEFT", pageFrame, "LEFT", 0, 0)
StyleButton(prevBtn, "上一页")
prevBtn:SetScript("OnClick", function()
if p.page > 0 then
p.page = p.page - 1
SFrames.TalentTree:RefreshBuildsPanel()
end
end)
p.prevBtn = prevBtn
local nextBtn = CreateFrame("Button", nil, pageFrame)
nextBtn:SetWidth(46)
nextBtn:SetHeight(18)
nextBtn:SetPoint("RIGHT", pageFrame, "RIGHT", 0, 0)
StyleButton(nextBtn, "下一页")
nextBtn:SetScript("OnClick", function()
p.page = p.page + 1
SFrames.TalentTree:RefreshBuildsPanel()
end)
p.nextBtn = nextBtn
self.buildsPanel = p
end
function SFrames.TalentTree:ToggleBuildsPanel()
self:CreateBuildsPanel()
if self.buildsPanel:IsShown() then
self.buildsPanel:Hide()
else
self.buildsPanel.selectedClass = self.viewingClass or self.playerClass
self.buildsPanel.page = 0
self:RefreshBuildsPanel()
self.buildsPanel:Show()
end
end
function SFrames.TalentTree:ShowBuildsPanel()
self:CreateBuildsPanel()
self.buildsPanel.selectedClass = self.viewingClass or self.playerClass
self.buildsPanel.page = 0
self:RefreshBuildsPanel()
self.buildsPanel:Show()
end
function SFrames.TalentTree:RefreshBuildsPanel()
if not self.buildsPanel then return end
local p = self.buildsPanel
local classKey = p.selectedClass or self.viewingClass or self.playerClass
local className = classKey
local classColor = { 1, 1, 1 }
for _, c in ipairs(CLASS_LIST) do
if c.key == classKey then className = c.name; classColor = c.color; break end
end
-- Update class bar highlight
for ci, cinfo in ipairs(CLASS_LIST) do
local cb = p.classBtns[ci]
if cb then
if cinfo.key == classKey then
SetPixelBackdrop(cb, T.tabActiveBg, classColor)
else
local st = GetBuildsStore()
local bl = st[cinfo.key]
local cnt = bl and table.getn(bl) or 0
if cnt > 0 then
SetPixelBackdrop(cb, T.slotBg, T.slotBorder)
else
SetPixelBackdrop(cb, T.emptySlotBg, T.emptySlotBd)
end
end
end
end
p.title:SetText("|c" .. GetHex() .. "方案管理 - |r" ..
string.format("|cff%02x%02x%02x%s|r", classColor[1]*255, classColor[2]*255, classColor[3]*255, className))
local store = GetBuildsStore()
local builds = store[classKey] or {}
local total = table.getn(builds)
local perPage = 8
local maxPage = math.max(math.ceil(total / perPage) - 1, 0)
if p.page > maxPage then p.page = maxPage end
local startIdx = p.page * perPage + 1
for i = 1, perPage do
local entry = p.entries[i]
local bIdx = startIdx + i - 1
if bIdx <= total then
local build = builds[bIdx]
entry.nameText:SetText(build.name or "?")
entry.ptsText:SetText("|cff888888(" .. (build.points or "?") .. ")|r")
entry.buildIdx = bIdx
entry.buildClass = classKey
SFrames:SetClassIcon(entry.classIcon, classKey)
entry.loadBtn:SetScript("OnClick", function()
SFrames.TalentTree:LoadBuild(this:GetParent().buildClass, this:GetParent().buildIdx)
end)
entry.delBtn:SetScript("OnClick", function()
SFrames.TalentTree:DeleteBuild(this:GetParent().buildClass, this:GetParent().buildIdx)
end)
entry.exportBtn:SetScript("OnClick", function()
local bi = this:GetParent().buildIdx
local ck = this:GetParent().buildClass
local bStore = GetBuildsStore()
local bList = bStore[ck]
if bList and bList[bi] then
local b = bList[bi]
SFrames.TalentTree:ShowExportDialogWithCode(b.code, ck, b.points, b.name)
end
end)
entry:Show()
else
entry:Hide()
end
end
if total == 0 then
p.emptyText:Show()
else
p.emptyText:Hide()
end
if total > perPage then
p.pageText:SetText((p.page + 1) .. "/" .. (maxPage + 1))
p.pageFrame:Show()
if p.page <= 0 then p.prevBtn:Disable() else p.prevBtn:Enable() end
if p.page >= maxPage then p.nextBtn:Disable() else p.nextBtn:Enable() end
else
p.pageFrame:Hide()
end
end
function SFrames.TalentTree:ShowExportDialogWithCode(code, classKey, points, buildName)
local className = classKey
for _, c in ipairs(CLASS_LIST) do
if c.key == classKey then className = c.name; break end
end
local urlBase = "https://talents.turtlecraft.gg/" .. string.lower(classKey) .. "?points="
if not self.exportFrame then
self:ShowExportDialog()
self.exportFrame:Hide()
end
local shareCode = classKey .. ":" .. (code or "")
self.exportFrame.currentCode = code or ""
self.exportFrame.currentClassKey = classKey
self.exportFrame.currentPoints = points or "?"
local titleStr = className .. " (" .. (points or "?") .. ")"
if buildName then titleStr = buildName .. " - " .. titleStr end
self.exportFrame.title:SetText("|c" .. GetHex() .. "导出天赋 - " .. titleStr .. "|r")
self.exportFrame.editBox:SetText(shareCode)
self.exportFrame.urlEditBox:SetText(urlBase .. (code or ""))
self.exportFrame:Show()
self.exportFrame.editBox:HighlightText()
self.exportFrame.editBox:SetFocus()
end
function SFrames.TalentTree:LoadBuild(classKey, buildIdx)
classKey = classKey or self.viewingClass or self.playerClass
local store = GetBuildsStore()
local builds = store[classKey]
if not builds or not builds[buildIdx] then return end
local build = builds[buildIdx]
if classKey ~= (self.viewingClass or self.playerClass) then
if classKey ~= self.playerClass then
local hasData = HasCacheData(classKey)
if not hasData then
UIErrorsFrame:AddMessage("该职业天赋数据不可用,无法预览", 1, 0.5, 0.5, 1)
return
end
end
if not self.simMode then self.simMode = true end
self.viewingClass = classKey
self.virtualPoints = {}
self:DestroyTrees()
self:BuildTrees()
self:UpdateSimModeLabel()
end
self:ImportTalentCode(build.code)
if self.buildsPanel then self.buildsPanel:Hide() end
DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 已加载方案「" .. (build.name or "?") .. "")
end
function SFrames.TalentTree:DeleteBuild(classKey, buildIdx)
classKey = classKey or self.viewingClass or self.playerClass
local store = GetBuildsStore()
local builds = store[classKey]
if not builds or not builds[buildIdx] then return end
local name = builds[buildIdx].name or "?"
table.remove(builds, buildIdx)
DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 已删除方案「" .. name .. "")
self:RefreshBuildsPanel()
end
--------------------------------------------------------------------------------
-- Default DB export: serializer + UI
--------------------------------------------------------------------------------
local function SerializeValue(val, indent, depth)
indent = indent or ""
depth = depth or 0
if depth > 20 then return "nil" end
local ni = indent .. " "
if val == nil then return "nil"
elseif type(val) == "boolean" then return val and "true" or "false"
elseif type(val) == "number" then return tostring(val)
elseif type(val) == "string" then
local s = string.gsub(val, "\\", "\\\\")
s = string.gsub(s, "\"", "\\\"")
s = string.gsub(s, "\n", "\\n")
s = string.gsub(s, "\r", "")
return "\"" .. s .. "\""
elseif type(val) == "table" then
local isArr = true
local maxN = 0
for k, _ in pairs(val) do
if type(k) ~= "number" then isArr = false; break end
if k > maxN then maxN = k end
end
if maxN == 0 then isArr = false end
local parts = {}
if isArr then
for i = 1, maxN do
table.insert(parts, ni .. SerializeValue(val[i], ni, depth + 1) .. ",")
end
else
local keys = {}
for k in pairs(val) do table.insert(keys, k) end
table.sort(keys, function(a, b)
if type(a) == "number" and type(b) == "number" then return a < b end
return tostring(a) < tostring(b)
end)
for _, k in ipairs(keys) do
local ks
if type(k) == "number" then
ks = "[" .. k .. "]"
elseif type(k) == "string" and string.find(k, "^[%a_][%w_]*$") then
ks = k
else
ks = "[" .. SerializeValue(k, ni, depth + 1) .. "]"
end
table.insert(parts, ni .. ks .. " = " .. SerializeValue(val[k], ni, depth + 1) .. ",")
end
end
if table.getn(parts) == 0 then return "{}" end
return "{\n" .. table.concat(parts, "\n") .. "\n" .. indent .. "}"
end
return "nil"
end
--------------------------------------------------------------------------------
-- /nui talentdb command handler
--------------------------------------------------------------------------------
function SFrames.TalentTree:HandleTalentDBCommand(args)
args = args or ""
if args == "export" then
self:ExportTalentDB()
elseif args == "status" or args == "" then
self:ShowTalentDBStatus()
else
SFrames:Print("/nui talentdb - 查看天赋缓存状态")
SFrames:Print("/nui talentdb export - 导出默认天赋数据库")
end
end
function SFrames.TalentTree:ShowTalentDBStatus()
local rawCache = SFramesGlobalDB and SFramesGlobalDB.talentCache or {}
local defaultDB = NanamiTalentDefaultDB or {}
SFrames:Print("=== 天赋数据库状态 ===")
for _, cls in ipairs(CLASS_LIST) do
local inCache = rawCache[cls.key] ~= nil
local inDefault = defaultDB[cls.key] ~= nil
local status
if inCache then
status = "|cff00ff00已缓存(本地)|r"
elseif inDefault then
status = "|cffffff00默认DB|r"
else
status = "|cffff0000未缓存|r"
end
local r, g, b = cls.color[1], cls.color[2], cls.color[3]
local hex = string.format("|cff%02x%02x%02x", r * 255, g * 255, b * 255)
SFrames:Print(" " .. hex .. cls.name .. "|r : " .. status)
end
end
function SFrames.TalentTree:ExportTalentDB()
local rawCache = SFramesGlobalDB and SFramesGlobalDB.talentCache
if not rawCache then
SFrames:Print("天赋缓存为空,请先登录各职业角色并打开天赋面板。")
return
end
local missing, found = {}, {}
for _, cls in ipairs(CLASS_LIST) do
if rawCache[cls.key] then
table.insert(found, cls.name)
else
table.insert(missing, cls.name)
end
end
if table.getn(found) == 0 then
SFrames:Print("没有任何天赋缓存数据,请先用各职业角色登录。")
return
end
if table.getn(missing) > 0 then
SFrames:Print("|cffffff00缺少职业:|r " .. table.concat(missing, ", "))
end
local exportTbl = {}
for k, v in pairs(rawCache) do exportTbl[k] = v end
local output = "-- Auto-generated by /nui talentdb export\n"
.. "-- Paste this entire content into TalentDefaultDB.lua\n\n"
.. "NanamiTalentDefaultDB = " .. SerializeValue(exportTbl, "") .. "\n"
self:ShowTalentDBExportDialog(output)
SFrames:Print("已缓存 " .. table.getn(found) .. " 个职业: " .. table.concat(found, ", "))
end
function SFrames.TalentTree:ShowTalentDBExportDialog(text)
if not self.talentDBExportFrame then
local f = CreateFrame("Frame", "NanamiTalentDBExportFrame", UIParent)
f:SetWidth(620)
f:SetHeight(420)
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("DIALOG")
f:Hide()
f.title = MakeFS(f, 13, "CENTER", T.titleColor)
f.title:SetPoint("TOP", f, "TOP", 0, -10)
f.title:SetText("|c" .. GetHex() .. "导出天赋默认数据库|r")
f.desc = MakeFS(f, 10, "CENTER", T.dimText)
f.desc:SetPoint("TOP", f.title, "BOTTOM", 0, -4)
f.desc:SetText("Ctrl+A 全选, Ctrl+C 复制, 粘贴到 TalentDefaultDB.lua")
local sf = CreateFrame("ScrollFrame", "NanamiTalentDBExportScroll", f, "UIPanelScrollFrameTemplate")
sf:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -42)
sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -30, 40)
local eb = CreateFrame("EditBox", "NanamiTalentDBExportEB", sf)
eb:SetWidth(560)
eb:SetFont(GetFont(), 10)
eb:SetMultiLine(true)
eb:SetAutoFocus(false)
eb:SetScript("OnEscapePressed", function() this:ClearFocus() end)
sf:SetScrollChild(eb)
f.editBox = eb
local closeBtn = CreateFrame("Button", nil, f)
closeBtn:SetWidth(60)
closeBtn:SetHeight(24)
closeBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 10)
StyleButton(closeBtn, "关闭")
closeBtn:SetScript("OnClick", function() f:Hide() end)
local selBtn = CreateFrame("Button", nil, f)
selBtn:SetWidth(60)
selBtn:SetHeight(24)
selBtn:SetPoint("RIGHT", closeBtn, "LEFT", -6, 0)
StyleButton(selBtn, "全选")
selBtn:SetScript("OnClick", function()
f.editBox:SetFocus()
f.editBox:HighlightText()
end)
self.talentDBExportFrame = f
end
self.talentDBExportFrame.editBox:SetText(text or "")
self.talentDBExportFrame:Show()
self.talentDBExportFrame.editBox:SetFocus()
self.talentDBExportFrame.editBox:HighlightText()
end