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