From ec9e3c29d6822e58a088405e41072f828ae1afcc Mon Sep 17 00:00:00 2001 From: rucky Date: Mon, 23 Mar 2026 10:25:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=A4=9A=E5=87=BA=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=20=E4=BF=AE=E5=A4=8D=E6=8B=BE=E5=8F=96=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E7=82=B9=E5=87=BB=E6=97=A0=E6=95=88=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E5=AE=A0=E7=89=A9=E8=AE=AD=E7=BB=83?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E4=B8=8D=E6=98=BE=E7=A4=BA=E8=AE=AD=E7=BB=83?= =?UTF-8?q?=E7=82=B9=E9=97=AE=E9=A2=98=20=E6=96=B0=E5=A2=9E=E5=A4=A9?= =?UTF-8?q?=E8=B5=8B=E5=88=86=E4=BA=AB=E5=88=B0=E8=81=8A=E5=A4=A9=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=20=E4=BF=AE=E5=A4=8D=E9=A3=9E=E8=A1=8C=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E6=97=A0=E6=B3=95=E5=85=B3=E9=97=AD=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E6=9C=AF=E5=A3=AB=E5=AE=A0=E7=89=A9?= =?UTF-8?q?=E7=9A=84=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98=20=E4=B8=BA?= =?UTF-8?q?=E5=A4=A9=E8=B5=8B=E7=95=8C=E9=9D=A2=E6=B7=BB=E5=8A=A0=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E6=95=B0=E6=8D=AE=E5=BA=93=E6=94=AF=E6=8C=81=20?= =?UTF-8?q?=E6=A1=86=E6=9E=B6=E7=8E=B0=E5=9C=A8=E4=B9=9F=E5=8F=AF=E8=87=AA?= =?UTF-8?q?=E4=B8=BB=E9=80=89=E6=8B=A9=E6=98=AF=E5=90=A6=E5=90=AF=E7=94=A8?= =?UTF-8?q?=20=E7=8E=A9=E5=AE=B6=E6=A1=86=E6=9E=B6=E5=92=8C=E7=9B=AE?= =?UTF-8?q?=E6=A0=87=E6=A1=86=E6=9E=B6=E5=8F=AF=E5=8F=96=E6=B6=88=E6=98=BE?= =?UTF-8?q?=E7=A4=BA3D=E5=A4=B4=E5=83=8F=E4=BB=A5=E5=8F=8A=E9=80=8F?= =?UTF-8?q?=E6=98=8E=E5=BA=A6=E4=BF=AE=E6=94=B9=20=E8=83=8C=E5=8C=85?= =?UTF-8?q?=E5=92=8C=E9=93=B6=E8=A1=8C=E4=B9=9F=E6=B7=BB=E5=8A=A0=E9=80=8F?= =?UTF-8?q?=E6=98=8E=E5=BA=A6=E8=87=AA=E5=AE=9A=E4=B9=89=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20=E4=BC=98=E5=8C=96tooltip=E6=80=A7=E8=83=BD=E5=92=8C?= =?UTF-8?q?=E8=83=8C=E5=8C=85=E7=89=A9=E5=93=81=E6=98=BE=E7=A4=BA=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=20=E4=BF=AE=E5=A4=8D=E5=B8=83=E5=B1=80=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=9C=A8ui=E7=BC=A9=E6=94=BE=E5=90=8E=E4=B8=8D?= =?UTF-8?q?=E8=83=BD=E6=AD=A3=E5=B8=B8=E5=AE=9A=E4=BD=8D=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20=E6=B7=BB=E5=8A=A0=E7=A1=AC=E6=A0=B8=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=8D=B1=E9=99=A9=E5=92=8C=E6=AD=BB=E4=BA=A1=E7=9A=84?= =?UTF-8?q?=E5=B7=A5=E4=BC=9A=E9=80=9A=E6=8A=A5=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=8B=BE=E5=8F=96=E5=92=8C=E5=B7=B2=E6=8B=BE=E5=8F=96=E7=9A=84?= =?UTF-8?q?=E6=A1=86=E4=BD=93=20=E7=AD=89=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ActionBars.lua | 85 +- Bags/Bank.lua | 2 + Bags/Container.lua | 5 +- BeastTrainingUI.lua | 1081 ++++++++ CharacterPanel.lua | 522 +++- Chat.lua | 49 +- ConfigUI.lua | 950 +++++-- Core.lua | 220 +- FlightMap.lua | 5 + GameMenu.lua | 24 +- KeyBindManager.lua | 633 +++++ LootDisplay.lua | 816 ++++++ MapReveal.lua | 225 ++ Merchant.lua | 20 +- Minimap.lua | 297 +- MinimapBuffs.lua | 35 +- Movers.lua | 998 +++++++ Nanami-UI.toc | 5 + PetStableSkin.lua | 80 +- SetupWizard.lua | 119 +- SocialUI.lua | 104 +- TalentDefaultDB.lua | 6266 ++++++++++++++++++++++++++++++++++++++++++ Tooltip.lua | 100 +- TradeSkillUI.lua | 11 +- TrainerUI.lua | 6 + Tweaks.lua | 2 + Units/Party.lua | 7 + Units/Pet.lua | 232 +- Units/Player.lua | 101 +- Units/Raid.lua | 7 + Units/TalentTree.lua | 1306 ++++++++- Units/Target.lua | 162 +- img/map_f_1.tga | Bin 0 -> 1048594 bytes img/map_f_2.tga | Bin 0 -> 1048594 bytes 34 files changed, 13897 insertions(+), 578 deletions(-) create mode 100644 BeastTrainingUI.lua create mode 100644 KeyBindManager.lua create mode 100644 LootDisplay.lua create mode 100644 Movers.lua create mode 100644 TalentDefaultDB.lua create mode 100644 img/map_f_1.tga create mode 100644 img/map_f_2.tga diff --git a/ActionBars.lua b/ActionBars.lua index 1fa2bed..d764cf1 100644 --- a/ActionBars.lua +++ b/ActionBars.lua @@ -21,6 +21,7 @@ local DEFAULTS = { buttonGap = 2, smallBarSize = 27, scale = 1.0, + alpha = 1.0, barCount = 3, showHotkey = true, showMacroName = false, @@ -442,7 +443,12 @@ function AB:CreateBars() local anchor = CreateFrame("Frame", "SFramesActionBarAnchor", UIParent) anchor:SetWidth(rowWidth) anchor:SetHeight(size * 3 + gap * 2) - anchor:SetPoint("BOTTOM", UIParent, "BOTTOM", db.bottomOffsetX, db.bottomOffsetY) + local abPos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ActionBarBottom"] + if abPos and abPos.point and abPos.relativePoint then + anchor:SetPoint(abPos.point, UIParent, abPos.relativePoint, abPos.xOfs or 0, abPos.yOfs or 0) + else + anchor:SetPoint("BOTTOM", UIParent, "BOTTOM", db.bottomOffsetX, db.bottomOffsetY) + end anchor:SetScale(db.scale) self.anchor = anchor @@ -582,7 +588,12 @@ function AB:CreateBars() local rightHolder = CreateFrame("Frame", "SFramesRightBarHolder", UIParent) rightHolder:SetWidth(size * 2 + gap) rightHolder:SetHeight((size + gap) * BUTTONS_PER_ROW - gap) - rightHolder:SetPoint("RIGHT", UIParent, "RIGHT", db.rightOffsetX, db.rightOffsetY) + local rbPos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ActionBarRight"] + if rbPos and rbPos.point and rbPos.relativePoint then + rightHolder:SetPoint(rbPos.point, UIParent, rbPos.relativePoint, rbPos.xOfs or 0, rbPos.yOfs or 0) + else + rightHolder:SetPoint("RIGHT", UIParent, "RIGHT", db.rightOffsetX, db.rightOffsetY) + end rightHolder:SetScale(db.scale) self.rightHolder = rightHolder @@ -761,6 +772,15 @@ function AB:ApplyConfig() end end + -- Alpha + local alpha = db.alpha or 1 + if alpha < 0.1 then alpha = 0.1 end + if alpha > 1 then alpha = 1 end + if self.anchor then self.anchor:SetAlpha(alpha) end + if self.rightHolder then self.rightHolder:SetAlpha(alpha) end + if self.stanceHolder then self.stanceHolder:SetAlpha(alpha) end + if self.petHolder then self.petHolder:SetAlpha(alpha) end + -- Hotkey / macro name(使用缓存表,避免每次 ApplyConfig 都分配临时表) if not self.allButtonsCache then self.allButtonsCache = {} @@ -1000,11 +1020,23 @@ function AB:ApplyGryphon() local ox = db.gryphonOffsetX or 30 local oy = db.gryphonOffsetY or 0 + local positions = SFramesDB and SFramesDB.Positions + self.gryphonLeft:ClearAllPoints() - self.gryphonLeft:SetPoint("BOTTOMRIGHT", self.anchor, "BOTTOMLEFT", ox, oy) + local posL = positions and positions["GryphonLeft"] + if posL and posL.point and posL.relativePoint then + self.gryphonLeft:SetPoint(posL.point, UIParent, posL.relativePoint, posL.xOfs or 0, posL.yOfs or 0) + else + self.gryphonLeft:SetPoint("BOTTOMRIGHT", self.anchor, "BOTTOMLEFT", ox, oy) + end self.gryphonRight:ClearAllPoints() - self.gryphonRight:SetPoint("BOTTOMLEFT", self.anchor, "BOTTOMRIGHT", -ox, oy) + local posR = positions and positions["GryphonRight"] + if posR and posR.point and posR.relativePoint then + self.gryphonRight:SetPoint(posR.point, UIParent, posR.relativePoint, posR.xOfs or 0, posR.yOfs or 0) + else + self.gryphonRight:SetPoint("BOTTOMLEFT", self.anchor, "BOTTOMRIGHT", -ox, oy) + end self.gryphonLeft:Show() self.gryphonRight:Show() @@ -1015,13 +1047,25 @@ end -------------------------------------------------------------------------------- function AB:ApplyPosition() local db = self:GetDB() + local positions = SFramesDB and SFramesDB.Positions + if self.anchor then self.anchor:ClearAllPoints() - self.anchor:SetPoint("BOTTOM", UIParent, "BOTTOM", db.bottomOffsetX, db.bottomOffsetY) + local pos = positions and positions["ActionBarBottom"] + if pos and pos.point and pos.relativePoint then + self.anchor:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) + else + self.anchor:SetPoint("BOTTOM", UIParent, "BOTTOM", db.bottomOffsetX, db.bottomOffsetY) + end end if self.rightHolder then self.rightHolder:ClearAllPoints() - self.rightHolder:SetPoint("RIGHT", UIParent, "RIGHT", db.rightOffsetX, db.rightOffsetY) + local pos = positions and positions["ActionBarRight"] + if pos and pos.point and pos.relativePoint then + self.rightHolder:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) + else + self.rightHolder:SetPoint("RIGHT", UIParent, "RIGHT", db.rightOffsetX, db.rightOffsetY) + end end end @@ -1435,6 +1479,35 @@ function AB:Initialize() SFrames:RegisterEvent("ZONE_CHANGED", FixRowAnchors) SFrames:RegisterEvent("ZONE_CHANGED_NEW_AREA", FixRowAnchors) SFrames:RegisterEvent("ZONE_CHANGED_INDOORS", FixRowAnchors) + + -- Register movers + if SFrames.Movers and SFrames.Movers.RegisterMover then + if self.anchor then + SFrames.Movers:RegisterMover("ActionBarBottom", self.anchor, "底部动作条", + "BOTTOM", "UIParent", "BOTTOM", db.bottomOffsetX, db.bottomOffsetY, + function() AB:ApplyStanceBar(); AB:ApplyPetBar(); AB:ApplyGryphon() end) + end + if self.rightHolder then + SFrames.Movers:RegisterMover("ActionBarRight", self.rightHolder, "右侧动作条", + "RIGHT", "UIParent", "RIGHT", db.rightOffsetX, db.rightOffsetY) + end + if self.stanceHolder then + SFrames.Movers:RegisterMover("StanceBar", self.stanceHolder, "姿态条", + "BOTTOMLEFT", "SFramesActionBarAnchor", "TOPLEFT", 0, db.buttonGap) + end + if self.petHolder then + SFrames.Movers:RegisterMover("PetBar", self.petHolder, "宠物条", + "BOTTOMLEFT", "SFramesActionBarAnchor", "TOPLEFT", 0, db.buttonGap) + end + if self.gryphonLeft then + SFrames.Movers:RegisterMover("GryphonLeft", self.gryphonLeft, "狮鹫(左)", + "BOTTOMRIGHT", "SFramesActionBarAnchor", "BOTTOMLEFT", db.gryphonOffsetX or 30, db.gryphonOffsetY or 0) + end + if self.gryphonRight then + SFrames.Movers:RegisterMover("GryphonRight", self.gryphonRight, "狮鹫(右)", + "BOTTOMLEFT", "SFramesActionBarAnchor", "BOTTOMRIGHT", -(db.gryphonOffsetX or 30), db.gryphonOffsetY or 0) + end + end end -------------------------------------------------------------------------------- diff --git a/Bags/Bank.lua b/Bags/Bank.lua index 2eab792..0ac687a 100644 --- a/Bags/Bank.lua +++ b/Bags/Bank.lua @@ -1621,6 +1621,8 @@ function SFrames.Bags.Bank:Initialize() bankShadow:SetBackdropBorderColor(0, 0, 0, 0.4) local scale = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.bankScale) == "number" and SFramesDB.Bags.bankScale) or 0.85 SFBankFrame:SetScale(scale) + local bankAlpha = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.bankAlpha) == "number" and SFramesDB.Bags.bankAlpha) or 1 + SFBankFrame:SetAlpha(bankAlpha) local bankTitleIco = SFrames:CreateIcon(SFBankFrame, "gold", 14) bankTitleIco:SetDrawLayer("OVERLAY") diff --git a/Bags/Container.lua b/Bags/Container.lua index e951b7a..e7ac283 100644 --- a/Bags/Container.lua +++ b/Bags/Container.lua @@ -578,8 +578,7 @@ local function ApplyBagFramePosition() if pos and pos.point and pos.relPoint and type(pos.x) == "number" and type(pos.y) == "number" then BagFrame:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) else - -- Default to left side; bank frame defaults to right side. - BagFrame:SetPoint("CENTER", UIParent, "CENTER", -360, 0) + BagFrame:SetPoint("RIGHT", UIParent, "RIGHT", -20, 0) end end @@ -942,6 +941,8 @@ function SFrames.Bags.Container:Initialize() bagShadow:SetBackdropBorderColor(0, 0, 0, 0.4) local scale = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.scale) == "number" and SFramesDB.Bags.scale) or 0.85 BagFrame:SetScale(scale) + local bagAlpha = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.alpha) == "number" and SFramesDB.Bags.alpha) or 1 + BagFrame:SetAlpha(bagAlpha) local titleIco = SFrames:CreateIcon(BagFrame, "backpack", 14) titleIco:SetDrawLayer("OVERLAY") diff --git a/BeastTrainingUI.lua b/BeastTrainingUI.lua new file mode 100644 index 0000000..a9daa33 --- /dev/null +++ b/BeastTrainingUI.lua @@ -0,0 +1,1081 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Beast Training UI (BeastTrainingUI.lua) +-- Dedicated panel for Hunter pet beast training (训练野兽) +-- Separate from TradeSkillUI; shows training points, skill ranks, etc. +-- NOTE: Lua 5.0 upvalue limit = 32 per closure; all state packed into tables. +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.BeastTrainingUI = {} +local BTUI = SFrames.BeastTrainingUI +SFramesDB = SFramesDB or {} + +local BEAST_NAMES = { + ["训练野兽"] = true, + ["Beast Training"] = true, +} + +function BTUI.IsBeastTraining() + local name = GetCraftName and GetCraftName() or "" + return BEAST_NAMES[name] == true +end + +-------------------------------------------------------------------------------- +-- Theme +-------------------------------------------------------------------------------- +local T = SFrames.Theme:Extend({ + tpGood = { 0.40, 0.90, 0.40 }, + tpLow = { 0.90, 0.60, 0.20 }, + tpNone = { 0.90, 0.30, 0.30 }, + rankText = { 0.80, 0.70, 1.00 }, + available = { 0.25, 1.00, 0.25 }, + unavailable = { 0.80, 0.20, 0.20 }, + learned = { 0.50, 0.50, 0.50 }, +}) + +-------------------------------------------------------------------------------- +-- Layout (packed to save upvalues) +-------------------------------------------------------------------------------- +local L = { + FRAME_W = 620, FRAME_H = 470, + HEADER_H = 52, SIDE_PAD = 10, + FILTER_H = 26, LIST_ROW_H = 28, + CAT_ROW_H = 20, BOTTOM_H = 44, + SCROLL_STEP = 36, SCROLLBAR_W = 12, + MAX_ROWS = 60, LEFT_W = 280, +} +L.RIGHT_W = L.FRAME_W - L.LEFT_W +L.CONTENT_W = L.RIGHT_W - L.SIDE_PAD * 2 +L.LIST_ROW_W = L.LEFT_W - L.SIDE_PAD * 2 - L.SCROLLBAR_W - 4 + +-------------------------------------------------------------------------------- +-- State (packed to save upvalues) +-------------------------------------------------------------------------------- +local S = { + MainFrame = nil, + selectedIndex = nil, + currentFilter = "all", + displayList = {}, + rowButtons = {}, + collapsedCats = {}, +} + +-------------------------------------------------------------------------------- +-- Tooltip scanner for training point cost & requirements +-------------------------------------------------------------------------------- +local scanTip = nil + +function BTUI.GetCraftExtendedInfo(index) + local name, rank, skillType, numAvail, _, _, tpCost = GetCraftInfo(index) + return name, rank, skillType, + tonumber(numAvail) or 0, + tonumber(tpCost) or 0 +end + +function BTUI.GetSkillTooltipLines(index) + if not scanTip then + scanTip = CreateFrame("GameTooltip", "SFramesBTScanTip", nil, "GameTooltipTemplate") + end + scanTip:SetOwner(WorldFrame, "ANCHOR_NONE") + scanTip:ClearLines() + local ok = pcall(scanTip.SetCraftSpell, scanTip, index) + if not ok then scanTip:Hide(); return {} end + + local lines = {} + local numLines = scanTip:NumLines() + for i = 2, numLines do + local leftFS = _G["SFramesBTScanTipTextLeft" .. i] + local rightFS = _G["SFramesBTScanTipTextRight" .. i] + if leftFS then + local lt = leftFS:GetText() or "" + local rt = rightFS and rightFS:GetText() or "" + local r, g, b = leftFS:GetTextColor() + if lt ~= "" or rt ~= "" then + table.insert(lines, { + left = lt, right = rt, + r = r, g = g, b = b, + }) + end + end + end + scanTip:Hide() + return lines +end + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +function BTUI.GetFont() + if SFrames and SFrames.GetFont then return SFrames:GetFont() end + return "Fonts\\ARIALN.TTF" +end + +function BTUI.SetRoundBackdrop(frame) + 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 }, + }) + frame:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) + frame:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) +end + +function BTUI.CreateShadow(parent) + local s = CreateFrame("Frame", nil, parent) + s:SetPoint("TOPLEFT", parent, "TOPLEFT", -4, 4) + s:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT", 4, -4) + 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.45) + s:SetBackdropBorderColor(0, 0, 0, 0.6) + s:SetFrameLevel(math.max(0, parent:GetFrameLevel() - 1)) + return s +end + +-------------------------------------------------------------------------------- +-- Widget Factories +-------------------------------------------------------------------------------- +function BTUI.CreateFilterBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 60); btn:SetHeight(20) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.5) + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(BTUI.GetFont(), 11, "OUTLINE"); fs:SetPoint("CENTER", 0, 0) + fs:SetText(text); fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + btn.label = 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]) + end) + btn:SetScript("OnLeave", function() + if this.active then + this:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + this:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 1) + else + 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], 0.5) + end + end) + function btn:SetActive(flag) + self.active = flag + if flag then + self:SetBackdropColor(T.btnHoverBg[1], T.btnHoverBg[2], T.btnHoverBg[3], T.btnHoverBg[4]) + self:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 1) + self.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + else + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + self:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], 0.5) + self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end + return btn +end + +function BTUI.CreateActionBtn(parent, text, w) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(w or 100); btn:SetHeight(28) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(BTUI.GetFont(), 12, "OUTLINE"); fs:SetPoint("CENTER", 0, 0) + fs:SetText(text); fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + btn.label = fs; btn.disabled = false + function btn:SetDisabled(flag) + self.disabled = flag + if flag then + self.label:SetTextColor(T.btnDisabledText[1], T.btnDisabledText[2], T.btnDisabledText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], 0.5) + else + self.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + self:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + end + end + btn:SetScript("OnEnter", function() + if not this.disabled then + 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]) + this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + end + end) + btn:SetScript("OnLeave", function() + if not this.disabled then + 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]) + this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end + end) + return btn +end + +function BTUI.CreateListRow(parent, idx) + local row = CreateFrame("Button", nil, parent) + row:SetWidth(L.LIST_ROW_W); row:SetHeight(L.LIST_ROW_H) + + local iconFrame = CreateFrame("Frame", nil, row) + iconFrame:SetWidth(L.LIST_ROW_H - 4); iconFrame:SetHeight(L.LIST_ROW_H - 4) + iconFrame:SetPoint("LEFT", row, "LEFT", 0, 0) + iconFrame:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + row.iconFrame = iconFrame + + local icon = iconFrame:CreateTexture(nil, "ARTWORK") + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 3, -3) + icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -3, 3) + row.icon = icon + + local font = BTUI.GetFont() + + local tpFS = row:CreateFontString(nil, "OVERLAY") + tpFS:SetFont(font, 10, "OUTLINE") + tpFS:SetPoint("RIGHT", row, "RIGHT", -4, 0) + tpFS:SetJustifyH("RIGHT") + tpFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + tpFS:Hide() + row.tpFS = tpFS + + local nameFS = row:CreateFontString(nil, "OVERLAY") + nameFS:SetFont(font, 11, "OUTLINE") + nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 5, 2) + nameFS:SetPoint("RIGHT", tpFS, "LEFT", -4, 0) + nameFS:SetJustifyH("LEFT") + nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + row.nameFS = nameFS + + local rankFS = row:CreateFontString(nil, "OVERLAY") + rankFS:SetFont(font, 9, "OUTLINE") + rankFS:SetPoint("TOPLEFT", nameFS, "BOTTOMLEFT", 0, 0) + rankFS:SetJustifyH("LEFT") + rankFS:SetTextColor(T.rankText[1], T.rankText[2], T.rankText[3]) + row.rankFS = rankFS + + local catFS = row:CreateFontString(nil, "OVERLAY") + catFS:SetFont(font, 11, "OUTLINE") + catFS:SetPoint("LEFT", row, "LEFT", 4, 0) + catFS:SetJustifyH("LEFT") + catFS:SetTextColor(T.catHeader[1], T.catHeader[2], T.catHeader[3]) + catFS:Hide() + row.catFS = catFS + + local catSep = row:CreateTexture(nil, "ARTWORK") + catSep:SetTexture("Interface\\Buttons\\WHITE8X8"); catSep:SetHeight(1) + catSep:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) + catSep:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0) + catSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) + catSep:Hide() + row.catSep = catSep + + local selBg = row:CreateTexture(nil, "ARTWORK") + selBg:SetTexture("Interface\\Buttons\\WHITE8X8"); selBg:SetAllPoints(row) + selBg:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.40) + selBg:Hide(); row.selBg = selBg + + local selGlow = row:CreateTexture(nil, "ARTWORK") + selGlow:SetTexture("Interface\\Buttons\\WHITE8X8") + selGlow:SetWidth(4); selGlow:SetHeight(L.LIST_ROW_H) + selGlow:SetPoint("LEFT", row, "LEFT", 0, 0) + selGlow:SetVertexColor(1, 0.65, 0.85, 1) + selGlow:Hide(); row.selGlow = selGlow + + local selTop = row:CreateTexture(nil, "OVERLAY") + selTop:SetTexture("Interface\\Buttons\\WHITE8X8"); selTop:SetHeight(1) + selTop:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0) + selTop:SetPoint("TOPRIGHT", row, "TOPRIGHT", 0, 0) + selTop:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.8) + selTop:Hide(); row.selTop = selTop + + local selBot = row:CreateTexture(nil, "OVERLAY") + selBot:SetTexture("Interface\\Buttons\\WHITE8X8"); selBot:SetHeight(1) + selBot:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 0, 0) + selBot:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0) + selBot:SetVertexColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.8) + selBot:Hide(); row.selBot = selBot + + local hl = row:CreateTexture(nil, "HIGHLIGHT") + hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") + hl:SetBlendMode("ADD"); hl:SetAllPoints(row); hl:SetAlpha(0.3) + row.highlight = hl + + row.craftIndex = nil; row.isHeader = false + + row:SetScript("OnEnter", function() + if this.craftIndex and not this.isHeader then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + local ok2 = pcall(GameTooltip.SetCraftSpell, GameTooltip, this.craftIndex) + if ok2 then GameTooltip:Show() else GameTooltip:Hide() end + end + end) + row:SetScript("OnLeave", function() GameTooltip:Hide() end) + + function row:SetAsHeader(name, collapsed) + self.isHeader = true; self:SetHeight(L.CAT_ROW_H) + self.iconFrame:Hide(); self.nameFS:Hide(); self.rankFS:Hide(); self.tpFS:Hide() + self.highlight:SetAlpha(0.15) + self.selBg:Hide(); self.selGlow:Hide(); self.selTop:Hide(); self.selBot:Hide() + self.catFS:SetText((collapsed and "+" or "-") .. " " .. (name or "")) + self.catFS:Show(); self.catSep:Show() + end + + function row:SetAsSkill(skill) + self.isHeader = false; self.craftIndex = skill.index + self:SetHeight(L.LIST_ROW_H) + self.iconFrame:Show(); self.nameFS:Show() + self.catFS:Hide(); self.catSep:Hide(); self.highlight:SetAlpha(0.3) + + local iconTex = GetCraftIcon and GetCraftIcon(skill.index) + self.icon:SetTexture(iconTex) + self.nameFS:SetText(skill.name) + + if skill.rank and skill.rank ~= "" then + self.rankFS:SetText(skill.rank) + self.rankFS:Show() + else + self.rankFS:SetText("") + self.rankFS:Hide() + end + + local tpCost = skill.tpCost or 0 + local canLearn = (tpCost > 0) + + if canLearn then + local remaining = BTUI.GetRemainingTP() + if remaining >= tpCost then + self.tpFS:SetTextColor(T.tpGood[1], T.tpGood[2], T.tpGood[3]) + else + self.tpFS:SetTextColor(T.tpNone[1], T.tpNone[2], T.tpNone[3]) + end + self.tpFS:SetText(tpCost .. " TP") + self.tpFS:Show() + self.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3]) + self.icon:SetVertexColor(1, 1, 1) + else + self.tpFS:Hide() + self.nameFS:SetTextColor(T.learned[1], T.learned[2], T.learned[3]) + self.icon:SetVertexColor(0.5, 0.5, 0.5) + end + end + + function row:Clear() + self.craftIndex = nil; self.isHeader = false + self.selBg:Hide(); self.selGlow:Hide(); self.selTop:Hide(); self.selBot:Hide() + self.tpFS:Hide() + self:Hide() + end + + return row +end + +-------------------------------------------------------------------------------- +-- Logic +-------------------------------------------------------------------------------- +function BTUI.BuildDisplayList() + S.displayList = {} + local numCrafts = GetNumCrafts and GetNumCrafts() or 0 + if numCrafts == 0 then return end + + local currentCat = nil + local catSkills = {} + local catOrder = {} + + for i = 1, numCrafts do + local name, rank, skillType, numAvail, tpCost = BTUI.GetCraftExtendedInfo(i) + if name then + if skillType == "header" then + currentCat = name + if not catSkills[name] then + catSkills[name] = {} + table.insert(catOrder, name) + end + else + if not currentCat then + currentCat = "技能" + if not catSkills[currentCat] then + catSkills[currentCat] = {} + table.insert(catOrder, currentCat) + end + end + local canLearn = (tpCost > 0) + local show = true + if S.currentFilter == "available" then + show = canLearn + elseif S.currentFilter == "used" then + show = (not canLearn) + end + if show then + table.insert(catSkills[currentCat], { + index = i, + name = name, + rank = rank or "", + skillType = skillType or "none", + numAvail = numAvail, + tpCost = tpCost, + }) + end + end + end + end + + local hasCats = table.getn(catOrder) > 1 + for _, catName in ipairs(catOrder) do + local skills = catSkills[catName] + if skills and table.getn(skills) > 0 then + if hasCats then + table.insert(S.displayList, { + type = "header", name = catName, + collapsed = S.collapsedCats[catName], + }) + end + if not S.collapsedCats[catName] then + for _, skill in ipairs(skills) do + table.insert(S.displayList, { type = "skill", data = skill }) + end + end + end + end +end + +function BTUI.GetRemainingTP() + if not GetPetTrainingPoints then return 0, 0 end + local ok, total, spent = pcall(GetPetTrainingPoints) + if not ok then return 0, 0 end + total = total or 0 + spent = spent or 0 + return total - spent, total +end + +function BTUI.UpdateTrainingPoints() + if not S.MainFrame then return end + local remaining, total = BTUI.GetRemainingTP() + local color = T.tpGood + if remaining <= 0 then color = T.tpNone + elseif remaining < 10 then color = T.tpLow end + S.MainFrame.tpValueFS:SetText(tostring(remaining)) + S.MainFrame.tpValueFS:SetTextColor(color[1], color[2], color[3]) +end + +function BTUI.UpdateList() + if not S.MainFrame or not S.MainFrame:IsVisible() then return end + BTUI.BuildDisplayList() + local content = S.MainFrame.listScroll.content + local count = table.getn(S.displayList) + local y = 0 + for i = 1, L.MAX_ROWS do + local row = S.rowButtons[i] + if i <= count then + local entry = S.displayList[i] + row:ClearAllPoints() + if entry.type == "header" then + row:SetAsHeader(entry.name, entry.collapsed) + row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + row.catName = entry.name + row:Show(); y = y + L.CAT_ROW_H + else + row:SetAsSkill(entry.data) + row:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y) + row.catName = nil + row:Show(); y = y + L.LIST_ROW_H + if S.selectedIndex == entry.data.index then + row.iconFrame:SetBackdropBorderColor(1, 0.65, 0.85, 1) + row.iconFrame:SetBackdropColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 0.5) + row.selBg:Show(); row.selGlow:Show(); row.selTop:Show(); row.selBot:Show() + row.nameFS:SetTextColor(1, 1, 1) + else + row.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + row.iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + row.selBg:Hide(); row.selGlow:Hide(); row.selTop:Hide(); row.selBot:Hide() + end + end + else row:Clear() end + end + content:SetHeight(math.max(1, y)) +end + +function BTUI.UpdateDetail() + if not S.MainFrame then return end + local detail = S.MainFrame.detail + if not S.selectedIndex then + detail.iconFrame:Hide() + detail.nameFS:SetText(""); detail.rankFS:SetText("") + detail.descFS:SetText(""); detail.costFS:SetText("") + detail.reqFS:SetText("") + S.MainFrame.trainBtn:SetDisabled(true) + return + end + + local name, rank, skillType, numAvail, tpCost = BTUI.GetCraftExtendedInfo(S.selectedIndex) + local iconTex = GetCraftIcon and GetCraftIcon(S.selectedIndex) + + detail.icon:SetTexture(iconTex); detail.iconFrame:Show() + detail.nameFS:SetText(name or "") + + local canLearn = (tpCost > 0) + if canLearn then + detail.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3]) + else + detail.nameFS:SetTextColor(T.learned[1], T.learned[2], T.learned[3]) + end + + if rank and rank ~= "" then + detail.rankFS:SetText(rank) + detail.rankFS:SetTextColor(T.rankText[1], T.rankText[2], T.rankText[3]) + else + detail.rankFS:SetText("") + end + + local desc = "" + if GetCraftDescription then + local ok, d = pcall(GetCraftDescription, S.selectedIndex) + if ok and d then desc = d end + end + detail.descFS:SetText(desc) + + local descH = detail.descFS:GetHeight() or 40 + detail.descScroll:GetScrollChild():SetHeight(math.max(1, descH)) + detail.descScroll:SetVerticalScroll(0) + + if tpCost > 0 then + local remaining = BTUI.GetRemainingTP() + local costColor = remaining >= tpCost and "|cff40ff40" or "|cffff4040" + detail.costFS:SetText("训练点数: " .. costColor .. tpCost .. "|r (剩余: " .. remaining .. ")") + else + detail.costFS:SetText("") + end + + local lines = BTUI.GetSkillTooltipLines(S.selectedIndex) + local reqParts = {} + for _, line in ipairs(lines) do + if line.r and line.r > 0.85 and line.g < 0.35 and line.b < 0.35 then + table.insert(reqParts, "|cffff4040" .. line.left .. "|r") + end + end + detail.reqFS:SetText(table.concat(reqParts, "\n")) + + S.MainFrame.trainBtn:SetDisabled(not canLearn) +end + +function BTUI.UpdateFilters() + if not S.MainFrame then return end + S.MainFrame.filterAll:SetActive(S.currentFilter == "all") + S.MainFrame.filterAvail:SetActive(S.currentFilter == "available") + S.MainFrame.filterUsed:SetActive(S.currentFilter == "used") +end + +function BTUI.FullUpdate() + BTUI.UpdateTrainingPoints() + BTUI.UpdateFilters() + BTUI.UpdateList() + BTUI.UpdateDetail() + if BTUI.UpdateScrollbar then BTUI.UpdateScrollbar() end +end + +function BTUI.SelectSkill(index) + S.selectedIndex = index + if SelectCraft then pcall(SelectCraft, index) end + BTUI.FullUpdate() +end + +function BTUI.ToggleCategory(catName) + if S.collapsedCats[catName] then S.collapsedCats[catName] = nil + else S.collapsedCats[catName] = true end + BTUI.FullUpdate() +end + +-------------------------------------------------------------------------------- +-- Hide Blizzard Craft Frame +-------------------------------------------------------------------------------- +function BTUI.CleanupBlizzardCraft() + if not CraftFrame then return end + CraftFrame:SetScript("OnHide", function() end) + if HideUIPanel then pcall(HideUIPanel, CraftFrame) end + if CraftFrame:IsVisible() then CraftFrame:Hide() end + CraftFrame:SetAlpha(0); CraftFrame:EnableMouse(false) +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function BTUI:Initialize() + if S.MainFrame then return end + local MF = CreateFrame("Frame", "SFramesBeastTrainingFrame", UIParent) + S.MainFrame = MF + MF:SetWidth(L.FRAME_W); MF:SetHeight(L.FRAME_H) + MF:SetPoint("LEFT", UIParent, "LEFT", 36, 0) + MF:SetFrameStrata("HIGH"); MF:SetToplevel(true); MF:EnableMouse(true) + MF:SetMovable(true); MF:RegisterForDrag("LeftButton") + MF:SetScript("OnDragStart", function() this:StartMoving() end) + MF:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + BTUI.SetRoundBackdrop(MF); BTUI.CreateShadow(MF) + + local font = BTUI.GetFont() + + -- ═══ Header ═════════════════════════════════════════════════════════ + local header = CreateFrame("Frame", nil, MF) + header:SetPoint("TOPLEFT", 0, 0); header:SetPoint("TOPRIGHT", 0, 0) + header:SetHeight(L.HEADER_H) + header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) + header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4]) + + local titleIco = SFrames:CreateIcon(header, "dragon", 16) + titleIco:SetDrawLayer("OVERLAY") + titleIco:SetPoint("TOPLEFT", header, "TOPLEFT", L.SIDE_PAD, -6) + titleIco:SetVertexColor(T.gold[1], T.gold[2], T.gold[3]) + + local titleFS = header:CreateFontString(nil, "OVERLAY") + titleFS:SetFont(font, 14, "OUTLINE") + titleFS:SetPoint("LEFT", titleIco, "RIGHT", 5, 0) + titleFS:SetJustifyH("LEFT") + titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + MF.titleFS = titleFS + + -- Training points in header + local tpIco = SFrames:CreateIcon(header, "star", 12) + tpIco:SetDrawLayer("OVERLAY") + tpIco:SetPoint("BOTTOMLEFT", header, "BOTTOMLEFT", L.SIDE_PAD, 8) + tpIco:SetVertexColor(0.55, 0.85, 0.4) + + local tpLabelFS = header:CreateFontString(nil, "OVERLAY") + tpLabelFS:SetFont(font, 11, "OUTLINE") + tpLabelFS:SetPoint("LEFT", tpIco, "RIGHT", 4, 0) + tpLabelFS:SetText("训练点数:") + tpLabelFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + + local tpValueFS = header:CreateFontString(nil, "OVERLAY") + tpValueFS:SetFont(font, 13, "OUTLINE") + tpValueFS:SetPoint("LEFT", tpLabelFS, "RIGHT", 6, 0) + tpValueFS:SetText("0") + tpValueFS:SetTextColor(T.tpGood[1], T.tpGood[2], T.tpGood[3]) + MF.tpValueFS = tpValueFS + + -- Pet level in header + local petLvlFS = header:CreateFontString(nil, "OVERLAY") + petLvlFS:SetFont(font, 10, "OUTLINE") + petLvlFS:SetPoint("LEFT", tpValueFS, "RIGHT", 16, 0) + petLvlFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + MF.petLvlFS = petLvlFS + + -- Close button + local closeBtn = CreateFrame("Button", nil, header) + closeBtn:SetWidth(20); closeBtn:SetHeight(20) + closeBtn:SetPoint("TOPRIGHT", header, "TOPRIGHT", -8, -6) + local closeTex = closeBtn:CreateTexture(nil, "ARTWORK") + closeTex:SetTexture("Interface\\AddOns\\Nanami-UI\\img\\icon") + closeTex:SetTexCoord(0.25, 0.375, 0, 0.125); closeTex:SetAllPoints() + closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) + closeBtn:SetScript("OnClick", function() S.MainFrame:Hide() end) + closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end) + closeBtn:SetScript("OnLeave", function() closeTex:SetVertexColor(T.dimText[1], T.dimText[2], T.dimText[3]) end) + + -- Separators + local hsep = MF:CreateTexture(nil, "ARTWORK") + hsep:SetTexture("Interface\\Buttons\\WHITE8X8"); hsep:SetHeight(1) + hsep:SetPoint("TOPLEFT", MF, "TOPLEFT", 4, -L.HEADER_H) + hsep:SetPoint("TOPRIGHT", MF, "TOPRIGHT", -4, -L.HEADER_H) + hsep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + local vdiv = MF:CreateTexture(nil, "ARTWORK") + vdiv:SetTexture("Interface\\Buttons\\WHITE8X8"); vdiv:SetWidth(1) + vdiv:SetPoint("TOP", MF, "TOPLEFT", L.LEFT_W, -(L.HEADER_H + 2)) + vdiv:SetPoint("BOTTOM", MF, "BOTTOMLEFT", L.LEFT_W, 4) + vdiv:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + -- ═══ LEFT: Filters + Skill List ═════════════════════════════════════ + local fb = CreateFrame("Frame", nil, MF) + fb:SetPoint("TOPLEFT", MF, "TOPLEFT", L.SIDE_PAD, -(L.HEADER_H + 4)) + fb:SetWidth(L.LEFT_W - L.SIDE_PAD * 2); fb:SetHeight(22) + + local fAll = BTUI.CreateFilterBtn(fb, "全部", 52) + fAll:SetPoint("LEFT", fb, "LEFT", 0, 0) + fAll:SetScript("OnClick", function() S.currentFilter = "all"; BTUI.FullUpdate() end) + MF.filterAll = fAll + + local fAvail = BTUI.CreateFilterBtn(fb, "可学", 52) + fAvail:SetPoint("LEFT", fAll, "RIGHT", 3, 0) + fAvail:SetScript("OnClick", function() S.currentFilter = "available"; BTUI.FullUpdate() end) + MF.filterAvail = fAvail + + local fUsed = BTUI.CreateFilterBtn(fb, "已学", 52) + fUsed:SetPoint("LEFT", fAvail, "RIGHT", 3, 0) + fUsed:SetScript("OnClick", function() S.currentFilter = "used"; BTUI.FullUpdate() end) + MF.filterUsed = fUsed + + -- List scroll area + local listTop = L.HEADER_H + L.FILTER_H + 8 + local ls = CreateFrame("ScrollFrame", "SFramesBTListScroll", MF) + ls:SetPoint("TOPLEFT", MF, "TOPLEFT", L.SIDE_PAD, -listTop) + ls:SetPoint("BOTTOMRIGHT", MF, "BOTTOMLEFT", L.SIDE_PAD + L.LIST_ROW_W, 6) + + local lc = CreateFrame("Frame", "SFramesBTListContent", ls) + lc:SetWidth(L.LIST_ROW_W); lc:SetHeight(1) + ls:SetScrollChild(lc) + ls:EnableMouseWheel(true) + + -- Scrollbar + local sbTrack = CreateFrame("Frame", nil, MF) + sbTrack:SetWidth(L.SCROLLBAR_W) + sbTrack:SetPoint("TOPLEFT", ls, "TOPRIGHT", 2, 0) + sbTrack:SetPoint("BOTTOMLEFT", ls, "BOTTOMRIGHT", 2, 0) + sbTrack:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + sbTrack:SetBackdropColor(T.progressBg[1], T.progressBg[2], T.progressBg[3], 0.6) + sbTrack:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.3) + + local sbThumb = CreateFrame("Button", nil, sbTrack) + sbThumb:SetWidth(L.SCROLLBAR_W - 2); sbThumb:SetHeight(30) + sbThumb:SetPoint("TOP", sbTrack, "TOP", 0, -1) + sbThumb:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + sbThumb:SetBackdropColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], 0.7) + sbThumb:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.5) + sbThumb:EnableMouse(true); sbThumb:SetMovable(true) + sbThumb:RegisterForDrag("LeftButton") + sbThumb._dragging = false + + sbThumb:SetScript("OnEnter", function() + this:SetBackdropColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], 1) + end) + sbThumb:SetScript("OnLeave", function() + this:SetBackdropColor(T.progressFill[1], T.progressFill[2], T.progressFill[3], 0.7) + end) + sbThumb:SetScript("OnDragStart", function() + this._dragging = true + this._startY = select(2, GetCursorPosition()) / (this:GetEffectiveScale()) + this._startScroll = ls:GetVerticalScroll() + end) + sbThumb:SetScript("OnDragStop", function() this._dragging = false end) + sbThumb:SetScript("OnUpdate", function() + if not this._dragging then return end + local cursorY = select(2, GetCursorPosition()) / (this:GetEffectiveScale()) + local delta = this._startY - cursorY + local trackH = sbTrack:GetHeight() - this:GetHeight() + if trackH <= 0 then return end + local scrollMax = BTUI.GetScrollMax() + local newScroll = this._startScroll + (delta / trackH) * scrollMax + newScroll = math.max(0, math.min(scrollMax, newScroll)) + ls:SetVerticalScroll(newScroll) + BTUI.UpdateScrollbar() + end) + + sbTrack:EnableMouse(true) + sbTrack:SetScript("OnMouseDown", function() + local trackTop = sbTrack:GetTop() + local cursorY = select(2, GetCursorPosition()) / (sbTrack:GetEffectiveScale()) + local clickRatio = (trackTop - cursorY) / sbTrack:GetHeight() + clickRatio = math.max(0, math.min(1, clickRatio)) + ls:SetVerticalScroll(clickRatio * BTUI.GetScrollMax()) + BTUI.UpdateScrollbar() + end) + + MF.sbTrack = sbTrack; MF.sbThumb = sbThumb + + function BTUI.GetScrollMax() + local contentH = ls.content and ls.content:GetHeight() or 0 + local viewH = ls:GetHeight() or 0 + return math.max(0, contentH - viewH) + end + + function BTUI.UpdateScrollbar() + if not MF.sbThumb or not MF.sbTrack then return end + local scrollMax = BTUI.GetScrollMax() + if scrollMax <= 0 then MF.sbThumb:Hide(); return end + MF.sbThumb:Show() + local trackH = MF.sbTrack:GetHeight() + local curScroll = ls:GetVerticalScroll() + local ratio = curScroll / scrollMax + ratio = math.max(0, math.min(1, ratio)) + local thumbH = math.max(20, trackH * (trackH / (trackH + scrollMax))) + MF.sbThumb:SetHeight(thumbH) + local maxOffset = trackH - thumbH - 2 + MF.sbThumb:ClearAllPoints() + MF.sbThumb:SetPoint("TOP", MF.sbTrack, "TOP", 0, -(1 + ratio * maxOffset)) + end + + ls:SetScript("OnMouseWheel", function() + local cur = this:GetVerticalScroll() + local mx = BTUI.GetScrollMax() + if arg1 > 0 then this:SetVerticalScroll(math.max(0, cur - L.SCROLL_STEP)) + else this:SetVerticalScroll(math.min(mx, cur + L.SCROLL_STEP)) end + BTUI.UpdateScrollbar() + end) + ls:SetScript("OnScrollRangeChanged", function() BTUI.UpdateScrollbar() end) + ls.content = lc; MF.listScroll = ls + + for i = 1, L.MAX_ROWS do + local row = BTUI.CreateListRow(lc, i) + row:SetWidth(L.LIST_ROW_W) + row:EnableMouseWheel(true) + row:SetScript("OnMouseWheel", function() + local sf = S.MainFrame.listScroll + local cur = sf:GetVerticalScroll() + local mx = BTUI.GetScrollMax() + if arg1 > 0 then sf:SetVerticalScroll(math.max(0, cur - L.SCROLL_STEP)) + else sf:SetVerticalScroll(math.min(mx, cur + L.SCROLL_STEP)) end + BTUI.UpdateScrollbar() + end) + row:SetScript("OnClick", function() + if this.isHeader and this.catName then + BTUI.ToggleCategory(this.catName) + elseif this.craftIndex then + BTUI.SelectSkill(this.craftIndex) + end + end) + S.rowButtons[i] = row + end + + -- ═══ RIGHT: Detail ══════════════════════════════════════════════════ + local rightX = L.LEFT_W + L.SIDE_PAD + local det = CreateFrame("Frame", nil, MF) + det:SetPoint("TOPLEFT", MF, "TOPLEFT", rightX, -(L.HEADER_H + 6)) + det:SetPoint("BOTTOMRIGHT", MF, "BOTTOMRIGHT", -L.SIDE_PAD, L.BOTTOM_H + 2) + MF.detail = det + + -- Icon + local dIF = CreateFrame("Frame", nil, det) + dIF:SetWidth(44); dIF:SetHeight(44); dIF:SetPoint("TOPLEFT", det, "TOPLEFT", 0, 0) + dIF:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 14, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + dIF:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) + dIF:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) + dIF:Hide(); det.iconFrame = dIF + + local dIcon = dIF:CreateTexture(nil, "ARTWORK") + dIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + dIcon:SetPoint("TOPLEFT", dIF, "TOPLEFT", 3, -3) + dIcon:SetPoint("BOTTOMRIGHT", dIF, "BOTTOMRIGHT", -3, 3) + det.icon = dIcon + + dIF:EnableMouse(true) + dIF:SetScript("OnEnter", function() + if S.selectedIndex then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + local ok2 = pcall(GameTooltip.SetCraftSpell, GameTooltip, S.selectedIndex) + if ok2 then GameTooltip:Show() else GameTooltip:Hide() end + end + end) + dIF:SetScript("OnLeave", function() GameTooltip:Hide() end) + + -- Name + local dName = det:CreateFontString(nil, "OVERLAY") + dName:SetFont(font, 14, "OUTLINE") + dName:SetPoint("TOPLEFT", dIF, "TOPRIGHT", 8, -2) + dName:SetPoint("RIGHT", det, "RIGHT", -4, 0) + dName:SetJustifyH("LEFT") + dName:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3]) + det.nameFS = dName + + -- Rank + local dRank = det:CreateFontString(nil, "OVERLAY") + dRank:SetFont(font, 12, "OUTLINE") + dRank:SetPoint("TOPLEFT", dName, "BOTTOMLEFT", 0, -2) + dRank:SetJustifyH("LEFT") + dRank:SetTextColor(T.rankText[1], T.rankText[2], T.rankText[3]) + det.rankFS = dRank + + -- Separator + local sep1 = det:CreateTexture(nil, "ARTWORK") + sep1:SetTexture("Interface\\Buttons\\WHITE8X8"); sep1:SetHeight(1) + sep1:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -54) + sep1:SetPoint("RIGHT", det, "RIGHT", 0, 0) + sep1:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) + + -- Description (scrollable) + local descScroll = CreateFrame("ScrollFrame", nil, det) + descScroll:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -60) + descScroll:SetPoint("RIGHT", det, "RIGHT", -4, 0) + descScroll:SetHeight(120) + descScroll:EnableMouseWheel(true) + descScroll:SetScript("OnMouseWheel", function() + local cur = this:GetVerticalScroll() + local maxVal = this:GetVerticalScrollRange() + if arg1 > 0 then this:SetVerticalScroll(math.max(0, cur - 14)) + else this:SetVerticalScroll(math.min(maxVal, cur + 14)) end + end) + det.descScroll = descScroll + + local descContent = CreateFrame("Frame", nil, descScroll) + descContent:SetWidth(L.CONTENT_W - 8); descContent:SetHeight(1) + descScroll:SetScrollChild(descContent) + + local dDesc = descContent:CreateFontString(nil, "OVERLAY") + dDesc:SetFont(font, 11) + dDesc:SetPoint("TOPLEFT", descContent, "TOPLEFT", 0, 0) + dDesc:SetWidth(L.CONTENT_W - 8) + dDesc:SetJustifyH("LEFT") + dDesc:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + det.descFS = dDesc + + -- Separator 2 + local sep2 = det:CreateTexture(nil, "ARTWORK") + sep2:SetTexture("Interface\\Buttons\\WHITE8X8"); sep2:SetHeight(1) + sep2:SetPoint("TOPLEFT", det, "TOPLEFT", 0, -186) + sep2:SetPoint("RIGHT", det, "RIGHT", 0, 0) + sep2:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3) + + -- TP Cost + local dCost = det:CreateFontString(nil, "OVERLAY") + dCost:SetFont(font, 12, "OUTLINE") + dCost:SetPoint("TOPLEFT", det, "TOPLEFT", 2, -194) + dCost:SetPoint("RIGHT", det, "RIGHT", -4, 0) + dCost:SetJustifyH("LEFT") + dCost:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + det.costFS = dCost + + -- Requirements + local dReq = det:CreateFontString(nil, "OVERLAY") + dReq:SetFont(font, 11, "OUTLINE") + dReq:SetPoint("TOPLEFT", dCost, "BOTTOMLEFT", 0, -4) + dReq:SetPoint("RIGHT", det, "RIGHT", -4, 0) + dReq:SetJustifyH("LEFT") + det.reqFS = dReq + + -- ═══ Bottom Bar ═════════════════════════════════════════════════════ + local bsep = MF:CreateTexture(nil, "ARTWORK") + bsep:SetTexture("Interface\\Buttons\\WHITE8X8"); bsep:SetHeight(1) + bsep:SetPoint("BOTTOMLEFT", MF, "BOTTOMLEFT", 4, L.BOTTOM_H) + bsep:SetPoint("BOTTOMRIGHT", MF, "BOTTOMRIGHT", -4, L.BOTTOM_H) + bsep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + local trainBtn = BTUI.CreateActionBtn(MF, "训练", 100) + trainBtn:SetPoint("BOTTOMRIGHT", MF, "BOTTOMRIGHT", -L.SIDE_PAD, 8) + trainBtn:SetScript("OnClick", function() + if this.disabled then return end + if S.selectedIndex and DoCraft then + DoCraft(S.selectedIndex) + end + end) + MF.trainBtn = trainBtn + + local closeB = BTUI.CreateActionBtn(MF, "关闭", 80) + closeB:SetPoint("BOTTOMRIGHT", trainBtn, "BOTTOMLEFT", -6, 0) + closeB:SetScript("OnClick", function() S.MainFrame:Hide() end) + + -- ═══ Events ═════════════════════════════════════════════════════════ + MF:SetScript("OnHide", function() + if CloseCraft then pcall(CloseCraft) end + BTUI.CleanupBlizzardCraft() + end) + + MF:RegisterEvent("CRAFT_SHOW") + MF:RegisterEvent("CRAFT_UPDATE") + MF:RegisterEvent("CRAFT_CLOSE") + MF:SetScript("OnEvent", function() + if event == "CRAFT_SHOW" then + if not BTUI.IsBeastTraining() then return end + if CraftFrame then + CraftFrame:SetScript("OnHide", function() end) + CraftFrame:SetAlpha(0); CraftFrame:EnableMouse(false) + end + S.selectedIndex = nil; S.currentFilter = "all"; S.collapsedCats = {} + local petName = UnitName("pet") or "" + if petName ~= "" then + MF.titleFS:SetText("训练野兽 - " .. petName) + else + MF.titleFS:SetText("训练野兽") + end + local petLvl = UnitLevel("pet") + if petLvl and petLvl > 0 then + MF.petLvlFS:SetText("宠物等级: " .. petLvl) + else + MF.petLvlFS:SetText("") + end + MF:Show() + BTUI.BuildDisplayList() + for _, entry in ipairs(S.displayList) do + if entry.type == "skill" then + BTUI.SelectSkill(entry.data.index); break + end + end + BTUI.FullUpdate() + MF._hideBlizzTimer = 0 + MF:SetScript("OnUpdate", function() + if not this._hideBlizzTimer then return end + this._hideBlizzTimer = this._hideBlizzTimer + arg1 + if this._hideBlizzTimer > 0.05 then + this._hideBlizzTimer = nil; this:SetScript("OnUpdate", nil) + BTUI.CleanupBlizzardCraft() + end + end) + elseif event == "CRAFT_UPDATE" then + if S.MainFrame:IsVisible() then + BTUI.FullUpdate() + end + elseif event == "CRAFT_CLOSE" then + if S.MainFrame:IsVisible() then + BTUI.CleanupBlizzardCraft() + S.MainFrame._hideBlizzTimer = nil + S.MainFrame:SetScript("OnUpdate", nil) + S.MainFrame:Hide() + end + end + end) + + MF:Hide() + tinsert(UISpecialFrames, "SFramesBeastTrainingFrame") +end + +-------------------------------------------------------------------------------- +-- Bootstrap +-------------------------------------------------------------------------------- +local bootstrap = CreateFrame("Frame") +bootstrap:RegisterEvent("PLAYER_LOGIN") +bootstrap:SetScript("OnEvent", function() + if event == "PLAYER_LOGIN" then + if SFramesDB.enableTradeSkill == nil then SFramesDB.enableTradeSkill = true end + if SFramesDB.enableTradeSkill ~= false then BTUI:Initialize() end + end +end) + +SLASH_BTDEBUG1 = "/btdebug" +SlashCmdList["BTDEBUG"] = function() + local p = "|cffff80ff[BT-Debug]|r " + local numCrafts = GetNumCrafts and GetNumCrafts() or 0 + DEFAULT_CHAT_FRAME:AddMessage(p .. "Total crafts: " .. numCrafts) + local remaining = BTUI.GetRemainingTP() + DEFAULT_CHAT_FRAME:AddMessage(p .. "Remaining TP: " .. remaining) + local shown = 0 + for i = 1, numCrafts do + local v1,v2,v3,v4,v5,v6,v7 = GetCraftInfo(i) + if v1 and v3 ~= "header" then + shown = shown + 1 + if shown <= 12 then + DEFAULT_CHAT_FRAME:AddMessage(p .. i .. ": " .. tostring(v1) + .. " " .. tostring(v2) .. " type=" .. tostring(v3) + .. " avail=" .. tostring(v4) .. " tp=" .. tostring(v7)) + end + end + end + DEFAULT_CHAT_FRAME:AddMessage(p .. "Total skills: " .. shown) +end diff --git a/CharacterPanel.lua b/CharacterPanel.lua index 0869657..f234833 100644 --- a/CharacterPanel.lua +++ b/CharacterPanel.lua @@ -81,6 +81,25 @@ local REP_STANDING = { [5] = "友善", [6] = "尊敬", [7] = "崇敬", [8] = "崇拜", } +local PET_TAB_INDEX = nil + +local PET_FOOD_MAP = { + ["Meat"] = "肉类", ["Fish"] = "鱼类", ["Cheese"] = "奶酪", + ["Bread"] = "面包", ["Fungus"] = "蘑菇", ["Fruit"] = "水果", + ["Raw Meat"] = "生肉", ["Raw Fish"] = "生鱼", + ["Cooked Meat"] = "熟肉", ["Cooked Fish"] = "熟鱼", +} + +local PET_HAPPINESS = { + [1] = { text = "不高兴", color = { 0.9, 0.2, 0.2 } }, + [2] = { text = "满足", color = { 0.9, 0.75, 0.2 } }, + [3] = { text = "高兴", color = { 0.2, 0.9, 0.2 } }, +} + +local BEAST_TRAINING_NAMES = { + ["Beast Training"] = true, ["训练野兽"] = true, +} + -------------------------------------------------------------------------------- -- EP Stat Weights per class (Turtle WoW) -- Physical DPS: 1 AP = 1 EP; Caster DPS: 1 SP = 1 EP @@ -716,19 +735,45 @@ end -------------------------------------------------------------------------------- -- Main Frame -------------------------------------------------------------------------------- +local function SaveCharPanelPosition() + if not (panel and SFramesDB) then return end + if not SFramesDB.charPanel then SFramesDB.charPanel = {} end + local point, _, relPoint, x, y = panel:GetPoint() + if not point or not relPoint then return end + SFramesDB.charPanel.position = { + point = point, + relPoint = relPoint, + x = x or 0, + y = y or 0, + } +end + +local function ApplyCharPanelPosition(f) + f:ClearAllPoints() + local pos = SFramesDB and SFramesDB.charPanel and SFramesDB.charPanel.position + if pos and pos.point and pos.relPoint and type(pos.x) == "number" and type(pos.y) == "number" then + f:SetPoint(pos.point, UIParent, pos.relPoint, pos.x, pos.y) + else + f:SetPoint("LEFT", UIParent, "LEFT", 20, 0) + end +end + local function CreateMainFrame() if panel then return panel end local f = CreateFrame("Frame", "SFramesCharacterPanel", UIParent) f:SetWidth(FRAME_W) f:SetHeight(FRAME_H) - f:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + ApplyCharPanelPosition(f) f:SetFrameStrata("HIGH") f:EnableMouse(true) f:SetMovable(true) f:RegisterForDrag("LeftButton") f:SetScript("OnDragStart", function() this:StartMoving() end) - f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + f:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + SaveCharPanelPosition() + end) f:SetClampedToScreen(true) SetRoundBackdrop(f, T.bg, T.border) CreateShadow(f, 4) @@ -868,6 +913,12 @@ local function CreateMainFrame() MakeSep(f, 6, -HEADER_H, -6, -HEADER_H) + local _, playerClass = UnitClass("player") + if (playerClass == "HUNTER" or playerClass == "WARLOCK") and not PET_TAB_INDEX then + table.insert(TAB_NAMES, "宠物") + PET_TAB_INDEX = table.getn(TAB_NAMES) + end + -- Tab bar tabs = {} pages = {} @@ -1048,6 +1099,7 @@ function CP:UpdateCurrentTab() elseif tab == 2 then self:UpdateReputation() elseif tab == 3 then self:UpdateSkills() elseif tab == 4 then self:UpdateHonor() + elseif PET_TAB_INDEX and tab == PET_TAB_INDEX then self:UpdatePet() end end @@ -1229,6 +1281,7 @@ function CP:BuildAllPages() self:BuildReputationPage() self:BuildSkillsPage() self:BuildHonorPage() + if PET_TAB_INDEX then self:BuildPetPage() end end -------------------------------------------------------------------------------- @@ -2645,7 +2698,13 @@ function CP:ShowSwapPopup(slot) hl:SetAllPoints(row) row:SetScript("OnClick", function() - UseContainerItem(this.itemBag, this.itemSlot) + local targetID = popup.anchorSlot and popup.anchorSlot.slotID + if targetID then + PickupContainerItem(this.itemBag, this.itemSlot) + PickupInventoryItem(targetID) + else + UseContainerItem(this.itemBag, this.itemSlot) + end popup:Hide() CP:ScheduleEquipUpdate() end) @@ -3233,6 +3292,29 @@ do end) end + if BEAST_TRAINING_NAMES[sn] then + local tp = 0 + if GetPetTrainingPoints then + local ok2, total, spent = pcall(GetPetTrainingPoints) + if ok2 then tp = (total or 0) - (spent or 0) end + end + local tpFs = MakeFS(sf, 8, "LEFT", { 0.55, 0.85, 0.4 }) + tpFs:SetPoint("TOPLEFT", nfs, "BOTTOMLEFT", 0, -1) + tpFs:SetText("可用训练点数: " .. tostring(tp)) + sf:EnableMouse(true) + sf.skillName = sn + sf:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:AddLine(this.skillName or "Beast Training", 1, 0.82, 0) + GameTooltip:AddLine("打开训练野兽窗口,教授宠物技能", 0.7, 0.7, 0.7) + GameTooltip:AddLine("消耗训练点数来教授宠物各种技能", 0.7, 0.7, 0.7) + GameTooltip:Show() + end) + sf:SetScript("OnLeave", function() GameTooltip:Hide() end) + sf:SetHeight(rowH + 12) + y = y - 12 + end + table.insert(page.skillRows, { frame = sf }) y = y - rowH end @@ -3324,6 +3406,432 @@ function CP:UpdateHonor() end end +-------------------------------------------------------------------------------- +-- Tab 5: Pet (Hunter) +-------------------------------------------------------------------------------- +function CP:BuildPetPage() + if not PET_TAB_INDEX then return end + local page = pages[PET_TAB_INDEX] + if not page or page.built then return end + page.built = true + + local cw = CONTENT_W + local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4 + local pad = 8 + + local pc = CreateFrame("Frame", nil, page) + pc:SetAllPoints(page) + page.petContent = pc + + page.noPetText = MakeFS(page, 12, "CENTER", T.dimText) + page.noPetText:SetPoint("CENTER", page, "CENTER", 0, 0) + page.noPetText:SetText("当前没有宠物") + page.noPetText:Hide() + + -- 3D Model + local modelH = 180 + local modelW = cw - 8 + local modelBg = CreateFrame("Frame", nil, pc) + modelBg:SetWidth(modelW) + modelBg:SetHeight(modelH) + modelBg:SetPoint("TOP", pc, "TOP", 0, -2) + SetRoundBackdrop(modelBg, T.modelBg, T.modelBorder) + page.modelBgFrame = modelBg + + local modelFrame = CreateFrame("Frame", nil, pc) + modelFrame:SetWidth(modelW - 8) + modelFrame:SetHeight(modelH - 8) + modelFrame:SetPoint("CENTER", modelBg, "CENTER", 0, 0) + modelFrame:SetFrameLevel(pc:GetFrameLevel() + 5) + + local model = CreateFrame("PlayerModel", NextName("PetModel"), modelFrame) + model:SetAllPoints(modelFrame) + page.model = model + page.modelFrame = modelFrame + + model:EnableMouse(true) + model:EnableMouseWheel(1) + model.rotating = false + model.curFacing = 0.4 + model.curScale = 0.55 + model.posX = 0 + model.posY = -0.5 + + model:SetScript("OnMouseDown", function() + if arg1 == "LeftButton" then + this.rotating = true + this.startX = GetCursorPosition() + this.startFacing = this.curFacing or 0 + elseif arg1 == "RightButton" then + this.panning = true + local cx, cy = GetCursorPosition() + this.panStartX = cx + this.panStartY = cy + this.panOriginX = this.posX or 0 + this.panOriginY = this.posY or 0 + end + end) + model:SetScript("OnMouseUp", function() + if arg1 == "LeftButton" then this.rotating = false + elseif arg1 == "RightButton" then this.panning = false end + end) + model:SetScript("OnMouseWheel", function() + local ns = (this.curScale or 1) + arg1 * 0.1 + if ns < 0.3 then ns = 0.3 end + if ns > 3.0 then ns = 3.0 end + this.curScale = ns + this:SetModelScale(ns) + end) + model:SetScript("OnUpdate", function() + if this.rotating then + local cx = GetCursorPosition() + local diff = (cx - (this.startX or cx)) * 0.01 + this.curFacing = (this.startFacing or 0) + diff + this:SetFacing(this.curFacing) + elseif this.panning then + local cx, cy = GetCursorPosition() + local es = this:GetEffectiveScale() + if es < 0.01 then es = 1 end + local dx = (cx - (this.panStartX or cx)) / (es * 35) + local dy = (cy - (this.panStartY or cy)) / (es * 35) + this.posX = (this.panOriginX or 0) + dx + this.posY = (this.panOriginY or 0) + dy + this:SetPosition(this.posY, 0, this.posX) + end + end) + + -- Name overlay at bottom of model + page.petNameText = MakeFS(pc, 12, "LEFT", T.gold) + page.petNameText:SetPoint("BOTTOMLEFT", modelBg, "BOTTOMLEFT", 8, 4) + page.petFamilyText = MakeFS(pc, 9, "RIGHT", T.dimText) + page.petFamilyText:SetPoint("BOTTOMRIGHT", modelBg, "BOTTOMRIGHT", -8, 4) + + -- Scrollable stats area below model + local statsTop = -(modelH + 6) + local statsH = contentH - modelH - 6 + local scrollArea = CreateScrollFrame(pc, cw, statsH) + scrollArea:SetPoint("TOPLEFT", pc, "TOPLEFT", 0, statsTop) + page.scrollArea = scrollArea + local child = scrollArea.child + + local sY = -4 + + -- Info line: happiness, loyalty, training points + page.happyLabel = MakeFS(child, 9, "LEFT", T.labelText) + page.happyLabel:SetPoint("TOPLEFT", child, "TOPLEFT", pad, sY) + page.happyLabel:SetText("心情:") + page.happyValue = MakeFS(child, 9, "LEFT", T.valueText) + page.happyValue:SetPoint("LEFT", page.happyLabel, "RIGHT", 2, 0) + + page.loyalLabel = MakeFS(child, 9, "LEFT", T.labelText) + page.loyalLabel:SetPoint("LEFT", page.happyValue, "RIGHT", 10, 0) + page.loyalLabel:SetText("忠诚度:") + page.loyalValue = MakeFS(child, 9, "LEFT", T.valueText) + page.loyalValue:SetPoint("LEFT", page.loyalLabel, "RIGHT", 2, 0) + + page.tpLabel = MakeFS(child, 9, "LEFT", T.labelText) + page.tpLabel:SetPoint("LEFT", page.loyalValue, "RIGHT", 10, 0) + page.tpLabel:SetText("训练点:") + page.tpValue = MakeFS(child, 9, "LEFT", { 0.55, 0.85, 0.4 }) + page.tpValue:SetPoint("LEFT", page.tpLabel, "RIGHT", 2, 0) + sY = sY - 16 + + -- XP bar + page.xpSectionLabel = MakeFS(child, 9, "LEFT", T.sectionTitle) + page.xpSectionLabel:SetPoint("TOPLEFT", child, "TOPLEFT", pad, sY) + page.xpSectionLabel:SetText("经验值") + sY = sY - 12 + + local xpBf = CreateFrame("Frame", nil, child) + xpBf:SetHeight(8) + xpBf:SetPoint("TOPLEFT", child, "TOPLEFT", pad, sY) + xpBf:SetPoint("TOPRIGHT", child, "TOPRIGHT", -pad, sY) + SetPixelBackdrop(xpBf, T.barBg, { 0.15, 0.15, 0.18, 0.5 }) + page.xpBarFrame = xpBf + + local xpFill = xpBf:CreateTexture(nil, "ARTWORK") + xpFill:SetTexture(SFrames:GetTexture()) + xpFill:SetVertexColor(0.4, 0.65, 0.85, 0.9) + xpFill:SetPoint("TOPLEFT", xpBf, "TOPLEFT", 1, -1) + xpFill:SetPoint("BOTTOMLEFT", xpBf, "BOTTOMLEFT", 1, 1) + xpFill:SetWidth(1) + page.xpBarFill = xpFill + + page.xpBarText = MakeFS(xpBf, 7, "CENTER", { 1, 1, 1 }) + page.xpBarText:SetPoint("CENTER", xpBf, "CENTER", 0, 0) + sY = sY - 14 + + -- Stats dual-column section + sY = self:CreateStatSection(child, "属性与攻防", sY) + + local leftLabels = { "力量", "敏捷", "耐力", "智力", "精神" } + local rightLabels = { "攻击", "强度", "伤害", "防御", "护甲" } + page.petStatLeft = {} + page.petStatRight = {} + + for idx = 1, 5 do + local row1 = {} + row1.label = MakeFS(child, 9, "LEFT", T.labelText) + row1.label:SetPoint("TOPLEFT", child, "TOPLEFT", 14, sY) + row1.label:SetText(leftLabels[idx] .. ":") + row1.value = MakeFS(child, 9, "RIGHT", T.valueText) + row1.value:SetPoint("TOPLEFT", child, "TOPLEFT", 56, sY) + row1.value:SetWidth(80) + row1.value:SetJustifyH("RIGHT") + table.insert(page.petStatLeft, row1) + + local row2 = {} + row2.label = MakeFS(child, 9, "LEFT", T.labelText) + row2.label:SetPoint("TOPLEFT", child, "TOPLEFT", 160, sY) + row2.label:SetText(rightLabels[idx] .. ":") + row2.value = MakeFS(child, 9, "RIGHT", T.valueText) + row2.value:SetPoint("TOPRIGHT", child, "TOPRIGHT", -14, sY) + row2.value:SetWidth(80) + row2.value:SetJustifyH("RIGHT") + table.insert(page.petStatRight, row2) + + sY = sY - 14 + end + + -- Resistances + sY = sY - 4 + sY = self:CreateStatSection(child, "抗性", sY) + page.resStats = {} + local resSchools = { 2, 3, 4, 5, 6 } + local resPerRow = 3 + local resColW = math.floor((cw - 28) / resPerRow) + + for idx = 1, 5 do + local col = math.mod(idx - 1, resPerRow) + local rowOff = math.floor((idx - 1) / resPerRow) + local rx = 14 + col * resColW + local ry = sY - rowOff * 14 + local row = {} + local school = resSchools[idx] + local rc = T.resistColors[school] or T.labelText + row.label = MakeFS(child, 9, "LEFT", rc) + row.label:SetPoint("TOPLEFT", child, "TOPLEFT", rx, ry) + row.label:SetText(RESIST_NAMES[school] .. ":") + row.value = MakeFS(child, 9, "LEFT", T.valueText) + row.value:SetPoint("LEFT", row.label, "RIGHT", 2, 0) + row.school = school + table.insert(page.resStats, row) + end + sY = sY - math.ceil(5 / resPerRow) * 14 + + -- Food (manually created so we can show/hide for warlock) + sY = sY - 4 + local foodHeader = MakeFS(child, 11, "LEFT", T.sectionTitle) + foodHeader:SetPoint("TOPLEFT", child, "TOPLEFT", 8, sY) + foodHeader:SetText("喜好食物") + local foodSep = child:CreateTexture(nil, "ARTWORK") + foodSep:SetTexture("Interface\\Buttons\\WHITE8X8") + foodSep:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4]) + foodSep:SetHeight(1) + foodSep:SetPoint("TOPLEFT", child, "TOPLEFT", 8, sY - 14) + foodSep:SetPoint("TOPRIGHT", child, "TOPRIGHT", -8, sY - 14) + sY = sY - 18 + page.foodHeader = foodHeader + page.foodSep = foodSep + page.foodText = MakeFS(child, 9, "LEFT", T.valueText) + page.foodText:SetPoint("TOPLEFT", child, "TOPLEFT", 14, sY) + page.foodText:SetWidth(cw - 28) + sY = sY - 16 + + page.fullContentH = math.abs(sY) + 8 + scrollArea:SetContentHeight(page.fullContentH) +end + +function CP:UpdatePet() + if not PET_TAB_INDEX then return end + local page = pages[PET_TAB_INDEX] + if not page or not page.built then return end + + local hasPetUI = HasPetUI and HasPetUI() + local hasPet = UnitExists("pet") and hasPetUI + + if not hasPet then + page.petContent:Hide() + page.noPetText:Show() + return + end + + page.petContent:Show() + page.noPetText:Hide() + + -- Detect hunter pet vs warlock demon + local _, isHunterPet = HasPetUI() + + -- Set 3D model (only reload when pet identity changes to avoid animation reset) + if page.model then + local petKey = (UnitName("pet") or "") .. ":" .. (UnitLevel("pet") or 0) + if page.model.lastPetKey ~= petKey then + page.model.lastPetKey = petKey + page.model.curFacing = 0.4 + page.model.curScale = 0.55 + page.model.posX = 0 + page.model.posY = -0.5 + page.model:SetUnit("pet") + page.model:SetFacing(0.4) + page.model:SetModelScale(0.55) + page.model:SetPosition(-0.5, 0, 0) + end + end + + local petName = UnitName("pet") or "未知" + local petLevel = UnitLevel("pet") or 0 + page.petNameText:SetText(petName .. " |cff88bbddLv." .. petLevel .. "|r") + + local family = "" + if UnitCreatureFamily then + local ok, val = pcall(UnitCreatureFamily, "pet") + if ok and val then family = val end + end + page.petFamilyText:SetText(family) + + -- Hunter-only: happiness, loyalty, training points, XP, food + if isHunterPet then + page.happyLabel:Show(); page.happyValue:Show() + page.loyalLabel:Show(); page.loyalValue:Show() + page.tpLabel:Show(); page.tpValue:Show() + if page.xpSectionLabel then page.xpSectionLabel:Show() end + page.xpBarFrame:Show() + if page.foodHeader then page.foodHeader:Show() end + if page.foodSep then page.foodSep:Show() end + page.foodText:Show() + + local happiness = 0 + if GetPetHappiness then + local ok, val = pcall(GetPetHappiness) + if ok and val then happiness = val end + end + local hData = PET_HAPPINESS[happiness] + if hData then + page.happyValue:SetText(hData.text) + page.happyValue:SetTextColor(hData.color[1], hData.color[2], hData.color[3]) + else + page.happyValue:SetText("--") + page.happyValue:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3]) + end + + local loyalty = "--" + if GetPetLoyalty then + local ok, val = pcall(GetPetLoyalty) + if ok and val then loyalty = val end + end + page.loyalValue:SetText(tostring(loyalty)) + + local tp = 0 + if GetPetTrainingPoints then + local ok, total, spent = pcall(GetPetTrainingPoints) + if ok then tp = (total or 0) - (spent or 0) end + end + page.tpValue:SetText(tostring(tp)) + + local curXP, maxXP = 0, 1 + if GetPetExperience then + local ok, cx, mx = pcall(GetPetExperience) + if ok then curXP = cx or 0; maxXP = mx or 1 end + end + if maxXP == 0 then maxXP = 1 end + local xpPct = curXP / maxXP + local bw = page.xpBarFrame:GetWidth() - 2 + if bw < 1 then bw = 1 end + page.xpBarFill:SetWidth(math.max(bw * xpPct, 1)) + page.xpBarText:SetText(curXP .. " / " .. maxXP) + else + page.happyLabel:Hide(); page.happyValue:Hide() + page.loyalLabel:Hide(); page.loyalValue:Hide() + page.tpLabel:Hide(); page.tpValue:Hide() + if page.xpSectionLabel then page.xpSectionLabel:Hide() end + page.xpBarFrame:Hide() + if page.foodHeader then page.foodHeader:Hide() end + if page.foodSep then page.foodSep:Hide() end + page.foodText:Hide() + end + + -- Base stats (UnitStat returns base, effective in vanilla; use first non-nil) + local statLabels = { "力量", "敏捷", "耐力", "智力", "精神" } + for i, r in ipairs(page.petStatLeft) do + local base, eff = UnitStat("pet", i) + local val = eff or base or 0 + r.label:SetText(statLabels[i] .. ":") + r.value:SetText(tostring(val)) + end + + -- Combat stats + local mainBase, mainMod = 0, 0 + if UnitAttack then + local ok, b, m = pcall(UnitAttack, "pet") + if ok then mainBase = b or 0; mainMod = m or 0 end + end + local apBase, apPos, apNeg = 0, 0, 0 + if UnitAttackPower then + local ok, b, p, n = pcall(UnitAttackPower, "pet") + if ok then apBase = b or 0; apPos = p or 0; apNeg = n or 0 end + end + local ap = apBase + apPos + apNeg + local minDmg, maxDmg = 0, 0 + if UnitDamage then + local ok, d1, d2 = pcall(UnitDamage, "pet") + if ok then minDmg = d1 or 0; maxDmg = d2 or 0 end + end + local defBase, defMod = 0, 0 + if UnitDefense then + local ok, b, m = pcall(UnitDefense, "pet") + if ok then defBase = b or 0; defMod = m or 0 end + end + local armorBase, armorEff = 0, 0 + if UnitArmor then + local ok, b, e = pcall(UnitArmor, "pet") + if ok then armorBase = b or 0; armorEff = e or 0 end + end + + local combatLabels = { "攻击:", "强度:", "伤害:", "防御:", "护甲:" } + local combatVals = { + tostring(mainBase + mainMod), + tostring(ap), + string.format("%d-%d", math.floor(minDmg), math.floor(maxDmg)), + tostring(defBase + defMod), + tostring(armorEff > 0 and armorEff or armorBase), + } + for i, r in ipairs(page.petStatRight) do + r.label:SetText(combatLabels[i]) + r.value:SetText(combatVals[i] or "0") + end + + for _, r in ipairs(page.resStats) do + local base, bonus = 0, 0 + if UnitResistance then + local ok, b, bn = pcall(UnitResistance, "pet", r.school) + if ok then base = b or 0; bonus = bn or 0 end + end + r.label:SetText(RESIST_NAMES[r.school] .. ":") + r.value:SetText(tostring(base + bonus)) + end + + if isHunterPet then + local foodStr = "" + if GetPetFoodTypes then + local ok, r1, r2, r3, r4, r5, r6 = pcall(GetPetFoodTypes) + if ok then + local foods = {} + if r1 and r1 ~= "" then table.insert(foods, PET_FOOD_MAP[r1] or r1) end + if r2 and r2 ~= "" then table.insert(foods, PET_FOOD_MAP[r2] or r2) end + if r3 and r3 ~= "" then table.insert(foods, PET_FOOD_MAP[r3] or r3) end + if r4 and r4 ~= "" then table.insert(foods, PET_FOOD_MAP[r4] or r4) end + if r5 and r5 ~= "" then table.insert(foods, PET_FOOD_MAP[r5] or r5) end + if r6 and r6 ~= "" then table.insert(foods, PET_FOOD_MAP[r6] or r6) end + foodStr = table.concat(foods, "、") + end + end + page.foodText:SetText(foodStr) + page.scrollArea:SetContentHeight(page.fullContentH) + else + page.scrollArea:SetContentHeight(page.fullContentH) + end +end + -------------------------------------------------------------------------------- -- Events -------------------------------------------------------------------------------- @@ -3336,6 +3844,8 @@ local cpEvents = { "UNIT_ATTACK", "UNIT_DEFENSE", "UNIT_RESISTANCES", "CHAT_MSG_SKILL", "CHAT_MSG_COMBAT_HONOR_GAIN", "CHARACTER_POINTS_CHANGED", "PLAYER_ENTERING_WORLD", + "UNIT_PET", "PET_UI_UPDATE", "PET_BAR_UPDATE", + "UNIT_PET_EXPERIENCE", "PET_UI_CLOSE", "UNIT_HAPPINESS", } for _, ev in ipairs(cpEvents) do pcall(function() eventFrame:RegisterEvent(ev) end) @@ -3372,12 +3882,16 @@ ToggleCharacter = function(tab) end return end + if tab == "PetPaperDollFrame" then + CreateMainFrame() + CP:Toggle(PET_TAB_INDEX or 1) + return + end local tabMap = { ["PaperDollFrame"] = 1, ["ReputationFrame"] = 2, ["SkillFrame"] = 3, ["HonorFrame"] = 4, - ["PetPaperDollFrame"] = 1, } CP:Toggle(tabMap[tab] or 1) end diff --git a/Chat.lua b/Chat.lua index 9e63ff8..bafdae4 100644 --- a/Chat.lua +++ b/Chat.lua @@ -1677,20 +1677,31 @@ local function SkinPopupEditBox(popup) local eb = GetPopupEditBox(popup) if not eb or eb._sfSkinned then return end eb._sfSkinned = true - local regions = { eb:GetRegions() } - for _, r in ipairs(regions) do - if r and r:GetObjectType() == "Texture" then - r:SetAlpha(0) + + local name = eb:GetName() or "" + if name ~= "" then + for _, suffix in ipairs({"Left", "Middle", "Mid", "Right"}) do + local tex = _G[name .. suffix] + if tex and tex.SetAlpha then tex:SetAlpha(0) end end end - eb:SetBackdrop({ + + local bg = CreateFrame("Frame", nil, eb) + bg:SetPoint("TOPLEFT", eb, "TOPLEFT", -3, 3) + bg:SetPoint("BOTTOMRIGHT", eb, "BOTTOMRIGHT", 3, -3) + bg:SetFrameLevel(math.max((eb:GetFrameLevel() or 1) - 1, 0)) + bg:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, edgeSize = 12, insets = { left = 3, right = 3, top = 3, bottom = 3 }, }) - eb:SetBackdropColor(0.08, 0.06, 0.1, 0.95) - eb:SetBackdropBorderColor(0.5, 0.4, 0.55, 0.8) + bg:SetBackdropColor(0.08, 0.06, 0.1, 0.95) + bg:SetBackdropBorderColor(0.5, 0.4, 0.55, 0.8) + eb._sfBg = bg + + if eb.SetTextInsets then eb:SetTextInsets(6, 6, 2, 2) end + if eb.SetTextColor then eb:SetTextColor(1, 1, 1) end end local function ResolvePopupFrame(whichKey, dialog) @@ -4496,7 +4507,7 @@ function SFrames.Chat:CreateContainer() titleBtn:SetHeight(20) titleBtn:SetFrameStrata("HIGH") titleBtn:SetFrameLevel(f:GetFrameLevel() + 20) - titleBtn:RegisterForClicks("LeftButtonUp") + titleBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp") local titleBtnThrottle = 0 titleBtn:SetScript("OnUpdate", function() titleBtnThrottle = titleBtnThrottle + arg1 @@ -4506,8 +4517,14 @@ function SFrames.Chat:CreateContainer() this:SetWidth(tw + 28) end) titleBtn:SetScript("OnClick", function() - if SFrames and SFrames.ConfigUI then - SFrames.ConfigUI:OpenUI() + if arg1 == "RightButton" then + if SFrames.Movers and SFrames.Movers.ToggleLayoutMode then + SFrames.Movers:ToggleLayoutMode() + end + else + if SFrames and SFrames.ConfigUI then + SFrames.ConfigUI:OpenUI() + end end end) titleBtn:SetScript("OnEnter", function() @@ -4516,7 +4533,8 @@ function SFrames.Chat:CreateContainer() GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT") GameTooltip:ClearLines() GameTooltip:AddLine("Nanami UI 设置", 1, 0.84, 0.94) - GameTooltip:AddLine("点击打开主设置面板", 0.85, 0.85, 0.85) + GameTooltip:AddLine("左键打开主设置面板", 0.85, 0.85, 0.85) + GameTooltip:AddLine("右键开启布局模式", 0.85, 0.85, 0.85) GameTooltip:Show() end) titleBtn:SetScript("OnLeave", function() @@ -5296,6 +5314,8 @@ function SFrames.Chat:SwitchActiveChatFrame(tab) if cf then self:EnforceChatWindowLock(cf) if cf == activeChatFrame then + if UIFrameFadeRemoveFrame then pcall(UIFrameFadeRemoveFrame, cf) end + cf.fadeInfo = nil if cf.SetAlpha then cf:SetAlpha(1) end if cf.EnableMouse then cf:EnableMouse(true) end if cf.SetFrameLevel and self.frame and self.frame.inner then @@ -5306,6 +5326,8 @@ function SFrames.Chat:SwitchActiveChatFrame(tab) self.chatFrameIsCombat = isCombat self:RefreshChatBounds() else + if UIFrameFadeRemoveFrame then pcall(UIFrameFadeRemoveFrame, cf) end + cf.fadeInfo = nil cf:Hide() if cf.SetAlpha then cf:SetAlpha(0) end if cf.EnableMouse then cf:EnableMouse(false) end @@ -6775,6 +6797,11 @@ function SFrames.Chat:Initialize() end end end + + if SFrames.Movers and SFrames.Movers.RegisterMover and self.frame then + SFrames.Movers:RegisterMover("ChatFrame", self.frame, "聊天框", + "BOTTOMLEFT", "UIParent", "BOTTOMLEFT", 0, 0) + end end function SFrames.Chat:ShowCopyDialog(text) diff --git a/ConfigUI.lua b/ConfigUI.lua index 717177c..09b036c 100644 --- a/ConfigUI.lua +++ b/ConfigUI.lua @@ -413,6 +413,8 @@ local function EnsureDB() if type(SFramesDB.playerPowerHeight) ~= "number" then SFramesDB.playerPowerHeight = 9 end if SFramesDB.playerShowClass == nil then SFramesDB.playerShowClass = true end if SFramesDB.playerShowClassIcon == nil then SFramesDB.playerShowClassIcon = true end + if SFramesDB.playerShowPortrait == nil then SFramesDB.playerShowPortrait = true end + if type(SFramesDB.playerFrameAlpha) ~= "number" then SFramesDB.playerFrameAlpha = 1 end if type(SFramesDB.playerNameFontSize) ~= "number" then SFramesDB.playerNameFontSize = 10 end if type(SFramesDB.playerValueFontSize) ~= "number" then SFramesDB.playerValueFontSize = 10 end @@ -423,6 +425,8 @@ local function EnsureDB() if type(SFramesDB.targetPowerHeight) ~= "number" then SFramesDB.targetPowerHeight = 9 end if SFramesDB.targetShowClass == nil then SFramesDB.targetShowClass = true end if SFramesDB.targetShowClassIcon == nil then SFramesDB.targetShowClassIcon = true end + if SFramesDB.targetShowPortrait == nil then SFramesDB.targetShowPortrait = true end + if type(SFramesDB.targetFrameAlpha) ~= "number" then SFramesDB.targetFrameAlpha = 1 end if type(SFramesDB.targetNameFontSize) ~= "number" then SFramesDB.targetNameFontSize = 10 end if type(SFramesDB.targetValueFontSize) ~= "number" then SFramesDB.targetValueFontSize = 10 end if SFramesDB.targetDistanceEnabled == nil then SFramesDB.targetDistanceEnabled = true end @@ -486,6 +490,8 @@ local function EnsureDB() for key, value in pairs(defaults) do if SFramesDB.Bags[key] == nil then SFramesDB.Bags[key] = value end end + if type(SFramesDB.Bags.alpha) ~= "number" then SFramesDB.Bags.alpha = 1 end + if type(SFramesDB.Bags.bankAlpha) ~= "number" then SFramesDB.Bags.bankAlpha = 1 end if type(SFramesDB.Minimap) ~= "table" then SFramesDB.Minimap = {} end if SFramesDB.Minimap.enabled == nil then SFramesDB.Minimap.enabled = true end @@ -493,6 +499,7 @@ local function EnsureDB() if SFramesDB.Minimap.showClock == nil then SFramesDB.Minimap.showClock = true end if SFramesDB.Minimap.showCoords == nil then SFramesDB.Minimap.showCoords = true end if SFramesDB.Minimap.mapStyle == nil then SFramesDB.Minimap.mapStyle = "auto" end + if SFramesDB.Minimap.mapShape == nil then SFramesDB.Minimap.mapShape = "square1" end if type(SFramesDB.Minimap.posX) ~= "number" then SFramesDB.Minimap.posX = -5 end if type(SFramesDB.Minimap.posY) ~= "number" then SFramesDB.Minimap.posY = -5 end @@ -540,12 +547,16 @@ local function EnsureDB() scale = 1.0, barCount = 3, showHotkey = true, showMacroName = false, rangeColoring = true, showPetBar = true, showStanceBar = true, showRightBars = true, buttonRounded = false, buttonInnerShadow = false, + alpha = 1.0, } for k, v in pairs(abDef) do if SFramesDB.ActionBars[k] == nil then SFramesDB.ActionBars[k] = v end end if SFramesDB.enableUnitFrames == nil then SFramesDB.enableUnitFrames = true end + if SFramesDB.enablePlayerFrame == nil then SFramesDB.enablePlayerFrame = true end + if SFramesDB.enableTargetFrame == nil then SFramesDB.enableTargetFrame = true end + if SFramesDB.enablePartyFrame == nil then SFramesDB.enablePartyFrame = true end if SFramesDB.enableChat == nil then SFramesDB.enableChat = true end if type(SFramesDB.Theme) ~= "table" then SFramesDB.Theme = {} end @@ -841,7 +852,7 @@ function SFrames.ConfigUI:BuildUIPage() -- Section 内容从 y=-32 开始(标题24 + 分隔线 + 8px 间距) -- 每个选项行占 26px,描述文字占 16px - local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 830) + local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 990) local root = uiScroll.child -- ── 初始化向导 ────────────────────────────────────────────── @@ -854,8 +865,61 @@ function SFrames.ConfigUI:BuildUIPage() end end) + -- ── 布局模式 ────────────────────────────────────────────────── + local layoutSection = CreateSection(root, "布局模式", 8, -78, 520, 62, font) + CreateDesc(layoutSection, "进入布局模式,拖拽调整所有 UI 元素的位置", 14, -30, font, 380) + CreateButton(layoutSection, "进入布局模式", 370, -30, 140, 24, function() + if SFrames.ConfigUI.frame then SFrames.ConfigUI.frame:Hide() end + if SFrames.Movers and SFrames.Movers.ToggleLayoutMode then + SFrames.Movers:ToggleLayoutMode() + end + end) + + -- ── UI 缩放 ────────────────────────────────────────────────── + local scaleSection = CreateSection(root, "UI 缩放", 8, -148, 520, 82, font) + + local function ApplyUIScaleKeepPos(newScale) + UIParent:SetScale(newScale) + local panel = SFrames.ConfigUI.frame + if panel and panel:IsShown() then + panel:ClearAllPoints() + panel:SetPoint("CENTER", UIParent, "CENTER", 0, 0) + end + end + + table.insert(controls, CreateCheckBox(scaleSection, + "启用自定义 UI 缩放", 14, -34, + function() return GetCVar("useUiScale") == "1" end, + function(checked) + SetCVar("useUiScale", checked and "1" or "0") + if checked then + local s = tonumber(GetCVar("uiScale")) or 1.0 + ApplyUIScaleKeepPos(s) + else + ApplyUIScaleKeepPos(1.0) + end + end + )) + CreateDesc(scaleSection, "调用系统 UI 缩放,对所有界面元素生效", 36, -50, font) + + local uiScaleSlider = CreateSlider(scaleSection, "缩放比例", 270, -34, 200, 0.64, 1.00, 0.01, + function() return tonumber(GetCVar("uiScale")) or 1.0 end, + function(value) + SetCVar("uiScale", tostring(value)) + end, + function(v) return string.format("%.0f%%", v * 100) end + ) + table.insert(controls, uiScaleSlider) + + uiScaleSlider:SetScript("OnMouseUp", function() + if GetCVar("useUiScale") == "1" then + local s = tonumber(GetCVar("uiScale")) or 1.0 + ApplyUIScaleKeepPos(s) + end + end) + -- ── 全局 ────────────────────────────────────────────────────── - local globalSection = CreateSection(root, "全局", 8, -78, 520, 288, font) + local globalSection = CreateSection(root, "全局", 8, -238, 520, 288, font) table.insert(controls, CreateCheckBox(globalSection, "显示玩家/目标等级文本", 14, -34, @@ -950,8 +1014,21 @@ function SFrames.ConfigUI:BuildUIPage() )) CreateDesc(globalSection, "替换默认邮箱窗口,支持批量收取和多物品发送(需重载UI)", 36, -266, font) + table.insert(controls, CreateCheckBox(globalSection, + "启用自定义拾取界面", 270, -250, + function() + if type(SFramesDB.lootDisplay) ~= "table" then return true end + return SFramesDB.lootDisplay.enable ~= false + end, + function(checked) + if type(SFramesDB.lootDisplay) ~= "table" then SFramesDB.lootDisplay = {} end + SFramesDB.lootDisplay.enable = checked + end + )) + CreateDesc(globalSection, "替换原生拾取窗口,品质染色+拾取提示(需重载UI)", 292, -266, font) + -- ── 增强功能(Libs 库集成)────────────────────────────────── - local enhSection = CreateSection(root, "增强功能(需安装 !Libs 插件)", 8, -376, 520, 204, font) + local enhSection = CreateSection(root, "增强功能(需安装 !Libs 插件)", 8, -536, 520, 204, font) table.insert(controls, CreateCheckBox(enhSection, "血条平滑动画(需 /reload)", 14, -34, @@ -1003,7 +1080,7 @@ function SFrames.ConfigUI:BuildUIPage() CreateLabel(enhSection, "提示:以上功能需要安装对应的 !Libs 库才能生效。", 14, -172, font, 10, 0.6, 0.6, 0.65) -- ── ShaguTweaks 功能移植 ────────────────────────────────────── - local tweaksSection = CreateSection(root, "ShaguTweaks 功能移植(需 /reload 生效)", 8, -590, 520, 214, font) + local tweaksSection = CreateSection(root, "ShaguTweaks 功能移植(需 /reload 生效)", 8, -750, 520, 214, font) table.insert(controls, CreateCheckBox(tweaksSection, "自动切换姿态/形态", 14, -34, @@ -1068,37 +1145,43 @@ function SFrames.ConfigUI:BuildPlayerPage() if SFrames.Player and SFrames.Player.UpdateAll then SFrames.Player:UpdateAll() end end - local playerSection = CreateSection(page, "玩家框体", 8, -8, 520, 254, font) + local playerSection = CreateSection(page, "玩家框体", 8, -8, 520, 342, font) - table.insert(controls, CreateSlider(playerSection, "缩放", 14, -46, 150, 0.7, 1.8, 0.05, + table.insert(controls, CreateCheckBox(playerSection, + "启用 Nanami 玩家框体(需 /reload)", 12, -28, + function() return SFramesDB.enablePlayerFrame ~= false end, + function(checked) SFramesDB.enablePlayerFrame = checked end + )) + + table.insert(controls, CreateSlider(playerSection, "缩放", 14, -72, 150, 0.7, 1.8, 0.05, function() return SFramesDB.playerFrameScale end, function(value) SFramesDB.playerFrameScale = value end, function(v) return string.format("%.2f", v) end, function() RefreshPlayer() end )) - table.insert(controls, CreateSlider(playerSection, "框体宽度", 170, -46, 150, 170, 420, 1, + table.insert(controls, CreateSlider(playerSection, "框体宽度", 170, -72, 150, 170, 420, 1, function() return SFramesDB.playerFrameWidth end, function(value) SFramesDB.playerFrameWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshPlayer() end )) - table.insert(controls, CreateSlider(playerSection, "头像宽度", 326, -46, 130, 32, 95, 1, + table.insert(controls, CreateSlider(playerSection, "头像宽度", 326, -72, 130, 32, 95, 1, function() return SFramesDB.playerPortraitWidth end, function(value) SFramesDB.playerPortraitWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshPlayer() end )) - table.insert(controls, CreateSlider(playerSection, "生命条高度", 14, -108, 150, 14, 80, 1, + table.insert(controls, CreateSlider(playerSection, "生命条高度", 14, -134, 150, 14, 80, 1, function() return SFramesDB.playerHealthHeight end, function(value) SFramesDB.playerHealthHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshPlayer() end )) - table.insert(controls, CreateSlider(playerSection, "能量条高度", 170, -108, 150, 6, 40, 1, + table.insert(controls, CreateSlider(playerSection, "能量条高度", 170, -134, 150, 6, 40, 1, function() return SFramesDB.playerPowerHeight end, function(value) SFramesDB.playerPowerHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, @@ -1106,39 +1189,57 @@ function SFrames.ConfigUI:BuildPlayerPage() )) table.insert(controls, CreateCheckBox(playerSection, - "玩家姓名显示职业", 326, -106, + "玩家姓名显示职业", 326, -132, function() return SFramesDB.playerShowClass ~= false end, function(checked) SFramesDB.playerShowClass = checked end, function() RefreshPlayer() end )) table.insert(controls, CreateCheckBox(playerSection, - "玩家显示职业图标", 326, -132, + "玩家显示职业图标", 326, -158, function() return SFramesDB.playerShowClassIcon ~= false end, function(checked) SFramesDB.playerShowClassIcon = checked end, function() RefreshPlayer() end )) - table.insert(controls, CreateSlider(playerSection, "姓名字号", 14, -170, 150, 8, 18, 1, + table.insert(controls, CreateCheckBox(playerSection, + "启用3D头像", 12, -188, + function() return SFramesDB.playerShowPortrait ~= false end, + function(checked) SFramesDB.playerShowPortrait = checked end, + function() RefreshPlayer() end + )) + + table.insert(controls, CreateSlider(playerSection, "姓名字号", 14, -226, 150, 8, 18, 1, function() return SFramesDB.playerNameFontSize end, function(value) SFramesDB.playerNameFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshPlayer() end )) - table.insert(controls, CreateSlider(playerSection, "数值字号", 170, -170, 150, 8, 18, 1, + table.insert(controls, CreateSlider(playerSection, "数值字号", 170, -226, 150, 8, 18, 1, function() return SFramesDB.playerValueFontSize end, function(value) SFramesDB.playerValueFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshPlayer() end )) + table.insert(controls, CreateSlider(playerSection, "透明度", 326, -226, 130, 0.1, 1.0, 0.05, + function() return SFramesDB.playerFrameAlpha end, + function(value) SFramesDB.playerFrameAlpha = value end, + function(v) return string.format("%.0f%%", v * 100) end, + function(value) + if SFrames.Player and SFrames.Player.frame then + SFrames.Player.frame:SetAlpha(value) + end + end + )) + CreateLabel(playerSection, - "提示:宽度/头像/血条/能量条可独立实时调节。", - 14, -221, font, 10, 0.9, 0.9, 0.9) + "提示:关闭头像后职业图标移到框体最左侧,框体仅显示血条。", + 14, -299, font, 10, 0.9, 0.9, 0.9) -- ── 宠物框体 ────────────────────────────────────────────────── - local petSection = CreateSection(page, "宠物框体", 8, -270, 520, 98, font) + local petSection = CreateSection(page, "宠物框体", 8, -358, 520, 98, font) table.insert(controls, CreateCheckBox(petSection, "显示宠物框体", 14, -34, @@ -1171,37 +1272,43 @@ function SFrames.ConfigUI:BuildTargetPage() if SFrames.Target and SFrames.Target.UpdateAll then SFrames.Target:UpdateAll() end end - local targetSection = CreateSection(page, "目标框体", 8, -8, 520, 294, font) + local targetSection = CreateSection(page, "目标框体", 8, -8, 520, 382, font) - table.insert(controls, CreateSlider(targetSection, "缩放", 14, -46, 150, 0.7, 1.8, 0.05, + table.insert(controls, CreateCheckBox(targetSection, + "启用 Nanami 目标框体(需 /reload)", 12, -28, + function() return SFramesDB.enableTargetFrame ~= false end, + function(checked) SFramesDB.enableTargetFrame = checked end + )) + + table.insert(controls, CreateSlider(targetSection, "缩放", 14, -72, 150, 0.7, 1.8, 0.05, function() return SFramesDB.targetFrameScale end, function(value) SFramesDB.targetFrameScale = value end, function(v) return string.format("%.2f", v) end, function() RefreshTarget() end )) - table.insert(controls, CreateSlider(targetSection, "框体宽度", 170, -46, 150, 170, 420, 1, + table.insert(controls, CreateSlider(targetSection, "框体宽度", 170, -72, 150, 170, 420, 1, function() return SFramesDB.targetFrameWidth end, function(value) SFramesDB.targetFrameWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshTarget() end )) - table.insert(controls, CreateSlider(targetSection, "头像宽度", 326, -46, 130, 32, 95, 1, + table.insert(controls, CreateSlider(targetSection, "头像宽度", 326, -72, 130, 32, 95, 1, function() return SFramesDB.targetPortraitWidth end, function(value) SFramesDB.targetPortraitWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshTarget() end )) - table.insert(controls, CreateSlider(targetSection, "生命条高度", 14, -108, 150, 14, 80, 1, + table.insert(controls, CreateSlider(targetSection, "生命条高度", 14, -134, 150, 14, 80, 1, function() return SFramesDB.targetHealthHeight end, function(value) SFramesDB.targetHealthHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshTarget() end )) - table.insert(controls, CreateSlider(targetSection, "能量条高度", 170, -108, 150, 6, 40, 1, + table.insert(controls, CreateSlider(targetSection, "能量条高度", 170, -134, 150, 6, 40, 1, function() return SFramesDB.targetPowerHeight end, function(value) SFramesDB.targetPowerHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, @@ -1209,35 +1316,53 @@ function SFrames.ConfigUI:BuildTargetPage() )) table.insert(controls, CreateCheckBox(targetSection, - "目标姓名显示职业", 326, -106, + "目标姓名显示职业", 326, -132, function() return SFramesDB.targetShowClass ~= false end, function(checked) SFramesDB.targetShowClass = checked end, function() RefreshTarget() end )) table.insert(controls, CreateCheckBox(targetSection, - "目标显示职业图标", 326, -132, + "目标显示职业图标", 326, -158, function() return SFramesDB.targetShowClassIcon ~= false end, function(checked) SFramesDB.targetShowClassIcon = checked end, function() RefreshTarget() end )) - table.insert(controls, CreateSlider(targetSection, "姓名字号", 14, -170, 150, 8, 18, 1, + table.insert(controls, CreateCheckBox(targetSection, + "启用3D头像", 12, -188, + function() return SFramesDB.targetShowPortrait ~= false end, + function(checked) SFramesDB.targetShowPortrait = checked end, + function() RefreshTarget() end + )) + + table.insert(controls, CreateSlider(targetSection, "姓名字号", 14, -226, 150, 8, 18, 1, function() return SFramesDB.targetNameFontSize end, function(value) SFramesDB.targetNameFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshTarget() end )) - table.insert(controls, CreateSlider(targetSection, "数值字号", 170, -170, 150, 8, 18, 1, + table.insert(controls, CreateSlider(targetSection, "数值字号", 170, -226, 150, 8, 18, 1, function() return SFramesDB.targetValueFontSize end, function(value) SFramesDB.targetValueFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshTarget() end )) + table.insert(controls, CreateSlider(targetSection, "透明度", 326, -226, 130, 0.1, 1.0, 0.05, + function() return SFramesDB.targetFrameAlpha end, + function(value) SFramesDB.targetFrameAlpha = value end, + function(v) return string.format("%.0f%%", v * 100) end, + function(value) + if SFrames.Target and SFrames.Target.frame then + SFrames.Target.frame:SetAlpha(value) + end + end + )) + table.insert(controls, CreateCheckBox(targetSection, - "启用目标距离文本", 326, -168, + "启用目标距离文本", 326, -266, function() return SFramesDB.targetDistanceEnabled ~= false end, function(checked) SFramesDB.targetDistanceEnabled = checked end, function(checked) @@ -1251,13 +1376,13 @@ function SFrames.ConfigUI:BuildTargetPage() end )) - table.insert(controls, CreateSlider(targetSection, "距离文本缩放", 14, -232, 150, 0.7, 1.8, 0.05, + table.insert(controls, CreateSlider(targetSection, "距离文本缩放", 14, -318, 150, 0.7, 1.8, 0.05, function() return SFramesDB.targetDistanceScale end, function(value) SFramesDB.targetDistanceScale = value end, function(v) return string.format("%.2f", v) end, function(value) - if SFrames.Target and SFrames.Target.distanceFrame then - SFrames.Target.distanceFrame:SetScale(value) + if SFrames.Target and SFrames.Target.ApplyDistanceScale then + SFrames.Target:ApplyDistanceScale(value) end end )) @@ -1280,18 +1405,24 @@ function SFrames.ConfigUI:BuildPartyPage() local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 320) local root = uiScroll.child - local partySection = CreateSection(root, "小队", 8, -8, 520, 356, font) + local partySection = CreateSection(root, "小队", 8, -8, 520, 382, font) - CreateButton(partySection, "解锁小队框架", 14, -26, 130, 22, function() + table.insert(controls, CreateCheckBox(partySection, + "启用 Nanami 小队框体(需 /reload)", 12, -28, + function() return SFramesDB.enablePartyFrame ~= false end, + function(checked) SFramesDB.enablePartyFrame = checked end + )) + + CreateButton(partySection, "解锁小队框架", 14, -52, 130, 22, function() if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end end) - CreateButton(partySection, "锁定小队框架", 154, -26, 130, 22, function() + CreateButton(partySection, "锁定小队框架", 154, -52, 130, 22, function() if SFrames and SFrames.LockFrames then SFrames:LockFrames() end end) table.insert(controls, CreateCheckBox(partySection, - "横向布局(关闭为竖向)", 12, -60, + "横向布局(关闭为竖向)", 12, -86, function() return SFramesDB.partyLayout == "horizontal" end, function(checked) if checked then SFramesDB.partyLayout = "horizontal" else SFramesDB.partyLayout = "vertical" end @@ -1304,70 +1435,70 @@ function SFrames.ConfigUI:BuildPartyPage() end )) - table.insert(controls, CreateSlider(partySection, "缩放", 14, -108, 150, 0.7, 1.8, 0.05, + table.insert(controls, CreateSlider(partySection, "缩放", 14, -134, 150, 0.7, 1.8, 0.05, function() return SFramesDB.partyFrameScale end, function(value) SFramesDB.partyFrameScale = value end, function(v) return string.format("%.2f", v) end, function() RefreshParty() end )) - table.insert(controls, CreateSlider(partySection, "框体宽度", 170, -108, 150, 120, 320, 1, + table.insert(controls, CreateSlider(partySection, "框体宽度", 170, -134, 150, 120, 320, 1, function() return SFramesDB.partyFrameWidth end, function(value) SFramesDB.partyFrameWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) - table.insert(controls, CreateSlider(partySection, "框体高度", 326, -108, 130, 28, 80, 1, + table.insert(controls, CreateSlider(partySection, "框体高度", 326, -134, 130, 28, 80, 1, function() return SFramesDB.partyFrameHeight end, function(value) SFramesDB.partyFrameHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) - table.insert(controls, CreateSlider(partySection, "头像宽度", 14, -170, 150, 24, 64, 1, + table.insert(controls, CreateSlider(partySection, "头像宽度", 14, -196, 150, 24, 64, 1, function() return SFramesDB.partyPortraitWidth end, function(value) SFramesDB.partyPortraitWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) - table.insert(controls, CreateSlider(partySection, "生命条高度", 170, -170, 150, 10, 60, 1, + table.insert(controls, CreateSlider(partySection, "生命条高度", 170, -196, 150, 10, 60, 1, function() return SFramesDB.partyHealthHeight end, function(value) SFramesDB.partyHealthHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) - table.insert(controls, CreateSlider(partySection, "能量条高度", 326, -170, 130, 6, 30, 1, + table.insert(controls, CreateSlider(partySection, "能量条高度", 326, -196, 130, 6, 30, 1, function() return SFramesDB.partyPowerHeight end, function(value) SFramesDB.partyPowerHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) - table.insert(controls, CreateSlider(partySection, "横向间距", 14, -232, 150, 0, 40, 1, + table.insert(controls, CreateSlider(partySection, "横向间距", 14, -258, 150, 0, 40, 1, function() return SFramesDB.partyHorizontalGap end, function(value) SFramesDB.partyHorizontalGap = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) - table.insert(controls, CreateSlider(partySection, "纵向间距", 170, -232, 150, 0, 80, 1, + table.insert(controls, CreateSlider(partySection, "纵向间距", 170, -258, 150, 0, 80, 1, function() return SFramesDB.partyVerticalGap end, function(value) SFramesDB.partyVerticalGap = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) - table.insert(controls, CreateSlider(partySection, "姓名字号", 326, -232, 130, 8, 18, 1, + table.insert(controls, CreateSlider(partySection, "姓名字号", 326, -258, 130, 8, 18, 1, function() return SFramesDB.partyNameFontSize end, function(value) SFramesDB.partyNameFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshParty() end )) - table.insert(controls, CreateSlider(partySection, "数值字号", 14, -294, 150, 8, 18, 1, + table.insert(controls, CreateSlider(partySection, "数值字号", 14, -320, 150, 8, 18, 1, function() return SFramesDB.partyValueFontSize end, function(value) SFramesDB.partyValueFontSize = value end, function(v) return tostring(math.floor(v + 0.5)) end, @@ -1375,20 +1506,20 @@ function SFrames.ConfigUI:BuildPartyPage() )) table.insert(controls, CreateCheckBox(partySection, - "显示小队增益", 170, -292, + "显示小队增益", 170, -318, function() return SFramesDB.partyShowBuffs ~= false end, function(checked) SFramesDB.partyShowBuffs = checked end, function() RefreshParty() end )) table.insert(controls, CreateCheckBox(partySection, - "显示小队减益", 326, -292, + "显示小队减益", 326, -318, function() return SFramesDB.partyShowDebuffs ~= false end, function(checked) SFramesDB.partyShowDebuffs = checked end, function() RefreshParty() end )) - CreateButton(partySection, "小队测试模式", 326, -324, 130, 22, function() + CreateButton(partySection, "小队测试模式", 326, -350, 130, 22, function() if SFrames.Party and SFrames.Party.TestMode then SFrames.Party:TestMode() end end) uiScroll:UpdateRange() @@ -1401,10 +1532,10 @@ function SFrames.ConfigUI:BuildBagPage() local page = self.bagPage local controls = {} - local bagScroll = CreateScrollArea(page, 4, -4, 548, 458, 510) + local bagScroll = CreateScrollArea(page, 4, -4, 548, 458, 630) local root = bagScroll.child - local bagSection = CreateSection(root, "背包", 8, -8, 502, 196, font) + local bagSection = CreateSection(root, "背包", 8, -8, 502, 246, font) table.insert(controls, CreateCheckBox(bagSection, "启用 Nanami 背包(需 /reload)", 12, -28, @@ -1449,13 +1580,22 @@ function SFrames.ConfigUI:BuildBagPage() end )) - CreateButton(bagSection, "打开背包", 258, -150, 220, 24, function() + table.insert(controls, CreateSlider(bagSection, "背包透明度", 258, -150, 220, 0.1, 1.0, 0.05, + function() return SFramesDB.Bags.alpha or 1 end, + function(value) SFramesDB.Bags.alpha = value end, + function(value) return string.format("%.0f%%", value * 100) end, + function(value) + if SFramesBagFrame then SFramesBagFrame:SetAlpha(value) end + end + )) + + CreateButton(bagSection, "打开背包", 16, -196, 220, 24, function() if SFrames and SFrames.Bags and SFrames.Bags.Container and SFrames.Bags.Container.Open then SFrames.Bags.Container:Open() end end) - local bankSection = CreateSection(root, "银行", 8, -210, 502, 155, font) + local bankSection = CreateSection(root, "银行", 8, -260, 502, 215, font) table.insert(controls, CreateSlider(bankSection, "银行列数", 16, -50, 220, 4, 24, 1, function() return SFramesDB.Bags.bankColumns end, @@ -1488,7 +1628,16 @@ function SFrames.ConfigUI:BuildBagPage() end )) - CreateButton(bankSection, "打开离线银行", 258, -110, 220, 24, function() + table.insert(controls, CreateSlider(bankSection, "银行透明度", 258, -110, 220, 0.1, 1.0, 0.05, + function() return SFramesDB.Bags.bankAlpha or 1 end, + function(value) SFramesDB.Bags.bankAlpha = value end, + function(value) return string.format("%.0f%%", value * 100) end, + function(value) + if SFramesBankFrame then SFramesBankFrame:SetAlpha(value) end + end + )) + + CreateButton(bankSection, "打开离线银行", 16, -170, 220, 24, function() if SFrames and SFrames.Bags and SFrames.Bags.Bank and SFrames.Bags.Bank.OpenOffline then SFrames.Bags.Bank:OpenOffline() end @@ -1497,7 +1646,7 @@ function SFrames.ConfigUI:BuildBagPage() --------------------------------------------------------------------------- -- Sell Price Database Section --------------------------------------------------------------------------- - local priceSection = CreateSection(root, "售价数据库", 8, -371, 502, 126, font) + local priceSection = CreateSection(root, "售价数据库", 8, -481, 502, 126, font) local function CountCacheEntries() local n = 0 @@ -1898,10 +2047,10 @@ function SFrames.ConfigUI:BuildActionBarPage() end end - local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 978) + local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 1040) local root = uiScroll.child - local abSection = CreateSection(root, "动作条", 8, -8, 520, 948, font) + local abSection = CreateSection(root, "动作条", 8, -8, 520, 840, font) table.insert(controls, CreateCheckBox(abSection, "启用动作条接管(需 /reload 生效)", 12, -28, @@ -1957,112 +2106,114 @@ function SFrames.ConfigUI:BuildActionBarPage() function() RefreshAB() end )) + table.insert(controls, CreateSlider(abSection, "透明度", 14, -242, 150, 0.1, 1.0, 0.05, + function() return SFramesDB.ActionBars.alpha or 1 end, + function(value) SFramesDB.ActionBars.alpha = value end, + function(v) return string.format("%.0f%%", v * 100) end, + function(value) + if SFrames.ActionBars then + if SFrames.ActionBars.anchor then SFrames.ActionBars.anchor:SetAlpha(value) end + if SFrames.ActionBars.rightHolder then SFrames.ActionBars.rightHolder:SetAlpha(value) end + if SFrames.ActionBars.stanceHolder then SFrames.ActionBars.stanceHolder:SetAlpha(value) end + if SFrames.ActionBars.petHolder then SFrames.ActionBars.petHolder:SetAlpha(value) end + end + end + )) + table.insert(controls, CreateCheckBox(abSection, - "显示快捷键文字", 12, -242, + "显示快捷键文字", 12, -300, function() return SFramesDB.ActionBars.showHotkey ~= false end, function(checked) SFramesDB.ActionBars.showHotkey = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, - "显示宏名称", 200, -242, + "显示宏名称", 200, -300, function() return SFramesDB.ActionBars.showMacroName == true end, function(checked) SFramesDB.ActionBars.showMacroName = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, - "超距红色着色(技能超出射程时按钮变红)", 12, -270, + "超距红色着色(技能超出射程时按钮变红)", 12, -328, function() return SFramesDB.ActionBars.rangeColoring ~= false end, function(checked) SFramesDB.ActionBars.rangeColoring = checked end )) table.insert(controls, CreateCheckBox(abSection, - "显示宠物动作条", 12, -298, + "显示宠物动作条", 12, -356, function() return SFramesDB.ActionBars.showPetBar ~= false end, function(checked) SFramesDB.ActionBars.showPetBar = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, - "显示姿态栏", 200, -298, + "显示姿态栏", 200, -356, function() return SFramesDB.ActionBars.showStanceBar ~= false end, function(checked) SFramesDB.ActionBars.showStanceBar = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, - "显示右侧动作条(两列竖向栏)", 12, -326, + "显示右侧动作条(两列竖向栏)", 12, -384, function() return SFramesDB.ActionBars.showRightBars ~= false end, function(checked) SFramesDB.ActionBars.showRightBars = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, - "始终显示动作条(空格子也显示背景框)", 12, -354, + "始终显示动作条(空格子也显示背景框)", 12, -412, function() return SFramesDB.ActionBars.alwaysShowGrid == true end, function(checked) SFramesDB.ActionBars.alwaysShowGrid = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, - "按钮圆角", 12, -382, + "按钮圆角", 12, -440, function() return SFramesDB.ActionBars.buttonRounded == true end, function(checked) SFramesDB.ActionBars.buttonRounded = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, - "按钮内阴影", 200, -382, + "按钮内阴影", 200, -440, function() return SFramesDB.ActionBars.buttonInnerShadow == true end, function(checked) SFramesDB.ActionBars.buttonInnerShadow = checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, - "显示动作条狮鹫(在底部动作条两侧显示装饰狮鹫)", 12, -410, + "显示动作条狮鹫(在底部动作条两侧显示装饰狮鹫)", 12, -468, function() return SFramesDB.ActionBars.hideGryphon == false end, function(checked) SFramesDB.ActionBars.hideGryphon = not checked end, function() RefreshAB() end )) table.insert(controls, CreateCheckBox(abSection, - "狮鹫置于动作条之上(否则在动作条之下)", 12, -438, + "狮鹫置于动作条之上(否则在动作条之下)", 12, -496, function() return SFramesDB.ActionBars.gryphonOnTop == true end, function(checked) SFramesDB.ActionBars.gryphonOnTop = checked end, function() RefreshAB() end )) - table.insert(controls, CreateSlider(abSection, "狮鹫宽度", 14, -492, 150, 24, 200, 2, + table.insert(controls, CreateSlider(abSection, "狮鹫宽度", 14, -550, 150, 24, 200, 2, function() return SFramesDB.ActionBars.gryphonWidth or 64 end, function(value) SFramesDB.ActionBars.gryphonWidth = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshAB() end )) - table.insert(controls, CreateSlider(abSection, "狮鹫高度", 180, -492, 150, 24, 200, 2, + table.insert(controls, CreateSlider(abSection, "狮鹫高度", 180, -550, 150, 24, 200, 2, function() return SFramesDB.ActionBars.gryphonHeight or 64 end, function(value) SFramesDB.ActionBars.gryphonHeight = value end, function(v) return tostring(math.floor(v + 0.5)) end, function() RefreshAB() end )) - table.insert(controls, CreateSlider(abSection, "水平偏移(向内重叠)", 14, -554, 150, -50, 100, 1, - function() return SFramesDB.ActionBars.gryphonOffsetX or 30 end, - function(value) SFramesDB.ActionBars.gryphonOffsetX = value end, - function(v) return tostring(math.floor(v + 0.5)) end, - function() RefreshAB() end - )) - - table.insert(controls, CreateSlider(abSection, "垂直偏移", 180, -554, 150, -100, 100, 1, - function() return SFramesDB.ActionBars.gryphonOffsetY or 0 end, - function(value) SFramesDB.ActionBars.gryphonOffsetY = value end, - function(v) return tostring(math.floor(v + 0.5)) end, - function() RefreshAB() end - )) + CreateDesc(abSection, "使用布局模式 (/nui layout) 调整狮鹫位置", 14, -612, font, 480) -- 狮鹫样式选择器(带图例预览) - CreateLabel(abSection, "狮鹫样式:", 14, -606, font, 11, 0.85, 0.75, 0.80) + CreateLabel(abSection, "狮鹫样式:", 14, -664, font, 11, 0.85, 0.75, 0.80) local GRYPHON_STYLES_UI = { { key = "dragonflight", label = "巨龙时代", @@ -2077,7 +2228,7 @@ function SFrames.ConfigUI:BuildActionBarPage() local styleBorders = {} local styleStartX = 14 - local styleY = -626 + local styleY = -684 for idx, style in ipairs(GRYPHON_STYLES_UI) do local xOff = styleStartX + (idx - 1) * 125 @@ -2159,47 +2310,293 @@ function SFrames.ConfigUI:BuildActionBarPage() end end - CreateLabel(abSection, "底部动作条位置:", 14, -720, font, 11, 0.85, 0.75, 0.80) - - table.insert(controls, CreateSlider(abSection, "底部 X 偏移", 14, -756, 220, -500, 500, 1, - function() return SFramesDB.ActionBars.bottomOffsetX or 0 end, - function(value) SFramesDB.ActionBars.bottomOffsetX = value end, - function(v) return tostring(math.floor(v + 0.5)) end, - function() RefreshAB() end - )) - - table.insert(controls, CreateSlider(abSection, "底部 Y 偏移", 270, -756, 220, 0, 500, 1, - function() return SFramesDB.ActionBars.bottomOffsetY or 2 end, - function(value) SFramesDB.ActionBars.bottomOffsetY = value end, - function(v) return tostring(math.floor(v + 0.5)) end, - function() RefreshAB() end - )) - - CreateLabel(abSection, "右侧动作条位置:", 14, -812, font, 11, 0.85, 0.75, 0.80) - - table.insert(controls, CreateSlider(abSection, "右侧 X 偏移", 14, -848, 220, -500, 0, 1, - function() return SFramesDB.ActionBars.rightOffsetX or -4 end, - function(value) SFramesDB.ActionBars.rightOffsetX = value end, - function(v) return tostring(math.floor(v + 0.5)) end, - function() RefreshAB() end - )) - - table.insert(controls, CreateSlider(abSection, "右侧 Y 偏移", 270, -848, 220, -500, 500, 1, - function() return SFramesDB.ActionBars.rightOffsetY or -80 end, - function(value) SFramesDB.ActionBars.rightOffsetY = value end, - function(v) return tostring(math.floor(v + 0.5)) end, - function() RefreshAB() end - )) + CreateLabel(abSection, "动作条位置:", 14, -778, font, 11, 0.85, 0.75, 0.80) + CreateDesc(abSection, "使用 /nui layout 或右键聊天框 Nanami 标题进入布局模式调整动作条位置", 14, -798, font, 480) CreateLabel(abSection, "提示:启用/禁用动作条接管需要 /reload 才能生效。", - 14, -904, font, 10, 1, 0.92, 0.38) + 14, -818, font, 10, 1, 0.92, 0.38) uiScroll:UpdateRange() self.actionBarControls = controls self.actionBarScroll = uiScroll end +function SFrames.ConfigUI:BuildKeybindsPage() + local font = SFrames:GetFont() + local page = self.keybindsPage + local controls = {} + + local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 660) + local root = uiScroll.child + + -- Quick keybind mode entry + local bindSection = CreateSection(root, "动作条按键绑定", 8, -8, 520, 80, font) + + CreateButton(bindSection, "进入按键绑定模式", 14, -30, 160, 24, function() + if SFrames.ActionBars and SFrames.ActionBars.EnterKeyBindMode then + if self.frame then self.frame:Hide() end + SFrames.ActionBars:EnterKeyBindMode() + end + end) + + CreateDesc(bindSection, + "悬停动作条/姿态栏/宠物栏按钮,按下键盘键/鼠标键(3-5)/滚轮绑定。右键清除。ESC 退出。", + 184, -32, font, 320) + + -- Profile management + local kbSection = CreateSection(root, "按键绑定方案管理", 8, -98, 520, 520, font) + + CreateDesc(kbSection, + "保存/加载所有按键绑定(包含动作条、移动、聊天、目标选择、菜单等全部设定),可在角色间共享方案。", + 14, -28, font, 490) + + -- Profile list container + local listHolder = CreateFrame("Frame", "SFramesKBMListHolder", kbSection) + listHolder:SetWidth(496) + listHolder:SetHeight(192) + listHolder:SetPoint("TOPLEFT", kbSection, "TOPLEFT", 12, -56) + listHolder:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 10, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + listHolder:SetBackdropColor(0.08, 0.06, 0.1, 0.85) + listHolder:SetBackdropBorderColor(0.4, 0.35, 0.45, 0.8) + + local ROW_HEIGHT = 24 + local MAX_VISIBLE = 8 + local selectedProfile = nil + local profileRows = {} + + local listScroll = CreateFrame("ScrollFrame", "SFramesKBMListScroll", listHolder) + listScroll:SetPoint("TOPLEFT", listHolder, "TOPLEFT", 4, -4) + listScroll:SetPoint("BOTTOMRIGHT", listHolder, "BOTTOMRIGHT", -4, 4) + + local listChild = CreateFrame("Frame", "SFramesKBMListChild", listScroll) + listChild:SetWidth(488) + listChild:SetHeight(ROW_HEIGHT * MAX_VISIBLE) + listScroll:SetScrollChild(listChild) + + listScroll:EnableMouseWheel(true) + listScroll:SetScript("OnMouseWheel", function() + local cur = listScroll:GetVerticalScroll() or 0 + local maxS = listChild:GetHeight() - listScroll:GetHeight() + if maxS < 0 then maxS = 0 end + local newVal = cur - arg1 * ROW_HEIGHT + if newVal < 0 then newVal = 0 end + if newVal > maxS then newVal = maxS end + listScroll:SetVerticalScroll(newVal) + end) + + local emptyLabel = listChild:CreateFontString(nil, "OVERLAY") + emptyLabel:SetFont(font, 11, "OUTLINE") + emptyLabel:SetPoint("CENTER", listChild, "CENTER", 0, 0) + emptyLabel:SetText("暂无保存的方案") + emptyLabel:SetTextColor(0.5, 0.5, 0.5) + + local function RefreshProfileList() + local KBM = SFrames.KeyBindManager + if not KBM then return end + local list = KBM:GetProfileList() + + for _, row in ipairs(profileRows) do + row:Hide() + end + + if table.getn(list) == 0 then + emptyLabel:Show() + listChild:SetHeight(ROW_HEIGHT * MAX_VISIBLE) + return + end + emptyLabel:Hide() + + local totalH = table.getn(list) * ROW_HEIGHT + if totalH < ROW_HEIGHT * MAX_VISIBLE then totalH = ROW_HEIGHT * MAX_VISIBLE end + listChild:SetHeight(totalH) + + for idx, name in ipairs(list) do + local row = profileRows[idx] + if not row then + row = CreateFrame("Button", nil, listChild) + row:SetWidth(480) + row:SetHeight(ROW_HEIGHT) + row:EnableMouse(true) + + row.bg = row:CreateTexture(nil, "BACKGROUND") + row.bg:SetAllPoints() + row.bg:SetTexture("Interface\\Buttons\\WHITE8X8") + row.bg:SetVertexColor(0.15, 0.12, 0.18, 0) + + row.nameText = row:CreateFontString(nil, "OVERLAY") + row.nameText:SetFont(font, 11, "OUTLINE") + row.nameText:SetPoint("LEFT", row, "LEFT", 8, 0) + row.nameText:SetWidth(200) + row.nameText:SetJustifyH("LEFT") + + row.infoText = row:CreateFontString(nil, "OVERLAY") + row.infoText:SetFont(font, 9, "OUTLINE") + row.infoText:SetPoint("RIGHT", row, "RIGHT", -8, 0) + row.infoText:SetWidth(240) + row.infoText:SetJustifyH("RIGHT") + row.infoText:SetTextColor(0.6, 0.6, 0.65) + + row.highlight = row:CreateTexture(nil, "HIGHLIGHT") + row.highlight:SetAllPoints() + row.highlight:SetTexture("Interface\\Buttons\\WHITE8X8") + row.highlight:SetVertexColor(0.4, 0.35, 0.5, 0.2) + + row:SetScript("OnClick", function() + selectedProfile = this.profileName + RefreshProfileList() + end) + + profileRows[idx] = row + end + + row:SetPoint("TOPLEFT", listChild, "TOPLEFT", 0, -(idx - 1) * ROW_HEIGHT) + row.profileName = name + + local info = KBM:GetProfileInfo(name) + row.nameText:SetText(name) + + local infoStr = "" + if info then + infoStr = info.charName .. " | " .. info.count .. " 条绑定" + if info.timestamp and info.timestamp > 0 then + local d = date and date("%m/%d %H:%M", info.timestamp) or "" + if d ~= "" then infoStr = infoStr .. " | " .. d end + end + end + row.infoText:SetText(infoStr) + + if selectedProfile == name then + row.bg:SetVertexColor(0.3, 0.25, 0.45, 0.6) + row.nameText:SetTextColor(1, 0.85, 0.4) + else + row.bg:SetVertexColor(0.15, 0.12, 0.18, (math.mod(idx, 2) == 0) and 0.25 or 0) + row.nameText:SetTextColor(0.9, 0.88, 0.92) + end + + row:Show() + end + end + + -- Buttons row 1: Save / Load / Delete / Rename + local btnY1 = -256 + CreateButton(kbSection, "保存当前绑定", 12, btnY1, 120, 24, function() + local KBM = SFrames.KeyBindManager + if not KBM then return end + KBM.ShowInputDialog("|cffffcc00保存按键绑定方案|r", "", function(name) + KBM:SaveProfile(name) + RefreshProfileList() + end) + end) + + CreateButton(kbSection, "加载方案", 142, btnY1, 100, 24, function() + local KBM = SFrames.KeyBindManager + if not KBM or not selectedProfile then + SFrames:Print("请先在列表中选择一个方案") + return + end + KBM.ShowConfirmDialog( + "确定要加载方案 |cffffd100" .. selectedProfile .. "|r 吗?\n当前所有按键绑定将被替换。", + function() + KBM:LoadProfile(selectedProfile) + RefreshProfileList() + end) + end) + + CreateButton(kbSection, "删除", 252, btnY1, 80, 24, function() + local KBM = SFrames.KeyBindManager + if not KBM or not selectedProfile then + SFrames:Print("请先在列表中选择一个方案") + return + end + KBM.ShowConfirmDialog( + "确定要删除方案 |cffffd100" .. selectedProfile .. "|r 吗?", + function() + KBM:DeleteProfile(selectedProfile) + selectedProfile = nil + RefreshProfileList() + end) + end) + + CreateButton(kbSection, "重命名", 342, btnY1, 90, 24, function() + local KBM = SFrames.KeyBindManager + if not KBM or not selectedProfile then + SFrames:Print("请先在列表中选择一个方案") + return + end + local old = selectedProfile + KBM.ShowInputDialog("|cffffcc00重命名方案|r", old, function(newName) + local ok, err = KBM:RenameProfile(old, newName) + if ok then + selectedProfile = newName + else + SFrames:Print("|cffff4444重命名失败: " .. (err or "") .. "|r") + end + RefreshProfileList() + end) + end) + + -- Buttons row 2: Export / Import + local btnY2 = -288 + CreateButton(kbSection, "导出当前绑定", 12, btnY2, 130, 24, function() + local KBM = SFrames.KeyBindManager + if KBM then KBM:ShowExportDialog() end + end) + + CreateButton(kbSection, "导入绑定", 152, btnY2, 130, 24, function() + local KBM = SFrames.KeyBindManager + if KBM then KBM:ShowImportDialog() end + end) + + CreateButton(kbSection, "导出选中方案", 292, btnY2, 130, 24, function() + local KBM = SFrames.KeyBindManager + if not KBM or not selectedProfile then + SFrames:Print("请先在列表中选择一个方案") + return + end + if not SFramesGlobalDB then SFramesGlobalDB = {} end + if not SFramesGlobalDB.KeyBindProfiles then return end + local profile = SFramesGlobalDB.KeyBindProfiles[selectedProfile] + if profile and profile.bindings then + local text = KBM:SerializeBindings(profile.bindings) + KBM:ShowExportDialog() + local ef = _G["SFramesKBMExport"] + if ef and ef.edit then + ef.edit:SetText(text) + ef.edit:HighlightText() + end + end + end) + + -- Info section + CreateLabel(kbSection, "涵盖范围:", 14, -326, font, 10, 0.85, 0.75, 0.80) + CreateDesc(kbSection, + "移动按键 | 聊天按键 | 动作条快捷键 | 目标选择 | 界面面板 |\n宠物/姿态栏 | 镜头控制 | 多动作条 | 所有系统按键绑定", + 14, -340, font, 490) + + CreateLabel(kbSection, "使用说明:", 14, -380, font, 10, 0.85, 0.75, 0.80) + CreateDesc(kbSection, + "1. 点击「保存当前绑定」将当前所有按键设定存为方案\n2. 在列表中选中方案后点击「加载方案」恢复\n3. 导出会将绑定编码为字符串,可安全复制分享给其他玩家\n4. 方案存储在全局变量中,所有角色共享", + 14, -394, font, 490) + + CreateLabel(kbSection, "命令行:", 14, -452, font, 10, 0.85, 0.75, 0.80) + CreateDesc(kbSection, + "/nui keybinds save <名称> | /nui keybinds load <名称> | /nui keybinds list", + 14, -466, font, 490) + + RefreshProfileList() + self.RefreshKBMList = RefreshProfileList + + uiScroll:UpdateRange() + self.keybindsControls = controls + self.keybindsScroll = uiScroll +end + function SFrames.ConfigUI:BuildMinimapPage() local font = SFrames:GetFont() local page = self.minimapPage @@ -2211,10 +2608,94 @@ function SFrames.ConfigUI:BuildMinimapPage() end end - local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 782) + local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 800) local root = uiScroll.child - local mmSection = CreateSection(root, "小地图", 8, -8, 520, 540, font) + -- ══════════════════════════════════════════════════════════════ + -- 世界地图 & 迷雾揭示 (合并 · 置顶) + -- ══════════════════════════════════════════════════════════════ + local wmSection = CreateSection(root, "世界地图", 8, -8, 520, 260, font) + + table.insert(controls, CreateCheckBox(wmSection, + "启用全新世界地图界面", 14, -34, + function() return SFramesDB.WorldMap.enabled == true end, + function(checked) + SFramesDB.WorldMap.enabled = checked + if checked then SFramesDB.Tweaks.worldMapWindow = false end + end + )) + CreateDesc(wmSection, "Nanami主题窗口化地图,隐藏原生装饰(需重载UI)", 36, -50, font) + + table.insert(controls, CreateCheckBox(wmSection, + "启用导航地图", 270, -34, + function() + return SFramesDB.WorldMap.nav and SFramesDB.WorldMap.nav.enabled == true + end, + function(checked) + if type(SFramesDB.WorldMap.nav) ~= "table" then + SFramesDB.WorldMap.nav = { enabled = false, width = 350, alpha = 0.70, locked = false } + end + SFramesDB.WorldMap.nav.enabled = checked + if SFrames.WorldMap and SFrames.WorldMap.ToggleNav then + if (checked and not (NanamiNavMap and NanamiNavMap:IsVisible())) + or (not checked and NanamiNavMap and NanamiNavMap:IsVisible()) then + SFrames.WorldMap:ToggleNav() + end + end + end + )) + CreateDesc(wmSection, "实时导航地图,/nui nav | 滚轮缩放 | Ctrl+滚轮透明度", 292, -50, font) + + -- 迷雾揭示 (合并在同一 section) + CreateLabel(wmSection, "── 迷雾揭示 ──", 14, -76, font, 11, 0.70, 0.60, 0.65) + + table.insert(controls, CreateCheckBox(wmSection, + "启用地图迷雾揭示", 14, -96, + function() return SFramesDB.MapReveal.enabled ~= false end, + function(checked) + SFramesDB.MapReveal.enabled = checked + if SFrames.MapReveal and SFrames.MapReveal.Refresh then + SFrames.MapReveal:Refresh() + end + end + )) + CreateDesc(wmSection, "在世界地图上显示未探索区域(变暗显示),需要 LibMapOverlayData", 36, -112, font) + + table.insert(controls, CreateSlider(wmSection, "未探索区域亮度", 14, -140, 220, 0.2, 1.0, 0.05, + function() return SFramesDB.MapReveal.unexploredAlpha or 0.7 end, + function(value) SFramesDB.MapReveal.unexploredAlpha = value end, + function(v) return string.format("%.0f%%", v * 100) end, + function(value) + if SFrames.MapReveal and SFrames.MapReveal.SetAlpha then + SFrames.MapReveal:SetAlpha(value) + end + end + )) + + CreateButton(wmSection, "扫描所有地图", 270, -148, 120, 24, function() + if SFrames.MapReveal and SFrames.MapReveal.ScanAllMaps then + SFrames.MapReveal:ScanAllMaps() + else + SFrames:Print("MapReveal 模块不可用") + end + end) + CreateDesc(wmSection, "遍历所有大陆区域,发现新地图并自动补充迷雾数据", 292, -174, font) + + CreateButton(wmSection, "导出扫描数据", 400, -148, 100, 24, function() + if SFrames.MapReveal and SFrames.MapReveal.ExportScannedData then + SFrames.MapReveal:ExportScannedData() + else + SFrames:Print("MapReveal 模块不可用") + end + end) + + CreateLabel(wmSection, "版本更新后建议先扫描地图,再重新打开世界地图查看效果。", 14, -200, font, 10, 0.6, 0.6, 0.65) + CreateDesc(wmSection, "命令: /nui mapscan | /nui mapexport | /nui mapreveal 切换", 14, -216, font) + + -- ══════════════════════════════════════════════════════════════ + -- 小地图 (移至下方) + -- ══════════════════════════════════════════════════════════════ + local mmSection = CreateSection(root, "小地图", 8, -278, 520, 480, font) table.insert(controls, CreateCheckBox(mmSection, "启用 Nanami 小地图皮肤", 14, -36, @@ -2244,42 +2725,52 @@ function SFrames.ConfigUI:BuildMinimapPage() )) CreateDesc(mmSection, "在小地图圆内底部显示当前坐标", 292, -150, font) - local function ApplyPos() - if SFrames.Minimap and SFrames.Minimap.ApplyPosition then - SFrames.Minimap.ApplyPosition() - end + CreateDesc(mmSection, "使用 /nui layout 或右键聊天框 Nanami 标题进入布局模式调整位置", 14, -182, font, 480) + + -- ── Shape selector (方形 / 圆形) ────────────────────────────── + CreateLabel(mmSection, "地图形状:", 14, -210, font, 11, 0.85, 0.75, 0.80) + + local shapeKeys = {"square1", "square2", "circle"} + local shapeNames = {"方形·金", "方形·暗", "圆形"} + local shapeBtns = {} + local shapeLbls = {} + + for i = 1, 3 do + local btn = CreateFrame("Button", nil, mmSection) + btn:SetWidth(72) + btn:SetHeight(22) + btn:SetPoint("TOPLEFT", mmSection, "TOPLEFT", 100 + (i - 1) * 80, -208) + btn:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 8, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + local lbl = btn:CreateFontString(nil, "OVERLAY") + lbl:SetFont(font, 10, "OUTLINE") + lbl:SetPoint("CENTER", btn, "CENTER", 0, 0) + lbl:SetText(shapeNames[i]) + btn._sfShapeKey = shapeKeys[i] + shapeBtns[i] = btn + shapeLbls[i] = lbl end - table.insert(controls, CreateSlider(mmSection, "水平位置 (X)", 14, -182, 220, -800, 0, 1, - function() return SFramesDB.Minimap.posX or -5 end, - function(value) SFramesDB.Minimap.posX = value end, - function(v) return string.format("%.0f", v) end, - function() ApplyPos() end - )) + CreateLabel(mmSection, "提示: 缩放、位置和样式修改后实时生效。", 14, -238, font, 10, 0.6, 0.6, 0.65) - table.insert(controls, CreateSlider(mmSection, "垂直位置 (Y)", 270, -182, 220, -800, 0, 1, - function() return SFramesDB.Minimap.posY or -5 end, - function(value) SFramesDB.Minimap.posY = value end, - function(v) return string.format("%.0f", v) end, - function() ApplyPos() end - )) + -- ── Circular style container (only visible when shape == "circle") ── + local circleStyleFrame = CreateFrame("Frame", nil, mmSection) + circleStyleFrame:SetPoint("TOPLEFT", mmSection, "TOPLEFT", 0, -256) + circleStyleFrame:SetWidth(520) + circleStyleFrame:SetHeight(220) - CreateButton(mmSection, "重置位置", 14, -226, 130, 22, function() - SFramesDB.Minimap.posX = -5 - SFramesDB.Minimap.posY = -5 - ApplyPos() - end) - - -- Map frame style selector with texture previews (5 columns x 2 rows) - CreateLabel(mmSection, "边框样式:", 14, -256, font, 11, 0.85, 0.75, 0.80) + CreateLabel(circleStyleFrame, "圆形边框样式:", 14, -4, font, 11, 0.85, 0.75, 0.80) local styles = SFrames.Minimap and SFrames.Minimap.MAP_STYLES or {} - -- "Auto (match class)" button - local autoBtn = CreateFrame("Button", nil, mmSection) + local autoBtn = CreateFrame("Button", nil, circleStyleFrame) autoBtn:SetWidth(100) autoBtn:SetHeight(18) - autoBtn:SetPoint("TOPLEFT", mmSection, "TOPLEFT", 130, -256) + autoBtn:SetPoint("TOPLEFT", circleStyleFrame, "TOPLEFT", 130, -2) autoBtn:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", @@ -2311,7 +2802,7 @@ function SFrames.ConfigUI:BuildMinimapPage() local gapX = 10 local gapY = 18 local styleStartX = 14 - local styleY = -280 + local styleY = -26 local function HighlightSelection() local current = SFramesDB.Minimap.mapStyle or "auto" @@ -2333,10 +2824,10 @@ function SFrames.ConfigUI:BuildMinimapPage() local xOff = styleStartX + col * (cellW + gapX) local yOff = styleY - row * (cellH + gapY) - local preview = CreateFrame("Frame", nil, mmSection) + local preview = CreateFrame("Frame", nil, circleStyleFrame) preview:SetWidth(cellW) preview:SetHeight(cellH) - preview:SetPoint("TOPLEFT", mmSection, "TOPLEFT", xOff, yOff) + preview:SetPoint("TOPLEFT", circleStyleFrame, "TOPLEFT", xOff, yOff) local bg = preview:CreateTexture(nil, "BACKGROUND") bg:SetAllPoints() @@ -2391,70 +2882,36 @@ function SFrames.ConfigUI:BuildMinimapPage() HighlightSelection() - local totalRows = math.ceil(table.getn(styles) / cols) - local tipY = styleY - totalRows * (cellH + gapY) - 10 - CreateLabel(mmSection, "提示: 缩放、位置和样式修改后实时生效。", 14, tipY, font, 10, 0.6, 0.6, 0.65) - - -- ── 世界地图 ────────────────────────────────────────────────── - local wmSection = CreateSection(root, "世界地图", 8, -558, 520, 130, font) - - table.insert(controls, CreateCheckBox(wmSection, - "启用全新世界地图界面", 14, -34, - function() return SFramesDB.WorldMap.enabled == true end, - function(checked) - SFramesDB.WorldMap.enabled = checked - if checked then SFramesDB.Tweaks.worldMapWindow = false end + -- Shape UI update: highlight active shape button, toggle circular style visibility + local function UpdateShapeUI() + local current = SFramesDB.Minimap.mapShape or "square1" + for i = 1, 3 do + if shapeKeys[i] == current then + shapeBtns[i]:SetBackdropColor(0.2, 0.4, 0.2, 0.9) + shapeBtns[i]:SetBackdropBorderColor(0.4, 0.8, 0.3, 1) + shapeLbls[i]:SetTextColor(0.5, 1, 0.5) + else + shapeBtns[i]:SetBackdropColor(0.15, 0.15, 0.15, 0.7) + shapeBtns[i]:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.6) + shapeLbls[i]:SetTextColor(0.6, 0.6, 0.6) + end + end + if current == "circle" then + circleStyleFrame:Show() + else + circleStyleFrame:Hide() end - )) - CreateDesc(wmSection, "Nanami主题窗口化地图,隐藏原生装饰(需重载UI)", 36, -50, font) - - table.insert(controls, CreateCheckBox(wmSection, - "启用导航地图", 14, -68, - function() - return SFramesDB.WorldMap.nav and SFramesDB.WorldMap.nav.enabled == true - end, - function(checked) - if type(SFramesDB.WorldMap.nav) ~= "table" then -SFramesDB.WorldMap.nav = { enabled = false, width = 350, alpha = 0.70, locked = false } end - SFramesDB.WorldMap.nav.enabled = checked - if SFrames.WorldMap and SFrames.WorldMap.ToggleNav then - if (checked and not (NanamiNavMap and NanamiNavMap:IsVisible())) - or (not checked and NanamiNavMap and NanamiNavMap:IsVisible()) then - SFrames.WorldMap:ToggleNav() - end - end - end - )) - CreateDesc(wmSection, "以玩家为中心的实时导航地图,半透明覆盖,可拖动", 36, -84, font) - CreateDesc(wmSection, "命令: /nui nav | 滚轮缩放 | Ctrl+滚轮透明度", 36, -96, font) - -- ── 地图迷雾揭示 ─────────────────────────────────────────── - local mapRevealSection = CreateSection(root, "世界地图迷雾揭示", 8, -698, 520, 112, font) + for i = 1, 3 do + shapeBtns[i]:SetScript("OnClick", function() + SFramesDB.Minimap.mapShape = this._sfShapeKey + UpdateShapeUI() + RefreshMinimap() + end) + end - table.insert(controls, CreateCheckBox(mapRevealSection, - "启用地图迷雾揭示", 14, -34, - function() return SFramesDB.MapReveal.enabled ~= false end, - function(checked) - SFramesDB.MapReveal.enabled = checked - if SFrames.MapReveal and SFrames.MapReveal.Refresh then - SFrames.MapReveal:Refresh() - end - end - )) - CreateDesc(mapRevealSection, "在世界地图上显示未探索区域(变暗显示)", 36, -50, font) - CreateDesc(mapRevealSection, "需要 LibMapOverlayData 库支持", 36, -62, font) - - table.insert(controls, CreateSlider(mapRevealSection, "未探索区域亮度", 270, -64, 200, 0.2, 1.0, 0.05, - function() return SFramesDB.MapReveal.unexploredAlpha or 0.7 end, - function(value) SFramesDB.MapReveal.unexploredAlpha = value end, - function(v) return string.format("%.0f%%", v * 100) end, - function(value) - if SFrames.MapReveal and SFrames.MapReveal.SetAlpha then - SFrames.MapReveal:SetAlpha(value) - end - end - )) + UpdateShapeUI() uiScroll:UpdateRange() self.minimapControls = controls @@ -2478,7 +2935,7 @@ function SFrames.ConfigUI:BuildBuffPage() local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 520) local root = uiScroll.child - local buffSection = CreateSection(root, "Buff / Debuff 栏", 8, -8, 520, 490, font) + local buffSection = CreateSection(root, "Buff / Debuff 栏", 8, -8, 520, 420, font) -- Row 1: enable + show timer table.insert(controls, CreateCheckBox(buffSection, @@ -2562,32 +3019,22 @@ function SFrames.ConfigUI:BuildBuffPage() posStartX = posStartX + 42 end - -- Row 6: X/Y offset sliders - table.insert(controls, CreateSlider(buffSection, "水平偏移 (X)", 14, -286, 220, -800, 800, 1, - function() return SFramesDB.MinimapBuffs.offsetX or 0 end, - function(value) SFramesDB.MinimapBuffs.offsetX = value end, - function(v) return string.format("%.0f", v) end, - function() RefreshBuffs() end - )) - - table.insert(controls, CreateSlider(buffSection, "垂直偏移 (Y)", 270, -286, 220, -800, 800, 1, - function() return SFramesDB.MinimapBuffs.offsetY or 0 end, - function(value) SFramesDB.MinimapBuffs.offsetY = value end, - function(v) return string.format("%.0f", v) end, - function() RefreshBuffs() end - )) + -- Row 6: position hint + CreateDesc(buffSection, "使用 /nui layout 进入布局模式调整 Buff 栏位置", 14, -286, font, 480) -- Row 7: action buttons - CreateButton(buffSection, "重置默认位置", 14, -340, 120, 24, function() + CreateButton(buffSection, "重置默认位置", 14, -310, 120, 24, function() SFramesDB.MinimapBuffs.offsetX = 0 SFramesDB.MinimapBuffs.offsetY = 0 SFramesDB.MinimapBuffs.position = "TOPRIGHT" SFramesDB.MinimapBuffs.growDirection = "LEFT" + if SFramesDB.Positions then SFramesDB.Positions["MinimapBuffs"] = nil end + if SFramesDB.Positions then SFramesDB.Positions["MinimapDebuffs"] = nil end RefreshBuffs() end) local simBtn - simBtn = CreateButton(buffSection, "模拟预览", 142, -340, 100, 24, function() + simBtn = CreateButton(buffSection, "模拟预览", 142, -310, 100, 24, function() if not SFrames.MinimapBuffs then return end if SFrames.MinimapBuffs._simulating then SFrames.MinimapBuffs:StopSimulation() @@ -2599,10 +3046,10 @@ function SFrames.ConfigUI:BuildBuffPage() end) -- Row 8: tips - CreateLabel(buffSection, "模拟预览:显示假 Buff / Debuff 以预览布局效果,不影响实际状态。", 14, -374, font, 9, 0.65, 0.58, 0.62) - CreateLabel(buffSection, "Debuff 边框颜色: |cff3399ff魔法|r |cff9900ff诅咒|r |cff996600疾病|r |cff009900毒药|r |cffcc0000物理|r", 14, -390, font, 9, 0.65, 0.58, 0.62) + CreateLabel(buffSection, "模拟预览:显示假 Buff / Debuff 以预览布局效果,不影响实际状态。", 14, -344, font, 9, 0.65, 0.58, 0.62) + CreateLabel(buffSection, "Debuff 边框颜色: |cff3399ff魔法|r |cff9900ff诅咒|r |cff996600疾病|r |cff009900毒药|r |cffcc0000物理|r", 14, -360, font, 9, 0.65, 0.58, 0.62) - CreateLabel(buffSection, "提示:启用/禁用 Buff 栏需要 /reload 才能生效。其他调整实时生效。", 14, -414, font, 10, 0.6, 0.6, 0.65) + CreateLabel(buffSection, "提示:启用/禁用 Buff 栏需要 /reload 才能生效。其他调整实时生效。", 14, -384, font, 10, 0.6, 0.6, 0.65) uiScroll:UpdateRange() self.buffControls = controls @@ -2620,12 +3067,13 @@ function SFrames.ConfigUI:ShowPage(mode) self.partyPage:Hide() self.charPage:Hide() self.actionBarPage:Hide() + self.keybindsPage:Hide() self.minimapPage:Hide() self.buffPage:Hide() self.personalizePage:Hide() self.themePage:Hide() - local allTabs = { self.uiTab, self.playerTab, self.targetTab, self.partyTab, self.raidTab, self.bagTab, self.charTab, self.actionBarTab, self.minimapTab, self.buffTab, self.personalizeTab, self.themeTab } + local allTabs = { self.uiTab, self.playerTab, self.targetTab, self.partyTab, self.raidTab, self.bagTab, self.charTab, self.actionBarTab, self.keybindsTab, self.minimapTab, self.buffTab, self.personalizeTab, self.themeTab } for _, tab in ipairs(allTabs) do tab.sfSoftActive = false tab:Enable() @@ -2688,6 +3136,15 @@ function SFrames.ConfigUI:ShowPage(mode) self.title:SetText("Nanami-UI 设置 - 动作条") self:RefreshControls(self.actionBarControls) if self.actionBarScroll and self.actionBarScroll.Reset then self.actionBarScroll:Reset() end + elseif mode == "keybinds" then + self.keybindsPage:Show() + self.keybindsTab.sfSoftActive = true + self.keybindsTab:Disable() + self.keybindsTab:RefreshVisual() + self.title:SetText("Nanami-UI 设置 - 按键绑定方案") + self:RefreshControls(self.keybindsControls) + if self.keybindsScroll and self.keybindsScroll.Reset then self.keybindsScroll:Reset() end + if self.RefreshKBMList then self.RefreshKBMList() end elseif mode == "minimap" then self.minimapPage:Show() self.minimapTab.sfSoftActive = true @@ -2882,10 +3339,19 @@ function SFrames.ConfigUI:EnsureFrame() StyleButton(tabActionBar) AddBtnIcon(tabActionBar, "attack", nil, "left") + local tabKeybinds = CreateFrame("Button", "SFramesConfigTabKeybinds", panel, "UIPanelButtonTemplate") + tabKeybinds:SetWidth(100) + tabKeybinds:SetHeight(28) + tabKeybinds:SetPoint("TOP", tabActionBar, "BOTTOM", 0, -4) + tabKeybinds:SetText("按键方案") + tabKeybinds:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("keybinds") end) + StyleButton(tabKeybinds) + AddBtnIcon(tabKeybinds, "key", nil, "left") + local tabMinimap = CreateFrame("Button", "SFramesConfigTabMinimap", panel, "UIPanelButtonTemplate") tabMinimap:SetWidth(100) tabMinimap:SetHeight(28) - tabMinimap:SetPoint("TOP", tabActionBar, "BOTTOM", 0, -4) + tabMinimap:SetPoint("TOP", tabKeybinds, "BOTTOM", 0, -4) tabMinimap:SetText("地图设置") tabMinimap:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("minimap") end) StyleButton(tabMinimap) @@ -2947,6 +3413,9 @@ function SFrames.ConfigUI:EnsureFrame() local actionBarPage = CreateFrame("Frame", "SFramesConfigActionBarPage", content) actionBarPage:SetAllPoints(content) + local keybindsPage = CreateFrame("Frame", "SFramesConfigKeybindsPage", content) + keybindsPage:SetAllPoints(content) + local minimapPage = CreateFrame("Frame", "SFramesConfigMinimapPage", content) minimapPage:SetAllPoints(content) @@ -2969,6 +3438,7 @@ function SFrames.ConfigUI:EnsureFrame() self.partyTab = tabParty self.charTab = tabChar self.actionBarTab = tabActionBar + self.keybindsTab = tabKeybinds self.minimapTab = tabMinimap self.buffTab = tabBuff self.personalizeTab = tabPersonalize @@ -2982,6 +3452,7 @@ function SFrames.ConfigUI:EnsureFrame() self.partyPage = partyPage self.charPage = charPage self.actionBarPage = actionBarPage + self.keybindsPage = keybindsPage self.minimapPage = minimapPage self.buffPage = buffPage self.personalizePage = personalizePage @@ -3053,7 +3524,7 @@ function SFrames.ConfigUI:BuildPersonalizePage() CreateDesc(trainerSection, "点击按钮预览升级提醒效果", 392, -55, font, 100) -- ── 鼠标提示框 ──────────────────────────────────────────────── - local tooltipSection = CreateSection(root, "鼠标提示框", 8, -118, 520, 120, font) + local tooltipSection = CreateSection(root, "鼠标提示框", 8, -118, 520, 160, font) CreateDesc(tooltipSection, "选择游戏提示框的显示位置方式(三选一)", 14, -28, font) local function RefreshTooltipMode() @@ -3091,8 +3562,20 @@ function SFrames.ConfigUI:BuildPersonalizePage() table.insert(controls, cbCustom) CreateDesc(tooltipSection, "启用后屏幕上会出现绿色锚点,拖动到目标位置后锁定", 292, -62, font) + table.insert(controls, CreateSlider(tooltipSection, + "提示框缩放", 14, -100, 200, 0.5, 2.0, 0.05, + function() return SFramesDB.tooltipScale or 1.0 end, + function(value) + SFramesDB.tooltipScale = value + if SFrames.FloatingTooltip and SFrames.FloatingTooltip.ApplyScale then + SFrames.FloatingTooltip:ApplyScale() + end + end, + function(v) return string.format("%.0f%%", v * 100) end + )) + -- ── AFK 待机动画 ────────────────────────────────────────────── - local afkSection = CreateSection(root, "AFK 待机动画", 8, -248, 520, 146, font) + local afkSection = CreateSection(root, "AFK 待机动画", 8, -288, 520, 146, font) table.insert(controls, CreateCheckBox(afkSection, "启用 AFK 待机画面", 14, -34, @@ -3505,6 +3988,7 @@ function SFrames.ConfigUI:EnsurePage(mode) elseif mode == "raid" then self:BuildRaidPage() elseif mode == "char" then self:BuildCharPage() elseif mode == "actionbar" then self:BuildActionBarPage() + elseif mode == "keybinds" then self:BuildKeybindsPage() elseif mode == "minimap" then self:BuildMinimapPage() elseif mode == "buff" then self:BuildBuffPage() elseif mode == "personalize" then self:BuildPersonalizePage() @@ -3517,7 +4001,7 @@ function SFrames.ConfigUI:Build(mode) self:EnsureFrame() local page = string.lower(mode or "ui") - local validPages = { ui = true, player = true, target = true, bags = true, char = true, party = true, raid = true, actionbar = true, minimap = true, buff = true, personalize = true, theme = true } + local validPages = { ui = true, player = true, target = true, bags = true, char = true, party = true, raid = true, actionbar = true, keybinds = true, minimap = true, buff = true, personalize = true, theme = true } if not validPages[page] then page = "ui" end if self.frame:IsShown() and self.activePage == page then diff --git a/Core.lua b/Core.lua index ca3752f..cd11162 100644 --- a/Core.lua +++ b/Core.lua @@ -2,6 +2,44 @@ SFrames = {} DEFAULT_CHAT_FRAME:AddMessage("SF: Loading Core.lua...") +do + local _orig_wipe = wipe + if _orig_wipe then + wipe = function(t) + if t == nil then return end + return _orig_wipe(t) + end + end + local _orig_tinsert = tinsert + if _orig_tinsert then + tinsert = function(t, a2, a3) + if type(t) ~= "table" then return end + if a3 ~= nil then + return _orig_tinsert(t, a2, a3) + else + return _orig_tinsert(t, a2) + end + end + end +end + +do + local _orig_UIFrameFade = UIFrameFade + if _orig_UIFrameFade then + UIFrameFade = function(frame, fadeInfo) + if not frame or not fadeInfo then return end + return _orig_UIFrameFade(frame, fadeInfo) + end + end + + local origOnUpdate = UIParent and UIParent.GetScript and UIParent:GetScript("OnUpdate") + if origOnUpdate then + UIParent:SetScript("OnUpdate", function() + pcall(origOnUpdate) + end) + end +end + BINDING_HEADER_NANAMI_UI = "Nanami-UI" BINDING_NAME_NANAMI_TOGGLE_NAV = "切换导航地图" @@ -118,14 +156,22 @@ function SFrames:DoFullInitialize() SFrames.Tooltip = CreateFrame("GameTooltip", "SFramesScanTooltip", nil, "GameTooltipTemplate") SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") + SFrames.Tooltip:SetAlpha(0) + SFrames.Tooltip:Hide() -- Phase 1: Critical modules (unit frames, action bars) — must load immediately if SFramesDB.enableUnitFrames ~= false then - if SFrames.Player and SFrames.Player.Initialize then SFrames.Player:Initialize() end - if SFrames.Pet and SFrames.Pet.Initialize then SFrames.Pet:Initialize() end - if SFrames.Target and SFrames.Target.Initialize then SFrames.Target:Initialize() end - if SFrames.ToT and SFrames.ToT.Initialize then SFrames.ToT:Initialize() end - if SFrames.Party and SFrames.Party.Initialize then SFrames.Party:Initialize() end + if SFramesDB.enablePlayerFrame ~= false then + if SFrames.Player and SFrames.Player.Initialize then SFrames.Player:Initialize() end + if SFrames.Pet and SFrames.Pet.Initialize then SFrames.Pet:Initialize() end + end + if SFramesDB.enableTargetFrame ~= false then + if SFrames.Target and SFrames.Target.Initialize then SFrames.Target:Initialize() end + if SFrames.ToT and SFrames.ToT.Initialize then SFrames.ToT:Initialize() end + end + if SFramesDB.enablePartyFrame ~= false then + if SFrames.Party and SFrames.Party.Initialize then SFrames.Party:Initialize() end + end end if SFrames.FloatingTooltip and SFrames.FloatingTooltip.Initialize then SFrames.FloatingTooltip:Initialize() end @@ -150,6 +196,7 @@ function SFrames:DoFullInitialize() { "MapIcons", function() if SFrames.MapIcons and SFrames.MapIcons.Initialize then SFrames.MapIcons:Initialize() end end }, { "Tweaks", function() if SFrames.Tweaks and SFrames.Tweaks.Initialize then SFrames.Tweaks:Initialize() end end }, { "AFKScreen", function() if SFrames.AFKScreen and SFrames.AFKScreen.Initialize then SFrames.AFKScreen:Initialize() end end }, + { "LootDisplay", function() if SFrames.LootDisplay and SFrames.LootDisplay.Initialize then SFrames.LootDisplay:Initialize() end end }, } local idx = 1 @@ -344,7 +391,11 @@ function SFrames:InitSlashCommands() SFrames:Print("/nui afk - toggle AFK screen") SFrames:Print("/nui pin - 地图标记 (clear/share)") SFrames:Print("/nui nav - 切换导航地图") + SFrames:Print("/nui mapscan - 扫描所有地图更新迷雾揭示数据") + SFrames:Print("/nui mapexport - 导出扫描到的地图数据") + SFrames:Print("/nui layout - 布局模式(拖拽调整所有框体位置)") SFrames:Print("/nui bind - 按键绑定模式(悬停按钮+按键)") + SFrames:Print("/nui talentdb - 天赋默认数据库管理/导出") elseif cmd == "ui" or cmd == "uiconfig" then if SFrames.ConfigUI and SFrames.ConfigUI.Build then SFrames.ConfigUI:Build("ui") end elseif cmd == "chat" or cmd == "chatconfig" then @@ -390,12 +441,78 @@ function SFrames:InitSlashCommands() else SFrames:Print("WorldMap module unavailable.") end + elseif cmd == "mapscan" or cmd == "scanmap" then + if SFrames.MapReveal and SFrames.MapReveal.ScanAllMaps then + SFrames.MapReveal:ScanAllMaps() + else + SFrames:Print("MapReveal module unavailable.") + end + elseif cmd == "mapexport" then + if SFrames.MapReveal and SFrames.MapReveal.ExportScannedData then + SFrames.MapReveal:ExportScannedData() + else + SFrames:Print("MapReveal module unavailable.") + end + elseif cmd == "layout" or cmd == "movers" then + if SFrames.Movers and SFrames.Movers.ToggleLayoutMode then + SFrames.Movers:ToggleLayoutMode() + else + SFrames:Print("Layout mode unavailable.") + end elseif cmd == "bind" or cmd == "keybind" then if SFrames.ActionBars and SFrames.ActionBars.ToggleKeyBindMode then SFrames.ActionBars:ToggleKeyBindMode() else SFrames:Print("ActionBars module unavailable.") end + elseif cmd == "keybinds" or cmd == "kb" then + local KBM = SFrames.KeyBindManager + if not KBM then + SFrames:Print("KeyBindManager module unavailable.") + elseif args == "" then + if SFrames.ConfigUI and SFrames.ConfigUI.Build then + SFrames.ConfigUI:Build("keybinds") + end + elseif string.find(args, "^save ") then + local name = string.gsub(args, "^save ", "") + if name ~= "" then KBM:SaveProfile(name) end + elseif string.find(args, "^load ") then + local name = string.gsub(args, "^load ", "") + if name ~= "" then KBM:LoadProfile(name) end + elseif string.find(args, "^delete ") then + local name = string.gsub(args, "^delete ", "") + if name ~= "" then KBM:DeleteProfile(name) end + elseif args == "list" then + local list = KBM:GetProfileList() + if table.getn(list) == 0 then + SFrames:Print("没有保存的按键绑定方案") + else + SFrames:Print("按键绑定方案列表:") + for _, name in ipairs(list) do + local info = KBM:GetProfileInfo(name) + local desc = info and (info.charName .. ", " .. info.count .. " 条") or "" + SFrames:Print(" |cffffd100" .. name .. "|r " .. desc) + end + end + elseif args == "export" then + KBM:ShowExportDialog() + elseif args == "import" then + KBM:ShowImportDialog() + else + SFrames:Print("/nui keybinds - 打开设置面板") + SFrames:Print("/nui keybinds save <名称> - 保存当前绑定为方案") + SFrames:Print("/nui keybinds load <名称> - 加载方案") + SFrames:Print("/nui keybinds delete <名称> - 删除方案") + SFrames:Print("/nui keybinds list - 列出所有方案") + SFrames:Print("/nui keybinds export - 导出当前绑定") + SFrames:Print("/nui keybinds import - 导入绑定") + end + elseif cmd == "talentdb" or cmd == "tdb" then + if SFrames.TalentTree and SFrames.TalentTree.HandleTalentDBCommand then + SFrames.TalentTree:HandleTalentDBCommand(args) + else + SFrames:Print("TalentTree module unavailable.") + end elseif cmd == "debugbuffs" or cmd == "db" then local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9" DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r 当前所有 Buff:") @@ -437,7 +554,7 @@ function SFrames:InitSlashCommands() if SFrames.ConfigUI and SFrames.ConfigUI.Build then SFrames.ConfigUI:Build("ui") end else local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9" - DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r Commands: /nui, /nui ui, /nui bags, /nui chat, /nui unlock, /nui lock, /nui test, /nui partyh, /nui partyv, /nui focushelp, /nui mapreveal, /nui stats, /nui afk, /nui pin, /nui bind") + DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r Commands: /nui, /nui ui, /nui bags, /nui chat, /nui layout, /nui unlock, /nui lock, /nui test, /nui partyh, /nui partyv, /nui focushelp, /nui mapreveal, /nui mapscan, /nui stats, /nui afk, /nui pin, /nui bind, /nui keybinds") end end end @@ -480,41 +597,44 @@ function SFrames:HideBlizzardFrames() if SFramesDB and SFramesDB.enableUnitFrames == false then -- Keep Blizzard unit frames when Nanami frames are disabled else - -- Hide Player Frame - if PlayerFrame then - PlayerFrame:UnregisterAllEvents() - PlayerFrame:Hide() - PlayerFrame.Show = function() end + if not SFramesDB or SFramesDB.enablePlayerFrame ~= false then + if PlayerFrame then + PlayerFrame:UnregisterAllEvents() + PlayerFrame:Hide() + PlayerFrame.Show = function() end + end + if PetFrame then + PetFrame:UnregisterAllEvents() + PetFrame:Hide() + PetFrame.Show = function() end + end end - -- Hide Pet Frame - if PetFrame then - PetFrame:UnregisterAllEvents() - PetFrame:Hide() - PetFrame.Show = function() end + if not SFramesDB or SFramesDB.enableTargetFrame ~= false then + if TargetFrame then + TargetFrame:UnregisterAllEvents() + TargetFrame:Hide() + TargetFrame.Show = function() end + end + if ComboFrame then + ComboFrame:UnregisterAllEvents() + ComboFrame:Hide() + ComboFrame.Show = function() end + ComboFrame.fadeInfo = ComboFrame.fadeInfo or {} + if ComboFrame_Update then + ComboFrame_Update = function() end + end + end end - -- Hide Target Frame - if TargetFrame then - TargetFrame:UnregisterAllEvents() - TargetFrame:Hide() - TargetFrame.Show = function() end - end - - -- Hide Combo Frame - if ComboFrame then - ComboFrame:UnregisterAllEvents() - ComboFrame:Hide() - ComboFrame.Show = function() end - end - - -- Hide Party Frames - for i = 1, 4 do - local pf = _G["PartyMemberFrame"..i] - if pf then - pf:UnregisterAllEvents() - pf:Hide() - pf.Show = function() end + if not SFramesDB or SFramesDB.enablePartyFrame ~= false then + for i = 1, 4 do + local pf = _G["PartyMemberFrame"..i] + if pf then + pf:UnregisterAllEvents() + pf:Hide() + pf.Show = function() end + end end end end @@ -523,12 +643,12 @@ function SFrames:HideBlizzardFrames() if (not SFramesDB) or (SFramesDB.enableRaidFrames ~= false) then local function NeuterBlizzardRaidUI() - -- Default Classic UI (1.12) if RaidFrame then RaidFrame:UnregisterAllEvents() + RaidFrame:SetScript("OnEvent", nil) + RaidFrame:SetScript("OnUpdate", nil) end - -- Prevent Raid groups from updating and showing for i = 1, NUM_RAID_GROUPS or 8 do local rgf = _G["RaidGroupButton"..i] if rgf then @@ -536,11 +656,18 @@ function SFrames:HideBlizzardFrames() end end - -- Override pullout generation - RaidPullout_Update = function() end - RaidPullout_OnEvent = function() end + RaidPullout_Update = function() return {} end + RaidPullout_OnEvent = function() return {} end + RaidGroupFrame_OnEvent = function() return {} end + RaidGroupFrame_Update = RaidGroupFrame_Update or function() end + + if not RAID_SUBGROUP_LISTS then RAID_SUBGROUP_LISTS = {} end + for i = 1, NUM_RAID_GROUPS or 8 do + if not RAID_SUBGROUP_LISTS[i] then + RAID_SUBGROUP_LISTS[i] = {} + end + end - -- Hide individual pullout frames that might already exist for i = 1, 40 do local pf = _G["RaidPullout"..i] if pf then @@ -549,13 +676,7 @@ function SFrames:HideBlizzardFrames() pf.Show = function() end end end - - -- Hide standard GroupFrames - if RaidGroupFrame_OnEvent then - RaidGroupFrame_OnEvent = function() end - end - -- Hide newer/backported Compact Raid Frames if they exist if CompactRaidFrameManager then CompactRaidFrameManager:UnregisterAllEvents() CompactRaidFrameManager:Hide() @@ -570,7 +691,6 @@ function SFrames:HideBlizzardFrames() NeuterBlizzardRaidUI() - -- Hook ADDON_LOADED to catch Blizzard_RaidUI loaded on demand local raidHook = CreateFrame("Frame") raidHook:RegisterEvent("ADDON_LOADED") raidHook:SetScript("OnEvent", function() diff --git a/FlightMap.lua b/FlightMap.lua index efb9b28..c6bb792 100644 --- a/FlightMap.lua +++ b/FlightMap.lua @@ -647,6 +647,11 @@ function FM:UpdateDestinations() table.sort(reachable, function(a, b) return a.cost < b.cost end) + if table.getn(reachable) == 0 then + CloseTaxiMap() + return + end + local npcName = UnitName("NPC") or "飞行管理员" FM.titleFS:SetText(npcName .. " - 飞行路线") diff --git a/GameMenu.lua b/GameMenu.lua index ae18581..ceb49db 100644 --- a/GameMenu.lua +++ b/GameMenu.lua @@ -68,8 +68,6 @@ end -------------------------------------------------------------------------------- local MENU_BUTTON_ICONS = { ["GameMenuButtonContinue"] = "exit", - ["GameMenuButtonOptions"] = "settings", - ["GameMenuButtonSoundOptions"] = "sound", ["GameMenuButtonUIOptions"] = "talent", ["GameMenuButtonKeybindings"] = "menu", ["GameMenuButtonRatings"] = "backpack", @@ -219,8 +217,6 @@ end local BUTTON_ORDER = { "GameMenuButtonContinue", "__NANAMI_SETTINGS__", - "GameMenuButtonOptions", - "GameMenuButtonSoundOptions", "GameMenuButtonUIOptions", "GameMenuButtonKeybindings", "GameMenuButtonRatings", @@ -229,6 +225,11 @@ local BUTTON_ORDER = { "GameMenuButtonQuit", } +local HIDDEN_BUTTONS = { + "GameMenuButtonOptions", + "GameMenuButtonSoundOptions", +} + -------------------------------------------------------------------------------- -- Frame Styling (called once at PLAYER_LOGIN, before first show) -------------------------------------------------------------------------------- @@ -268,6 +269,18 @@ local function StyleGameMenuFrame() -- Create settings button local sBt = CreateSettingsButton(GameMenuFrame) + -- Hide removed buttons (Video / Sound) + for _, name in ipairs(HIDDEN_BUTTONS) do + local btn = _G[name] + if btn then + btn:Hide() + btn:SetWidth(0.001) + btn:SetHeight(0.001) + btn:ClearAllPoints() + btn:SetPoint("TOPLEFT", GameMenuFrame, "TOPLEFT", 0, 0) + end + end + -- Build a lookup of known names for quick check local knownSet = {} for _, name in ipairs(BUTTON_ORDER) do @@ -275,6 +288,9 @@ local function StyleGameMenuFrame() knownSet[name] = true end end + for _, name in ipairs(HIDDEN_BUTTONS) do + knownSet[name] = true + end -- Collect all child buttons that are NOT the settings button local children = { GameMenuFrame:GetChildren() } diff --git a/KeyBindManager.lua b/KeyBindManager.lua new file mode 100644 index 0000000..133bf23 --- /dev/null +++ b/KeyBindManager.lua @@ -0,0 +1,633 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: KeyBind Manager +-- Profile-based keybinding management (save/load/delete/rename/export/import) +-- Covers ALL keybindings, not just action bars. +-------------------------------------------------------------------------------- + +SFrames.KeyBindManager = {} + +local KBM = SFrames.KeyBindManager + +-------------------------------------------------------------------------------- +-- Data helpers +-------------------------------------------------------------------------------- + +local function EnsureDB() + if not SFramesGlobalDB then SFramesGlobalDB = {} end + if not SFramesGlobalDB.KeyBindProfiles then SFramesGlobalDB.KeyBindProfiles = {} end +end + +local function GetCharName() + return UnitName("player") or "Unknown" +end + +local function GetTimestamp() + return time and time() or 0 +end + +-------------------------------------------------------------------------------- +-- Collect / Apply bindings +-------------------------------------------------------------------------------- + +function KBM:CollectAllBindings() + local result = {} + local n = GetNumBindings() + for i = 1, n do + local command, key1, key2 = GetBinding(i) + if command and (key1 or key2) then + table.insert(result, { + command = command, + key1 = key1, + key2 = key2, + }) + end + end + return result +end + +function KBM:ClearAllBindings() + local n = GetNumBindings() + for i = 1, n do + local command, key1, key2 = GetBinding(i) + if key2 then SetBinding(key2, nil) end + if key1 then SetBinding(key1, nil) end + end +end + +function KBM:ApplyBindings(data) + if not data or type(data) ~= "table" then return false end + + self:ClearAllBindings() + + local applied = 0 + for _, entry in ipairs(data) do + if entry.command then + if entry.key1 then + SetBinding(entry.key1, entry.command) + applied = applied + 1 + end + if entry.key2 then + SetBinding(entry.key2, entry.command) + applied = applied + 1 + end + end + end + + SaveBindings(2) + + if SFrames.ActionBars and SFrames.ActionBars.RefreshAllHotkeys then + SFrames.ActionBars:RefreshAllHotkeys() + end + + return true, applied +end + +-------------------------------------------------------------------------------- +-- Base64 encode / decode (pure Lua 5.0 compatible) +-------------------------------------------------------------------------------- + +local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + +local function Base64Encode(src) + if not src or src == "" then return "" end + local out = {} + local len = string.len(src) + local i = 1 + while i <= len do + local a = string.byte(src, i) + local b = (i + 1 <= len) and string.byte(src, i + 1) or 0 + local c = (i + 2 <= len) and string.byte(src, i + 2) or 0 + local remaining = len - i + 1 + + local n = a * 65536 + b * 256 + c + + local c1 = math.floor(n / 262144) + local c2 = math.floor(math.mod(n, 262144) / 4096) + local c3 = math.floor(math.mod(n, 4096) / 64) + local c4 = math.mod(n, 64) + + table.insert(out, string.sub(B64, c1 + 1, c1 + 1)) + table.insert(out, string.sub(B64, c2 + 1, c2 + 1)) + if remaining >= 2 then + table.insert(out, string.sub(B64, c3 + 1, c3 + 1)) + else + table.insert(out, "=") + end + if remaining >= 3 then + table.insert(out, string.sub(B64, c4 + 1, c4 + 1)) + else + table.insert(out, "=") + end + + i = i + 3 + end + return table.concat(out) +end + +local B64_INV = {} +for i = 1, 64 do + B64_INV[string.sub(B64, i, i)] = i - 1 +end + +local function Base64Decode(src) + if not src or src == "" then return "" end + src = string.gsub(src, "%s+", "") + local out = {} + local len = string.len(src) + local i = 1 + while i <= len do + local v1 = B64_INV[string.sub(src, i, i)] or 0 + local v2 = B64_INV[string.sub(src, i + 1, i + 1)] or 0 + local v3 = B64_INV[string.sub(src, i + 2, i + 2)] + local v4 = B64_INV[string.sub(src, i + 3, i + 3)] + + local n = v1 * 262144 + v2 * 4096 + (v3 or 0) * 64 + (v4 or 0) + + table.insert(out, string.char(math.floor(n / 65536))) + if v3 then + table.insert(out, string.char(math.floor(math.mod(n, 65536) / 256))) + end + if v4 then + table.insert(out, string.char(math.mod(n, 256))) + end + + i = i + 4 + end + return table.concat(out) +end + +-------------------------------------------------------------------------------- +-- Serialization (export/import text format, Base64 encoded) +-------------------------------------------------------------------------------- + +local EXPORT_PREFIX = "!NKB1!" +local SEP = "\t" + +local function EncodeRaw(data) + local lines = {} + for _, entry in ipairs(data) do + local line = entry.command + if entry.key1 then + line = line .. SEP .. entry.key1 + else + line = line .. SEP + end + if entry.key2 then + line = line .. SEP .. entry.key2 + end + table.insert(lines, line) + end + return table.concat(lines, "\n") +end + +local function DecodeRaw(raw) + local data = {} + for line in string.gfind(raw .. "\n", "(.-)\n") do + line = string.gsub(line, "\r", "") + if line ~= "" and string.sub(line, 1, 1) ~= "#" then + local parts = {} + for part in string.gfind(line .. SEP, "(.-)" .. SEP) do + table.insert(parts, part) + end + local command = parts[1] + if command and command ~= "" then + local key1 = parts[2] + local key2 = parts[3] + if key1 == "" then key1 = nil end + if key2 == "" then key2 = nil end + if key1 or key2 then + table.insert(data, { + command = command, + key1 = key1, + key2 = key2, + }) + end + end + end + end + return data +end + +function KBM:SerializeBindings(data) + if not data then data = self:CollectAllBindings() end + local raw = EncodeRaw(data) + return EXPORT_PREFIX .. Base64Encode(raw) +end + +function KBM:DeserializeBindings(text) + if not text or text == "" then return nil, "文本为空" end + text = string.gsub(text, "^%s+", "") + text = string.gsub(text, "%s+$", "") + + if string.len(text) == 0 then return nil, "文本为空" end + + local raw + if string.sub(text, 1, string.len(EXPORT_PREFIX)) == EXPORT_PREFIX then + local encoded = string.sub(text, string.len(EXPORT_PREFIX) + 1) + raw = Base64Decode(encoded) + if not raw or raw == "" then return nil, "解码失败" end + else + raw = text + end + + local data = DecodeRaw(raw) + if table.getn(data) == 0 then return nil, "未找到有效的绑定数据" end + return data +end + +-------------------------------------------------------------------------------- +-- Profile CRUD +-------------------------------------------------------------------------------- + +function KBM:SaveProfile(name) + if not name or name == "" then return false, "方案名不能为空" end + EnsureDB() + local data = self:CollectAllBindings() + SFramesGlobalDB.KeyBindProfiles[name] = { + timestamp = GetTimestamp(), + charName = GetCharName(), + bindings = data, + } + SFrames:Print("按键绑定方案已保存: |cffffd100" .. name .. "|r (" .. table.getn(data) .. " 条)") + return true +end + +function KBM:LoadProfile(name) + if not name or name == "" then return false, "方案名不能为空" end + EnsureDB() + local profile = SFramesGlobalDB.KeyBindProfiles[name] + if not profile then return false, "方案不存在: " .. name end + + local ok, count = self:ApplyBindings(profile.bindings) + if ok then + SFrames:Print("按键绑定方案已加载: |cffffd100" .. name .. "|r (" .. count .. " 个按键)") + end + return ok +end + +function KBM:DeleteProfile(name) + if not name or name == "" then return false end + EnsureDB() + if not SFramesGlobalDB.KeyBindProfiles[name] then return false end + SFramesGlobalDB.KeyBindProfiles[name] = nil + SFrames:Print("按键绑定方案已删除: |cffffd100" .. name .. "|r") + return true +end + +function KBM:RenameProfile(oldName, newName) + if not oldName or oldName == "" or not newName or newName == "" then + return false, "名称不能为空" + end + EnsureDB() + local profiles = SFramesGlobalDB.KeyBindProfiles + if not profiles[oldName] then return false, "方案不存在" end + if profiles[newName] then return false, "目标名称已存在" end + + profiles[newName] = profiles[oldName] + profiles[oldName] = nil + SFrames:Print("按键绑定方案已重命名: |cffffd100" .. oldName .. "|r -> |cffffd100" .. newName .. "|r") + return true +end + +function KBM:GetProfileList() + EnsureDB() + local list = {} + for name, _ in pairs(SFramesGlobalDB.KeyBindProfiles) do + table.insert(list, name) + end + table.sort(list) + return list +end + +function KBM:GetProfileInfo(name) + EnsureDB() + local p = SFramesGlobalDB.KeyBindProfiles[name] + if not p then return nil end + return { + name = name, + charName = p.charName or "?", + timestamp = p.timestamp or 0, + count = p.bindings and table.getn(p.bindings) or 0, + } +end + +-------------------------------------------------------------------------------- +-- Export / Import Dialog UI +-------------------------------------------------------------------------------- + +local exportFrame, importFrame + +local function CreateDialogFrame(name, titleText, width, height) + local f = CreateFrame("Frame", name, UIParent) + f:SetWidth(width) + f:SetHeight(height) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 80) + f:SetFrameStrata("TOOLTIP") + f:SetFrameLevel(200) + f:SetToplevel(true) + f:EnableMouse(true) + f:SetMovable(true) + f:SetClampedToScreen(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + if SFrames and SFrames.CreateBackdrop then + SFrames:CreateBackdrop(f) + else + f:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + f:SetBackdropColor(0.08, 0.07, 0.1, 0.96) + f:SetBackdropBorderColor(0.4, 0.4, 0.4, 0.9) + end + + local font = (SFrames and SFrames.GetFont) and SFrames:GetFont() or "Fonts\\ARIALN.TTF" + + local title = f:CreateFontString(nil, "OVERLAY") + title:SetFont(font, 13, "OUTLINE") + title:SetPoint("TOP", f, "TOP", 0, -10) + title:SetText(titleText) + local _T = SFrames.ActiveTheme + if _T and _T.title then + title:SetTextColor(_T.title[1], _T.title[2], _T.title[3]) + else + title:SetTextColor(1, 0.82, 0) + end + f.title = title + + local close = CreateFrame("Button", nil, f, "UIPanelCloseButton") + close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -2, -2) + close:SetWidth(20) + close:SetHeight(20) + + return f, font +end + +local function CreateThemedButton(parent, text, x, y, width, height, onClick) + local name = "SFramesKBM_Btn_" .. string.gsub(text, "%s", "") .. "_" .. tostring(math.random(10000, 99999)) + local btn = CreateFrame("Button", name, parent, "UIPanelButtonTemplate") + btn:SetWidth(width) + btn:SetHeight(height) + btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y) + btn:SetText(text) + btn:SetScript("OnClick", onClick) + + local font = (SFrames and SFrames.GetFont) and SFrames:GetFont() or "Fonts\\ARIALN.TTF" + local _T = SFrames.ActiveTheme + if not _T then return btn end + + local function HideBtnTex(tex) + if not tex then return end + if tex.SetTexture then tex:SetTexture(nil) end + if tex.SetAlpha then tex:SetAlpha(0) end + if tex.Hide then tex:Hide() end + end + HideBtnTex(btn:GetNormalTexture()) + HideBtnTex(btn:GetPushedTexture()) + HideBtnTex(btn:GetHighlightTexture()) + HideBtnTex(btn:GetDisabledTexture()) + for _, sfx in ipairs({"Left","Right","Middle"}) do + local t = _G[name .. sfx] + if t then t:SetAlpha(0); t:Hide() end + end + btn: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 }, + }) + btn:SetBackdropColor(_T.btnBg[1], _T.btnBg[2], _T.btnBg[3], _T.btnBg[4]) + btn:SetBackdropBorderColor(_T.btnBorder[1], _T.btnBorder[2], _T.btnBorder[3], _T.btnBorder[4]) + local fs = btn:GetFontString() + if fs then + fs:SetFont(font, 11, "OUTLINE") + fs:SetTextColor(_T.btnText[1], _T.btnText[2], _T.btnText[3]) + end + 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]) + local t = this:GetFontString() + if t then t:SetTextColor(_T.btnActiveText[1], _T.btnActiveText[2], _T.btnActiveText[3]) 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]) + local t = this:GetFontString() + if t then t:SetTextColor(_T.btnText[1], _T.btnText[2], _T.btnText[3]) end + 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 btn +end + +function KBM:ShowExportDialog() + local text = self:SerializeBindings() + + if not exportFrame then + local f, font = CreateDialogFrame("SFramesKBMExport", "|cffffcc00导出按键绑定|r", 480, 340) + + local desc = f:CreateFontString(nil, "OVERLAY") + desc:SetFont(font, 10, "OUTLINE") + desc:SetPoint("TOP", f.title, "BOTTOM", 0, -4) + desc:SetText("已编码为字符串,Ctrl+A 全选,Ctrl+C 复制") + desc:SetTextColor(0.7, 0.7, 0.7) + + local sf = CreateFrame("ScrollFrame", "SFramesKBMExportScroll", f, "UIPanelScrollFrameTemplate") + sf:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -48) + sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -32, 42) + + local edit = CreateFrame("EditBox", "SFramesKBMExportEdit", sf) + edit:SetWidth(420) + edit:SetHeight(200) + edit:SetMultiLine(true) + edit:SetFont(font, 10, "OUTLINE") + edit:SetAutoFocus(false) + edit:SetScript("OnEscapePressed", function() f:Hide() end) + sf:SetScrollChild(edit) + f.edit = edit + + CreateThemedButton(f, "关闭", 190, -306, 100, 26, function() + f:Hide() + end) + + exportFrame = f + end + + exportFrame.edit:SetText(text) + exportFrame:Show() + exportFrame.edit:HighlightText() + exportFrame.edit:SetFocus() +end + +function KBM:ShowImportDialog() + if not importFrame then + local f, font = CreateDialogFrame("SFramesKBMImport", "|cffffcc00导入按键绑定|r", 480, 370) + + local desc = f:CreateFontString(nil, "OVERLAY") + desc:SetFont(font, 10, "OUTLINE") + desc:SetPoint("TOP", f.title, "BOTTOM", 0, -4) + desc:SetText("将编码后的字符串粘贴到下方 (Ctrl+V),然后点击导入") + desc:SetTextColor(0.7, 0.7, 0.7) + + local sf = CreateFrame("ScrollFrame", "SFramesKBMImportScroll", f, "UIPanelScrollFrameTemplate") + sf:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -48) + sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -32, 72) + + local edit = CreateFrame("EditBox", "SFramesKBMImportEdit", sf) + edit:SetWidth(420) + edit:SetHeight(200) + edit:SetMultiLine(true) + edit:SetFont(font, 10, "OUTLINE") + edit:SetAutoFocus(false) + edit:SetScript("OnEscapePressed", function() f:Hide() end) + sf:SetScrollChild(edit) + f.edit = edit + + local statusLabel = f:CreateFontString(nil, "OVERLAY") + statusLabel:SetFont(font, 10, "OUTLINE") + statusLabel:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 14, 46) + statusLabel:SetWidth(300) + statusLabel:SetJustifyH("LEFT") + statusLabel:SetText("") + f.statusLabel = statusLabel + + CreateThemedButton(f, "导入并应用", 120, -336, 120, 26, function() + local inputText = f.edit:GetText() + if not inputText or inputText == "" then + f.statusLabel:SetText("|cffff4444请先粘贴绑定数据|r") + return + end + local data, err = KBM:DeserializeBindings(inputText) + if not data then + f.statusLabel:SetText("|cffff4444错误: " .. (err or "未知") .. "|r") + return + end + local ok, count = KBM:ApplyBindings(data) + if ok then + f.statusLabel:SetText("|cff44ff44成功导入 " .. count .. " 个按键绑定|r") + SFrames:Print("已从文本导入 " .. count .. " 个按键绑定") + else + f.statusLabel:SetText("|cffff4444导入失败|r") + end + end) + + CreateThemedButton(f, "取消", 250, -336, 100, 26, function() + f:Hide() + end) + + importFrame = f + end + + importFrame.edit:SetText("") + importFrame.statusLabel:SetText("") + importFrame:Show() + importFrame.edit:SetFocus() +end + +-------------------------------------------------------------------------------- +-- Input name dialog (for save / rename) +-------------------------------------------------------------------------------- + +local inputDialog + +local function ShowInputDialog(titleText, defaultText, callback) + if not inputDialog then + local f, font = CreateDialogFrame("SFramesKBMInput", "", 360, 120) + + local edit = CreateFrame("EditBox", "SFramesKBMInputEdit", f) + edit:SetWidth(320) + edit:SetHeight(24) + edit:SetPoint("TOP", f, "TOP", 0, -40) + edit:SetFont(font, 12, "OUTLINE") + edit:SetAutoFocus(true) + edit:SetMaxLetters(50) + edit:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 10, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + edit:SetBackdropColor(0.1, 0.1, 0.1, 0.9) + edit:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.8) + edit:SetTextInsets(6, 6, 0, 0) + f.edit = edit + + CreateThemedButton(f, "确定", 90, -78, 80, 24, function() + local val = f.edit:GetText() + if val and val ~= "" and f.callback then + f.callback(val) + end + f:Hide() + end) + + CreateThemedButton(f, "取消", 185, -78, 80, 24, function() + f:Hide() + end) + + edit:SetScript("OnEscapePressed", function() f:Hide() end) + edit:SetScript("OnEnterPressed", function() + local val = f.edit:GetText() + if val and val ~= "" and f.callback then + f.callback(val) + end + f:Hide() + end) + + inputDialog = f + end + + inputDialog.title:SetText(titleText) + inputDialog.edit:SetText(defaultText or "") + inputDialog.callback = callback + inputDialog:Show() + inputDialog.edit:SetFocus() + inputDialog.edit:HighlightText() +end + +-------------------------------------------------------------------------------- +-- Confirm dialog +-------------------------------------------------------------------------------- + +local confirmDialog + +local function ShowConfirmDialog(message, callback) + if not confirmDialog then + local f, font = CreateDialogFrame("SFramesKBMConfirm", "", 360, 110) + + local msg = f:CreateFontString(nil, "OVERLAY") + msg:SetFont(font, 11, "OUTLINE") + msg:SetPoint("TOP", f, "TOP", 0, -34) + msg:SetWidth(320) + msg:SetJustifyH("CENTER") + msg:SetTextColor(0.9, 0.9, 0.9) + f.msg = msg + + CreateThemedButton(f, "确定", 90, -70, 80, 24, function() + if f.callback then f.callback() end + f:Hide() + end) + + CreateThemedButton(f, "取消", 185, -70, 80, 24, function() + f:Hide() + end) + + confirmDialog = f + end + + confirmDialog.title:SetText("|cffffcc00确认|r") + confirmDialog.msg:SetText(message) + confirmDialog.callback = callback + confirmDialog:Show() +end + +KBM.ShowInputDialog = ShowInputDialog +KBM.ShowConfirmDialog = ShowConfirmDialog diff --git a/LootDisplay.lua b/LootDisplay.lua new file mode 100644 index 0000000..6454bc0 --- /dev/null +++ b/LootDisplay.lua @@ -0,0 +1,816 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Loot Display (拾取界面 + 已拾取提示) +-------------------------------------------------------------------------------- + +SFrames = SFrames or {} +SFrames.LootDisplay = SFrames.LootDisplay or {} + +local LD = SFrames.LootDisplay + +-------------------------------------------------------------------------------- +-- Constants +-------------------------------------------------------------------------------- +local ROW_HEIGHT = 32 +local ROW_WIDTH = 180 +local ROW_GAP = 2 +local ICON_SIZE = 26 +local TITLE_HEIGHT = 22 + +local ALERT_WIDTH = 240 +local ALERT_HEIGHT = 32 +local ALERT_GAP = 3 +local ALERT_ICON = 24 +local ALERT_FADE_DUR = 0.6 +local ALERT_FLOAT = 22 +local ALERT_HOLD = 3.5 +local ALERT_STAGGER = 0.25 +local MAX_ALERTS = 10 + +local QUALITY_COLORS = { + [0] = { 0.62, 0.62, 0.62 }, + [1] = { 0.92, 0.92, 0.88 }, + [2] = { 0.12, 1.00, 0.00 }, + [3] = { 0.00, 0.44, 0.87 }, + [4] = { 0.64, 0.21, 0.93 }, + [5] = { 1.00, 0.50, 0.00 }, + [6] = { 0.90, 0.80, 0.50 }, +} + +-- Shared rounded backdrop template (matches rest of Nanami-UI) +local ROUND_BACKDROP = { + 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 ROUND_BACKDROP_SMALL = { + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, +} + +local ROUND_BACKDROP_SHADOW = { + 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 }, +} + +local ICON_BACKDROP = { + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 10, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, +} + +local lootRows = {} +local activeAlerts = {} +local alertAnchor = nil +local alertPool = {} + +-------------------------------------------------------------------------------- +-- Helpers +-------------------------------------------------------------------------------- +local function T() + return SFrames.ActiveTheme or {} +end + +local function Font() + return (SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" +end + +local function GetDB() + if not SFramesDB then SFramesDB = {} end + if type(SFramesDB.lootDisplay) ~= "table" then + SFramesDB.lootDisplay = { + enable = true, + alertEnable = true, + alertFadeDelay = ALERT_HOLD, + scale = 1.0, + } + end + return SFramesDB.lootDisplay +end + +local function QColor(quality) + local q = tonumber(quality) or 1 + local c = QUALITY_COLORS[q] + if c then return c[1], c[2], c[3] end + return 0.92, 0.92, 0.88 +end + +local function ColorHex(r, g, b) + return string.format("%02x%02x%02x", r * 255, g * 255, b * 255) +end + +local function GetItemTexture(itemIdOrLink) + if not itemIdOrLink then return nil end + local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 = GetItemInfo(itemIdOrLink) + if a10 and type(a10) == "string" then return a10 end + if a9 and type(a9) == "string" and string.find(a9, "Interface") then return a9 end + if a8 and type(a8) == "string" and string.find(a8, "Interface") then return a8 end + return nil +end + +local function ParseItemLink(link) + if not link then return nil end + local _, _, colorHex, itemId, itemName = string.find(link, "|c(%x+)|Hitem:(%d+).-|h%[(.-)%]|h|r") + if not itemName then return nil end + local id = tonumber(itemId) or 0 + local _, _, quality = GetItemInfo(id) + return itemName, quality or 1, id +end + +-------------------------------------------------------------------------------- +-- ▸ LOOT FRAME (拾取窗口) +-------------------------------------------------------------------------------- +local lootFrame = nil + +local function CreateLootFrame() + if lootFrame then return lootFrame end + + local th = T() + local bg = th.panelBg or { 0.06, 0.05, 0.09, 0.95 } + local bd = th.panelBorder or { 0.30, 0.25, 0.42, 0.90 } + local acc = th.accent or { 1, 0.5, 0.8, 0.98 } + + lootFrame = CreateFrame("Frame", "NanamiLootFrame", UIParent) + lootFrame:SetFrameStrata("FULLSCREEN_DIALOG") + lootFrame:SetFrameLevel(50) + lootFrame:SetWidth(ROW_WIDTH + 14) + lootFrame:SetHeight(TITLE_HEIGHT + 3 * (ROW_HEIGHT + ROW_GAP) + 10) + lootFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 50, -200) + lootFrame:SetClampedToScreen(true) + lootFrame:SetMovable(true) + lootFrame:EnableMouse(false) + + -- Shadow + local shadow = CreateFrame("Frame", nil, lootFrame) + shadow:SetPoint("TOPLEFT", lootFrame, "TOPLEFT", -5, 5) + shadow:SetPoint("BOTTOMRIGHT", lootFrame, "BOTTOMRIGHT", 5, -5) + shadow:SetFrameLevel(math.max(lootFrame:GetFrameLevel() - 1, 0)) + shadow:SetBackdrop(ROUND_BACKDROP_SHADOW) + shadow:SetBackdropColor(0, 0, 0, 0.55) + shadow:SetBackdropBorderColor(0, 0, 0, 0.40) + shadow:EnableMouse(false) + + -- Background blocker: prevents clicks passing through to frames behind the loot window + local blocker = CreateFrame("Frame", nil, lootFrame) + blocker:SetAllPoints(lootFrame) + blocker:SetFrameLevel(lootFrame:GetFrameLevel()) + blocker:EnableMouse(true) + + -- Main panel (rounded) + lootFrame:SetBackdrop(ROUND_BACKDROP) + lootFrame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 0.95) + lootFrame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 0.90) + + -- Top accent line + local acLine = lootFrame:CreateTexture(nil, "ARTWORK") + acLine:SetTexture("Interface\\Buttons\\WHITE8x8") + acLine:SetHeight(2) + acLine:SetPoint("TOPLEFT", lootFrame, "TOPLEFT", 5, -5) + acLine:SetPoint("TOPRIGHT", lootFrame, "TOPRIGHT", -5, -5) + acLine:SetVertexColor(acc[1], acc[2], acc[3], 0.65) + + -- Title bar: handles dragging the entire loot frame + local titleBar = CreateFrame("Frame", nil, lootFrame) + titleBar:SetHeight(TITLE_HEIGHT) + titleBar:SetPoint("TOPLEFT", lootFrame, "TOPLEFT", 0, 0) + titleBar:SetPoint("TOPRIGHT", lootFrame, "TOPRIGHT", 0, 0) + titleBar:SetFrameLevel(lootFrame:GetFrameLevel() + 1) + titleBar:EnableMouse(true) + titleBar:RegisterForDrag("LeftButton") + titleBar:SetScript("OnDragStart", function() lootFrame:StartMoving() end) + titleBar:SetScript("OnDragStop", function() lootFrame:StopMovingOrSizing() end) + + -- Title text + local titleFS = titleBar:CreateFontString(nil, "OVERLAY") + titleFS:SetFont(Font(), 11, "OUTLINE") + titleFS:SetPoint("TOPLEFT", lootFrame, "TOPLEFT", 10, -8) + local tc = th.title or { 1, 0.88, 1 } + titleFS:SetTextColor(tc[1], tc[2], tc[3], 0.95) + titleFS:SetText("拾取") + lootFrame._title = titleFS + + -- Close button + local closeBtn = CreateFrame("Button", nil, lootFrame) + closeBtn:SetWidth(18) + closeBtn:SetHeight(18) + closeBtn:SetPoint("TOPRIGHT", lootFrame, "TOPRIGHT", -5, -5) + closeBtn:SetFrameLevel(lootFrame:GetFrameLevel() + 5) + closeBtn:RegisterForClicks("LeftButtonUp") + closeBtn:SetBackdrop(ROUND_BACKDROP_SMALL) + local cbg = th.buttonBg or { 0.35, 0.08, 0.12, 0.85 } + local cbd = th.buttonBorder or { 0.50, 0.18, 0.22, 0.80 } + closeBtn:SetBackdropColor(cbg[1], cbg[2], cbg[3], cbg[4] or 0.85) + closeBtn:SetBackdropBorderColor(cbd[1], cbd[2], cbd[3], cbd[4] or 0.80) + local closeFS = closeBtn:CreateFontString(nil, "OVERLAY") + closeFS:SetFont(Font(), 10, "OUTLINE") + closeFS:SetPoint("CENTER", 0, 0) + closeFS:SetText("×") + closeFS:SetTextColor(0.9, 0.65, 0.65, 1) + closeBtn:SetScript("OnClick", function() CloseLoot() end) + closeBtn:SetScript("OnEnter", function() + this:SetBackdropColor(0.55, 0.12, 0.15, 0.95) + this:SetBackdropBorderColor(0.9, 0.30, 0.35, 1) + closeFS:SetTextColor(1, 1, 1, 1) + end) + closeBtn:SetScript("OnLeave", function() + this:SetBackdropColor(cbg[1], cbg[2], cbg[3], cbg[4] or 0.85) + this:SetBackdropBorderColor(cbd[1], cbd[2], cbd[3], cbd[4] or 0.80) + closeFS:SetTextColor(0.9, 0.65, 0.65, 1) + end) + + lootFrame:Hide() + return lootFrame +end + +-------------------------------------------------------------------------------- +-- Loot row +-------------------------------------------------------------------------------- +local function CreateLootRow(parent, index) + local th = T() + local slotBg = th.slotBg or { 0.07, 0.06, 0.10, 0.85 } + local slotBd = th.slotBorder or { 0.18, 0.16, 0.28, 0.60 } + local hoverBd = th.slotHover or { 0.38, 0.40, 0.90 } + local acc = th.accent or { 1, 0.5, 0.8 } + + local row = CreateFrame("Button", "NanamiLootRow" .. index, parent) + row:SetWidth(ROW_WIDTH) + row:SetHeight(ROW_HEIGHT) + row:SetFrameLevel(parent:GetFrameLevel() + 2) + row:RegisterForClicks("LeftButtonUp") + + row:SetBackdrop(ROUND_BACKDROP_SMALL) + row:SetBackdropColor(slotBg[1], slotBg[2], slotBg[3], slotBg[4] or 0.85) + row:SetBackdropBorderColor(slotBd[1], slotBd[2], slotBd[3], slotBd[4] or 0.60) + + -- Left quality accent bar + local qBar = row:CreateTexture(nil, "OVERLAY") + qBar:SetTexture("Interface\\Buttons\\WHITE8X8") + qBar:SetWidth(2) + qBar:SetPoint("TOPLEFT", row, "TOPLEFT", 3, -3) + qBar:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 3, 3) + qBar:SetVertexColor(1, 1, 1, 0.6) + row.qBar = qBar + + -- Icon frame (rounded border) + local iconFrame = CreateFrame("Frame", nil, row) + iconFrame:SetWidth(ICON_SIZE + 4) + iconFrame:SetHeight(ICON_SIZE + 4) + iconFrame:SetPoint("LEFT", row, "LEFT", 7, 0) + iconFrame:SetFrameLevel(row:GetFrameLevel() + 1) + iconFrame:SetBackdrop(ICON_BACKDROP) + iconFrame:SetBackdropColor(0, 0, 0, 0.60) + iconFrame:SetBackdropBorderColor(slotBd[1], slotBd[2], slotBd[3], 0.70) + iconFrame:EnableMouse(false) + row.iconFrame = iconFrame + + local icon = iconFrame:CreateTexture(nil, "ARTWORK") + icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 2, -2) + icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -2, 2) + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + row.icon = icon + + -- Name + local nameFS = row:CreateFontString(nil, "OVERLAY") + nameFS:SetFont(Font(), 10, "OUTLINE") + nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 4, 0) + nameFS:SetPoint("RIGHT", row, "RIGHT", -6, 0) + nameFS:SetJustifyH("LEFT") + row.nameFS = nameFS + + -- Count (bottom-right of icon) + local countFS = row:CreateFontString(nil, "OVERLAY") + countFS:SetFont(Font(), 8, "OUTLINE") + countFS:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -2, 2) + countFS:SetJustifyH("RIGHT") + countFS:SetTextColor(1, 1, 1, 0.95) + row.countFS = countFS + + row._slotBg = slotBg + row._slotBd = slotBd + row._hoverBd = hoverBd + row._acc = acc + row:EnableMouse(false) + + row:Hide() + return row +end + +-------------------------------------------------------------------------------- +-- Update loot frame +-------------------------------------------------------------------------------- +local function UpdateLootFrame() + local db = GetDB() + if not db.enable then return end + + local numItems = GetNumLootItems() + if not numItems or numItems == 0 then + if lootFrame then lootFrame:Hide() end + return + end + + CreateLootFrame() + + local validSlots = {} + for i = 1, numItems do + local texture, itemName, quantity, quality = GetLootSlotInfo(i) + if texture then + table.insert(validSlots, i) + end + end + + local numValid = table.getn(validSlots) + if numValid == 0 then + if lootFrame then lootFrame:Hide() end + return + end + + local totalH = TITLE_HEIGHT + (numValid * (ROW_HEIGHT + ROW_GAP)) + 10 + lootFrame:SetWidth(ROW_WIDTH + 14) + lootFrame:SetHeight(totalH) + lootFrame:SetScale(db.scale or 1.0) + + while table.getn(lootRows) < numValid do + local idx = table.getn(lootRows) + 1 + lootRows[idx] = CreateLootRow(lootFrame, idx) + end + + for i = 1, table.getn(lootRows) do lootRows[i]:Hide() end + + for displayIdx = 1, numValid do + local slotIdx = validSlots[displayIdx] + local row = lootRows[displayIdx] + if not row then break end + + row:ClearAllPoints() + row:SetPoint("TOPLEFT", lootFrame, "TOPLEFT", 7, -(TITLE_HEIGHT + 2 + (displayIdx - 1) * (ROW_HEIGHT + ROW_GAP))) + row:SetWidth(ROW_WIDTH) + + local texture, itemName, quantity, quality = GetLootSlotInfo(slotIdx) + row.slotIndex = slotIdx + + row.icon:SetTexture(texture or "Interface\\Icons\\INV_Misc_QuestionMark") + + local r, g, b = QColor(quality) + row._qualColor = { r, g, b } + row.qBar:SetVertexColor(r, g, b, 0.90) + row.iconFrame:SetBackdropBorderColor(r, g, b, 0.65) + row:SetBackdropBorderColor(r, g, b, 0.30) + + if itemName then + row.nameFS:SetText("|cff" .. ColorHex(r, g, b) .. itemName .. "|r") + else + row.nameFS:SetText("") + end + + if quantity and quantity > 1 then + row.countFS:SetText(tostring(quantity)) + else + row.countFS:SetText("") + end + + row:Show() + + -- Overlay the Blizzard LootButton on top for click handling + local maxBtns = LOOTFRAME_NUMITEMS or 4 + if displayIdx <= maxBtns then + local blizzBtn = _G["LootButton" .. displayIdx] + if blizzBtn then + blizzBtn:SetID(slotIdx) + blizzBtn:SetParent(lootFrame) + blizzBtn:ClearAllPoints() + blizzBtn:SetAllPoints(row) + blizzBtn:SetFrameStrata("FULLSCREEN_DIALOG") + blizzBtn:SetFrameLevel(row:GetFrameLevel() + 10) + blizzBtn:SetAlpha(0) + blizzBtn:EnableMouse(true) + blizzBtn:Show() + + local rowRef = row + blizzBtn._nanamiRow = rowRef + blizzBtn:SetScript("OnEnter", function() + local rw = this._nanamiRow + if rw and rw._acc then + rw:SetBackdropBorderColor(rw._acc[1], rw._acc[2], rw._acc[3], 0.85) + rw:SetBackdropColor(rw._hoverBd[1], rw._hoverBd[2], rw._hoverBd[3], 0.35) + end + if rw and rw.slotIndex then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + if LootSlotIsItem(rw.slotIndex) then + GameTooltip:SetLootItem(rw.slotIndex) + else + local t, n = GetLootSlotInfo(rw.slotIndex) + if n then GameTooltip:SetText(n) end + end + GameTooltip:Show() + end + end) + blizzBtn:SetScript("OnLeave", function() + local rw = this._nanamiRow + if rw and rw._slotBg then + rw:SetBackdropColor(rw._slotBg[1], rw._slotBg[2], rw._slotBg[3], rw._slotBg[4] or 0.85) + if rw._qualColor then + rw:SetBackdropBorderColor(rw._qualColor[1], rw._qualColor[2], rw._qualColor[3], 0.35) + else + rw:SetBackdropBorderColor(rw._slotBd[1], rw._slotBd[2], rw._slotBd[3], rw._slotBd[4] or 0.60) + end + end + GameTooltip:Hide() + end) + end + end + end + + -- Hide unused Blizzard buttons + local maxBtns = LOOTFRAME_NUMITEMS or 4 + for i = numValid + 1, maxBtns do + local blizzBtn = _G["LootButton" .. i] + if blizzBtn then blizzBtn:Hide() end + end + + -- Position: use saved mover position if exists, otherwise follow cursor + if not lootFrame._posApplied then + local hasSaved = false + if SFrames.Movers and SFrames.Movers.ApplyPosition then + hasSaved = SFrames.Movers:ApplyPosition("LootFrame", lootFrame, + "TOPLEFT", "UIParent", "TOPLEFT", 50, -200) + end + if hasSaved then + lootFrame._posApplied = true + end + end + if not lootFrame._posApplied then + local cx, cy = GetCursorPosition() + local uiS = UIParent:GetEffectiveScale() + lootFrame:ClearAllPoints() + lootFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", cx / uiS - 30, cy / uiS + 16) + end + + lootFrame:Show() +end + +local function CloseLootFrame() + -- Return Blizzard buttons to LootFrame + local maxBtns = LOOTFRAME_NUMITEMS or 4 + for i = 1, maxBtns do + local blizzBtn = _G["LootButton" .. i] + if blizzBtn and LootFrame then + blizzBtn:SetParent(LootFrame) + blizzBtn:SetAlpha(1) + blizzBtn:Hide() + blizzBtn._nanamiRow = nil + end + end + if lootFrame then lootFrame:Hide() end + for i = 1, table.getn(lootRows) do lootRows[i]:Hide() end +end + +-------------------------------------------------------------------------------- +-- ▸ LOOT ALERTS (已拾取提示) +-------------------------------------------------------------------------------- +local function CreateAlertAnchor() + if alertAnchor then return alertAnchor end + + alertAnchor = CreateFrame("Frame", "NanamiLootAlertAnchor", UIParent) + alertAnchor:SetWidth(ALERT_WIDTH) + alertAnchor:SetHeight(ALERT_HEIGHT) + alertAnchor:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 40, 340) + alertAnchor:EnableMouse(false) + + if SFrames.Movers and SFrames.Movers.ApplyPosition then + SFrames.Movers:ApplyPosition("LootAlert", alertAnchor, + "BOTTOMLEFT", "UIParent", "BOTTOMLEFT", 40, 340) + end + return alertAnchor +end + +local function CreateAlertFrame() + local th = T() + local idx = table.getn(alertPool) + 1 + local bg = th.panelBg or { 0.06, 0.05, 0.09, 0.92 } + local bd = th.panelBorder or { 0.30, 0.25, 0.42, 0.80 } + + local f = CreateFrame("Frame", "NanamiLootAlert" .. idx, UIParent) + f:SetFrameStrata("HIGH") + f:SetFrameLevel(20) + f:SetWidth(ALERT_WIDTH) + f:SetHeight(ALERT_HEIGHT) + + -- Rounded backdrop matching UI theme + f:SetBackdrop(ROUND_BACKDROP_SMALL) + f:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 0.92) + f:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 0.80) + + -- Left quality accent bar + local qBar = f:CreateTexture(nil, "OVERLAY") + qBar:SetTexture("Interface\\Buttons\\WHITE8X8") + qBar:SetWidth(2) + qBar:SetPoint("TOPLEFT", f, "TOPLEFT", 3, -3) + qBar:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 3, 3) + qBar:SetVertexColor(1, 1, 1, 0.7) + f.qBar = qBar + + -- Icon with rounded border + local iconFrame = CreateFrame("Frame", nil, f) + iconFrame:SetWidth(ALERT_ICON + 4) + iconFrame:SetHeight(ALERT_ICON + 4) + iconFrame:SetPoint("LEFT", f, "LEFT", 7, 0) + iconFrame:SetFrameLevel(f:GetFrameLevel() + 1) + iconFrame:SetBackdrop(ICON_BACKDROP) + iconFrame:SetBackdropColor(0, 0, 0, 0.55) + iconFrame:SetBackdropBorderColor(0.25, 0.22, 0.30, 0.6) + f.iconFrame = iconFrame + + local icon = iconFrame:CreateTexture(nil, "ARTWORK") + icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 2, -2) + icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -2, 2) + icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + f.icon = icon + + -- Name + local nameFS = f:CreateFontString(nil, "OVERLAY") + nameFS:SetFont(Font(), 10, "OUTLINE") + nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 5, 0) + nameFS:SetPoint("RIGHT", f, "RIGHT", -30, 0) + nameFS:SetJustifyH("LEFT") + f.nameFS = nameFS + + -- Count (right side) + local countFS = f:CreateFontString(nil, "OVERLAY") + countFS:SetFont(Font(), 10, "OUTLINE") + countFS:SetPoint("RIGHT", f, "RIGHT", -6, 0) + countFS:SetJustifyH("RIGHT") + local dim = th.dimText or { 0.55, 0.55, 0.60 } + countFS:SetTextColor(dim[1], dim[2], dim[3], 0.95) + f.countFS = countFS + + f:Hide() + table.insert(alertPool, f) + return f +end + +local function GetAlertFrame() + for i = 1, table.getn(alertPool) do + if not alertPool[i]._inUse then return alertPool[i] end + end + return CreateAlertFrame() +end + +local function LayoutAlerts() + CreateAlertAnchor() + for i = 1, table.getn(activeAlerts) do + local af = activeAlerts[i] + if af._fadeState ~= "fading" then + af:ClearAllPoints() + af:SetPoint("BOTTOMLEFT", alertAnchor, "BOTTOMLEFT", 0, (i - 1) * (ALERT_HEIGHT + ALERT_GAP)) + end + end +end + +local function RemoveAlert(frame) + frame._inUse = false + frame:SetAlpha(0) + frame:Hide() + frame:SetScript("OnUpdate", nil) + local newActive = {} + for i = 1, table.getn(activeAlerts) do + if activeAlerts[i] ~= frame then + table.insert(newActive, activeAlerts[i]) + end + end + activeAlerts = newActive + LayoutAlerts() +end + +local function StartAlertFade(frame, delay) + frame._fadeState = "waiting" + frame._fadeElapsed = 0 + frame._fadeDelay = delay + + frame:SetScript("OnUpdate", function() + this._fadeElapsed = (this._fadeElapsed or 0) + arg1 + + if this._fadeState == "waiting" then + if this._fadeElapsed >= this._fadeDelay then + this._fadeState = "fading" + this._fadeElapsed = 0 + this._baseY = 0 + for idx = 1, table.getn(activeAlerts) do + if activeAlerts[idx] == this then + this._baseY = (idx - 1) * (ALERT_HEIGHT + ALERT_GAP) + break + end + end + end + elseif this._fadeState == "fading" then + local p = this._fadeElapsed / ALERT_FADE_DUR + if p >= 1 then + RemoveAlert(this) + else + this:SetAlpha(1 - p) + this:ClearAllPoints() + this:SetPoint("BOTTOMLEFT", alertAnchor, "BOTTOMLEFT", + 0, this._baseY + p * ALERT_FLOAT) + end + end + end) +end + +local function PruneAlerts() + while table.getn(activeAlerts) > MAX_ALERTS do + RemoveAlert(activeAlerts[1]) + end +end + +local function ShowLootAlert(texture, name, quality, quantity, link) + local db = GetDB() + if not db.alertEnable then return end + + for i = 1, table.getn(activeAlerts) do + local af = activeAlerts[i] + if af._itemName == name and af._fadeState == "waiting" then + af._quantity = (af._quantity or 1) + (quantity or 1) + if af._quantity > 1 then + af.countFS:SetText("x" .. af._quantity) + end + af._fadeElapsed = 0 + return + end + end + + PruneAlerts() + CreateAlertAnchor() + + local f = GetAlertFrame() + f._inUse = true + f._itemName = name + f._quantity = quantity or 1 + f._link = link + + -- Set icon texture + local iconTex = texture or "Interface\\Icons\\INV_Misc_QuestionMark" + f.icon:SetTexture(iconTex) + + local r, g, b = QColor(quality) + f.qBar:SetVertexColor(r, g, b, 0.90) + f.iconFrame:SetBackdropBorderColor(r, g, b, 0.60) + + f.nameFS:SetText("|cff" .. ColorHex(r, g, b) .. (name or "???") .. "|r") + + if f._quantity > 1 then + f.countFS:SetText("x" .. f._quantity) + else + f.countFS:SetText("") + end + + -- Re-apply theme backdrop colors (pool reuse) + local th = T() + local bg = th.panelBg or { 0.06, 0.05, 0.09, 0.92 } + local bd = th.panelBorder or { 0.30, 0.25, 0.42, 0.80 } + f:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 0.92) + f:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 0.80) + + f:SetAlpha(1) + f:Show() + + table.insert(activeAlerts, f) + LayoutAlerts() + + local hold = db.alertFadeDelay or ALERT_HOLD + local stagger = table.getn(activeAlerts) * ALERT_STAGGER + StartAlertFade(f, hold + stagger) +end + +-------------------------------------------------------------------------------- +-- Parse CHAT_MSG_LOOT +-------------------------------------------------------------------------------- +local function ParseLootMessage(msg) + if not msg then return end + + local _, _, link, countStr = string.find(msg, "(|c%x+|Hitem.-%[.-%]|h|r)x(%d+)") + if not link then + _, _, link = string.find(msg, "(|c%x+|Hitem.-%[.-%]|h|r)") + end + + if link then + local itemName, quality, itemId = ParseItemLink(link) + if itemName then + local count = tonumber(countStr) or 1 + -- Try full link first (most reliable), then itemId, then itemString + local tex = GetItemTexture(link) + if not tex and itemId and itemId > 0 then + tex = GetItemTexture(itemId) + end + if not tex and itemId and itemId > 0 then + tex = GetItemTexture("item:" .. itemId .. ":0:0:0") + end + ShowLootAlert(tex, itemName, quality, count, link) + end + return + end + + -- Money + local isMoney = false + if string.find(msg, "铜") or string.find(msg, "银") or string.find(msg, "金") + or string.find(msg, "[Cc]opper") or string.find(msg, "[Ss]ilver") or string.find(msg, "[Gg]old") then + if string.find(msg, "拾取") or string.find(msg, "获得") or string.find(msg, "loot") or string.find(msg, "receive") then + isMoney = true + end + end + + if isMoney then + local cleanMsg = string.gsub(msg, "|c%x+", "") + cleanMsg = string.gsub(cleanMsg, "|r", "") + local _, _, moneyPart = string.find(cleanMsg, "[::]%s*(.+)$") + if not moneyPart then + _, _, moneyPart = string.find(cleanMsg, "loot%s+(.+)$") + end + if not moneyPart then + _, _, moneyPart = string.find(cleanMsg, "获得了?%s*(.+)$") + end + if moneyPart then + moneyPart = string.gsub(moneyPart, "[%.。]+$", "") + ShowLootAlert("Interface\\Icons\\INV_Misc_Coin_01", moneyPart, 1, 1, nil) + end + end +end + +-------------------------------------------------------------------------------- +-- Initialize +-------------------------------------------------------------------------------- +function LD:Initialize() + local db = GetDB() + if not db.enable then return end + + CreateLootFrame() + CreateAlertAnchor() + + -- Apply saved positions so frames have valid coordinates for the Mover system + if SFrames.Movers and SFrames.Movers.ApplyPosition then + local applied = SFrames.Movers:ApplyPosition("LootFrame", lootFrame, + "TOPLEFT", "UIParent", "TOPLEFT", 50, -200) + if applied then lootFrame._posApplied = true end + + SFrames.Movers:ApplyPosition("LootAlert", alertAnchor, + "BOTTOMLEFT", "UIParent", "BOTTOMLEFT", 40, 340) + end + + if SFrames.Movers and SFrames.Movers.RegisterMover then + SFrames.Movers:RegisterMover("LootFrame", lootFrame, "拾取窗口", + "TOPLEFT", "UIParent", "TOPLEFT", 50, -200) + SFrames.Movers:RegisterMover("LootAlert", alertAnchor, "已拾取提示", + "BOTTOMLEFT", "UIParent", "BOTTOMLEFT", 40, 340) + end + + SFrames:RegisterEvent("LOOT_OPENED", function() + if GetDB().enable then UpdateLootFrame() end + end) + + SFrames:RegisterEvent("LOOT_SLOT_CLEARED", function() + if GetDB().enable and lootFrame and lootFrame:IsShown() then + UpdateLootFrame() + end + end) + + SFrames:RegisterEvent("LOOT_CLOSED", function() + CloseLootFrame() + end) + + SFrames:RegisterEvent("CHAT_MSG_LOOT", function() + local playerName = UnitName("player") + if not playerName then return end + local msg = arg1 or "" + if string.find(msg, playerName) or string.find(msg, "你获得") or string.find(msg, "你拾取") or string.find(msg, "You receive") then + ParseLootMessage(msg) + end + end) + + local function HideBlizzardLoot() + if LootFrame then + LootFrame:EnableMouse(false) + LootFrame:SetAlpha(0) + LootFrame:ClearAllPoints() + LootFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000) + local origShow = LootFrame.Show + LootFrame.Show = function(self) + origShow(self) + self:EnableMouse(false) + self:SetAlpha(0) + self:ClearAllPoints() + self:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000) + end + end + end + HideBlizzardLoot() + + local lootHook = CreateFrame("Frame") + lootHook:RegisterEvent("ADDON_LOADED") + lootHook:SetScript("OnEvent", function() + if arg1 == "Blizzard_Loot" then HideBlizzardLoot() end + end) +end diff --git a/MapReveal.lua b/MapReveal.lua index 6105df0..5532bbf 100644 --- a/MapReveal.lua +++ b/MapReveal.lua @@ -16,6 +16,7 @@ local errata = { } -- Turtle WoW new/modified zones not present in LibMapOverlayData +-- Updated for latest Turtle WoW version local TurtleWoW_Zones = { ["StonetalonMountains"] = { "SUNROCKRETREAT:512:256:256:256", "WINDSHEARCRAG:256:256:512:256", @@ -65,6 +66,9 @@ local TurtleWoW_Zones = { }, } +-- Runtime-discovered overlay data (populated by ScanAllMaps) +local scannedOverlays = {} + local function IsTurtleWoW() return TargetHPText and TargetHPPercText end @@ -85,6 +89,23 @@ local function PatchOverlayDB() for zone, data in pairs(TurtleWoW_Zones) do db[zone] = data end + + for zone, data in pairs(scannedOverlays) do + if not db[zone] then + db[zone] = data + end + end +end + +local function MergeScannedData() + local db = GetOverlayDB() + if not db then return end + + for zone, data in pairs(scannedOverlays) do + if not db[zone] or table.getn(db[zone]) < table.getn(data) then + db[zone] = data + end + end end local function GetConfig() @@ -285,3 +306,207 @@ function MapReveal:Refresh() WorldMapFrame_Update() end end + +-------------------------------------------------------------------------------- +-- Map Scanner: enumerate all continents/zones, collect overlay data from +-- explored areas, discover new zones, and merge into the overlay database. +-- Usage: /nui mapscan or SFrames.MapReveal:ScanAllMaps() +-------------------------------------------------------------------------------- +local scanFrame = nil +local scanQueue = {} +local scanIndex = 0 +local scanRunning = false +local scanResults = {} +local scanNewZones = {} +local scanUpdatedZones = {} +local savedMapC, savedMapZ = 0, 0 + +local function ExtractOverlayName(fullPath) + if not fullPath or fullPath == "" then return nil end + local _, _, name = string.find(fullPath, "\\([^\\]+)$") + return name and string.upper(name) or nil +end + +local function ProcessScanZone() + if scanIndex > table.getn(scanQueue) then + MapReveal:FinishScan() + return + end + + local entry = scanQueue[scanIndex] + SetMapZoom(entry.cont, entry.zone) + + local mapFile = GetMapInfo and GetMapInfo() or "" + if mapFile == "" then + scanIndex = scanIndex + 1 + return + end + + local numOverlays = GetNumMapOverlays and GetNumMapOverlays() or 0 + if numOverlays > 0 then + local overlays = {} + for i = 1, numOverlays do + local texName, texW, texH, offX, offY = GetMapOverlayInfo(i) + if texName and texName ~= "" then + local name = ExtractOverlayName(texName) + if name then + table.insert(overlays, name .. ":" .. texW .. ":" .. texH .. ":" .. offX .. ":" .. offY) + end + end + end + + if table.getn(overlays) > 0 then + local db = GetOverlayDB() + local existing = db and db[mapFile] + local existingCount = existing and table.getn(existing) or 0 + + if not existing then + scanNewZones[mapFile] = overlays + scanResults[mapFile] = { overlays = overlays, status = "new", count = table.getn(overlays) } + elseif table.getn(overlays) > existingCount then + scanUpdatedZones[mapFile] = overlays + scanResults[mapFile] = { overlays = overlays, status = "updated", count = table.getn(overlays), oldCount = existingCount } + else + scanResults[mapFile] = { status = "ok", count = existingCount } + end + end + end + + scanIndex = scanIndex + 1 +end + +function MapReveal:FinishScan() + scanRunning = false + if scanFrame then + scanFrame:SetScript("OnUpdate", nil) + end + + if savedMapZ > 0 then + SetMapZoom(savedMapC, savedMapZ) + elseif savedMapC > 0 then + SetMapZoom(savedMapC, 0) + else + if SetMapToCurrentZone then SetMapToCurrentZone() end + end + + local cf = DEFAULT_CHAT_FRAME + local newCount = 0 + local updCount = 0 + + for zone, overlays in pairs(scanNewZones) do + newCount = newCount + 1 + scannedOverlays[zone] = overlays + end + for zone, overlays in pairs(scanUpdatedZones) do + updCount = updCount + 1 + scannedOverlays[zone] = overlays + end + + MergeScannedData() + + cf:AddMessage("|cffffb3d9[Nanami-UI]|r 地图扫描完成!") + cf:AddMessage(string.format(" 扫描了 |cff00ff00%d|r 个区域", table.getn(scanQueue))) + + if newCount > 0 then + cf:AddMessage(string.format(" 发现 |cff00ff00%d|r 个新区域 (已自动添加迷雾数据):", newCount)) + for zone, overlays in pairs(scanNewZones) do + cf:AddMessage(" |cff00ffff" .. zone .. "|r (" .. table.getn(overlays) .. " 个覆盖层)") + end + end + + if updCount > 0 then + cf:AddMessage(string.format(" 更新了 |cffffff00%d|r 个区域 (发现更多已探索覆盖层):", updCount)) + for zone, info in pairs(scanResults) do + if info.status == "updated" then + cf:AddMessage(" |cffffff00" .. zone .. "|r (" .. (info.oldCount or 0) .. " -> " .. info.count .. ")") + end + end + end + + if newCount == 0 and updCount == 0 then + cf:AddMessage(" 所有区域数据已是最新,未发现变动。") + end + + cf:AddMessage(" 提示: 新发现的区域仅记录已探索区域的覆盖层,完全探索后再次扫描可获取完整数据。") + + if WorldMapFrame and WorldMapFrame:IsShown() then + WorldMapFrame_Update() + end +end + +function MapReveal:ScanAllMaps() + if scanRunning then + SFrames:Print("地图扫描正在进行中...") + return + end + + local db = GetOverlayDB() + if not db then + SFrames:Print("MapReveal: 未找到覆盖层数据库,无法扫描。") + return + end + + scanRunning = true + scanQueue = {} + scanIndex = 1 + scanResults = {} + scanNewZones = {} + scanUpdatedZones = {} + + savedMapC = GetCurrentMapContinent and GetCurrentMapContinent() or 0 + savedMapZ = GetCurrentMapZone and GetCurrentMapZone() or 0 + + local numContinents = 0 + if GetMapContinents then + local continents = { GetMapContinents() } + numContinents = table.getn(continents) + end + + for c = 1, numContinents do + local zones = { GetMapZones(c) } + for z = 1, table.getn(zones) do + table.insert(scanQueue, { cont = c, zone = z, name = zones[z] or "" }) + end + end + + SFrames:Print("开始扫描所有地图... (共 " .. table.getn(scanQueue) .. " 个区域)") + + if not scanFrame then + scanFrame = CreateFrame("Frame") + end + + local scanElapsed = 0 + scanFrame:SetScript("OnUpdate", function() + scanElapsed = scanElapsed + (arg1 or 0) + if scanElapsed < 0.02 then return end + scanElapsed = 0 + + if not scanRunning then return end + local batch = 0 + while scanIndex <= table.getn(scanQueue) and batch < 5 do + ProcessScanZone() + batch = batch + 1 + end + if scanIndex > table.getn(scanQueue) then + MapReveal:FinishScan() + end + end) +end + +function MapReveal:ExportScannedData() + local cf = DEFAULT_CHAT_FRAME + local hasData = false + + for zone, overlays in pairs(scannedOverlays) do + hasData = true + cf:AddMessage("|cffffb3d9[MapReveal Export]|r |cff00ffff" .. zone .. "|r = {") + for _, entry in ipairs(overlays) do + cf:AddMessage(' "' .. entry .. '",') + end + cf:AddMessage("}") + end + + if not hasData then + cf:AddMessage("|cffffb3d9[MapReveal]|r 没有扫描到的新数据可导出。先运行 /nui mapscan") + end +end diff --git a/Merchant.lua b/Merchant.lua index 2c8e98e..f798ea5 100644 --- a/Merchant.lua +++ b/Merchant.lua @@ -426,13 +426,18 @@ function MUI:BuyMultiple(index, totalAmount) end local name, _, price, batchQty, numAvailable = GetMerchantItemInfo(index) + local batchSize = (batchQty and batchQty > 0) and batchQty or 1 + local numPurchases = math.ceil(totalAmount / batchSize) - if numAvailable > -1 and totalAmount > numAvailable then - totalAmount = numAvailable + if numAvailable > -1 and numPurchases > numAvailable then + numPurchases = numAvailable end - for i = 1, totalAmount do - BuyMerchantItem(index, 1) + while numPurchases > 0 do + local batch = numPurchases + if batch > 255 then batch = 255 end + BuyMerchantItem(index, batch) + numPurchases = numPurchases - batch end end @@ -612,12 +617,13 @@ local function CreateMerchantButton(parent, id) local name, _, price, quantity, numAvailable = GetMerchantItemInfo(btn.itemIndex) if not name then return end local popupLink = GetMerchantItemLink(btn.itemIndex) + local batchSize = (quantity and quantity > 0) and quantity or 1 local maxCanAfford = 9999 if price and price > 0 then - maxCanAfford = math.floor(GetMoney() / price) + maxCanAfford = math.floor(GetMoney() / price) * batchSize end - if numAvailable > -1 and numAvailable < maxCanAfford then - maxCanAfford = numAvailable + if numAvailable > -1 and numAvailable * batchSize < maxCanAfford then + maxCanAfford = numAvailable * batchSize end if maxCanAfford < 1 then return end EnsureBuyPopup() diff --git a/Minimap.lua b/Minimap.lua index 6462412..5ce5cf4 100644 --- a/Minimap.lua +++ b/Minimap.lua @@ -19,6 +19,28 @@ local MAP_STYLES = { { key = "ss", tex = "Interface\\AddOns\\Nanami-UI\\img\\ss", label = "术士", plateY = -6, textColor = {1, 1, 1} }, { key = "dly", tex = "Interface\\AddOns\\Nanami-UI\\img\\dly", label = "德鲁伊", plateY = -6, textColor = {0.22, 0.13, 0.07} }, } +local SQUARE_STYLES = { + { + key = "square1", + label = "方形·金", + tex = "Interface\\AddOns\\Nanami-UI\\img\\map_f_1", + texSize = 512, + mapX = 9, mapY = 9, mapW = 486, mapH = 486, + zoneOverlay = true, + textColor = {0.85, 0.75, 0.55}, + }, + { + key = "square2", + label = "方形·暗", + tex = "Interface\\AddOns\\Nanami-UI\\img\\map_f_2", + texSize = 512, + mapX = 52, mapY = 61, mapW = 418, mapH = 418, + zonePlateX = 104, zonePlateY = 18, zonePlateW = 302, zonePlateH = 45, + textColor = {0.8, 0.8, 0.8}, + }, +} +local SQUARE_MASK = "Interface\\BUTTONS\\WHITE8X8" + local TEX_SIZE = 512 local CIRCLE_CX = 256 local CIRCLE_CY = 256 @@ -47,6 +69,7 @@ local DEFAULTS = { showClock = true, showCoords = true, mapStyle = "auto", + mapShape = "square1", posX = -5, posY = -5, mailIconX = nil, @@ -54,7 +77,7 @@ local DEFAULTS = { } local container, overlayFrame, overlayTex -local zoneFs, clockFs, clockBg, coordFs +local zoneFs, clockFs, clockBg, coordFs, zoneBg local built = false -------------------------------------------------------------------------------- @@ -93,6 +116,19 @@ local function GetMapTexture() return GetCurrentStyle().tex end +local function IsSquareShape() + local shape = GetDB().mapShape or "square1" + return shape == "square1" or shape == "square2" +end + +local function GetSquareStyle() + local shape = GetDB().mapShape or "square1" + for _, s in ipairs(SQUARE_STYLES) do + if s.key == shape then return s end + end + return SQUARE_STYLES[1] +end + local function S(texPx, frameSize) return texPx / TEX_SIZE * frameSize end @@ -103,11 +139,17 @@ end local function ApplyPosition() if not container then return end - local db = GetDB() - local x = tonumber(db.posX) or -5 - local y = tonumber(db.posY) or -5 - container:ClearAllPoints() - container:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", x, y) + local pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["Minimap"] + if pos and pos.point and pos.relativePoint then + container:ClearAllPoints() + container:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) + else + local db = GetDB() + local x = tonumber(db.posX) or -5 + local y = tonumber(db.posY) or -5 + container:ClearAllPoints() + container:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", x, y) + end end -------------------------------------------------------------------------------- @@ -178,7 +220,7 @@ local function BuildFrame() built = true local fs = FrameSize() - local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs)) + local isSquare = IsSquareShape() -- Main container container = CreateFrame("Frame", "SFramesMinimapContainer", UIParent) @@ -192,19 +234,35 @@ local function BuildFrame() -- Reparent the actual minimap into our container Minimap:SetParent(container) Minimap:ClearAllPoints() - Minimap:SetPoint("CENTER", container, "TOPLEFT", - S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs)) - Minimap:SetWidth(mapDiam) - Minimap:SetHeight(mapDiam) + if isSquare then + local sq = GetSquareStyle() + local mapW = sq.mapW / sq.texSize * fs + local mapH = sq.mapH / sq.texSize * fs + local cx = (sq.mapX + sq.mapW / 2) / sq.texSize * fs + local cy = (sq.mapY + sq.mapH / 2) / sq.texSize * fs + Minimap:SetPoint("CENTER", container, "TOPLEFT", cx, -cy) + Minimap:SetWidth(mapW) + Minimap:SetHeight(mapH) + else + local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs)) + Minimap:SetPoint("CENTER", container, "TOPLEFT", + S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs)) + Minimap:SetWidth(mapDiam) + Minimap:SetHeight(mapDiam) + end Minimap:SetFrameStrata("LOW") Minimap:SetFrameLevel(2) Minimap:Show() if Minimap.SetMaskTexture then - Minimap:SetMaskTexture("Textures\\MinimapMask") + if isSquare then + Minimap:SetMaskTexture(SQUARE_MASK) + else + Minimap:SetMaskTexture("Textures\\MinimapMask") + end end - -- Decorative overlay (map.tga with transparent circle) + -- Decorative overlay (frame texture) overlayFrame = CreateFrame("Frame", nil, container) overlayFrame:SetAllPoints(container) overlayFrame:SetFrameStrata("LOW") @@ -212,22 +270,58 @@ local function BuildFrame() overlayFrame:EnableMouse(false) overlayTex = overlayFrame:CreateTexture(nil, "ARTWORK") - overlayTex:SetTexture(GetMapTexture()) + if isSquare then + overlayTex:SetTexture(GetSquareStyle().tex) + else + overlayTex:SetTexture(GetMapTexture()) + end overlayTex:SetAllPoints(overlayFrame) - -- Zone name on the scroll plate (horizontally centered on frame) - local style = GetCurrentStyle() - local pcy = S(PLATE_Y + PLATE_H / 2 + (style.plateY or -6), fs) + -- Zone name background (semi-transparent strip for square styles) + zoneBg = CreateFrame("Frame", nil, overlayFrame) + zoneBg:SetFrameLevel(overlayFrame:GetFrameLevel() - 1) + zoneBg:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + }) + zoneBg:SetBackdropColor(0, 0, 0, 0.5) + zoneBg:Hide() + -- Zone name zoneFs = overlayFrame:CreateFontString(nil, "OVERLAY") - zoneFs:SetFont(SFrames:GetFont(), 11, "") - zoneFs:SetPoint("CENTER", container, "TOP", 0, -pcy) - zoneFs:SetWidth(S(PLATE_W + 60, fs)) - zoneFs:SetHeight(S(PLATE_H, fs)) zoneFs:SetJustifyH("CENTER") zoneFs:SetJustifyV("MIDDLE") - local tc = style.textColor or {0.22, 0.13, 0.07} - zoneFs:SetTextColor(tc[1], tc[2], tc[3]) + + if isSquare then + local sq = GetSquareStyle() + zoneFs:SetFont(SFrames:GetFont(), 11, "OUTLINE") + if sq.zonePlateX then + local px = (sq.zonePlateX + sq.zonePlateW / 2) / sq.texSize * fs + local py = (sq.zonePlateY + sq.zonePlateH / 2) / sq.texSize * fs + zoneFs:SetPoint("CENTER", container, "TOPLEFT", px, -py) + zoneFs:SetWidth(sq.zonePlateW / sq.texSize * fs) + zoneFs:SetHeight(sq.zonePlateH / sq.texSize * fs) + else + zoneFs:SetPoint("TOP", Minimap, "TOP", 0, -3) + zoneFs:SetWidth(Minimap:GetWidth() * 0.7) + zoneFs:SetHeight(18) + zoneBg:ClearAllPoints() + zoneBg:SetPoint("TOP", Minimap, "TOP", 0, 0) + zoneBg:SetWidth(Minimap:GetWidth()) + zoneBg:SetHeight(20) + zoneBg:Show() + end + local tc = sq.textColor or {1, 1, 1} + zoneFs:SetTextColor(tc[1], tc[2], tc[3]) + else + local style = GetCurrentStyle() + zoneFs:SetFont(SFrames:GetFont(), 11, "") + local pcy = S(PLATE_Y + PLATE_H / 2 + (style.plateY or -6), fs) + zoneFs:SetPoint("CENTER", container, "TOP", 0, -pcy) + zoneFs:SetWidth(S(PLATE_W + 60, fs)) + zoneFs:SetHeight(S(PLATE_H, fs)) + local tc = style.textColor or {0.22, 0.13, 0.07} + zoneFs:SetTextColor(tc[1], tc[2], tc[3]) + end -- Clock background (semi-transparent rounded) clockBg = CreateFrame("Frame", nil, overlayFrame) @@ -244,8 +338,12 @@ local function BuildFrame() clockBg:SetBackdropBorderColor(_clkBd[1], _clkBd[2], _clkBd[3], _clkBd[4]) clockBg:SetWidth(46) clockBg:SetHeight(18) - clockBg:SetPoint("CENTER", container, "BOTTOM", 0, - S((TEX_SIZE - CIRCLE_CY - CIRCLE_R) / 2, fs)) + if isSquare then + clockBg:SetPoint("TOP", Minimap, "BOTTOM", 0, -2) + else + clockBg:SetPoint("CENTER", container, "BOTTOM", 0, + S((TEX_SIZE - CIRCLE_CY - CIRCLE_R) / 2, fs)) + end -- Clock text clockFs = clockBg:CreateFontString(nil, "OVERLAY") @@ -255,7 +353,7 @@ local function BuildFrame() local _clkTxt = _A.clockText or { 0.92, 0.84, 0.72 } clockFs:SetTextColor(_clkTxt[1], _clkTxt[2], _clkTxt[3]) - -- Coordinates (inside circle, near bottom) + -- Coordinates (inside map area, near bottom) coordFs = overlayFrame:CreateFontString(nil, "OVERLAY") coordFs:SetFont(SFrames:GetFont(), 9, "OUTLINE") coordFs:SetPoint("BOTTOM", Minimap, "BOTTOM", 0, 8) @@ -430,47 +528,114 @@ function MM:Refresh() if not container then return end local fs = FrameSize() - local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs)) + local isSquare = IsSquareShape() container:SetWidth(fs) container:SetHeight(fs) - Minimap:ClearAllPoints() - Minimap:SetPoint("CENTER", container, "TOPLEFT", - S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs)) - Minimap:SetWidth(mapDiam) - Minimap:SetHeight(mapDiam) - - if zoneFs then - local style = GetCurrentStyle() - local pcy = S(PLATE_Y + PLATE_H / 2 + (style.plateY or -6), fs) - zoneFs:ClearAllPoints() - zoneFs:SetPoint("CENTER", container, "TOP", 0, -pcy) - zoneFs:SetWidth(S(PLATE_W + 60, fs)) - local tc = style.textColor or {0.22, 0.13, 0.07} - zoneFs:SetTextColor(tc[1], tc[2], tc[3]) + -- Apply mask + if Minimap.SetMaskTexture then + if isSquare then + Minimap:SetMaskTexture(SQUARE_MASK) + else + Minimap:SetMaskTexture("Textures\\MinimapMask") + end end + -- Position and size minimap + Minimap:ClearAllPoints() + if isSquare then + local sq = GetSquareStyle() + local mapW = sq.mapW / sq.texSize * fs + local mapH = sq.mapH / sq.texSize * fs + local cx = (sq.mapX + sq.mapW / 2) / sq.texSize * fs + local cy = (sq.mapY + sq.mapH / 2) / sq.texSize * fs + Minimap:SetPoint("CENTER", container, "TOPLEFT", cx, -cy) + Minimap:SetWidth(mapW) + Minimap:SetHeight(mapH) + else + local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs)) + Minimap:SetPoint("CENTER", container, "TOPLEFT", + S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs)) + Minimap:SetWidth(mapDiam) + Minimap:SetHeight(mapDiam) + end + + -- Update overlay texture + if overlayTex then + if isSquare then + overlayTex:SetTexture(GetSquareStyle().tex) + else + overlayTex:SetTexture(GetMapTexture()) + end + end + + -- Update zone text + if zoneFs then + zoneFs:ClearAllPoints() + if isSquare then + local sq = GetSquareStyle() + zoneFs:SetFont(SFrames:GetFont(), 11, "OUTLINE") + if sq.zonePlateX then + local px = (sq.zonePlateX + sq.zonePlateW / 2) / sq.texSize * fs + local py = (sq.zonePlateY + sq.zonePlateH / 2) / sq.texSize * fs + zoneFs:SetPoint("CENTER", container, "TOPLEFT", px, -py) + zoneFs:SetWidth(sq.zonePlateW / sq.texSize * fs) + zoneFs:SetHeight(sq.zonePlateH / sq.texSize * fs) + else + zoneFs:SetPoint("TOP", Minimap, "TOP", 0, -3) + zoneFs:SetWidth(Minimap:GetWidth() * 0.7) + zoneFs:SetHeight(18) + end + local tc = sq.textColor or {1, 1, 1} + zoneFs:SetTextColor(tc[1], tc[2], tc[3]) + else + zoneFs:SetFont(SFrames:GetFont(), 11, "") + local style = GetCurrentStyle() + local pcy = S(PLATE_Y + PLATE_H / 2 + (style.plateY or -6), fs) + zoneFs:SetPoint("CENTER", container, "TOP", 0, -pcy) + zoneFs:SetWidth(S(PLATE_W + 60, fs)) + local tc = style.textColor or {0.22, 0.13, 0.07} + zoneFs:SetTextColor(tc[1], tc[2], tc[3]) + end + end + + -- Zone background (semi-transparent strip, only for square styles with zoneOverlay) + if zoneBg then + if isSquare and GetSquareStyle().zoneOverlay then + zoneBg:ClearAllPoints() + zoneBg:SetPoint("TOP", Minimap, "TOP", 0, 0) + zoneBg:SetWidth(Minimap:GetWidth()) + zoneBg:SetHeight(20) + zoneBg:Show() + else + zoneBg:Hide() + end + end + + -- Clock if clockBg then clockBg:ClearAllPoints() - clockBg:SetPoint("CENTER", container, "BOTTOM", 0, - S((TEX_SIZE - CIRCLE_CY - CIRCLE_R) / 2, fs)) + if isSquare then + clockBg:SetPoint("TOP", Minimap, "BOTTOM", 0, -2) + else + clockBg:SetPoint("CENTER", container, "BOTTOM", 0, + S((TEX_SIZE - CIRCLE_CY - CIRCLE_R) / 2, fs)) + end end + -- Coords if coordFs then coordFs:ClearAllPoints() coordFs:SetPoint("BOTTOM", Minimap, "BOTTOM", 0, 8) end - if overlayTex then - overlayTex:SetTexture(GetMapTexture()) - end - UpdateZoneText() RepositionIcons() end MM.MAP_STYLES = MAP_STYLES +MM.SQUARE_STYLES = SQUARE_STYLES -------------------------------------------------------------------------------- -- Shield: re-apply our skin after other addons (ShaguTweaks etc.) touch Minimap @@ -481,24 +646,40 @@ local function ShieldMinimap() if shielded then return end shielded = true - -- Override any external changes to Minimap parent / position / size if Minimap:GetParent() ~= container then Minimap:SetParent(container) end local fs = FrameSize() - local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs)) + local isSquare = IsSquareShape() + Minimap:ClearAllPoints() - Minimap:SetPoint("CENTER", container, "TOPLEFT", - S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs)) - Minimap:SetWidth(mapDiam) - Minimap:SetHeight(mapDiam) + if isSquare then + local sq = GetSquareStyle() + local mapW = sq.mapW / sq.texSize * fs + local mapH = sq.mapH / sq.texSize * fs + local cx = (sq.mapX + sq.mapW / 2) / sq.texSize * fs + local cy = (sq.mapY + sq.mapH / 2) / sq.texSize * fs + Minimap:SetPoint("CENTER", container, "TOPLEFT", cx, -cy) + Minimap:SetWidth(mapW) + Minimap:SetHeight(mapH) + else + local mapDiam = math.floor(S((CIRCLE_R + 8) * 2, fs)) + Minimap:SetPoint("CENTER", container, "TOPLEFT", + S(CIRCLE_CX, fs), -S(CIRCLE_CY, fs)) + Minimap:SetWidth(mapDiam) + Minimap:SetHeight(mapDiam) + end Minimap:SetFrameStrata("LOW") Minimap:SetFrameLevel(2) Minimap:Show() if Minimap.SetMaskTexture then - Minimap:SetMaskTexture("Textures\\MinimapMask") + if isSquare then + Minimap:SetMaskTexture(SQUARE_MASK) + else + Minimap:SetMaskTexture("Textures\\MinimapMask") + end end HideDefaultElements() @@ -587,4 +768,12 @@ function MM:Initialize() if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI Minimap error: " .. tostring(err) .. "|r") end + + if SFrames.Movers and SFrames.Movers.RegisterMover and container then + SFrames.Movers:RegisterMover("Minimap", container, "小地图", + "TOPRIGHT", "UIParent", "TOPRIGHT", db.posX or -5, db.posY or -5, + function() + pcall(RepositionIcons) + end) + end end diff --git a/MinimapBuffs.lua b/MinimapBuffs.lua index ee16e1b..1ade096 100644 --- a/MinimapBuffs.lua +++ b/MinimapBuffs.lua @@ -20,7 +20,9 @@ function SFrames:GetBuffName(buffIndex) SFrames.Tooltip:ClearLines() SFrames.Tooltip:SetPlayerBuff(buffIndex) local nameObj = SFramesScanTooltipTextLeft1 - return nameObj and nameObj:GetText() + local name = nameObj and nameObj:GetText() + SFrames.Tooltip:Hide() + return name end function SFrames:IsBuffHidden(buffIndex) @@ -239,13 +241,19 @@ end function MB:ApplyPosition() if not self.buffContainer then return end - local db = GetDB() - local pos = db.position or "TOPRIGHT" - local x = BASE_X + (db.offsetX or 0) - local y = BASE_Y + (db.offsetY or 0) - self.buffContainer:ClearAllPoints() - self.buffContainer:SetPoint(pos, UIParent, pos, x, y) + local saved = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["MinimapBuffs"] + if saved and saved.point and saved.relativePoint then + self.buffContainer:ClearAllPoints() + self.buffContainer:SetPoint(saved.point, UIParent, saved.relativePoint, saved.xOfs or 0, saved.yOfs or 0) + else + local db = GetDB() + local pos = db.position or "TOPRIGHT" + local x = BASE_X + (db.offsetX or 0) + local y = BASE_Y + (db.offsetY or 0) + self.buffContainer:ClearAllPoints() + self.buffContainer:SetPoint(pos, UIParent, pos, x, y) + end self:AnchorDebuffs() end @@ -478,6 +486,7 @@ function MB:UpdateDebuffs() SFrames.Tooltip:ClearLines() SFrames.Tooltip:SetPlayerBuff(buffIndex) local dTypeStr = SFramesScanTooltipTextRight1 and SFramesScanTooltipTextRight1:GetText() + SFrames.Tooltip:Hide() if dTypeStr and dTypeStr ~= "" then debuffType = dTypeStr end local c = DEBUFF_TYPE_COLORS[debuffType] or DEBUFF_DEFAULT_COLOR @@ -662,4 +671,16 @@ function MB:Initialize() end) self:UpdateBuffs() + + if SFrames.Movers and SFrames.Movers.RegisterMover then + if self.buffContainer then + SFrames.Movers:RegisterMover("MinimapBuffs", self.buffContainer, "Buff", + "TOPRIGHT", "UIParent", "TOPRIGHT", BASE_X + (db.offsetX or 0), BASE_Y + (db.offsetY or 0), + function() MB:AnchorDebuffs() end) + end + if self.debuffContainer then + SFrames.Movers:RegisterMover("MinimapDebuffs", self.debuffContainer, "Debuff", + "TOPRIGHT", "UIParent", "TOPRIGHT", BASE_X + (db.offsetX or 0), BASE_Y - 50) + end + end end diff --git a/Movers.lua b/Movers.lua new file mode 100644 index 0000000..b0ae333 --- /dev/null +++ b/Movers.lua @@ -0,0 +1,998 @@ +-------------------------------------------------------------------------------- +-- Nanami-UI: Layout Mode (Mover System) +-------------------------------------------------------------------------------- + +SFrames.Movers = {} + +local M = SFrames.Movers +local registry = {} +local moverFrames = {} +local gridFrame = nil +local controlBar = nil +local overlayFrame = nil +local isLayoutMode = false + +local GRID_SPACING = 50 +local SNAP_THRESHOLD = 10 + +local MOVER_BACKDROP = { + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, +} +local MOVER_BACKDROP_2PX = { + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Buttons\\WHITE8x8", + edgeSize = 2, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, +} + +-------------------------------------------------------------------------------- +-- Theme helper +-------------------------------------------------------------------------------- +local function T() + return SFrames.ActiveTheme or {} +end + +local function Font() + return (SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF" +end + +-------------------------------------------------------------------------------- +-- Config helpers +-------------------------------------------------------------------------------- +local function GetLayoutCfg() + if not SFramesDB then SFramesDB = {} end + if type(SFramesDB.layoutMode) ~= "table" then + SFramesDB.layoutMode = { + snapEnabled = true, + snapThreshold = SNAP_THRESHOLD, + showGrid = true, + } + end + return SFramesDB.layoutMode +end + +local function EnsurePositions() + if not SFramesDB then SFramesDB = {} end + if not SFramesDB.Positions then SFramesDB.Positions = {} end + return SFramesDB.Positions +end + +-------------------------------------------------------------------------------- +-- Snap logic +-------------------------------------------------------------------------------- +local function GetFrameEdges(frame) + local l = frame:GetLeft() or 0 + local r = frame:GetRight() or 0 + local tp = frame:GetTop() or 0 + local b = frame:GetBottom() or 0 + return l, r, tp, b, (l + r) / 2, (tp + b) / 2 +end + +local function ApplySnap(mover) + local cfg = GetLayoutCfg() + if not cfg.snapEnabled then return end + if IsShiftKeyDown() then return end + + local threshold = cfg.snapThreshold or SNAP_THRESHOLD + local ml, mr, mt, mb, mcx, mcy = GetFrameEdges(mover) + local mw, mh = mr - ml, mt - mb + + local screenW = UIParent:GetRight() or UIParent:GetWidth() + local screenH = UIParent:GetTop() or UIParent:GetHeight() + local screenCX, screenCY = screenW / 2, screenH / 2 + + local snapX, snapY = nil, nil + local bestDX, bestDY = threshold + 1, threshold + 1 + + if math.abs(ml) < bestDX then bestDX = math.abs(ml); snapX = 0 end + if math.abs(mr - screenW) < bestDX then bestDX = math.abs(mr - screenW); snapX = screenW - mw end + if math.abs(mt - screenH) < bestDY then bestDY = math.abs(mt - screenH); snapY = screenH - mh end + if math.abs(mb) < bestDY then bestDY = math.abs(mb); snapY = 0 end + + if math.abs(mcx - screenCX) < bestDX then bestDX = math.abs(mcx - screenCX); snapX = screenCX - mw / 2 end + if math.abs(mcy - screenCY) < bestDY then bestDY = math.abs(mcy - screenCY); snapY = screenCY - mh / 2 end + + for name, _ in pairs(registry) do + local other = moverFrames[name] + if other and other ~= mover and other:IsShown() then + local ol, or2, ot, ob, ocx, ocy = GetFrameEdges(other) + local px = { + { ml, or2, 0 }, { mr, ol, -mw }, { ml, ol, 0 }, + { mr, or2, -mw }, { mcx, ocx, -mw / 2 }, + } + for _, p in ipairs(px) do + local d = math.abs(p[1] - p[2]) + if d < bestDX then bestDX = d; snapX = p[2] + p[3] end + end + local py = { + { math.abs(mb - ot), ot }, { math.abs(mt - ob), ob - mh }, + { math.abs(mt - ot), ot - mh }, { math.abs(mb - ob), ob }, + { math.abs(mcy - ocy), ocy - mh / 2 }, + } + for _, p in ipairs(py) do + if p[1] < bestDY then bestDY = p[1]; snapY = p[2] end + end + end + end + + if bestDX <= threshold and snapX then + mover:ClearAllPoints() + local curB = (bestDY <= threshold and snapY) or (mover:GetBottom() or 0) + mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", snapX, curB) + return + end + if bestDY <= threshold and snapY then + mover:ClearAllPoints() + mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", mover:GetLeft() or 0, snapY) + end +end + +-------------------------------------------------------------------------------- +-- Position save / load +-------------------------------------------------------------------------------- +local function SaveMoverPosition(name, mover) + local positions = EnsurePositions() + local l = mover:GetLeft() + local b = mover:GetBottom() + local screenW = UIParent:GetRight() or UIParent:GetWidth() + local screenH = UIParent:GetTop() or UIParent:GetHeight() + if not l or not b then return end + + local cx = l + (mover:GetWidth() or 0) / 2 + local cy = b + (mover:GetHeight() or 0) / 2 + + local point, relPoint, xOfs, yOfs + if cx < screenW / 3 then + if cy > screenH * 2 / 3 then + point = "TOPLEFT"; relPoint = "TOPLEFT" + xOfs = l; yOfs = -(screenH - (b + (mover:GetHeight() or 0))) + elseif cy < screenH / 3 then + point = "BOTTOMLEFT"; relPoint = "BOTTOMLEFT" + xOfs = l; yOfs = b + else + point = "LEFT"; relPoint = "LEFT" + xOfs = l; yOfs = cy - screenH / 2 + end + elseif cx > screenW * 2 / 3 then + local r = mover:GetRight() or 0 + if cy > screenH * 2 / 3 then + point = "TOPRIGHT"; relPoint = "TOPRIGHT" + xOfs = r - screenW; yOfs = -(screenH - (b + (mover:GetHeight() or 0))) + elseif cy < screenH / 3 then + point = "BOTTOMRIGHT"; relPoint = "BOTTOMRIGHT" + xOfs = r - screenW; yOfs = b + else + point = "RIGHT"; relPoint = "RIGHT" + xOfs = r - screenW; yOfs = cy - screenH / 2 + end + else + if cy > screenH * 2 / 3 then + point = "TOP"; relPoint = "TOP" + xOfs = cx - screenW / 2; yOfs = -(screenH - (b + (mover:GetHeight() or 0))) + elseif cy < screenH / 3 then + point = "BOTTOM"; relPoint = "BOTTOM" + xOfs = cx - screenW / 2; yOfs = b + else + point = "CENTER"; relPoint = "CENTER" + xOfs = cx - screenW / 2; yOfs = cy - screenH / 2 + end + end + + positions[name] = { point = point, relativePoint = relPoint, xOfs = xOfs, yOfs = yOfs } +end + +-------------------------------------------------------------------------------- +-- Sync mover <-> actual frame +-------------------------------------------------------------------------------- +local function UpdateCoordText(mover) + if not mover or not mover._coordText then return end + local l = mover:GetLeft() + local b = mover:GetBottom() + if l and b then + mover._coordText:SetText(string.format("%.0f, %.0f", l, b)) + end +end + +local function SyncMoverToFrame(name) + local entry = registry[name] + local mover = moverFrames[name] + if not entry or not mover then return end + local frame = entry.frame + if not frame then return end + + local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale() + local w = (frame:GetWidth() or 100) * scale + local h = (frame:GetHeight() or 50) * scale + + mover:SetWidth(math.max(w, 72)) + mover:SetHeight(math.max(h, 40)) + + local l = frame:GetLeft() + local b = frame:GetBottom() + if l and b then + mover:ClearAllPoints() + mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", l * scale, b * scale) + else + local positions = EnsurePositions() + local pos = positions[name] + if pos and pos.point and pos.relativePoint then + local tempF = CreateFrame("Frame", nil, UIParent) + tempF:SetWidth(w) + tempF:SetHeight(h) + tempF:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) + local tl = tempF:GetLeft() + local tb = tempF:GetBottom() + if tl and tb then + mover:ClearAllPoints() + mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", tl, tb) + end + tempF:Hide() + else + mover:ClearAllPoints() + local relTo = _G[entry.defaultRelativeTo] or UIParent + mover:SetPoint(entry.defaultPoint, relTo, entry.defaultRelPoint, + entry.defaultX or 0, entry.defaultY or 0) + end + end + UpdateCoordText(mover) +end + +local function SyncFrameToMover(name) + local entry = registry[name] + local mover = moverFrames[name] + if not entry or not mover then return end + local frame = entry.frame + if not frame then return end + + local moverL = mover:GetLeft() or 0 + local moverB = mover:GetBottom() or 0 + + SaveMoverPosition(name, mover) + + local positions = EnsurePositions() + local pos = positions[name] + if pos then + frame:ClearAllPoints() + frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) + + local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale() + local newL = frame:GetLeft() or 0 + local newB = frame:GetBottom() or 0 + local dL = newL * scale - moverL + local dB = newB * scale - moverB + if math.abs(dL) > 2 or math.abs(dB) > 2 then + SFrames:Print(string.format( + "|cffff6666[位置偏差]|r |cffaaddff%s|r anchor=%s ofs=(%.1f,%.1f) dL=%.1f dB=%.1f scale=%.2f", + entry.label or name, pos.point, pos.xOfs or 0, pos.yOfs or 0, dL, dB, scale)) + end + end + + if entry.onMoved then entry.onMoved() end + UpdateCoordText(mover) +end + +-------------------------------------------------------------------------------- +-- Nudge helper +-------------------------------------------------------------------------------- +local function NudgeMover(name, dx, dy) + local mover = moverFrames[name] + if not mover then return end + local l = mover:GetLeft() + local b = mover:GetBottom() + if not l or not b then return end + mover:ClearAllPoints() + mover:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", l + dx, b + dy) + SyncFrameToMover(name) +end + +-------------------------------------------------------------------------------- +-- Pixel-art triangle arrow helper +-- Draws a triangle using 4 horizontal/vertical strips for a clean geometric look +-------------------------------------------------------------------------------- +local function CreateTriangle(parent, direction, r, g, b, a) + local strips = {} + + if direction == "UP" then + local widths = { 2, 5, 8, 11 } + local heights = { 2, 2, 2, 2 } + local yOfs = { 3, 1, -1, -3 } + for i = 1, 4 do + local s = parent:CreateTexture(nil, "OVERLAY") + s:SetTexture("Interface\\Buttons\\WHITE8x8") + s:SetWidth(widths[i]) + s:SetHeight(heights[i]) + s:SetPoint("CENTER", parent, "CENTER", 0, yOfs[i]) + s:SetVertexColor(r, g, b, a) + table.insert(strips, s) + end + elseif direction == "DOWN" then + local widths = { 11, 8, 5, 2 } + local heights = { 2, 2, 2, 2 } + local yOfs = { 3, 1, -1, -3 } + for i = 1, 4 do + local s = parent:CreateTexture(nil, "OVERLAY") + s:SetTexture("Interface\\Buttons\\WHITE8x8") + s:SetWidth(widths[i]) + s:SetHeight(heights[i]) + s:SetPoint("CENTER", parent, "CENTER", 0, yOfs[i]) + s:SetVertexColor(r, g, b, a) + table.insert(strips, s) + end + elseif direction == "LEFT" then + local widths = { 2, 2, 2, 2 } + local heights = { 2, 5, 8, 11 } + local xOfs = { -3, -1, 1, 3 } + for i = 1, 4 do + local s = parent:CreateTexture(nil, "OVERLAY") + s:SetTexture("Interface\\Buttons\\WHITE8x8") + s:SetWidth(widths[i]) + s:SetHeight(heights[i]) + s:SetPoint("CENTER", parent, "CENTER", xOfs[i], 0) + s:SetVertexColor(r, g, b, a) + table.insert(strips, s) + end + elseif direction == "RIGHT" then + local widths = { 2, 2, 2, 2 } + local heights = { 11, 8, 5, 2 } + local xOfs = { -3, -1, 1, 3 } + for i = 1, 4 do + local s = parent:CreateTexture(nil, "OVERLAY") + s:SetTexture("Interface\\Buttons\\WHITE8x8") + s:SetWidth(widths[i]) + s:SetHeight(heights[i]) + s:SetPoint("CENTER", parent, "CENTER", xOfs[i], 0) + s:SetVertexColor(r, g, b, a) + table.insert(strips, s) + end + end + + return strips +end + +local function SetTriangleColor(strips, r, g, b, a) + for _, s in ipairs(strips) do + s:SetVertexColor(r, g, b, a) + end +end + +-------------------------------------------------------------------------------- +-- Arrow nudge button (triangle-based) +-------------------------------------------------------------------------------- +local ARROW_SIZE = 16 +local ARROW_REPEAT_DELAY = 0.45 +local ARROW_REPEAT_RATE = 0.04 + +local function CreateArrowButton(parent, moverName, anchor, ofsX, ofsY, direction, dx, dy) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(ARROW_SIZE) + btn:SetHeight(ARROW_SIZE) + btn:SetPoint(anchor, parent, anchor, ofsX, ofsY) + btn:SetFrameLevel(parent:GetFrameLevel() + 5) + btn:SetMovable(true) + btn:RegisterForDrag("LeftButton") + + local th = T() + local dimC = th.dimText or { 0.5, 0.5, 0.55 } + local accentC = th.accent or { 1, 0.5, 0.8 } + + local bgTex = btn:CreateTexture(nil, "BACKGROUND") + bgTex:SetTexture("Interface\\Buttons\\WHITE8x8") + bgTex:SetAllPoints(btn) + bgTex:SetVertexColor(0, 0, 0, 0) + btn._bg = bgTex + + local tri = CreateTriangle(btn, direction, dimC[1], dimC[2], dimC[3], 0.55) + btn._tri = tri + + btn._elapsed = 0 + btn._held = false + btn._dragging = false + btn._delay = ARROW_REPEAT_DELAY + + btn:SetScript("OnClick", function() + if not this._dragging then + NudgeMover(moverName, dx, dy) + end + this._dragging = false + end) + btn:SetScript("OnMouseDown", function() + this._held = true + this._dragging = false + this._elapsed = 0 + this._delay = ARROW_REPEAT_DELAY + end) + btn:SetScript("OnMouseUp", function() + this._held = false + end) + btn:SetScript("OnDragStart", function() + this._held = false + this._dragging = true + parent:StartMoving() + end) + btn:SetScript("OnDragStop", function() + parent:StopMovingOrSizing() + ApplySnap(parent) + SyncFrameToMover(moverName) + this._dragging = false + end) + btn:SetScript("OnUpdate", function() + if not this._held then return end + this._elapsed = this._elapsed + arg1 + if this._elapsed >= this._delay then + this._elapsed = 0 + this._delay = ARROW_REPEAT_RATE + NudgeMover(moverName, dx, dy) + end + end) + btn:SetScript("OnEnter", function() + this._bg:SetVertexColor(accentC[1], accentC[2], accentC[3], 0.18) + SetTriangleColor(this._tri, 1, 1, 1, 1) + if parent._SetHover then parent:_SetHover(true) end + end) + btn:SetScript("OnLeave", function() + this._held = false + this._bg:SetVertexColor(0, 0, 0, 0) + SetTriangleColor(this._tri, dimC[1], dimC[2], dimC[3], 0.55) + if parent._SetHover then parent:_SetHover(false) end + end) + + return btn +end + +-------------------------------------------------------------------------------- +-- Create individual mover frame +-------------------------------------------------------------------------------- +local function CreateMoverFrame(name, entry) + if moverFrames[name] then return moverFrames[name] end + + local mover = CreateFrame("Button", "SFramesMover_" .. name, UIParent) + mover:SetFrameStrata("FULLSCREEN") + mover:SetFrameLevel(100) + mover:SetWidth(100) + mover:SetHeight(40) + mover:SetClampedToScreen(true) + mover:SetMovable(true) + mover:EnableMouse(true) + mover:RegisterForDrag("LeftButton") + mover:RegisterForClicks("RightButtonUp") + + mover:SetBackdrop(MOVER_BACKDROP_2PX) + + local th = T() + local secBg = th.sectionBg or { 0.12, 0.10, 0.18, 0.82 } + local accent = th.accent or { 1, 0.5, 0.8, 0.98 } + local accentLine = th.accentLine or { 0.6, 0.9, 1, 0.9 } + + mover:SetBackdropColor(secBg[1], secBg[2], secBg[3], secBg[4] or 0.82) + mover:SetBackdropBorderColor(accent[1], accent[2], accent[3], 0.6) + + -- Top accent stripe + local stripe = mover:CreateTexture(nil, "ARTWORK") + stripe:SetTexture("Interface\\Buttons\\WHITE8x8") + stripe:SetHeight(2) + stripe:SetPoint("TOPLEFT", mover, "TOPLEFT", 2, -2) + stripe:SetPoint("TOPRIGHT", mover, "TOPRIGHT", -2, -2) + stripe:SetVertexColor(accentLine[1], accentLine[2], accentLine[3], accentLine[4] or 0.9) + mover._stripe = stripe + + -- Label + local label = mover:CreateFontString(nil, "OVERLAY") + label:SetFont(Font(), 11, "OUTLINE") + label:SetPoint("CENTER", mover, "CENTER", 0, 5) + label:SetText(entry.label or name) + local titleC = th.title or { 1, 0.9, 1 } + label:SetTextColor(titleC[1], titleC[2], titleC[3], 1) + mover._label = label + + -- Coordinate readout + local coord = mover:CreateFontString(nil, "OVERLAY") + coord:SetFont(Font(), 9, "OUTLINE") + coord:SetPoint("CENTER", mover, "CENTER", 0, -7) + local dimC = th.dimText or { 0.5, 0.5, 0.55 } + coord:SetTextColor(dimC[1], dimC[2], dimC[3], 0.9) + coord:SetText("") + mover._coordText = coord + + -- Triangle arrow buttons + CreateArrowButton(mover, name, "TOP", 0, -1, "UP", 0, 1) + CreateArrowButton(mover, name, "BOTTOM", 0, 1, "DOWN", 0, -1) + CreateArrowButton(mover, name, "LEFT", 1, 0, "LEFT", -1, 0) + CreateArrowButton(mover, name, "RIGHT", -1, 0, "RIGHT", 1, 0) + + -- Hover state manager (tracks child enter/leave) + local hoverCount = 0 + function mover:_SetHover(isEnter) + if isEnter then + hoverCount = hoverCount + 1 + else + hoverCount = hoverCount - 1 + if hoverCount < 0 then hoverCount = 0 end + end + if hoverCount > 0 then + self:SetBackdropBorderColor(1, 1, 1, 0.95) + stripe:SetVertexColor(1, 1, 1, 1) + else + self:SetBackdropBorderColor(accent[1], accent[2], accent[3], 0.6) + stripe:SetVertexColor(accentLine[1], accentLine[2], accentLine[3], accentLine[4] or 0.9) + end + end + + mover:SetScript("OnDragStart", function() + this:StartMoving() + end) + + mover:SetScript("OnDragStop", function() + this:StopMovingOrSizing() + ApplySnap(this) + SyncFrameToMover(name) + end) + + mover:SetScript("OnClick", function() + if arg1 == "RightButton" then + M:ResetMover(name) + end + end) + + mover:SetScript("OnEnter", function() + this:_SetHover(true) + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:ClearLines() + GameTooltip:AddLine(entry.label or name, 1, 0.84, 0.94) + GameTooltip:AddDoubleLine("拖拽", "移动位置", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7) + GameTooltip:AddDoubleLine("箭头", "微调 1 像素", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7) + GameTooltip:AddDoubleLine("右键", "重置位置", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7) + GameTooltip:AddDoubleLine("Shift+拖拽", "禁用磁吸", 0.7, 0.7, 0.7, 0.7, 0.7, 0.7) + GameTooltip:Show() + end) + + mover:SetScript("OnLeave", function() + this:_SetHover(false) + GameTooltip:Hide() + end) + + mover:Hide() + moverFrames[name] = mover + return mover +end + +-------------------------------------------------------------------------------- +-- Dark overlay +-------------------------------------------------------------------------------- +local function CreateOverlay() + if overlayFrame then return overlayFrame end + + overlayFrame = CreateFrame("Frame", "SFramesLayoutOverlay", UIParent) + overlayFrame:SetFrameStrata("DIALOG") + overlayFrame:SetFrameLevel(0) + overlayFrame:SetAllPoints(UIParent) + overlayFrame:EnableMouse(false) + + local bg = overlayFrame:CreateTexture(nil, "BACKGROUND") + bg:SetTexture("Interface\\Buttons\\WHITE8x8") + bg:SetAllPoints(overlayFrame) + bg:SetVertexColor(0, 0, 0, 0.45) + overlayFrame._bg = bg + + overlayFrame:Hide() + return overlayFrame +end + +-------------------------------------------------------------------------------- +-- Grid overlay +-------------------------------------------------------------------------------- +local function CreateGrid() + if gridFrame then return gridFrame end + + gridFrame = CreateFrame("Frame", "SFramesLayoutGrid", UIParent) + gridFrame:SetFrameStrata("DIALOG") + gridFrame:SetFrameLevel(1) + gridFrame:SetAllPoints(UIParent) + gridFrame:EnableMouse(false) + gridFrame._lines = {} + + local th = T() + local axisColor = th.accent or { 1, 0.5, 0.8 } + local lineColor = th.dimText or { 0.5, 0.5, 0.55 } + + local function MakeLine(r, g, b, a) + local tex = gridFrame:CreateTexture(nil, "BACKGROUND") + tex:SetTexture("Interface\\Buttons\\WHITE8x8") + tex:SetVertexColor(r, g, b, a) + table.insert(gridFrame._lines, tex) + return tex + end + + local screenW = UIParent:GetRight() or UIParent:GetWidth() + local screenH = UIParent:GetTop() or UIParent:GetHeight() + local halfW = math.floor(screenW / 2) + local halfH = math.floor(screenH / 2) + + local cv = MakeLine(axisColor[1], axisColor[2], axisColor[3], 0.45) + cv:SetWidth(1) + cv:SetPoint("TOP", gridFrame, "TOP", 0, 0) + cv:SetPoint("BOTTOM", gridFrame, "BOTTOM", 0, 0) + + local ch = MakeLine(axisColor[1], axisColor[2], axisColor[3], 0.45) + ch:SetHeight(1) + ch:SetPoint("LEFT", gridFrame, "LEFT", 0, 0) + ch:SetPoint("RIGHT", gridFrame, "RIGHT", 0, 0) + + local i = GRID_SPACING + while i < halfW do + local vl = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10) + vl:SetWidth(1) + vl:SetPoint("TOP", gridFrame, "TOP", -i, 0) + vl:SetPoint("BOTTOM", gridFrame, "BOTTOM", -i, 0) + + local vr = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10) + vr:SetWidth(1) + vr:SetPoint("TOP", gridFrame, "TOP", i, 0) + vr:SetPoint("BOTTOM", gridFrame, "BOTTOM", i, 0) + i = i + GRID_SPACING + end + + i = GRID_SPACING + while i < halfH do + local ha = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10) + ha:SetHeight(1) + ha:SetPoint("LEFT", gridFrame, "LEFT", 0, i) + ha:SetPoint("RIGHT", gridFrame, "RIGHT", 0, i) + + local hb = MakeLine(lineColor[1], lineColor[2], lineColor[3], 0.10) + hb:SetHeight(1) + hb:SetPoint("LEFT", gridFrame, "LEFT", 0, -i) + hb:SetPoint("RIGHT", gridFrame, "RIGHT", 0, -i) + i = i + GRID_SPACING + end + + gridFrame:Hide() + return gridFrame +end + +-------------------------------------------------------------------------------- +-- Rounded button helper (for control bar) – matches UI-wide rounded style +-------------------------------------------------------------------------------- +local ROUND_BACKDROP = { + 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 function MakeControlButton(parent, text, width, xOfs, onClick) + local th = T() + local btnBg = th.buttonBg or { 0.16, 0.12, 0.22, 0.94 } + local btnBd = th.buttonBorder or { 0.35, 0.30, 0.50, 0.90 } + local btnHBg = th.buttonHoverBg or { 0.22, 0.18, 0.30, 0.96 } + local btnDnBg = th.buttonDownBg or { 0.08, 0.04, 0.06, 0.95 } + local btnText = th.buttonText or { 0.85, 0.82, 0.92 } + local btnHTxt = th.buttonActiveText or { 1, 0.92, 0.96 } + + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(width) + btn:SetHeight(26) + btn:SetPoint("LEFT", parent, "LEFT", xOfs, 0) + btn:SetBackdrop(ROUND_BACKDROP) + btn:SetBackdropColor(btnBg[1], btnBg[2], btnBg[3], btnBg[4] or 0.94) + btn:SetBackdropBorderColor(btnBd[1], btnBd[2], btnBd[3], btnBd[4] or 0.90) + + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(Font(), 10, "OUTLINE") + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text) + fs:SetTextColor(btnText[1], btnText[2], btnText[3], 1) + btn._text = fs + + btn:SetScript("OnClick", onClick) + btn:SetScript("OnEnter", function() + this:SetBackdropColor(btnHBg[1], btnHBg[2], btnHBg[3], btnHBg[4] or 0.96) + local a = th.accent or { 1, 0.5, 0.8 } + this:SetBackdropBorderColor(a[1], a[2], a[3], 0.95) + this._text:SetTextColor(btnHTxt[1], btnHTxt[2], btnHTxt[3], 1) + end) + btn:SetScript("OnLeave", function() + this:SetBackdropColor(btnBg[1], btnBg[2], btnBg[3], btnBg[4] or 0.94) + this:SetBackdropBorderColor(btnBd[1], btnBd[2], btnBd[3], btnBd[4] or 0.90) + this._text:SetTextColor(btnText[1], btnText[2], btnText[3], 1) + end) + btn:SetScript("OnMouseDown", function() + this:SetBackdropColor(btnDnBg[1], btnDnBg[2], btnDnBg[3], btnDnBg[4] or 0.95) + end) + btn:SetScript("OnMouseUp", function() + this:SetBackdropColor(btnHBg[1], btnHBg[2], btnHBg[3], btnHBg[4] or 0.96) + end) + + return btn +end + +-------------------------------------------------------------------------------- +-- Control bar +-------------------------------------------------------------------------------- +local function CreateControlBar() + if controlBar then return controlBar end + + local th = T() + local panelBg = th.headerBg or th.panelBg or { 0.08, 0.06, 0.12, 0.98 } + local panelBd = th.panelBorder or { 0.35, 0.30, 0.50, 0.90 } + local accent = th.accent or { 1, 0.5, 0.8, 0.98 } + local titleC = th.title or { 1, 0.88, 1 } + + controlBar = CreateFrame("Frame", "SFramesLayoutControlBar", UIParent) + controlBar:SetFrameStrata("FULLSCREEN_DIALOG") + controlBar:SetFrameLevel(200) + controlBar:SetWidth(480) + controlBar:SetHeight(44) + controlBar:SetPoint("TOP", UIParent, "TOP", 0, -8) + controlBar:SetClampedToScreen(true) + controlBar:SetMovable(true) + controlBar:EnableMouse(true) + controlBar:RegisterForDrag("LeftButton") + controlBar:SetScript("OnDragStart", function() this:StartMoving() end) + controlBar:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + controlBar:SetBackdrop(ROUND_BACKDROP) + controlBar:SetBackdropColor(panelBg[1], panelBg[2], panelBg[3], panelBg[4] or 0.98) + controlBar:SetBackdropBorderColor(panelBd[1], panelBd[2], panelBd[3], panelBd[4] or 0.90) + + local title = controlBar:CreateFontString(nil, "OVERLAY") + title:SetFont(Font(), 13, "OUTLINE") + title:SetPoint("LEFT", controlBar, "LEFT", 14, 0) + title:SetText("Nanami 布局") + title:SetTextColor(titleC[1], titleC[2], titleC[3], 1) + + local sep = controlBar:CreateTexture(nil, "ARTWORK") + sep:SetTexture("Interface\\Buttons\\WHITE8x8") + sep:SetWidth(1) + sep:SetHeight(24) + sep:SetPoint("LEFT", controlBar, "LEFT", 118, 0) + sep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.5) + + local bx = 128 + + -- Snap toggle + local snapBtn = MakeControlButton(controlBar, "", 76, bx, function() + local cfg = GetLayoutCfg() + cfg.snapEnabled = not cfg.snapEnabled + if controlBar._updateSnap then controlBar._updateSnap() end + end) + controlBar.snapBtn = snapBtn + + local function UpdateSnapBtnText() + local cfg = GetLayoutCfg() + local a2 = T().accent or { 1, 0.5, 0.8 } + if cfg.snapEnabled then + snapBtn._text:SetText("|cff66ee66开|r 磁吸") + snapBtn:SetBackdropBorderColor(a2[1], a2[2], a2[3], 0.7) + else + snapBtn._text:SetText("|cffee6666关|r 磁吸") + snapBtn:SetBackdropBorderColor(0.4, 0.2, 0.2, 0.7) + end + end + controlBar._updateSnap = UpdateSnapBtnText + + -- Grid toggle + local gridBtn = MakeControlButton(controlBar, "", 76, bx + 84, function() + local cfg = GetLayoutCfg() + cfg.showGrid = not cfg.showGrid + if controlBar._updateGrid then controlBar._updateGrid() end + if gridFrame then + if cfg.showGrid then gridFrame:Show() else gridFrame:Hide() end + end + end) + controlBar.gridBtn = gridBtn + + local function UpdateGridBtnText() + local cfg = GetLayoutCfg() + local a2 = T().accent or { 1, 0.5, 0.8 } + if cfg.showGrid then + gridBtn._text:SetText("|cff66ee66开|r 网格") + gridBtn:SetBackdropBorderColor(a2[1], a2[2], a2[3], 0.7) + else + gridBtn._text:SetText("|cffee6666关|r 网格") + gridBtn:SetBackdropBorderColor(0.4, 0.2, 0.2, 0.7) + end + end + controlBar._updateGrid = UpdateGridBtnText + + -- Reset all + local resetBtn = MakeControlButton(controlBar, "全部重置", 76, bx + 176, function() + M:ResetAllMovers() + end) + local wbGold = th.wbGold or { 1, 0.88, 0.55 } + resetBtn._text:SetTextColor(wbGold[1], wbGold[2], wbGold[3], 1) + + -- Close + local closeBtn = CreateFrame("Button", nil, controlBar) + closeBtn:SetWidth(60) + closeBtn:SetHeight(26) + closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, 0) + closeBtn:SetBackdrop(ROUND_BACKDROP) + closeBtn:SetBackdropColor(0.35, 0.08, 0.10, 0.95) + closeBtn:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90) + local closeText = closeBtn:CreateFontString(nil, "OVERLAY") + closeText:SetFont(Font(), 10, "OUTLINE") + closeText:SetPoint("CENTER", 0, 0) + closeText:SetText("关闭") + closeText:SetTextColor(1, 0.65, 0.65, 1) + + closeBtn:SetScript("OnClick", function() M:ExitLayoutMode() end) + closeBtn:SetScript("OnEnter", function() + this:SetBackdropColor(0.50, 0.12, 0.15, 0.98) + this:SetBackdropBorderColor(1, 0.35, 0.40, 1) + closeText:SetTextColor(1, 1, 1, 1) + end) + closeBtn:SetScript("OnLeave", function() + this:SetBackdropColor(0.35, 0.08, 0.10, 0.95) + this:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90) + closeText:SetTextColor(1, 0.65, 0.65, 1) + end) + closeBtn:SetScript("OnMouseDown", function() + this:SetBackdropColor(0.25, 0.04, 0.06, 0.98) + end) + closeBtn:SetScript("OnMouseUp", function() + this:SetBackdropColor(0.50, 0.12, 0.15, 0.98) + end) + controlBar.closeBtn = closeBtn + + controlBar:Hide() + return controlBar +end + +-------------------------------------------------------------------------------- +-- Register mover +-------------------------------------------------------------------------------- +function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, defaultRelPoint, defaultX, defaultY, onMoved) + if not name or not frame then return end + + registry[name] = { + frame = frame, + label = label or name, + defaultPoint = defaultPoint or "CENTER", + defaultRelativeTo = defaultRelativeTo or "UIParent", + defaultRelPoint = defaultRelPoint or "CENTER", + defaultX = defaultX or 0, + defaultY = defaultY or 0, + onMoved = onMoved, + } + + CreateMoverFrame(name, registry[name]) +end + +-------------------------------------------------------------------------------- +-- Enter / Exit layout mode +-------------------------------------------------------------------------------- +function M:EnterLayoutMode() + if isLayoutMode then return end + isLayoutMode = true + + if SFrames.ConfigUI and SFramesConfigPanel and SFramesConfigPanel:IsShown() then + SFramesConfigPanel:Hide() + end + + CreateOverlay() + CreateGrid() + CreateControlBar() + + overlayFrame:Show() + + local cfg = GetLayoutCfg() + if cfg.showGrid then gridFrame:Show() end + if controlBar._updateSnap then controlBar._updateSnap() end + if controlBar._updateGrid then controlBar._updateGrid() end + controlBar:Show() + + SFrames:Print(string.format( + "|cff66eeff[布局]|r UIScale=%.2f screenW=%.0f screenH=%.0f (GetRight=%.0f GetTop=%.0f)", + UIParent:GetEffectiveScale(), + UIParent:GetWidth(), UIParent:GetHeight(), + UIParent:GetRight() or 0, UIParent:GetTop() or 0)) + + for name, _ in pairs(registry) do + SyncMoverToFrame(name) + local mover = moverFrames[name] + if mover then mover:Show() end + end + + SFrames:Print("布局模式已开启 - 拖拽移动 | 箭头微调 | 右键重置 | Shift禁用磁吸") +end + +function M:ExitLayoutMode() + if not isLayoutMode then return end + isLayoutMode = false + + for name, _ in pairs(registry) do + SyncFrameToMover(name) + local mover = moverFrames[name] + if mover then mover:Hide() end + end + + if gridFrame then gridFrame:Hide() end + if controlBar then controlBar:Hide() end + if overlayFrame then overlayFrame:Hide() end + + SFrames:Print("布局模式已关闭 - 所有位置已保存") +end + +function M:ToggleLayoutMode() + if isLayoutMode then self:ExitLayoutMode() else self:EnterLayoutMode() end +end + +function M:IsLayoutMode() + return isLayoutMode +end + +-------------------------------------------------------------------------------- +-- Reset movers +-------------------------------------------------------------------------------- +function M:ResetMover(name) + local entry = registry[name] + if not entry then return end + + local positions = EnsurePositions() + positions[name] = nil + + local frame = entry.frame + if frame then + frame:ClearAllPoints() + local relTo = _G[entry.defaultRelativeTo] or UIParent + frame:SetPoint(entry.defaultPoint, relTo, entry.defaultRelPoint, + entry.defaultX, entry.defaultY) + end + + if entry.onMoved then entry.onMoved() end + if isLayoutMode then SyncMoverToFrame(name) end + + SFrames:Print((entry.label or name) .. " 位置已重置") +end + +function M:ResetAllMovers() + for name, _ in pairs(registry) do + local entry = registry[name] + local positions = EnsurePositions() + positions[name] = nil + + local frame = entry.frame + if frame then + frame:ClearAllPoints() + local relTo = _G[entry.defaultRelativeTo] or UIParent + frame:SetPoint(entry.defaultPoint, relTo, entry.defaultRelPoint, + entry.defaultX, entry.defaultY) + end + if entry.onMoved then entry.onMoved() end + end + + if isLayoutMode then + for name, _ in pairs(registry) do SyncMoverToFrame(name) end + end + + SFrames:Print("所有位置已重置为默认") +end + +-------------------------------------------------------------------------------- +-- Apply saved position to a frame (for modules to call on init) +-------------------------------------------------------------------------------- +function M:ApplyPosition(name, frame, defaultPoint, defaultRelTo, defaultRelPoint, defaultX, defaultY) + local positions = EnsurePositions() + local pos = positions[name] + if pos and pos.point and pos.relativePoint then + frame:ClearAllPoints() + frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) + return true + else + frame:ClearAllPoints() + local relFrame = (defaultRelTo and _G[defaultRelTo]) or UIParent + frame:SetPoint(defaultPoint or "CENTER", relFrame, defaultRelPoint or "CENTER", + defaultX or 0, defaultY or 0) + return false + end +end + +-------------------------------------------------------------------------------- +-- Utility +-------------------------------------------------------------------------------- +function M:GetRegistry() + return registry +end diff --git a/Nanami-UI.toc b/Nanami-UI.toc index c5f6796..3a82999 100644 --- a/Nanami-UI.toc +++ b/Nanami-UI.toc @@ -9,6 +9,7 @@ Bindings.xml Core.lua Config.lua +Movers.lua Media.lua IconMap.lua Factory.lua @@ -31,12 +32,14 @@ Units\Pet.lua Units\Target.lua Units\ToT.lua Units\Party.lua +TalentDefaultDB.lua Units\TalentTree.lua SellPriceDB.lua GearScore.lua Tooltip.lua Units\Raid.lua ActionBars.lua +KeyBindManager.lua Bags\Offline.lua Bags\Sort.lua @@ -47,11 +50,13 @@ Bags\Core.lua Merchant.lua Trade.lua Roll.lua +LootDisplay.lua QuestUI.lua BookUI.lua QuestLogSkin.lua TrainerUI.lua TradeSkillDB.lua +BeastTrainingUI.lua TradeSkillUI.lua CharacterPanel.lua StatSummary.lua diff --git a/PetStableSkin.lua b/PetStableSkin.lua index ac6cd25..53a41e3 100644 --- a/PetStableSkin.lua +++ b/PetStableSkin.lua @@ -437,7 +437,7 @@ local function ApplySkin() end end - -- 14b) Aggressive cleanup: clear backdrops/textures on ALL non-essential children + -- 14b) Cleanup: only target known decorative inset/background children local knownFrames = {} if PetStableModel then knownFrames[PetStableModel] = true end if PetStableFrameCloseButton then knownFrames[PetStableFrameCloseButton] = true end @@ -453,83 +453,15 @@ local function ApplySkin() for _, child in ipairs(allCh) do if not knownFrames[child] and not child.sfSkinned and not child.sfOverlay and not child.sfBorder then - NukeTextures(child) - if child.SetBackdrop then child:SetBackdrop(nil) end - local subCh = { child:GetChildren() } - for _, sc in ipairs(subCh) do - NukeTextures(sc) - if sc.SetBackdrop then sc:SetBackdrop(nil) end + local cName = child:GetName() or "" + if string.find(cName, "Inset") or string.find(cName, "Bg") + or string.find(cName, "Border") or string.find(cName, "Tab") then + NukeTextures(child) + if child.SetBackdrop then child:SetBackdrop(nil) end end end end - -- 15) Auto-compact: measure left padding, then apply uniformly to right & bottom - local frameTop = frame:GetTop() - local frameLeft = frame:GetLeft() - local frameRight = frame:GetRight() - local frameBot = frame:GetBottom() - if frameTop and frameLeft and frameRight and frameBot then - local contentLeft = frameRight - local contentRight = frameLeft - local contentBot = frameTop - local function Scan(obj) - if not obj:IsShown() then return end - local l = obj.GetLeft and obj:GetLeft() - local r = obj.GetRight and obj:GetRight() - local b = obj.GetBottom and obj:GetBottom() - if l and l < contentLeft then contentLeft = l end - if r and r > contentRight then contentRight = r end - if b and b < contentBot then contentBot = b end - end - local children = { frame:GetChildren() } - for _, child in ipairs(children) do - if not child.sfOverlay and not child.sfBorder then - Scan(child) - end - end - local regions = { frame:GetRegions() } - for _, r in ipairs(regions) do - if not r.sfNuked and not r.sfKeep then - Scan(r) - end - end - -- Also scan named Blizzard content elements directly - local contentNames = { - "PetStableCurrentPet", "PetStablePurchaseButton", - "PetStableModel", "PetStableFrameCloseButton", - } - for i = 1, 20 do - table.insert(contentNames, "PetStableStableSlot" .. i) - table.insert(contentNames, "PetStableSlot" .. i) - end - for _, n in ipairs(contentNames) do - local obj = _G[n] - if obj and obj.IsShown and obj:IsShown() then Scan(obj) end - end - -- Scan visible FontStrings on the frame (they are content) - for _, r in ipairs(regions) do - if r:IsObjectType("FontString") and r:IsShown() and r:GetText() - and r:GetText() ~= "" then - local l = r:GetLeft() - local ri = r:GetRight() - local b = r:GetBottom() - if l and l < contentLeft then contentLeft = l end - if ri and ri > contentRight then contentRight = ri end - if b and b < contentBot then contentBot = b end - end - end - local leftPad = contentLeft - frameLeft - if leftPad < 4 then leftPad = 4 end - if leftPad > 16 then leftPad = 16 end - local newW = (contentRight - frameLeft) + leftPad - local newH = (frameTop - contentBot) + leftPad - if newW < frame:GetWidth() then - frame:SetWidth(newW) - end - if newH < frame:GetHeight() then - frame:SetHeight(newH) - end - end end -------------------------------------------------------------------------------- diff --git a/SetupWizard.lua b/SetupWizard.lua index 380afd2..d7a8960 100644 --- a/SetupWizard.lua +++ b/SetupWizard.lua @@ -338,6 +338,7 @@ local function GetDefaultChoices() minimapShowClock = true, minimapShowCoords = true, minimapMapStyle = "auto", + minimapMapShape = "circle", buffEnabled = true, buffIconSize = 30, buffIconsPerRow = 8, @@ -395,6 +396,7 @@ local function GetCurrentChoices() if db.Minimap.showClock ~= nil then c.minimapShowClock = db.Minimap.showClock end if db.Minimap.showCoords ~= nil then c.minimapShowCoords = db.Minimap.showCoords end if db.Minimap.mapStyle ~= nil then c.minimapMapStyle = db.Minimap.mapStyle end + if db.Minimap.mapShape ~= nil then c.minimapMapShape = db.Minimap.mapShape end end if db.MinimapBuffs then if db.MinimapBuffs.enabled ~= nil then c.buffEnabled = db.MinimapBuffs.enabled end @@ -459,6 +461,7 @@ local function ApplyChoices() SFramesDB.Minimap.showClock = c.minimapShowClock SFramesDB.Minimap.showCoords = c.minimapShowCoords SFramesDB.Minimap.mapStyle = c.minimapMapStyle + SFramesDB.Minimap.mapShape = c.minimapMapShape if type(SFramesDB.MinimapBuffs) ~= "table" then SFramesDB.MinimapBuffs = {} end SFramesDB.MinimapBuffs.enabled = c.buffEnabled @@ -816,12 +819,122 @@ local function BuildExtras(page) MakeCheck(p, "显示坐标", x + 160, y, function() return choices.minimapShowCoords end, function(v) choices.minimapShowCoords = v end) + + local function GetMode() + local shape = choices.minimapMapShape or "circle" + if shape == "square1" or shape == "square2" then return "square" end + if (choices.minimapMapStyle or "auto") == "auto" then return "auto" end + return "round" + end + + local circleFrame = CreateFrame("Frame", nil, p) + circleFrame:SetPoint("TOPLEFT", p, "TOPLEFT", x, y - 50) + circleFrame:SetWidth(CONTENT_W) + circleFrame:SetHeight(56) + circleFrame:Hide() + + MakeLabel(circleFrame, "圆形边框:", 0, 0, 10, 0.78, 0.72, 0.76) + local mapStyles = SFrames.Minimap and SFrames.Minimap.MAP_STYLES or {} + local CS, CG = 28, 4 + local circleBtns = {} + + local autoStyleBtn = MakeButton(circleFrame, "自动", 38, CS, nil) + autoStyleBtn:ClearAllPoints() + autoStyleBtn:SetPoint("TOPLEFT", circleFrame, "TOPLEFT", 0, -16) + + local function RefreshCircleHighlight() + local cur = choices.minimapMapStyle or "auto" + local matched = (cur == "auto") + for _, b in ipairs(circleBtns) do + if b.styleKey == cur then + b:SetBackdropBorderColor(1, 0.78, 0.2, 1) + matched = true + else + b:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + end + end + if not matched then + choices.minimapMapStyle = "auto" + end + autoStyleBtn.sfActive = ((choices.minimapMapStyle or "auto") == "auto") + autoStyleBtn:RefreshVisual() + end + + autoStyleBtn:SetScript("OnClick", function() + choices.minimapMapStyle = "auto" + RefreshCircleHighlight() + PlaySound("igMainMenuOptionCheckBoxOn") + end) + + for idx, style in ipairs(mapStyles) do + local sb = CreateFrame("Button", WN("CS"), circleFrame) + sb:SetWidth(CS); sb:SetHeight(CS) + sb:SetPoint("TOPLEFT", circleFrame, "TOPLEFT", + 42 + (idx - 1) * (CS + CG), -16) + sb:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 8, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + sb:SetBackdropColor(0.08, 0.08, 0.1, 0.85) + + local ptex = sb:CreateTexture(nil, "ARTWORK") + ptex:SetTexture(style.tex) + ptex:SetPoint("CENTER") + ptex:SetWidth(CS - 4); ptex:SetHeight(CS - 4) + + sb.styleKey = style.key + sb._sfLabel = style.label + sb:SetScript("OnClick", function() + choices.minimapMapStyle = this.styleKey + RefreshCircleHighlight() + PlaySound("igMainMenuOptionCheckBoxOn") + end) + sb:SetScript("OnEnter", function() + this:SetBackdropBorderColor(0.7, 0.7, 0.7, 1) + GameTooltip:SetOwner(this, "ANCHOR_TOP") + GameTooltip:SetText(this._sfLabel) + GameTooltip:Show() + end) + sb:SetScript("OnLeave", function() + local cur = choices.minimapMapStyle or "auto" + if this.styleKey == cur then + this:SetBackdropBorderColor(1, 0.78, 0.2, 1) + else + this:SetBackdropBorderColor(0.3, 0.3, 0.35, 1) + end + GameTooltip:Hide() + end) + + table.insert(circleBtns, sb) + end + RefreshCircleHighlight() + MakeLabel(p, "地图风格:", x, y - 26, 10, 0.78, 0.72, 0.76) MakeBtnGroup(p, x + 60, y - 24, { {key="auto", label="自动", w=50}, {key="round", label="圆形", w=50}, {key="square", label="方形", w=50} }, - function() return choices.minimapMapStyle end, - function(v) choices.minimapMapStyle = v end) - return 54 + GetMode, + function(v) + if v == "auto" then + choices.minimapMapShape = "circle" + choices.minimapMapStyle = "auto" + circleFrame:Hide() + elseif v == "round" then + choices.minimapMapShape = "circle" + if choices.minimapMapStyle == "auto" then + choices.minimapMapStyle = "map" + end + RefreshCircleHighlight() + circleFrame:Show() + elseif v == "square" then + choices.minimapMapShape = "square1" + circleFrame:Hide() + end + end) + + if GetMode() == "round" then circleFrame:Show() end + return 110 end, "worldmap") AddFeature("Buff 栏", "自定义 Buff/Debuff 显示", "buffEnabled", function(p, x, y) diff --git a/SocialUI.lua b/SocialUI.lua index a70e829..64eab7a 100644 --- a/SocialUI.lua +++ b/SocialUI.lua @@ -516,12 +516,24 @@ local origFriendsFrameShow local function HideBlizzardFriends() if not FriendsFrame then return end origFriendsFrameShow = FriendsFrame.Show + FriendsFrame:Hide() FriendsFrame:SetAlpha(0) FriendsFrame:EnableMouse(false) FriendsFrame:ClearAllPoints() FriendsFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMRIGHT", 2000, 2000) + FriendsFrame:UnregisterAllEvents() FriendsFrame.Show = function() end + if UIPanelWindows then + UIPanelWindows["FriendsFrame"] = nil + end + + for i = table.getn(UISpecialFrames), 1, -1 do + if UISpecialFrames[i] == "FriendsFrame" then + table.remove(UISpecialFrames, i) + end + end + if SetWhoToUI then local origSetWhoToUI = SetWhoToUI SetWhoToUI = function(flag) @@ -1248,8 +1260,82 @@ end -------------------------------------------------------------------------------- -- Tab 3: Guild -------------------------------------------------------------------------------- +local motdPopup = nil +local function ShowMotdPopup(motdStr) + if not motdPopup then + local f = CreateFrame("Frame", "SFramesSocialMotdPopup", UIParent) + f:SetWidth(360) + f:SetHeight(220) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 60) + f:SetFrameStrata("DIALOG") + f:SetFrameLevel(120) + SetRoundBackdrop(f, T.panelBg, T.panelBorder) + CreateShadow(f) + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + local titleFS = MakeFS(f, 12, "CENTER", T.gold) + titleFS:SetPoint("TOP", f, "TOP", 0, -10) + titleFS:SetText("公会公告") + f.titleFS = titleFS + + MakeSep(f, -26) + + local scrollFrame = CreateFrame("ScrollFrame", NextName("MotdScroll"), f) + scrollFrame:SetPoint("TOPLEFT", f, "TOPLEFT", 12, -30) + scrollFrame:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -12, 40) + + local child = CreateFrame("Frame", nil, scrollFrame) + child:SetWidth(336 - 24) + child:SetHeight(1) + scrollFrame:SetScrollChild(child) + + local bodyFS = child:CreateFontString(nil, "OVERLAY") + bodyFS:SetFont(GetFont(), 11, "OUTLINE") + bodyFS:SetJustifyH("LEFT") + bodyFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3]) + bodyFS:SetPoint("TOPLEFT", child, "TOPLEFT", 0, 0) + bodyFS:SetWidth(336 - 24) + f.bodyFS = bodyFS + + local scrollOffset = 0 + local scrollMax = 0 + + f.UpdateScroll = function() + local textH = bodyFS:GetHeight() or 0 + local viewH = scrollFrame:GetHeight() or 1 + scrollMax = math.max(0, textH - viewH) + if scrollOffset > scrollMax then scrollOffset = scrollMax end + scrollFrame:SetVerticalScroll(scrollOffset) + child:SetHeight(math.max(textH, viewH)) + end + + scrollFrame:EnableMouseWheel(true) + scrollFrame:SetScript("OnMouseWheel", function() + scrollOffset = scrollOffset - (arg1 or 0) * 18 + if scrollOffset < 0 then scrollOffset = 0 end + if scrollOffset > scrollMax then scrollOffset = scrollMax end + scrollFrame:SetVerticalScroll(scrollOffset) + end) + + local closeBtn = MakeButton(f, "关闭", 100, 24) + closeBtn:SetPoint("BOTTOM", f, "BOTTOM", 0, 10) + closeBtn:SetScript("OnClick", function() f:Hide() end) + + f:Hide() + motdPopup = f + end + + motdPopup.bodyFS:SetText(motdStr or "无公告") + motdPopup:Show() + motdPopup.UpdateScroll() +end + local function BuildGuildPage(page) - local motdFrame = CreateFrame("Frame", nil, page) + local motdFrame = CreateFrame("Button", NextName("MotdBtn"), page) motdFrame:SetHeight(36) motdFrame:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0) motdFrame:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, 0) @@ -1257,13 +1343,24 @@ local function BuildGuildPage(page) local motdLabel = MakeFS(motdFrame, 9, "LEFT", T.dimText) motdLabel:SetPoint("TOPLEFT", motdFrame, "TOPLEFT", 6, -2) - motdLabel:SetText("公会公告:") + motdLabel:SetText("公会公告: (点击查看完整)") local motdText = MakeFS(motdFrame, 10, "LEFT", T.bodyText) motdText:SetPoint("TOPLEFT", motdLabel, "BOTTOMLEFT", 0, -2) motdText:SetPoint("RIGHT", motdFrame, "RIGHT", -6, 0) page.motdText = motdText + motdFrame:SetScript("OnClick", function() + local motd = GetGuildRosterMOTD and GetGuildRosterMOTD() or "" + ShowMotdPopup(motd ~= "" and motd or "无公告") + end) + motdFrame:SetScript("OnEnter", function() + SetPixelBackdrop(this, { 0.12, 0.06, 0.09, 0.9 }, T.tabActiveBorder) + end) + motdFrame:SetScript("OnLeave", function() + SetPixelBackdrop(this, { 0.08, 0.04, 0.06, 0.8 }, T.slotBorder) + end) + -- Toolbar row: search + filter buttons local toolBar = CreateFrame("Frame", nil, page) toolBar:SetHeight(18) @@ -2459,6 +2556,9 @@ function SUI:Initialize() local wasPending = whoQueryPending whoQueryPending = false if SetWhoToUI then SetWhoToUI(1) end + if FriendsFrame and FriendsFrame:IsShown() then + FriendsFrame:Hide() + end if MainFrame and MainFrame:IsShown() and currentMainTab == 2 then WhoDebug("更新列表显示") SUI:UpdateWhoList() diff --git a/TalentDefaultDB.lua b/TalentDefaultDB.lua new file mode 100644 index 0000000..558a9bb --- /dev/null +++ b/TalentDefaultDB.lua @@ -0,0 +1,6266 @@ +-- Nanami-UI Default Talent Database +-- Pre-cached talent tree data for all 9 classes +-- Generated via /nui talentdb export after logging in to each class +-- When a player logs in with a class, live API data takes priority over this DB + +-- Auto-generated by /nui talentdb export +-- Paste this entire content into TalentDefaultDB.lua + +NanamiTalentDefaultDB = { + DRUID = { + [1] = { + background = "DruidBalance", + icon = "Interface\\Icons\\Spell_Nature_Lightning", + name = "平衡", + numTalents = 20, + talents = { + { + column = 1, + desc = { + "等级 0/5", + "使你的愤怒法术的施法时间和公共冷却时间减少0.1秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_AbolishMagic", + maxRank = 5, + name = "强化愤怒", + tier = 1, + }, + { + column = 2, + desc = { + "等级 1/1", + "50法力值", + "瞬发法术", + "激活之后,任何击中施法者的敌人都有100%的几率被施展纠缠根须(等级 1)。只能在户外使用,可生效1次,持续45秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_NaturesWrath", + maxRank = 1, + name = "自然之握", + tier = 1, + }, + { + column = 3, + desc = { + "等级 4/4", + "使你的自然之握纠缠敌人的几率提高65%。", + }, + icon = "Interface\\Icons\\Spell_Nature_NaturesWrath", + maxRank = 4, + name = "强化自然之握", + prereqColumn = 2, + prereqTier = 1, + tier = 1, + }, + { + column = 4, + desc = { + "等级 0/2", + "杀死一个可获得经验或荣誉的目标后,有50%的几率使你在施法时仍保持100%的法力恢复速度,持续15秒。", + }, + icon = "Interface\\Icons\\INV_Misc_Gem_Emerald_01", + maxRank = 2, + name = "林栖者的祝福", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/3", + "使你在施放平衡系法术时有23%的几率不会因受到伤害而延迟。", + }, + icon = "Interface\\Icons\\Spell_Nature_Sleep", + maxRank = 3, + name = "梦境指引", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/2", + "使你的月火术的伤害和致命一击几率提高5%。", + }, + icon = "Interface\\Icons\\Spell_Nature_StarFall", + maxRank = 2, + name = "强化月火术", + tier = 2, + }, + { + column = 3, + desc = { + "等级 1/3", + "使你在所有形态下的物理攻击所能造成的伤害提高3%。同时近战攻击和法术命中几率提高1%。", + " ", + "下一级:", + "使你在所有形态下的物理攻击所能造成的伤害提高6%。同时近战攻击和法术命中几率提高2%。", + }, + icon = "Interface\\Icons\\INV_Staff_01", + maxRank = 3, + name = "武器平衡", + tier = 2, + }, + { + column = 4, + desc = { + "等级 0/3", + "使你的所有变形法术所消耗的法力值降低10%。", + }, + icon = "Interface\\Icons\\Spell_Nature_WispSplode", + maxRank = 3, + name = "自然变形", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要10点平衡系天赋", + "使你的星火术、月火术、飓风、虫群和愤怒所能造成的伤害提高3%。", + }, + icon = "Interface\\Icons\\Spell_Nature_MoonGlow", + maxRank = 3, + name = "月怒", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要3点武器平衡", + "需要10点平衡系天赋", + "以自然的力量强化德鲁伊的武器,每次近战攻击或攻击性法术命中敌人都有一定几率令德鲁伊进入节能施法状态。该状态可以让你的下一个伤害法术、治疗法术或攻击技能所消耗的法力值、怒气值或能量值降低100%。", + }, + icon = "Interface\\Icons\\Spell_Nature_CrystalBall", + maxRank = 1, + name = "清晰预兆", + prereqColumn = 3, + prereqTier = 2, + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要10点平衡系天赋", + "使你的愤怒、纠缠根须、精灵之火、月火术、星火术、虫群、飓风、解除诅咒、驱毒术和消毒术的射程增加10%。", + }, + icon = "Interface\\Icons\\Spell_Nature_NatureTouchGrow", + maxRank = 2, + name = "自然延伸", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要2点强化月火术", + "需要15点平衡系天赋", + "使你的星火术、月火术和愤怒的致命一击伤害提高20%。", + }, + icon = "Interface\\Icons\\Spell_Nature_Purge", + maxRank = 5, + name = "复仇", + prereqColumn = 2, + prereqTier = 2, + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要15点平衡系天赋", + "使你的月火术、星火术、愤怒、飓风、虫群、治疗之触、愈合和回春术所消耗的法力值减少3%。", + }, + icon = "Interface\\Icons\\Spell_Nature_Sentinal", + maxRank = 3, + name = "月光", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要1点枭兽形态", + "需要20点平衡系天赋", + "在枭兽形态下受到攻击有10%的几率进入狂怒状态,使你在施法时有30%的几率不会因受到伤害而延迟,并每秒恢复1%最大法力值,持续10秒。此效果每30秒只能触发一次。", + }, + icon = "Interface\\Icons\\Ability_Druid_OwlkinFrenzy", + maxRank = 3, + name = "枭兽狂怒", + prereqColumn = 2, + prereqTier = 5, + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点平衡系天赋", + "164法力值", + "瞬发法术", + "德鲁伊进入枭兽形态,在这种形态下,从装备提供的护甲值提高180%,平衡系法术的法力消耗减少20%,半径30码范围内的所有队友的法术致命一击率提高3%。枭兽形态下只能施放平衡系的法术、激活和解除诅咒。变身可以解除施法者身上的所有变形和移动限制效果。", + }, + icon = "Interface\\Icons\\Spell_Nature_ForceOfNature", + maxRank = 1, + name = "枭兽形态", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要3点月光", + "需要20点平衡系天赋", + "你的任何法术造成法术致命一击后,下一个法术的施法时间减少0.5秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_NaturesBlessing", + maxRank = 1, + name = "自然之赐", + prereqColumn = 3, + prereqTier = 4, + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要20点平衡系天赋", + "使你的星火术的施法时间减少0.2秒,有5%的几率将目标击昏3秒。", + }, + icon = "Interface\\Icons\\Spell_Arcane_StarFire", + maxRank = 3, + name = "强化星火术", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要25点平衡系天赋", + "你的虫群有6%的几率使你的下一个星火术的施法时间减少0秒。\n你的月火术有6%的几率使你的下一个愤怒的法力值消耗减少50%。", + }, + icon = "Interface\\Icons\\Ability_Druid_ManaTree", + maxRank = 5, + name = "万物平衡", + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要25点平衡系天赋", + "你的飓风法术的法力值消耗降低10%,并使敌人的攻击速度降低12%。", + }, + icon = "Interface\\Icons\\Ability_Druid_GaleWinds", + maxRank = 2, + name = "阵风", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要30点平衡系天赋", + "协调自然和星界的能量,你的愤怒造成伤害时有30%的几率使你获得月蚀并提高奥术伤害;星火术造成伤害时有50%的几率使你获得日蚀并提高自然伤害。伤害加成为10%加上你的法术致命一击几率的60%。每种效果持续15秒,并有单独的30秒冷却时间,两种效果不能同时发生。", + }, + icon = "Interface\\Icons\\Ability_Druid_Eclipse", + maxRank = 1, + name = "日月之蚀", + tier = 7, + }, + }, + }, + [2] = { + background = "DruidFeralCombat", + icon = "Interface\\Icons\\Ability_Physical_Taunt", + name = "野性战斗", + numTalents = 18, + talents = { + { + column = 2, + desc = { + "等级 5/5", + "使你的槌击、挥击、野蛮撕咬、爪击和扫击技能的怒气或能量消耗减少5点。", + }, + icon = "Interface\\Icons\\Ability_Hunter_Pet_Hyena", + maxRank = 5, + name = "凶暴", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你的挫志咆哮的攻击强度减弱效果提高8%,凶猛撕咬技能所造成的伤害提高3%。", + }, + icon = "Interface\\Icons\\Ability_Druid_DemoralizingRoar", + maxRank = 5, + name = "野性侵略", + tier = 1, + }, + { + column = 1, + desc = { + "等级 3/3", + "使你在熊和巨熊形态下所造成的威胁值提高15%,并在你潜行时降低敌人侦测到你的几率。", + }, + icon = "Interface\\Icons\\Ability_Ambush", + maxRank = 3, + name = "野性本能", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/2", + "使你的重击和突袭技能的击昏效果持续时间延长0.5秒。", + }, + icon = "Interface\\Icons\\Ability_Druid_Bash", + maxRank = 2, + name = "野蛮冲撞", + tier = 2, + }, + { + column = 3, + desc = { + "等级 3/3", + "使你由装备而得到的护甲值提高10%。", + }, + icon = "Interface\\Icons\\INV_Misc_Pelt_Bear_03", + maxRank = 3, + name = "厚皮", + tier = 2, + }, + { + column = 4, + desc = { + "等级 0/3", + "撕扯的伤害提高5%,你对目标造成的每个流血效果都会使爪击的伤害提高10%。", + }, + icon = "Interface\\Icons\\Ability_Druid_Disembowel", + maxRank = 3, + name = "迸裂创伤", + tier = 2, + }, + { + column = 1, + desc = { + "等级 2/2", + "使你在猎豹形态下的移动速度提高30%,只能在户外生效。另外,还可使你在熊、巨熊、猎豹形态下的躲闪几率提高4%。", + }, + icon = "Interface\\Icons\\Spell_Nature_SpiritWolf", + maxRank = 2, + name = "野性迅捷", + tier = 3, + }, + { + column = 2, + desc = { + "等级 1/1", + "5怒气", + "瞬发", + "需要熊形态, 巨熊形态", + "向目标冲锋,使其停止动作,并使其在4秒内不能施放任何法术。", + }, + icon = "Interface\\Icons\\Ability_Hunter_Pet_Bear", + maxRank = 1, + name = "野性冲锋", + tier = 3, + }, + { + column = 3, + desc = { + "等级 3/3", + "使你在猎豹、熊或巨熊形态下的致命一击几率提高6%。", + }, + icon = "Interface\\Icons\\INV_Misc_MonsterClaw_04", + maxRank = 3, + name = "锋利兽爪", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/2", + "使你在熊形态和巨熊形态下对目标造成致命一击后,有50%的几率获得5点怒气值,猎豹形态下对目标造成致命一击后额外增加一个连击点数。", + }, + icon = "Interface\\Icons\\Ability_Racial_Cannibalize", + maxRank = 2, + name = "原始狂怒", + prereqColumn = 3, + prereqTier = 3, + tier = 3, + }, + { + column = 2, + desc = { + "等级 3/3", + "使你在猎豹、熊和巨熊形态下的近战攻击强度提高10%。你的爪击、扫击、槌击、挥击和野蛮撕咬的伤害提高20%。", + }, + icon = "Interface\\Icons\\Ability_Hunter_Pet_Cat", + maxRank = 3, + name = "猛兽攻击", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/2", + "使猛虎之怒的持续时间延长6秒,狂怒现在会立即产生5点怒气值。此外,猛虎之怒和狂怒会使你的攻击速度提高10%,持续9秒。", + }, + icon = "Interface\\Icons\\Ability_GhoulFrenzy", + maxRank = 2, + name = "血性狂乱", + prereqColumn = 3, + prereqTier = 3, + tier = 4, + }, + { + column = 4, + desc = { + "等级 0/2", + "撕碎的伤害提高5%,消耗的能量值降低6点。", + }, + icon = "Interface\\Icons\\Spell_Shadow_VampiricAura", + maxRank = 2, + name = "强化撕碎", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/2", + "在猎豹形态下,你造成的流血效果的周期性伤害将为你恢复3能量。在熊或巨熊形态下闪避攻击将为你注入远古灵魂,每秒恢复2怒气值,持续5秒。此效果每9秒只能发生一次。", + }, + icon = "Interface\\Icons\\Spell_Shadow_UnholyFrenzy", + maxRank = 2, + name = "远古蛮力", + tier = 5, + }, + { + column = 3, + desc = { + "等级 1/1", + "瞬发", + "需要豹形态, 熊形态, 巨熊形态", + "猎豹形态下移除所有恐惧效果并提高100%能量回复。熊形态下提高20%生命值上限,效果结束扣除生命值。持续时间20秒。", + }, + icon = "Interface\\Icons\\Ability_Druid_Berserk", + maxRank = 1, + name = "狂暴", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要25点野性战斗系天赋", + "使你的智力提高4%。另外,在熊或巨熊形态下,你的耐力提高4%,在猎豹形态下,你的力量提高4%。", + }, + icon = "Interface\\Icons\\Spell_Holy_BlessingOfAgility", + maxRank = 5, + name = "野性之心", + prereqColumn = 2, + prereqTier = 4, + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要25点野性战斗系天赋", + "你的槌击、挥击和野蛮撕咬会将造成伤害的5%作为治疗返还。你的凶猛撕咬每消耗一个连击点数,就有10%的几率刷新你的扫击和撕扯效果,并额外增加一个连击点数。", + }, + icon = "Interface\\Icons\\Ability_Druid_Ravage", + maxRank = 2, + name = "屠戮", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要30点野性战斗系天赋", + "在猎豹、熊或巨熊形态下,使半径45码范围内的所有小队成员的远程和近战攻击打出致命一击的几率提高3%。", + }, + icon = "Interface\\Icons\\Spell_Nature_UnyeildingStamina", + maxRank = 1, + name = "兽群领袖", + tier = 7, + }, + }, + }, + [3] = { + background = "DruidRestoration", + icon = "Interface\\Icons\\Spell_Nature_HealingTouch", + name = "恢复", + numTalents = 16, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使你的野性印记和野性赐福的效果提高7%。", + }, + icon = "Interface\\Icons\\Spell_Nature_Regeneration", + maxRank = 5, + name = "强化野性印记", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "在你有20%的几率在进入熊形态和巨熊形态时获得10点怒气值,或者在进入猎豹形态时获得40点能量值。", + }, + icon = "Interface\\Icons\\Spell_Holy_BlessingOfStamina", + maxRank = 5, + name = "激怒", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/5", + "需要5点恢复系天赋", + "使你的治疗之触的施法时间减少0.1秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_HealingTouch", + maxRank = 5, + name = "强化治疗之触", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要5点恢复系天赋", + "使你在施放愈合、治疗之触或宁静时有14%的几率不因受到伤害而延迟。", + }, + icon = "Interface\\Icons\\Spell_Nature_HealingWaveGreater", + maxRank = 5, + name = "自然集中", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点恢复系天赋", + "使你的法术所造成的威胁值减少4%。", + }, + icon = "Interface\\Icons\\Ability_EyeOfTheOwl", + maxRank = 5, + name = "微妙", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要10点恢复系天赋", + "119法力值", + "瞬发法术", + "吞噬友方目标身上的一个回春术或愈合的持续效果,并立即为其回复生命值,其数值等于12秒的回春效果或18秒的愈合效果。", + }, + icon = "Interface\\Icons\\INV_Relics_IdolofRejuvenation", + maxRank = 1, + name = "迅捷治愈", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要10点恢复系天赋", + "使你法术技能的持续伤害效果和持续治疗效果提高5%。", + }, + icon = "Interface\\Icons\\btnorbofdepths", + maxRank = 3, + name = "起源", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要10点恢复系天赋", + "使你在施法时仍保持5%的法力回复速度。", + }, + icon = "Interface\\Icons\\Spell_Frost_WindWalkOn", + maxRank = 3, + name = "反射", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要15点恢复系天赋", + "使你的所有治疗法术的效果提高2%。", + }, + icon = "Interface\\Icons\\Spell_Nature_ProtectionformNature", + maxRank = 5, + name = "自然赐福", + tier = 4, + }, + { + column = 4, + desc = { + "等级 0/5", + "需要15点恢复系天赋", + "使你的治疗之触、愈合和宁静所消耗的法力值减少2%。", + }, + icon = "Interface\\Icons\\Spell_Holy_ElunesGrace", + maxRank = 5, + name = "宁静之魂", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要5点强化治疗之触", + "需要20点恢复系天赋", + "使用治疗之触治疗受到愈合或回春术影响的目标,将使你的下一个治疗之触的施法时间减少0.15秒,并返还其法力消耗的5%,持续20秒。", + }, + icon = "Interface\\Icons\\inv_misc_herb_02", + maxRank = 2, + name = "艾森娜之花", + prereqColumn = 1, + prereqTier = 2, + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要3点起源", + "需要20点恢复系天赋", + "瞬发", + "激活之后,你的下一个自然法术会成为瞬发法术。", + }, + icon = "Interface\\Icons\\Spell_Nature_RavenForm", + maxRank = 1, + name = "自然迅捷", + prereqColumn = 3, + prereqTier = 3, + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要20点恢复系天赋", + "如果目标身上存在回春效果,愈合的持续治疗效果增加10%。", + }, + icon = "Interface\\Icons\\inv_relics_idolofhealth", + maxRank = 3, + name = "庇佑", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要5点自然赐福", + "需要25点恢复系天赋", + "使你的愈合法术产生极效治疗效果的几率提高10%。", + }, + icon = "Interface\\Icons\\Spell_Nature_ResistNature", + maxRank = 5, + name = "强化愈合", + prereqColumn = 2, + prereqTier = 4, + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要25点恢复系天赋", + "使你的宁静法术的治疗效果提高20%。", + }, + icon = "Interface\\Icons\\Spell_Nature_Tranquility", + maxRank = 2, + name = "强化宁静", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要5点强化愈合", + "需要30点恢复系天赋", + "209法力值", + "瞬发法术", + "\"变成生命之树,由装备而获得的护甲值提高180%,附近所有队友的治疗效果提高,数值相当于你的精神总值的20%。你的移动速度降低20%,并且你不能施放伤害性法术或治疗之触,但持续性治疗法术所消耗的法力值降低20%。\n\n变身可以解除施法者身上的一切变形和移动限制效果。\"", + }, + icon = "Interface\\Icons\\Ability_Druid_TreeofLife", + maxRank = 1, + name = "生命之树形态", + prereqColumn = 2, + prereqTier = 6, + tier = 7, + }, + }, + }, + numTabs = 3, + }, + HUNTER = { + [1] = { + background = "HunterBeastMastery", + icon = "Interface\\Icons\\Ability_Hunter_BeastTaming", + name = "野兽控制", + numTalents = 19, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "当雄鹰守护激活时,所有普通远程攻击都有10%的几率使你的远程攻击速度提高3%,持续12秒。当孤狼守护激活时,所有普通近战攻击都有10%的几率使你的近战攻击速度提高3%,持续12秒。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Nature_RavenForm", + maxRank = 5, + name = "迅捷守护", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "你的宠物继承你的角色耐力的6%,并使你的宠物的生命值提高2%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Nature_Reincarnation", + maxRank = 5, + name = "耐久训练", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要5点野兽控制系天赋", + "使你的野兽之眼的效果持续时间延长30秒。\n\n在野兽之眼引导期间,你宠物造成的伤害额外提高15%,并且集中值的回复速度提高15%。", + }, + icon = "Interface\\Icons\\Ability_EyeOfTheOwl", + maxRank = 2, + name = "强化野兽之眼", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要5点野兽控制系天赋", + "使你的灵猴守护提供2%的额外躲闪几率。当孤狼守护激活时,你近战伤害的2%作为治疗返还。", + }, + icon = "Interface\\Icons\\Ability_Hunter_AspectOfTheMonkey", + maxRank = 3, + name = "强化原始守护", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要5点野兽控制系天赋", + "你的宠物继承你12%的护甲值,并使你的宠物的护甲值提高7%。", + }, + icon = "Interface\\Icons\\INV_Misc_Pelt_Bear_03", + maxRank = 3, + name = "厚皮", + tier = 2, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要5点野兽控制系天赋", + "使你的复活宠物法术的施法时间减少3秒,法力值消耗降低20%,宠物复活后的生命值提高15%。", + }, + icon = "Interface\\Icons\\Ability_Hunter_BeastSoothe", + maxRank = 2, + name = "强化复活宠物", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要10点野兽控制系天赋", + "使你的猎豹守护和豹群守护的速度加成效果提高3%。并使你的宠物的移动速度提高15%。", + }, + icon = "Interface\\Icons\\Ability_Mount_JungleTiger", + maxRank = 2, + name = "寻路", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要10点野兽控制系天赋", + "你的奥术射击、稳固射击或猛禽一击命中后,使你的宠物在6秒内的下一次攻击额外造成相当于你自身攻击强度20%的物理伤害,此效果每3秒只能触发一次。", + }, + icon = "Interface\\Icons\\spell_coordinated_assault_1", + maxRank = 1, + name = "协同突袭", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要10点野兽控制系天赋", + "使你的宠物所造成的伤害提高4%。", + }, + icon = "Interface\\Icons\\Ability_BullRush", + maxRank = 5, + name = "狂怒释放", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要15点野兽控制系天赋", + "使你的宠物的集中值回复速度每一跳增加2点,宠物特殊技能的冷却时间减少10%。", + }, + icon = "Interface\\Icons\\Spell_Nature_AbolishMagic", + maxRank = 2, + name = "野兽戒律", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要15点野兽控制系天赋", + "使你的治疗宠物技能的效果提高20%,并且每一跳都有15%的几率驱散宠物身上的1个诅咒、疾病、魔法或中毒效果。", + }, + icon = "Interface\\Icons\\Ability_Hunter_MendPet", + maxRank = 2, + name = "强化治疗宠物", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要15点野兽控制系天赋", + "使你的宠物打出致命一击的几率提高3%。", + }, + icon = "Interface\\Icons\\INV_Misc_MonsterClaw_04", + maxRank = 5, + name = "凶暴", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要20点野兽控制系天赋", + "你的攻击有5%的几率使你的宠物进入狂怒状态,使其造成40%的额外伤害,持续8秒。", + }, + icon = "Interface\\Icons\\ability_hunter_goforthethroat", + maxRank = 3, + name = "血之气息", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要3点血之气息", + "需要20点野兽控制系天赋", + "206法力值", + "瞬发法术", + "使宠物获得血之气息效果并进入疯狂状态,持续18秒。在这种状态下,宠物不会有任何恐惧或怜悯,也无法停止下来,除非被杀死。", + }, + icon = "Interface\\Icons\\Ability_Druid_FerociousBite", + maxRank = 1, + name = "狂野怒火", + prereqColumn = 1, + prereqTier = 5, + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/1", + "需要20点野兽控制系天赋", + "137法力值", + "瞬发法术", + "命令你的宠物在下次击中敌人时进行胁迫,使目标昏迷3秒。并使宠物的威胁值产生速度提高50%,持续8秒。", + }, + icon = "Interface\\Icons\\Ability_Devour", + maxRank = 1, + name = "胁迫", + tier = 5, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要25点野兽控制系天赋", + "使你的宠物物理命中几率提高4%,法术命中几率提高9%,宠物的近战技能等级提高5点。", + }, + icon = "Interface\\Icons\\Ability_Druid_Rake", + maxRank = 2, + name = "野兽精准", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要25点野兽控制系天赋", + "你的宠物获得相当于你远程攻击强度12%的近战攻击强度,相当于你远程攻击强度7%的法术伤害和治疗效果。当你的宠物被激活后,你和你的宠物都会每5秒回复1%的生命值。", + }, + icon = "Interface\\Icons\\Ability_Druid_DemoralizingRoar", + maxRank = 2, + name = "灵魂联结", + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点凶暴", + "需要25点野兽控制系天赋", + "使你的宠物有20%的几率在对敌人造成致命一击后获得攻击速度提高30%的效果,持续8秒。", + }, + icon = "Interface\\Icons\\INV_Misc_MonsterClaw_03", + maxRank = 5, + name = "狂乱", + prereqColumn = 3, + prereqTier = 4, + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要2点灵魂联结", + "需要30点野兽控制系天赋", + "86法力值", + "瞬发法术", + "命令你的宠物立即发动攻击,造成其攻击强度80%的伤害,只能在猎人对目标造成致命一击后使用。", + }, + icon = "Interface\\Icons\\ability_hunter_killcommand", + maxRank = 1, + name = "杀戮命令", + prereqColumn = 2, + prereqTier = 6, + tier = 7, + }, + }, + }, + [2] = { + background = "HunterMarksmanship", + icon = "Interface\\Icons\\Ability_Marksmanship", + name = "射击", + numTalents = 16, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使你的震荡射击有4%的几率令目标昏迷3秒。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Frost_Stun", + maxRank = 5, + name = "强化震荡射击", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你施放射击和钉刺技能所消耗的法力值减少2%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Frost_WizardMark", + maxRank = 5, + name = "效率", + tier = 1, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要5点射击系天赋", + "毒蛇钉刺所造成的伤害提高6%,蝰蛇钉刺抽取法力值的效果提高1%,毒蝎钉刺额外降低目标攻击速度2%。", + }, + icon = "Interface\\Icons\\Ability_Hunter_Quickshot", + maxRank = 5, + name = "强化钉刺", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点射击系天赋", + "使你的远程武器造成致命一击的几率提高1%。", + }, + icon = "Interface\\Icons\\Ability_SearingArrow", + maxRank = 5, + name = "夺命射击", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要10点射击系天赋", + "使你的远程武器的射程延长3码。", + }, + icon = "Interface\\Icons\\Ability_TownWatch", + maxRank = 2, + name = "鹰眼", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要10点射击系天赋", + "50法力值", + "2秒施法时间", + "瞄准目标射击,使远程伤害提高70点。", + }, + icon = "Interface\\Icons\\INV_Spear_07", + maxRank = 1, + name = "瞄准射击", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要10点射击系天赋", + "奥术射击的冷却时间减少0.5秒,瞄准射击的冷却时间减少2秒。", + }, + icon = "Interface\\Icons\\Ability_ImpalingBolt", + maxRank = 3, + name = "迅捷射击", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要15点射击系天赋", + "你的普通远程攻击有3%几率发射额外的一次射击,此次额外射击不会消耗弹药。", + }, + icon = "Interface\\Icons\\INV_Misc_Quiver_03", + maxRank = 2, + name = "无尽箭袋", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点夺命射击", + "需要15点射击系天赋", + "使你的远程武器致命一击伤害提高6%。", + }, + icon = "Interface\\Icons\\Ability_PierceDamage", + maxRank = 5, + name = "致死射击", + prereqColumn = 3, + prereqTier = 2, + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/1", + "需要20点射击系天赋", + "137法力值", + "瞬发法术", + "短程射击,对目标造成50%的武器伤害,并使其困惑4秒。任何伤害都会打断该效果。使用之后结束你的攻击。", + }, + icon = "Interface\\Icons\\Ability_GolemStormBolt", + maxRank = 1, + name = "驱散射击", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点瞄准射击", + "需要20点射击系天赋", + "你的瞄准射击会额外造成相当于其伤害值5%的火焰、奥术或自然伤害,具体伤害类型取决于当前激活的效果。此外,还会根据元素属性提供额外增益:\n爆炸弹药:你的下一次多重射击会使命中目标爆炸,对目标周围5码范围内的敌人造成20%的远程武器伤害。\n剧毒弹药:你的下一次毒蛇钉刺会造成100%的额外伤害,并施加腐蚀性毒素,使目标的护甲值降低240点,持续15秒。\n魔力弹药:你的下一次奥术射击会造成100%的额外伤害,并削弱目标的魔法抗性,使其受到法术伤害提高3%,持续6秒。", + }, + icon = "Interface\\Icons\\spell_hunter_exoticmunitions_poisoned", + maxRank = 1, + name = "元素弹药", + prereqColumn = 2, + prereqTier = 3, + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要5点致死射击", + "需要20点射击系天赋", + "你的瞄准射击、稳固射击和多重射击造成致命一击后会使目标流血,在8秒内累计造成15%伤害。此伤害不产生威胁值。", + }, + icon = "Interface\\Icons\\ability_hunter_disarmingshot", + maxRank = 2, + name = "穿刺射击", + prereqColumn = 3, + prereqTier = 4, + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要20点射击系天赋", + "多重射击和乱射的伤害提高5%,乱射的施法时间减少1秒。", + }, + icon = "Interface\\Icons\\Ability_UpgradeMoonGlaive", + maxRank = 3, + name = "弹幕", + tier = 5, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要25点射击系天赋", + "使你的稳固射击和瞄准射击造成的伤害增加5%。", + }, + icon = "Interface\\Icons\\Ability_Marksmanship", + maxRank = 2, + name = "强化射击", + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要25点射击系天赋", + "使你的远程武器造成的伤害提高2%。", + }, + icon = "Interface\\Icons\\INV_Weapon_Rifle_06", + maxRank = 5, + name = "远程武器专精", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点元素弹药", + "需要30点射击系天赋", + "你的稳固射击、瞄准射击和奥术射击暴击有100%的几率重置瞄准射击的冷却时间并触发荷枪实弹。荷枪实弹会使瞄准射击的施法时间减少1秒,且能够击中你和目标之间的所有敌人,持续10秒或直到施放瞄准射击为止。", + }, + icon = "Interface\\Icons\\ability_hunter_lockandload", + maxRank = 1, + name = "荷枪实弹", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + [3] = { + background = "HunterSurvival", + icon = "Interface\\Icons\\Ability_Hunter_SwiftStrike", + name = "生存", + numTalents = 20, + talents = { + { + column = 1, + desc = { + "等级 0/3", + "对野兽、巨人、龙类和人型生物造成的所有伤害提高1%,对野兽、巨人、龙类和人型生物造成的暴击伤害提高1%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Holy_PrayerOfHealing", + maxRank = 3, + name = "强化杀戮", + tier = 1, + }, + { + column = 2, + desc = { + "等级 0/5", + "降低所有陷阱和近战技能的法力消耗2%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\INV_Misc_Book_08", + maxRank = 5, + name = "足智多谋", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/2", + "使你的招架几率和攻击速度提高1%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Parry", + maxRank = 2, + name = "迅捷反射", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要5点生存系天赋", + "使你的献祭陷阱、冰霜陷阱和爆炸陷阱有8%的几率困住目标,令它们无法移动,持续5秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_StrangleVines", + maxRank = 3, + name = "诱捕", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要5点生存系天赋", + "你的副手武器造成的伤害提高13%,并使你的割伤、猛禽一击、猫鼬撕咬、切碎和摔绊的致命一击几率提高3%。", + }, + icon = "Interface\\Icons\\Ability_Racial_BloodRage", + maxRank = 2, + name = "野蛮打击", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要5点生存系天赋", + "使你的摔绊技能有14%的几率令目标在5秒内无法移动。", + }, + icon = "Interface\\Icons\\Ability_Rogue_Trip", + maxRank = 3, + name = "强化摔绊", + tier = 2, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要5点生存系天赋", + "当你没有控制宠物时,造成伤害提高3%。", + }, + icon = "Interface\\Icons\\ability_hunter_improvedtracking", + maxRank = 2, + name = "孤军奋战", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要10点生存系天赋", + "如果陷阱在放置后的前5秒内未被触发,则冰霜陷阱效果的持续时间和火焰陷阱效果的伤害将增加13%。", + }, + icon = "Interface\\Icons\\INV_Misc_Map_01", + maxRank = 2, + name = "神机妙算", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要10点生存系天赋", + "使你的生命值上限提高2%。", + }, + icon = "Interface\\Icons\\Trade_Survival", + maxRank = 5, + name = "生存专家", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要10点生存系天赋", + "137法力值", + "瞬发法术", + "一次横扫攻击,对你前方10码锥形区域内最多5名敌人造成60%武器伤害。\n此技能与多重射击共享冷却时间。", + }, + icon = "Interface\\Icons\\INV_ThrowingKnife_06", + maxRank = 1, + name = "切碎", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/1", + "需要10点生存系天赋", + "瞬发", + "激活之后,使你的躲闪和招架几率提高25%,持续10秒。", + }, + icon = "Interface\\Icons\\Ability_Whirlwind", + maxRank = 1, + name = "威慑", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要15点生存系天赋", + "你的猫鼬撕咬和触发的火焰陷阱现在会对目标施加最高等级的毒蛇钉刺,持续时间为20%。此方法施加的毒蛇钉刺会忽略目标的抗性和免疫效果。", + }, + icon = "Interface\\Icons\\INV_Misc_Herb_16", + maxRank = 2, + name = "钉刺之毒", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要15点生存系天赋", + "命中几率提高1%,双持时额外提高1%。并使你抵抗移动限制效果的几率提高5%。", + }, + icon = "Interface\\Icons\\Ability_Kick", + maxRank = 3, + name = "稳固", + tier = 4, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要15点生存系天赋", + "使敌人抵抗你的假死技能的几率降低4%。", + }, + icon = "Interface\\Icons\\Ability_Rogue_FeignDeath", + maxRank = 2, + name = "强化假死", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要20点生存系天赋", + "使你的所有攻击造成致命一击的几率提高1%,近战致命一击伤害加成提高7%。", + }, + icon = "Interface\\Icons\\Spell_Holy_BlessingOfStamina", + maxRank = 3, + name = "杀戮本能", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要20点生存系天赋", + "冰霜陷阱效果的持续时间和火焰陷阱效果的伤害增加10%,并使敌人抵抗你的陷阱效果的几率降低4%。", + }, + icon = "Interface\\Icons\\Ability_Ensnare", + maxRank = 3, + name = "陷阱掌握", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要1点切碎", + "需要20点生存系天赋", + "137法力值", + "瞬发法术", + "创伤目标,造成相当于你近战攻击强度40%的伤害,并使其流血,在8秒内造成20%的伤害。仅限对目标造成致命一击后使用。从目标侧面使用此技能,可使其伤害提高15%。", + }, + icon = "Interface\\Icons\\spell_lacerate_1C", + maxRank = 1, + name = "割伤", + prereqColumn = 3, + prereqTier = 3, + tier = 5, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要25点生存系天赋", + "猛禽一击和猫鼬撕咬的冷却时间减少0.5秒,且造成伤害提高5%。", + }, + icon = "Interface\\Icons\\Ability_Hunter_SwiftStrike", + maxRank = 2, + name = "恶毒打击", + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要25点生存系天赋", + "使你的敏捷提高2%。并使你的近战攻击强度提高,数值相当于你敏捷的20%。", + }, + icon = "Interface\\Icons\\Spell_Nature_Invisibilty", + maxRank = 5, + name = "闪电反射", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要3点陷阱掌握", + "需要30点生存系天赋", + "献祭陷阱和爆炸陷阱的法力值消耗降低20%,且伤害效果受到你的近战攻击强度加成,并允许你在战斗中使用所有陷阱。", + }, + icon = "Interface\\Icons\\Spell_Nature_Slow", + maxRank = 1, + name = "捕猎高手", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + numTabs = 3, + }, + MAGE = { + [1] = { + background = "MageArcane", + icon = "Interface\\Icons\\Spell_Nature_WispSplode", + name = "奥术", + numTalents = 19, + talents = { + { + column = 1, + desc = { + "等级 0/2", + "使你的目标对你的所有法术的抗性降低5点,并使你的奥术系法术所造成的威胁值降低20%。", + }, + icon = "Interface\\Icons\\Spell_Holy_DispelMagic", + maxRank = 2, + name = "奥术精妙", + tier = 1, + }, + { + column = 2, + desc = { + "等级 0/3", + "使你的所有抗性提高4点,并且每当你的一个法术被部分或完全抵抗时,你都会恢复你总法力的1%。此效果每2秒可发生一次。", + }, + icon = "Interface\\Icons\\Spell_Nature_AstralRecalGroup", + maxRank = 3, + name = "魔法吸收", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你有20%的几率在施放奥术飞弹时不会因为受到伤害而中断施法。", + }, + icon = "Interface\\Icons\\Spell_Nature_StarFall", + maxRank = 5, + name = "强化奥术飞弹", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要5点奥术系天赋", + "使你的魔杖造成的伤害提高13%,魔杖的命中几率增加5%。你的魔杖攻击成功造成伤害后有一定几率让你回复0法力值。", + }, + icon = "Interface\\Icons\\INV_Wand_01", + maxRank = 2, + name = "魔杖专精", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要5点奥术系天赋", + "使你的敌人抵抗你的奥术魔法的几率降低2%。", + }, + icon = "Interface\\Icons\\Spell_Holy_Devotion", + maxRank = 5, + name = "奥术集中", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点奥术系天赋", + "使你有2%的几率在施放任何一种伤害性法术之后进入节能施法状态。节能施法状态可以使你的下一个伤害性法术所消耗的法力值减少100%。此效果每10秒可发生一次。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ManaBurn", + maxRank = 5, + name = "奥术专注", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/1", + "需要10点奥术系天赋", + "使你的魔法抑制和魔法增效的效果提高100%。并允许对敌人施放,仅对等级62或更低的目标有效。", + }, + icon = "Interface\\Icons\\Spell_Nature_AbolishMagic", + maxRank = 1, + name = "魔法协调", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要10点奥术系天赋", + "使你的奥术法术造成致命一击效果的几率提高2%。", + }, + icon = "Interface\\Icons\\Spell_Nature_WispSplode", + maxRank = 3, + name = "奥术冲击", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要10点奥术系天赋", + "80法力值", + "2.5秒施法时间", + "集中奥术能量摧毁目标,造成101到114点奥术伤害,并在8秒时间内使你奥术飞弹的伤害和法力消耗增加20%。", + }, + icon = "Interface\\Icons\\Spell_Arcane_Blast", + maxRank = 1, + name = "奥术溃裂", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要15点奥术系天赋", + "使你的法力护盾每吸收一点伤害所消耗的法力值减少13%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_DetectLesserInvisibility", + maxRank = 2, + name = "强化法力护盾", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要15点奥术系天赋", + "使你的法术反制有50%的几率使目标沉默4秒。", + }, + icon = "Interface\\Icons\\Spell_Frost_IceShock", + maxRank = 2, + name = "强化法术反制", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要1点奥术溃裂", + "需要15点奥术系天赋", + "你的奥术飞弹有5%的几率重置奥术溃裂的冷却时间,并使奥术溃裂在下次施放时回复所消耗基础法力值,该效果每15秒只能发生一次。", + }, + icon = "Interface\\Icons\\Spell_Nature_StormReach", + maxRank = 3, + name = "时间融合", + prereqColumn = 3, + prereqTier = 3, + tier = 4, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要15点奥术系天赋", + "使你在施法时仍保持7%的法力回复速度。当总法力值低于35%时,此效果将变为原来的三倍。", + }, + icon = "Interface\\Icons\\Spell_Shadow_SiphonMana", + maxRank = 3, + name = "奥术冥想", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要20点奥术系天赋", + "你的伤害性奥术法术有8%的几率不受控制地爆发,消耗2%的基础法力值来造成25%的额外伤害。", + }, + icon = "Interface\\Icons\\Spell_Shadow_Teleport", + maxRank = 3, + name = "奥术增效", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点奥术系天赋", + "瞬发", + "激活之后,你的下一个施法时间低于10秒的法师法术会成为瞬发法术。", + }, + icon = "Interface\\Icons\\Spell_Nature_EnchantArmor", + maxRank = 1, + name = "气定神闲", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要20点奥术系天赋", + "你的奥术飞弹现在可以获得提高施法速度效果加成,从而减少飞弹发射间隔和总的引导时间。此外,你的奥术法术的冷却时间将减少与施法速度增加的相同百分比。你的奥术法术施法速度提高5%。", + }, + icon = "Interface\\Icons\\Spell_Arcane_PortalDarnassus", + maxRank = 1, + name = "奥术急速", + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要20点奥术系天赋", + "使你的奥术法术的暴击伤害增加50%。", + }, + icon = "Interface\\Icons\\INV_Enchant_EssenceMagicLarge", + maxRank = 2, + name = "奥术增强", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要25点奥术系天赋", + "你的伤害性奥术法术击中目标后有4%的几率复制50%的伤害。此效果自动触发,复制的奥术飞弹会与原始法术同时引导。", + }, + icon = "Interface\\Icons\\Spell_Mage_FocusingCrystal", + maxRank = 5, + name = "共鸣涌动", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点气定神闲", + "需要30点奥术系天赋", + "瞬发", + "激活后,你的施法速度提高30%,但每秒消耗1%的总法力值,并使所有法力值恢复效果减少50%。\n如果你的总法力低于10%,你将因无法控制的力量而剧烈燃烧,立即死亡。此效果持续20秒且无法取消。", + }, + icon = "Interface\\Icons\\Spell_Nature_Lightning", + maxRank = 1, + name = "奥术强化", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + [2] = { + background = "MageFire", + icon = "Interface\\Icons\\Spell_Fire_Fire", + name = "火焰", + numTalents = 17, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使你的火球术的施法时间减少0.1秒。", + }, + icon = "Interface\\Icons\\Spell_Fire_FlameBolt", + maxRank = 5, + name = "强化火球术", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你的火焰魔法有2%的几率令目标昏迷2秒。", + }, + icon = "Interface\\Icons\\Spell_Fire_MeteorStorm", + maxRank = 5, + name = "冲击", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/5", + "需要5点火焰系天赋", + "你的火焰法术在造成致命一击后使目标燃烧,令其在4秒内承受相当于该法术伤害8%的额外伤害。", + }, + icon = "Interface\\Icons\\Spell_Fire_Incinerate", + maxRank = 5, + name = "点燃", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要5点火焰系天赋", + "使你的火焰法术的射程增加3码,冲击波的影响半径增加10%。", + }, + icon = "Interface\\Icons\\Spell_Fire_Flare", + maxRank = 2, + name = "烈焰投掷", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要5点火焰系天赋", + "火焰冲击的冷却时间减少0.5秒,公共冷却时间减少0.3秒。", + }, + icon = "Interface\\Icons\\Spell_Fire_Fireball", + maxRank = 3, + name = "强化火焰冲击", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要10点火焰系天赋", + "使你的火焰冲击和灼烧法术的致命一击率提高2%。", + }, + icon = "Interface\\Icons\\Spell_Fire_FlameShock", + maxRank = 2, + name = "烧尽", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要10点火焰系天赋", + "使你的烈焰风暴造成致命一击的几率提高5%。", + }, + icon = "Interface\\Icons\\Spell_Fire_SelfDestruct", + maxRank = 3, + name = "强化烈焰风暴", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要10点火焰系天赋", + "125法力值", + "6秒施法时间", + "发射一枚巨大的火球,对目标造成141到187点火焰伤害,并在12秒内造成总计56点额外伤害。", + }, + icon = "Interface\\Icons\\Spell_Fire_Fireball02", + maxRank = 1, + name = "炎爆术", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要10点火焰系天赋", + "使你的火焰系法术有35%的几率不会因为你受到伤害而被干扰,火焰系法术所造成的威胁值降低15%。", + }, + icon = "Interface\\Icons\\Spell_Fire_Fire", + maxRank = 2, + name = "燃烧之魂", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要15点火焰系天赋", + "使你的灼烧和火焰冲击有33%的几率令目标更易受到火焰伤害,在其受到火焰系攻击时承受的伤害提高3%,持续30秒。可叠加5次。", + }, + icon = "Interface\\Icons\\Spell_Fire_SoulBurn", + maxRank = 3, + name = "火焰易伤", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要15点火焰系天赋", + "你的防护火焰结界有10%的几率将火焰法术反射给施法者。", + }, + icon = "Interface\\Icons\\Spell_Fire_FireArmor", + maxRank = 2, + name = "强化防护火焰结界", + tier = 4, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要15点火焰系天赋", + "使你的火焰系和冰霜系法术在造成致命一击效果之后为施法者回复该法术所消耗基础法力值的15%。", + }, + icon = "Interface\\Icons\\Spell_Fire_MasterOfElements", + maxRank = 3, + name = "元素大师", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/1", + "需要20点火焰系天赋", + "215法力值", + "瞬发法术", + "施法者放出一道火焰冲击波,对10码内所有被冲击波触及的敌人造成154到186点火焰伤害并晕眩6秒。", + }, + icon = "Interface\\Icons\\Spell_Holy_Excorcism_02", + maxRank = 1, + name = "冲击波", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要20点火焰系天赋", + "使你的火焰法术造成致命一击的几率提高2%。", + }, + icon = "Interface\\Icons\\Spell_Nature_WispHeal", + maxRank = 3, + name = "火焰重击", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要1点炎爆术", + "需要20点火焰系天赋", + "你的火球术和火焰冲击造成致命一击后有50%的几率使你获得一层法术连击效果。法术连击效果使你下一个炎爆术的施法时间每层减少1秒,持续3分钟。最多可叠加5次。", + }, + icon = "Interface\\Icons\\Ability_Mage_Firestarter", + maxRank = 2, + name = "法术连击", + prereqColumn = 3, + prereqTier = 3, + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要25点火焰系天赋", + "使你的火焰法术造成的伤害提高2%。", + }, + icon = "Interface\\Icons\\Spell_Fire_Immolation", + maxRank = 5, + name = "火焰强化", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要3点火焰重击", + "需要30点火焰系天赋", + "瞬发", + "激活之后,你每一次施放火焰系伤害性法术,你的该类法术的致命一击几率都会提高10%。这个效果会一直持续到你使用火焰系法术造成了3次致命一击。", + }, + icon = "Interface\\Icons\\Spell_Fire_SealOfFire", + maxRank = 1, + name = "燃烧", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + [3] = { + background = "MageFrost", + icon = "Interface\\Icons\\Spell_Frost_FreezingBreath", + name = "冰霜", + numTalents = 19, + talents = { + { + column = 1, + desc = { + "等级 0/2", + "使你的霜甲术和冰甲术所提供的护甲值和抗性提高15%。另外,你的防护冰霜结界有10%的几率将冰霜系法术和魔法效果反射给施法者。", + }, + icon = "Interface\\Icons\\Spell_Frost_FrostWard", + maxRank = 2, + name = "冰霜障壁", + tier = 1, + }, + { + column = 2, + desc = { + "等级 0/5", + "使你的寒冰箭的施法时间减少0.1秒。", + }, + icon = "Interface\\Icons\\Spell_Frost_FrostBolt02", + maxRank = 5, + name = "强化寒冰箭", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/3", + "使你的目标抵抗你的火焰和冰霜系法术的几率降低2%。", + }, + icon = "Interface\\Icons\\Spell_Ice_MagicDamage", + maxRank = 3, + name = "元素精准", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要5点冰霜系天赋", + "使你的冰霜法术所造成的伤害提高2%。", + }, + icon = "Interface\\Icons\\Spell_Frost_Frostbolt", + maxRank = 3, + name = "刺骨寒冰", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要5点冰霜系天赋", + "使你的寒冷效果有5%的几率将目标冰冻5秒。", + }, + icon = "Interface\\Icons\\Spell_Frost_FrostArmor", + maxRank = 3, + name = "霜寒刺骨", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要5点冰霜系天赋", + "使你的冰霜新星的冷却时间减少2秒。", + }, + icon = "Interface\\Icons\\Spell_Frost_FreezingBreath", + maxRank = 2, + name = "强化冰霜新星", + tier = 2, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要5点冰霜系天赋", + "使你的冰冷效果的持续时间延长1秒,并使目标身上的减速效果提高4%。", + }, + icon = "Interface\\Icons\\Spell_Frost_Wisp", + maxRank = 3, + name = "极寒冰霜", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/5", + "需要10点冰霜系天赋", + "使你的冰霜法术致命一击所造成的伤害提高20%。", + }, + icon = "Interface\\Icons\\Spell_Frost_IceShard", + maxRank = 5, + name = "寒冰碎片", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要10点冰霜系天赋", + "瞬发", + "激活之后,使你的所有冰霜法术的冷却时间结束。", + }, + icon = "Interface\\Icons\\Spell_Frost_WizardMark", + maxRank = 1, + name = "急速冷却", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要10点冰霜系天赋", + "为你的暴风雪法术增加冰冷效果,使目标的移动速度降低20%,持续1.50秒。", + }, + icon = "Interface\\Icons\\Spell_Frost_IceStorm", + maxRank = 3, + name = "强化暴风雪", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要15点冰霜系天赋", + "使你的冰霜新星和冰锥术的有效半径以及寒冰箭和暴风雪的射程提高10%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_DarkRitual", + maxRank = 2, + name = "极寒延伸", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要15点冰霜系天赋", + "使你的所有冰霜法术所消耗的法力值减少5%,冰霜系法术所造成的威胁值降低10%。", + }, + icon = "Interface\\Icons\\Spell_Frost_Stun", + maxRank = 3, + name = "冰霜导能", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要2点强化冰霜新星", + "需要15点冰霜系天赋", + "使你的所有法术在击中被冰冻的敌人时造成致命一击的几率提高10%。", + }, + icon = "Interface\\Icons\\Spell_Frost_FrostShock", + maxRank = 5, + name = "碎冰", + prereqColumn = 3, + prereqTier = 2, + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点冰霜系天赋", + "15法力值", + "瞬发法术", + "你被一道寒冰屏障所笼罩,在10秒内不会受到任何物理和法术伤害,但是在这期间你也无法攻击、移动或施法。", + }, + icon = "Interface\\Icons\\Spell_Frost_Frost", + maxRank = 1, + name = "寒冰屏障", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要20点冰霜系天赋", + "200法力值", + "需引导", + "你利用冰霜的魔网线冻结自己,并向敌人发射冰柱,每1秒造成101冰霜伤害,持续5秒。受到伤害时有很高的几率会打碎你的冰牢,对你造成相当于你基础生命值30%的冰霜伤害。提前取消效果不会消除冻结效果。", + }, + icon = "Interface\\Icons\\Spell_Frost_FrostBlast", + maxRank = 1, + name = "冰柱", + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要20点冰霜系天赋", + "使你的冰锥术所造成的伤害提高15%。", + }, + icon = "Interface\\Icons\\Spell_Frost_Glacier", + maxRank = 3, + name = "强化冰锥术", + tier = 5, + }, + { + column = 1, + desc = { + "等级 0/5", + "需要25点冰霜系天赋", + "你的冰霜系伤害法术有20%的几率附加深冬之寒效果,令冰霜系法术对目标造成致命一击的几率提高2%,效果持续15秒,可叠加最多5次。", + }, + icon = "Interface\\Icons\\Spell_Frost_ChillingBlast", + maxRank = 5, + name = "深冬之寒", + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要1点冰柱", + "需要25点冰霜系天赋", + "你施加的冻结效果被目标永久免疫时,你有50%的几率获得冰霜速冻效果。冰霜速冻会重置冰柱的冷却时间,并使你下一次施放的冰柱引导时间和每个冰柱的间隔时间缩短80%。", + }, + icon = "Interface\\Icons\\Spell_Fire_FrostResistanceTotem", + maxRank = 2, + name = "冰霜速冻", + prereqColumn = 3, + prereqTier = 5, + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点寒冰屏障", + "需要30点冰霜系天赋", + "305法力值", + "瞬发法术", + "立即为你加上魔法护盾,可吸收438点伤害,并使你的冰霜伤害提高10%,持续1分钟。护盾存在期间,施法不会因受到伤害而延迟,且冰霜伤害额外提高5%。", + }, + icon = "Interface\\Icons\\Spell_Ice_Lament", + maxRank = 1, + name = "寒冰护体", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + numTabs = 3, + }, + PALADIN = { + [1] = { + background = "PaladinHoly", + icon = "Interface\\Icons\\Spell_Holy_HolyBolt", + name = "神圣", + numTalents = 17, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使你的力量提高2%。", + }, + icon = "Interface\\Icons\\Ability_GolemThunderClap", + maxRank = 5, + name = "神圣之力", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你的智力值上限提高2%。", + }, + icon = "Interface\\Icons\\Spell_Nature_Sleep", + maxRank = 5, + name = "神圣智慧", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要5点神圣系天赋", + "施放审判会使你的下一个圣光术的施法时间减少0.3秒。", + }, + icon = "Interface\\Icons\\ability_paladin_judgementblue", + maxRank = 3, + name = "神圣制裁", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要5点神圣系天赋", + "使你的圣光闪现和圣光术有35%的几率在受到伤害时不会延长施法时间。", + }, + icon = "Interface\\Icons\\Spell_Arcane_Blink", + maxRank = 2, + name = "精神集中", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点神圣系天赋", + "使你的正义圣印和正义审判所能造成的伤害提高2%。", + }, + icon = "Interface\\Icons\\Ability_ThunderBolt", + maxRank = 5, + name = "强化正义圣印", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要10点神圣系天赋", + "使你的圣光术、圣光闪现和神圣震击的治疗效果提高4%。", + }, + icon = "Interface\\Icons\\Spell_Holy_HolyBolt", + maxRank = 3, + name = "治疗之光", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要10点神圣系天赋", + "瞬发", + "使半径30码范围内的队友的神圣系攻击对敌人造成的伤害提高10%。每个圣骑士在同一时间内只能开启一种光环,且同类光环的效果无法叠加。", + }, + icon = "Interface\\Icons\\Spell_Holy_MindVision", + maxRank = 1, + name = "圣洁光环", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要10点神圣系天赋", + "被你的圣疗术治疗的目标因装备而获得的护甲值提高15%,持续2分钟。另外,圣疗术的冷却时间减少10分钟。", + }, + icon = "Interface\\Icons\\Spell_Holy_LayOnHands", + maxRank = 2, + name = "强化圣疗术", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要10点神圣系天赋", + "使你抵抗恐惧和困惑效果的几率提高5%。", + }, + icon = "Interface\\Icons\\Spell_Holy_UnyieldingFaith", + maxRank = 2, + name = "不灭信仰", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要15点神圣系天赋", + "使你的专注光环的效果提高5%,所有受到光环影响的队友抵抗沉默和打断效果的几率提高5%。", + }, + icon = "Interface\\Icons\\Spell_Holy_MindSooth", + maxRank = 3, + name = "强化专注光环", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要15点神圣系天赋", + "在你的圣光闪现、圣光术或神圣震击造成极效治疗效果后,你将恢复该法术消耗的基础法力值的12%。", + }, + icon = "Interface\\Icons\\Spell_Holy_GreaterHeal", + maxRank = 5, + name = "启发", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要15点神圣系天赋", + "增加你的法术造成的治疗效果,数值相当于你从装备获得护甲值的1%。", + }, + icon = "Interface\\Icons\\INV_Shoulder_30", + maxRank = 2, + name = "铁壁", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/5", + "需要1点神圣震击", + "需要20点神圣系天赋", + "提高你使用神圣震击造成致命一击的几率10%。", + }, + icon = "Interface\\Icons\\Spell_Holy_Heal", + maxRank = 5, + name = "神恩术", + prereqColumn = 2, + prereqTier = 5, + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点神圣系天赋", + "280法力值", + "瞬发法术", + "以神圣的能量冲击目标,造成97到104点神圣伤害,或为盟友恢复311到320点生命值。施放神圣震击有一定几率重置其冷却时间。", + }, + icon = "Interface\\Icons\\Spell_Holy_SearingLight", + maxRank = 1, + name = "神圣震击", + tier = 5, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要25点神圣系天赋", + "使你的圣光术和圣光闪现造成致命一击的几率提高2%。", + }, + icon = "Interface\\Icons\\Spell_Holy_Power", + maxRank = 3, + name = "神圣强化", + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要1点神圣震击", + "需要25点神圣系天赋", + "十字军打击有20%的几率重置你的神圣震击的冷却时间。此外,你的神圣打击的治疗效果提高20%,并获得5%的额外治疗效果加成。", + }, + icon = "Interface\\Icons\\Spell_Holy_ReviveChampion", + maxRank = 5, + name = "神佑打击", + prereqColumn = 2, + prereqTier = 5, + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点神圣震击", + "需要30点神圣系天赋", + "你造成的极效治疗会使目标获得晨辉效果,持续30秒。当目标受到伤害时恢复248到289点生命值,并消耗晨辉效果。", + }, + icon = "Interface\\Icons\\Spell_Holy_AuraMastery", + maxRank = 1, + name = "晨辉", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + [2] = { + background = "PaladinProtection", + icon = "Interface\\Icons\\Spell_Holy_DevotionAura", + name = "防护", + numTalents = 16, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使你的虔诚光环提供额外护甲值的效果增强5%。", + }, + icon = "Interface\\Icons\\Spell_Holy_DevotionAura", + maxRank = 5, + name = "强化虔诚光环", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "受到近战和远程攻击时有2%几率使你的盾牌格挡几率提高3%。持续10秒或格挡5次攻击。", + }, + icon = "Interface\\Icons\\Ability_Defend", + maxRank = 5, + name = "盾牌壁垒", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要5点防护系天赋", + "使你的近战武器和技能击中目标的几率提高1%。", + }, + icon = "Interface\\Icons\\Ability_Rogue_Ambush", + maxRank = 3, + name = "精确", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要5点防护系天赋", + "使你的保护之手的冷却时间减少60秒,自由之手的效果持续时间延长3秒。", + }, + icon = "Interface\\Icons\\Spell_Holy_SealOfProtection", + maxRank = 2, + name = "守护者的宠爱", + tier = 2, + }, + { + column = 4, + desc = { + "等级 0/5", + "需要5点防护系天赋", + "使你因装备而获得的护甲值提高2%。", + }, + icon = "Interface\\Icons\\Spell_Holy_Devotion", + maxRank = 5, + name = "坚韧", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要10点防护系天赋", + "使你的正义之怒所产生的威胁值提高25%。", + }, + icon = "Interface\\Icons\\Spell_Holy_SealOfFury", + maxRank = 3, + name = "强化正义之怒", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要10点防护系天赋", + "60法力值", + "瞬发法术", + "为友方目标施加祝福,使其所受到的所有类型的伤害都减少最多10点,持续10分钟。另外,当目标格挡一次近战攻击时,攻击者会受到14点神圣伤害。每个圣骑士在同一时间内只能给目标施加一种祝福,同类型的祝福不能重叠。", + }, + icon = "Interface\\Icons\\Spell_Nature_LightningShield", + maxRank = 1, + name = "庇护祝福", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要5点盾牌壁垒", + "需要10点防护系天赋", + "使你的盾牌所能吸收的伤害提高10%。并有33%的几率在发生格挡时恢复2%的法力值。这种效果每5秒才能生效一次。", + }, + icon = "Interface\\Icons\\INV_Shield_06", + maxRank = 3, + name = "盾牌专精", + prereqColumn = 3, + prereqTier = 1, + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要10点防护系天赋", + "使你的防御技能提高7点。", + }, + icon = "Interface\\Icons\\Spell_Magic_LesserInvisibilty", + maxRank = 3, + name = "预知", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要15点防护系天赋", + "清算之手击中目标的几率提高4%。", + }, + icon = "Interface\\Icons\\Spell_Holy_Redemption", + maxRank = 2, + name = "强化清算之手", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要15点防护系天赋", + "使你的制裁之锤的冷却时间减少5秒。", + }, + icon = "Interface\\Icons\\Spell_Holy_SealOfMight", + maxRank = 3, + name = "强化制裁之锤", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要3点强化正义之怒", + "需要20点防护系天赋", + "当正义之怒激活时,你受到的伤害会降低3%。", + }, + icon = "Interface\\Icons\\Ability_Warrior_SwordandBoard", + maxRank = 3, + name = "正义防御", + prereqColumn = 1, + prereqTier = 3, + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点防护系天赋", + "135法力值", + "瞬发法术", + "需要盾牌", + "使你的格挡几率提高45%,持续10秒。在此期间每次成功格挡都会对攻击者造成35点神圣伤害,这种伤害所造成的威胁值提高50%。每次成功格挡会消耗掉一次格挡机会,最多可格挡4次。", + }, + icon = "Interface\\Icons\\Spell_Holy_BlessingOfProtection", + maxRank = 1, + name = "神圣之盾", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要20点防护系天赋", + "使你在格挡敌人攻击时,有10%的几率发动一次额外攻击。", + }, + icon = "Interface\\Icons\\Spell_Holy_BlessingOfStrength", + maxRank = 5, + name = "清算", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要25点防护系天赋", + "神圣打击的伤害提高5%且造成5%的额外威胁值。十字军打击使你获得狂热防御,下一次格挡的伤害量降低6%。", + }, + icon = "Interface\\Icons\\Spell_Holy_SealofBLood", + maxRank = 5, + name = "正义打击", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点神圣之盾", + "需要30点防护系天赋", + "200法力值", + "瞬发法术", + "需要盾牌", + "用盾牌猛击目标,造成274到301神圣伤害并使你受到的伤害减少30%,持续12秒。", + }, + icon = "Interface\\Icons\\Ability_Warrior_VictoryRush", + maxRank = 1, + name = "正义壁垒", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + [3] = { + background = "PaladinCombat", + icon = "Interface\\Icons\\Spell_Holy_AuraOfLight", + name = "惩戒", + numTalents = 16, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使你的力量祝福和智慧祝福的效果提高4%。", + }, + icon = "Interface\\Icons\\Spell_Holy_SpiritualGuidence", + maxRank = 5, + name = "强化祝福", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你的审判和圣印所消耗的法力值减少3%。", + }, + icon = "Interface\\Icons\\Spell_Frost_WindWalkOn", + maxRank = 5, + name = "祈福", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要5点惩戒系天赋", + "使你的审判法术的冷却时间减少1秒。", + }, + icon = "Interface\\Icons\\Spell_Holy_RighteousFury", + maxRank = 2, + name = "强化审判", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要5点惩戒系天赋", + "使你的十字军圣印的近战攻击强度加成和十字军审判的神圣伤害加成提高5%。", + }, + icon = "Interface\\Icons\\Spell_Holy_HolySmite", + maxRank = 3, + name = "强化十字军圣印", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点惩戒系天赋", + "使你的招架几率提高1%。", + }, + icon = "Interface\\Icons\\Ability_Parry", + maxRank = 5, + name = "偏斜", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要10点惩戒系天赋", + "使你的惩戒光环所能造成的伤害提高25%。", + }, + icon = "Interface\\Icons\\Spell_Holy_AuraOfLight", + maxRank = 2, + name = "强化惩罚光环", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要10点惩戒系天赋", + "使你用近战武器对敌人造成致命一击的几率提高1%。", + }, + icon = "Interface\\Icons\\Spell_Holy_RetributionAura", + maxRank = 5, + name = "定罪", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要10点惩戒系天赋", + "4法力值", + "瞬发法术", + "为友方目标施加祝福,使其所有属性提高10%,持续10分钟。每个圣骑士在同一时间内只能给目标施加一种祝福,同类型的祝福不能重叠。", + }, + icon = "Interface\\Icons\\Spell_Magic_MageArmor", + maxRank = 1, + name = "王者祝福", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要10点惩戒系天赋", + "使你的移动速度和坐骑移动速度提高4%。这个效果不与其它同类效果叠加。", + }, + icon = "Interface\\Icons\\Spell_Holy_PersuitofJustice", + maxRank = 2, + name = "正义追击", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要15点惩戒系天赋", + "使你的双手近战武器造成的伤害提高2%,并使你的双手剑、双手锤和双手斧的武器技能增加1。", + }, + icon = "Interface\\Icons\\INV_Hammer_04", + maxRank = 3, + name = "双手武器专精", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要15点惩戒系天赋", + "使圣骑士的近战攻击有一定的几率令目标造成的伤害降低3%,持续10秒,仅对62级或更低的敌人有效。", + }, + icon = "Interface\\Icons\\Spell_Holy_Vindication", + maxRank = 3, + name = "辩护", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要20点惩戒系天赋", + "所有对你造成致命一击的法术都会对其施法者造成15%的伤害,但最大数值不会超过圣骑士生命值总量的50%。", + }, + icon = "Interface\\Icons\\Spell_Holy_EyeforanEye", + maxRank = 2, + name = "以眼还眼", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要5点定罪", + "需要20点惩戒系天赋", + "当你的武器攻击、法术或技能在对敌人造成致命一击之后,能使你获得1%的伤害加成,并使你产生的威胁值减少2%,持续30秒。此效果最多可叠加3次。正义之怒激活时,威胁值降低效果无效。", + }, + icon = "Interface\\Icons\\Ability_Racial_Avatar", + maxRank = 5, + name = "复仇", + prereqColumn = 2, + prereqTier = 3, + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要20点惩戒系天赋", + "65法力值", + "瞬发法术", + "使圣骑士在攻击时有一定几率对目标造成与攻击伤害的70%等量的神圣伤害。圣骑士在同一时间内只能激活一种圣印。持续30秒。\n\n释放这种圣印的能量将对目标造成审判效果,对其立刻造成46到51点神圣伤害,若目标昏迷或瘫痪则造成93到101点神圣伤害。", + }, + icon = "Interface\\Icons\\Ability_Warrior_InnerRage", + maxRank = 1, + name = "命令圣印", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要25点惩戒系天赋", + "十字军打击造成额外的2%伤害,每叠加一次狂热可使你的攻击和施法速度额外增加2%。\n神圣打击为你注入神圣威能,使你的力量增加4%,持续20秒。", + }, + icon = "Interface\\Icons\\Spell_Holy_CrusaderStrike", + maxRank = 5, + name = "复仇打击", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要30点惩戒系天赋", + "60法力值", + "瞬发法术", + "使目标进入冥想状态,最多持续6秒。任何伤害都会唤醒目标。当效果被免疫时,目标会忏悔自己的罪孽,每次近战攻击时都会受到80点神圣伤害,持续20秒。", + }, + icon = "Interface\\Icons\\Spell_Holy_PrayerOfHealing", + maxRank = 1, + name = "忏悔", + tier = 7, + }, + }, + }, + numTabs = 3, + }, + PRIEST = { + [1] = { + background = "PriestDiscipline", + icon = "Interface\\Icons\\Spell_Holy_AuraOfLight", + name = "戒律", + numTalents = 18, + talents = { + { + column = 1, + desc = { + "等级 2/2", + "魔杖的伤害和命中几率提高25%,弓的伤害和命中几率提高10%。\n魔杖和弓的攻击命中时有几率恢复28点法力值。\n比魔杖专精(等级 1)触发几率更高。", + }, + icon = "Interface\\Icons\\INV_Wand_01", + maxRank = 2, + name = "魔杖专精", + tier = 1, + }, + { + column = 2, + desc = { + "等级 0/3", + "使敌人抵抗你的神圣法术和戒律法术的几率降低2%。", + }, + icon = "Interface\\Icons\\Spell_Holy_Retribution", + maxRank = 3, + name = "穿透之光", + tier = 1, + }, + { + column = 3, + desc = { + "等级 5/5", + "使你的瞬发法术、攻击性神圣法术和戒律法术所消耗的法力值减少10%。", + }, + icon = "Interface\\Icons\\Ability_Hibernation", + maxRank = 5, + name = "精神敏锐", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/5", + "使你的法术造成的威胁值降低4%。", + }, + icon = "Interface\\Icons\\Spell_Nature_ManaRegenTotem", + maxRank = 5, + name = "无声消退", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/5", + "使你抵抗昏迷、恐惧和沉默效果的几率提高3%。", + }, + icon = "Interface\\Icons\\Spell_Magic_MageArmor", + maxRank = 5, + name = "坚定意志", + tier = 2, + }, + { + column = 3, + desc = { + "等级 1/2", + "使你有50%的几率在受到敌人的近战或远程致命一击后得到专注施法效果,持续6秒。专注施法效果可以让你在施法时不会因为受到伤害而延长施法时间,且令你抵抗打断效果的几率提高10%。", + " ", + "下一级:", + "使你有100%的几率在受到敌人的近战或远程致命一击后得到专注施法效果,持续6秒。专注施法效果可以让你在施法时不会因为受到伤害而延长施法时间,且令你抵抗打断效果的几率提高20%。", + }, + icon = "Interface\\Icons\\Spell_Holy_Restoration", + maxRank = 2, + name = "殉难", + tier = 2, + }, + { + column = 4, + desc = { + "等级 2/2", + "使你的真言术:韧和坚韧祷言的效果提高30%。", + }, + icon = "Interface\\Icons\\Spell_Holy_WordFortitude", + maxRank = 2, + name = "强化真言术:韧", + tier = 2, + }, + { + column = 1, + desc = { + "等级 2/2", + "使你的心灵之火的效果提高30%。", + }, + icon = "Interface\\Icons\\Spell_Holy_InnerFire", + maxRank = 2, + name = "强化心灵之火", + tier = 3, + }, + { + column = 2, + desc = { + "等级 1/1", + "瞬发", + "激活之后,你的下一个法术所消耗的法力值减少100%,造成致命一击或极效治疗的几率提高25%(如果它有可能造成这些效果的话)。", + }, + icon = "Interface\\Icons\\Spell_Frost_WindWalkOn", + maxRank = 1, + name = "心灵专注", + tier = 3, + }, + { + column = 3, + desc = { + "等级 3/3", + "使你的真言术:盾所吸收的伤害量提高15%。", + }, + icon = "Interface\\Icons\\Spell_Holy_PowerWordShield", + maxRank = 3, + name = "强化真言术:盾", + tier = 3, + }, + { + column = 4, + desc = { + "等级 3/3", + "使你在施法时仍保持15%的法力回复速度。", + }, + icon = "Interface\\Icons\\Spell_Nature_Sleep", + maxRank = 3, + name = "冥想", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/3", + "你的伤害性神圣和戒律法术造成致命一击伤害时有33%的几率使你的下一个惩击法术瞬发,且消耗的法力值减少50%,持续10秒。", + }, + icon = "Interface\\Icons\\Spell_Holy_SearingLightPriest", + maxRank = 3, + name = "灼热之光", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/2", + "你的神圣之火有50%的几率使你的神圣伤害提高12%,持续10秒。", + }, + icon = "Interface\\Icons\\Spell_Holy_SearingLight", + maxRank = 2, + name = "纯净火焰", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要20点戒律系天赋", + "你的总智力提高3%,施法速度提高1%。", + }, + icon = "Interface\\Icons\\Spell_Nature_EnchantArmor", + maxRank = 3, + name = "心灵之力", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点戒律系天赋", + "58法力值", + "瞬发法术", + "启发一名队友,使你在施放有益的戒律或神圣法术时有10%的几率、施放有害的戒律或神圣法术时有15%的几率燃烧该队友,对造成其总生命值的4%的神圣伤害,并使你和该队友的法术伤害和治疗效果提高10%,持续8秒。如果此法术用于自己,法术伤害和治疗效果的提升将增加到15%。你每次只能对一名队友使用,持续30分钟。", + }, + icon = "Interface\\Icons\\btnholyscriptures", + maxRank = 1, + name = "启发", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要20点戒律系天赋", + "当你的真言术:盾被打破时,返还其基础法力消耗的25%,并使你的神圣法术伤害和治疗效果提高,数值相当于盾总吸收量的10%,持续8秒。", + }, + icon = "Interface\\Icons\\Spell_Holy_BlessingOfProtection", + maxRank = 1, + name = "反馈护盾", + prereqColumn = 3, + prereqTier = 3, + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要25点戒律系天赋", + "使你的法术伤害和攻击性法术的致命一击几率提高1%,并且使你的真言术:盾所吸收的伤害量提高4%。", + }, + icon = "Interface\\Icons\\Spell_Nature_SlowingTotem", + maxRank = 5, + name = "意志之力", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点启发", + "需要30点戒律系天赋", + "31法力值", + "瞬发法术", + "责罚目标,造成139到160点神圣伤害并使敌人迷惑,最多持续2秒,目标受到任何伤害都会中断此效果。如果目标是盟友,则使其攻击和施法速度增加13%,持续8秒,如果对盟友造成致命一击,则增加至12秒。无法对生命值低于80%且等级低于35的盟友使用。", + }, + icon = "Interface\\Icons\\Spell_Holy_UnyieldingFaith", + maxRank = 1, + name = "责罚", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + [2] = { + background = "PriestHoly", + icon = "Interface\\Icons\\Spell_Holy_LayOnHands", + name = "神圣", + numTalents = 16, + talents = { + { + column = 1, + desc = { + "等级 0/3", + "使你的恢复法术的治疗效果提高5%。", + }, + icon = "Interface\\Icons\\Spell_Holy_Renew", + maxRank = 3, + name = "强化恢复", + tier = 1, + }, + { + column = 2, + desc = { + "等级 0/2", + "使你有35%的几率在施放任何神圣法术时不会因为受到伤害而延迟。", + }, + icon = "Interface\\Icons\\Spell_Holy_HealingFocus", + maxRank = 2, + name = "神圣专注", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你的神圣和戒律法术造成致命一击的几率提高1%。", + }, + icon = "Interface\\Icons\\Spell_Holy_SealOfSalvation", + maxRank = 5, + name = "神圣专精", + tier = 1, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要5点神圣系天赋", + "使你的惩击、神圣之火、治疗术和强效治疗术的施法时间减少0.1秒。", + }, + icon = "Interface\\Icons\\Spell_Holy_SealOfWrath", + maxRank = 5, + name = "神圣之怒", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要5点神圣系天赋", + "使你受到的所有法术伤害降低3%。", + }, + icon = "Interface\\Icons\\Spell_Holy_SpellWarding", + maxRank = 3, + name = "法术屏障", + tier = 2, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要5点神圣系天赋", + "使你的惩击、神圣之火和责罚的射程,治疗祷言和神圣新星的作用半径提高10%。", + }, + icon = "Interface\\Icons\\Spell_Holy_Purify", + maxRank = 2, + name = "神圣延伸", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要10点神圣系天赋", + "在遭受近战或远程攻击的致命一击之后,为你在6秒内恢复相当于该伤害总量8%的生命值。", + }, + icon = "Interface\\Icons\\Spell_Holy_BlessedRecovery", + maxRank = 3, + name = "神恩回复", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要10点神圣系天赋", + "在你的快速治疗、治疗术、强效治疗术或治疗祷言对目标造成极效治疗效果后,使目标的护甲值提高8%,持续15秒。", + }, + icon = "Interface\\Icons\\Spell_Holy_LayOnHands", + maxRank = 3, + name = "灵感", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/1", + "需要10点神圣系天赋", + "139法力值", + "瞬发法术", + "制造一次以施法者为中心的神圣能量爆炸,对半径10码范围内的所有目标造成27到32点神圣伤害,并为半径10码范围内的所有小队成员恢复54到63点生命值。在暗影形态下使用此法术会对你造成伤害,而不是治疗,这些效果的仇恨值较低。", + }, + icon = "Interface\\Icons\\Spell_Holy_HolyNova", + maxRank = 1, + name = "神圣新星", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要3点强化恢复", + "需要15点神圣系天赋", + "目标存在恢复效果时,你的治疗法术的效果提高3%。", + }, + icon = "Interface\\Icons\\Spell_Holy_Resurrection", + maxRank = 2, + name = "迅速恢复", + prereqColumn = 1, + prereqTier = 1, + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要15点神圣系天赋", + "使你的次级治疗术、治疗术、强效治疗术和治疗祷言的法力值消耗降低5%。", + }, + icon = "Interface\\Icons\\Spell_Holy_Heal02", + maxRank = 3, + name = "强化治疗术", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要15点神圣系天赋", + "使你的法术的治疗和伤害效果提高,数值最多相当于你的精神值的5%。", + }, + icon = "Interface\\Icons\\Spell_Holy_SpiritualGuidence", + maxRank = 5, + name = "精神指引", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点神圣系天赋", + "精神提高5%。在死亡时,牧师变成一个救赎之魂,持续10秒。救赎之魂无法移动、攻击,也不会受到任何法术或效果的影响。在这个形态下,牧师可以施放任何治疗法术,不需消耗任何法力值。当救赎之魂效果结束时,牧师死亡。", + }, + icon = "Interface\\Icons\\INV_Enchant_EssenceEternalLarge", + maxRank = 1, + name = "救赎之魂", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要20点神圣系天赋", + "增加你的光明之泉的治疗次数1。", + }, + icon = "Interface\\Icons\\Spell_Holy_SummonLightwell", + maxRank = 3, + name = "光明储能", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要25点神圣系天赋", + "使你的治疗法术的治疗效果提高6%。", + }, + icon = "Interface\\Icons\\Spell_Nature_MoonGlow", + maxRank = 5, + name = "精神治疗", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点救赎之魂", + "需要30点神圣系天赋", + "250法力值", + "10秒施法时间", + "将友方目标宣告为你的勇士,持续2小时。誓约存在时,勇士受到的所有伤害降低5%,所有抗性提高28点,并允许你对其使用各种勇士法术。勇士受到伤害的2%将作为法力值返还给你。同一时间只能标记一名勇士。", + }, + icon = "Interface\\Icons\\Spell_Holy_ProclaimChampion_02", + maxRank = 1, + name = "勇士誓约", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + [3] = { + background = "PriestShadow", + icon = "Interface\\Icons\\Spell_Shadow_Possession", + name = "暗影", + numTalents = 17, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使你有20%的几率在杀死一个敌人并因此获得经验值之后精神属性提高100%。在这段时间里,你的法力值可以在施法时仍保持50%的回复速度。持续15秒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_Requiem", + maxRank = 5, + name = "精神分流", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你的暗影系伤害性法术有2%的几率令目标昏迷2秒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_GatherShadows", + maxRank = 5, + name = "昏阙", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要5点暗影系天赋", + "使你的暗影法术造成的威胁值降低8%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ShadowWard", + maxRank = 3, + name = "暗影亲和", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要5点暗影系天赋", + "使你的暗言术:痛的持续时间延长3秒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ShadowWordPain", + maxRank = 2, + name = "强化暗言术:痛", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点暗影系天赋", + "使目标抵抗你的暗影法术的几率下降2%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_BurningSpirit", + maxRank = 5, + name = "暗影集中", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要10点暗影系天赋", + "使你的心灵尖啸的冷却时间减少2秒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_PsychicScream", + maxRank = 2, + name = "强化心灵尖啸", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要10点暗影系天赋", + "使你的心灵震爆的冷却时间减少0.5秒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_UnholyFrenzy", + maxRank = 5, + name = "强化心灵震爆", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要10点暗影系天赋", + "45法力值", + "需引导", + "以暗影能量攻击目标的灵魂,在3秒内对其造成总计75点暗影伤害,并使其移动速度降低50%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_SiphonMana", + maxRank = 1, + name = "精神鞭笞", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要10点暗影系天赋", + "使你的法力燃烧的施法时间减少0.25秒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ManaBurn", + maxRank = 2, + name = "强化法力燃烧", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要15点暗影系天赋", + "使你的渐隐术的冷却时间减少3秒。", + }, + icon = "Interface\\Icons\\Spell_Magic_LesserInvisibilty", + maxRank = 2, + name = "强化渐隐术", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要15点暗影系天赋", + "使你的暗影系伤害性法术的射程提高6%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ChillTouch", + maxRank = 3, + name = "暗影延伸", + tier = 4, + }, + { + column = 4, + desc = { + "等级 0/5", + "需要15点暗影系天赋", + "你的暗影系伤害性法术有20%的机会使你的目标在受到暗影系攻击时更脆弱,受到的伤害提高3%,持续15秒。此效果最多可叠加5次。", + }, + icon = "Interface\\Icons\\Spell_Shadow_BlackPlague", + maxRank = 5, + name = "暗影之波", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/1", + "需要2点强化心灵尖啸", + "需要20点暗影系天赋", + "202法力值", + "瞬发法术", + "使目标沉默,在5秒内不能施法。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ImpPhaseShift", + maxRank = 1, + name = "沉默", + prereqColumn = 1, + prereqTier = 3, + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点暗影系天赋", + "36法力值", + "瞬发法术", + "暗影魔法的能量笼罩你的目标,使你对其造成的暗影伤害总量的20%转而治疗所有队友。效果持续1分钟。", + }, + icon = "Interface\\Icons\\Spell_Shadow_UnsummonBuilding", + maxRank = 1, + name = "吸血鬼的拥抱", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要1点吸血鬼的拥抱", + "需要20点暗影系天赋", + "吸血鬼的拥抱的治疗量额外提高2%,同时也会为队友恢复法力值,数值相当于你对目标造成的暗影伤害的1%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ImprovedVampiricEmbrace", + maxRank = 2, + name = "吸血鬼之触", + prereqColumn = 2, + prereqTier = 5, + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要25点暗影系天赋", + "使你的暗影法术伤害提高2%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_Twilight", + maxRank = 5, + name = "黑暗", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点吸血鬼的拥抱", + "需要30点暗影系天赋", + "209法力值", + "瞬发法术", + "进入暗影形态,使你能造成的暗影伤害提高15%,受到物理攻击时承受的伤害降低15%。但是在这个形态下,你不能施放神圣系的法术。", + }, + icon = "Interface\\Icons\\Spell_Shadow_Shadowform", + maxRank = 1, + name = "暗影形态", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + numTabs = 3, + }, + ROGUE = { + [1] = { + background = "RogueAssassination", + icon = "Interface\\Icons\\Ability_Rogue_Garrote", + name = "刺杀", + numTalents = 18, + talents = { + { + column = 1, + desc = { + "等级 0/3", + "使你的剔骨技能所造成的伤害提高5%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Rogue_Eviscerate", + maxRank = 3, + name = "强化剔骨", + tier = 1, + }, + { + column = 2, + desc = { + "等级 0/2", + "在你杀死一个可为你提供经验值或荣誉值的敌人后,你的下一个产生连击点数的技能造成致命一击的几率提高20%,效果持续40秒。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_FiegnDead", + maxRank = 2, + name = "冷酷攻击", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你的致命一击几率提高1%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Racial_BloodRage", + maxRank = 5, + name = "恶意", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要5点刺杀系天赋", + "使你的终结技有33%的几率为目标增加一个连击点数。", + }, + icon = "Interface\\Icons\\Ability_Druid_Disembowel", + maxRank = 3, + name = "无情", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要5点刺杀系天赋", + "使你对人形生物、巨人、野兽和龙类目标造成的所有伤害提高1%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_DeathScream", + maxRank = 2, + name = "谋杀", + tier = 2, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要5点刺杀系天赋", + "使你的切割和兴奋的效果持续时间延长15%。", + }, + icon = "Interface\\Icons\\Ability_Rogue_SliceDice", + maxRank = 3, + name = "强化刀锋战术", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/1", + "需要10点刺杀系天赋", + "你的终结技每拥有一个连击点数就有20%的几率恢复20点能量值,并使你的终结技伤害在30秒内提高5%,最多可叠加5次。", + }, + icon = "Interface\\Icons\\Ability_Warrior_DecisiveStrike", + maxRank = 1, + name = "无情打击", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要10点刺杀系天赋", + "使你的投掷武器和致命投掷的射程增加3码,你的致命投掷有50%的几率使副手武器的毒药立即生效。", + }, + icon = "Interface\\Icons\\INV_ThrowingKnife_01", + maxRank = 2, + name = "投掷武器专精", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点恶意", + "需要10点刺杀系天赋", + "产生连击点数的技能致命一击伤害加成提高6%。", + }, + icon = "Interface\\Icons\\Ability_CriticalStrike", + maxRank = 5, + name = "致命偷袭", + prereqColumn = 3, + prereqTier = 1, + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要15点刺杀系天赋", + "割裂的持续时间延长4秒。每次使用割裂时,无论效果是否生效,你的近战伤害都会在原有持续时间内每个连击点数提高1%。", + }, + icon = "Interface\\Icons\\INV_Misc_Bone_09", + maxRank = 2, + name = "血腥气息", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要15点刺杀系天赋", + "使你的毒素所造成的伤害提高10%,并使你的毒药抵抗驱散效果的几率提高14%。", + }, + icon = "Interface\\Icons\\Ability_Rogue_FeignDeath", + maxRank = 3, + name = "恶性毒药", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要15点刺杀系天赋", + "使你的毒药对敌人生效的几率提高3%。", + }, + icon = "Interface\\Icons\\Ability_Poisons", + maxRank = 3, + name = "强化毒药", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要20点刺杀系天赋", + "毒药被抵抗的几率降低4%,并使毒药生效时有15%的几率不消耗次数。", + }, + icon = "Interface\\Icons\\Ability_Creature_Poison_06", + maxRank = 3, + name = "高效毒药", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点刺杀系天赋", + "20能量", + "瞬发", + "终结技,使毒药的效果和对毒药的生效几率提高30%。每增加一个连击点数,毒药持续时间将延长:\n 1点:12秒\n 2点:16秒\n 3点:20秒\n 4点:24秒\n 5点:28秒", + }, + icon = "Interface\\Icons\\INV_Sword_31", + maxRank = 1, + name = "毒伤", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要20点刺杀系天赋", + "瞬发", + "激活之后,你的下一次邪恶攻击、背刺、伏击、双刃毒袭或剔骨造成致命一击的几率提高100%。", + }, + icon = "Interface\\Icons\\Spell_Ice_Lament", + maxRank = 1, + name = "冷血", + tier = 5, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要25点刺杀系天赋", + "使你的能量值上限提高5点。每次对目标施加毒药时,你都有50%的几率获得2点能量值。", + }, + icon = "Interface\\Icons\\Spell_Nature_EarthBindTotem", + maxRank = 2, + name = "精力", + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要25点刺杀系天赋", + "如果你的某个可以增加连击点数的技能造成了致命一击,那么它就有20%的几率增加一个额外的连击点数。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ChillTouch", + maxRank = 5, + name = "封印命运", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点毒伤", + "需要30点刺杀系天赋", + "45能量", + "瞬发", + "使用两把武器攻击,造成30点伤害,并附加相当于你30%攻击强度的伤害,同时施加两把武器的毒药。奖励1个连击点数。", + }, + icon = "Interface\\Icons\\spell_double_dose_3", + maxRank = 1, + name = "双刃毒袭", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + [2] = { + background = "RogueCombat", + icon = "Interface\\Icons\\INV_Weapon_ShortBlade_14", + name = "战斗", + numTalents = 18, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使你在从背后使用背刺、绞喉或伏击技能偷袭敌人时所造成的伤害值提高3%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Warrior_WarCry", + maxRank = 5, + name = "伺机而动", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你的躲闪几率提高1%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Nature_Invisibilty", + maxRank = 5, + name = "闪电反射", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/5", + "需要5点战斗系天赋", + "使你的招架几率提高1%。", + }, + icon = "Interface\\Icons\\Ability_Parry", + maxRank = 5, + name = "偏斜", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要5点战斗系天赋", + "使你的背刺技能造成致命一击的几率提高10%,并且背刺有15%的几率使你获得一个额外的连击点数。", + }, + icon = "Interface\\Icons\\Ability_BackStab", + maxRank = 3, + name = "强化背刺", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点战斗系天赋", + "使你的近战武器击中目标的几率提高1%。", + }, + icon = "Interface\\Icons\\Ability_Marksmanship", + maxRank = 5, + name = "精确", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/1", + "需要5点偏斜", + "需要10点战斗系天赋", + "10能量", + "瞬发", + "在招架了敌人的攻击之后可以使用的技能,对目标造成150%的武器伤害,并使其被缴械,持续6秒。", + }, + icon = "Interface\\Icons\\Ability_Warrior_Challange", + maxRank = 1, + name = "还击", + prereqColumn = 1, + prereqTier = 2, + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要10点战斗系天赋", + "使你在启动疾跑技能时有50%的几率移除所有移动限制效果。", + }, + icon = "Interface\\Icons\\Ability_Rogue_Sprint", + maxRank = 2, + name = "强化疾跑", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要10点战斗系天赋", + "使你有15%的几率在成功躲闪敌人的攻击或完全抵抗一个法术之后获得一个连击点数。", + }, + icon = "Interface\\Icons\\Spell_Nature_MirrorImage", + maxRank = 3, + name = "调整", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要15点战斗系天赋", + "使你的脚踢技能有50%的几率令目标沉默2秒。", + }, + icon = "Interface\\Icons\\Ability_Kick", + maxRank = 2, + name = "强化脚踢", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要15点战斗系天赋", + "使你的锤类武器击中目标时有1%的机会将其击晕3秒。", + }, + icon = "Interface\\Icons\\INV_Mace_01", + maxRank = 5, + name = "震荡打击", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点精确", + "需要15点战斗系天赋", + "使你的副手武器造成的伤害提高10%。", + }, + icon = "Interface\\Icons\\Ability_DualWield", + maxRank = 5, + name = "双武器专精", + prereqColumn = 3, + prereqTier = 2, + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要20点战斗系天赋", + "你使用单手锤、匕首和拳套造成致命一击的几率提高2%。", + }, + icon = "Interface\\Icons\\INV_Weapon_ShortBlade_05", + maxRank = 2, + name = "近身格斗", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点战斗系天赋", + "10能量", + "瞬发", + "一次突然袭击,造成攻击强度25%的伤害。仅在目标闪避后才可使用,并且无法被格挡、闪避或招架。奖励1个连击点数。", + }, + icon = "Interface\\Icons\\Ability_Rogue_SurpriseAttack", + maxRank = 1, + name = "突袭", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要20点战斗系天赋", + "使你在用斧类和剑类武器击中敌人后有2%的几率进行一次额外的攻击。", + }, + icon = "Interface\\Icons\\INV_Sword_27", + maxRank = 2, + name = "劈斩", + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要20点战斗系天赋", + "使你的单手斧、匕首、拳套、单手锤和单手剑的武器技能提高3点。", + }, + icon = "Interface\\Icons\\Spell_Holy_BlessingOfStrength", + maxRank = 2, + name = "武器专家", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要1点突袭", + "需要25点战斗系天赋", + "近战攻击速度提高2%,并且缩短能量回复间隔时间,缩短量受到你的敏捷加成。", + }, + icon = "Interface\\Icons\\Ability_Warrior_WeaponMastery", + maxRank = 2, + name = "刀锋冲刺", + prereqColumn = 2, + prereqTier = 5, + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要25点战斗系天赋", + "使你的邪恶攻击、剔骨、还击、突袭技能的伤害提高3%。", + }, + icon = "Interface\\Icons\\Ability_Racial_Avatar", + maxRank = 3, + name = "侵略", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要30点战斗系天赋", + "瞬发", + "使你的能量值回复速度提高100%,持续15秒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ShadowWordDominate", + maxRank = 1, + name = "冲动", + tier = 7, + }, + }, + }, + [3] = { + background = "RogueSubtlety", + icon = "Interface\\Icons\\Ability_Ambush", + name = "敏锐", + numTalents = 20, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使你在潜行后的移动速度提高3%,潜行技能的冷却时间降低1秒。当你在潜行状态下时,降低敌人侦测到你的几率。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Stealth", + maxRank = 5, + name = "伪装", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/2", + "使你的破甲技能令目标护甲值降低的效果提高25%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Warrior_Riposte", + maxRank = 2, + name = "强化破甲", + tier = 1, + }, + { + column = 4, + desc = { + "等级 0/3", + "使你的凿击技能的效果持续时间延长0.5秒。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Gouge", + maxRank = 3, + name = "强化凿击", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要5点敏锐系天赋", + "使伏击技能的致命一击几率提高15%,如果伏击技能未造成致命一击,则返还5能量。", + }, + icon = "Interface\\Icons\\Ability_Rogue_Ambush", + maxRank = 3, + name = "强化伏击", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要5点敏锐系天赋", + "使你的消失和致盲技能的冷却时间缩短45秒。", + }, + icon = "Interface\\Icons\\Spell_Magic_LesserInvisibilty", + maxRank = 2, + name = "飘忽不定", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要5点敏锐系天赋", + "割裂和绞喉造成的伤害提高10%,并使你的攻击忽略目标100点护甲值。", + }, + icon = "Interface\\Icons\\INV_Sword_17", + maxRank = 3, + name = "锯齿利刃", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要10点敏锐系天赋", + "使你有33%的几率在使用伏击、绞喉或偷袭技能后获得1个额外的连击点数。", + }, + icon = "Interface\\Icons\\Spell_Shadow_Fumble", + maxRank = 3, + name = "先发制人", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要10点敏锐系天赋", + "鬼魅攻击的能量值消耗降低3点。使用鬼魅攻击后移动速度提高5%,并使30码范围内的队友移动速度提高2%,持续5秒。", + }, + icon = "Interface\\Icons\\INV_Sword_02", + maxRank = 3, + name = "强化鬼魅攻击", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要10点敏锐系天赋", + "35能量", + "瞬发", + "在你周围8码范围内制造一片浓烟,持续8秒。烟雾中的所有目标在持续时间内被攻击和法术命中的几率降低20%。", + }, + icon = "Interface\\Icons\\spell_smoke_bomb_5", + maxRank = 1, + name = "烟雾弹", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/1", + "需要10点敏锐系天赋", + "40能量", + "瞬发", + "立即对目标造成110%的武器伤害,并令其流血不止,使其在受到物理攻击时所承受的伤害提高最多2%。可最多生效50次,或者持续15秒。奖励1个连击点数。", + }, + icon = "Interface\\Icons\\Spell_Shadow_LifeDrain", + maxRank = 1, + name = "出血", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要15点敏锐系天赋", + "施放消失后,你和20码内的队友会被包裹在暗影中,并获得一个吸收伤害的护盾,数值为你的最大生命值的6%,持续20秒。", + }, + icon = "Interface\\Icons\\spell_cloaked_in_shadows_2", + maxRank = 2, + name = "暗影遮蔽", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要15点敏锐系天赋", + "你的闷棍和致盲会在效果结束或未生效时,使目标造成伤害降低5%,持续8秒。使用闷棍后有50%的几率继续保持潜行状态。", + }, + icon = "Interface\\Icons\\Ability_Sap", + maxRank = 2, + name = "底牌", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要15点敏锐系天赋", + "扰乱的能量值消耗降低5点,被你的扰乱技能影响的目标命中几率降低2%,持续6秒,此效果一定生效。", + }, + icon = "Interface\\Icons\\Ability_Rogue_Shadowstep", + maxRank = 3, + name = "炫目迷雾", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要20点敏锐系天赋", + "使你的偷袭和绞喉技能所消耗的能量值减少10点。", + }, + icon = "Interface\\Icons\\Spell_Shadow_SummonSuccubus", + maxRank = 2, + name = "卑鄙", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点敏锐系天赋", + "瞬发", + "激活之后,这项技能立刻令你的其它盗贼技能的冷却时间结束。", + }, + icon = "Interface\\Icons\\Spell_Shadow_AntiShadow", + maxRank = 1, + name = "伺机待发", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要20点敏锐系天赋", + "30能量", + "瞬发", + "终结技,在目标身上刻上印记,持续6秒。印记会累积伤害,数值相当于目标在印记持续时间内受到的伤害的百分比,最高可达你攻击强度的一定百分比。当印记累计伤害达到最大或印记消失时,目标将受到全部累积的物理伤害。每个连击点数都会提升这两项数值:\n1点:累积10%的伤害\n最高可达攻击强度的50%\n2点:累积20%的伤害\n最高可达攻击强度的100%\n3点:累积30%的伤害\n最高可达攻击强度的150%\n4点:累积40%的伤害\n最高可达攻击强度的200%\n5点:累积50%的伤害\n最高可达攻击强度的250%", + }, + icon = "Interface\\Icons\\spell_shadow_of_death_3B", + maxRank = 1, + name = "死亡之影", + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要1点出血", + "需要20点敏锐系天赋", + "出血的能量消耗降低2,同时出血造成的物理伤害加成提高50%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_BloodBoil", + maxRank = 2, + name = "血肉模糊", + prereqColumn = 4, + prereqTier = 3, + tier = 5, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要25点敏锐系天赋", + "你或20码范围内小队成员的物理攻击造成致命一击后使你回复2点能量值。", + }, + icon = "Interface\\Icons\\INV_ThrowingKnife_05", + maxRank = 2, + name = "盗贼的尊严", + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要25点敏锐系天赋", + "你的潜行起手技有20%的几率,终结技每个连击点数有4%的几率,可使队友的致命一击几率提高2%,持续12秒,此效果最多可叠加2次。", + }, + icon = "Interface\\Icons\\INV_Misc_Key_03", + maxRank = 5, + name = "嫁祸诀窍", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点伺机待发", + "需要30点敏锐系天赋", + "40能量", + "瞬发", + "立即攻击目标,造成135%的武器伤害,并揭示目标的弱点,使你所在的小队成员攻击强度提高你近战攻击强度的30%,法术伤害提高你近战攻击强度的18%,持续8秒。此攻击无法被格挡、闪避或招架,奖励2个连击点数。", + }, + icon = "Interface\\Icons\\Ability_Creature_Cursed_02", + maxRank = 1, + name = "死亡标记", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + numTabs = 3, + }, + SHAMAN = { + [1] = { + background = "ShamanElementalCombat", + icon = "Interface\\Icons\\Spell_Fire_Volcano", + name = "元素", + numTalents = 17, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "火焰、冰霜和自然的攻击性法术消耗的法力值减少2%。", + }, + icon = "Interface\\Icons\\Spell_Nature_WispSplode", + maxRank = 5, + name = "传导", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "火焰、冰霜和自然法术造成的伤害提高1%。", + }, + icon = "Interface\\Icons\\Spell_Fire_Fireball", + maxRank = 5, + name = "震荡", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要5点元素系天赋", + "使你的石爪图腾的生命值提高25%,地缚图腾的影响范围增加10%。", + }, + icon = "Interface\\Icons\\Spell_Nature_StoneClawTotem", + maxRank = 2, + name = "大地之握", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要5点元素系天赋", + "使你在被火焰、冰霜和自然系法术击中时所承受的伤害降低4%。", + }, + icon = "Interface\\Icons\\Spell_Nature_ElementalAbsorption", + maxRank = 3, + name = "元素防护", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要5点元素系天赋", + "你的近战攻击和法术命中几率提高1%,你的近战攻击造成致命一击后将使你的法术命中几率提高3%,持续10秒。", + }, + icon = "Interface\\Icons\\Spell_Fire_ElementalDevastation", + maxRank = 3, + name = "元素浩劫", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/1", + "需要10点元素系天赋", + "在你的火焰、冰霜、自然伤害性法术或近战攻击造成致命一击后进入节能施法状态。这个状态可以使你接下来2个伤害性法术或技能所消耗的法力值减少60%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ManaBurn", + maxRank = 1, + name = "元素集中", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要10点元素系天赋", + "使你的震击法术的冷却时间减少0.3秒。", + }, + icon = "Interface\\Icons\\Spell_Frost_FrostWard", + maxRank = 3, + name = "回响", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要10点元素系天赋", + "使你的闪电箭和闪电链造成致命一击的几率提高1%。", + }, + icon = "Interface\\Icons\\Spell_Nature_CallStorm", + maxRank = 5, + name = "雷霆召唤", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要10点元素系天赋", + "使用熔岩爆裂刷新烈焰震击时,造成相当于刷新持续时间30%的伤害。", + }, + icon = "Interface\\Icons\\Spell_Fire_MeteorStorm", + maxRank = 2, + name = "强化熔岩爆裂", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要15点元素系天赋", + "使你的火焰新星图腾激活所需的延迟时间减少1秒,熔岩图腾所造成的威胁值降低25%,灼热图腾的攻击速度提高10%且攻击范围增加5码。", + }, + icon = "Interface\\Icons\\Spell_Fire_SealOfFire", + maxRank = 2, + name = "强化火焰图腾", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要15点元素系天赋", + "大地之盾最大充能次数增加2次。大地之盾激活时,施放伤害性法术时避免受到延迟的几率额外提高25%,且直接伤害法术会回复1次充能次数。", + }, + icon = "Interface\\Icons\\earthshaker_slam_16", + maxRank = 2, + name = "大地的召唤", + tier = 4, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要15点元素系天赋", + "火焰图腾和火焰法术造成的伤害提高5%,烈焰震击的范围增加3码。", + }, + icon = "Interface\\Icons\\Spell_Fire_Immolation", + maxRank = 3, + name = "烈焰召唤", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要20点元素系天赋", + "使你的闪电箭和闪电链的射程延长3码。", + }, + icon = "Interface\\Icons\\Spell_Nature_StormReach", + maxRank = 2, + name = "风暴来临", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点元素系天赋", + "瞬发", + "使你的火焰、冰霜和自然伤害提高15%,攻击性法术的法力消耗降低20%,持续10秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_WispHeal", + maxRank = 1, + name = "元素掌握", + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要20点元素系天赋", + "火焰、冰霜和自然法术造成的伤害提高5%,灼热图腾、熔岩图腾和火焰新星图腾以及你的火焰、冰霜和自然法术的致命一击伤害加成提高50%。", + }, + icon = "Interface\\Icons\\Spell_Fire_Volcano", + maxRank = 2, + name = "元素之怒", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点雷霆召唤", + "需要25点元素系天赋", + "使你的闪电箭和闪电链的施法时间减少0.2秒。", + }, + icon = "Interface\\Icons\\Spell_Lightning_LightningBolt01", + maxRank = 5, + name = "闪电掌握", + prereqColumn = 3, + prereqTier = 3, + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点元素掌握", + "需要30点元素系天赋", + "225法力值", + "2.5秒施法时间", + "震碎目标下方的地面,对其造成262到291点自然伤害,并对其10码范围内的所有目标造成相当于初始伤害35%的自然伤害,同时在地面留下破碎区域,使区域内敌人移动速度降低15%。4秒后,破碎区域发生余震,对区域内所有敌人造成相当于初始伤害30%的自然伤害。", + }, + icon = "Interface\\Icons\\Spell_Nature_Earthquake", + maxRank = 1, + name = "地震术", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + [2] = { + background = "ShamanEnhancement", + icon = "Interface\\Icons\\Spell_Nature_UnyeildingStamina", + name = "增强", + numTalents = 16, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使你的所有的属性总值提高1%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_GrimWard", + maxRank = 5, + name = "先祖知识", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你用盾牌格挡攻击的几率提高1%,格挡成功所减免的伤害量提高6%。", + }, + icon = "Interface\\Icons\\INV_Shield_06", + maxRank = 5, + name = "盾牌专精", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要5点增强系天赋", + "你获得相当于你的图腾造成的45%的额外威胁值。", + }, + icon = "Interface\\Icons\\Spell_Nature_AgitatingTotem", + maxRank = 2, + name = "图腾之盟", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要5点增强系天赋", + "使你的武器造成致命一击的几率提高1%。", + }, + icon = "Interface\\Icons\\Ability_ThunderBolt", + maxRank = 5, + name = "雷鸣猛击", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要5点增强系天赋", + "你的护盾法术充能次数增加2,但激活之间的冷却时间增加1秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_LightningShield", + maxRank = 3, + name = "稳固护盾", + tier = 2, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要5点增强系天赋", + "使你的幽魂之狼法术的施法时间减少1秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_SpiritWolf", + maxRank = 2, + name = "强化幽魂之狼", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要10点增强系天赋", + "你的物理攻击、武器强化效果以及风暴打击和闪电打击所造成的威胁值降低8%,此效果在石化武器激活时无效。", + }, + icon = "Interface\\Icons\\Spell_Nature_Tranquility", + maxRank = 3, + name = "平静之风", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要3点稳固护盾", + "需要10点增强系天赋", + "6法力值", + "瞬发法术", + "立即攻击你的目标,造成20%的武器伤害和额外的10%的自然伤害。此攻击还会触发你当前存在的萨满护盾效果,并消耗1层护盾。", + }, + icon = "Interface\\Icons\\Spell_Nature_ThunderClap", + maxRank = 1, + name = "闪电打击", + prereqColumn = 3, + prereqTier = 2, + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要10点增强系天赋", + "使你因装备而获得的护甲值提高5%,并使你的躲闪几率增加2%。", + }, + icon = "Interface\\Icons\\Spell_Nature_AncestralGuardian", + maxRank = 3, + name = "先祖守护", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要5点雷鸣猛击", + "需要15点增强系天赋", + "造成致命一击之后,你的下3次攻击的攻击速度提高8%。", + }, + icon = "Interface\\Icons\\Ability_GhoulFrenzy", + maxRank = 5, + name = "乱舞", + prereqColumn = 2, + prereqTier = 2, + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要15点增强系天赋", + "装备盾牌时,从盾牌获得的护甲值提高15%,产生的威胁值提高5%。", + }, + icon = "Interface\\Icons\\Spell_Nature_SpiritArmor", + maxRank = 2, + name = "幽魂甲", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要20点增强系天赋", + "使你的大地之力图腾和风之优雅图腾的效果提高12%。石肤图腾的伤害减免效果提高15%且盾牌格挡效果提高15%。根基图腾的冷却时间减少1秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_EarthBindTotem", + maxRank = 2, + name = "强化图腾", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要20点增强系天赋", + "为你的武器注入能量,带来不同的特殊效果:\n火舌:攻击命中敌人时,火焰图腾和火焰法术造成的伤害提高10%,持续5秒。\n冰封:触发几率提高8%,若目标处于冰霜震击状态,则必定造成致命一击。\n风怒:触发额外攻击时,攻击速度提高1%,持续5秒。最多可叠加2次。\n石化:你的物理伤害会生成一个相当于造成伤害20%的护盾,装备盾牌时护盾数值翻三倍。该护盾可吸收受到伤害的5%,持续8秒或直至吸收足够的伤害。护盾的耐久度不能超过最大生命值的20%。", + }, + icon = "Interface\\Icons\\Spell_Fire_FlameTounge", + maxRank = 3, + name = "元素武器", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要20点增强系天赋", + "5法力值", + "瞬发法术", + "使你获得一次额外的攻击机会,造成100%武器伤害,并使你造成的下2次自然伤害提高25%,持续12秒。", + }, + icon = "Interface\\Icons\\Ability_Shaman_StormStrike", + maxRank = 1, + name = "风暴打击", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要25点增强系天赋", + "使用的所有武器、风暴打击和闪电打击造成的伤害提高2%,瞬发法术的致命一击几率提高2%。", + }, + icon = "Interface\\Icons\\Spell_Fire_EnchantWeapon", + maxRank = 5, + name = "元素之赐", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要3点元素武器", + "需要30点增强系天赋", + "14法力值", + "瞬发法术", + "进入狂暴状态,使你的攻击速度提高20%,施法速度提高20%,持续30秒。在此效果下,你的近战攻击造成致命一击会使30码范围内所有小队成员的攻击和施法速度提高8%,持续6秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_BloodLust", + maxRank = 1, + name = "嗜血", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + [3] = { + background = "ShamanRestoration", + icon = "Interface\\Icons\\Spell_Nature_HealingWaveGreater", + name = "恢复", + numTalents = 16, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使你的治疗波的施法时间减少0.15秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_MagicImmunity", + maxRank = 5, + name = "强化治疗波", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你的治疗法术所消耗的法力值减少1%,图腾消耗的法力值减少5%。", + }, + icon = "Interface\\Icons\\Spell_Frost_ManaRecharge", + maxRank = 5, + name = "潮汐集中", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要5点恢复系天赋", + "使你的复生法术的冷却时间减少10分钟,并使你在重生后所获得的生命值和法力值提高10%。", + }, + icon = "Interface\\Icons\\Spell_Nature_Reincarnation", + maxRank = 2, + name = "强化复生", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要5点恢复系天赋", + "在你的任何一个治疗法术对目标造成极效治疗效果后,使目标因装备而获得的护甲值提高8%,持续15秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_UndyingStrength", + maxRank = 3, + name = "先祖治疗", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点恢复系天赋", + "使你的治疗法术和闪电法术的致命一击几率提高1%。", + }, + icon = "Interface\\Icons\\Spell_Nature_Tranquility", + maxRank = 5, + name = "潮汐掌握", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要10点恢复系天赋", + "你的治疗波和次级治疗波有33%的几率、治疗链有11%的几率使你下一次治疗波或治疗链对该目标的治疗效果提高6%,持续15秒,可叠加3次。", + }, + icon = "Interface\\Icons\\Spell_Nature_HealingWay", + maxRank = 3, + name = "治疗之道", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要10点恢复系天赋", + "使你在施放任意治疗法术的时候有35%的几率避免因受到伤害而被打断。", + }, + icon = "Interface\\Icons\\Spell_Nature_Regenerate", + maxRank = 2, + name = "治疗专注", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要10点恢复系天赋", + "使图腾对友方目标的持续时间增加20%,并使图腾召回所返还的法力值额外增加15%。", + }, + icon = "Interface\\Icons\\Spell_Nature_NullWard", + maxRank = 1, + name = "图腾掌握", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要10点恢复系天赋", + "使你的所有自然法术所造成的威胁值降低5%。", + }, + icon = "Interface\\Icons\\Spell_Nature_HealingTouch", + maxRank = 3, + name = "自然之赐", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要15点恢复系天赋", + "使你的法力之泉图腾的法力消耗降低10%,并使你的治疗之泉图腾的效果提高5%。", + }, + icon = "Interface\\Icons\\Spell_Nature_ManaRegenTotem", + maxRank = 5, + name = "恢复图腾", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要15点恢复系天赋", + "当水之护盾激活时,每5秒恢复3点法力值,每层水之护盾额外提高1%的法力回复速度。", + }, + icon = "Interface\\Icons\\Ability_Shaman_WaterShield", + maxRank = 3, + name = "强化水之护盾", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要3点治疗之道", + "需要20点恢复系天赋", + "在治疗受到你的治疗之道效果影响的目标时,你有15%的几率恢复相当于该法术基础法力消耗15%的法力值。", + }, + icon = "Interface\\Icons\\spell_arcane_manatap", + maxRank = 2, + name = "海潮涌动", + prereqColumn = 1, + prereqTier = 3, + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要20点恢复系天赋", + "瞬发", + "激活之后,你的下一个施法时间低于10秒的自然法术会成为瞬发法术,受影响的伤害性法术效果降低25%。", + }, + icon = "Interface\\Icons\\Spell_Nature_RavenForm", + maxRank = 1, + name = "先祖迅捷", + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要3点强化水之护盾", + "需要20点恢复系天赋", + "你的治疗波和次级治疗波有25%的几率恢复你水之护盾的一次充能。当充能达到最大时,此效果会消耗一个充能。", + }, + icon = "Interface\\Icons\\Spell_Shaman_TidalWaves", + maxRank = 2, + name = "逆流", + prereqColumn = 3, + prereqTier = 4, + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要5点恢复图腾", + "需要25点恢复系天赋", + "减少你的治疗链法术的施法时间0.2秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_HealingWaveGreater", + maxRank = 5, + name = "强化治疗链", + prereqColumn = 2, + prereqTier = 4, + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要5点强化治疗链", + "需要30点恢复系天赋", + "405法力值", + "瞬发法术", + "将一个盟友的灵魂与35码内其他盟友的灵魂联系起来。目标所受伤害的30%会分摊给附近的盟友,持续20秒。", + }, + icon = "Interface\\Icons\\Spell_Shaman_SpiritLink", + maxRank = 1, + name = "灵魂连接", + prereqColumn = 2, + prereqTier = 6, + tier = 7, + }, + }, + }, + numTabs = 3, + }, + WARLOCK = { + [1] = { + background = "WarlockCurses", + icon = "Interface\\Icons\\Spell_Shadow_UnsummonBuilding", + name = "痛苦", + numTalents = 18, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使敌人抵抗你的痛苦系法术的几率降低2%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Shadow_UnsummonBuilding", + maxRank = 5, + name = "镇压", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你的腐蚀术的施法时间减少0.3秒。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Shadow_AbominationExplosion", + maxRank = 5, + name = "强化腐蚀术", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要5点痛苦系天赋", + "虚弱诅咒造成的攻击速度降低效果提高3%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_CurseOfMannoroth", + maxRank = 2, + name = "强化虚弱诅咒", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要5点痛苦系天赋", + "使你的痛苦系法术被驱散的几率额外降低5%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_SoulLeech_1", + maxRank = 3, + name = "坚韧暗影", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要5点痛苦系天赋", + "使你的生命分流法术所转化的法力值提高10%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_BurningSpirit", + maxRank = 2, + name = "强化生命分流", + tier = 2, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要5点痛苦系天赋", + "使你的吸取灵魂、吸取生命和吸取法力的效果提高5%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_LifeDrain02", + maxRank = 2, + name = "强化吸取", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要10点痛苦系天赋", + "使你的痛苦诅咒所造成的伤害提高3%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_CurseOfSargeras", + maxRank = 3, + name = "强化痛苦诅咒", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要10点痛苦系天赋", + "你的痛苦法术有35%的几率不会因受到伤害而延迟。", + }, + icon = "Interface\\Icons\\Spell_Shadow_FingerOfDeath", + maxRank = 2, + name = "恶魔专注", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要10点痛苦系天赋", + "129法力值", + "瞬发法术", + "使目标的速度降低10%,持续12秒。每个术士只能对一个目标施加一种诅咒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_GrimWard", + maxRank = 1, + name = "疲劳诅咒", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要15点痛苦系天赋", + "使你的痛苦系法术的射程延长10%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_CallofBone", + maxRank = 2, + name = "无情延伸", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要15点痛苦系天赋", + "使你的腐蚀术、暗影收割和吸取法术有2%的几率在对敌人造成伤害之后令你进入暗影冥思状态。暗影冥思可以令你的下一个暗影箭的施法时间减少100%,且必定命中。", + }, + icon = "Interface\\Icons\\Spell_Shadow_Twilight", + maxRank = 2, + name = "夜幕", + tier = 4, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要15点痛苦系天赋", + "你对目标施加的每个痛苦法术效果都会使吸取灵魂、暗影收割和死亡缠绕的伤害额外提高2%,最多可被4个效果加成。", + }, + icon = "Interface\\Icons\\Spell_Shadow_Haunting", + maxRank = 3, + name = "灵魂虹吸", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要1点生命虹吸", + "需要20点痛苦系天赋", + "痛苦系法术的施法速度提高6%。此外,你的痛苦系法术会受到施法速度提高效果加成,缩短持续伤害法术和引导法术的周期性间隔时间和总持续时间,缩短量为施法速度提高效果的50%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_DetectInvisibility", + maxRank = 2, + name = "快速衰弱", + prereqColumn = 2, + prereqTier = 5, + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点痛苦系天赋", + "150法力值", + "瞬发法术", + "在30秒内将目标的150点生命值转移给施法者。", + }, + icon = "Interface\\Icons\\Spell_Shadow_Requiem", + maxRank = 1, + name = "生命虹吸", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要1点疲劳诅咒", + "需要20点痛苦系天赋", + "使你的疲劳诅咒的减速效果提高15%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_GrimWard", + maxRank = 2, + name = "强化疲劳诅咒", + prereqColumn = 3, + prereqTier = 3, + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/1", + "需要20点痛苦系天赋", + "你的痛苦诅咒可以与除厄运诅咒之外的其他诅咒一起使用。\n\n施放鲁莽诅咒、暗影诅咒或元素诅咒时,还会使目标受到你最高等级的痛苦诅咒效果。", + }, + icon = "Interface\\Icons\\Spell_Shadow_CurseOfAchimonde", + maxRank = 1, + name = "邪咒", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要25点痛苦系天赋", + "使你的暗影法术所造成的伤害或吸取的生命值提高2%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ShadeTrueSight", + maxRank = 5, + name = "暗影掌握", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要5点暗影掌握", + "需要30点痛苦系天赋", + "230法力值", + "需引导", + "汲取目标的生命力,在8秒内造成704点暗影伤害。引导期间,你对目标施放的痛苦系法术周期性间隔时间缩短30%。如果目标在引导此法术期间死亡,暗影收割的冷却时间将重置。", + }, + icon = "Interface\\Icons\\Spell_Shadow_SoulLeech", + maxRank = 1, + name = "暗影收割", + prereqColumn = 2, + prereqTier = 6, + tier = 7, + }, + }, + }, + [2] = { + background = "WarlockSummoning", + icon = "Interface\\Icons\\Spell_Shadow_CurseOfTounges", + name = "恶魔学识", + numTalents = 18, + talents = { + { + column = 2, + desc = { + "等级 0/2", + "你的恶魔的移动速度提高5%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Rogue_EnvelopingShadows", + maxRank = 2, + name = "邪恶追击", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你的耐力提高3%,同时使你的精神降低1%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Shadow_Metamorphosis", + maxRank = 5, + name = "恶魔之拥", + tier = 1, + }, + { + column = 4, + desc = { + "等级 0/3", + "没有控制任何恶魔时,你造成的所有伤害提高2%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\trade_archaeology_highbornesoulmirror", + maxRank = 3, + name = "灵魂囚禁", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要5点恶魔学识系天赋", + "使你的生命通道转化的生命值提高,数值相当于恶魔损失生命值的10%;法力通道转化的法力值提高,数值相当于恶魔损失法力值的10%。增加的额外数值会从你身上扣除该效果的30%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_LifeDrain", + maxRank = 2, + name = "灵魂通道", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/3", + "需要5点恶魔学识系天赋", + "使你的魔甲术的效果提高20%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_RagingScream", + maxRank = 3, + name = "恶魔庇护", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要5点恶魔学识系天赋", + "使你当前恶魔的智力继承你自身总智力的10%,并允许它们在施法时继续保持5%的法力恢复速度。", + }, + icon = "Interface\\Icons\\Spell_Holy_MagicalSentry", + maxRank = 3, + name = "恶魔智力", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要10点恶魔学识系天赋", + "瞬发", + "你的下一个普通恶魔召唤法术的施法时间减少4.5秒,法力消耗减少40%。", + }, + icon = "Interface\\Icons\\Spell_Nature_RemoveCurse", + maxRank = 1, + name = "恶魔支配", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要10点恶魔学识系天赋", + "使你当前恶魔的耐力继承你自身总耐力的10%,并使它们受到致命一击的几率降低1%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_AntiShadow", + maxRank = 5, + name = "恶魔耐力", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/1", + "需要3点灵魂囚禁", + "需要10点恶魔学识系天赋", + "瞬发", + "激活之后,牺牲你当前所召唤的恶魔,使你获得一种特殊效果。如果在此期间你召唤或奴役任意一个恶魔,该效果就会被取消。\n\n小鬼:使你的法术伤害提高4%。\n\n虚空行者:使你每4秒回复3%的生命值。\n\n魅魔:使你造成的威胁值降低10%。\n\n地狱猎犬:使你每4秒回复3%的法力值。", + }, + icon = "Interface\\Icons\\Spell_Shadow_PsychicScream", + maxRank = 1, + name = "恶魔牺牲", + prereqColumn = 4, + prereqTier = 1, + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要15点恶魔学识系天赋", + "使你的邪能石、愤怒石、虚空石和法术石的效果提高25%,火焰石的暴击几率加成提高50%。", + }, + icon = "Interface\\Icons\\inv_jewelry_talisman_fireice", + maxRank = 2, + name = "强化魔石", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要1点恶魔支配", + "需要15点恶魔学识系天赋", + "使你召唤小鬼、虚空行者、魅魔和地狱猎犬的施法时间减少2秒,法力值消耗降低30%。地狱火、末日仪式和恶魔之门的冷却时间减少25%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ImpPhaseShift", + maxRank = 2, + name = "召唤大师", + prereqColumn = 2, + prereqTier = 3, + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要15点恶魔学识系天赋", + "使你的魅魔的诱惑、虚空行者的折磨、小鬼的血之契印、地狱猎犬的腐坏之血的效果提高15%。", + }, + icon = "Interface\\Icons\\inv_misc_book_06", + maxRank = 3, + name = "虚空研究", + tier = 4, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要15点恶魔学识系天赋", + "使你的恶魔造成的伤害提高5%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ShadowWordDominate", + maxRank = 3, + name = "邪恶强化", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点恶魔学识系天赋", + "80法力值", + "瞬发法术", + "将能量注入你的恶魔,移除恶魔身上所有的控制效果,并使其造成的伤害提高20%,持续10秒。在此期间,该恶魔会受到相当于其基础生命值40%的伤害。", + }, + icon = "Interface\\Icons\\ability_warlock_power_overwhelming", + maxRank = 1, + name = "超越之力", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要20点恶魔学识系天赋", + "使你的恶魔近战、法术的命中几率和致命一击几率提高1%,且恶魔的近战、法术命中几率和致命一击几率继承你自身法术命中几率和法术致命一击几率的30%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_CurseOfTounges", + maxRank = 3, + name = "恶魔精准", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要25点恶魔学识系天赋", + "使术士和召唤的恶魔均获得一个特殊效果,只要该恶魔处于激活状态就不会消失。\n\n小鬼 - 法力值消耗降低3%。\n\n虚空行者 - 受到物理伤害降低2%。\n\n魅魔 - 所有伤害提高2%。\n\n地狱猎犬 - 所有抗性提高8点。\n\n高级恶魔 - 法术致命一击几率提高2%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ShadowPact", + maxRank = 5, + name = "恶魔学识大师", + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要25点恶魔学识系天赋", + "你的直接伤害法术命中后使你的恶魔受到你5%的火焰和暗影法术伤害加成,持续20秒。你的通道法术每一跳都会刷新该效果的持续时间。最多可叠加3次。", + }, + icon = "Interface\\Icons\\ability_warlock_demonicpower", + maxRank = 3, + name = "释放潜力", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要5点恶魔学识大师", + "需要30点恶魔学识系天赋", + "当恶魔被你控制时,你和恶魔造成的伤害提高5%,你所受伤害的20%被恶魔分担。你召唤的高级恶魔和被奴役的恶魔将不再提前脱离控制。", + }, + icon = "Interface\\Icons\\Spell_Shadow_GatherShadows", + maxRank = 1, + name = "灵魂链接", + prereqColumn = 2, + prereqTier = 6, + tier = 7, + }, + }, + }, + [3] = { + background = "WarlockDestruction", + icon = "Interface\\Icons\\Spell_Fire_Incinerate", + name = "毁灭", + numTalents = 16, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "你的暗影箭和吸取灵魂有2%的几率触发暗影易伤,使你对目标造成的暗影伤害提高20%,持续10秒。造成致命一击时触发几率更高。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Shadow_ShadowBolt", + maxRank = 5, + name = "暗影易伤", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你的毁灭系法术所消耗的法力值减少2%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Fire_WindsofWoe", + maxRank = 5, + name = "灾变", + tier = 1, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要5点毁灭系天赋", + "小鬼火焰箭的施法时间减少0.3秒,魅魔剧痛鞭笞的冷却时间减少3秒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_CurseOfTounges", + maxRank = 2, + name = "恶魔迅捷", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要5点毁灭系天赋", + "使你的暗影箭、灼热之痛和献祭的施法时间减少0.1秒,灵魂之火的施法时间减少0.4秒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_DeathPact", + maxRank = 5, + name = "灾祸", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要5点毁灭系天赋", + "献祭的持续伤害提高2%,并使你的毁灭系法术有4%的几率使目标移动速度降低50%,持续5秒。", + }, + icon = "Interface\\Icons\\Spell_Fire_Fire", + maxRank = 3, + name = "清算", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要10点毁灭系天赋", + "使你有35%的几率在施放火焰法术时不会因受到伤害而被打断。", + }, + icon = "Interface\\Icons\\Spell_Fire_LavaSpawn", + maxRank = 2, + name = "强烈", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要10点毁灭系天赋", + "105法力值", + "瞬发法术", + "材料:灵魂碎片", + "立即使用暗影能量冲击目标,对其造成91到104点暗影伤害。如果目标受到暗影灼烧伤害之后的5秒内死亡,且施法者因此获得经验值或荣誉,则施法者获得一块灵魂碎片。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ScourgeBuild", + maxRank = 1, + name = "暗影灼烧", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要10点毁灭系天赋", + "使你的毁灭系法术的致命一击几率提高1%。", + }, + icon = "Interface\\Icons\\Spell_Fire_FlameShock", + maxRank = 5, + name = "破坏", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要2点强烈", + "需要15点毁灭系天赋", + "使你的火焰之雨、地狱烈焰和灵魂之火有13%的几率使目标昏迷3秒。", + }, + icon = "Interface\\Icons\\Spell_Fire_Volcano", + maxRank = 2, + name = "火焰冲撞", + prereqColumn = 1, + prereqTier = 3, + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要15点毁灭系天赋", + "使你的毁灭系法术的射程增加10%,地狱烈焰的影响半径增加10%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_CorpseExplode", + maxRank = 2, + name = "毁灭延伸", + tier = 4, + }, + { + column = 4, + desc = { + "等级 0/5", + "需要15点毁灭系天赋", + "使你的灼热之痛造成致命一击的几率提高2%。", + }, + icon = "Interface\\Icons\\Spell_Fire_SoulBurn", + maxRank = 5, + name = "强化灼热之痛", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要20点毁灭系天赋", + "你的灵魂之火有50%的几率返还一个灵魂碎片,并使你的火焰伤害提高10%,持续30秒。", + }, + icon = "Interface\\Icons\\Spell_Fire_Fireball02", + maxRank = 2, + name = "强化灵魂之火", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要20点毁灭系天赋", + "使你的献祭法术的伤害提高4%。", + }, + icon = "Interface\\Icons\\Spell_Fire_Immolation", + maxRank = 5, + name = "强化献祭", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/1", + "需要5点破坏", + "需要20点毁灭系天赋", + "使你的毁灭系法术的致命一击伤害加成提高100%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_ShadowWordPain", + maxRank = 1, + name = "毁灭", + prereqColumn = 3, + prereqTier = 3, + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要25点毁灭系天赋", + "使你的火焰法术造成的伤害提高2%。", + }, + icon = "Interface\\Icons\\Spell_Fire_SelfDestruct", + maxRank = 5, + name = "余烬风暴", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要5点强化献祭", + "需要30点毁灭系天赋", + "165法力值", + "瞬发法术", + "点燃目标,造成240到306点火焰伤害,并消耗3秒的献祭效果来造成等量的伤害。", + }, + icon = "Interface\\Icons\\Spell_Fire_Fireball", + maxRank = 1, + name = "燃烧", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + numTabs = 3, + }, + WARRIOR = { + [1] = { + background = "WarriorArms", + icon = "Interface\\Icons\\INV_Sword_27", + name = "武器", + numTalents = 18, + talents = { + { + column = 1, + desc = { + "等级 3/3", + "使你的英勇打击技能所消耗的怒气值减少3点。", + }, + icon = "Interface\\Icons\\Ability_Rogue_Ambush", + maxRank = 3, + name = "强化英勇打击", + tier = 1, + }, + { + column = 2, + desc = { + "等级 3/5", + "在改变姿态的时候可以保留最多15点怒气值。", + " ", + "下一级:", + "在改变姿态的时候可以保留最多20点怒气值。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Nature_EnchantArmor", + maxRank = 5, + name = "战术掌握", + tier = 1, + }, + { + column = 3, + desc = { + "等级 2/2", + "使你的撕裂技能的流血伤害效果提高20%。", + }, + icon = "Interface\\Icons\\Ability_Gouge", + maxRank = 2, + name = "强化撕裂", + tier = 1, + }, + { + column = 1, + desc = { + "等级 2/2", + "使你的冲锋技能积攒的怒气值提高10点。", + }, + icon = "Interface\\Icons\\Ability_Warrior_Charge", + maxRank = 2, + name = "强化冲锋", + tier = 2, + }, + { + column = 2, + desc = { + "等级 0/5", + "使你的招架几率提高1%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Parry", + maxRank = 5, + name = "偏斜", + tier = 2, + }, + { + column = 4, + desc = { + "等级 0/3", + "使你的雷霆一击技能所消耗的怒气值减少1点,造成的伤害提高20%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_ThunderClap", + maxRank = 3, + name = "强化雷霆一击", + tier = 2, + }, + { + column = 1, + desc = { + "等级 1/1", + "20怒气", + "瞬发", + "进行一次特殊的攻击,造成35%的武器伤害,并根据你主手武器的不同,触发额外效果。\n\n锤:使目标迷惑,持续3秒。对其造成任何伤害都会解除此效果。\n剑:使目标失去武器,持续3秒。\n斧:使目标无法移动,持续4秒。\n长柄:如果目标正在骑乘,额外造成100%的武器伤害,并将其从坐骑上打下来。\n拳套:击倒目标,持续2秒。\n法杖:使你的招架几率提高25%,持续10秒。\n匕首:使目标沉默,持续3秒。", + }, + icon = "Interface\\Icons\\master_strike_1", + maxRank = 1, + name = "特效打击", + tier = 3, + }, + { + column = 2, + desc = { + "等级 1/2", + "使你的压制技能造成致命一击的几率提高25%。", + " ", + "下一级:", + "使你的压制技能造成致命一击的几率提高50%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\INV_Sword_05", + maxRank = 2, + name = "强化压制", + tier = 3, + }, + { + column = 3, + desc = { + "等级 3/3", + "你的致命一击导致目标流血,使其在6秒内遭受相当于你的武器平均伤害值的60%的伤害。", + }, + icon = "Interface\\Icons\\Ability_BackStab", + maxRank = 3, + name = "重伤", + prereqColumn = 3, + prereqTier = 1, + tier = 3, + }, + { + column = 2, + desc = { + "等级 3/3", + "使你的双手近战武器造成的伤害提高6%,所有双手武器的技能增加3点。", + }, + icon = "Interface\\Icons\\INV_Axe_09", + maxRank = 3, + name = "双手武器专精", + tier = 4, + }, + { + column = 3, + desc = { + "等级 2/2", + "使你在战斗姿态、防御姿态和狂暴姿态下的各种技能的致命一击伤害加成提高20%。", + }, + icon = "Interface\\Icons\\Ability_SearingArrow", + maxRank = 2, + name = "穿刺", + prereqColumn = 3, + prereqTier = 3, + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/5", + "根据使用的武器,给予战士额外的效果。\n\n斧 - 致命一击几率提高1%。\n锤 - 攻击忽略目标每级1.2点护甲。\n剑 - 击中目标后,有1%的几率对同一目标进行额外攻击。\n长柄武器 - 长柄武器的近战攻击射程增加0.4码。", + "点击这里学习", + }, + icon = "Interface\\Icons\\garrison_weaponupgrade", + maxRank = 5, + name = "武器大师", + prereqColumn = 1, + prereqTier = 3, + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "20怒气", + "瞬发", + "需要战斗姿态", + "你在接下来的5次技能或主手近战攻击中可以攻击到一个额外的敌人。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Rogue_SliceDice", + maxRank = 1, + name = "横扫攻击", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/3", + "斩杀的基础伤害提高15%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Hunter_SwiftStrike", + maxRank = 3, + name = "精准砍杀", + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/3", + "使你的反击风暴、鲁莽和盾墙技能的冷却时间减少4分钟。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Warrior_ImprovedDisciplines", + maxRank = 3, + name = "强化戒律", + tier = 5, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要25点武器系天赋", + "使你的猛击技能的施放时间和公共冷却时间减少0.3秒。", + }, + icon = "Interface\\Icons\\Ability_Warrior_DecisiveStrike", + maxRank = 2, + name = "强化猛击", + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要3点精准砍杀", + "需要25点武器系天赋", + "你的最大怒气值增加10。", + }, + icon = "Interface\\Icons\\Ability_Warrior_StrengthOfArms", + maxRank = 3, + name = "无边怒火", + prereqColumn = 3, + prereqTier = 5, + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点横扫攻击", + "需要30点武器系天赋", + "30怒气", + "瞬发", + "一次邪恶的攻击,对目标造成115%的武器伤害,并使目标受伤,任何形式的治疗对其产生的效果降低50%,持续10秒。", + }, + icon = "Interface\\Icons\\Ability_Warrior_SavageBlow", + maxRank = 1, + name = "致死打击", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + [2] = { + background = "WarriorFury", + icon = "Interface\\Icons\\Ability_Warrior_BattleShout", + name = "狂怒", + numTalents = 17, + talents = { + { + column = 2, + desc = { + "等级 0/5", + "使你的所有怒吼的作用范围提高12%,战斗怒吼和挫志怒吼的持续时间提高12%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Nature_Purge", + maxRank = 5, + name = "震耳嗓音", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/5", + "使你用近战武器对敌人造成致命一击的几率提高1%。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Rogue_Eviscerate", + maxRank = 5, + name = "残忍", + tier = 1, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要5点狂怒系天赋", + "使你的副手武器所造成的伤害提高5%,并且使你的副手武器命中率增加2%。", + }, + icon = "Interface\\Icons\\Ability_DualWield", + maxRank = 5, + name = "双武器专精", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点狂怒系天赋", + "使你有15%的几率在对敌人造成近战伤害之后获得额外的怒气值,双手武器的效果加倍。", + }, + icon = "Interface\\Icons\\Spell_Nature_StoneClawTotem", + maxRank = 5, + name = "怒不可遏", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/5", + "需要10点狂怒系天赋", + "使你的战斗怒吼提高近战攻击强度的效果提高5%,挫志怒吼降低敌人近战攻击强度的效果提高5%。", + }, + icon = "Interface\\Icons\\Ability_Warrior_CommandingShout", + maxRank = 5, + name = "强化怒吼", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要10点狂怒系天赋", + "10怒气", + "瞬发", + "使战士身边的所有敌人眩晕,移动速度降低50%,持续6秒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_DeathScream", + maxRank = 1, + name = "刺耳怒吼", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要10点狂怒系天赋", + "在受到致命一击之后的6秒内为你回复生命值总量的2%。", + }, + icon = "Interface\\Icons\\Spell_Shadow_SummonImp", + maxRank = 3, + name = "血之狂热", + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要15点狂怒系天赋", + "拦截和援护的冷却时间减少5秒,冲锋的冷却时间减少2秒。", + }, + icon = "Interface\\Icons\\Ability_Rogue_Sprint", + maxRank = 2, + name = "战场机动", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要15点狂怒系天赋", + "受到敌人的致命一击之后,你造成的近战伤害获得4%的额外加成,持续8秒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_UnholyFrenzy", + maxRank = 5, + name = "狂怒", + tier = 4, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要15点狂怒系天赋", + "你的拳击技能有50%几率使目标减速,持续4秒,目标该系法术沉默的持续时间延长1秒。", + }, + icon = "Interface\\Icons\\INV_Gauntlets_04", + maxRank = 2, + name = "强化拳击", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/3", + "需要20点狂怒系天赋", + "旋风斩的冷却时间减少1秒。", + }, + icon = "Interface\\Icons\\Ability_Whirlwind", + maxRank = 3, + name = "强化旋风斩", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点狂怒系天赋", + "10怒气", + "瞬发", + "激活之后,你的物理攻击伤害提高20%,并免疫恐惧效果,但是你的护甲值和所有抗性都降低20%。持续30秒。", + }, + icon = "Interface\\Icons\\Spell_Shadow_DeathPact", + maxRank = 1, + name = "死亡之愿", + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要20点狂怒系天赋", + "斩杀的怒气值消耗降低2点。", + }, + icon = "Interface\\Icons\\INV_Sword_48", + maxRank = 2, + name = "强化斩杀", + tier = 5, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要25点狂怒系天赋", + "使用狂暴之怒之后立即获得5点怒气值,并在激活时有50%的几率解除自身的移动限制效果。", + }, + icon = "Interface\\Icons\\Spell_Nature_AncestralGuardian", + maxRank = 2, + name = "强化狂暴之怒", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要1点死亡之愿", + "需要25点狂怒系天赋", + "在你的普通近战攻击打出致命一击之后,使你的下3次近战攻击速度提高10%。", + }, + icon = "Interface\\Icons\\Ability_GhoulFrenzy", + maxRank = 5, + name = "乱舞", + prereqColumn = 2, + prereqTier = 5, + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要5点狂怒", + "需要25点狂怒系天赋", + "你在狂怒、死亡之愿或鲁莽效果的影响下时,你的近战攻击和技能会为你恢复最大生命值的1%。", + }, + icon = "Interface\\Icons\\Racial_Troll_Berserk", + maxRank = 3, + name = "饮血者", + prereqColumn = 3, + prereqTier = 4, + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要30点狂怒系天赋", + "30怒气", + "瞬发", + "立即进行一次攻击,对目标造成100点伤害加上35%攻击强度的额外伤害,并使你的移动速度提高10%,持续6秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_BloodLust", + maxRank = 1, + name = "嗜血", + tier = 7, + }, + }, + }, + [3] = { + background = "WarriorProtection", + icon = "Interface\\Icons\\INV_Shield_06", + name = "防护", + numTalents = 19, + talents = { + { + column = 1, + desc = { + "等级 0/2", + "使你的血性狂暴技能激活瞬间所产生的怒气值增加2点。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Ability_Racial_BloodRage", + maxRank = 2, + name = "强化血性狂暴", + tier = 1, + }, + { + column = 2, + desc = { + "等级 0/5", + "使你用盾牌格挡攻击的几率提高1%,并且在格档后获得1点怒气值。", + "点击这里学习", + }, + icon = "Interface\\Icons\\INV_Shield_06", + maxRank = 5, + name = "盾牌专精", + tier = 1, + }, + { + column = 3, + desc = { + "等级 0/3", + "使你的防御技能提高7点。", + "点击这里学习", + }, + icon = "Interface\\Icons\\Spell_Nature_MirrorImage", + maxRank = 3, + name = "预知", + tier = 1, + }, + { + column = 2, + desc = { + "等级 0/5", + "需要5点防护系天赋", + "使你抵抗昏迷和魅惑效果的几率提高3%。", + }, + icon = "Interface\\Icons\\Spell_Magic_MageArmor", + maxRank = 5, + name = "钢铁意志", + tier = 2, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要5点防护系天赋", + "使你因装备而获得的护甲值提高2%,并且你的盾牌吸收的伤害量提高3%。", + }, + icon = "Interface\\Icons\\Spell_Holy_Devotion", + maxRank = 5, + name = "坚韧", + tier = 2, + }, + { + column = 1, + desc = { + "等级 0/1", + "需要2点强化血性狂暴", + "需要10点防护系天赋", + "瞬发", + "激活之后,使你暂时获得30%的生命值,持续20秒。在效果解除之后,这些生命值会被扣除。", + }, + icon = "Interface\\Icons\\Spell_Holy_AshesToAshes", + maxRank = 1, + name = "破釜沉舟", + prereqColumn = 1, + prereqTier = 1, + tier = 3, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要10点防护系天赋", + "使用援护后有50%几率获得免疫移动限制效果,持续3秒。", + }, + icon = "Interface\\Icons\\Ability_Warrior_Intervene", + maxRank = 2, + name = "强化援护", + tier = 3, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要10点防护系天赋", + "使你的嘲讽技能的冷却时间减少1秒。", + }, + icon = "Interface\\Icons\\Spell_Nature_Reincarnation", + maxRank = 2, + name = "强化嘲讽", + tier = 3, + }, + { + column = 4, + desc = { + "等级 0/3", + "需要10点防护系天赋", + "使你的复仇技能有15%的几率令目标昏迷3秒,并且冷却时间减少0.5秒。", + }, + icon = "Interface\\Icons\\Ability_Warrior_Revenge", + maxRank = 3, + name = "强化复仇", + tier = 3, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要15点防护系天赋", + "使你的盾牌猛击有50%几率额外再驱散目标身上1个魔法效果,盾击技能有50%的几率使目标沉默3秒。", + }, + icon = "Interface\\Icons\\Ability_Warrior_ShieldBash", + maxRank = 2, + name = "禁令", + tier = 4, + }, + { + column = 2, + desc = { + "等级 0/2", + "需要15点防护系天赋", + "使你的缴械技能的冷却时间减少10秒。", + }, + icon = "Interface\\Icons\\Ability_Warrior_Disarm", + maxRank = 2, + name = "强化缴械", + tier = 4, + }, + { + column = 3, + desc = { + "等级 0/5", + "需要15点防护系天赋", + "使你在防御姿态下由于攻击而造成的威胁值提高3%。", + }, + icon = "Interface\\Icons\\Ability_Warrior_InnerRage", + maxRank = 5, + name = "挑衅", + tier = 4, + }, + { + column = 1, + desc = { + "等级 0/5", + "需要20点防护系天赋", + "使你的单手近战武器所能造成的伤害提高2%。", + }, + icon = "Interface\\Icons\\INV_Sword_20", + maxRank = 5, + name = "单手武器专精", + tier = 5, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要20点防护系天赋", + "20怒气", + "瞬发", + "需要盾牌", + "用盾牌击打目标,对其造成174到184点伤害(此伤害数值受到盾牌格挡值和攻击强度加成影响),并有50%的几率驱散目标身上的1个魔法效果,同时产生大量的威胁值。", + }, + icon = "Interface\\Icons\\INV_Shield_05", + maxRank = 1, + name = "盾牌猛击", + tier = 5, + }, + { + column = 3, + desc = { + "等级 0/2", + "需要1点盾牌猛击", + "需要20点防护系天赋", + "盾牌猛击的冷却时间减少0.75秒,使用盾牌猛击会使你有35%几率在4.50秒内格挡下一次攻击。", + }, + icon = "Interface\\Icons\\Ability_Warrior_ShieldMastery", + maxRank = 2, + name = "强化盾牌猛击", + prereqColumn = 2, + prereqTier = 5, + tier = 5, + }, + { + column = 4, + desc = { + "等级 0/2", + "需要3点强化复仇", + "需要20点防护系天赋", + "使你的复仇技能造成的伤害提高25%,复仇命中后有50%的几率返还其怒气消耗。", + }, + icon = "Interface\\Icons\\ability_warrior_reprisal", + maxRank = 2, + name = "报复", + prereqColumn = 4, + prereqTier = 3, + tier = 5, + }, + { + column = 1, + desc = { + "等级 0/2", + "需要25点防护系天赋", + "盾墙的持续时间延长1秒,冷却时间减少5分钟。", + }, + icon = "Interface\\Icons\\Ability_Warrior_ShieldWall", + maxRank = 2, + name = "强化盾墙", + tier = 6, + }, + { + column = 3, + desc = { + "等级 0/3", + "需要25点防护系天赋", + "佩戴盾牌时,防御姿态增加威胁值的60%在离开防御姿态后继续生效。", + }, + icon = "Interface\\Icons\\ability_warrior_stalwartprotector", + maxRank = 3, + name = "防御战术", + tier = 6, + }, + { + column = 2, + desc = { + "等级 0/1", + "需要1点盾牌猛击", + "需要30点防护系天赋", + "5码距离", + "瞬发", + "立即产生10点怒气值,对目标造成115点伤害,并使其昏迷3秒。此技能造成大量威胁值,并可穿透敌人100%的护甲。", + }, + icon = "Interface\\Icons\\Ability_ThunderBolt", + maxRank = 1, + name = "震荡猛击", + prereqColumn = 2, + prereqTier = 5, + tier = 7, + }, + }, + }, + numTabs = 3, + }, +} + diff --git a/Tooltip.lua b/Tooltip.lua index d0d349c..0256e96 100644 --- a/Tooltip.lua +++ b/Tooltip.lua @@ -147,6 +147,11 @@ function SFrames.FloatingTooltip:Initialize() bg:SetAllPoints(bgFrame) GameTooltip._nanamiBGTex = bg + bgFrame:SetScript("OnUpdate", function() + if not GameTooltip:IsVisible() then + this:Hide() + end + end) end -------------------------------------------------------------------------- @@ -191,16 +196,27 @@ function SFrames.FloatingTooltip:Initialize() GameTooltipStatusBar.SetStatusBarColor = function() return end end + -------------------------------------------------------------------------- + -- Flag: true when tooltip was positioned via GameTooltip_SetDefaultAnchor + -- (world mouseover units). False for bag/bank/inventory item tooltips + -- that have their own anchoring — those should NOT be cursor-followed. + -------------------------------------------------------------------------- + local ttUsesDefaultAnchor = false + -------------------------------------------------------------------------- -- Track mouseover name/level for health estimation -------------------------------------------------------------------------- local ttMouseName, ttMouseLevel + local ttHadUnit = false local barEvents = CreateFrame("Frame", nil, GameTooltipStatusBar) barEvents:RegisterEvent("UPDATE_MOUSEOVER_UNIT") barEvents:SetScript("OnEvent", function() ttMouseName = UnitName("mouseover") ttMouseLevel = UnitLevel("mouseover") + if UnitExists("mouseover") then + ttHadUnit = true + end end) local function TT_Abbreviate(val) @@ -242,6 +258,7 @@ function SFrames.FloatingTooltip:Initialize() local orig_SetOwner = GameTooltip.SetOwner GameTooltip.SetOwner = function(self, owner, anchor, xOff, yOff) ttOwner = owner + ttUsesDefaultAnchor = false return orig_SetOwner(self, owner, anchor, xOff, yOff) end @@ -312,43 +329,64 @@ function SFrames.FloatingTooltip:Initialize() end end) - -- OnUpdate: throttled backdrop/bar refresh and cursor tracking + -- OnUpdate: line formatting (once) + cursor tracking (every frame) local orig_OnUpdate = GameTooltip:GetScript("OnUpdate") - local ttThrottle = 0 + local ttFormatThrottle = 0 GameTooltip:SetScript("OnUpdate", function() if orig_OnUpdate then orig_OnUpdate() end - ttThrottle = ttThrottle + arg1 - if ttThrottle < 0.05 then return end - ttThrottle = 0 + local isCursorMode = ttUsesDefaultAnchor and SFramesDB + and SFramesDB.tooltipMode == "CURSOR" and not ttIsMapMarker - TT_ApplyBackdrop(this) - TT_SyncBGFrame() - if not ttIsMapMarker then - TT_ShowBar(UnitExists("mouseover")) + if isCursorMode then + local hasUnit = UnitExists("mouseover") + + if ttHadUnit and not hasUnit then + TT_ShowBar(false) + if GameTooltip._nanamiBGFrame then + GameTooltip._nanamiBGFrame:Hide() + end + this:Hide() + return + end + + if not hasUnit then + TT_ShowBar(false) + end + + local x, y = GetCursorPosition() + local uiScale = UIParent:GetEffectiveScale() + local ttScale = this:GetScale() or 1 + if uiScale and uiScale > 0 and ttScale > 0 then + local effScale = uiScale * ttScale + local tx = (x / effScale) + 16 + local ty = (y / effScale) - 16 + this:ClearAllPoints() + this:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", tx, ty) + TT_SyncBGFrame() + end end + + ttFormatThrottle = ttFormatThrottle + arg1 + if ttFormatThrottle < 0.05 then return end + ttFormatThrottle = 0 + if not linesFormatted then linesFormatted = true if not ttIsMapMarker and UnitExists("mouseover") then SFrames.FloatingTooltip:FormatLines(this) end - end - - if SFramesDB and SFramesDB.tooltipMode == "CURSOR" and not ttIsMapMarker then - local x, y = GetCursorPosition() - local scale = UIParent:GetEffectiveScale() - if scale and scale > 0 then - this:ClearAllPoints() - this:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", (x / scale) + 16, (y / scale) - 16) - end + TT_SyncBGFrame() end end) - -- OnHide: hide bg frame, reset flag and owner tracking + -- OnHide: hide bg frame, health bar, reset flags and owner tracking local orig_OnHide = GameTooltip:GetScript("OnHide") GameTooltip:SetScript("OnHide", function() linesFormatted = false ttOwner = nil + ttHadUnit = false + TT_ShowBar(false) if GameTooltip._nanamiBGFrame then GameTooltip._nanamiBGFrame:Hide() end @@ -416,9 +454,11 @@ function SFrames.FloatingTooltip:Initialize() else orig_GameTooltip_SetDefaultAnchor(tooltip, parent) end + ttUsesDefaultAnchor = true end end SFrames.FloatingTooltip:ApplyConfig() + SFrames.FloatingTooltip:ApplyScale() -- WorldMapTooltip: raw textures on a child frame (SetBackdrop is unreliable) if WorldMapTooltip and not WorldMapTooltip._nanamiBG then @@ -451,15 +491,35 @@ function SFrames.FloatingTooltip:Initialize() if SFrames.ItemCompare and SFrames.ItemCompare.HookTooltips then SFrames.ItemCompare:HookTooltips() end + + if SFrames.Movers and SFrames.Movers.RegisterMover and self.anchor then + SFrames.Movers:RegisterMover("Tooltip", self.anchor, "提示框", + "BOTTOMRIGHT", "UIParent", "BOTTOMRIGHT", -20, 100) + end end function SFrames.FloatingTooltip:ApplyConfig() - if SFramesDB and SFramesDB.tooltipX and SFramesDB.tooltipY and self.anchor then + if not self.anchor then return end + local pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["Tooltip"] + if pos and pos.point and pos.relativePoint then + self.anchor:ClearAllPoints() + self.anchor:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0) + elseif SFramesDB and SFramesDB.tooltipX and SFramesDB.tooltipY then self.anchor:ClearAllPoints() self.anchor:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", SFramesDB.tooltipX, SFramesDB.tooltipY) end end +function SFrames.FloatingTooltip:ApplyScale() + local scale = SFramesDB and SFramesDB.tooltipScale or 1.0 + if scale < 0.5 then scale = 0.5 end + if scale > 2.0 then scale = 2.0 end + GameTooltip:SetScale(scale) + if GameTooltip._nanamiBGFrame then + GameTooltip._nanamiBGFrame:SetScale(scale) + end +end + function SFrames.FloatingTooltip:ToggleAnchor(show) if not self.anchor then return end if show then diff --git a/TradeSkillUI.lua b/TradeSkillUI.lua index be4fa8b..ee6bf84 100644 --- a/TradeSkillUI.lua +++ b/TradeSkillUI.lua @@ -81,7 +81,7 @@ local PROF_SPELLS = { ["急救"]=true,["First Aid"]=true, ["熔炼"]=true,["Smelting"]=true, ["毒药"]=true,["Poisons"]=true, - ["训练野兽"]=true,["Beast Training"]=true, + ["生存"]=true,["Survival"]=true, } -------------------------------------------------------------------------------- @@ -1129,7 +1129,12 @@ function TSUI.UpdateList() row.selGlow:Show() row.selTop:Show() row.selBot:Show() - row.nameFS:SetTextColor(1, 1, 1) + local dc = T.DIFFICULTY[entry.data.difficulty] or T.DIFFICULTY.trivial + row.nameFS:SetTextColor( + math.min(1, dc[1] + 0.3), + math.min(1, dc[2] + 0.3), + math.min(1, dc[3] + 0.3) + ) else row.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4]) row.iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4]) @@ -2056,6 +2061,8 @@ function TSUI:Initialize() S.MainFrame:Hide() end elseif event == "CRAFT_SHOW" then + local craftName = GetCraftName and GetCraftName() or "" + if craftName == "训练野兽" or craftName == "Beast Training" then return end S.switchStartTime = nil S.currentMode = "craft" if CraftFrame then diff --git a/TrainerUI.lua b/TrainerUI.lua index d040ab3..1f9bdd3 100644 --- a/TrainerUI.lua +++ b/TrainerUI.lua @@ -1181,6 +1181,12 @@ function TUI:Initialize() ClassTrainerFrame:EnableMouse(false) end + if SetTrainerServiceTypeFilter then + SetTrainerServiceTypeFilter("available", 1) + SetTrainerServiceTypeFilter("unavailable", 1) + SetTrainerServiceTypeFilter("used", 1) + end + selectedIndex = nil currentFilter = "all" collapsedCats = {} diff --git a/Tweaks.lua b/Tweaks.lua index df120a4..4161009 100644 --- a/Tweaks.lua +++ b/Tweaks.lua @@ -83,6 +83,8 @@ local function InitAutoDismount() local scanner = CreateFrame("GameTooltip", "NanamiDismountScan", nil, "GameTooltipTemplate") scanner:SetOwner(WorldFrame, "ANCHOR_NONE") + scanner:SetAlpha(0) + scanner:Hide() local mountStrings = { "^Increases speed by (.+)%%", diff --git a/Units/Party.lua b/Units/Party.lua index cf2a639..9d4a1b4 100644 --- a/Units/Party.lua +++ b/Units/Party.lua @@ -582,6 +582,11 @@ function SFrames.Party:Initialize() end) self:UpdateAll() + + if SFrames.Movers and SFrames.Movers.RegisterMover and self.parent then + SFrames.Movers:RegisterMover("PartyFrame", self.parent, "小队", + "TOPLEFT", "UIParent", "TOPLEFT", 15, -150) + end end function SFrames.Party:CreateAuras(index) @@ -1042,6 +1047,7 @@ function SFrames.Party:UpdateAuras(unit) SFrames.Tooltip:ClearLines() SFrames.Tooltip:SetUnitDebuff(unit, i) local timeLeft = SFrames:GetAuraTimeLeft(unit, i, false) + SFrames.Tooltip:Hide() if timeLeft and timeLeft > 0 then local newExp = GetTime() + timeLeft if not b.expirationTime or math.abs(b.expirationTime - newExp) > 2 then @@ -1093,6 +1099,7 @@ function SFrames.Party:UpdateAuras(unit) SFrames.Tooltip:ClearLines() SFrames.Tooltip:SetUnitBuff(unit, i) local timeLeft = SFrames:GetAuraTimeLeft(unit, i, true) + SFrames.Tooltip:Hide() if timeLeft and timeLeft > 0 then local newExp = GetTime() + timeLeft if not b.expirationTime or math.abs(b.expirationTime - newExp) > 2 then diff --git a/Units/Pet.lua b/Units/Pet.lua index 1eaed82..3970667 100644 --- a/Units/Pet.lua +++ b/Units/Pet.lua @@ -7,6 +7,220 @@ local function Clamp(value, minValue, maxValue) return value end +function SFrames.Pet:ShowContextMenu() + if not self.contextMenu then + self.contextMenu = CreateFrame("Frame", "SFramesPetContextDD", UIParent, "UIDropDownMenuTemplate") + end + UIDropDownMenu_Initialize(self.contextMenu, function() + local info + + info = {} + info.text = "查看属性" + info.notCheckable = 1 + info.func = function() + ToggleCharacter("PetPaperDollFrame") + end + UIDropDownMenu_AddButton(info) + + local hasPetUI, isHunterPet = HasPetUI() + if isHunterPet then + info = {} + info.text = "重命名" + info.notCheckable = 1 + info.func = function() SFrames.Pet:ShowRenameDialog() end + UIDropDownMenu_AddButton(info) + + info = {} + info.text = "解散宠物" + info.notCheckable = 1 + info.func = function() if PetDismiss then PetDismiss() end end + UIDropDownMenu_AddButton(info) + + info = {} + info.text = "放弃宠物" + info.notCheckable = 1 + info.textR = 1; info.textG = 0.3; info.textB = 0.3 + info.func = function() if PetAbandon then PetAbandon() end end + UIDropDownMenu_AddButton(info) + else + info = {} + info.text = "解散宠物" + info.notCheckable = 1 + info.func = function() if PetDismiss then PetDismiss() end end + UIDropDownMenu_AddButton(info) + end + + info = {} + info.text = CANCEL or "取消" + info.notCheckable = 1 + info.func = function() CloseDropDownMenus() end + UIDropDownMenu_AddButton(info) + end, "MENU") + ToggleDropDownMenu(1, nil, self.contextMenu, "SFramesPetFrame", 106, 27) +end + +function SFrames.Pet:CreateRenameFrame() + local T = SFrames.ActiveTheme + local font = SFrames:GetFont() + local outline = (SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" + + local f = CreateFrame("Frame", "SFramesPetRenameDialog", UIParent) + f:SetWidth(300) + f:SetHeight(120) + f:SetPoint("CENTER", UIParent, "CENTER", 0, 80) + f:SetFrameStrata("DIALOG") + f:SetToplevel(true) + f:EnableMouse(true) + f:SetMovable(true) + f:RegisterForDrag("LeftButton") + f:SetScript("OnDragStart", function() this:StartMoving() end) + f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + f: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 }, + }) + f:SetBackdropColor(T.panelBg[1], T.panelBg[2], T.panelBg[3], T.panelBg[4]) + f:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], T.panelBorder[4]) + + local shadow = CreateFrame("Frame", nil, f) + shadow:SetPoint("TOPLEFT", f, "TOPLEFT", -4, 4) + shadow:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 4, -4) + shadow: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 }, + }) + shadow:SetBackdropColor(0, 0, 0, 0.45) + shadow:SetBackdropBorderColor(0, 0, 0, 0.6) + shadow:SetFrameLevel(math.max(0, f:GetFrameLevel() - 1)) + + local header = CreateFrame("Frame", nil, f) + header:SetPoint("TOPLEFT", f, "TOPLEFT", 3, -3) + header:SetPoint("TOPRIGHT", f, "TOPRIGHT", -3, -3) + header:SetHeight(26) + header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" }) + header:SetBackdropColor(T.headerBg[1], T.headerBg[2], T.headerBg[3], T.headerBg[4]) + + local titleFS = header:CreateFontString(nil, "OVERLAY") + titleFS:SetFont(font, 12, outline) + titleFS:SetPoint("CENTER", header, "CENTER", 0, 0) + titleFS:SetText("宠物重命名") + titleFS:SetTextColor(T.gold[1], T.gold[2], T.gold[3]) + + local hsep = f:CreateTexture(nil, "ARTWORK") + hsep:SetTexture("Interface\\Buttons\\WHITE8X8") + hsep:SetHeight(1) + hsep:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -29) + hsep:SetPoint("TOPRIGHT", f, "TOPRIGHT", -4, -29) + hsep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4]) + + local eb = CreateFrame("EditBox", "SFramesPetRenameEditBox", f) + eb:SetWidth(260) + eb:SetHeight(24) + eb:SetPoint("TOP", f, "TOP", 0, -42) + eb:SetFont(font, 12, outline) + eb:SetAutoFocus(false) + eb:SetMaxLetters(24) + eb:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + eb:SetBackdropColor(T.inputBg[1], T.inputBg[2], T.inputBg[3], T.inputBg[4]) + eb:SetBackdropBorderColor(T.inputBorder[1], T.inputBorder[2], T.inputBorder[3], T.inputBorder[4]) + eb:SetTextInsets(8, 8, 0, 0) + eb:SetTextColor(1, 1, 1) + + eb:SetScript("OnEnterPressed", function() + SFrames.Pet:DoRename(this:GetText()) + end) + eb:SetScript("OnEscapePressed", function() + SFrames.Pet.renameFrame:Hide() + end) + eb:SetScript("OnEditFocusGained", function() + this:SetBackdropBorderColor(T.accent[1], T.accent[2], T.accent[3], 1) + end) + eb:SetScript("OnEditFocusLost", function() + this:SetBackdropBorderColor(T.inputBorder[1], T.inputBorder[2], T.inputBorder[3], T.inputBorder[4]) + end) + + f.editBox = eb + + local function CreateBtn(text, parent) + local btn = CreateFrame("Button", nil, parent) + btn:SetWidth(120) + btn:SetHeight(26) + btn:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeFile = "Interface\\Buttons\\WHITE8X8", + tile = false, tileSize = 0, edgeSize = 1, + insets = { left = 1, right = 1, top = 1, bottom = 1 }, + }) + btn:SetBackdropColor(T.btnBg[1], T.btnBg[2], T.btnBg[3], T.btnBg[4]) + btn:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4]) + local fs = btn:CreateFontString(nil, "OVERLAY") + fs:SetFont(font, 11, outline) + fs:SetPoint("CENTER", 0, 0) + fs:SetText(text) + fs:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + btn.label = 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]) + this.label:SetTextColor(T.btnActiveText[1], T.btnActiveText[2], T.btnActiveText[3]) + 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]) + this.label:SetTextColor(T.btnText[1], T.btnText[2], T.btnText[3]) + end) + return btn + end + + local confirmBtn = CreateBtn("确定", f) + confirmBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOM", -4, 10) + confirmBtn:SetScript("OnClick", function() + SFrames.Pet:DoRename(f.editBox:GetText()) + end) + + local cancelBtn = CreateBtn("取消", f) + cancelBtn:SetPoint("BOTTOMLEFT", f, "BOTTOM", 4, 10) + cancelBtn:SetScript("OnClick", function() + f:Hide() + end) + + f:Hide() + table.insert(UISpecialFrames, "SFramesPetRenameDialog") + self.renameFrame = f +end + +function SFrames.Pet:ShowRenameDialog() + if not UnitExists("pet") then return end + if not self.renameFrame then + self:CreateRenameFrame() + end + local currentName = UnitName("pet") or "" + self.renameFrame.editBox:SetText(currentName) + self.renameFrame:Show() + self.renameFrame.editBox:SetFocus() + self.renameFrame.editBox:HighlightText() +end + +function SFrames.Pet:DoRename(name) + if not name or name == "" then return end + if PetRename then + PetRename(name) + end + if self.renameFrame then + self.renameFrame:Hide() + end +end + function SFrames.Pet:Initialize() local f = CreateFrame("Button", "SFramesPetFrame", UIParent) f:SetWidth(150) @@ -45,7 +259,7 @@ function SFrames.Pet:Initialize() TargetUnit("pet") end else - ToggleDropDownMenu(1, nil, PetFrameDropDown, "SFramesPetFrame", 106, 27) + SFrames.Pet:ShowContextMenu() end end) @@ -158,6 +372,22 @@ function SFrames.Pet:Initialize() self:InitFoodFeature() self:UpdateAll() + + if SFrames.Movers and SFrames.Movers.RegisterMover and self.frame then + SFrames.Movers:RegisterMover("PetFrame", self.frame, "宠物", + "TOPLEFT", "SFramesPlayerFrame", "BOTTOMLEFT", 10, -55) + end + + if StaticPopup_Show then + local origStaticPopupShow = StaticPopup_Show + StaticPopup_Show = function(which, a1, a2, a3) + if which == "RENAME_PET" then + SFrames.Pet:ShowRenameDialog() + return + end + return origStaticPopupShow(which, a1, a2, a3) + end + end end function SFrames.Pet:UpdateAll() diff --git a/Units/Player.lua b/Units/Player.lua index 89e42c8..46c4a2f 100644 --- a/Units/Player.lua +++ b/Units/Player.lua @@ -105,27 +105,52 @@ function SFrames.Player:ApplyConfig() local cfg = self:GetConfig() local f = self.frame + local db = SFramesDB or {} + + local showPortrait = db.playerShowPortrait ~= false + local frameAlpha = tonumber(db.playerFrameAlpha) or 1 + frameAlpha = Clamp(frameAlpha, 0.1, 1.0) f:SetScale(cfg.scale) f:SetWidth(cfg.width) f:SetHeight(cfg.height) + f:SetAlpha(frameAlpha) - if f.portrait then - f.portrait:SetWidth(cfg.portraitWidth) - f.portrait:SetHeight(cfg.height - 2) - end - - if f.portraitBG then - f.portraitBG:ClearAllPoints() - f.portraitBG:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) - f.portraitBG:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMRIGHT", 1, -1) - end - - if f.health then - f.health:ClearAllPoints() - f.health:SetPoint("TOPLEFT", f.portrait, "TOPRIGHT", 1, 0) - f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) - f.health:SetHeight(cfg.healthHeight) + if showPortrait then + if f.portrait then + f.portrait:SetWidth(cfg.portraitWidth) + f.portrait:SetHeight(cfg.height - 2) + f.portrait:Show() + end + if f.portraitBG then + f.portraitBG:ClearAllPoints() + f.portraitBG:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) + f.portraitBG:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMRIGHT", 1, -1) + f.portraitBG:Show() + end + if f.health then + f.health:ClearAllPoints() + f.health:SetPoint("TOPLEFT", f.portrait, "TOPRIGHT", 1, 0) + f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) + f.health:SetHeight(cfg.healthHeight) + end + if f.classIcon and f.classIcon.overlay then + f.classIcon.overlay:ClearAllPoints() + f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0) + end + else + if f.portrait then f.portrait:Hide() end + if f.portraitBG then f.portraitBG:Hide() end + if f.health then + f.health:ClearAllPoints() + f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) + f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) + f.health:SetHeight(cfg.healthHeight) + end + if f.classIcon and f.classIcon.overlay then + f.classIcon.overlay:ClearAllPoints() + f.classIcon.overlay:SetPoint("CENTER", f, "TOPLEFT", 8, 0) + end end if f.healthBGFrame then @@ -147,6 +172,21 @@ function SFrames.Player:ApplyConfig() f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1) end + if f.restOverlay then + if showPortrait then f.restOverlay:SetAlpha(1) else f.restOverlay:SetAlpha(0) end + end + + if f.castbar then + f.castbar:ClearAllPoints() + if showPortrait then + f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", 0, 6) + f.castbar:SetPoint("BOTTOMLEFT", f.portrait, "TOPLEFT", SFrames.Config.castbarHeight + 6, 6) + else + f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", 0, 6) + f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", SFrames.Config.castbarHeight + 6, 6) + end + end + local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" local fontPath = SFrames:GetFont() @@ -471,7 +511,13 @@ function SFrames.Player:Initialize() SFrames:RegisterEvent("PARTY_MEMBERS_CHANGED", function() self:UpdateLeaderIcon() end) SFrames:RegisterEvent("PARTY_LEADER_CHANGED", function() self:UpdateLeaderIcon() end) SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcon() end) - SFrames:RegisterEvent("UNIT_PORTRAIT_UPDATE", function() if arg1 == "player" then self.frame.portrait:SetUnit("player") self.frame.portrait:SetCamera(0) self.frame.portrait:SetPosition(-1.0, 0, 0) end end) + SFrames:RegisterEvent("UNIT_PORTRAIT_UPDATE", function() + if arg1 == "player" and self.frame.portrait and not (SFramesDB and SFramesDB.playerShowPortrait == false) then + self.frame.portrait:SetUnit("player") + self.frame.portrait:SetCamera(0) + self.frame.portrait:SetPosition(-1.0, 0, 0) + end + end) SFrames:RegisterEvent("UNIT_DISPLAYPOWER", function() if arg1 == "player" then self:UpdatePowerType(); self:UpdatePower() end end) SFrames:RegisterEvent("UPDATE_SHAPESHIFT_FORM", function() self:UpdatePowerType(); self:UpdatePower() end) SFrames:RegisterEvent("PLAYER_UPDATE_RESTING", function() self:UpdateRestingStatus() end) @@ -587,6 +633,12 @@ function SFrames.Player:ScanTrainer() return end + if SetTrainerServiceTypeFilter then + SetTrainerServiceTypeFilter("available", 1) + SetTrainerServiceTypeFilter("unavailable", 1) + SetTrainerServiceTypeFilter("used", 1) + end + local _, classEn = UnitClass("player") if not classEn or not SFramesDB then self.scanningTrainer = nil return end @@ -966,9 +1018,12 @@ function SFrames.Player:UpdateAll() formattedLevel = formattedLevel .. " " end - self.frame.portrait:SetUnit("player") - self.frame.portrait:SetCamera(0) - self.frame.portrait:SetPosition(-1.0, 0, 0) + local showPortrait = not (SFramesDB and SFramesDB.playerShowPortrait == false) + if showPortrait and self.frame.portrait then + self.frame.portrait:SetUnit("player") + self.frame.portrait:SetCamera(0) + self.frame.portrait:SetPosition(-1.0, 0, 0) + end -- Class Color for Health local localizedClass, class = UnitClass("player") @@ -1477,6 +1532,12 @@ function SFrames.Player:Initialize() CastingBarFrame:UnregisterAllEvents() CastingBarFrame:Hide() end + + -- Register mover + if SFrames.Movers and SFrames.Movers.RegisterMover and self.frame then + SFrames.Movers:RegisterMover("PlayerFrame", self.frame, "玩家", + "CENTER", "UIParent", "CENTER", -200, -100) + end end -------------------------------------------------------------------------------- diff --git a/Units/Raid.lua b/Units/Raid.lua index 042c169..35c7476 100644 --- a/Units/Raid.lua +++ b/Units/Raid.lua @@ -259,6 +259,11 @@ function SFrames.Raid:Initialize() SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcons() end) self:UpdateAll() + + if SFrames.Movers and SFrames.Movers.RegisterMover and self.parent then + SFrames.Movers:RegisterMover("RaidFrame", self.parent, "团队", + "TOPLEFT", "UIParent", "TOPLEFT", 15, -200) + end end function SFrames.Raid:EnsureFrames() @@ -1003,6 +1008,7 @@ function SFrames.Raid:UpdateAuras(unit) SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") SFrames.Tooltip:SetUnitBuff(unit, i) local buffName = SFramesScanTooltipTextLeft1:GetText() + SFrames.Tooltip:Hide() if buffName then for pos, listData in pairs(buffsNeeded) do @@ -1039,6 +1045,7 @@ function SFrames.Raid:UpdateAuras(unit) SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") SFrames.Tooltip:SetUnitDebuff(unit, i) local debuffName = SFramesScanTooltipTextLeft1:GetText() + SFrames.Tooltip:Hide() if debuffName then for pos, listData in pairs(buffsNeeded) do diff --git a/Units/TalentTree.lua b/Units/TalentTree.lua index 2d6596f..23eb568 100644 --- a/Units/TalentTree.lua +++ b/Units/TalentTree.lua @@ -5,7 +5,7 @@ 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 +local FRAME_HEIGHT = 550 -------------------------------------------------------------------------------- -- Theme: Pink Cat-Paw @@ -30,6 +30,12 @@ local CLASS_LIST = { { 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 -------------------------------------------------------------------------------- @@ -133,12 +139,126 @@ local function StyleButton(btn, label) end -------------------------------------------------------------------------------- --- Talent data cache (stores talent trees per class in SFramesGlobalDB) +-- 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 - return SFramesGlobalDB.talentCache + + 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() @@ -262,12 +382,17 @@ function SFrames.TalentTree:Initialize() 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 -------------------------------------------------------------------------------- @@ -358,11 +483,19 @@ function SFrames.TalentTree:CreateMainFrame() 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() + 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() @@ -378,40 +511,86 @@ function SFrames.TalentTree:CreateMainFrame() 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.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("LEFT", f.pointsText, "LEFT", 0, -18) + f.simLabel:SetPoint("BOTTOMLEFT", f.overlay, "BOTTOMLEFT", 14, 18) + f.simLabel:SetWidth(FRAME_WIDTH - 120) f.simLabel:SetText("") - -- Bottom buttons + 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.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) - StyleButton(f.btnSimMode, "|cff888888模拟加点: 关|r") + f.btnSimMode:SetFrameLevel(btnLevel) + StyleButton(f.btnSimMode, "|cff888888预览模式: 关|r") f.simModeText = f.btnSimMode.nanamiLabel - f.btnSimMode.nanamiTooltip = { "模拟加点模式", "开启后可用左键虚拟加点、右键取消,\n确认后点「应用天赋」才会实际扣分。\n可切换职业模拟其他职业加点。" } + 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 = {} @@ -427,20 +606,23 @@ end 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) + 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 then + 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]) @@ -455,20 +637,22 @@ function SFrames.TalentTree:UpdateSimModeLabel() 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.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.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.simModeText:SetText("|cff888888预览模式: 关|r") self.frame.simLabel:SetText("") self.frame.classBar:Hide() - self.frame.btnApply:Show() + self.frame.btnApply:Hide() + self.frame.btnReset:Hide() if self.viewingClass and self.viewingClass ~= self.playerClass then self.viewingClass = self.playerClass self.virtualPoints = {} @@ -489,9 +673,9 @@ function SFrames.TalentTree:SwitchViewClass(classKey) 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) + local hasData = HasCacheData(classKey) + if not hasData then + UIErrorsFrame:AddMessage("该职业天赋数据不可用,请先用该职业角色登录或更新默认数据库", 1, 0.5, 0.5, 1) return end end @@ -541,7 +725,7 @@ function SFrames.TalentTree:BuildTrees() local tabFrame = CreateFrame("Frame", nil, self.frame) tabFrame:SetWidth(TAB_WIDTH) - tabFrame:SetHeight(FRAME_HEIGHT - 90) + tabFrame:SetHeight(FRAME_HEIGHT - 120) local offsetX = 10 + ((t - 1) * (TAB_WIDTH + 5)) tabFrame:SetPoint("TOPLEFT", self.frame, "TOPLEFT", offsetX, treeTop) @@ -556,16 +740,17 @@ function SFrames.TalentTree:BuildTrees() end local tTitle = MakeFS(tabFrame, 14, "CENTER", T.titleColor) - tTitle:SetPoint("TOP", tabFrame, "TOP", 0, -10) + 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, -2) + 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 @@ -579,7 +764,7 @@ function SFrames.TalentTree:BuildTrees() 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 + local y = -(tier - 1) * (ICON_SIZE + ICON_SPACING_Y) - GRID_TOP btn:SetPoint("TOPLEFT", tabFrame, "TOPLEFT", x, y) btn.icon = btn:CreateTexture(nil, "ARTWORK") @@ -668,9 +853,9 @@ function SFrames.TalentTree:BuildTrees() 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 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) - 44 + local cY = -(tier - 1) * (ICON_SIZE + ICON_SPACING_Y) - GRID_TOP local pCenterX = pX + (ICON_SIZE / 2) local pCenterY = pY - (ICON_SIZE / 2) @@ -912,6 +1097,20 @@ function SFrames.TalentTree:ResetVirtualPoints() 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 @@ -940,12 +1139,15 @@ function SFrames.TalentTree:ApplyVirtualPoints() for tb = 1, GetNumTalentTabs() do local treeTalents = {} for idx = 1, GetNumTalents(tb) do - local name, icon, tier, column, realRank = GetTalentInfo(tb, idx) + 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}) + table.insert(treeTalents, { + tab = tb, index = idx, tier = tier or 1, + name = name or "?", toRank = realRank + i, maxRank = maxRank or 1, + }) end end end @@ -961,7 +1163,11 @@ function SFrames.TalentTree:ApplyVirtualPoints() end local total = table.getn(self.applyQueue) - DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 开始应用 " .. total .. " 个天赋点...") + 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") @@ -976,9 +1182,11 @@ function SFrames.TalentTree:ApplyVirtualPoints() 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 天赋应用完成。") + DEFAULT_CHAT_FRAME:AddMessage("|c" .. GetHex() .. "Nanami:|r 天赋应用完成!共应用 " .. (self.applyTotal or 0) .. " 个天赋点。") + self.frame.simLabel:SetText("|cff00ff00天赋应用完成!|r") end local function TryNextTalent() @@ -987,6 +1195,9 @@ function SFrames.TalentTree:ApplyVirtualPoints() 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 @@ -1113,3 +1324,1024 @@ function SFrames.TalentTree:Update() 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 diff --git a/Units/Target.lua b/Units/Target.lua index 5240561..6e127c8 100644 --- a/Units/Target.lua +++ b/Units/Target.lua @@ -11,6 +11,10 @@ local function Clamp(value, minValue, maxValue) return value end +local DIST_BASE_WIDTH = 80 +local DIST_BASE_HEIGHT = 24 +local DIST_BASE_FONTSIZE = 14 + function SFrames.Target:GetDistance(unit) if not UnitExists(unit) then return nil end if UnitIsUnit(unit, "player") then return "0 码" end @@ -69,27 +73,60 @@ function SFrames.Target:ApplyConfig() local cfg = self:GetConfig() local f = self.frame + local db = SFramesDB or {} + + local showPortrait = db.targetShowPortrait ~= false + local frameAlpha = tonumber(db.targetFrameAlpha) or 1 + frameAlpha = Clamp(frameAlpha, 0.1, 1.0) f:SetScale(cfg.scale) f:SetWidth(cfg.width) f:SetHeight(cfg.height) + f:SetAlpha(frameAlpha) - if f.portrait then - f.portrait:SetWidth(cfg.portraitWidth) - f.portrait:SetHeight(cfg.height - 2) - end - - if f.portraitBG then - f.portraitBG:ClearAllPoints() - f.portraitBG:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -1, 0) - f.portraitBG:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) - end - - if f.health then - f.health:ClearAllPoints() - f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) - f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0) - f.health:SetHeight(cfg.healthHeight) + if showPortrait then + if f.portrait then + f.portrait:SetWidth(cfg.portraitWidth) + f.portrait:SetHeight(cfg.height - 2) + f.portrait:Show() + end + if f.portraitBG then + f.portraitBG:ClearAllPoints() + f.portraitBG:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -1, 0) + f.portraitBG:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0) + f.portraitBG:Show() + end + if f.health then + f.health:ClearAllPoints() + f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) + f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0) + f.health:SetHeight(cfg.healthHeight) + end + if f.classIcon and f.classIcon.overlay then + f.classIcon.overlay:ClearAllPoints() + f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0) + end + if f.comboText then + f.comboText:ClearAllPoints() + f.comboText:SetPoint("CENTER", f.portrait, "CENTER", 0, 0) + end + else + if f.portrait then f.portrait:Hide() end + if f.portraitBG then f.portraitBG:Hide() end + if f.health then + f.health:ClearAllPoints() + f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1) + f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1) + f.health:SetHeight(cfg.healthHeight) + end + if f.classIcon and f.classIcon.overlay then + f.classIcon.overlay:ClearAllPoints() + f.classIcon.overlay:SetPoint("CENTER", f, "TOPRIGHT", -8, 0) + end + if f.comboText then + f.comboText:ClearAllPoints() + f.comboText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0) + end end if f.healthBGFrame then @@ -124,9 +161,20 @@ function SFrames.Target:ApplyConfig() f.powerText:SetFont(fontPath, cfg.valueFont, outline) end + if f.castbar then + f.castbar:ClearAllPoints() + if showPortrait then + f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", 0, 6) + f.castbar:SetPoint("BOTTOMRIGHT", f.portrait, "TOPRIGHT", -(SFrames.Config.castbarHeight + 6), 6) + else + f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", 0, 6) + f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -(SFrames.Config.castbarHeight + 6), 6) + end + end + if self.distanceFrame then local dScale = tonumber(SFramesDB and SFramesDB.targetDistanceScale) or 1 - self.distanceFrame:SetScale(Clamp(dScale, 0.7, 1.8)) + self:ApplyDistanceScale(dScale) end if UnitExists("target") then @@ -134,22 +182,36 @@ function SFrames.Target:ApplyConfig() end end +function SFrames.Target:ApplyDistanceScale(scale) + local f = self.distanceFrame + if not f then return end + scale = Clamp(tonumber(scale) or 1, 0.7, 1.8) + f:SetWidth(DIST_BASE_WIDTH * scale) + f:SetHeight(DIST_BASE_HEIGHT * scale) + if f.text then + local fontPath = SFrames:GetFont() + local outline = (SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE" + local fontSize = math.max(8, math.floor(DIST_BASE_FONTSIZE * scale + 0.5)) + f.text:SetFont(fontPath, fontSize, outline) + end +end + function SFrames.Target:InitializeDistanceFrame() local f = CreateFrame("Button", "SFramesTargetDistanceFrame", UIParent) - f:SetWidth(80) - f:SetHeight(24) f:SetFrameStrata("HIGH") - local frameScale = (SFramesDB and type(SFramesDB.targetDistanceScale) == "number") and SFramesDB.targetDistanceScale or 1 - f:SetScale(frameScale) - + + local dScale = (SFramesDB and type(SFramesDB.targetDistanceScale) == "number") and SFramesDB.targetDistanceScale or 1 + dScale = Clamp(dScale, 0.7, 1.8) + f:SetWidth(DIST_BASE_WIDTH * dScale) + f:SetHeight(DIST_BASE_HEIGHT * dScale) + if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["TargetDistanceFrame"] then local pos = SFramesDB.Positions["TargetDistanceFrame"] f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs) else - -- Default position: Center of screen for visibility if first time f:SetPoint("CENTER", UIParent, "CENTER", 0, 100) end - + f:SetMovable(true) f:EnableMouse(true) f:RegisterForDrag("LeftButton") @@ -161,29 +223,29 @@ function SFrames.Target:InitializeDistanceFrame() local point, relativeTo, relativePoint, xOfs, yOfs = this:GetPoint() SFramesDB.Positions["TargetDistanceFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs } end) - + SFrames:CreateUnitBackdrop(f) - f:SetBackdrop(nil) -- Remove border and background for natural look - - f.text = SFrames:CreateFontString(f, 14, "CENTER") + f:SetBackdrop(nil) + + local fontSize = math.max(8, math.floor(DIST_BASE_FONTSIZE * dScale + 0.5)) + f.text = SFrames:CreateFontString(f, fontSize, "CENTER") f.text:SetPoint("CENTER", f, "CENTER", 0, 0) f.text:SetTextColor(1, 0.8, 0.2) f.text:SetShadowColor(0, 0, 0, 1) f.text:SetShadowOffset(1, -1) - + SFrames.Target.distanceFrame = f f:Hide() - - -- Distance Updater on the frame itself + f.timer = 0 f:SetScript("OnUpdate", function() if SFramesDB and SFramesDB.targetDistanceEnabled == false then if this:IsShown() then this:Hide() end return end - if not UnitExists("target") then + if not UnitExists("target") then if this:IsShown() then this:Hide() end - return + return end this.timer = this.timer + (arg1 or 0) if this.timer >= 0.4 then @@ -391,7 +453,13 @@ function SFrames.Target:Initialize() SFrames:RegisterEvent("UNIT_MAXRAGE", function() if arg1 == "target" then self:UpdatePower() end end) SFrames:RegisterEvent("PLAYER_COMBO_POINTS", function() self:UpdateComboPoints() end) SFrames:RegisterEvent("UNIT_DISPLAYPOWER", function() if arg1 == "target" then self:UpdatePowerType() end end) - SFrames:RegisterEvent("UNIT_PORTRAIT_UPDATE", function() if arg1 == "target" then self.frame.portrait:SetUnit("target") self.frame.portrait:SetCamera(0) self.frame.portrait:SetPosition(-1.0, 0, 0) end end) + SFrames:RegisterEvent("UNIT_PORTRAIT_UPDATE", function() + if arg1 == "target" and self.frame.portrait and not (SFramesDB and SFramesDB.targetShowPortrait == false) then + self.frame.portrait:SetUnit("target") + self.frame.portrait:SetCamera(0) + self.frame.portrait:SetPosition(-1.0, 0, 0) + end + end) SFrames:RegisterEvent("UNIT_DYNAMIC_FLAGS", function() if arg1 == "target" then self:UpdateAll() end end) SFrames:RegisterEvent("UNIT_FACTION", function() if arg1 == "target" then self:UpdateAll() end end) SFrames:RegisterEvent("RAID_TARGET_UPDATE", function() self:UpdateRaidIcon() end) @@ -413,7 +481,15 @@ function SFrames.Target:Initialize() -- If target already exists on load (e.g. after /reload), show and update it immediately self:OnTargetChanged() - -- Distance Updater removed from target frame + -- Register movers + if SFrames.Movers and SFrames.Movers.RegisterMover then + SFrames.Movers:RegisterMover("TargetFrame", f, "目标", + "CENTER", "UIParent", "CENTER", 200, -100) + if SFrames.Target.distanceFrame then + SFrames.Movers:RegisterMover("TargetDistanceFrame", SFrames.Target.distanceFrame, "目标距离", + "CENTER", "UIParent", "CENTER", 0, 100) + end + end end function SFrames.Target:OnTargetChanged() @@ -444,11 +520,14 @@ function SFrames.Target:UpdateAll() self:UpdateRaidIcon() self:UpdateAuras() - self.frame.portrait:SetUnit("target") - self.frame.portrait:SetCamera(0) - self.frame.portrait:Hide() - self.frame.portrait:Show() - self.frame.portrait:SetPosition(-1.0, 0, 0) + local showPortrait = not (SFramesDB and SFramesDB.targetShowPortrait == false) + if showPortrait and self.frame.portrait then + self.frame.portrait:SetUnit("target") + self.frame.portrait:SetCamera(0) + self.frame.portrait:Hide() + self.frame.portrait:Show() + self.frame.portrait:SetPosition(-1.0, 0, 0) + end local name = UnitName("target") or "" local level = UnitLevel("target") @@ -901,11 +980,11 @@ function SFrames.Target:UpdateAuras() if texture then b.icon:SetTexture(texture) - -- Scrape tooltip for duration SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE") SFrames.Tooltip:ClearLines() SFrames.Tooltip:SetUnitBuff("target", i) local timeLeft = SFrames:GetAuraTimeLeft("target", i, true) + SFrames.Tooltip:Hide() if timeLeft and timeLeft > 0 then b.expirationTime = GetTime() + timeLeft b.cdText:SetText(SFrames:FormatTime(timeLeft)) @@ -974,6 +1053,7 @@ function SFrames.Target:UpdateAuras() SFrames.Tooltip:ClearLines() SFrames.Tooltip:SetUnitDebuff("target", i) timeLeft = SFrames:GetAuraTimeLeft("target", i, false) + SFrames.Tooltip:Hide() end if timeLeft and timeLeft > 0 then diff --git a/img/map_f_1.tga b/img/map_f_1.tga new file mode 100644 index 0000000000000000000000000000000000000000..e485323fdbcb17821dc9a27245a15d1f0e41229d GIT binary patch literal 1048594 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 literal 0 HcmV?d00001 diff --git a/img/map_f_2.tga b/img/map_f_2.tga new file mode 100644 index 0000000000000000000000000000000000000000..697eefe83b029596ec9aef110c751f3747b56e95 GIT binary patch literal 1048594 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 literal 0 HcmV?d00001