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