2380 lines
90 KiB
Lua
2380 lines
90 KiB
Lua
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
|