diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..26835e6
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,8 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(find . -name \"*.lua\" -exec grep -l \"database\\\\|DATABASE\\\\|Encyclopedia\\\\|encyclop\" {} \\\\;)",
+ "Bash(ls -lah *.lua)"
+ ]
+ }
+}
diff --git a/ActionBars.lua b/ActionBars.lua
index a3f2976..15de28a 100644
--- a/ActionBars.lua
+++ b/ActionBars.lua
@@ -26,7 +26,6 @@ local DEFAULTS = {
showHotkey = true,
showMacroName = false,
rangeColoring = true,
- behindGlow = true,
showPetBar = true,
showStanceBar = true,
showRightBars = true,
@@ -44,6 +43,11 @@ local DEFAULTS = {
bottomOffsetY = 2,
rightOffsetX = -4,
rightOffsetY = -80,
+ rightBar1PerRow = 1,
+ rightBar2PerRow = 1,
+ bottomBar1PerRow = 12,
+ bottomBar2PerRow = 12,
+ bottomBar3PerRow = 12,
}
local BUTTONS_PER_ROW = 12
@@ -103,6 +107,23 @@ local function LayoutColumn(buttons, parent, size, gap)
end
end
+local function LayoutGrid(buttons, parent, size, gap, perRow)
+ local count = table.getn(buttons)
+ if count == 0 then return end
+ local numCols = perRow
+ local numRows = math.ceil(count / perRow)
+ parent:SetWidth(numCols * size + math.max(numCols - 1, 0) * gap)
+ parent:SetHeight(numRows * size + math.max(numRows - 1, 0) * gap)
+ for i, b in ipairs(buttons) do
+ b:SetWidth(size)
+ b:SetHeight(size)
+ b:ClearAllPoints()
+ local col = math.fmod(i - 1, perRow)
+ local row = math.floor((i - 1) / perRow)
+ b:SetPoint("TOPLEFT", parent, "TOPLEFT", col * (size + gap), -row * (size + gap))
+ end
+end
+
--------------------------------------------------------------------------------
-- DB
--------------------------------------------------------------------------------
@@ -445,12 +466,17 @@ function AB:CreateBars()
local db = self:GetDB()
local size = db.buttonSize
local gap = db.buttonGap
- local rowWidth = (size + gap) * BUTTONS_PER_ROW - gap
+
+ local bpr1 = db.bottomBar1PerRow or 12
+ local bpr1Cols = bpr1
+ local bpr1Rows = math.ceil(BUTTONS_PER_ROW / bpr1)
+ local bar1W = bpr1Cols * size + math.max(bpr1Cols - 1, 0) * gap
+ local bar1H = bpr1Rows * size + math.max(bpr1Rows - 1, 0) * gap
-- === BOTTOM BARS ===
local anchor = CreateFrame("Frame", "SFramesActionBarAnchor", UIParent)
- anchor:SetWidth(rowWidth)
- anchor:SetHeight(size * 3 + gap * 2)
+ anchor:SetWidth(bar1W)
+ anchor:SetHeight(bar1H)
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)
@@ -462,8 +488,8 @@ function AB:CreateBars()
-- Row 1: ActionButton1-12 (safe to reparent, uses page calc)
local row1 = CreateFrame("Frame", "SFramesMainBar", anchor)
- row1:SetWidth(rowWidth)
- row1:SetHeight(size)
+ row1:SetWidth(bar1W)
+ row1:SetHeight(bar1H)
row1:SetPoint("BOTTOMLEFT", anchor, "BOTTOMLEFT", 0, 0)
self.row1 = row1
@@ -483,8 +509,8 @@ function AB:CreateBars()
BonusActionBarFrame:SetParent(row1)
BonusActionBarFrame:ClearAllPoints()
BonusActionBarFrame:SetPoint("BOTTOMLEFT", row1, "BOTTOMLEFT", 0, 0)
- BonusActionBarFrame:SetWidth(rowWidth)
- BonusActionBarFrame:SetHeight(size)
+ BonusActionBarFrame:SetWidth(bar1W)
+ BonusActionBarFrame:SetHeight(bar1H)
BonusActionBarFrame:SetFrameLevel(row1:GetFrameLevel() + 5)
for i = 1, BUTTONS_PER_ROW do
@@ -505,7 +531,8 @@ function AB:CreateBars()
local db = AB:GetDB()
local s = db.buttonSize
local g = db.buttonGap
- LayoutRow(AB.bonusButtons, BonusActionBarFrame, s, g)
+ local bpr = db.bottomBar1PerRow or 12
+ LayoutGrid(AB.bonusButtons, BonusActionBarFrame, s, g, bpr)
local btnLevel = BonusActionBarFrame:GetFrameLevel() + 1
for _, btn in ipairs(AB.bonusButtons) do
btn:EnableMouse(true)
@@ -523,13 +550,32 @@ function AB:CreateBars()
pi:SetTextColor(0.9, 0.8, 0.2, 0.9)
self.pageIndicator = pi
- -- Row 2: reposition the original MultiBarBottomLeft frame
+ -- Row 2: independent anchor for MultiBarBottomLeft
+ local bpr2 = db.bottomBar2PerRow or 12
+ local bpr2Cols = bpr2
+ local bpr2Rows = math.ceil(BUTTONS_PER_ROW / bpr2)
+ local bar2W = bpr2Cols * size + math.max(bpr2Cols - 1, 0) * gap
+ local bar2H = bpr2Rows * size + math.max(bpr2Rows - 1, 0) * gap
+
+ local row2Anchor = CreateFrame("Frame", "SFramesRow2Anchor", UIParent)
+ row2Anchor:SetWidth(bar2W)
+ row2Anchor:SetHeight(bar2H)
+ row2Anchor:SetScale(db.scale)
+ self.row2Anchor = row2Anchor
+
+ local r2Pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ActionBarRow2"]
+ if r2Pos and r2Pos.point and r2Pos.relativePoint then
+ row2Anchor:SetPoint(r2Pos.point, UIParent, r2Pos.relativePoint, r2Pos.xOfs or 0, r2Pos.yOfs or 0)
+ else
+ row2Anchor:SetPoint("BOTTOMLEFT", anchor, "TOPLEFT", 0, gap)
+ end
+
if MultiBarBottomLeft then
- MultiBarBottomLeft:SetParent(anchor)
+ MultiBarBottomLeft:SetParent(row2Anchor)
MultiBarBottomLeft:ClearAllPoints()
- MultiBarBottomLeft:SetPoint("BOTTOMLEFT", row1, "TOPLEFT", 0, gap)
- MultiBarBottomLeft:SetWidth(rowWidth)
- MultiBarBottomLeft:SetHeight(size)
+ MultiBarBottomLeft:SetPoint("BOTTOMLEFT", row2Anchor, "BOTTOMLEFT", 0, 0)
+ MultiBarBottomLeft:SetWidth(bar2W)
+ MultiBarBottomLeft:SetHeight(bar2H)
MultiBarBottomLeft:Show()
end
self.row2 = MultiBarBottomLeft
@@ -544,13 +590,32 @@ function AB:CreateBars()
end
end
- -- Row 3: reposition the original MultiBarBottomRight frame
+ -- Row 3: independent anchor for MultiBarBottomRight
+ local bpr3 = db.bottomBar3PerRow or 12
+ local bpr3Cols = bpr3
+ local bpr3Rows = math.ceil(BUTTONS_PER_ROW / bpr3)
+ local bar3W = bpr3Cols * size + math.max(bpr3Cols - 1, 0) * gap
+ local bar3H = bpr3Rows * size + math.max(bpr3Rows - 1, 0) * gap
+
+ local row3Anchor = CreateFrame("Frame", "SFramesRow3Anchor", UIParent)
+ row3Anchor:SetWidth(bar3W)
+ row3Anchor:SetHeight(bar3H)
+ row3Anchor:SetScale(db.scale)
+ self.row3Anchor = row3Anchor
+
+ local r3Pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ActionBarRow3"]
+ if r3Pos and r3Pos.point and r3Pos.relativePoint then
+ row3Anchor:SetPoint(r3Pos.point, UIParent, r3Pos.relativePoint, r3Pos.xOfs or 0, r3Pos.yOfs or 0)
+ else
+ row3Anchor:SetPoint("BOTTOMLEFT", row2Anchor, "TOPLEFT", 0, gap)
+ end
+
if MultiBarBottomRight then
- MultiBarBottomRight:SetParent(anchor)
+ MultiBarBottomRight:SetParent(row3Anchor)
MultiBarBottomRight:ClearAllPoints()
- MultiBarBottomRight:SetPoint("BOTTOMLEFT", MultiBarBottomLeft or row1, "TOPLEFT", 0, gap)
- MultiBarBottomRight:SetWidth(rowWidth)
- MultiBarBottomRight:SetHeight(size)
+ MultiBarBottomRight:SetPoint("BOTTOMLEFT", row3Anchor, "BOTTOMLEFT", 0, 0)
+ MultiBarBottomRight:SetWidth(bar3W)
+ MultiBarBottomRight:SetHeight(bar3H)
MultiBarBottomRight:Show()
end
self.row3 = MultiBarBottomRight
@@ -592,23 +657,43 @@ function AB:CreateBars()
self.gryphonRight = rightCap
- -- === RIGHT-SIDE BARS ===
- local rightHolder = CreateFrame("Frame", "SFramesRightBarHolder", UIParent)
- rightHolder:SetWidth(size * 2 + gap)
- rightHolder:SetHeight((size + gap) * BUTTONS_PER_ROW - gap)
- 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)
+ -- === RIGHT-SIDE BARS (independent holders) ===
+ -- Migrate legacy "ActionBarRight" position to "RightBar1"
+ if SFramesDB and SFramesDB.Positions then
+ if SFramesDB.Positions["ActionBarRight"] and not SFramesDB.Positions["RightBar1"] then
+ SFramesDB.Positions["RightBar1"] = SFramesDB.Positions["ActionBarRight"]
+ end
+ end
+
+ local perRow1 = db.rightBar1PerRow or 1
+ local perRow2 = db.rightBar2PerRow or 1
+ local numCols1 = perRow1
+ local numRows1 = math.ceil(BUTTONS_PER_ROW / perRow1)
+ local rb1W = numCols1 * size + math.max(numCols1 - 1, 0) * gap
+ local rb1H = numRows1 * size + math.max(numRows1 - 1, 0) * gap
+ local numCols2 = perRow2
+ local numRows2 = math.ceil(BUTTONS_PER_ROW / perRow2)
+ local rb2W = numCols2 * size + math.max(numCols2 - 1, 0) * gap
+ local rb2H = numRows2 * size + math.max(numRows2 - 1, 0) * gap
+
+ -- RightBar1: MultiBarRight
+ local rb1Holder = CreateFrame("Frame", "SFramesRightBar1Holder", UIParent)
+ rb1Holder:SetWidth(rb1W)
+ rb1Holder:SetHeight(rb1H)
+ rb1Holder:SetScale(db.scale)
+ self.rightBar1Holder = rb1Holder
+
+ local rb1Pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["RightBar1"]
+ if rb1Pos and rb1Pos.point and rb1Pos.relativePoint then
+ rb1Holder:SetPoint(rb1Pos.point, UIParent, rb1Pos.relativePoint, rb1Pos.xOfs or 0, rb1Pos.yOfs or 0)
+ else
+ rb1Holder:SetPoint("RIGHT", UIParent, "RIGHT", db.rightOffsetX, db.rightOffsetY)
end
- rightHolder:SetScale(db.scale)
- self.rightHolder = rightHolder
if MultiBarRight then
- MultiBarRight:SetParent(rightHolder)
+ MultiBarRight:SetParent(rb1Holder)
MultiBarRight:ClearAllPoints()
- MultiBarRight:SetPoint("TOPRIGHT", rightHolder, "TOPRIGHT", 0, 0)
+ MultiBarRight:SetPoint("TOPLEFT", rb1Holder, "TOPLEFT", 0, 0)
MultiBarRight:Show()
end
@@ -622,10 +707,24 @@ function AB:CreateBars()
end
end
+ -- RightBar2: MultiBarLeft
+ local rb2Holder = CreateFrame("Frame", "SFramesRightBar2Holder", UIParent)
+ rb2Holder:SetWidth(rb2W)
+ rb2Holder:SetHeight(rb2H)
+ rb2Holder:SetScale(db.scale)
+ self.rightBar2Holder = rb2Holder
+
+ local rb2Pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["RightBar2"]
+ if rb2Pos and rb2Pos.point and rb2Pos.relativePoint then
+ rb2Holder:SetPoint(rb2Pos.point, UIParent, rb2Pos.relativePoint, rb2Pos.xOfs or 0, rb2Pos.yOfs or 0)
+ else
+ rb2Holder:SetPoint("TOPRIGHT", rb1Holder, "TOPLEFT", -gap, 0)
+ end
+
if MultiBarLeft then
- MultiBarLeft:SetParent(rightHolder)
+ MultiBarLeft:SetParent(rb2Holder)
MultiBarLeft:ClearAllPoints()
- MultiBarLeft:SetPoint("TOPRIGHT", MultiBarRight or rightHolder, "TOPLEFT", -gap, 0)
+ MultiBarLeft:SetPoint("TOPLEFT", rb2Holder, "TOPLEFT", 0, 0)
MultiBarLeft:Show()
end
@@ -639,9 +738,12 @@ function AB:CreateBars()
end
end
+ -- Legacy compat: keep rightHolder reference pointing to rb1Holder
+ self.rightHolder = rb1Holder
+
-- === STANCE BAR ===
local stanceHolder = CreateFrame("Frame", "SFramesStanceHolder", UIParent)
- stanceHolder:SetWidth(rowWidth)
+ stanceHolder:SetWidth(bar1W)
stanceHolder:SetHeight(size)
stanceHolder:SetScale(db.scale)
self.stanceHolder = stanceHolder
@@ -658,7 +760,7 @@ function AB:CreateBars()
-- === PET BAR ===
local petHolder = CreateFrame("Frame", "SFramesPetHolder", UIParent)
- petHolder:SetWidth(rowWidth)
+ petHolder:SetWidth(bar1W)
petHolder:SetHeight(size)
petHolder:SetScale(db.scale)
self.petHolder = petHolder
@@ -710,73 +812,136 @@ function AB:ApplyConfig()
local size = db.buttonSize
local gap = db.buttonGap
- local rowWidth = (size + gap) * BUTTONS_PER_ROW - gap
- local colHeight = (size + gap) * BUTTONS_PER_ROW - gap
- local totalHeight = db.barCount * size + (db.barCount - 1) * gap
- -- Bottom bars anchor
+ -- Bottom bar 1 dimensions
+ local bpr1 = db.bottomBar1PerRow or 12
+ local bpr1Cols = bpr1
+ local bpr1Rows = math.ceil(BUTTONS_PER_ROW / bpr1)
+ local bar1W = bpr1Cols * size + math.max(bpr1Cols - 1, 0) * gap
+ local bar1H = bpr1Rows * size + math.max(bpr1Rows - 1, 0) * gap
+
+ -- Bottom bars anchor (row1 only)
self.anchor:SetScale(db.scale)
- self.anchor:SetWidth(rowWidth)
- self.anchor:SetHeight(totalHeight)
+ self.anchor:SetWidth(bar1W)
+ self.anchor:SetHeight(bar1H)
-- Row 1
- self.row1:SetWidth(rowWidth)
- self.row1:SetHeight(size)
- LayoutRow(self.mainButtons, self.row1, size, gap)
+ self.row1:SetWidth(bar1W)
+ self.row1:SetHeight(bar1H)
+ LayoutGrid(self.mainButtons, self.row1, size, gap, bpr1)
-- Bonus bar (druid forms) — same layout as row 1, overlays when active
if self.bonusButtons and BonusActionBarFrame then
- BonusActionBarFrame:SetWidth(rowWidth)
- BonusActionBarFrame:SetHeight(size)
- LayoutRow(self.bonusButtons, BonusActionBarFrame, size, gap)
+ BonusActionBarFrame:SetWidth(bar1W)
+ BonusActionBarFrame:SetHeight(bar1H)
+ LayoutGrid(self.bonusButtons, BonusActionBarFrame, size, gap, bpr1)
end
- -- Row 2
+ -- Bottom bar 2 dimensions
+ local bpr2 = db.bottomBar2PerRow or 12
+ local bpr2Cols = bpr2
+ local bpr2Rows = math.ceil(BUTTONS_PER_ROW / bpr2)
+ local bar2W = bpr2Cols * size + math.max(bpr2Cols - 1, 0) * gap
+ local bar2H = bpr2Rows * size + math.max(bpr2Rows - 1, 0) * gap
+
+ -- Row 2 (independent anchor)
+ if self.row2Anchor then
+ self.row2Anchor:SetScale(db.scale)
+ self.row2Anchor:SetWidth(bar2W)
+ self.row2Anchor:SetHeight(bar2H)
+ end
if self.row2 then
- self.row2:SetWidth(rowWidth)
- self.row2:SetHeight(size)
+ self.row2:SetWidth(bar2W)
+ self.row2:SetHeight(bar2H)
self.row2:ClearAllPoints()
- self.row2:SetPoint("BOTTOMLEFT", self.row1, "TOPLEFT", 0, gap)
- LayoutRow(self.bar2Buttons, self.row2, size, gap)
- if db.barCount >= 2 then self.row2:Show() else self.row2:Hide() end
+ self.row2:SetPoint("BOTTOMLEFT", self.row2Anchor or self.anchor, "BOTTOMLEFT", 0, 0)
+ LayoutGrid(self.bar2Buttons, self.row2, size, gap, bpr2)
+ if db.barCount >= 2 then
+ if self.row2Anchor then self.row2Anchor:Show() end
+ self.row2:Show()
+ else
+ if self.row2Anchor then self.row2Anchor:Hide() end
+ self.row2:Hide()
+ end
end
- -- Row 3
+ -- Bottom bar 3 dimensions
+ local bpr3 = db.bottomBar3PerRow or 12
+ local bpr3Cols = bpr3
+ local bpr3Rows = math.ceil(BUTTONS_PER_ROW / bpr3)
+ local bar3W = bpr3Cols * size + math.max(bpr3Cols - 1, 0) * gap
+ local bar3H = bpr3Rows * size + math.max(bpr3Rows - 1, 0) * gap
+
+ -- Row 3 (independent anchor)
+ if self.row3Anchor then
+ self.row3Anchor:SetScale(db.scale)
+ self.row3Anchor:SetWidth(bar3W)
+ self.row3Anchor:SetHeight(bar3H)
+ end
if self.row3 then
- self.row3:SetWidth(rowWidth)
- self.row3:SetHeight(size)
+ self.row3:SetWidth(bar3W)
+ self.row3:SetHeight(bar3H)
self.row3:ClearAllPoints()
- self.row3:SetPoint("BOTTOMLEFT", self.row2 or self.row1, "TOPLEFT", 0, gap)
- LayoutRow(self.bar3Buttons, self.row3, size, gap)
- if db.barCount >= 3 then self.row3:Show() else self.row3:Hide() end
+ self.row3:SetPoint("BOTTOMLEFT", self.row3Anchor or self.anchor, "BOTTOMLEFT", 0, 0)
+ LayoutGrid(self.bar3Buttons, self.row3, size, gap, bpr3)
+ if db.barCount >= 3 then
+ if self.row3Anchor then self.row3Anchor:Show() end
+ self.row3:Show()
+ else
+ if self.row3Anchor then self.row3Anchor:Hide() end
+ self.row3:Hide()
+ end
end
- -- Right-side bars
- if self.rightHolder then
- self.rightHolder:SetScale(db.scale)
- self.rightHolder:SetWidth(size * 2 + gap)
- self.rightHolder:SetHeight(colHeight)
+ -- Right-side bar 1 (MultiBarRight, grid layout)
+ if self.rightBar1Holder then
+ self.rightBar1Holder:SetScale(db.scale)
+ local perRow1 = db.rightBar1PerRow or 1
+ local numCols1 = perRow1
+ local numRows1 = math.ceil(BUTTONS_PER_ROW / perRow1)
+ local rb1W = numCols1 * size + math.max(numCols1 - 1, 0) * gap
+ local rb1H = numRows1 * size + math.max(numRows1 - 1, 0) * gap
+ self.rightBar1Holder:SetWidth(rb1W)
+ self.rightBar1Holder:SetHeight(rb1H)
if MultiBarRight then
- MultiBarRight:SetWidth(size)
- MultiBarRight:SetHeight(colHeight)
+ MultiBarRight:SetWidth(rb1W)
+ MultiBarRight:SetHeight(rb1H)
+ LayoutGrid(self.rightButtons, MultiBarRight, size, gap, perRow1)
MultiBarRight:ClearAllPoints()
- MultiBarRight:SetPoint("TOPRIGHT", self.rightHolder, "TOPRIGHT", 0, 0)
- LayoutColumn(self.rightButtons, MultiBarRight, size, gap)
- end
-
- if MultiBarLeft then
- MultiBarLeft:SetWidth(size)
- MultiBarLeft:SetHeight(colHeight)
- MultiBarLeft:ClearAllPoints()
- MultiBarLeft:SetPoint("TOPRIGHT", MultiBarRight or self.rightHolder, "TOPLEFT", -gap, 0)
- LayoutColumn(self.leftButtons, MultiBarLeft, size, gap)
+ MultiBarRight:SetPoint("TOPLEFT", self.rightBar1Holder, "TOPLEFT", 0, 0)
end
if db.showRightBars then
- self.rightHolder:Show()
+ self.rightBar1Holder:Show()
else
- self.rightHolder:Hide()
+ self.rightBar1Holder:Hide()
+ end
+ end
+
+ -- Right-side bar 2 (MultiBarLeft, grid layout)
+ if self.rightBar2Holder then
+ self.rightBar2Holder:SetScale(db.scale)
+ local perRow2 = db.rightBar2PerRow or 1
+ local numCols2 = perRow2
+ local numRows2 = math.ceil(BUTTONS_PER_ROW / perRow2)
+ local rb2W = numCols2 * size + math.max(numCols2 - 1, 0) * gap
+ local rb2H = numRows2 * size + math.max(numRows2 - 1, 0) * gap
+ self.rightBar2Holder:SetWidth(rb2W)
+ self.rightBar2Holder:SetHeight(rb2H)
+
+ if MultiBarLeft then
+ MultiBarLeft:SetWidth(rb2W)
+ MultiBarLeft:SetHeight(rb2H)
+ LayoutGrid(self.leftButtons, MultiBarLeft, size, gap, perRow2)
+ MultiBarLeft:ClearAllPoints()
+ MultiBarLeft:SetPoint("TOPLEFT", self.rightBar2Holder, "TOPLEFT", 0, 0)
+ end
+
+ if db.showRightBars then
+ self.rightBar2Holder:Show()
+ else
+ self.rightBar2Holder:Hide()
end
end
@@ -785,7 +950,10 @@ function AB:ApplyConfig()
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.row2Anchor then self.row2Anchor:SetAlpha(alpha) end
+ if self.row3Anchor then self.row3Anchor:SetAlpha(alpha) end
+ if self.rightBar1Holder then self.rightBar1Holder:SetAlpha(alpha) end
+ if self.rightBar2Holder then self.rightBar2Holder:SetAlpha(alpha) end
if self.stanceHolder then self.stanceHolder:SetAlpha(alpha) end
if self.petHolder then self.petHolder:SetAlpha(alpha) end
@@ -893,6 +1061,13 @@ end
--------------------------------------------------------------------------------
-- Stance bar
--------------------------------------------------------------------------------
+function AB:GetTopRowAnchor()
+ local db = self:GetDB()
+ if db.barCount >= 3 and self.row3Anchor then return self.row3Anchor end
+ if db.barCount >= 2 and self.row2Anchor then return self.row2Anchor end
+ return self.anchor
+end
+
function AB:ApplyStanceBar()
local db = self:GetDB()
local size = db.smallBarSize
@@ -906,16 +1081,19 @@ function AB:ApplyStanceBar()
end
self.stanceHolder:SetScale(db.scale)
- local topRow = self.row1
- if db.barCount >= 3 and self.row3 then topRow = self.row3
- elseif db.barCount >= 2 and self.row2 then topRow = self.row2 end
-
- self.stanceHolder:ClearAllPoints()
- self.stanceHolder:SetPoint("BOTTOMLEFT", topRow, "TOPLEFT", 0, gap)
local totalW = numForms * size + (numForms - 1) * gap
self.stanceHolder:SetWidth(totalW)
self.stanceHolder:SetHeight(size)
+
+ self.stanceHolder:ClearAllPoints()
+ local positions = SFramesDB and SFramesDB.Positions
+ local pos = positions and positions["StanceBar"]
+ if pos and pos.point and pos.relativePoint then
+ self.stanceHolder:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
+ else
+ self.stanceHolder:SetPoint("BOTTOMLEFT", self:GetTopRowAnchor(), "TOPLEFT", 0, gap)
+ end
self.stanceHolder:Show()
for i, b in ipairs(self.stanceButtons) do
@@ -951,21 +1129,24 @@ function AB:ApplyPetBar()
self.petHolder:SetScale(db.scale)
- self.petHolder:ClearAllPoints()
- local numForms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0
- if db.showStanceBar and numForms > 0 and self.stanceHolder:IsShown() then
- self.petHolder:SetPoint("BOTTOMLEFT", self.stanceHolder, "TOPLEFT", 0, gap)
- else
- local topRow = self.row1
- if db.barCount >= 3 and self.row3 then topRow = self.row3
- elseif db.barCount >= 2 and self.row2 then topRow = self.row2 end
- self.petHolder:SetPoint("BOTTOMLEFT", topRow, "TOPLEFT", 0, gap)
- end
-
local numPet = 10
local totalW = numPet * size + (numPet - 1) * gap
self.petHolder:SetWidth(totalW)
self.petHolder:SetHeight(size)
+
+ self.petHolder:ClearAllPoints()
+ local positions = SFramesDB and SFramesDB.Positions
+ local pos = positions and positions["PetBar"]
+ if pos and pos.point and pos.relativePoint then
+ self.petHolder:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
+ else
+ local numForms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0
+ if db.showStanceBar and numForms > 0 and self.stanceHolder and self.stanceHolder:IsShown() then
+ self.petHolder:SetPoint("BOTTOMLEFT", self.stanceHolder, "TOPLEFT", 0, gap)
+ else
+ self.petHolder:SetPoint("BOTTOMLEFT", self:GetTopRowAnchor(), "TOPLEFT", 0, gap)
+ end
+ end
self.petHolder:Show()
for i, b in ipairs(self.petButtons) do
@@ -1050,12 +1231,219 @@ function AB:ApplyGryphon()
self.gryphonRight:Show()
end
+--------------------------------------------------------------------------------
+-- Layout presets
+--------------------------------------------------------------------------------
+AB.PRESETS = {
+ { id = 1, name = "经典", desc = "底部堆叠 + 右侧竖栏" },
+ { id = 2, name = "宽屏", desc = "左4x3 + 底部堆叠 + 右4x3" },
+ { id = 3, name = "堆叠", desc = "全部堆叠于底部中央" },
+}
+
+function AB:ApplyPreset(presetId)
+ if not self.anchor then return end
+ local db = self:GetDB()
+ if not SFramesDB.Positions then SFramesDB.Positions = {} end
+ local positions = SFramesDB.Positions
+
+ local size = db.buttonSize
+ local gap = db.buttonGap
+ local smSize = db.smallBarSize
+ local rowW = (size + gap) * BUTTONS_PER_ROW - gap
+ local bottomY = db.bottomOffsetY or 2
+ local step = size + gap
+ local leftX = -rowW / 2
+
+ local clearKeys = {
+ "ActionBarBottom", "ActionBarRow2", "ActionBarRow3",
+ "RightBar1", "RightBar2", "StanceBar", "PetBar",
+ "PlayerFrame", "TargetFrame", "PetFrame", "FocusFrame", "ToTFrame",
+ }
+ for _, key in ipairs(clearKeys) do
+ positions[key] = nil
+ end
+
+ local numForms = GetNumShapeshiftForms and GetNumShapeshiftForms() or 0
+ local hasStance = db.showStanceBar and numForms > 0
+ local stanceH = hasStance and (smSize + gap) or 0
+
+ db.bottomBar1PerRow = 12
+ db.bottomBar2PerRow = 12
+ db.bottomBar3PerRow = 12
+
+ if presetId == 1 then
+ -- Classic: stacked bottom bars, vertical right side bars
+ db.rightBar1PerRow = 1
+ db.rightBar2PerRow = 1
+ db.showRightBars = true
+ local rx = db.rightOffsetX or -4
+ local ry = db.rightOffsetY or -80
+ positions["RightBar1"] = {
+ point = "RIGHT", relativePoint = "RIGHT",
+ xOfs = rx, yOfs = ry,
+ }
+ positions["RightBar2"] = {
+ point = "RIGHT", relativePoint = "RIGHT",
+ xOfs = rx - size - gap, yOfs = ry,
+ }
+ positions["StanceBar"] = {
+ point = "BOTTOMLEFT", relativePoint = "BOTTOM",
+ xOfs = leftX, yOfs = bottomY + step * 3,
+ }
+ positions["PetBar"] = {
+ point = "BOTTOMLEFT", relativePoint = "BOTTOM",
+ xOfs = leftX, yOfs = bottomY + step * 3 + stanceH,
+ }
+
+ elseif presetId == 2 then
+ -- Widescreen: left 4x3, center stack, right 4x3
+ db.rightBar1PerRow = 4
+ db.rightBar2PerRow = 4
+ db.showRightBars = true
+ positions["RightBar1"] = {
+ point = "BOTTOMRIGHT", relativePoint = "BOTTOM",
+ xOfs = -(rowW / 2 + gap * 2), yOfs = bottomY,
+ }
+ positions["RightBar2"] = {
+ point = "BOTTOMLEFT", relativePoint = "BOTTOM",
+ xOfs = rowW / 2 + gap * 2, yOfs = bottomY,
+ }
+ positions["StanceBar"] = {
+ point = "BOTTOMLEFT", relativePoint = "BOTTOM",
+ xOfs = leftX, yOfs = bottomY + step * 3,
+ }
+ positions["PetBar"] = {
+ point = "BOTTOMLEFT", relativePoint = "BOTTOM",
+ xOfs = leftX, yOfs = bottomY + step * 3 + stanceH,
+ }
+
+ elseif presetId == 3 then
+ -- Stacked: all stacked at bottom center
+ db.rightBar1PerRow = 12
+ db.rightBar2PerRow = 12
+ db.showRightBars = true
+ local barH3 = 3 * step
+ positions["RightBar1"] = {
+ point = "BOTTOM", relativePoint = "BOTTOM",
+ xOfs = 0, yOfs = bottomY + barH3,
+ }
+ positions["RightBar2"] = {
+ point = "BOTTOM", relativePoint = "BOTTOM",
+ xOfs = 0, yOfs = bottomY + barH3 + step,
+ }
+ positions["StanceBar"] = {
+ point = "BOTTOMLEFT", relativePoint = "BOTTOM",
+ xOfs = leftX, yOfs = bottomY + barH3 + step * 2,
+ }
+ positions["PetBar"] = {
+ point = "BOTTOMLEFT", relativePoint = "BOTTOM",
+ xOfs = leftX, yOfs = bottomY + barH3 + step * 2 + stanceH,
+ }
+ local upShift = step * 2
+ positions["PlayerFrame"] = {
+ point = "CENTER", relativePoint = "CENTER",
+ xOfs = -200, yOfs = -100 + upShift,
+ }
+ positions["TargetFrame"] = {
+ point = "CENTER", relativePoint = "CENTER",
+ xOfs = 200, yOfs = -100 + upShift,
+ }
+ end
+
+ -- Castbar: centered above PetBar, explicit UIParent coords (avoids layout-engine timing issues)
+ local petBarPos = positions["PetBar"]
+ if petBarPos then
+ local petTopY = petBarPos.yOfs + smSize
+ positions["PlayerCastbar"] = {
+ point = "BOTTOM", relativePoint = "BOTTOM",
+ xOfs = 0, yOfs = petTopY + 6,
+ }
+ end
+
+ -- Calculate Pet/Focus positions relative to Player/Target
+ local playerPos = positions["PlayerFrame"]
+ local px = playerPos and playerPos.xOfs or -200
+ local py = playerPos and playerPos.yOfs or -100
+
+ local targetPos = positions["TargetFrame"]
+ local tx = targetPos and targetPos.xOfs or 200
+ local ty = targetPos and targetPos.yOfs or -100
+
+ local pf = _G["SFramesPlayerFrame"]
+ local pfScale = pf and (pf:GetEffectiveScale() / UIParent:GetEffectiveScale()) or 1
+ local pfW = ((pf and pf:GetWidth()) or 220) * pfScale
+ local pfH = ((pf and pf:GetHeight()) or 50) * pfScale
+
+ local tf = _G["SFramesTargetFrame"]
+ local tfScale = tf and (tf:GetEffectiveScale() / UIParent:GetEffectiveScale()) or 1
+ local tfW = ((tf and tf:GetWidth()) or 220) * tfScale
+ local tfH = ((tf and tf:GetHeight()) or 50) * tfScale
+
+ local petGap, focGap
+ if presetId == 1 then
+ petGap, focGap = 75, 71
+ elseif presetId == 2 then
+ petGap, focGap = 62, 58
+ else
+ petGap, focGap = 52, 42
+ end
+
+ positions["PetFrame"] = {
+ point = "TOPLEFT", relativePoint = "CENTER",
+ xOfs = px - pfW / 2,
+ yOfs = py - pfH / 2 - petGap,
+ }
+ positions["FocusFrame"] = {
+ point = "TOPLEFT", relativePoint = "CENTER",
+ xOfs = tx - tfW / 2,
+ yOfs = ty - tfH / 2 - focGap,
+ }
+ positions["ToTFrame"] = {
+ point = "BOTTOMLEFT", relativePoint = "CENTER",
+ xOfs = tx + tfW / 2 + 5,
+ yOfs = ty - tfH / 2,
+ }
+
+ db.layoutPreset = presetId
+ self:ApplyConfig()
+
+ if SFrames.Movers then
+ local reg = SFrames.Movers:GetRegistry()
+ for _, key in ipairs({"PlayerFrame", "TargetFrame", "PetFrame", "FocusFrame", "ToTFrame"}) do
+ local entry = reg[key]
+ if entry and entry.frame then
+ SFrames.Movers:ApplyPosition(key, entry.frame,
+ entry.defaultPoint, entry.defaultRelativeTo,
+ entry.defaultRelPoint, entry.defaultX, entry.defaultY)
+ end
+ end
+ end
+
+ if SFrames.Player and SFrames.Player.ApplyCastbarPosition then
+ SFrames.Player:ApplyCastbarPosition()
+ end
+
+ if SFrames.Movers and SFrames.Movers:IsLayoutMode() then
+ for name, entry in pairs(SFrames.Movers:GetRegistry()) do
+ local frame = entry and entry.frame
+ local shouldSync = entry.alwaysShowInLayout
+ or (frame and frame.IsShown and frame:IsShown())
+ if shouldSync then
+ SFrames.Movers:SyncMoverToFrame(name)
+ end
+ end
+ end
+
+ SFrames:Print("|cff66eeff[布局预设]|r 已应用方案: " .. (self.PRESETS[presetId] and self.PRESETS[presetId].name or tostring(presetId)))
+end
+
--------------------------------------------------------------------------------
-- Slider-based position update
--------------------------------------------------------------------------------
function AB:ApplyPosition()
local db = self:GetDB()
local positions = SFramesDB and SFramesDB.Positions
+ local gap = db.buttonGap
if self.anchor then
self.anchor:ClearAllPoints()
@@ -1066,13 +1454,44 @@ function AB:ApplyPosition()
self.anchor:SetPoint("BOTTOM", UIParent, "BOTTOM", db.bottomOffsetX, db.bottomOffsetY)
end
end
- if self.rightHolder then
- self.rightHolder:ClearAllPoints()
- local pos = positions and positions["ActionBarRight"]
+
+ if self.row2Anchor then
+ self.row2Anchor:ClearAllPoints()
+ local pos = positions and positions["ActionBarRow2"]
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)
+ self.row2Anchor: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)
+ self.row2Anchor:SetPoint("BOTTOMLEFT", self.anchor, "TOPLEFT", 0, gap)
+ end
+ end
+
+ if self.row3Anchor then
+ self.row3Anchor:ClearAllPoints()
+ local pos = positions and positions["ActionBarRow3"]
+ if pos and pos.point and pos.relativePoint then
+ self.row3Anchor:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
+ else
+ self.row3Anchor:SetPoint("BOTTOMLEFT", self.row2Anchor or self.anchor, "TOPLEFT", 0, gap)
+ end
+ end
+
+ if self.rightBar1Holder then
+ self.rightBar1Holder:ClearAllPoints()
+ local pos = positions and positions["RightBar1"]
+ if pos and pos.point and pos.relativePoint then
+ self.rightBar1Holder:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
+ else
+ self.rightBar1Holder:SetPoint("RIGHT", UIParent, "RIGHT", db.rightOffsetX, db.rightOffsetY)
+ end
+ end
+
+ if self.rightBar2Holder then
+ self.rightBar2Holder:ClearAllPoints()
+ local pos = positions and positions["RightBar2"]
+ if pos and pos.point and pos.relativePoint then
+ self.rightBar2Holder:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
+ else
+ self.rightBar2Holder:SetPoint("TOPRIGHT", self.rightBar1Holder or UIParent, "TOPLEFT", -gap, 0)
end
end
end
@@ -1112,13 +1531,17 @@ function AB:UpdateBonusBar()
local db = self:GetDB()
local size = db.buttonSize
local gap = db.buttonGap
- local rowWidth = (size + gap) * BUTTONS_PER_ROW - gap
+ local bpr = db.bottomBar1PerRow or 12
+ local numC = bpr
+ local numR = math.ceil(BUTTONS_PER_ROW / bpr)
+ local bW = numC * size + math.max(numC - 1, 0) * gap
+ local bH = numR * size + math.max(numR - 1, 0) * gap
BonusActionBarFrame:ClearAllPoints()
BonusActionBarFrame:SetPoint("BOTTOMLEFT", self.row1, "BOTTOMLEFT", 0, 0)
- BonusActionBarFrame:SetWidth(rowWidth)
- BonusActionBarFrame:SetHeight(size)
+ BonusActionBarFrame:SetWidth(bW)
+ BonusActionBarFrame:SetHeight(bH)
BonusActionBarFrame:SetFrameLevel(self.row1:GetFrameLevel() + 5)
- LayoutRow(self.bonusButtons, BonusActionBarFrame, size, gap)
+ LayoutGrid(self.bonusButtons, BonusActionBarFrame, size, gap, bpr)
local btnLevel = BonusActionBarFrame:GetFrameLevel() + 1
for _, b in ipairs(self.bonusButtons) do
b:EnableMouse(true)
@@ -1150,91 +1573,6 @@ local function GetOrCreateRangeOverlay(b)
return ov
end
---------------------------------------------------------------------------------
--- Behind-skill glow: highlight skills that require being behind the target
---------------------------------------------------------------------------------
--- Icon texture substrings (lowercase) that identify "must be behind" skills.
--- Matching by texture is the most reliable method in Vanilla (no GetActionInfo).
-local BEHIND_SKILL_ICONS = {
- -- Rogue
- "ability_backstab", -- 背刺 (Backstab)
- "ability_rogue_ambush", -- 伏击 (Ambush)
- "ability_ambush", -- 伏击 (Ambush, alternate icon)
- -- Druid (cat form)
- "spell_shadow_vampiricaura",-- 撕碎 (Shred)
- "ability_shred", -- 撕碎 (Shred, alternate)
- "ability_druid_ravage", -- 突袭 (Ravage)
-}
-
--- Build a fast lookup set (lowercased)
-local behindIconSet = {}
-for _, v in ipairs(BEHIND_SKILL_ICONS) do
- behindIconSet[string.lower(v)] = true
-end
-
--- Check if an action slot's icon matches a behind-only skill
-local function IsBehindSkillAction(action)
- if not action or not HasAction(action) then return false end
- local tex = GetActionTexture(action)
- if not tex then return false end
- tex = string.lower(tex)
- for pattern, _ in pairs(behindIconSet) do
- if string.find(tex, pattern) then return true end
- end
- return false
-end
-
--- Create / retrieve the green glow overlay for behind-skill highlighting
-local function GetOrCreateBehindGlow(b)
- if b.sfBehindGlow then return b.sfBehindGlow end
- local inset = b.sfIconInset or 2
-
- -- Outer glow frame (slightly larger than button)
- local glow = CreateFrame("Frame", nil, b)
- glow:SetFrameLevel(b:GetFrameLevel() + 3)
- glow:SetPoint("TOPLEFT", b, "TOPLEFT", -2, 2)
- glow:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 2, -2)
-
- -- Green border textures (4 edges)
- local thickness = 2
- local r, g, a = 0.2, 1.0, 0.7
-
- local top = glow:CreateTexture(nil, "OVERLAY")
- top:SetTexture("Interface\\Buttons\\WHITE8X8")
- top:SetPoint("TOPLEFT", glow, "TOPLEFT", 0, 0)
- top:SetPoint("TOPRIGHT", glow, "TOPRIGHT", 0, 0)
- top:SetHeight(thickness)
- top:SetVertexColor(r, g, 0.3, a)
-
- local bot = glow:CreateTexture(nil, "OVERLAY")
- bot:SetTexture("Interface\\Buttons\\WHITE8X8")
- bot:SetPoint("BOTTOMLEFT", glow, "BOTTOMLEFT", 0, 0)
- bot:SetPoint("BOTTOMRIGHT", glow, "BOTTOMRIGHT", 0, 0)
- bot:SetHeight(thickness)
- bot:SetVertexColor(r, g, 0.3, a)
-
- local left = glow:CreateTexture(nil, "OVERLAY")
- left:SetTexture("Interface\\Buttons\\WHITE8X8")
- left:SetPoint("TOPLEFT", glow, "TOPLEFT", 0, 0)
- left:SetPoint("BOTTOMLEFT", glow, "BOTTOMLEFT", 0, 0)
- left:SetWidth(thickness)
- left:SetVertexColor(r, g, 0.3, a)
-
- local right = glow:CreateTexture(nil, "OVERLAY")
- right:SetTexture("Interface\\Buttons\\WHITE8X8")
- right:SetPoint("TOPRIGHT", glow, "TOPRIGHT", 0, 0)
- right:SetPoint("BOTTOMRIGHT", glow, "BOTTOMRIGHT", 0, 0)
- right:SetWidth(thickness)
- right:SetVertexColor(r, g, 0.3, a)
-
- glow:Hide()
- b.sfBehindGlow = glow
- return glow
-end
-
--- Cached behind state, updated by the range-check timer
-local isBehindTarget = false
-
function AB:SetupRangeCheck()
local rangeFrame = CreateFrame("Frame", "SFramesActionBarRangeCheck", UIParent)
rangeFrame.timer = 0
@@ -1247,18 +1585,6 @@ function AB:SetupRangeCheck()
local db = AB:GetDB()
- -- Update behind state (shared with behind-glow logic)
- local behindGlowEnabled = db.behindGlow ~= false
- local hasEnemy = UnitExists("target") and UnitCanAttack("player", "target")
- and not UnitIsDead("target")
- if behindGlowEnabled and hasEnemy
- and type(UnitXP) == "function" then
- local ok, val = pcall(UnitXP, "behind", "player", "target")
- isBehindTarget = ok and val and true or false
- else
- isBehindTarget = false
- end
-
local function CheckRange(buttons, idFunc)
local getID = idFunc or ActionButton_GetPagedID
if not getID then return end
@@ -1275,20 +1601,8 @@ function AB:SetupRangeCheck()
ov:Hide()
end
end
- -- Behind-skill glow
- if behindGlowEnabled and IsBehindSkillAction(action) then
- local glow = GetOrCreateBehindGlow(b)
- if isBehindTarget then
- glow:Show()
- else
- glow:Hide()
- end
- elseif b.sfBehindGlow then
- b.sfBehindGlow:Hide()
- end
else
if b.sfRangeOverlay then b.sfRangeOverlay:Hide() end
- if b.sfBehindGlow then b.sfBehindGlow:Hide() end
end
end
end
@@ -1420,16 +1734,15 @@ function AB:Initialize()
local db = AB:GetDB()
local size = db.buttonSize
local gap = db.buttonGap
+ local bpr = db.bottomBar1PerRow or 12
for idx, btn in ipairs(AB.bonusButtons) do
if btn == b then
btn:SetWidth(size)
btn:SetHeight(size)
btn:ClearAllPoints()
- if idx == 1 then
- btn:SetPoint("BOTTOMLEFT", BonusActionBarFrame, "BOTTOMLEFT", 0, 0)
- else
- btn:SetPoint("LEFT", AB.bonusButtons[idx - 1], "RIGHT", gap, 0)
- end
+ local col = math.fmod(idx - 1, bpr)
+ local row = math.floor((idx - 1) / bpr)
+ btn:SetPoint("TOPLEFT", BonusActionBarFrame, "TOPLEFT", col * (size + gap), -row * (size + gap))
break
end
end
@@ -1455,9 +1768,22 @@ function AB:Initialize()
end
end
+ -- Shared helper: re-anchor row2/row3 inside their independent holders after
+ -- Blizzard code resets their positions.
+ local function FixRowParenting()
+ if not AB.anchor then return end
+ if AB.row2 and AB.row2Anchor then
+ AB.row2:ClearAllPoints()
+ AB.row2:SetPoint("BOTTOMLEFT", AB.row2Anchor, "BOTTOMLEFT", 0, 0)
+ end
+ if AB.row3 and AB.row3Anchor then
+ AB.row3:ClearAllPoints()
+ AB.row3:SetPoint("BOTTOMLEFT", AB.row3Anchor, "BOTTOMLEFT", 0, 0)
+ end
+ end
+
-- Hook MultiActionBar_Update: Blizzard 在 PLAYER_ENTERING_WORLD 等事件中调用此函数,
-- 它会根据经验条/声望条的高度重设 MultiBarBottomLeft/Right 的位置,覆盖我们的布局。
- -- 仅修正 row2/row3 的锚点,不调用 ApplyConfig 避免触发 MultiBarBottomLeft:Show 再次递归。
local origMultiActionBarUpdate = MultiActionBar_Update
if origMultiActionBarUpdate then
local inMultiBarHook = false
@@ -1465,15 +1791,7 @@ function AB:Initialize()
pcall(origMultiActionBarUpdate)
if AB.anchor and not inMultiBarHook then
inMultiBarHook = true
- local g = AB:GetDB().buttonGap
- if AB.row2 then
- AB.row2:ClearAllPoints()
- AB.row2:SetPoint("BOTTOMLEFT", AB.row1, "TOPLEFT", 0, g)
- end
- if AB.row3 then
- AB.row3:ClearAllPoints()
- AB.row3:SetPoint("BOTTOMLEFT", AB.row2 or AB.row1, "TOPLEFT", 0, g)
- end
+ FixRowParenting()
inMultiBarHook = false
end
end
@@ -1481,21 +1799,12 @@ function AB:Initialize()
-- Hook UIParent_ManageFramePositions: Blizzard 在进出战斗、切换地图等场景调用此
-- 函数重排 MultiBar 位置(会考虑经验条/声望条高度),覆盖我们的布局。
- -- 在原函数执行完毕后立即修正 row2/row3 锚点。
local origManageFramePositions = UIParent_ManageFramePositions
if origManageFramePositions then
UIParent_ManageFramePositions = function(a1, a2, a3)
origManageFramePositions(a1, a2, a3)
- if AB.anchor and AB.row1 then
- local g = AB:GetDB().buttonGap
- if AB.row2 then
- AB.row2:ClearAllPoints()
- AB.row2:SetPoint("BOTTOMLEFT", AB.row1, "TOPLEFT", 0, g)
- end
- if AB.row3 then
- AB.row3:ClearAllPoints()
- AB.row3:SetPoint("BOTTOMLEFT", AB.row2 or AB.row1, "TOPLEFT", 0, g)
- end
+ if AB.anchor then
+ FixRowParenting()
end
if SFrames and SFrames.Chat then
SFrames.Chat:ReanchorChatFrames()
@@ -1547,13 +1856,17 @@ function AB:Initialize()
local db = AB:GetDB()
local size = db.buttonSize
local gap = db.buttonGap
- local rowWidth = (size + gap) * BUTTONS_PER_ROW - gap
+ local bpr = db.bottomBar1PerRow or 12
+ local numC = bpr
+ local numR = math.ceil(BUTTONS_PER_ROW / bpr)
+ local bW = numC * size + math.max(numC - 1, 0) * gap
+ local bH = numR * size + math.max(numR - 1, 0) * gap
BonusActionBarFrame:ClearAllPoints()
BonusActionBarFrame:SetPoint("BOTTOMLEFT", AB.row1, "BOTTOMLEFT", 0, 0)
- BonusActionBarFrame:SetWidth(rowWidth)
- BonusActionBarFrame:SetHeight(size)
+ BonusActionBarFrame:SetWidth(bW)
+ BonusActionBarFrame:SetHeight(bH)
BonusActionBarFrame:SetFrameLevel(AB.row1:GetFrameLevel() + 5)
- LayoutRow(AB.bonusButtons, BonusActionBarFrame, size, gap)
+ LayoutGrid(AB.bonusButtons, BonusActionBarFrame, size, gap, bpr)
local btnLevel = BonusActionBarFrame:GetFrameLevel() + 1
for _, b in ipairs(AB.bonusButtons) do
b:EnableMouse(true)
@@ -1581,16 +1894,8 @@ function AB:Initialize()
-- 进出战斗和切换区域时立即修正行间距,防止 Blizzard 布局覆盖
local function FixRowAnchors()
- if not AB.anchor or not AB.row1 then return end
- local g = AB:GetDB().buttonGap
- if AB.row2 then
- AB.row2:ClearAllPoints()
- AB.row2:SetPoint("BOTTOMLEFT", AB.row1, "TOPLEFT", 0, g)
- end
- if AB.row3 then
- AB.row3:ClearAllPoints()
- AB.row3:SetPoint("BOTTOMLEFT", AB.row2 or AB.row1, "TOPLEFT", 0, g)
- end
+ if not AB.anchor then return end
+ FixRowParenting()
end
SFrames:RegisterEvent("PLAYER_REGEN_DISABLED", FixRowAnchors)
@@ -1602,21 +1907,39 @@ function AB:Initialize()
-- Register movers
if SFrames.Movers and SFrames.Movers.RegisterMover then
if self.anchor then
- SFrames.Movers:RegisterMover("ActionBarBottom", self.anchor, "底部动作条",
+ 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, "右侧动作条",
+ if self.row2Anchor then
+ SFrames.Movers:RegisterMover("ActionBarRow2", self.row2Anchor, "底部动作条2",
+ "BOTTOMLEFT", "SFramesActionBarAnchor", "TOPLEFT", 0, db.buttonGap,
+ function() AB:ApplyStanceBar(); AB:ApplyPetBar() end)
+ end
+ if self.row3Anchor then
+ SFrames.Movers:RegisterMover("ActionBarRow3", self.row3Anchor, "底部动作条3",
+ "BOTTOMLEFT", "SFramesRow2Anchor", "TOPLEFT", 0, db.buttonGap,
+ function() AB:ApplyStanceBar(); AB:ApplyPetBar() end)
+ end
+ if self.rightBar1Holder then
+ SFrames.Movers:RegisterMover("RightBar1", self.rightBar1Holder, "右侧动作条1",
"RIGHT", "UIParent", "RIGHT", db.rightOffsetX, db.rightOffsetY)
end
+ if self.rightBar2Holder then
+ SFrames.Movers:RegisterMover("RightBar2", self.rightBar2Holder, "右侧动作条2",
+ "TOPRIGHT", "SFramesRightBar1Holder", "TOPLEFT", -db.buttonGap, 0)
+ end
+ local topAnchorName = "SFramesActionBarAnchor"
+ if db.barCount >= 3 and self.row3Anchor then topAnchorName = "SFramesRow3Anchor"
+ elseif db.barCount >= 2 and self.row2Anchor then topAnchorName = "SFramesRow2Anchor" end
+
if self.stanceHolder then
SFrames.Movers:RegisterMover("StanceBar", self.stanceHolder, "姿态条",
- "BOTTOMLEFT", "SFramesActionBarAnchor", "TOPLEFT", 0, db.buttonGap)
+ "BOTTOMLEFT", topAnchorName, "TOPLEFT", 0, db.buttonGap)
end
if self.petHolder then
SFrames.Movers:RegisterMover("PetBar", self.petHolder, "宠物条",
- "BOTTOMLEFT", "SFramesActionBarAnchor", "TOPLEFT", 0, db.buttonGap)
+ "BOTTOMLEFT", topAnchorName, "TOPLEFT", 0, db.buttonGap)
end
if self.gryphonLeft then
SFrames.Movers:RegisterMover("GryphonLeft", self.gryphonLeft, "狮鹫(左)",
@@ -1650,6 +1973,10 @@ do
end
end
+function AB:RegisterBindButton(buttonName, bindCommand)
+ BUTTON_BINDING_MAP[buttonName] = bindCommand
+end
+
--------------------------------------------------------------------------------
-- Hotkey text refresh: update the HotKey FontString on buttons to reflect
-- current keybindings (works for all bars including stance and pet).
@@ -1687,6 +2014,15 @@ function AB:RefreshAllHotkeys()
Refresh(self.leftButtons)
Refresh(self.stanceButtons)
Refresh(self.petButtons)
+ if SFrames.ExtraBar and SFrames.ExtraBar.buttons then
+ local ebDb = SFrames.ExtraBar:GetDB()
+ if ebDb.enable then
+ local ebCount = math.min(ebDb.buttonCount or 12, 48)
+ for i = 1, ebCount do
+ RefreshButtonHotkey(SFrames.ExtraBar.buttons[i])
+ end
+ end
+ end
end
local IGNORE_KEYS = {
@@ -1860,6 +2196,16 @@ function AB:EnterKeyBindMode()
Collect(self.leftButtons)
Collect(self.stanceButtons)
Collect(self.petButtons)
+ if SFrames.ExtraBar and SFrames.ExtraBar.buttons then
+ local ebDb = SFrames.ExtraBar:GetDB()
+ if ebDb.enable then
+ local ebCount = math.min(ebDb.buttonCount or 12, 48)
+ for i = 1, ebCount do
+ local b = SFrames.ExtraBar.buttons[i]
+ if b then table.insert(allButtons, b) end
+ end
+ end
+ end
for _, b in ipairs(allButtons) do
local ov = CreateBindOverlay(b)
diff --git a/AuraTracker.lua b/AuraTracker.lua
new file mode 100644
index 0000000..56fd57b
--- /dev/null
+++ b/AuraTracker.lua
@@ -0,0 +1,594 @@
+SFrames.AuraTracker = SFrames.AuraTracker or {}
+
+local AT = SFrames.AuraTracker
+
+AT.units = AT.units or {}
+AT.unitRefs = AT.unitRefs or {}
+AT.durationCache = AT.durationCache or { buff = {}, debuff = {} }
+AT.initialized = AT.initialized or false
+
+local SOURCE_PRIORITY = {
+ player_native = 6,
+ superwow = 5,
+ nanamiplates = 4,
+ shagutweaks = 3,
+ combat_log = 2,
+ estimated = 1,
+}
+
+local function GetNow()
+ return GetTime and GetTime() or 0
+end
+
+local function IsBuffType(auraType)
+ return auraType == "buff"
+end
+
+local function ClampPositive(value)
+ value = tonumber(value) or 0
+ if value > 0 then
+ return value
+ end
+ return nil
+end
+
+local function SafeUnitGUID(unit)
+ if unit and UnitGUID then
+ local ok, guid = pcall(UnitGUID, unit)
+ if ok and guid and guid ~= "" then
+ return guid
+ end
+ end
+ return nil
+end
+
+local function SafeUnitName(unit)
+ if unit and UnitName then
+ local ok, name = pcall(UnitName, unit)
+ if ok and name and name ~= "" then
+ return name
+ end
+ end
+ return nil
+end
+
+local function GetUnitKey(unit)
+ local guid = SafeUnitGUID(unit)
+ if guid then
+ return guid
+ end
+ local name = SafeUnitName(unit)
+ if name then
+ return "name:" .. name
+ end
+ return unit and ("unit:" .. unit) or nil
+end
+
+local function DurationCacheKey(auraType, spellId, name, texture)
+ if spellId and spellId > 0 then
+ return auraType .. ":id:" .. tostring(spellId)
+ end
+ if name and name ~= "" then
+ return auraType .. ":name:" .. string.lower(name)
+ end
+ return auraType .. ":tex:" .. tostring(texture or "")
+end
+
+local function BuildStateKeys(auraType, spellId, name, texture, casterKey)
+ local keys = {}
+ if spellId and spellId > 0 then
+ tinsert(keys, auraType .. ":id:" .. tostring(spellId))
+ if casterKey and casterKey ~= "" then
+ tinsert(keys, auraType .. ":id:" .. tostring(spellId) .. ":caster:" .. casterKey)
+ end
+ end
+ if name and name ~= "" then
+ local lowerName = string.lower(name)
+ tinsert(keys, auraType .. ":name:" .. lowerName)
+ if casterKey and casterKey ~= "" then
+ tinsert(keys, auraType .. ":name:" .. lowerName .. ":caster:" .. casterKey)
+ end
+ if texture and texture ~= "" then
+ tinsert(keys, auraType .. ":name:" .. lowerName .. ":tex:" .. texture)
+ end
+ elseif texture and texture ~= "" then
+ tinsert(keys, auraType .. ":tex:" .. texture)
+ end
+ return keys
+end
+
+local function TooltipLine1()
+ return getglobal("SFramesScanTooltipTextLeft1")
+end
+
+local function CLMatch(msg, pattern)
+ if not msg or not pattern or pattern == "" then return nil end
+ local pat = string.gsub(pattern, "%%%d?%$?s", "(.+)")
+ pat = string.gsub(pat, "%%%d?%$?d", "(%%d+)")
+ for a, b, c in string.gfind(msg, pat) do
+ return a, b, c
+ end
+ return nil
+end
+
+function AT:GetUnitState(unitOrGUID)
+ local key = unitOrGUID
+ if not key then return nil end
+ if string.find(key, "^target") or string.find(key, "^player") or string.find(key, "^party") or string.find(key, "^raid") or string.find(key, "^pet") then
+ key = GetUnitKey(key)
+ end
+ if not key then return nil end
+ if not self.units[key] then
+ self.units[key] = {
+ guid = key,
+ buffs = {},
+ debuffs = {},
+ maps = { buff = {}, debuff = {} },
+ snapshotCount = 0,
+ lastSeen = 0,
+ name = nil,
+ level = 0,
+ }
+ end
+ return self.units[key]
+end
+
+function AT:ClearUnit(unitOrGUID)
+ local key = unitOrGUID
+ if not key then return end
+ if not self.units[key] then
+ key = GetUnitKey(unitOrGUID)
+ end
+ if not key then return end
+ self.units[key] = nil
+
+ for unit, guid in pairs(self.unitRefs) do
+ if guid == key then
+ self.unitRefs[unit] = nil
+ end
+ end
+end
+
+function AT:ClearCurrentTarget()
+ local oldGUID = self.unitRefs["target"]
+ self.unitRefs["target"] = nil
+ if oldGUID then
+ local state = self.units[oldGUID]
+ if state then
+ state.maps.buff = {}
+ state.maps.debuff = {}
+ end
+ end
+end
+
+function AT:RememberDuration(auraType, spellId, name, texture, duration)
+ duration = ClampPositive(duration)
+ if not duration then return end
+ self.durationCache[auraType][DurationCacheKey(auraType, spellId, name, texture)] = duration
+end
+
+function AT:GetRememberedDuration(auraType, spellId, name, texture)
+ return self.durationCache[auraType][DurationCacheKey(auraType, spellId, name, texture)]
+end
+
+function AT:ReadAuraName(unit, index, isBuff, auraID)
+ if auraID and auraID > 0 and SpellInfo then
+ local ok, spellName = pcall(SpellInfo, auraID)
+ if ok and spellName and spellName ~= "" then
+ return spellName
+ end
+ end
+
+ -- Tooltip scan is expensive and can crash on invalid unit/index state.
+ -- Only attempt if unit still exists and the aura slot is still valid.
+ if not SFrames.Tooltip then return nil end
+ if not unit or not UnitExists or not UnitExists(unit) then return nil end
+
+ local ok, text = pcall(function()
+ SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
+ SFrames.Tooltip:ClearLines()
+ if isBuff then
+ SFrames.Tooltip:SetUnitBuff(unit, index)
+ else
+ SFrames.Tooltip:SetUnitDebuff(unit, index)
+ end
+ local line = TooltipLine1()
+ local t = line and line.GetText and line:GetText() or nil
+ SFrames.Tooltip:Hide()
+ return t
+ end)
+
+ if ok and text and text ~= "" then
+ return text
+ end
+ return nil
+end
+
+function AT:GetPlayerAuraTime(unit, index, isBuff, texture)
+ if not UnitIsUnit or not UnitIsUnit(unit, "player") then
+ return nil, nil, nil
+ end
+ if not GetPlayerBuff or not GetPlayerBuffTexture or not GetPlayerBuffTimeLeft then
+ return nil, nil, nil
+ end
+
+ local filter = isBuff and "HELPFUL" or "HARMFUL"
+ for i = 0, 31 do
+ local buffIndex = GetPlayerBuff(i, filter)
+ if buffIndex and buffIndex >= 0 and GetPlayerBuffTexture(buffIndex) == texture then
+ local timeLeft = ClampPositive(GetPlayerBuffTimeLeft(buffIndex))
+ if timeLeft then
+ return timeLeft, nil, "player_native"
+ end
+ end
+ end
+ return nil, nil, nil
+end
+
+function AT:GetExternalTime(unit, auraType, index, state)
+ local isBuff = IsBuffType(auraType)
+ local timeLeft, duration, source
+
+ if state and state.texture then
+ timeLeft, duration, source = self:GetPlayerAuraTime(unit, index, isBuff, state.texture)
+ if timeLeft then
+ return timeLeft, duration, source
+ end
+ end
+
+ if not isBuff and NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff then
+ local effect, _, _, _, _, npDuration, npTimeLeft = NanamiPlates_SpellDB:UnitDebuff(unit, index)
+ npTimeLeft = ClampPositive(npTimeLeft)
+ npDuration = ClampPositive(npDuration)
+ if effect and effect ~= "" and (not state.name or state.name == "") then
+ state.name = effect
+ end
+ if npTimeLeft then
+ return npTimeLeft, npDuration, "nanamiplates"
+ end
+ end
+
+ if not isBuff and ShaguTweaks and ShaguTweaks.libdebuff and ShaguTweaks.libdebuff.UnitDebuff then
+ local _, _, _, _, _, shaguDuration, shaguTimeLeft = ShaguTweaks.libdebuff:UnitDebuff(unit, index)
+ shaguTimeLeft = ClampPositive(shaguTimeLeft)
+ shaguDuration = ClampPositive(shaguDuration)
+ if shaguTimeLeft then
+ return shaguTimeLeft, shaguDuration, "shagutweaks"
+ end
+ end
+
+ return nil, nil, nil
+end
+
+function AT:ApplyTiming(state, timeLeft, duration, source, now, allowWeaker)
+ timeLeft = ClampPositive(timeLeft)
+ if not timeLeft then return false end
+
+ duration = ClampPositive(duration) or state.duration or timeLeft
+ local newPriority = SOURCE_PRIORITY[source] or 0
+ local oldPriority = SOURCE_PRIORITY[state.source] or 0
+
+ if not allowWeaker and state.expirationTime and state.expirationTime > now and oldPriority > newPriority then
+ return false
+ end
+
+ state.duration = duration
+ state.expirationTime = now + timeLeft
+ state.appliedAt = state.expirationTime - duration
+ state.source = source
+ state.isEstimated = (source == "estimated") and 1 or nil
+
+ self:RememberDuration(state.auraType, state.spellId, state.name, state.texture, duration)
+ return true
+end
+
+function AT:CaptureAura(unit, auraType, index)
+ local isBuff = IsBuffType(auraType)
+ local texture, count, dispelType, auraID
+ local hasSuperWoW = SFrames.superwow_active and SpellInfo
+
+ if isBuff then
+ texture, auraID = UnitBuff(unit, index)
+ else
+ texture, count, dispelType, auraID = UnitDebuff(unit, index)
+ end
+
+ if not texture then
+ return nil
+ end
+
+ local spellId = (hasSuperWoW and type(auraID) == "number" and auraID > 0) and auraID or nil
+ -- Only call ReadAuraName when we have a spellId (fast path) or for the first 16 slots
+ -- to avoid spamming tooltip API on every aura slot every UNIT_AURA event.
+ local name
+ if spellId or index <= 16 then
+ name = self:ReadAuraName(unit, index, isBuff, spellId)
+ end
+
+ return {
+ auraType = auraType,
+ index = index,
+ spellId = spellId,
+ name = name,
+ texture = texture,
+ stacks = tonumber(count) or 0,
+ dispelType = dispelType,
+ casterKey = nil,
+ }
+end
+
+function AT:FindPreviousState(unitState, auraType, index, info)
+ local previous = unitState.maps[auraType][index]
+ if previous then
+ local keys = BuildStateKeys(auraType, info.spellId, info.name, info.texture, info.casterKey)
+ local prevKeys = BuildStateKeys(auraType, previous.spellId, previous.name, previous.texture, previous.casterKey)
+
+ for _, key in ipairs(keys) do
+ for _, prevKey in ipairs(prevKeys) do
+ if key == prevKey then
+ return previous
+ end
+ end
+ end
+ end
+
+ local states = IsBuffType(auraType) and unitState.buffs or unitState.debuffs
+ local keys = BuildStateKeys(auraType, info.spellId, info.name, info.texture, info.casterKey)
+ for _, key in ipairs(keys) do
+ local state = states[key]
+ if state then
+ return state
+ end
+ end
+
+ return nil
+end
+
+function AT:UpdateSnapshotState(unitState, unit, auraType, index, info, now)
+ local state = self:FindPreviousState(unitState, auraType, index, info)
+ if not state then
+ state = {
+ guid = unitState.guid,
+ auraType = auraType,
+ firstSeenAt = now,
+ }
+ end
+
+ state.guid = unitState.guid
+ state.auraType = auraType
+ state.spellId = info.spellId
+ state.name = info.name or state.name
+ state.texture = info.texture
+ state.stacks = info.stacks
+ state.dispelType = info.dispelType
+ state.casterKey = info.casterKey
+ state.lastSeen = now
+ state.index = index
+
+ local timeLeft, duration, source = self:GetExternalTime(unit, auraType, index, state)
+ if timeLeft then
+ self:ApplyTiming(state, timeLeft, duration, source, now)
+ elseif state.expirationTime and state.expirationTime <= now then
+ state.expirationTime = nil
+ state.duration = nil
+ state.appliedAt = nil
+ state.source = nil
+ end
+
+ return state
+end
+
+function AT:RebuildStateMaps(unitState, auraType, slots, now)
+ local active = {}
+ local newMap = {}
+
+ for index, state in pairs(slots) do
+ if state then
+ for _, key in ipairs(BuildStateKeys(auraType, state.spellId, state.name, state.texture, state.casterKey)) do
+ active[key] = state
+ end
+ newMap[index] = state
+ end
+ end
+
+ if IsBuffType(auraType) then
+ unitState.buffs = active
+ else
+ unitState.debuffs = active
+ end
+ unitState.maps[auraType] = newMap
+ unitState.lastSeen = now
+end
+
+function AT:ApplyCombatHint(auraType, auraName)
+ if not auraName or auraName == "" then return end
+ if auraType ~= "debuff" then return end
+ local guid = self.unitRefs["target"]
+ if not guid then return end
+
+ local unitState = self.units[guid]
+ if not unitState then return end
+
+ local active = IsBuffType(auraType) and unitState.buffs or unitState.debuffs
+ local state = active[auraType .. ":name:" .. string.lower(auraName)]
+ if not state then return end
+
+ local remembered = self:GetRememberedDuration(auraType, state.spellId, state.name, state.texture)
+ if remembered then
+ self:ApplyTiming(state, remembered, remembered, "combat_log", GetNow(), true)
+ end
+end
+
+function AT:ClearCombatHint(auraName)
+ if not auraName or auraName == "" then return end
+ local guid = self.unitRefs["target"]
+ if not guid then return end
+
+ local unitState = self.units[guid]
+ if not unitState then return end
+
+ local lowerName = string.lower(auraName)
+ for _, auraType in ipairs({ "buff", "debuff" }) do
+ local active = IsBuffType(auraType) and unitState.buffs or unitState.debuffs
+ local state = active[auraType .. ":name:" .. lowerName]
+ if state then
+ state.expirationTime = nil
+ state.duration = nil
+ state.appliedAt = nil
+ state.source = nil
+ end
+ end
+end
+
+function AT:HandleCombatMessage(msg)
+ if not msg or msg == "" or not UnitExists or not UnitExists("target") then return end
+
+ local targetName = SafeUnitName("target")
+ if not targetName then return end
+
+ local targetUnit, auraName = CLMatch(msg, AURAADDEDOTHERHARMFUL or "%s is afflicted by %s.")
+ if targetUnit == targetName and auraName then
+ self:ApplyCombatHint("debuff", auraName)
+ return
+ end
+
+ auraName, targetUnit = CLMatch(msg, AURAREMOVEDOTHER or "%s fades from %s.")
+ if targetUnit == targetName and auraName then
+ self:ClearCombatHint(auraName)
+ end
+end
+
+function AT:HandleAuraSnapshot(unit)
+ if not unit then return end
+ if not UnitExists or not UnitExists(unit) then
+ if unit == "target" then
+ self:ClearCurrentTarget()
+ end
+ return
+ end
+
+ local guid = GetUnitKey(unit)
+ if not guid then return end
+
+ local now = GetNow()
+ local unitState = self:GetUnitState(guid)
+ unitState.guid = guid
+ unitState.name = SafeUnitName(unit)
+ unitState.level = (UnitLevel and UnitLevel(unit)) or 0
+ unitState.lastSeen = now
+
+ self.unitRefs[unit] = guid
+
+ local buffSlots = {}
+ local debuffSlots = {}
+
+ for i = 1, 32 do
+ local info = self:CaptureAura(unit, "buff", i)
+ if info then
+ buffSlots[i] = self:UpdateSnapshotState(unitState, unit, "buff", i, info, now)
+ end
+ end
+
+ for i = 1, 32 do
+ local info = self:CaptureAura(unit, "debuff", i)
+ if info then
+ debuffSlots[i] = self:UpdateSnapshotState(unitState, unit, "debuff", i, info, now)
+ end
+ end
+
+ self:RebuildStateMaps(unitState, "buff", buffSlots, now)
+ self:RebuildStateMaps(unitState, "debuff", debuffSlots, now)
+
+ unitState.snapshotCount = unitState.snapshotCount + 1
+end
+
+function AT:GetAuraState(unit, auraType, index)
+ local unitState = self:GetUnitState(unit)
+ if not unitState then return nil end
+ local map = unitState.maps[auraType]
+ return map and map[index] or nil
+end
+
+function AT:GetAuraTimeLeft(unit, auraType, index)
+ local state = self:GetAuraState(unit, auraType, index)
+ if not state or not state.expirationTime then
+ return nil
+ end
+
+ local remaining = state.expirationTime - GetNow()
+ if remaining > 0 then
+ return remaining
+ end
+ return nil
+end
+
+function AT:PurgeStaleUnits()
+ local now = GetNow()
+ local activeTargetGUID = self.unitRefs["target"]
+ for guid, state in pairs(self.units) do
+ if guid ~= activeTargetGUID and state.lastSeen and (now - state.lastSeen) > 120 then
+ self.units[guid] = nil
+ end
+ end
+end
+
+function AT:OnEvent()
+ if event == "PLAYER_TARGET_CHANGED" then
+ if UnitExists and UnitExists("target") then
+ self:HandleAuraSnapshot("target")
+ else
+ self:ClearCurrentTarget()
+ end
+ self:PurgeStaleUnits()
+ return
+ end
+
+ if event == "UNIT_AURA" and arg1 == "target" then
+ self:HandleAuraSnapshot("target")
+ return
+ end
+
+ if event == "PLAYER_ENTERING_WORLD" then
+ self:ClearCurrentTarget()
+ self:PurgeStaleUnits()
+ return
+ end
+
+ if arg1 and string.find(event, "CHAT_MSG_SPELL") then
+ self:HandleCombatMessage(arg1)
+ end
+end
+
+function AT:Initialize()
+ if self.initialized then return end
+ self.initialized = true
+
+ local frame = CreateFrame("Frame", "SFramesAuraTracker", UIParent)
+ self.frame = frame
+
+ frame:RegisterEvent("PLAYER_TARGET_CHANGED")
+ frame:RegisterEvent("UNIT_AURA")
+ frame:RegisterEvent("PLAYER_ENTERING_WORLD")
+ local chatEvents = {
+ "CHAT_MSG_SPELL_SELF_DAMAGE",
+ "CHAT_MSG_SPELL_SELF_BUFF",
+ "CHAT_MSG_SPELL_PARTY_DAMAGE",
+ "CHAT_MSG_SPELL_PARTY_BUFF",
+ "CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE",
+ "CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF",
+ "CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE",
+ "CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF",
+ "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE",
+ "CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE",
+ "CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE",
+ "CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF",
+ "CHAT_MSG_SPELL_CREATURE_VS_PARTY_BUFF",
+ "CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF",
+ }
+ for _, ev in ipairs(chatEvents) do
+ frame:RegisterEvent(ev)
+ end
+ frame:SetScript("OnEvent", function()
+ AT:OnEvent()
+ end)
+end
diff --git a/Bindings.xml b/Bindings.xml
index 2e26d22..c4337e0 100644
--- a/Bindings.xml
+++ b/Bindings.xml
@@ -4,4 +4,149 @@
SFrames.WorldMap:ToggleNav()
end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(1) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(2) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(3) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(4) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(5) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(6) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(7) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(8) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(9) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(10) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(11) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(12) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(13) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(14) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(15) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(16) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(17) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(18) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(19) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(20) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(21) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(22) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(23) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(24) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(25) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(26) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(27) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(28) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(29) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(30) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(31) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(32) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(33) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(34) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(35) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(36) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(37) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(38) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(39) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(40) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(41) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(42) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(43) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(44) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(45) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(46) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(47) end
+
+
+ if SFrames and SFrames.ExtraBar then SFrames.ExtraBar:RunButton(48) end
+
diff --git a/CharacterPanel.lua b/CharacterPanel.lua
index 1422773..b030f86 100644
--- a/CharacterPanel.lua
+++ b/CharacterPanel.lua
@@ -338,6 +338,58 @@ local BASE_SPELL_CRIT = {
DRUID = 1.8, SHAMAN = 2.3, PALADIN = 0,
}
+--------------------------------------------------------------------------------
+-- Temporary weapon enchant crit detection (sharpening stones / scopes)
+-- Scans tooltip green text for crit keywords, returns crit% bonus (e.g. 2)
+--------------------------------------------------------------------------------
+local _tempEnchTip
+local function GetTempEnchantCrit(slotId)
+ if not GetWeaponEnchantInfo then return 0 end
+ -- slotId 16=MainHand, 17=OffHand, 18=Ranged
+ local hasMain, _, _, hasOff
+ hasMain, _, _, hasOff = GetWeaponEnchantInfo()
+ if slotId == 16 and not hasMain then return 0 end
+ if slotId == 17 and not hasOff then return 0 end
+ if slotId == 18 and not hasMain and not hasOff then
+ -- ranged slot: some servers report via hasMain for ranged-only classes
+ -- try scanning anyway if there's a ranged weapon equipped
+ if not GetInventoryItemLink("player", 18) then return 0 end
+ end
+
+ if not _tempEnchTip then
+ _tempEnchTip = CreateFrame("GameTooltip", "SFramesCPTempEnchTip", UIParent, "GameTooltipTemplate")
+ _tempEnchTip:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -300, -300)
+ end
+ local tip = _tempEnchTip
+ tip:SetOwner(UIParent, "ANCHOR_NONE")
+ tip:ClearLines()
+ tip:SetInventoryItem("player", slotId)
+ local n = tip:NumLines()
+ if not n or n < 2 then return 0 end
+
+ for i = 2, n do
+ local obj = _G["SFramesCPTempEnchTipTextLeft" .. i]
+ if obj then
+ local txt = obj:GetText()
+ if txt and txt ~= "" then
+ local r, g, b = obj:GetTextColor()
+ -- green text = enchant/buff line
+ if g > 0.8 and r < 0.5 and b < 0.5 then
+ -- Match patterns like: "+2% 致命一击" / "+2% Critical" / "致命一击几率提高2%"
+ local _, _, pct = string.find(txt, "(%d+)%%%s*致命")
+ if not pct then _, _, pct = string.find(txt, "致命.-(%d+)%%") end
+ if not pct then _, _, pct = string.find(txt, "(%d+)%%%s*[Cc]rit") end
+ if not pct then _, _, pct = string.find(txt, "[Cc]rit.-(%d+)%%") end
+ if not pct then _, _, pct = string.find(txt, "(%d+)%%%s*暴击") end
+ if not pct then _, _, pct = string.find(txt, "暴击.-(%d+)%%") end
+ if pct then return tonumber(pct) or 0 end
+ end
+ end
+ end
+ end
+ return 0
+end
+
local function CalcMeleeCrit()
local _, class = UnitClass("player")
class = class or ""
@@ -523,8 +575,9 @@ local function FullMeleeCrit()
end
local gearCrit = GetGearBonus("CRIT")
local talentCrit = GetTalentBonus("meleeCrit")
- return baseCrit + agiCrit + gearCrit + talentCrit,
- baseCrit, agiCrit, gearCrit, talentCrit
+ local tempCrit = GetTempEnchantCrit(16)
+ return baseCrit + agiCrit + gearCrit + talentCrit + tempCrit,
+ baseCrit, agiCrit, gearCrit, talentCrit, tempCrit
end
local function FullRangedCrit()
local _, class = UnitClass("player")
@@ -537,8 +590,9 @@ local function FullRangedCrit()
end
local gearCrit = GetGearBonus("RANGEDCRIT") + GetGearBonus("CRIT")
local talentCrit = GetTalentBonus("rangedCrit")
- return baseCrit + agiCrit + gearCrit + talentCrit,
- baseCrit, agiCrit, gearCrit, talentCrit
+ local tempCrit = GetTempEnchantCrit(18)
+ return baseCrit + agiCrit + gearCrit + talentCrit + tempCrit,
+ baseCrit, agiCrit, gearCrit, talentCrit, tempCrit
end
local function FullSpellCrit()
local _, class = UnitClass("player")
@@ -659,6 +713,7 @@ CS.FullSpellHit = FullSpellHit
CS.GetTalentDetailsFor = GetTalentDetailsFor
CS.GetGearBonus = GetGearBonus
CS.GetItemBonusLib = GetItemBonusLib
+CS.GetTempEnchantCrit = GetTempEnchantCrit
CS.AGI_PER_MELEE_CRIT = AGI_PER_MELEE_CRIT
SFrames.CharacterPanel.CS = CS
@@ -2009,7 +2064,7 @@ function CP:BuildEquipmentPage()
local crit = CS.SafeGetMeleeCrit()
CS.TipKV("当前暴击率:", string.format("%.2f%%", crit), 0.7,0.7,0.75, 1,1,0.5)
else
- local total, base, agiC, gearC, talC = CS.FullMeleeCrit()
+ local total, base, agiC, gearC, talC, tempC = CS.FullMeleeCrit()
CS.TipLine("来源分项:", 0.5,0.8,1)
if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end
if agiC > 0 then CS.TipKV(" 敏捷暴击:", string.format("%.2f%%", agiC)) end
@@ -2021,6 +2076,9 @@ function CP:BuildEquipmentPage()
d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
end
end
+ if tempC and tempC > 0 then
+ CS.TipKV(" 临时附魔(磨刀石):", string.format("+%d%%", tempC), 0.7,0.7,0.75, 0.3,1,0.3)
+ end
GameTooltip:AddLine(" ")
CS.TipKV("合计暴击率:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5)
CS.TipLine("(Buff 暴击未计入)", 0.8,0.5,0.3)
@@ -2135,7 +2193,7 @@ function CP:BuildEquipmentPage()
if fromAPI then
CS.TipKV("当前暴击率:", string.format("%.2f%%", CS.SafeGetRangedCrit()), 0.7,0.7,0.75, 1,1,0.5)
else
- local total, base, agiC, gearC, talC = CS.FullRangedCrit()
+ local total, base, agiC, gearC, talC, tempC = CS.FullRangedCrit()
CS.TipLine("来源分项:", 0.5,0.8,1)
if base > 0 then CS.TipKV(" 基础暴击:", string.format("%.2f%%", base)) end
if agiC > 0 then CS.TipKV(" 敏捷暴击:", string.format("%.2f%%", agiC)) end
@@ -2147,6 +2205,9 @@ function CP:BuildEquipmentPage()
d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
end
end
+ if tempC and tempC > 0 then
+ CS.TipKV(" 临时附魔(瞄准镜):", string.format("+%d%%", tempC), 0.7,0.7,0.75, 0.3,1,0.3)
+ end
GameTooltip:AddLine(" ")
CS.TipKV("合计暴击率:", string.format("%.2f%%", total), 0.7,0.7,0.75, 1,1,0.5)
CS.TipLine("(Buff 暴击未计入)", 0.8,0.5,0.3)
diff --git a/Chat.lua b/Chat.lua
index 20317c2..3b88aac 100644
--- a/Chat.lua
+++ b/Chat.lua
@@ -18,6 +18,7 @@ local DEFAULTS = {
topPadding = 30,
bottomPadding = 8,
bgAlpha = 0.45,
+ hoverTransparent = true,
activeTab = 1,
editBoxPosition = "bottom",
editBoxX = 0,
@@ -739,27 +740,87 @@ local function GetTranslateFilterKeyForEvent(event)
return TRANSLATE_EVENT_FILTERS[event]
end
-local function ParseHardcoreDeathMessage(text)
- if type(text) ~= "string" or text == "" then return nil end
- if not string.find(text, "硬核") and not string.find(text, "死亡") then
- local lower = string.lower(text)
- if not string.find(lower, "hc news") and not string.find(lower, "has fallen")
- and not string.find(lower, "died") and not string.find(lower, "slain") then
- return nil
+-- ============================================================
+-- HC 公会成员缓存:用于"仅通报工会成员"过滤
+-- ============================================================
+local HCGuildMemberCache = {}
+
+local function RefreshHCGuildCache()
+ HCGuildMemberCache = {}
+ if not (IsInGuild and IsInGuild()) then return end
+ if not GetNumGuildMembers then return end
+ local total = GetNumGuildMembers()
+ for i = 1, total do
+ local name = GetGuildRosterInfo(i)
+ if type(name) == "string" and name ~= "" then
+ HCGuildMemberCache[name] = true
end
end
+end
+
+local function IsHCGuildMember(name)
+ return type(name) == "string" and HCGuildMemberCache[name] == true
+end
+
+-- 从 HC 系统消息中提取主角色名(死亡/升级均支持)
+local function ParseHCCharacterName(text)
+ if type(text) ~= "string" then return nil end
+ -- 中文死亡格式: "硬核角色 NAME(等级 N)"
+ local _, _, n1 = string.find(text, "硬核角色%s+(.-)(等级")
+ if n1 and n1 ~= "" then return n1 end
+ -- 中文升级格式: "NAME 在硬核模式中已达到"
+ local _, _, n2 = string.find(text, "^(.-)%s+在硬核模式")
+ if n2 and n2 ~= "" then return n2 end
+ -- 英文死亡格式: "Hardcore character NAME (Level N)"
+ local _, _, n3 = string.find(text, "[Hh]ardcore character%s+(.-)%s+%(Level")
+ if n3 and n3 ~= "" then return n3 end
+ -- 英文升级格式: "NAME has reached level"
+ local _, _, n4 = string.find(text, "^(.-)%s+has reached level")
+ if n4 and n4 ~= "" then return n4 end
+ return nil
+end
+
+-- 检测HC等级里程碑消息(如"达到20级");返回等级数字,否则返回nil
+local function ParseHardcoreLevelMessage(text)
+ if type(text) ~= "string" or text == "" then return nil end
+ local lower = string.lower(text)
+ -- 英文: "has reached level X" / "reached level X"
+ if string.find(lower, "reached level") then
+ local _, _, lvl = string.find(lower, "reached level%s+(%d+)")
+ return tonumber(lvl) or 1
+ end
+ -- 中文: "达到 X 级" / "已达到X级"
+ if string.find(text, "达到") then
+ local _, _, lvl = string.find(text, "达到%s*(%d+)%s*级")
+ if lvl then return tonumber(lvl) end
+ end
+ return nil
+end
+
+-- 检测HC死亡通报消息;排除等级里程碑;返回等级数字,否则返回nil
+local function ParseHardcoreDeathMessage(text)
+ if type(text) ~= "string" or text == "" then return nil end
+ -- 先排除等级里程碑消息,避免误判(里程碑消息也含"死亡"字样)
+ if ParseHardcoreLevelMessage(text) then return nil end
+ -- 初步过滤:必须含有死亡/击杀相关关键词
+ local lower = string.lower(text)
+ local hasHC = string.find(text, "硬核") or string.find(lower, "hardcore") or string.find(lower, "hc")
+ local hasDead = string.find(text, "死亡") or string.find(text, "击杀")
+ or string.find(lower, "has fallen") or string.find(lower, "slain")
+ or string.find(lower, "died") or string.find(lower, "hc news")
+ if not hasDead then return nil end
local clean = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
clean = string.gsub(clean, "|r", "")
- local _, _, lvlStr = string.find(clean, "Level%s+(%d+)")
+ -- 英文格式: Level: 17 / Level 17
+ local _, _, lvlStr = string.find(clean, "Level%s*:%s*(%d+)")
if lvlStr then return tonumber(lvlStr) end
- local _, _, lvlStr2 = string.find(clean, "(%d+)%s*级")
+ local _, _, lvlStr2 = string.find(clean, "Level%s+(%d+)")
if lvlStr2 then return tonumber(lvlStr2) end
- local _, _, lvlStr3 = string.find(clean, "Level:%s+(%d+)")
+ -- 中文格式: 等级 1 / 等级1 / (等级 1)
+ local _, _, lvlStr3 = string.find(clean, "等级%s*(%d+)")
if lvlStr3 then return tonumber(lvlStr3) end
- local lower = string.lower(clean)
- if string.find(lower, "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(lower, "has fallen"))) then
- return 1
- end
+ -- 兜底:有死亡关键词即返回1
+ if hasDead then return 1 end
return nil
end
@@ -774,6 +835,68 @@ local function CleanTextForTranslation(text)
return clean
end
+-- HC 死亡消息的句式精确解析:提取怪物名和地点名,翻译后原位替换,玩家名不动
+-- 中文格式: "...硬核角色 [玩家](等级 N)被 [怪物](等级 N)击杀。这发生在 [地点]。..."
+-- 英文格式: "...character [PLAYER] (Level N) has been slain by [MONSTER] (Level N)...in [ZONE]."
+local function TranslateHCDeathParts(text, onDone)
+ local api = _G.STranslateAPI
+ local canTranslate = api and api.IsReady and api.IsReady() and api.ForceToChinese
+
+ -- 提取怪物名(中文句式)
+ local _, _, monster = string.find(text, ")被%s*(.-)(等级")
+ -- 提取地点名(中文句式)
+ local _, _, zone = string.find(text, "这发生在%s*(.-)。")
+
+ -- 英文句式兜底
+ if not monster then
+ local _, _, m = string.find(text, "slain by%s+(.-)%s+%(Level")
+ if m then monster = m end
+ end
+ if not zone then
+ local _, _, z = string.find(text, "This happened in%s+(.-)[%.%!]")
+ if z then zone = z end
+ end
+
+ -- 收集需要翻译的词(含英文字母才有翻译意义)
+ local targets = {}
+ if monster and string.find(monster, "[A-Za-z]") then
+ table.insert(targets, monster)
+ end
+ if zone and string.find(zone, "[A-Za-z]") and zone ~= monster then
+ table.insert(targets, zone)
+ end
+
+ if not canTranslate or table.getn(targets) == 0 then
+ onDone(text)
+ return
+ end
+
+ -- 并行翻译,全部完成后替换回原文
+ local out = text
+ local total = table.getn(targets)
+ local doneCount = 0
+ for i = 1, total do
+ local orig = targets[i]
+ api.ForceToChinese(orig, function(translated, err, meta)
+ if translated and translated ~= "" and translated ~= orig then
+ local esc = string.gsub(orig, "([%(%)%.%%%+%-%*%?%[%^%$])", "%%%1")
+ local safeRepl = string.gsub(translated, "%%", "%%%%")
+ out = string.gsub(out, esc, safeRepl)
+ end
+ doneCount = doneCount + 1
+ if doneCount == total then
+ onDone(out)
+ end
+ end, "Nanami-UI-HC")
+ end
+end
+
+-- 等级里程碑消息:玩家名无需翻译(是玩家自选名),整条消息已由服务器本地化,直接转发
+local function TranslateHCLevelParts(text, onDone)
+ -- 仅当存在非玩家名的英文才翻译;里程碑消息唯一英文就是玩家名,故直接原文转发
+ onDone(text)
+end
+
local function ForceHide(object)
if not object then return end
object:Hide()
@@ -789,6 +912,66 @@ local function ForceInvisible(object)
if object.EnableMouse then object:EnableMouse(false) end
end
+local function StripChatFrameArtwork(chatFrame)
+ if not chatFrame then return end
+
+ local frameID = chatFrame.GetID and chatFrame:GetID()
+ if frameID then
+ if SetChatWindowColor then
+ pcall(function() SetChatWindowColor(frameID, 0, 0, 0) end)
+ end
+ if SetChatWindowAlpha then
+ pcall(function() SetChatWindowAlpha(frameID, 0) end)
+ end
+ end
+
+ if FCF_SetWindowAlpha then
+ pcall(function() FCF_SetWindowAlpha(chatFrame, 0) end)
+ end
+
+ if chatFrame.SetBackdropColor then
+ chatFrame:SetBackdropColor(0, 0, 0, 0)
+ end
+ if chatFrame.SetBackdropBorderColor then
+ chatFrame:SetBackdropBorderColor(0, 0, 0, 0)
+ end
+ if chatFrame.SetBackdrop then
+ pcall(function() chatFrame:SetBackdrop(nil) end)
+ end
+
+ local frameName = chatFrame.GetName and chatFrame:GetName()
+ if type(frameName) == "string" and frameName ~= "" then
+ local legacyTextures = {
+ frameName .. "Background",
+ frameName .. "BackgroundLeft",
+ frameName .. "BackgroundMiddle",
+ frameName .. "BackgroundRight",
+ frameName .. "BottomButton",
+ frameName .. "ButtonFrame",
+ frameName .. "ButtonFrameBackground",
+ }
+ for _, texName in ipairs(legacyTextures) do
+ local tex = _G[texName]
+ if tex then
+ if tex.SetTexture then tex:SetTexture(nil) end
+ if tex.SetVertexColor then tex:SetVertexColor(0, 0, 0, 0) end
+ if tex.SetAlpha then tex:SetAlpha(0) end
+ tex:Hide()
+ end
+ end
+ end
+
+ local regions = { chatFrame:GetRegions() }
+ for _, region in ipairs(regions) do
+ if region and region.GetObjectType and region:GetObjectType() == "Texture" then
+ if region.SetTexture then region:SetTexture(nil) end
+ if region.SetVertexColor then region:SetVertexColor(0, 0, 0, 0) end
+ if region.SetAlpha then region:SetAlpha(0) end
+ region:Hide()
+ end
+ end
+end
+
local function CreateFont(parent, size, justify)
if SFrames and SFrames.CreateFontString then
return SFrames:CreateFontString(parent, size, justify)
@@ -1547,6 +1730,9 @@ local function EnsureDB()
if type(db.editBoxY) ~= "number" then db.editBoxY = tonumber(db.editBoxY) or DEFAULTS.editBoxY end
if db.translateEnabled == nil then db.translateEnabled = true end
if db.chatMonitorEnabled == nil then db.chatMonitorEnabled = true end
+ if db.hcDeathToGuild == nil then db.hcDeathToGuild = true end
+ if db.hcLevelToGuild == nil then db.hcLevelToGuild = true end
+ if db.hcGuildMemberOnly == nil then db.hcGuildMemberOnly = false end
if type(db.layoutVersion) ~= "number" then db.layoutVersion = 1 end
if db.layoutVersion < 2 then
db.topPadding = DEFAULTS.topPadding
@@ -1895,6 +2081,7 @@ function SFrames.Chat:GetConfig()
topPadding = math.floor(Clamp(db.topPadding, 24, 64) + 0.5),
bottomPadding = math.floor(Clamp(db.bottomPadding, 4, 18) + 0.5),
bgAlpha = Clamp(db.bgAlpha, 0, 1),
+ hoverTransparent = (db.hoverTransparent ~= false),
editBoxPosition = editBoxPosition,
editBoxX = tonumber(db.editBoxX) or DEFAULTS.editBoxX,
editBoxY = tonumber(db.editBoxY) or DEFAULTS.editBoxY,
@@ -3321,6 +3508,19 @@ function SFrames.Chat:RefreshConfigFrame()
end
end
+ if self.cfgMonitorSection then
+ self.cfgMonitorSection:SetAlpha(1)
+ end
+ if self.cfgMonitorCb then
+ self.cfgMonitorCb:Enable()
+ end
+ if self.cfgMonitorDesc then
+ self.cfgMonitorDesc:SetTextColor(0.7, 0.7, 0.74)
+ end
+ if self.cfgMonitorReloadHint then
+ self.cfgMonitorReloadHint:SetTextColor(0.9, 0.75, 0.5)
+ end
+
if self.configControls then
for i = 1, table.getn(self.configControls) do
local ctrl = self.configControls[i]
@@ -3485,12 +3685,12 @@ function SFrames.Chat:EnsureConfigFrame()
transDesc:SetPoint("TOPLEFT", engineSection, "TOPLEFT", 38, -50)
transDesc:SetWidth(520)
transDesc:SetJustifyH("LEFT")
- transDesc:SetText("关闭后将完全停止调用 STranslateAPI 翻译接口,所有标签的自动翻译均不生效。")
+ transDesc:SetText("关闭后将完全停止调用 STranslateAPI 翻译接口,所有标签的自动翻译均不生效。聊天监控可独立启用。")
transDesc:SetTextColor(0.7, 0.7, 0.74)
local monitorSection = CreateCfgSection(generalPage, "聊天消息监控", 0, -136, 584, 160, fontPath)
- AddControl(CreateCfgCheck(monitorSection, "启用聊天消息监控与收集", 16, -30,
+ local monitorCb = CreateCfgCheck(monitorSection, "启用聊天消息监控与收集", 16, -30,
function() return EnsureDB().chatMonitorEnabled ~= false end,
function(checked)
EnsureDB().chatMonitorEnabled = (checked == true)
@@ -3498,15 +3698,19 @@ function SFrames.Chat:EnsureConfigFrame()
function()
SFrames.Chat:RefreshConfigFrame()
end
- ))
+ )
+ AddControl(monitorCb)
+ self.cfgMonitorCb = monitorCb
+ self.cfgMonitorSection = monitorSection
local monDesc = monitorSection:CreateFontString(nil, "OVERLAY")
monDesc:SetFont(fontPath, 10, "OUTLINE")
monDesc:SetPoint("TOPLEFT", monitorSection, "TOPLEFT", 38, -50)
monDesc:SetWidth(520)
monDesc:SetJustifyH("LEFT")
- monDesc:SetText("启用后将拦截聊天消息,提供消息历史缓存、右键复制 [+] 标记、频道翻译触发等功能。\n关闭后消息将原样通过,不做任何处理(翻译、复制等功能不可用)。")
+ monDesc:SetText("启用后将拦截聊天消息,提供消息历史缓存、右键复制 [+] 标记、职业染色等功能。\n可独立于 AI 翻译开关使用。关闭后消息将原样通过,[+] 复制等功能不可用。")
monDesc:SetTextColor(0.7, 0.7, 0.74)
+ self.cfgMonitorDesc = monDesc
local reloadHint = monitorSection:CreateFontString(nil, "OVERLAY")
reloadHint:SetFont(fontPath, 10, "OUTLINE")
@@ -3515,11 +3719,12 @@ function SFrames.Chat:EnsureConfigFrame()
reloadHint:SetJustifyH("LEFT")
reloadHint:SetText("提示:更改监控开关后建议 /reload 以确保完全生效。")
reloadHint:SetTextColor(0.9, 0.75, 0.5)
+ self.cfgMonitorReloadHint = reloadHint
end
local windowPage = CreatePage("window")
do
- local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 274, fontPath)
+ local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 304, fontPath)
AddControl(CreateCfgSlider(appearance, "宽度", 16, -46, 260, 320, 900, 1,
function() return EnsureDB().width end,
function(v) EnsureDB().width = v end,
@@ -3580,13 +3785,18 @@ function SFrames.Chat:EnsureConfigFrame()
function(checked) EnsureDB().showPlayerLevel = (checked == true) end,
function() SFrames.Chat:RefreshConfigFrame() end
))
+ AddControl(CreateCfgCheck(appearance, "悬停显示背景", 16, -248,
+ function() return EnsureDB().hoverTransparent ~= false end,
+ function(checked) EnsureDB().hoverTransparent = (checked == true) end,
+ function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
+ ))
self.cfgWindowSummaryText = appearance:CreateFontString(nil, "OVERLAY")
self.cfgWindowSummaryText:SetFont(fontPath, 10, "OUTLINE")
self.cfgWindowSummaryText:SetPoint("BOTTOMLEFT", appearance, "BOTTOMLEFT", 16, 10)
self.cfgWindowSummaryText:SetTextColor(0.74, 0.74, 0.8)
- local inputSection = CreateCfgSection(windowPage, "输入框", 0, -290, 584, 114, fontPath)
+ local inputSection = CreateCfgSection(windowPage, "输入框", 0, -320, 584, 114, fontPath)
self.cfgInputModeText = inputSection:CreateFontString(nil, "OVERLAY")
self.cfgInputModeText:SetFont(fontPath, 11, "OUTLINE")
self.cfgInputModeText:SetPoint("TOPLEFT", inputSection, "TOPLEFT", 16, -30)
@@ -3618,7 +3828,7 @@ function SFrames.Chat:EnsureConfigFrame()
inputTip:SetText("建议优先使用顶部或底部模式;自由拖动适合特殊布局。")
inputTip:SetTextColor(0.74, 0.74, 0.8)
- local actionSection = CreateCfgSection(windowPage, "窗口操作", 0, -398, 584, 96, fontPath)
+ local actionSection = CreateCfgSection(windowPage, "窗口操作", 0, -428, 584, 96, fontPath)
CreateCfgButton(actionSection, "重置位置", 16, -32, 108, 24, function()
SFrames.Chat:ResetPosition()
SFrames.Chat:RefreshConfigFrame()
@@ -4050,7 +4260,7 @@ function SFrames.Chat:EnsureConfigFrame()
local hcPage = CreatePage("hc")
do
- local hcControls = CreateCfgSection(hcPage, "硬核生存服务器专属", 0, 0, 584, 182, fontPath)
+ local hcControls = CreateCfgSection(hcPage, "硬核生存服务器专属", 0, 0, 584, 382, fontPath)
local hcStatusText = hcControls:CreateFontString(nil, "OVERLAY")
hcStatusText:SetFont(fontPath, 10, "OUTLINE")
@@ -4095,7 +4305,7 @@ function SFrames.Chat:EnsureConfigFrame()
deathTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -112)
deathTip:SetWidth(540)
deathTip:SetJustifyH("LEFT")
- deathTip:SetText("关闭那些“某某在XX级死亡”的系统提示。")
+ deathTip:SetText("关闭那些[某某在XX级死亡]的系统提示。")
deathTip:SetTextColor(0.8, 0.7, 0.7)
AddControl(CreateCfgSlider(hcControls, "最低死亡通报等级", 340, -82, 210, 0, 60, 1,
@@ -4104,6 +4314,48 @@ function SFrames.Chat:EnsureConfigFrame()
function(v) return (v == 0) and "所有击杀" or (tostring(v) .. " 级及以上") end,
function() SFrames.Chat:RefreshConfigFrame() end
))
+
+ AddControl(CreateCfgCheck(hcControls, "翻译死亡通报并转发到公会频道", 16, -138,
+ function() return EnsureDB().hcDeathToGuild ~= false end,
+ function(checked) EnsureDB().hcDeathToGuild = (checked == true) end,
+ function() SFrames.Chat:RefreshConfigFrame() end
+ ))
+
+ local guildTip = hcControls:CreateFontString(nil, "OVERLAY")
+ guildTip:SetFont(fontPath, 10, "OUTLINE")
+ guildTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -164)
+ guildTip:SetWidth(540)
+ guildTip:SetJustifyH("LEFT")
+ guildTip:SetText("开启 AI 翻译时,将死亡黄字系统消息翻译后自动发送到公会频道。需 AI 翻译已启用。")
+ guildTip:SetTextColor(0.8, 0.7, 0.7)
+
+ AddControl(CreateCfgCheck(hcControls, "翻译等级里程碑并转发到公会频道", 16, -204,
+ function() return EnsureDB().hcLevelToGuild ~= false end,
+ function(checked) EnsureDB().hcLevelToGuild = (checked == true) end,
+ function() SFrames.Chat:RefreshConfigFrame() end
+ ))
+
+ local levelGuildTip = hcControls:CreateFontString(nil, "OVERLAY")
+ levelGuildTip:SetFont(fontPath, 10, "OUTLINE")
+ levelGuildTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -228)
+ levelGuildTip:SetWidth(540)
+ levelGuildTip:SetJustifyH("LEFT")
+ levelGuildTip:SetText("开启 AI 翻译时,将[达到X级]里程碑系统消息翻译后转发到公会频道。与死亡通报独立控制。")
+ levelGuildTip:SetTextColor(0.8, 0.7, 0.7)
+
+ AddControl(CreateCfgCheck(hcControls, "仅通报工会成员的死亡/升级消息", 16, -270,
+ function() return EnsureDB().hcGuildMemberOnly == true end,
+ function(checked) EnsureDB().hcGuildMemberOnly = (checked == true) end,
+ function() SFrames.Chat:RefreshConfigFrame() end
+ ))
+
+ local guildMemberTip = hcControls:CreateFontString(nil, "OVERLAY")
+ guildMemberTip:SetFont(fontPath, 10, "OUTLINE")
+ guildMemberTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -296)
+ guildMemberTip:SetWidth(540)
+ guildMemberTip:SetJustifyH("LEFT")
+ guildMemberTip:SetText("勾选后,仅当死亡/升级角色为本工会成员时才转发到公会频道。默认关闭(通报所有人)。")
+ guildMemberTip:SetTextColor(0.8, 0.7, 0.7)
end
local close = CreateCfgButton(panel, "保存", 430, -588, 150, 28, function()
@@ -4399,6 +4651,7 @@ function SFrames.Chat:CreateContainer()
})
chatShadow:SetBackdropColor(0, 0, 0, 0.55)
chatShadow:SetBackdropBorderColor(0, 0, 0, 0.4)
+ f.chatShadow = chatShadow
local topGlow = f:CreateTexture(nil, "BACKGROUND")
topGlow:SetTexture("Interface\\Buttons\\WHITE8X8")
@@ -4674,6 +4927,9 @@ function SFrames.Chat:CreateContainer()
scrollTrack:SetPoint("BOTTOM", scrollDownBtn, "TOP", 0, 2)
scrollTrack:SetWidth(4)
scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0.9)
+ f.scrollUpBtn = scrollUpBtn
+ f.scrollDownBtn = scrollDownBtn
+ f.scrollTrack = scrollTrack
local resize = CreateFrame("Button", nil, f)
resize:SetWidth(16)
@@ -4709,6 +4965,67 @@ function SFrames.Chat:CreateContainer()
f.resizeHandle = resize
self.frame = f
+ -- ── Hover-transparent: fade background/chrome when mouse is not over chat ──
+ f.sfHoverAlpha = 0
+ f.sfHoverTarget = 0
+ local FADE_SPEED = 4.0 -- alpha per second
+ -- Apply initial transparent state immediately after first config apply
+ f.sfHoverInitPending = true
+
+ local function IsMouseOverChat()
+ if MouseIsOver(f) then return true end
+ if SFrames.Chat.editBackdrop and SFrames.Chat.editBackdrop:IsShown() and MouseIsOver(SFrames.Chat.editBackdrop) then return true end
+ if SFrames.Chat.configFrame and SFrames.Chat.configFrame:IsShown() then return true end
+ local editBox = ChatFrameEditBox or ChatFrame1EditBox
+ if editBox and editBox:IsShown() then return true end
+ return false
+ end
+
+ local hoverFrame = CreateFrame("Frame", nil, f)
+ hoverFrame:SetScript("OnUpdate", function()
+ if not (SFrames and SFrames.Chat and SFrames.Chat.frame) then return end
+ local cfg = SFrames.Chat:GetConfig()
+ -- On first tick, snap to correct state (no animation)
+ if f.sfHoverInitPending then
+ f.sfHoverInitPending = nil
+ if cfg.hoverTransparent then
+ local over = IsMouseOverChat() and 1 or 0
+ f.sfHoverAlpha = over
+ f.sfHoverTarget = over
+ SFrames.Chat:ApplyHoverAlpha(over)
+ else
+ f.sfHoverAlpha = 1
+ f.sfHoverTarget = 1
+ end
+ return
+ end
+ if not cfg.hoverTransparent then
+ if f.sfHoverAlpha ~= 1 then
+ f.sfHoverAlpha = 1
+ SFrames.Chat:ApplyHoverAlpha(1)
+ end
+ return
+ end
+ local target = IsMouseOverChat() and 1 or 0
+ f.sfHoverTarget = target
+ local cur = f.sfHoverAlpha or 1
+ if math.abs(cur - target) < 0.01 then
+ if cur ~= target then
+ f.sfHoverAlpha = target
+ SFrames.Chat:ApplyHoverAlpha(target)
+ end
+ return
+ end
+ local dt = arg1 or 0.016
+ if target > cur then
+ cur = math.min(cur + FADE_SPEED * dt, target)
+ else
+ cur = math.max(cur - FADE_SPEED * dt, target)
+ end
+ f.sfHoverAlpha = cur
+ SFrames.Chat:ApplyHoverAlpha(cur)
+ end)
+
if not self.hiddenConfigButton then
local hiddenConfigButton = CreateFrame("Button", "SFramesChatHiddenConfigButton", UIParent, "UIPanelButtonTemplate")
hiddenConfigButton:SetWidth(74)
@@ -4749,9 +5066,45 @@ function SFrames.Chat:CreateContainer()
f:SetWidth(Clamp(db.width, 320, 900))
f:SetHeight(Clamp(db.height, 120, 460))
- -- Background alpha: always show at configured bgAlpha
+ -- Background alpha: respect hoverTransparent on init
local bgA = Clamp(db.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
- f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
+ if db.hoverTransparent ~= false then
+ f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0)
+ f:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], 0)
+ if f.chatShadow then
+ f.chatShadow:SetBackdropColor(0, 0, 0, 0)
+ f.chatShadow:SetBackdropBorderColor(0, 0, 0, 0)
+ end
+ if f.innerShade then
+ f.innerShade:SetVertexColor(0, 0, 0, 0)
+ f.innerShade:Hide()
+ end
+ if f.watermark then
+ f.watermark:SetVertexColor(1, 0.78, 0.9, 0)
+ f.watermark:Hide()
+ end
+ if f.title and f.title.SetAlpha then f.title:SetAlpha(0)
+ elseif f.title and f.title.SetTextColor then f.title:SetTextColor(1, 0.82, 0.93, 0) end
+ if f.title then f.title:Hide() end
+ if f.titleBtn then
+ f.titleBtn:EnableMouse(false)
+ f.titleBtn:Hide()
+ end
+ if f.leftCat then
+ f.leftCat:SetVertexColor(1, 0.82, 0.9, 0)
+ f.leftCat:Hide()
+ end
+ if f.tabBar then f.tabBar:SetAlpha(0) f.tabBar:Hide() end
+ if f.configButton then f.configButton:SetAlpha(0) f.configButton:EnableMouse(false) f.configButton:Hide() end
+ if f.whisperButton then f.whisperButton:SetAlpha(0) f.whisperButton:EnableMouse(false) f.whisperButton:Hide() end
+ if f.scrollUpBtn then f.scrollUpBtn:SetAlpha(0) f.scrollUpBtn:EnableMouse(false) f.scrollUpBtn:Hide() end
+ if f.scrollDownBtn then f.scrollDownBtn:SetAlpha(0) f.scrollDownBtn:EnableMouse(false) f.scrollDownBtn:Hide() end
+ if f.scrollTrack then f.scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0) f.scrollTrack:Hide() end
+ if f.resizeHandle then f.resizeHandle:SetAlpha(0) f.resizeHandle:EnableMouse(false) f.resizeHandle:Hide() end
+ if f.hint then f.hint:Hide() end
+ else
+ f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
+ end
end
function SFrames.Chat:HideDefaultChrome()
@@ -5225,8 +5578,15 @@ function SFrames.Chat:ApplyChatFrameBaseStyle(chatFrame, isCombat)
end
if chatFrame.SetHyperlinksEnabled then chatFrame:SetHyperlinksEnabled(1) end
if chatFrame.SetIndentedWordWrap then chatFrame:SetIndentedWordWrap(false) end
- if chatFrame.SetShadowOffset then chatFrame:SetShadowOffset(1, -1) end
- if chatFrame.SetShadowColor then chatFrame:SetShadowColor(0, 0, 0, 0.92) end
+ if chatFrame.SetShadowOffset then chatFrame:SetShadowOffset(0, 0) end
+ if chatFrame.SetShadowColor then chatFrame:SetShadowColor(0, 0, 0, 0) end
+ StripChatFrameArtwork(chatFrame)
+ if not chatFrame.sfArtworkHooked and chatFrame.HookScript then
+ chatFrame.sfArtworkHooked = true
+ chatFrame:HookScript("OnShow", function()
+ StripChatFrameArtwork(chatFrame)
+ end)
+ end
self:EnforceChatWindowLock(chatFrame)
if not chatFrame.sfDragLockHooked and chatFrame.HookScript then
@@ -6324,6 +6684,122 @@ function SFrames.Chat:StyleEditBox()
end
end
+-- Apply hover-transparent alpha to background, shadow, and chrome elements.
+-- alpha=0 means fully transparent (mouse away), alpha=1 means fully visible (mouse over).
+function SFrames.Chat:ApplyHoverAlpha(alpha)
+ if not self.frame then return end
+ local f = self.frame
+ local cfg = self:GetConfig()
+ local bgA = Clamp(cfg.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
+ local chromeVisible = alpha > 0.01
+
+ -- Main backdrop
+ f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA * alpha)
+ local showBorder = (cfg.showBorder ~= false)
+ local borderR, borderG, borderB = self:GetBorderColorRGB()
+ if showBorder then
+ f:SetBackdropBorderColor(borderR, borderG, borderB, 0.95 * alpha)
+ else
+ f:SetBackdropBorderColor(borderR, borderG, borderB, 0)
+ end
+
+ -- Shadow
+ if f.chatShadow then
+ f.chatShadow:SetBackdropColor(0, 0, 0, 0.55 * alpha)
+ f.chatShadow:SetBackdropBorderColor(0, 0, 0, 0.4 * alpha)
+ end
+
+ -- Inner shade
+ if f.innerShade then
+ f.innerShade:SetVertexColor(0, 0, 0, 0.2 * alpha)
+ if chromeVisible then f.innerShade:Show() else f.innerShade:Hide() end
+ end
+
+ -- Watermark
+ if f.watermark then
+ f.watermark:SetVertexColor(1, 0.78, 0.9, 0.08 * alpha)
+ if chromeVisible then f.watermark:Show() else f.watermark:Hide() end
+ end
+
+ -- Title, tab bar, config button, whisper button, scroll buttons, resize handle
+ local chromeAlpha = alpha
+ if f.title and f.title.SetAlpha then
+ f.title:SetAlpha(chromeAlpha)
+ if chromeVisible then f.title:Show() else f.title:Hide() end
+ elseif f.title and f.title.SetTextColor then
+ f.title:SetTextColor(1, 0.82, 0.93, chromeAlpha)
+ if chromeVisible then f.title:Show() else f.title:Hide() end
+ end
+ if f.titleBtn then
+ f.titleBtn:EnableMouse(chromeVisible)
+ if chromeVisible then f.titleBtn:Show() else f.titleBtn:Hide() end
+ end
+ if f.leftCat then
+ f.leftCat:SetVertexColor(1, 0.82, 0.9, 0.8 * chromeAlpha)
+ if chromeVisible then f.leftCat:Show() else f.leftCat:Hide() end
+ end
+ if f.tabBar then
+ f.tabBar:SetAlpha(chromeAlpha)
+ if chromeVisible then f.tabBar:Show() else f.tabBar:Hide() end
+ end
+ if f.configButton then
+ f.configButton:SetAlpha(chromeAlpha)
+ f.configButton:EnableMouse(chromeVisible)
+ if chromeVisible then f.configButton:Show() else f.configButton:Hide() end
+ end
+ if f.whisperButton then
+ f.whisperButton:SetAlpha(chromeAlpha)
+ f.whisperButton:EnableMouse(chromeVisible)
+ if chromeVisible then f.whisperButton:Show() else f.whisperButton:Hide() end
+ end
+ if f.scrollUpBtn then
+ f.scrollUpBtn:SetAlpha(chromeAlpha)
+ f.scrollUpBtn:EnableMouse(chromeVisible)
+ if chromeVisible then f.scrollUpBtn:Show() else f.scrollUpBtn:Hide() end
+ end
+ if f.scrollDownBtn then
+ f.scrollDownBtn:SetAlpha(chromeAlpha)
+ f.scrollDownBtn:EnableMouse(chromeVisible)
+ if chromeVisible then f.scrollDownBtn:Show() else f.scrollDownBtn:Hide() end
+ end
+ if f.scrollTrack then
+ f.scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0.9 * chromeAlpha)
+ if chromeVisible then f.scrollTrack:Show() else f.scrollTrack:Hide() end
+ end
+ if f.resizeHandle then
+ f.resizeHandle:SetAlpha(chromeAlpha)
+ f.resizeHandle:EnableMouse(chromeVisible)
+ if chromeVisible then f.resizeHandle:Show() else f.resizeHandle:Hide() end
+ end
+ if f.hint then
+ if chromeVisible then f.hint:Show() else f.hint:Hide() end
+ end
+
+ -- Edit backdrop (only backdrop colors, not child alpha — preserve editbox text visibility)
+ if self.editBackdrop then
+ local editBox = ChatFrameEditBox or ChatFrame1EditBox
+ local editBackdropVisible = chromeVisible and editBox and editBox:IsShown()
+ if self.editBackdrop.SetBackdropColor then
+ self.editBackdrop:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.96 * chromeAlpha)
+ end
+ if self.editBackdrop.SetBackdropBorderColor then
+ self.editBackdrop:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], 0.98 * chromeAlpha)
+ end
+ if self.editBackdrop.catIcon then
+ self.editBackdrop.catIcon:SetVertexColor(1, 0.84, 0.94, 0.9 * chromeAlpha)
+ end
+ if self.editBackdrop.topLine then
+ self.editBackdrop.topLine:SetVertexColor(1, 0.76, 0.9, 0.85 * chromeAlpha)
+ end
+ self.editBackdrop:EnableMouse(editBackdropVisible)
+ if editBackdropVisible then
+ self.editBackdrop:Show()
+ else
+ self.editBackdrop:Hide()
+ end
+ end
+end
+
function SFrames.Chat:ApplyFrameBorderStyle()
if not self.frame then return end
@@ -6413,7 +6889,12 @@ function SFrames.Chat:ApplyConfig()
end
local bgA = Clamp(cfg.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
- self.frame:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
+ local hAlpha = (self.frame.sfHoverAlpha ~= nil) and self.frame.sfHoverAlpha or 1
+ if cfg.hoverTransparent then
+ self.frame:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA * hAlpha)
+ else
+ self.frame:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
+ end
self:AttachChatFrame()
self:HideDefaultChrome()
@@ -6427,6 +6908,11 @@ function SFrames.Chat:ApplyConfig()
self:StartStabilizer()
self:SetUnlocked(SFrames and SFrames.isUnlocked)
self:RefreshConfigFrame()
+
+ -- Re-apply hover alpha so ApplyConfig doesn't leave chrome visible
+ if cfg.hoverTransparent and self.frame.sfHoverAlpha ~= nil then
+ self:ApplyHoverAlpha(self.frame.sfHoverAlpha)
+ end
end
function SFrames.Chat:Initialize()
@@ -6524,6 +7010,61 @@ function SFrames.Chat:Initialize()
end)
end
+ -- HC死亡系统消息:AI开启时翻译并转发到公会
+ if not SFrames.Chat._hcDeathGuildHooked then
+ SFrames.Chat._hcDeathGuildHooked = true
+ local hcDeathEvFrame = CreateFrame("Frame", "SFramesChatHCDeathGuildEvents", UIParent)
+ hcDeathEvFrame:RegisterEvent("CHAT_MSG_SYSTEM")
+ hcDeathEvFrame:SetScript("OnEvent", function()
+ if not (SFrames and SFrames.Chat) then return end
+ local db = EnsureDB()
+ -- AI翻译必须开启
+ if db.translateEnabled == false then return end
+ -- 必须在公会中才能发送
+ if not (IsInGuild and IsInGuild()) then return end
+ local messageText = arg1
+ if type(messageText) ~= "string" or messageText == "" then return end
+
+ local isDeath = ParseHardcoreDeathMessage(messageText) ~= nil
+ local isLevel = (not isDeath) and (ParseHardcoreLevelMessage(messageText) ~= nil)
+ if not isDeath and not isLevel then return end
+
+ -- 仅通报工会成员:提取角色名并检查缓存
+ if db.hcGuildMemberOnly then
+ local charName = ParseHCCharacterName(messageText)
+ if not charName or not IsHCGuildMember(charName) then return end
+ end
+
+ -- 死亡通报:检查 hcDeathToGuild 及等级过滤
+ if isDeath then
+ if db.hcDeathToGuild == false then return end
+ if db.hcDeathDisable then return end
+ local deathLvl = ParseHardcoreDeathMessage(messageText)
+ if db.hcDeathLevelMin and deathLvl and deathLvl > 0 and deathLvl < db.hcDeathLevelMin then return end
+ end
+
+ -- 等级里程碑:检查 hcLevelToGuild
+ if isLevel then
+ if db.hcLevelToGuild == false then return end
+ end
+
+ local cleanText = CleanTextForTranslation(messageText)
+ if cleanText == "" then return end
+ local function doSend(finalText)
+ pcall(function() SendChatMessage("[HC] " .. finalText, "GUILD") end)
+ end
+ if isDeath then
+ TranslateHCDeathParts(cleanText, doSend)
+ else
+ TranslateHCLevelParts(cleanText, doSend)
+ end
+ end)
+ end
+
+ SFrames:RegisterEvent("GUILD_ROSTER_UPDATE", function()
+ RefreshHCGuildCache()
+ end)
+
SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function()
if SFrames and SFrames.Chat then
SFrames.Chat:ApplyConfig()
@@ -6532,7 +7073,7 @@ function SFrames.Chat:Initialize()
end
LoadPersistentClassCache()
if IsInGuild and IsInGuild() and GuildRoster then
- GuildRoster()
+ GuildRoster() -- 触发 GUILD_ROSTER_UPDATE → RefreshHCGuildCache
end
SFrames:RefreshClassColorCache()
@@ -6714,6 +7255,17 @@ function SFrames.Chat:Initialize()
do
local db = EnsureDB()
+
+ -- HC系统消息(死亡/里程碑黄字)特殊处理:
+ -- AI翻译未开启时直接透传,不加 [+] 注释;
+ -- AI开启时正常记录,翻译转发由独立的 CHAT_MSG_SYSTEM 事件处理器完成。
+ if ParseHardcoreDeathMessage(text) or ParseHardcoreLevelMessage(text) then
+ if db.translateEnabled == false then
+ origAddMessage(self, text, r, g, b, alpha, holdTime)
+ return
+ end
+ end
+
local chanName = GetChannelNameFromChatLine(text)
if chanName and IsIgnoredChannelByDefault(chanName) then
@@ -7137,4 +7689,3 @@ end
end
SFrames:RegisterEvent("PLAYER_REGEN_DISABLED", ChatCombatReanchor)
SFrames:RegisterEvent("PLAYER_REGEN_ENABLED", ChatCombatReanchor)
-
diff --git a/ClassSkillData.lua b/ClassSkillData.lua
deleted file mode 100644
index 75b8a92..0000000
--- a/ClassSkillData.lua
+++ /dev/null
@@ -1,347 +0,0 @@
-SFrames.ClassSkillData = {
- WARRIOR = {
- [4] = {"冲锋", "撕裂"},
- [6] = {"雷霆一击"},
- [8] = {"英勇打击 2级", "断筋"},
- [10] = {"撕裂 2级", "血性狂暴"},
- [12] = {"压制", "盾击", "战斗怒吼 2级"},
- [14] = {"挫志怒吼", "复仇"},
- [16] = {"英勇打击 3级", "惩戒痛击", "盾牌格挡"},
- [18] = {"雷霆一击 2级", "缴械"},
- [20] = {"撕裂 3级", "反击风暴", "顺劈斩"},
- [22] = {"战斗怒吼 3级", "破甲攻击 2级", "破胆怒吼"},
- [24] = {"英勇打击 4级", "挫志怒吼 2级", "复仇 2级", "斩杀"},
- [26] = {"冲锋 2级", "惩戒痛击 2级", "挑战怒吼"},
- [28] = {"雷霆一击 3级", "压制 2级", "盾墙"},
- [30] = {"撕裂 4级", "顺劈斩 2级", "猛击", "狂暴姿态"},
- [32] = {"英勇打击 5级", "断筋 2级", "斩杀 2级", "战斗怒吼 4级", "盾击 2级", "狂暴之怒"},
- [34] = {"挫志怒吼 3级", "复仇 3级", "破甲攻击 3级"},
- [36] = {"惩戒痛击 3级", "旋风斩"},
- [38] = {"雷霆一击 4级", "猛击 2级"},
- [40] = {"英勇打击 6级", "撕裂 5级", "顺劈斩 3级", "斩杀 3级"},
- [42] = {"战斗怒吼 5级", "拦截 2级"},
- [44] = {"压制 3级", "挫志怒吼 4级", "复仇 4级"},
- [46] = {"冲锋 3级", "惩戒痛击 4级", "猛击 3级", "破甲攻击 4级"},
- [48] = {"英勇打击 7级", "雷霆一击 5级", "斩杀 4级"},
- [50] = {"撕裂 6级", "鲁莽", "顺劈斩 4级"},
- [52] = {"战斗怒吼 6级", "拦截 3级", "盾击 3级"},
- [54] = {"断筋 3级", "挫志怒吼 5级", "猛击 4级", "复仇 5级"},
- [56] = {"英勇打击 8级", "惩戒痛击 5级", "斩杀 5级"},
- [58] = {"雷霆一击 6级", "破甲攻击 5级"},
- [60] = {"撕裂 7级", "压制 4级", "顺劈斩 5级"},
- },
- PALADIN = {
- [4] = {"力量祝福", "审判"},
- [6] = {"圣光术 2级", "圣佑术", "十字军圣印"},
- [8] = {"纯净术", "制裁之锤"},
- [10] = {"圣疗术", "正义圣印 2级", "虔诚光环 2级", "保护祝福"},
- [12] = {"力量祝福 2级", "十字军圣印 2级"},
- [14] = {"圣光术 3级"},
- [16] = {"正义之怒", "惩罚光环"},
- [18] = {"正义圣印 3级", "圣佑术 2级"},
- [20] = {"驱邪术", "圣光闪现", "虔诚光环 3级"},
- [22] = {"圣光术 4级", "专注光环", "公正圣印", "力量祝福 3级", "十字军圣印 3级"},
- [24] = {"超度亡灵", "救赎 2级", "智慧祝福 2级", "制裁之锤 2级", "保护祝福 2级"},
- [26] = {"圣光闪现 2级", "正义圣印 4级", "拯救祝福", "惩罚光环 2级"},
- [28] = {"驱邪术 2级"},
- [30] = {"圣疗术 2级", "圣光术 5级", "光明圣印", "虔诚光环 4级", "神圣干涉"},
- [32] = {"冰霜抗性光环", "力量祝福 4级", "十字军圣印 4级"},
- [34] = {"智慧祝福 3级", "圣光闪现 3级", "正义圣印 5级", "圣盾术"},
- [36] = {"驱邪术 3级", "救赎 3级", "火焰抗性光环", "惩罚光环 3级"},
- [38] = {"圣光术 6级", "超度亡灵 2级", "智慧圣印", "保护祝福 3级"},
- [40] = {"光明祝福", "光明圣印 2级", "虔诚光环 5级", "制裁之锤 3级"},
- [42] = {"圣光闪现 4级", "正义圣印 6级", "力量祝福 5级", "十字军圣印 5级"},
- [44] = {"驱邪术 4级", "智慧祝福 4级", "冰霜抗性光环 2级"},
- [46] = {"圣光术 7级", "惩罚光环 4级"},
- [48] = {"救赎 4级", "智慧圣印 2级", "火焰抗性光环 2级"},
- [50] = {"圣疗术 3级", "圣光闪现 5级", "光明祝福 2级", "光明圣印 3级", "正义圣印 7级", "虔诚光环 6级", "圣盾术 2级"},
- [52] = {"驱邪术 5级", "超度亡灵 3级", "力量祝福 6级", "十字军圣印 6级", "强效力量祝福"},
- [54] = {"圣光术 8级", "智慧祝福 5级", "强效智慧祝福", "制裁之锤 4级"},
- [56] = {"冰霜抗性光环 3级", "惩罚光环 5级"},
- [58] = {"圣光闪现 6级", "智慧圣印 3级", "正义圣印 8级"},
- [60] = {"驱邪术 6级", "救赎 5级", "光明祝福 3级", "光明圣印 4级", "强效光明祝福", "虔诚光环 7级", "火焰抗性光环 3级", "强效力量祝福 2级"},
- },
- HUNTER = {
- [4] = {"灵猴守护", "毒蛇钉刺"},
- [6] = {"猎人印记", "奥术射击"},
- [8] = {"震荡射击", "猛禽一击 2级"},
- [10] = {"雄鹰守护", "毒蛇钉刺 2级", "持久耐力", "自然护甲", "追踪人型生物"},
- [12] = {"治疗宠物", "奥术射击 2级", "扰乱射击", "摔绊"},
- [14] = {"野兽之眼", "恐吓野兽", "鹰眼术"},
- [16] = {"猛禽一击 3级", "献祭陷阱", "猫鼬撕咬"},
- [18] = {"雄鹰守护 2级", "毒蛇钉刺 3级", "追踪亡灵", "多重射击"},
- [20] = {"治疗宠物 2级", "猎豹守护", "奥术射击 3级", "逃脱", "冰冻陷阱", "猛禽一击 4级"},
- [22] = {"猎人印记 2级", "毒蝎钉刺"},
- [24] = {"野兽知识", "追踪隐藏生物"},
- [26] = {"毒蛇钉刺 4级", "急速射击", "追踪元素生物", "献祭陷阱 2级"},
- [28] = {"治疗宠物 3级", "雄鹰守护 3级", "奥术射击 4级", "冰霜陷阱"},
- [30] = {"恐吓野兽 2级", "野兽守护", "多重射击 2级", "猫鼬撕咬 2级", "假死"},
- [32] = {"照明弹", "爆炸陷阱", "追踪恶魔", "猛禽一击 5级"},
- [34] = {"毒蛇钉刺 5级", "逃脱 2级"},
- [36] = {"治疗宠物 4级", "蝰蛇钉刺", "献祭陷阱 3级"},
- [38] = {"雄鹰守护 4级"},
- [40] = {"豹群守护", "猎人印记 3级", "乱射", "扰乱射击 4级", "冰冻陷阱 2级", "猛禽一击 6级", "追踪巨人"},
- [42] = {"毒蛇钉刺 6级", "多重射击 3级"},
- [44] = {"治疗宠物 5级", "奥术射击 6级", "爆炸陷阱 2级", "献祭陷阱 4级", "猫鼬撕咬 3级"},
- [46] = {"恐吓野兽 3级", "蝰蛇钉刺 2级"},
- [48] = {"雄鹰守护 5级", "猛禽一击 7级", "逃脱 3级"},
- [50] = {"毒蛇钉刺 7级", "乱射 2级", "追踪龙类"},
- [52] = {"治疗宠物 6级", "毒蝎钉刺 4级"},
- [54] = {"多重射击 4级", "爆炸陷阱 3级", "猫鼬撕咬 4级", "猛禽一击 8级"},
- [56] = {"蝰蛇钉刺 3级", "献祭陷阱 5级"},
- [58] = {"猎人印记 4级", "乱射 3级", "毒蛇钉刺 8级", "雄鹰守护 6级"},
- [60] = {"治疗宠物 7级", "奥术射击 8级", "扰乱射击 6级", "冰冻陷阱 3级", "摔绊 3级"},
- },
- ROGUE = {
- [2] = {"潜行"},
- [4] = {"背刺", "搜索"},
- [6] = {"邪恶攻击 2级", "凿击"},
- [8] = {"刺骨 2级", "闪避"},
- [10] = {"切割", "疾跑", "闷棍"},
- [12] = {"背刺 2级", "脚踢"},
- [14] = {"绞喉", "破甲", "邪恶攻击 3级"},
- [16] = {"刺骨 3级", "佯攻"},
- [18] = {"凿击 2级", "伏击"},
- [20] = {"割裂", "背刺 3级", "潜行 2级", "致残毒药"},
- [22] = {"绞喉 2级", "邪恶攻击 4级", "扰乱", "消失"},
- [24] = {"刺骨 4级", "麻痹毒药", "侦测陷阱"},
- [26] = {"偷袭", "破甲 2级", "伏击 2级", "脚踢 2级"},
- [28] = {"割裂 2级", "背刺 4级", "佯攻 2级", "闷棍 2级"},
- [30] = {"绞喉 3级", "邪恶攻击 5级", "肾击", "致命毒药"},
- [32] = {"凿击 3级", "致伤毒药"},
- [34] = {"疾跑 2级"},
- [36] = {"割裂 3级", "破甲 3级"},
- [38] = {"绞喉 4级", "致命毒药 2级", "麻痹毒药 2级"},
- [40] = {"邪恶攻击 6级", "佯攻 3级", "潜行 3级", "安全降落", "致伤毒药 2级", "消失 2级"},
- [42] = {"切割 2级"},
- [44] = {"割裂 4级", "背刺 6级"},
- [46] = {"绞喉 5级", "破甲 4级", "致命毒药 3级"},
- [48] = {"刺骨 7级", "凿击 4级", "闷棍 3级", "致伤毒药 3级"},
- [50] = {"肾击 2级", "邪恶攻击 7级", "伏击 5级", "致残毒药 2级"},
- [52] = {"割裂 5级", "背刺 7级", "麻痹毒药 3级"},
- [54] = {"绞喉 6级", "邪恶攻击 8级", "致命毒药 4级"},
- [56] = {"刺骨 8级", "破甲 5级", "致伤毒药 4级"},
- [58] = {"脚踢 4级", "疾跑 3级"},
- [60] = {"割裂 6级", "凿击 5级", "佯攻 4级", "背刺 8级", "潜行 4级"},
- },
- PRIEST = {
- [4] = {"暗言术:痛", "次级治疗术 2级"},
- [6] = {"真言术:盾", "惩击 2级"},
- [8] = {"恢复", "渐隐术"},
- [10] = {"暗言术:痛 2级", "心灵震爆", "复活术"},
- [12] = {"真言术:盾 2级", "心灵之火", "真言术:韧 2级", "祛病术"},
- [14] = {"恢复 2级", "心灵尖啸"},
- [16] = {"治疗术", "心灵震爆 2级"},
- [18] = {"真言术:盾 3级", "驱散魔法", "暗言术:痛 3级"},
- [20] = {"心灵之火 2级", "束缚亡灵", "快速治疗", "安抚心灵", "渐隐术 2级", "神圣之火"},
- [22] = {"惩击 4级", "心灵视界", "复活术 2级", "心灵震爆 3级"},
- [24] = {"真言术:盾 4级", "真言术:韧 3级", "法力燃烧", "神圣之火 2级"},
- [26] = {"恢复 4级", "暗言术:痛 4级"},
- [28] = {"治疗术 3级", "心灵震爆 4级", "心灵尖啸 2级"},
- [30] = {"真言术:盾 5级", "心灵之火 3级", "治疗祷言", "束缚亡灵 2级", "精神控制", "防护暗影", "渐隐术 3级"},
- [32] = {"法力燃烧 2级", "恢复 5级", "快速治疗 3级"},
- [34] = {"漂浮术", "暗言术:痛 5级", "心灵震爆 5级", "复活术 3级", "治疗术 4级"},
- [36] = {"真言术:盾 6级", "驱散魔法 2级", "真言术:韧 4级", "心灵之火 4级", "恢复 6级", "惩击 6级"},
- [38] = {"安抚心灵 2级"},
- [40] = {"法力燃烧 3级", "治疗祷言 2级", "防护暗影 2级", "心灵震爆 6级", "渐隐术 4级"},
- [42] = {"真言术:盾 7级", "神圣之火 5级", "心灵尖啸 3级"},
- [44] = {"恢复 7级", "精神控制 2级"},
- [46] = {"惩击 7级", "强效治疗术 2级", "心灵震爆 7级", "复活术 4级"},
- [48] = {"真言术:盾 8级", "真言术:韧 5级", "法力燃烧 4级", "神圣之火 6级", "恢复 8级", "暗言术:痛 7级"},
- [50] = {"心灵之火 5级", "治疗祷言 3级"},
- [52] = {"强效治疗术 3级", "心灵震爆 8级", "安抚心灵 3级"},
- [54] = {"真言术:盾 9级", "神圣之火 7级", "惩击 8级"},
- [56] = {"法力燃烧 5级", "恢复 9级", "防护暗影 3级", "心灵尖啸 4级", "暗言术:痛 8级"},
- [58] = {"复活术 5级", "强效治疗术 4级", "心灵震爆 9级"},
- [60] = {"真言术:盾 10级", "心灵之火 6级", "真言术:韧 6级", "束缚亡灵 3级", "治疗祷言 4级", "渐隐术 6级"},
- },
- SHAMAN = {
- [4] = {"地震术"},
- [6] = {"治疗波 2级", "地缚图腾"},
- [8] = {"闪电箭 2级", "石爪图腾", "地震术 2级", "闪电之盾"},
- [10] = {"烈焰震击", "火舌武器", "大地之力图腾"},
- [12] = {"净化术", "火焰新星图腾", "先祖之魂", "治疗波 3级"},
- [14] = {"闪电箭 3级", "地震术 3级"},
- [16] = {"闪电之盾 2级", "消毒术"},
- [18] = {"烈焰震击 2级", "火舌武器 2级", "石爪图腾 2级", "治疗波 4级", "战栗图腾"},
- [20] = {"闪电箭 4级", "冰霜震击", "幽魂之狼", "次级治疗波"},
- [22] = {"火焰新星图腾 2级", "水下呼吸", "祛病术"},
- [24] = {"净化术 2级", "地震术 4级", "大地之力图腾 2级", "闪电之盾 3级", "先祖之魂 2级"},
- [26] = {"闪电箭 5级", "熔岩图腾", "火舌武器 3级", "视界术", "法力之泉图腾"},
- [28] = {"石爪图腾 3级", "烈焰震击 3级", "火舌图腾", "水上行走", "次级治疗波 2级"},
- [30] = {"星界传送", "根基图腾", "风怒武器", "治疗之泉图腾"},
- [32] = {"闪电箭 6级", "火焰新星图腾 3级", "闪电之盾 4级", "治疗波 6级", "闪电链", "风怒图腾"},
- [34] = {"冰霜震击 2级", "岗哨图腾"},
- [36] = {"地震术 5级", "熔岩图腾 2级", "火舌武器 4级", "法力之泉图腾 2级", "次级治疗波 3级", "风墙图腾"},
- [38] = {"石爪图腾 4级", "大地之力图腾 3级", "火舌图腾 2级"},
- [40] = {"闪电箭 8级", "闪电链 2级", "烈焰震击 4级", "治疗波 7级", "治疗链", "治疗之泉图腾 3级", "风怒武器 2级"},
- [42] = {"火焰新星图腾 4级"},
- [44] = {"闪电之盾 6级", "冰霜震击 3级", "熔岩图腾 3级", "风墙图腾 2级"},
- [46] = {"火舌武器 5级", "治疗链 2级"},
- [48] = {"地震术 6级", "石爪图腾 5级", "火舌图腾 3级", "治疗波 8级"},
- [50] = {"闪电箭 9级", "治疗之泉图腾 4级", "风怒武器 3级"},
- [52] = {"烈焰震击 5级", "大地之力图腾 4级", "次级治疗波 5级"},
- [54] = {"闪电箭 10级"},
- [56] = {"闪电链 4级", "熔岩图腾 4级", "火舌图腾 4级", "风墙图腾 3级", "治疗波 9级", "法力之泉图腾 4级"},
- [58] = {"冰霜震击 4级"},
- [60] = {"风怒武器 4级", "次级治疗波 6级", "治疗之泉图腾 5级"},
- },
- MAGE = {
- [4] = {"造水术", "寒冰箭"},
- [6] = {"造食术", "火球术 2级", "火焰冲击"},
- [8] = {"变形术", "奥术飞弹"},
- [10] = {"霜甲术 2级", "冰霜新星"},
- [12] = {"缓落术", "造食术 2级", "火球术 3级"},
- [14] = {"魔爆术", "奥术智慧 2级", "火焰冲击 2级"},
- [16] = {"侦测魔法", "烈焰风暴"},
- [18] = {"解除次级诅咒", "魔法增效", "火球术 4级"},
- [20] = {"变形术 2级", "法力护盾", "闪现术", "霜甲术 3级", "暴风雪", "唤醒"},
- [22] = {"造食术 3级", "魔爆术 2级", "火焰冲击 3级", "灼烧"},
- [24] = {"火球术 5级", "烈焰风暴 2级", "法术反制"},
- [26] = {"寒冰箭 5级", "冰锥术"},
- [28] = {"奥术智慧 3级", "法力护盾 2级", "暴风雪 2级", "灼烧 2级", "冰霜新星 2级"},
- [30] = {"魔爆术 3级", "火球术 6级", "冰甲术"},
- [32] = {"造食术 4级", "烈焰风暴 3级", "寒冰箭 6级"},
- [34] = {"魔甲术", "冰锥术 2级", "灼烧 3级"},
- [36] = {"法力护盾 3级", "火球术 7级", "暴风雪 3级", "冰霜新星 3级"},
- [38] = {"魔爆术 4级", "寒冰箭 7级", "火焰冲击 5级"},
- [40] = {"造食术 5级", "奥术飞弹 5级", "火球术 8级", "冰甲术 2级", "灼烧 4级"},
- [42] = {"奥术智慧 4级"},
- [44] = {"法力护盾 4级", "暴风雪 4级", "寒冰箭 8级"},
- [46] = {"魔爆术 5级", "灼烧 5级"},
- [48] = {"火球术 9级", "奥术飞弹 6级", "烈焰风暴 5级"},
- [50] = {"造水术 6级", "寒冰箭 9级", "冰锥术 4级", "冰甲术 3级"},
- [52] = {"法力护盾 5级", "火球术 10级", "火焰冲击 7级", "冰霜新星 4级"},
- [54] = {"魔法增效 4级", "奥术飞弹 7级", "烈焰风暴 6级"},
- [56] = {"奥术智慧 5级", "寒冰箭 10级", "冰锥术 5级"},
- [58] = {"魔甲术 3级", "灼烧 7级"},
- [60] = {"变形术 4级", "法力护盾 6级", "火球术 11级", "暴风雪 6级", "冰甲术 4级"},
- },
- WARLOCK = {
- [2] = {"痛苦诅咒", "恐惧术"},
- [4] = {"腐蚀术", "虚弱诅咒"},
- [6] = {"暗影箭 3级"},
- [8] = {"痛苦诅咒 2级"},
- [10] = {"吸取灵魂", "献祭 2级", "恶魔皮肤 2级", "制造初级治疗石"},
- [12] = {"生命分流", "生命通道", "魔息术"},
- [14] = {"腐蚀术 2级", "吸取生命", "鲁莽诅咒"},
- [16] = {"生命分流 2级"},
- [18] = {"痛苦诅咒 3级", "灼热之痛"},
- [20] = {"献祭 3级", "生命通道 2级", "暗影箭 4级", "魔甲术", "火焰之雨"},
- [22] = {"吸取生命 2级", "虚弱诅咒 3级", "基尔罗格之眼"},
- [24] = {"腐蚀术 3级", "吸取灵魂 2级", "吸取法力", "感知恶魔"},
- [26] = {"生命分流 3级", "语言诅咒"},
- [28] = {"鲁莽诅咒 2级", "痛苦诅咒 4级", "生命通道 3级", "放逐术"},
- [30] = {"吸取生命 3级", "献祭 4级", "奴役恶魔", "地狱烈焰", "魔甲术 2级"},
- [32] = {"虚弱诅咒 4级", "恐惧术 2级", "元素诅咒", "防护暗影结界"},
- [34] = {"生命分流 4级", "吸取法力 2级", "火焰之雨 2级", "灼热之痛 3级"},
- [36] = {"生命通道 4级"},
- [38] = {"吸取灵魂 3级", "痛苦诅咒 5级"},
- [40] = {"恐惧嚎叫", "献祭 5级", "奴役恶魔 2级"},
- [42] = {"虚弱诅咒 5级", "鲁莽诅咒 3级", "死亡缠绕", "防护暗影结界 2级", "地狱烈焰 2级", "灼热之痛 4级"},
- [44] = {"吸取生命 5级", "生命通道 5级", "暗影箭 7级"},
- [46] = {"生命分流 5级", "火焰之雨 3级"},
- [48] = {"痛苦诅咒 6级", "放逐术 2级", "灵魂之火"},
- [50] = {"虚弱诅咒 6级", "死亡缠绕 2级", "恐惧嚎叫 2级", "魔甲术 4级", "吸取灵魂 4级", "吸取法力 4级", "暗影箭 8级", "灼热之痛 5级"},
- [52] = {"防护暗影结界 3级", "生命通道 6级"},
- [54] = {"腐蚀术 6级", "吸取生命 6级", "地狱烈焰 3级", "灵魂之火 2级"},
- [56] = {"鲁莽诅咒 4级", "死亡缠绕 3级"},
- [58] = {"痛苦诅咒 7级", "奴役恶魔 3级", "火焰之雨 4级", "灼热之痛 6级"},
- [60] = {"厄运诅咒", "元素诅咒 3级", "魔甲术 5级", "暗影箭 9级"},
- },
- DRUID = {
- [4] = {"月火术", "回春术"},
- [6] = {"荆棘术", "愤怒 2级"},
- [8] = {"纠缠根须", "治疗之触 2级"},
- [10] = {"月火术 2级", "回春术 2级", "挫志咆哮", "野性印记 2级"},
- [12] = {"愈合", "狂怒"},
- [14] = {"荆棘术 2级", "愤怒 3级", "重击"},
- [16] = {"月火术 3级", "回春术 3级", "挥击"},
- [18] = {"精灵之火", "休眠", "愈合 2级"},
- [20] = {"纠缠根须 2级", "星火术", "猎豹形态", "撕扯", "爪击", "治疗之触 4级", "潜行", "野性印记 3级", "复生"},
- [22] = {"愤怒 4级", "撕碎", "安抚动物"},
- [24] = {"荆棘术 3级", "挥击 2级", "扫击", "猛虎之怒", "解除诅咒"},
- [26] = {"星火术 2级", "月火术 5级", "爪击 2级", "治疗之触 5级", "驱毒术"},
- [28] = {"撕扯 2级", "挑战咆哮", "畏缩"},
- [30] = {"精灵之火 2级", "星火术 3级", "愤怒 5级", "旅行形态", "撕碎 2级", "重击 2级", "野性印记 4级", "宁静", "复生 2级"},
- [32] = {"挫志咆哮 3级", "挥击 3级", "毁灭", "治疗之触 6级", "凶猛撕咬"},
- [34] = {"荆棘术 4级", "月火术 6级", "回春术 6级", "扫击 2级", "爪击 3级"},
- [36] = {"愤怒 6级", "突袭", "狂暴回复"},
- [38] = {"纠缠根须 4级", "休眠 2级", "安抚动物 2级", "撕碎 3级"},
- [40] = {"星火术 4级", "飓风", "挥击 4级", "潜行 2级", "畏缩 2级", "巨熊形态", "凶猛撕咬 2级", "回春术 7级", "宁静 2级", "复生 3级", "激活"},
- [42] = {"挫志咆哮 4级", "毁灭 2级"},
- [44] = {"荆棘术 5级", "树皮术", "撕扯 4级", "扫击 3级", "治疗之触 8级"},
- [46] = {"愤怒 7级", "重击 3级", "突袭 2级"},
- [48] = {"纠缠根须 5级", "月火术 8级", "撕碎 4级"},
- [50] = {"星火术 5级", "宁静 3级", "复生 4级"},
- [52] = {"挫志咆哮 5级", "撕扯 5级", "畏缩 3级", "凶猛撕咬 4级", "回春术 9级"},
- [54] = {"荆棘术 6级", "愤怒 8级", "月火术 9级", "挥击 5级", "扫击 4级", "爪击 4级"},
- [56] = {"治疗之触 10级"},
- [58] = {"纠缠根须 6级", "星火术 6级", "月火术 10级", "爪击 5级", "毁灭 4级", "回春术 10级"},
- [60] = {"飓风 3级", "潜行 3级", "猛虎之怒 4级", "撕扯 6级", "宁静 4级", "复生 5级", "野性印记 7级", "愈合 9级"},
- },
-}
-
-SFrames.TalentTrainerSkills = {
- WARRIOR = {
- [48] = {{"致死打击 2级", "致死打击"}, {"嗜血 2级", "嗜血"}, {"盾牌猛击 2级", "盾牌猛击"}},
- [54] = {{"致死打击 3级", "致死打击"}, {"嗜血 3级", "嗜血"}, {"盾牌猛击 3级", "盾牌猛击"}},
- [60] = {{"致死打击 4级", "致死打击"}, {"嗜血 4级", "嗜血"}, {"盾牌猛击 4级", "盾牌猛击"}},
- },
- PALADIN = {
- [48] = {{"神圣震击 2级", "神圣震击"}},
- [56] = {{"神圣震击 3级", "神圣震击"}},
- },
- HUNTER = {
- [28] = {{"瞄准射击 2级", "瞄准射击"}},
- [36] = {{"瞄准射击 3级", "瞄准射击"}},
- [44] = {{"瞄准射击 4级", "瞄准射击"}},
- [52] = {{"瞄准射击 5级", "瞄准射击"}},
- [60] = {{"瞄准射击 6级", "瞄准射击"}},
- },
- ROGUE = {
- [46] = {{"出血 2级", "出血"}},
- [58] = {{"出血 3级", "出血"}},
- },
- PRIEST = {
- [28] = {{"精神鞭笞 2级", "精神鞭笞"}},
- [36] = {{"精神鞭笞 3级", "精神鞭笞"}},
- [44] = {{"精神鞭笞 4级", "精神鞭笞"}},
- [52] = {{"精神鞭笞 5级", "精神鞭笞"}},
- [60] = {{"精神鞭笞 6级", "精神鞭笞"}},
- },
- MAGE = {
- [24] = {{"炎爆术 2级", "炎爆术"}},
- [30] = {{"炎爆术 3级", "炎爆术"}},
- [36] = {{"炎爆术 4级", "炎爆术"}},
- [42] = {{"炎爆术 5级", "炎爆术"}},
- [48] = {{"炎爆术 6级", "炎爆术"}, {"冲击波 2级", "冲击波"}, {"寒冰屏障 2级", "寒冰屏障"}},
- [54] = {{"炎爆术 7级", "炎爆术"}},
- [56] = {{"冲击波 3级", "冲击波"}, {"寒冰屏障 3级", "寒冰屏障"}},
- [60] = {{"炎爆术 8级", "炎爆术"}, {"冲击波 4级", "冲击波"}, {"寒冰屏障 4级", "寒冰屏障"}},
- },
- WARLOCK = {
- [38] = {{"生命虹吸 2级", "生命虹吸"}},
- [48] = {{"生命虹吸 3级", "生命虹吸"}},
- [50] = {{"黑暗契约 2级", "黑暗契约"}},
- [58] = {{"生命虹吸 4级", "生命虹吸"}},
- [60] = {{"黑暗契约 3级", "黑暗契约"}},
- },
- DRUID = {
- [30] = {{"虫群 2级", "虫群"}},
- [40] = {{"虫群 3级", "虫群"}},
- [50] = {{"虫群 4级", "虫群"}},
- [60] = {{"虫群 5级", "虫群"}},
- },
-}
-
-SFrames.ClassMountQuests = {
- WARLOCK = {
- [40] = "职业坐骑任务:召唤恶马",
- [60] = "史诗坐骑任务:召唤恐惧战马",
- },
- PALADIN = {
- [40] = "职业坐骑任务:召唤战马",
- [60] = "史诗坐骑任务:召唤战驹",
- },
-}
diff --git a/ConfigUI.lua b/ConfigUI.lua
index 0d65e32..52727d4 100644
--- a/ConfigUI.lua
+++ b/ConfigUI.lua
@@ -35,6 +35,14 @@ local function Clamp(value, minValue, maxValue)
return value
end
+local function EnsureNumberDefault(tbl, key, value)
+ if type(tbl[key]) ~= "number" then tbl[key] = value end
+end
+
+local function EnsureStringDefault(tbl, key, value)
+ if type(tbl[key]) ~= "string" or tbl[key] == "" then tbl[key] = value end
+end
+
local SOFT_THEME = SFrames.ActiveTheme
local function EnsureSoftBackdrop(frame)
@@ -331,17 +339,25 @@ local function StyleSlider(slider, low, high, text)
end)
UpdateFill()
+ local font = (SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
if low then
+ low:SetFont(font, 8, "OUTLINE")
low:SetTextColor(SOFT_THEME.dimText[1], SOFT_THEME.dimText[2], SOFT_THEME.dimText[3])
low:ClearAllPoints()
- low:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 0, 0)
+ low:SetWidth(40)
+ low:SetJustifyH("LEFT")
+ low:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 10, 0)
end
if high then
+ high:SetFont(font, 8, "OUTLINE")
high:SetTextColor(SOFT_THEME.dimText[1], SOFT_THEME.dimText[2], SOFT_THEME.dimText[3])
high:ClearAllPoints()
- high:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", 0, 0)
+ high:SetWidth(40)
+ high:SetJustifyH("RIGHT")
+ high:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", -10, 0)
end
if text then
+ text:SetFont(font, 9, "OUTLINE")
text:SetTextColor(SOFT_THEME.text[1], SOFT_THEME.text[2], SOFT_THEME.text[3])
text:ClearAllPoints()
text:SetPoint("BOTTOM", slider, "TOP", 0, 2)
@@ -418,6 +434,25 @@ local function EnsureDB()
if type(SFramesDB.playerBgAlpha) ~= "number" then SFramesDB.playerBgAlpha = 0.9 end
if type(SFramesDB.playerNameFontSize) ~= "number" then SFramesDB.playerNameFontSize = 10 end
if type(SFramesDB.playerValueFontSize) ~= "number" then SFramesDB.playerValueFontSize = 10 end
+ local defaultPlayerPowerWidth = math.max(60, SFramesDB.playerFrameWidth - ((SFramesDB.playerShowPortrait ~= false) and SFramesDB.playerPortraitWidth or 0) - 2)
+ if type(SFramesDB.playerPowerWidth) ~= "number"
+ or math.abs(SFramesDB.playerPowerWidth - SFramesDB.playerFrameWidth) < 0.5
+ or math.abs(SFramesDB.playerPowerWidth - 168) < 0.5 then
+ SFramesDB.playerPowerWidth = defaultPlayerPowerWidth
+ end
+ if type(SFramesDB.playerPowerOffsetX) ~= "number" then SFramesDB.playerPowerOffsetX = 0 end
+ if type(SFramesDB.playerPowerOffsetY) ~= "number" then SFramesDB.playerPowerOffsetY = 0 end
+ if SFramesDB.playerPowerOnTop == nil then SFramesDB.playerPowerOnTop = false end
+ if SFramesDB.playerShowBorder == nil then SFramesDB.playerShowBorder = false end
+ if type(SFramesDB.playerCornerRadius) ~= "number" then SFramesDB.playerCornerRadius = 0 end
+ if type(SFramesDB.playerPortraitBgAlpha) ~= "number" then SFramesDB.playerPortraitBgAlpha = SFramesDB.playerBgAlpha end
+ if SFramesDB.playerHealthTexture == nil then SFramesDB.playerHealthTexture = "" end
+ if SFramesDB.playerPowerTexture == nil then SFramesDB.playerPowerTexture = "" end
+ if SFramesDB.playerNameFontKey == nil then SFramesDB.playerNameFontKey = "" end
+ if SFramesDB.playerHealthFontKey == nil then SFramesDB.playerHealthFontKey = "" end
+ if SFramesDB.playerPowerFontKey == nil then SFramesDB.playerPowerFontKey = "" end
+ if type(SFramesDB.playerHealthFontSize) ~= "number" then SFramesDB.playerHealthFontSize = SFramesDB.playerValueFontSize end
+ if type(SFramesDB.playerPowerFontSize) ~= "number" then SFramesDB.playerPowerFontSize = SFramesDB.playerValueFontSize end
if type(SFramesDB.targetFrameScale) ~= "number" then SFramesDB.targetFrameScale = 1 end
if type(SFramesDB.targetFrameWidth) ~= "number" then SFramesDB.targetFrameWidth = 220 end
@@ -431,9 +466,28 @@ local function EnsureDB()
if type(SFramesDB.targetBgAlpha) ~= "number" then SFramesDB.targetBgAlpha = 0.9 end
if type(SFramesDB.targetNameFontSize) ~= "number" then SFramesDB.targetNameFontSize = 10 end
if type(SFramesDB.targetValueFontSize) ~= "number" then SFramesDB.targetValueFontSize = 10 end
+ local defaultTargetPowerWidth = math.max(60, SFramesDB.targetFrameWidth - ((SFramesDB.targetShowPortrait ~= false) and SFramesDB.targetPortraitWidth or 0) - 2)
+ if type(SFramesDB.targetPowerWidth) ~= "number"
+ or math.abs(SFramesDB.targetPowerWidth - SFramesDB.targetFrameWidth) < 0.5 then
+ SFramesDB.targetPowerWidth = defaultTargetPowerWidth
+ end
+ if type(SFramesDB.targetPowerOffsetX) ~= "number" then SFramesDB.targetPowerOffsetX = 0 end
+ if type(SFramesDB.targetPowerOffsetY) ~= "number" then SFramesDB.targetPowerOffsetY = 0 end
+ if SFramesDB.targetPowerOnTop == nil then SFramesDB.targetPowerOnTop = false end
+ if SFramesDB.targetShowBorder == nil then SFramesDB.targetShowBorder = false end
+ if type(SFramesDB.targetCornerRadius) ~= "number" then SFramesDB.targetCornerRadius = 0 end
+ if type(SFramesDB.targetPortraitBgAlpha) ~= "number" then SFramesDB.targetPortraitBgAlpha = SFramesDB.targetBgAlpha end
+ if SFramesDB.targetHealthTexture == nil then SFramesDB.targetHealthTexture = "" end
+ if SFramesDB.targetPowerTexture == nil then SFramesDB.targetPowerTexture = "" end
+ if SFramesDB.targetNameFontKey == nil then SFramesDB.targetNameFontKey = "" end
+ if SFramesDB.targetHealthFontKey == nil then SFramesDB.targetHealthFontKey = "" end
+ if SFramesDB.targetPowerFontKey == nil then SFramesDB.targetPowerFontKey = "" end
+ if type(SFramesDB.targetHealthFontSize) ~= "number" then SFramesDB.targetHealthFontSize = SFramesDB.targetValueFontSize end
+ if type(SFramesDB.targetPowerFontSize) ~= "number" then SFramesDB.targetPowerFontSize = SFramesDB.targetValueFontSize end
if SFramesDB.targetDistanceEnabled == nil then SFramesDB.targetDistanceEnabled = true end
if type(SFramesDB.targetDistanceScale) ~= "number" then SFramesDB.targetDistanceScale = 1 end
if type(SFramesDB.targetDistanceFontSize) ~= "number" then SFramesDB.targetDistanceFontSize = 14 end
+ if SFramesDB.targetDistanceFontKey == nil then SFramesDB.targetDistanceFontKey = "" end
if type(SFramesDB.fontKey) ~= "string" then SFramesDB.fontKey = "default" end
-- Focus frame defaults
@@ -449,6 +503,28 @@ local function EnsureDB()
if type(SFramesDB.focusNameFontSize) ~= "number" then SFramesDB.focusNameFontSize = 11 end
if type(SFramesDB.focusValueFontSize) ~= "number" then SFramesDB.focusValueFontSize = 10 end
if type(SFramesDB.focusBgAlpha) ~= "number" then SFramesDB.focusBgAlpha = 0.9 end
+ local defaultFocusPowerWidth = math.max(60, SFramesDB.focusFrameWidth - ((SFramesDB.focusShowPortrait ~= false) and SFramesDB.focusPortraitWidth or 0) - 2)
+ if type(SFramesDB.focusPowerWidth) ~= "number"
+ or math.abs(SFramesDB.focusPowerWidth - SFramesDB.focusFrameWidth) < 0.5 then
+ SFramesDB.focusPowerWidth = defaultFocusPowerWidth
+ end
+ if type(SFramesDB.focusPowerOffsetX) ~= "number" then SFramesDB.focusPowerOffsetX = 0 end
+ if type(SFramesDB.focusPowerOffsetY) ~= "number" then SFramesDB.focusPowerOffsetY = 0 end
+ if SFramesDB.focusPowerOnTop == nil then SFramesDB.focusPowerOnTop = false end
+ if SFramesDB.focusShowBorder == nil then SFramesDB.focusShowBorder = false end
+ if type(SFramesDB.focusCornerRadius) ~= "number" then SFramesDB.focusCornerRadius = 0 end
+ if type(SFramesDB.focusPortraitBgAlpha) ~= "number" then SFramesDB.focusPortraitBgAlpha = SFramesDB.focusBgAlpha end
+ if SFramesDB.focusHealthTexture == nil then SFramesDB.focusHealthTexture = "" end
+ if SFramesDB.focusPowerTexture == nil then SFramesDB.focusPowerTexture = "" end
+ if SFramesDB.focusNameFontKey == nil then SFramesDB.focusNameFontKey = "" end
+ if SFramesDB.focusHealthFontKey == nil then SFramesDB.focusHealthFontKey = "" end
+ if SFramesDB.focusPowerFontKey == nil then SFramesDB.focusPowerFontKey = "" end
+ if SFramesDB.focusCastFontKey == nil then SFramesDB.focusCastFontKey = "" end
+ if SFramesDB.focusDistanceFontKey == nil then SFramesDB.focusDistanceFontKey = "" end
+ if type(SFramesDB.focusHealthFontSize) ~= "number" then SFramesDB.focusHealthFontSize = SFramesDB.focusValueFontSize end
+ if type(SFramesDB.focusPowerFontSize) ~= "number" then SFramesDB.focusPowerFontSize = SFramesDB.focusValueFontSize end
+ if type(SFramesDB.focusCastFontSize) ~= "number" then SFramesDB.focusCastFontSize = 10 end
+ if type(SFramesDB.focusDistanceFontSize) ~= "number" then SFramesDB.focusDistanceFontSize = 14 end
if SFramesDB.showPetFrame == nil then SFramesDB.showPetFrame = true end
if type(SFramesDB.petFrameScale) ~= "number" then SFramesDB.petFrameScale = 1 end
@@ -466,10 +542,33 @@ local function EnsureDB()
if type(SFramesDB.partyVerticalGap) ~= "number" then SFramesDB.partyVerticalGap = 30 end
if type(SFramesDB.partyNameFontSize) ~= "number" then SFramesDB.partyNameFontSize = 10 end
if type(SFramesDB.partyValueFontSize) ~= "number" then SFramesDB.partyValueFontSize = 10 end
+ local defaultPartyPowerWidth = math.max(40, SFramesDB.partyFrameWidth - SFramesDB.partyPortraitWidth - 5)
+ if type(SFramesDB.partyPowerWidth) ~= "number"
+ or math.abs(SFramesDB.partyPowerWidth - SFramesDB.partyFrameWidth) < 0.5 then
+ SFramesDB.partyPowerWidth = defaultPartyPowerWidth
+ end
+ if type(SFramesDB.partyPowerOffsetX) ~= "number" then SFramesDB.partyPowerOffsetX = 0 end
+ if type(SFramesDB.partyPowerOffsetY) ~= "number" then SFramesDB.partyPowerOffsetY = 0 end
+ if SFramesDB.partyPowerOnTop == nil then SFramesDB.partyPowerOnTop = false end
+ if SFramesDB.partyShowBorder == nil then SFramesDB.partyShowBorder = false end
+ if type(SFramesDB.partyCornerRadius) ~= "number" then SFramesDB.partyCornerRadius = 0 end
+ if type(SFramesDB.partyPortraitBgAlpha) ~= "number" then SFramesDB.partyPortraitBgAlpha = SFramesDB.partyBgAlpha end
+ if SFramesDB.partyHealthTexture == nil then SFramesDB.partyHealthTexture = "" end
+ if SFramesDB.partyPowerTexture == nil then SFramesDB.partyPowerTexture = "" end
+ if SFramesDB.partyNameFontKey == nil then SFramesDB.partyNameFontKey = "" end
+ if SFramesDB.partyHealthFontKey == nil then SFramesDB.partyHealthFontKey = "" end
+ if SFramesDB.partyPowerFontKey == nil then SFramesDB.partyPowerFontKey = "" end
+ if type(SFramesDB.partyHealthFontSize) ~= "number" then SFramesDB.partyHealthFontSize = SFramesDB.partyValueFontSize end
+ if type(SFramesDB.partyPowerFontSize) ~= "number" then SFramesDB.partyPowerFontSize = SFramesDB.partyValueFontSize end
if SFramesDB.partyShowBuffs == nil then SFramesDB.partyShowBuffs = true end
if SFramesDB.partyShowDebuffs == nil then SFramesDB.partyShowDebuffs = true end
+ if SFramesDB.partyPortrait3D == nil then SFramesDB.partyPortrait3D = true end
if type(SFramesDB.partyBgAlpha) ~= "number" then SFramesDB.partyBgAlpha = 0.9 end
+ if SFramesDB.totHealthTexture == nil then SFramesDB.totHealthTexture = "" end
+ if SFramesDB.petHealthTexture == nil then SFramesDB.petHealthTexture = "" end
+ if SFramesDB.petPowerTexture == nil then SFramesDB.petPowerTexture = "" end
+
if SFramesDB.raidLayout ~= "horizontal" and SFramesDB.raidLayout ~= "vertical" then
SFramesDB.raidLayout = "horizontal"
end
@@ -482,6 +581,23 @@ local function EnsureDB()
if type(SFramesDB.raidGroupGap) ~= "number" then SFramesDB.raidGroupGap = 8 end
if type(SFramesDB.raidNameFontSize) ~= "number" then SFramesDB.raidNameFontSize = 10 end
if type(SFramesDB.raidValueFontSize) ~= "number" then SFramesDB.raidValueFontSize = 9 end
+ local defaultRaidPowerWidth = math.max(20, SFramesDB.raidFrameWidth - 2)
+ if type(SFramesDB.raidPowerWidth) ~= "number"
+ or math.abs(SFramesDB.raidPowerWidth - SFramesDB.raidFrameWidth) < 0.5 then
+ SFramesDB.raidPowerWidth = defaultRaidPowerWidth
+ end
+ if type(SFramesDB.raidPowerOffsetX) ~= "number" then SFramesDB.raidPowerOffsetX = 0 end
+ if type(SFramesDB.raidPowerOffsetY) ~= "number" then SFramesDB.raidPowerOffsetY = 0 end
+ if SFramesDB.raidPowerOnTop == nil then SFramesDB.raidPowerOnTop = false end
+ if SFramesDB.raidShowBorder == nil then SFramesDB.raidShowBorder = false end
+ if type(SFramesDB.raidCornerRadius) ~= "number" then SFramesDB.raidCornerRadius = 0 end
+ if SFramesDB.raidHealthTexture == nil then SFramesDB.raidHealthTexture = "" end
+ if SFramesDB.raidPowerTexture == nil then SFramesDB.raidPowerTexture = "" end
+ if SFramesDB.raidNameFontKey == nil then SFramesDB.raidNameFontKey = "" end
+ if SFramesDB.raidHealthFontKey == nil then SFramesDB.raidHealthFontKey = "" end
+ if SFramesDB.raidPowerFontKey == nil then SFramesDB.raidPowerFontKey = "" end
+ if type(SFramesDB.raidHealthFontSize) ~= "number" then SFramesDB.raidHealthFontSize = SFramesDB.raidValueFontSize end
+ if type(SFramesDB.raidPowerFontSize) ~= "number" then SFramesDB.raidPowerFontSize = SFramesDB.raidValueFontSize end
if SFramesDB.raidShowPower == nil then SFramesDB.raidShowPower = true end
if SFramesDB.enableRaidFrames == nil then SFramesDB.enableRaidFrames = true end
if SFramesDB.raidHealthFormat == nil then SFramesDB.raidHealthFormat = "compact" end
@@ -494,9 +610,6 @@ local function EnsureDB()
if type(SFramesDB.afkDelay) ~= "number" then SFramesDB.afkDelay = 5 end
if SFramesDB.afkOutsideRest == nil then SFramesDB.afkOutsideRest = false end
- if SFramesDB.trainerReminder == nil then SFramesDB.trainerReminder = true end
- if SFramesDB.trainerCache == nil then SFramesDB.trainerCache = {} end
-
if SFramesDB.smoothBars == nil then SFramesDB.smoothBars = true end
if SFramesDB.mobRealHealth == nil then SFramesDB.mobRealHealth = true end
if SFramesDB.showItemLevel == nil then SFramesDB.showItemLevel = true end
@@ -533,7 +646,6 @@ local function EnsureDB()
if SFramesDB.Tweaks.superWoW == nil then SFramesDB.Tweaks.superWoW = true end
if SFramesDB.Tweaks.turtleCompat == nil then SFramesDB.Tweaks.turtleCompat = true end
if SFramesDB.Tweaks.cooldownNumbers == nil then SFramesDB.Tweaks.cooldownNumbers = true end
- if SFramesDB.Tweaks.behindIndicator == nil then SFramesDB.Tweaks.behindIndicator = true end
if SFramesDB.Tweaks.combatNotify == nil then SFramesDB.Tweaks.combatNotify = true end
if SFramesDB.Tweaks.darkUI == nil then SFramesDB.Tweaks.darkUI = false end
if SFramesDB.Tweaks.worldMapWindow == nil then SFramesDB.Tweaks.worldMapWindow = false end
@@ -569,15 +681,31 @@ local function EnsureDB()
local abDef = {
enable = true, buttonSize = 36, buttonGap = 2, smallBarSize = 27,
scale = 1.0, barCount = 3, showHotkey = true, showMacroName = false,
- rangeColoring = true, behindGlow = true, showPetBar = true, showStanceBar = true,
+ rangeColoring = true, showPetBar = true, showStanceBar = true,
showRightBars = true, buttonRounded = false, buttonInnerShadow = false,
alpha = 1.0,
bgAlpha = 0.9,
+ rightBar1PerRow = 1,
+ rightBar2PerRow = 1,
+ bottomBar1PerRow = 12,
+ bottomBar2PerRow = 12,
+ bottomBar3PerRow = 12,
}
for k, v in pairs(abDef) do
if SFramesDB.ActionBars[k] == nil then SFramesDB.ActionBars[k] = v end
end
+ if type(SFramesDB.ExtraBar) ~= "table" then SFramesDB.ExtraBar = {} end
+ local ebDef = {
+ enable = false, buttonCount = 12, perRow = 12, buttonSize = 36,
+ buttonGap = 2, align = "center", startSlot = 73, alpha = 1.0,
+ showHotkey = true, showCount = true,
+ buttonRounded = false, buttonInnerShadow = false,
+ }
+ for k, v in pairs(ebDef) do
+ if SFramesDB.ExtraBar[k] == nil then SFramesDB.ExtraBar[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
@@ -760,6 +888,113 @@ local function CreateButton(parent, text, x, y, width, height, onClick)
StyleButton(btn)
return btn
end
+
+local function CreateOptionGrid(parent, title, x, y, width, options, getter, setter, previewKind, previewSize, onValueChanged)
+ local font = SFrames:GetFont()
+ local controls = {}
+ local cols = previewKind == "font" and 3 or 5
+ local btnW = previewKind == "font" and 150 or 96
+ local btnH = previewKind == "font" and 22 or 48
+ local gapX = previewKind == "font" and 8 or 6
+ local gapY = 6
+
+ CreateLabel(parent, title, x, y, font, 10, 0.85, 0.75, 0.80)
+
+ local buttons = {}
+ for idx, entry in ipairs(options) do
+ local col = math.mod(idx - 1, cols)
+ local row = math.floor((idx - 1) / cols)
+ local bx = x + col * (btnW + gapX)
+ local by = y - 18 - row * (btnH + gapY)
+ local btn = CreateFrame("Button", NextWidgetName("OptionGrid"), parent)
+ btn:SetWidth(btnW)
+ btn:SetHeight(btnH)
+ btn:SetPoint("TOPLEFT", parent, "TOPLEFT", bx, by)
+ btn:SetBackdrop({
+ bgFile = "Interface\\Buttons\\WHITE8X8",
+ edgeFile = "Interface\\Buttons\\WHITE8X8",
+ tile = false, edgeSize = 1,
+ insets = { left = 1, right = 1, top = 1, bottom = 1 },
+ })
+ btn:SetBackdropColor(0.08, 0.08, 0.10, 0.9)
+ btn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
+ btn.optionKey = entry.key
+ btn.optionLabel = entry.label
+
+ if previewKind == "bar" then
+ local preview = CreateFrame("StatusBar", NextWidgetName("OptionGridBar"), btn)
+ preview:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
+ preview:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 14)
+ preview:SetStatusBarTexture(entry.path)
+ preview:SetStatusBarColor(0.2, 0.85, 0.3, 1)
+ preview:SetMinMaxValues(0, 1)
+ preview:SetValue(1)
+
+ local label = btn:CreateFontString(nil, "OVERLAY")
+ label:SetFont(font, 9, "OUTLINE")
+ label:SetPoint("BOTTOM", btn, "BOTTOM", 0, 3)
+ label:SetText(entry.label)
+ label:SetTextColor(0.8, 0.8, 0.8)
+ else
+ local label = btn:CreateFontString(nil, "OVERLAY")
+ local ok = pcall(label.SetFont, label, entry.path, previewSize or 11, "OUTLINE")
+ if not ok then
+ label:SetFont(font, previewSize or 11, "OUTLINE")
+ end
+ label:SetPoint("CENTER", btn, "CENTER", 0, 0)
+ label:SetText(entry.label)
+ label:SetTextColor(0.9, 0.9, 0.9)
+ end
+
+ local selectBorder = btn:CreateTexture(nil, "OVERLAY")
+ selectBorder:SetTexture("Interface\\Buttons\\WHITE8X8")
+ selectBorder:SetPoint("TOPLEFT", btn, "TOPLEFT", -1, 1)
+ selectBorder:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", 1, -1)
+ selectBorder:SetVertexColor(1, 0.85, 0.4, 0.8)
+ selectBorder:Hide()
+ btn.selectBorder = selectBorder
+
+ btn:SetScript("OnClick", function()
+ setter(this.optionKey)
+ for _, b in ipairs(buttons) do
+ local active = b.optionKey == this.optionKey
+ b:SetBackdropBorderColor(active and 1 or 0.3, active and 0.85 or 0.3, active and 0.6 or 0.35, 1)
+ if b.selectBorder then
+ if active then b.selectBorder:Show() else b.selectBorder:Hide() end
+ end
+ end
+ if onValueChanged then onValueChanged(this.optionKey) end
+ end)
+ btn:SetScript("OnEnter", function()
+ if this.optionKey ~= getter() then
+ this:SetBackdropBorderColor(0.7, 0.7, 0.8, 1)
+ end
+ GameTooltip:SetOwner(this, "ANCHOR_TOP")
+ GameTooltip:SetText(this.optionLabel)
+ GameTooltip:Show()
+ end)
+ btn:SetScript("OnLeave", function()
+ local active = this.optionKey == getter()
+ this:SetBackdropBorderColor(active and 1 or 0.3, active and 0.85 or 0.3, active and 0.6 or 0.35, 1)
+ GameTooltip:Hide()
+ end)
+
+ btn.Refresh = function()
+ local active = btn.optionKey == getter()
+ btn:SetBackdropBorderColor(active and 1 or 0.3, active and 0.85 or 0.3, active and 0.6 or 0.35, 1)
+ if btn.selectBorder then
+ if active then btn.selectBorder:Show() else btn.selectBorder:Hide() end
+ end
+ end
+ btn:Refresh()
+
+ table.insert(buttons, btn)
+ table.insert(controls, btn)
+ end
+
+ local rows = math.floor((table.getn(options) + cols - 1) / cols)
+ return controls, (rows * btnH) + ((rows - 1) * gapY) + 18
+end
local function CreateScrollArea(parent, x, y, width, height, childHeight)
local holder = CreateFrame("Frame", NextWidgetName("ScrollHolder"), parent)
holder:SetWidth(width)
@@ -1154,18 +1389,11 @@ function SFrames.ConfigUI:BuildUIPage()
CreateDesc(tweaksSection, "将整个游戏界面调暗为深色主题(默认关闭)", 292, -142, font, 218)
table.insert(controls, CreateCheckBox(tweaksSection,
- "背后指示器", 14, -172,
- function() return SFramesDB.Tweaks.behindIndicator ~= false end,
- function(checked) SFramesDB.Tweaks.behindIndicator = checked end
- ))
- CreateDesc(tweaksSection, "在目标距离旁显示 背后/正面 状态(需安装 UnitXP SP3)", 36, -188, font, 218)
-
- table.insert(controls, CreateCheckBox(tweaksSection,
- "进战斗后台通知", 270, -172,
+ "进战斗后台通知", 14, -172,
function() return SFramesDB.Tweaks.combatNotify ~= false end,
function(checked) SFramesDB.Tweaks.combatNotify = checked end
))
- CreateDesc(tweaksSection, "进入战斗时闪烁任务栏图标(需安装 UnitXP SP3)", 292, -188, font, 218)
+ CreateDesc(tweaksSection, "进入战斗时闪烁任务栏图标(需安装 UnitXP SP3)", 36, -188, font, 218)
table.insert(controls, CreateCheckBox(tweaksSection,
"鼠标指向施法", 14, -218,
@@ -1177,7 +1405,7 @@ function SFrames.ConfigUI:BuildUIPage()
CreateLabel(tweaksSection, "提示:以上所有选项修改后需要 /reload 才能生效。", 14, -264, font, 10, 0.6, 0.6, 0.65)
-- ── 聊天背景透明度 ──────────────────────────────────────
- local chatBgSection = CreateSection(root, "聊天窗口", 8, -1018, 520, 90, font)
+ local chatBgSection = CreateSection(root, "聊天窗口", 8, -1018, 520, 120, font)
table.insert(controls, CreateSlider(chatBgSection, "背景透明度", 14, -38, 220, 0, 1.0, 0.05,
function()
@@ -1195,20 +1423,399 @@ function SFrames.ConfigUI:BuildUIPage()
end
end
))
- CreateDesc(chatBgSection, "调整聊天窗口背景的透明度(仅影响背景,不影响文字)。也可在 /nui chat 中设置", 14, -68, font, 480)
+ table.insert(controls, CreateCheckBox(chatBgSection,
+ "悬停显示背景", 260, -36,
+ function()
+ local chatDB = SFramesDB and SFramesDB.Chat
+ return not chatDB or chatDB.hoverTransparent ~= false
+ end,
+ function(checked)
+ if not SFramesDB.Chat then SFramesDB.Chat = {} end
+ SFramesDB.Chat.hoverTransparent = (checked == true)
+ if SFrames.Chat and SFrames.Chat.ApplyConfig then
+ SFrames.Chat:ApplyConfig()
+ end
+ end
+ ))
+ CreateDesc(chatBgSection, "调整聊天窗口背景的透明度(仅影响背景,不影响文字)。勾选[悬停显示背景]后,鼠标离开时背景自动透明。", 14, -68, font, 480)
uiScroll:UpdateRange()
self.uiControls = controls
self.uiScroll = uiScroll
end
+local function AddFrameAdvancedSections(root, controls, spec)
+ local font = SFrames:GetFont()
+ local y = spec.startY
+ local apply = spec.apply
+ local prefix = spec.prefix
+ local showPowerHint = spec.showPowerHint
+ local powerSectionHeight = showPowerHint and 154 or 132
+
+ local powerSection = CreateSection(root, spec.powerTitle or "能量条", 8, y, 520, powerSectionHeight, font)
+ table.insert(controls, CreateSlider(powerSection, "宽度", 14, -42, 150, 40, 420, 1,
+ function() return SFramesDB[prefix .. "PowerWidth"] end,
+ function(value) SFramesDB[prefix .. "PowerWidth"] = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ apply
+ ))
+ table.insert(controls, CreateSlider(powerSection, "高度", 170, -42, 150, 4, 40, 1,
+ function() return SFramesDB[prefix .. "PowerHeight"] end,
+ function(value) SFramesDB[prefix .. "PowerHeight"] = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ apply
+ ))
+ table.insert(controls, CreateSlider(powerSection, "X 偏移", 326, -42, 130, -120, 120, 1,
+ function() return SFramesDB[prefix .. "PowerOffsetX"] end,
+ function(value) SFramesDB[prefix .. "PowerOffsetX"] = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ apply
+ ))
+ table.insert(controls, CreateSlider(powerSection, "Y 偏移", 14, -94, 150, -80, 80, 1,
+ function() return SFramesDB[prefix .. "PowerOffsetY"] end,
+ function(value) SFramesDB[prefix .. "PowerOffsetY"] = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ apply
+ ))
+ table.insert(controls, CreateCheckBox(powerSection,
+ "能量条在上层", 170, -92,
+ function() return SFramesDB[prefix .. "PowerOnTop"] == true end,
+ function(checked) SFramesDB[prefix .. "PowerOnTop"] = checked end,
+ apply
+ ))
+ if showPowerHint then
+ CreateDesc(powerSection, showPowerHint, 14, -118, font, 470)
+ end
+
+ y = y - (powerSectionHeight + 12)
+ local textureSection = CreateSection(root, "条材质", 8, y, 520, 182, font)
+ local texControls, texHeight = CreateOptionGrid(textureSection, "血条材质", 14, -34, 490, SFrames.BarTextures or {},
+ function() return SFramesDB[prefix .. "HealthTexture"] or "" end,
+ function(value) SFramesDB[prefix .. "HealthTexture"] = value end,
+ "bar", 0, apply)
+ for _, control in ipairs(texControls) do table.insert(controls, control) end
+ local texControls2 = CreateOptionGrid(textureSection, "能量条材质", 14, -(34 + texHeight + 12), 490, SFrames.BarTextures or {},
+ function() return SFramesDB[prefix .. "PowerTexture"] or "" end,
+ function(value) SFramesDB[prefix .. "PowerTexture"] = value end,
+ "bar", 0, apply)
+ for _, control in ipairs(texControls2) do table.insert(controls, control) end
+
+ y = y - 194
+ local fontSection = CreateSection(root, "字体", 8, y, 520, spec.fontSectionHeight or 286, font)
+ local blockY = -34
+ local function AddFontBlock(title, keyName, sizeKey)
+ local optControls, optHeight = CreateOptionGrid(fontSection, title, 14, blockY, 490, SFrames.FontChoices or {},
+ function() return SFramesDB[keyName] or "" end,
+ function(value) SFramesDB[keyName] = value end,
+ "font", 11, apply)
+ for _, control in ipairs(optControls) do table.insert(controls, control) end
+ table.insert(controls, CreateSlider(fontSection, title .. "字号", 14, blockY - optHeight - 26, 180, 8, 24, 1,
+ function() return SFramesDB[sizeKey] end,
+ function(value) SFramesDB[sizeKey] = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ apply
+ ))
+ blockY = blockY - optHeight - 74
+ end
+
+ AddFontBlock("姓名字体", prefix .. "NameFontKey", prefix .. "NameFontSize")
+ AddFontBlock("生命值字体", prefix .. "HealthFontKey", prefix .. "HealthFontSize")
+ AddFontBlock("能量值字体", prefix .. "PowerFontKey", prefix .. "PowerFontSize")
+
+ if spec.extraFontBlocks then
+ for _, extra in ipairs(spec.extraFontBlocks) do
+ AddFontBlock(extra.title, extra.keyName, extra.sizeKey)
+ end
+ end
+
+ return y - (spec.fontSectionHeight or 286) - 12
+end
+
+local FRAME_DEFAULT_BUILDERS = {
+ player = function()
+ local portraitHeight = (SFrames.Config and SFrames.Config.height or 45) - 2
+ return {
+ enablePlayerFrame = true, playerFrameScale = 1, playerFrameWidth = 220,
+ playerPortraitWidth = 50, playerPortraitHeight = portraitHeight, playerPortraitOffsetX = 0, playerPortraitOffsetY = 0, playerPortraitMode = "head",
+ playerHealthHeight = 38, playerPowerHeight = 9, playerPowerWidth = 166, playerPowerOffsetX = 0, playerPowerOffsetY = 0, playerPowerOnTop = false,
+ playerShowClass = false, playerShowClassIcon = true, playerShowPortrait = true,
+ playerFrameAlpha = 1, playerBgAlpha = 0.9, playerPortraitBgAlpha = 0.9, playerShowBorder = false, playerCornerRadius = 0,
+ playerNameFontSize = 10, playerValueFontSize = 10, playerHealthFontSize = 10, playerPowerFontSize = 10,
+ playerHealthTexture = "", playerPowerTexture = "", playerNameFontKey = "", playerHealthFontKey = "", playerPowerFontKey = "",
+ castbarStandalone = true, castbarRainbow = true, castbarWidth = 280, castbarHeight = 20, castbarAlpha = 1, powerRainbow = false, showPetFrame = true, petFrameScale = 1,
+ playerLeaderIconOffsetX = 0, playerLeaderIconOffsetY = 0, playerRaidIconOffsetX = 0, playerRaidIconOffsetY = 0,
+ petHealthTexture = "", petPowerTexture = "",
+ }
+ end,
+ target = function()
+ local portraitHeight = (SFrames.Config and SFrames.Config.height or 45) - 2
+ return {
+ enableTargetFrame = true, targetFrameScale = 1, targetFrameWidth = 220,
+ targetPortraitWidth = 50, targetPortraitHeight = portraitHeight, targetPortraitOffsetX = 0, targetPortraitOffsetY = 0, targetPortraitMode = "head",
+ targetHealthHeight = 38, targetPowerHeight = 9, targetPowerWidth = 168, targetPowerOffsetX = 0, targetPowerOffsetY = 0, targetPowerOnTop = false,
+ targetShowClass = false, targetShowClassIcon = true, targetShowPortrait = true,
+ targetFrameAlpha = 1, targetBgAlpha = 0.9, targetPortraitBgAlpha = 0.9, targetShowBorder = false, targetCornerRadius = 0,
+ targetNameFontSize = 10, targetValueFontSize = 10, targetHealthFontSize = 10, targetPowerFontSize = 10,
+ targetHealthTexture = "", targetPowerTexture = "", targetNameFontKey = "", targetHealthFontKey = "", targetPowerFontKey = "",
+ targetDistanceEnabled = true, targetDistanceOnFrame = true, targetDistanceScale = 1, targetDistanceFontSize = 14, targetDistanceFontKey = "",
+ totHealthTexture = "",
+ }
+ end,
+ focus = function()
+ return {
+ focusEnabled = true, focusFrameScale = 0.9, focusFrameWidth = 200,
+ focusPortraitWidth = 45, focusPortraitHeight = 43, focusPortraitOffsetX = 0, focusPortraitOffsetY = 0, focusPortraitMode = "head",
+ focusHealthHeight = 32, focusPowerHeight = 10, focusPowerWidth = 153, focusPowerOffsetX = 0, focusPowerOffsetY = 0, focusPowerOnTop = false,
+ focusShowPortrait = true, focusShowCastBar = true, focusShowAuras = true,
+ focusBgAlpha = 0.9, focusPortraitBgAlpha = 0.9, focusShowBorder = false, focusCornerRadius = 0,
+ focusNameFontSize = 11, focusValueFontSize = 10, focusHealthFontSize = 10, focusPowerFontSize = 10, focusCastFontSize = 10, focusDistanceFontSize = 14,
+ focusHealthTexture = "", focusPowerTexture = "", focusNameFontKey = "", focusHealthFontKey = "", focusPowerFontKey = "", focusCastFontKey = "", focusDistanceFontKey = "",
+ }
+ end,
+ party = function()
+ return {
+ enablePartyFrame = true, partyLayout = "vertical", partyFrameScale = 1, partyFrameWidth = 150, partyFrameHeight = 35,
+ partyPortraitWidth = 33, partyPortraitHeight = 33, partyPortraitOffsetX = 0, partyPortraitOffsetY = 0, partyPortraitMode = "head",
+ partyHealthHeight = 22, partyPowerHeight = 10, partyPowerWidth = 112, partyPowerOffsetX = 0, partyPowerOffsetY = 0, partyPowerOnTop = false,
+ partyHorizontalGap = 8, partyVerticalGap = 30, partyBgAlpha = 0.9, partyPortraitBgAlpha = 0.9, partyShowBorder = false, partyCornerRadius = 0,
+ partyNameFontSize = 10, partyValueFontSize = 10, partyHealthFontSize = 10, partyPowerFontSize = 10,
+ partyHealthTexture = "", partyPowerTexture = "", partyNameFontKey = "", partyHealthFontKey = "", partyPowerFontKey = "",
+ partyShowBuffs = true, partyShowDebuffs = true, partyPortrait3D = true,
+ }
+ end,
+ raid = function()
+ return {
+ enableRaidFrames = true, raidLayout = "horizontal", raidFrameScale = 1, raidFrameWidth = 60, raidFrameHeight = 40,
+ raidHealthHeight = 31, raidPowerHeight = 6, raidPowerWidth = 58, raidPowerOffsetX = 0, raidPowerOffsetY = 0, raidPowerOnTop = false,
+ raidHorizontalGap = 2, raidVerticalGap = 2, raidGroupGap = 8, raidBgAlpha = 0.9, raidShowBorder = false, raidCornerRadius = 0,
+ raidNameFontSize = 10, raidValueFontSize = 9, raidHealthFontSize = 9, raidPowerFontSize = 9,
+ raidHealthTexture = "", raidPowerTexture = "", raidNameFontKey = "", raidHealthFontKey = "", raidPowerFontKey = "",
+ raidShowPower = true, raidHealthFormat = "compact", raidShowGroupLabel = true,
+ }
+ end,
+}
+
+local activePreviewDropdown
+
+local function FindOptionEntry(options, key)
+ if type(options) ~= "table" then return nil end
+ for _, entry in ipairs(options) do
+ if entry.key == key then return entry end
+ end
+ return options[1]
+end
+
+local function ClosePreviewDropdown()
+ if activePreviewDropdown and activePreviewDropdown.Hide then activePreviewDropdown:Hide() end
+ activePreviewDropdown = nil
+end
+
+local function CreatePreviewDropdown(parent, label, x, y, width, options, getter, setter, previewKind, previewSize, onValueChanged)
+ local font = SFrames:GetFont()
+ CreateLabel(parent, label, x, y, font, 10, 0.85, 0.75, 0.80)
+
+ local button = CreateFrame("Button", NextWidgetName("PreviewDropdown"), parent)
+ button:SetWidth(width)
+ button:SetHeight(24)
+ button:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y - 16)
+ button:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 1, insets = { left = 1, right = 1, top = 1, bottom = 1 } })
+ button:SetBackdropColor(0.08, 0.08, 0.10, 0.92)
+ button:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
+
+ local preview = button:CreateFontString(nil, "OVERLAY")
+ preview:SetPoint("LEFT", button, "LEFT", 8, 0)
+ preview:SetPoint("RIGHT", button, "RIGHT", -24, 0)
+ preview:SetJustifyH("LEFT")
+ preview:SetTextColor(0.92, 0.92, 0.92)
+
+ local arrow = button:CreateFontString(nil, "OVERLAY")
+ arrow:SetFont(font, 10, "OUTLINE")
+ arrow:SetPoint("RIGHT", button, "RIGHT", -8, 0)
+ arrow:SetText("▼")
+ arrow:SetTextColor(0.8, 0.75, 0.8)
+
+ local menu = CreateFrame("Frame", NextWidgetName("PreviewDropdownMenu"), UIParent)
+ menu:SetWidth(width)
+ menu:SetHeight((table.getn(options or {}) * 22) + 8)
+ menu:SetFrameStrata("DIALOG")
+ menu:SetClampedToScreen(true)
+ EnsureSoftBackdrop(menu)
+ menu:SetBackdropColor(0.05, 0.05, 0.07, 0.96)
+ menu:SetBackdropBorderColor(0.45, 0.38, 0.46, 1)
+ menu:Hide()
+ button.menu = menu
+
+ for index, entry in ipairs(options or {}) do
+ local item = CreateFrame("Button", NextWidgetName("PreviewDropdownItem"), menu)
+ item:SetWidth(width - 10)
+ item:SetHeight(20)
+ item:SetPoint("TOPLEFT", menu, "TOPLEFT", 5, -5 - ((index - 1) * 22))
+ item.optionKey = entry.key
+
+ local itemText = item:CreateFontString(nil, "OVERLAY")
+ itemText:SetPoint("LEFT", item, "LEFT", 6, 0)
+ itemText:SetPoint("RIGHT", item, "RIGHT", -6, 0)
+ itemText:SetJustifyH("LEFT")
+ itemText:SetText(entry.label)
+ if previewKind == "font" then
+ local ok = pcall(itemText.SetFont, itemText, entry.path, previewSize or 11, "OUTLINE")
+ if not ok then itemText:SetFont(font, previewSize or 11, "OUTLINE") end
+ else
+ itemText:SetFont(font, 10, "OUTLINE")
+ end
+ itemText:SetTextColor(0.9, 0.9, 0.9)
+ item.text = itemText
+
+ item:SetScript("OnEnter", function() this.text:SetTextColor(1, 0.88, 0.55) end)
+ item:SetScript("OnLeave", function() this.text:SetTextColor(0.9, 0.9, 0.9) end)
+ item:SetScript("OnClick", function()
+ setter(this.optionKey)
+ ClosePreviewDropdown()
+ if button.Refresh then button:Refresh() end
+ if onValueChanged then onValueChanged(this.optionKey) end
+ end)
+ end
+
+ button:SetScript("OnHide", function() if this.menu then this.menu:Hide() end end)
+ button:SetScript("OnClick", function()
+ if this.menu:IsShown() then
+ ClosePreviewDropdown()
+ return
+ end
+ ClosePreviewDropdown()
+ this.menu:ClearAllPoints()
+ this.menu:SetPoint("TOPLEFT", this, "BOTTOMLEFT", 0, -2)
+ this.menu:Show()
+ activePreviewDropdown = this.menu
+ end)
+
+ button.Refresh = function()
+ local entry = FindOptionEntry(options, getter() or "")
+ if not entry then return end
+ preview:SetText(entry.label)
+ if previewKind == "font" then
+ local ok = pcall(preview.SetFont, preview, entry.path, previewSize or 11, "OUTLINE")
+ if not ok then preview:SetFont(font, previewSize or 11, "OUTLINE") end
+ else
+ preview:SetFont(font, 10, "OUTLINE")
+ end
+ end
+
+ button:Refresh()
+ return button
+end
+
+local function ResetFrameDefaults(kind)
+ local builder = FRAME_DEFAULT_BUILDERS[kind]
+ if not builder then return end
+ for key, value in pairs(builder()) do
+ SFramesDB[key] = value
+ end
+end
+
+local function CreateResetButton(parent, x, y, frameKind, refreshFn, controls)
+ return CreateButton(parent, "恢复默认", x, y, 100, 22, function()
+ ClosePreviewDropdown()
+ ResetFrameDefaults(frameKind)
+ EnsureDB()
+ if controls then SFrames.ConfigUI:RefreshControls(controls) end
+ if refreshFn then refreshFn() end
+ end)
+end
+
+local function AddFontControl(parent, controls, label, keyName, sizeKey, x, y, apply, sizeMin, sizeMax)
+ table.insert(controls, CreatePreviewDropdown(parent, label .. "字体", x, y, 220, SFrames.FontChoices or {},
+ function() return SFramesDB[keyName] or "" end,
+ function(value) SFramesDB[keyName] = value end,
+ "font", 11, apply))
+ table.insert(controls, CreateSlider(parent, label .. "字号", x + 244, y - 4, 180, sizeMin or 8, sizeMax or 24, 1,
+ function() return SFramesDB[sizeKey] end,
+ function(value) SFramesDB[sizeKey] = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ apply))
+end
+
+local function AddFontControl(parent, controls, label, keyName, sizeKey, x, y, apply, sizeMin, sizeMax)
+ CreateLabel(parent, label .. "字体", x, y, SFrames:GetFont(), 10, 0.85, 0.75, 0.80)
+
+ local choices = SFrames.FontChoices or {}
+ local previewBtn = CreateFrame("Button", NextWidgetName("FontPickerSafe"), parent)
+ previewBtn:SetWidth(220)
+ previewBtn:SetHeight(24)
+ previewBtn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y - 16)
+ previewBtn:SetBackdrop({
+ bgFile = "Interface\\Buttons\\WHITE8X8",
+ edgeFile = "Interface\\Buttons\\WHITE8X8",
+ tile = false, edgeSize = 1,
+ insets = { left = 1, right = 1, top = 1, bottom = 1 },
+ })
+ previewBtn:SetBackdropColor(0.08, 0.08, 0.10, 0.92)
+ previewBtn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
+
+ local previewText = previewBtn:CreateFontString(nil, "OVERLAY")
+ previewText:SetPoint("LEFT", previewBtn, "LEFT", 8, 0)
+ previewText:SetPoint("RIGHT", previewBtn, "RIGHT", -8, 0)
+ previewText:SetJustifyH("LEFT")
+ previewText:SetTextColor(0.92, 0.92, 0.92)
+
+ local function FindFontIndex()
+ local current = SFramesDB[keyName] or ""
+ for idx, entry in ipairs(choices) do
+ if entry.key == current then
+ return idx
+ end
+ end
+ return 1
+ end
+
+ local function ApplyFontIndex(index)
+ local entry = choices[index] or choices[1]
+ if not entry then return end
+ SFramesDB[keyName] = entry.key
+ previewText:SetText(entry.label)
+ local ok = pcall(previewText.SetFont, previewText, entry.path, 11, "OUTLINE")
+ if not ok then
+ previewText:SetFont(SFrames:GetFont(), 11, "OUTLINE")
+ end
+ if apply then apply() end
+ end
+
+ previewBtn.Refresh = function()
+ ApplyFontIndex(FindFontIndex())
+ end
+ previewBtn:SetScript("OnClick", function()
+ local idx = FindFontIndex() + 1
+ if idx > table.getn(choices) then idx = 1 end
+ ApplyFontIndex(idx)
+ end)
+ previewBtn:Refresh()
+ table.insert(controls, previewBtn)
+
+ table.insert(controls, CreateButton(parent, "<", x + 224, y - 16, 24, 24, function()
+ local idx = FindFontIndex() - 1
+ if idx < 1 then idx = table.getn(choices) end
+ ApplyFontIndex(idx)
+ end))
+
+ table.insert(controls, CreateButton(parent, ">", x + 252, y - 16, 24, 24, function()
+ local idx = FindFontIndex() + 1
+ if idx > table.getn(choices) then idx = 1 end
+ ApplyFontIndex(idx)
+ end))
+
+ table.insert(controls, CreateSlider(parent, label .. "字号", x + 284, y - 4, 140, sizeMin or 8, sizeMax or 24, 1,
+ function() return SFramesDB[sizeKey] end,
+ function(value) SFramesDB[sizeKey] = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ apply))
+end
function SFrames.ConfigUI:BuildPlayerPage()
local font = SFrames:GetFont()
local page = self.playerPage
local controls = {}
- local playerScroll = CreateScrollArea(page, 4, -4, 552, PANEL_HEIGHT - 110, 660)
+ local playerScroll = CreateScrollArea(page, 4, -4, 552, 420, 1960)
local root = playerScroll.child
local function RefreshPlayer()
@@ -1226,6 +1833,7 @@ function SFrames.ConfigUI:BuildPlayerPage()
function() return SFramesDB.enablePlayerFrame ~= false end,
function(checked) SFramesDB.enablePlayerFrame = checked end
))
+ table.insert(controls, CreateResetButton(playerSection, 356, -24, "player", RefreshPlayer, controls))
table.insert(controls, CreateSlider(playerSection, "缩放", 14, -72, 150, 0.7, 1.8, 0.05,
function() return SFramesDB.playerFrameScale end,
@@ -1328,7 +1936,7 @@ function SFrames.ConfigUI:BuildPlayerPage()
14, -355, font, 10, 0.9, 0.9, 0.9)
-- ── 施法条 ──────────────────────────────────────────────────
- local cbSection = CreateSection(root, "施法条", 8, -418, 520, 110, font)
+ local cbSection = CreateSection(root, "施法条", 8, -418, 520, 250, font)
table.insert(controls, CreateCheckBox(cbSection,
"独立施法条(显示在屏幕下方)", 14, -34,
@@ -1349,8 +1957,43 @@ function SFrames.ConfigUI:BuildPlayerPage()
))
CreateDesc(cbSection, "施法时条颜色会随时间流转变化", 36, -90, font)
+ table.insert(controls, CreateSlider(cbSection, "独立施法条宽度", 14, -120, 200, 100, 500, 5,
+ function() return SFramesDB.castbarWidth or 280 end,
+ function(value) SFramesDB.castbarWidth = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ function()
+ if SFrames.Player and SFrames.Player.ApplyCastbarPosition then
+ SFrames.Player:ApplyCastbarPosition()
+ end
+ end
+ ))
+
+ table.insert(controls, CreateSlider(cbSection, "独立施法条高度", 270, -120, 200, 10, 60, 1,
+ function() return SFramesDB.castbarHeight or 20 end,
+ function(value) SFramesDB.castbarHeight = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ function()
+ if SFrames.Player and SFrames.Player.ApplyCastbarPosition then
+ SFrames.Player:ApplyCastbarPosition()
+ end
+ end
+ ))
+
+ table.insert(controls, CreateSlider(cbSection, "施法条透明度", 14, -160, 200, 0.1, 1.0, 0.05,
+ function() return SFramesDB.castbarAlpha or 1 end,
+ function(value) SFramesDB.castbarAlpha = value end,
+ function(v) return string.format("%.0f%%", v * 100) end,
+ function(value)
+ local cb = SFrames.Player and SFrames.Player.frame and SFrames.Player.frame.castbar
+ if cb and cb:IsShown() then
+ cb:SetAlpha(value)
+ if cb.cbbg then cb.cbbg:SetAlpha(value) end
+ end
+ end
+ ))
+
-- ── 宠物框体 ──────────────────────────────────────────────────
- local petSection = CreateSection(root, "宠物框体", 8, -536, 520, 98, font)
+ local petSection = CreateSection(root, "宠物框体", 8, -676, 520, 98, font)
table.insert(controls, CreateCheckBox(petSection,
"显示宠物框体", 14, -34,
@@ -1367,6 +2010,56 @@ function SFrames.ConfigUI:BuildPlayerPage()
function(value) if SFrames.Pet and SFrames.Pet.frame then SFrames.Pet.frame:SetScale(value) end end
))
+ -- ── 图标位置 ──────────────────────────────────────────────────
+ local iconSection = CreateSection(root, "图标位置", 8, -754, 520, 142, font)
+ table.insert(controls, CreateSlider(iconSection, "队长图标 X", 14, -44, 150, -80, 80, 1,
+ function() return SFramesDB.playerLeaderIconOffsetX or 0 end,
+ function(value) SFramesDB.playerLeaderIconOffsetX = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ RefreshPlayer
+ ))
+ table.insert(controls, CreateSlider(iconSection, "队长图标 Y", 170, -44, 150, -80, 80, 1,
+ function() return SFramesDB.playerLeaderIconOffsetY or 0 end,
+ function(value) SFramesDB.playerLeaderIconOffsetY = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ RefreshPlayer
+ ))
+ table.insert(controls, CreateSlider(iconSection, "战斗标记 X", 14, -104, 150, -120, 120, 1,
+ function() return SFramesDB.playerRaidIconOffsetX or 0 end,
+ function(value) SFramesDB.playerRaidIconOffsetX = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ RefreshPlayer
+ ))
+ table.insert(controls, CreateSlider(iconSection, "战斗标记 Y", 170, -104, 150, -80, 80, 1,
+ function() return SFramesDB.playerRaidIconOffsetY or 0 end,
+ function(value) SFramesDB.playerRaidIconOffsetY = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ RefreshPlayer
+ ))
+
+ AddFrameAdvancedSections(root, controls, {
+ prefix = "player",
+ startY = -944,
+ apply = RefreshPlayer,
+ })
+
+ local function RefreshPet()
+ if SFrames.Pet and SFrames.Pet.ApplyConfig then SFrames.Pet:ApplyConfig() end
+ if SFrames.Pet and SFrames.Pet.UpdateAll then SFrames.Pet:UpdateAll() end
+ end
+
+ local petTexSection = CreateSection(root, "宠物框体 条材质", 8, -1800, 520, 182, font)
+ local petTexControls, petTexH = CreateOptionGrid(petTexSection, "血条材质", 14, -34, 490, SFrames.BarTextures or {},
+ function() return SFramesDB.petHealthTexture or "" end,
+ function(value) SFramesDB.petHealthTexture = value end,
+ "bar", 0, RefreshPet)
+ for _, c in ipairs(petTexControls) do table.insert(controls, c) end
+ local petTexControls2 = CreateOptionGrid(petTexSection, "能量条材质", 14, -(34 + petTexH + 12), 490, SFrames.BarTextures or {},
+ function() return SFramesDB.petPowerTexture or "" end,
+ function(value) SFramesDB.petPowerTexture = value end,
+ "bar", 0, RefreshPet)
+ for _, c in ipairs(petTexControls2) do table.insert(controls, c) end
+
playerScroll:UpdateRange()
self.playerControls = controls
self.playerScroll = playerScroll
@@ -1385,13 +2078,17 @@ function SFrames.ConfigUI:BuildTargetPage()
if SFrames.Target and SFrames.Target.UpdateAll then SFrames.Target:UpdateAll() end
end
- local targetSection = CreateSection(page, "目标框体", 8, -8, 520, 460, font)
+ local targetScroll = CreateScrollArea(page, 4, -4, 548, 420, 1780)
+ local targetRoot = targetScroll.child
+
+ local targetSection = CreateSection(targetRoot, "目标框体", 8, -8, 520, 460, font)
table.insert(controls, CreateCheckBox(targetSection,
"启用 Nanami 目标框体(需 /reload)", 12, -28,
function() return SFramesDB.enableTargetFrame ~= false end,
function(checked) SFramesDB.enableTargetFrame = checked end
))
+ table.insert(controls, CreateResetButton(targetSection, 356, -24, "target", RefreshTarget, controls))
table.insert(controls, CreateSlider(targetSection, "缩放", 14, -72, 150, 0.7, 1.8, 0.05,
function() return SFramesDB.targetFrameScale end,
@@ -1501,10 +2198,22 @@ function SFrames.ConfigUI:BuildTargetPage()
SFrames.Target.distanceFrame:Hide()
end
end
+ if SFrames.Target and SFrames.Target.frame and SFrames.Target.frame.distText then
+ if not checked then SFrames.Target.frame.distText:Hide() end
+ end
end
))
- table.insert(controls, CreateSlider(targetSection, "距离文本缩放", 14, -378, 150, 0.7, 1.8, 0.05,
+ table.insert(controls, CreateCheckBox(targetSection,
+ "距离显示在框体上", 326, -354,
+ function() return SFramesDB.targetDistanceOnFrame ~= false end,
+ function(checked) SFramesDB.targetDistanceOnFrame = checked end,
+ function()
+ if SFrames.Target then SFrames.Target:OnTargetChanged() end
+ end
+ ))
+
+ table.insert(controls, CreateSlider(targetSection, "距离文本缩放(独立框体)", 14, -406, 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,
@@ -1515,7 +2224,7 @@ function SFrames.ConfigUI:BuildTargetPage()
end
))
- table.insert(controls, CreateSlider(targetSection, "距离文字大小", 200, -378, 150, 8, 24, 1,
+ table.insert(controls, CreateSlider(targetSection, "距离文字大小", 200, -406, 150, 8, 24, 1,
function() return SFramesDB.targetDistanceFontSize or 14 end,
function(value) SFramesDB.targetDistanceFontSize = value end,
function(v) return tostring(math.floor(v + 0.5)) end,
@@ -1523,10 +2232,36 @@ function SFrames.ConfigUI:BuildTargetPage()
if SFrames.Target and SFrames.Target.ApplyDistanceScale then
SFrames.Target:ApplyDistanceScale(SFramesDB.targetDistanceScale or 1)
end
+ if SFrames.Target and SFrames.Target.ApplyConfig then
+ SFrames.Target:ApplyConfig()
+ end
end
))
+ AddFrameAdvancedSections(targetRoot, controls, {
+ prefix = "target",
+ startY = -516,
+ apply = RefreshTarget,
+ extraFontBlocks = {
+ { title = "距离文字字体", keyName = "targetDistanceFontKey", sizeKey = "targetDistanceFontSize" },
+ },
+ fontSectionHeight = 372,
+ })
+
+ local function RefreshToT()
+ if SFrames.ToT and SFrames.ToT.ApplyConfig then SFrames.ToT:ApplyConfig() end
+ end
+
+ local totTexSection = CreateSection(targetRoot, "目标的目标 条材质", 8, -1560, 520, 100, font)
+ local totTexControls, totTexH = CreateOptionGrid(totTexSection, "血条材质", 14, -34, 490, SFrames.BarTextures or {},
+ function() return SFramesDB.totHealthTexture or "" end,
+ function(value) SFramesDB.totHealthTexture = value end,
+ "bar", 0, RefreshToT)
+ for _, c in ipairs(totTexControls) do table.insert(controls, c) end
+
+ targetScroll:UpdateRange()
self.targetControls = controls
+ self.targetScroll = targetScroll
end
function SFrames.ConfigUI:BuildFocusPage()
@@ -1534,7 +2269,9 @@ function SFrames.ConfigUI:BuildFocusPage()
local page = self.focusPage
local controls = {}
- local focusSection = CreateSection(page, "焦点框体", 8, -8, 520, 380, font)
+ local focusScroll = CreateScrollArea(page, 4, -4, 548, 420, 1860)
+ local root = focusScroll.child
+ local focusSection = CreateSection(root, "焦点框体", 8, -8, 520, 390, font)
local function FocusApply()
if SFrames.Focus and SFrames.Focus.ApplySettings then
@@ -1547,6 +2284,7 @@ function SFrames.ConfigUI:BuildFocusPage()
function() return SFramesDB.focusEnabled ~= false end,
function(checked) SFramesDB.focusEnabled = checked end
))
+ table.insert(controls, CreateResetButton(focusSection, 356, -24, "focus", FocusApply, controls))
table.insert(controls, CreateSlider(focusSection, "缩放", 14, -72, 150, 0.5, 1.8, 0.05,
function() return SFramesDB.focusFrameScale end,
@@ -1618,7 +2356,20 @@ function SFrames.ConfigUI:BuildFocusPage()
CreateLabel(focusSection, "命令: /nui focus (设焦点) | /nui unfocus (取消) | /nui focustarget (选中焦点)", 14, -364, font, 9, 0.65, 0.58, 0.62)
CreateLabel(focusSection, "提示: 启用/禁用焦点框体需要 /reload,其余设置实时生效。", 14, -384, font, 10, 0.6, 0.6, 0.65)
+ AddFrameAdvancedSections(root, controls, {
+ prefix = "focus",
+ startY = -418,
+ apply = FocusApply,
+ extraFontBlocks = {
+ { title = "施法条字体", keyName = "focusCastFontKey", sizeKey = "focusCastFontSize" },
+ { title = "距离文字字体", keyName = "focusDistanceFontKey", sizeKey = "focusDistanceFontSize" },
+ },
+ fontSectionHeight = 458,
+ })
+
+ focusScroll:UpdateRange()
self.focusControls = controls
+ self.focusScroll = focusScroll
end
function SFrames.ConfigUI:BuildPartyPage()
@@ -1633,7 +2384,7 @@ function SFrames.ConfigUI:BuildPartyPage()
if SFrames.Party and SFrames.Party.UpdateAll then SFrames.Party:UpdateAll() end
end
- local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 480)
+ local uiScroll = CreateScrollArea(page, 4, -4, 548, 420, 1720)
local root = uiScroll.child
local partySection = CreateSection(root, "小队", 8, -8, 520, 460, font)
@@ -1643,6 +2394,7 @@ function SFrames.ConfigUI:BuildPartyPage()
function() return SFramesDB.enablePartyFrame ~= false end,
function(checked) SFramesDB.enablePartyFrame = checked end
))
+ table.insert(controls, CreateResetButton(partySection, 356, -24, "party", RefreshParty, controls))
CreateButton(partySection, "解锁小队框架", 14, -52, 130, 22, function()
if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end
@@ -1766,9 +2518,22 @@ function SFrames.ConfigUI:BuildPartyPage()
function() RefreshParty() end
))
+ table.insert(controls, CreateCheckBox(partySection,
+ "启用3D头像", 326, -380,
+ function() return SFramesDB.partyPortrait3D ~= false end,
+ function(checked) SFramesDB.partyPortrait3D = checked end,
+ function() RefreshParty() end
+ ))
+
CreateButton(partySection, "小队测试模式", 326, -410, 130, 22, function()
if SFrames.Party and SFrames.Party.TestMode then SFrames.Party:TestMode() end
end)
+ AddFrameAdvancedSections(root, controls, {
+ prefix = "party",
+ startY = -486,
+ apply = RefreshParty,
+ })
+
uiScroll:UpdateRange()
self.partyControls = controls
self.partyScroll = uiScroll
@@ -2009,7 +2774,7 @@ function SFrames.ConfigUI:BuildRaidPage()
end
end
- local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 600)
+ local uiScroll = CreateScrollArea(page, 4, -4, 548, 420, 1620)
local root = uiScroll.child
local raidSection = CreateSection(root, "团队框架设置", 8, -8, 520, 570, font)
@@ -2019,6 +2784,7 @@ function SFrames.ConfigUI:BuildRaidPage()
function() return SFramesDB.enableRaidFrames ~= false end,
function(checked) SFramesDB.enableRaidFrames = checked end
))
+ table.insert(controls, CreateResetButton(raidSection, 356, -24, "raid", RefreshRaid, controls))
CreateButton(raidSection, "解锁团队框架", 14, -56, 130, 22, function()
if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end
@@ -2164,7 +2930,15 @@ function SFrames.ConfigUI:BuildRaidPage()
end
end
end)
-
+
+ AddFrameAdvancedSections(root, controls, {
+ prefix = "raid",
+ startY = -592,
+ apply = RefreshRaid,
+ hasPortraitBg = false,
+ showPowerHint = "关闭团队能量条后,这些设置会保留。",
+ })
+
uiScroll:UpdateRange()
self.raidControls = controls
self.raidScroll = uiScroll
@@ -2317,10 +3091,10 @@ function SFrames.ConfigUI:BuildActionBarPage()
end
end
- local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 1040)
+ local uiScroll = CreateScrollArea(page, 4, -4, 548, 458, 1400)
local root = uiScroll.child
- local abSection = CreateSection(root, "动作条", 8, -8, 520, 840, font)
+ local abSection = CreateSection(root, "动作条", 8, -8, 520, 1040, font)
table.insert(controls, CreateCheckBox(abSection,
"启用动作条接管(需 /reload 生效)", 12, -28,
@@ -2383,7 +3157,10 @@ function SFrames.ConfigUI:BuildActionBarPage()
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.row2Anchor then SFrames.ActionBars.row2Anchor:SetAlpha(value) end
+ if SFrames.ActionBars.row3Anchor then SFrames.ActionBars.row3Anchor:SetAlpha(value) end
+ if SFrames.ActionBars.rightBar1Holder then SFrames.ActionBars.rightBar1Holder:SetAlpha(value) end
+ if SFrames.ActionBars.rightBar2Holder then SFrames.ActionBars.rightBar2Holder: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
@@ -2397,7 +3174,10 @@ function SFrames.ConfigUI:BuildActionBarPage()
function(value)
if SFrames.ActionBars then
if SFrames.ActionBars.anchor then ApplyBgAlphaToFrame(SFrames.ActionBars.anchor, value) end
- if SFrames.ActionBars.rightHolder then ApplyBgAlphaToFrame(SFrames.ActionBars.rightHolder, value) end
+ if SFrames.ActionBars.row2Anchor then ApplyBgAlphaToFrame(SFrames.ActionBars.row2Anchor, value) end
+ if SFrames.ActionBars.row3Anchor then ApplyBgAlphaToFrame(SFrames.ActionBars.row3Anchor, value) end
+ if SFrames.ActionBars.rightBar1Holder then ApplyBgAlphaToFrame(SFrames.ActionBars.rightBar1Holder, value) end
+ if SFrames.ActionBars.rightBar2Holder then ApplyBgAlphaToFrame(SFrames.ActionBars.rightBar2Holder, value) end
if SFrames.ActionBars.stanceHolder then ApplyBgAlphaToFrame(SFrames.ActionBars.stanceHolder, value) end
if SFrames.ActionBars.petHolder then ApplyBgAlphaToFrame(SFrames.ActionBars.petHolder, value) end
end
@@ -2425,85 +3205,114 @@ function SFrames.ConfigUI:BuildActionBarPage()
))
table.insert(controls, CreateCheckBox(abSection,
- "背后技能高亮(在目标背后时高亮背刺等技能)", 12, -356,
- function() return SFramesDB.ActionBars.behindGlow ~= false end,
- function(checked) SFramesDB.ActionBars.behindGlow = checked end
- ))
-
- table.insert(controls, CreateCheckBox(abSection,
- "显示宠物动作条", 12, -384,
+ "显示宠物动作条", 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, -384,
+ "显示姿态栏", 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, -412,
+ "显示右侧动作条", 12, -384,
function() return SFramesDB.ActionBars.showRightBars ~= false end,
function(checked) SFramesDB.ActionBars.showRightBars = checked end,
function() RefreshAB() end
))
+ table.insert(controls, CreateSlider(abSection, "右侧栏1每行按钮数", 14, -416, 150, 1, 12, 1,
+ function() return SFramesDB.ActionBars.rightBar1PerRow or 1 end,
+ function(value) SFramesDB.ActionBars.rightBar1PerRow = value end,
+ function(v) local n = math.floor(v + 0.5); if n == 1 then return "1 (竖)" elseif n == 12 then return "12 (横)" else return tostring(n) end end,
+ function() RefreshAB() end
+ ))
+
+ table.insert(controls, CreateSlider(abSection, "右侧栏2每行按钮数", 180, -416, 150, 1, 12, 1,
+ function() return SFramesDB.ActionBars.rightBar2PerRow or 1 end,
+ function(value) SFramesDB.ActionBars.rightBar2PerRow = value end,
+ function(v) local n = math.floor(v + 0.5); if n == 1 then return "1 (竖)" elseif n == 12 then return "12 (横)" else return tostring(n) end end,
+ function() RefreshAB() end
+ ))
+
+ table.insert(controls, CreateSlider(abSection, "底部栏1每行", 12, -456, 110, 1, 12, 1,
+ function() return SFramesDB.ActionBars.bottomBar1PerRow or 12 end,
+ function(value) SFramesDB.ActionBars.bottomBar1PerRow = value end,
+ function(v) local n = math.floor(v + 0.5); if n == 12 then return "12 (满)" else return tostring(n) end end,
+ function() RefreshAB() end
+ ))
+
+ table.insert(controls, CreateSlider(abSection, "底部栏2每行", 134, -456, 110, 1, 12, 1,
+ function() return SFramesDB.ActionBars.bottomBar2PerRow or 12 end,
+ function(value) SFramesDB.ActionBars.bottomBar2PerRow = value end,
+ function(v) local n = math.floor(v + 0.5); if n == 12 then return "12 (满)" else return tostring(n) end end,
+ function() RefreshAB() end
+ ))
+
+ table.insert(controls, CreateSlider(abSection, "底部栏3每行", 256, -456, 110, 1, 12, 1,
+ function() return SFramesDB.ActionBars.bottomBar3PerRow or 12 end,
+ function(value) SFramesDB.ActionBars.bottomBar3PerRow = value end,
+ function(v) local n = math.floor(v + 0.5); if n == 12 then return "12 (满)" else return tostring(n) end end,
+ function() RefreshAB() end
+ ))
+
table.insert(controls, CreateCheckBox(abSection,
- "始终显示动作条(空格子也显示背景框)", 12, -440,
+ "始终显示动作条(空格子也显示背景框)", 12, -514,
function() return SFramesDB.ActionBars.alwaysShowGrid == true end,
function(checked) SFramesDB.ActionBars.alwaysShowGrid = checked end,
function() RefreshAB() end
))
table.insert(controls, CreateCheckBox(abSection,
- "按钮圆角", 12, -468,
+ "按钮圆角", 12, -542,
function() return SFramesDB.ActionBars.buttonRounded == true end,
function(checked) SFramesDB.ActionBars.buttonRounded = checked end,
function() RefreshAB() end
))
table.insert(controls, CreateCheckBox(abSection,
- "按钮内阴影", 200, -468,
+ "按钮内阴影", 200, -542,
function() return SFramesDB.ActionBars.buttonInnerShadow == true end,
function(checked) SFramesDB.ActionBars.buttonInnerShadow = checked end,
function() RefreshAB() end
))
table.insert(controls, CreateCheckBox(abSection,
- "显示动作条狮鹫(在底部动作条两侧显示装饰狮鹫)", 12, -496,
+ "显示动作条狮鹫(在底部动作条两侧显示装饰狮鹫)", 12, -570,
function() return SFramesDB.ActionBars.hideGryphon == false end,
function(checked) SFramesDB.ActionBars.hideGryphon = not checked end,
function() RefreshAB() end
))
table.insert(controls, CreateCheckBox(abSection,
- "狮鹫置于动作条之上(否则在动作条之下)", 12, -524,
+ "狮鹫置于动作条之上(否则在动作条之下)", 12, -598,
function() return SFramesDB.ActionBars.gryphonOnTop == true end,
function(checked) SFramesDB.ActionBars.gryphonOnTop = checked end,
function() RefreshAB() end
))
- table.insert(controls, CreateSlider(abSection, "狮鹫宽度", 14, -578, 150, 24, 200, 2,
+ table.insert(controls, CreateSlider(abSection, "狮鹫宽度", 14, -652, 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, -578, 150, 24, 200, 2,
+ table.insert(controls, CreateSlider(abSection, "狮鹫高度", 180, -652, 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
))
- CreateDesc(abSection, "使用布局模式 (/nui layout) 调整狮鹫位置", 14, -640, font, 480)
+ CreateDesc(abSection, "使用布局模式 (/nui layout) 调整狮鹫位置", 14, -714, font, 480)
-- 狮鹫样式选择器(带图例预览)
- CreateLabel(abSection, "狮鹫样式:", 14, -692, font, 11, 0.85, 0.75, 0.80)
+ CreateLabel(abSection, "狮鹫样式:", 14, -766, font, 11, 0.85, 0.75, 0.80)
local GRYPHON_STYLES_UI = {
{ key = "dragonflight", label = "巨龙时代",
@@ -2518,7 +3327,7 @@ function SFrames.ConfigUI:BuildActionBarPage()
local styleBorders = {}
local styleStartX = 14
- local styleY = -712
+ local styleY = -786
for idx, style in ipairs(GRYPHON_STYLES_UI) do
local xOff = styleStartX + (idx - 1) * 125
@@ -2600,12 +3409,167 @@ function SFrames.ConfigUI:BuildActionBarPage()
end
end
- CreateLabel(abSection, "动作条位置:", 14, -806, font, 11, 0.85, 0.75, 0.80)
- CreateDesc(abSection, "使用 /nui layout 或右键聊天框 Nanami 标题进入布局模式调整动作条位置", 14, -826, font, 480)
+ -- Layout presets
+ CreateLabel(abSection, "布局预设:", 14, -908, font, 11, 0.85, 0.75, 0.80)
+ CreateDesc(abSection,
+ "快速切换动作条排列方式。选择预设会重置动作条位置并应用对应的排列。",
+ 14, -926, font, 480)
+
+ local presetDefs = (SFrames.ActionBars and SFrames.ActionBars.PRESETS) or {
+ { id = 1, name = "经典", desc = "底部堆叠 + 右侧竖栏" },
+ { id = 2, name = "宽屏", desc = "左4x3 + 底部堆叠 + 右4x3" },
+ { id = 3, name = "堆叠", desc = "全部堆叠于底部中央" },
+ }
+
+ local presetBtnW = 155
+ local presetBtnGap = 8
+ for pi = 1, 3 do
+ local pdef = presetDefs[pi]
+ local pLabel = pdef and pdef.name or ("方案" .. pi)
+ local pId = pi
+ local px = 14 + (pi - 1) * (presetBtnW + presetBtnGap)
+ CreateButton(abSection, pLabel, px, -946, presetBtnW, 24, function()
+ if SFrames.ActionBars and SFrames.ActionBars.ApplyPreset then
+ SFrames.ActionBars:ApplyPreset(pId)
+ RefreshAB()
+ end
+ end)
+ end
+
+ for pi = 1, 3 do
+ local pdef = presetDefs[pi]
+ local pdesc = pdef and pdef.desc or ""
+ local px = 14 + (pi - 1) * (presetBtnW + presetBtnGap)
+ CreateDesc(abSection, pdesc, px, -976, font, presetBtnW)
+ end
+
+ CreateLabel(abSection, "动作条位置:", 14, -1000, font, 11, 0.85, 0.75, 0.80)
+ CreateDesc(abSection, "使用 /nui layout 或右键聊天框 Nanami 标题进入布局模式调整动作条位置", 14, -1018, font, 480)
CreateLabel(abSection,
"提示:启用/禁用动作条接管需要 /reload 才能生效。",
- 14, -846, font, 10, 1, 0.92, 0.38)
+ 14, -1038, font, 10, 1, 0.92, 0.38)
+
+ -- ── 额外动作条 ──────────────────────────────────────────────
+ local function RefreshEB()
+ if SFrames.ExtraBar then
+ local edb = SFrames.ExtraBar:GetDB()
+ if edb.enable then
+ if not SFrames.ExtraBar.holder then
+ SFrames.ExtraBar:Enable()
+ else
+ SFrames.ExtraBar:ApplyConfig()
+ end
+ else
+ SFrames.ExtraBar:Disable()
+ end
+ end
+ end
+
+ local ebSection = CreateSection(root, "额外动作条", 8, -1056, 520, 310, font)
+
+ table.insert(controls, CreateCheckBox(ebSection,
+ "启用额外动作条(独立面板,使用空闲动作槽位)", 12, -30,
+ function() return SFramesDB.ExtraBar.enable == true end,
+ function(checked)
+ SFramesDB.ExtraBar.enable = checked
+ if checked then
+ if SFrames.ExtraBar then SFrames.ExtraBar:Enable() end
+ else
+ if SFrames.ExtraBar then SFrames.ExtraBar:Disable() end
+ end
+ end,
+ function() end
+ ))
+
+ table.insert(controls, CreateSlider(ebSection, "按钮总数", 14, -62, 150, 1, 48, 1,
+ function() return SFramesDB.ExtraBar.buttonCount or 12 end,
+ function(value) SFramesDB.ExtraBar.buttonCount = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ function() RefreshEB() end
+ ))
+
+ table.insert(controls, CreateSlider(ebSection, "每行按钮数", 180, -62, 150, 1, 12, 1,
+ function() return SFramesDB.ExtraBar.perRow or 12 end,
+ function(value) SFramesDB.ExtraBar.perRow = value end,
+ function(v) local n = math.floor(v + 0.5); if n == 12 then return "12 (满)" else return tostring(n) end end,
+ function() RefreshEB() end
+ ))
+
+ table.insert(controls, CreateSlider(ebSection, "按钮尺寸", 14, -102, 150, 20, 60, 1,
+ function() return SFramesDB.ExtraBar.buttonSize or 36 end,
+ function(value) SFramesDB.ExtraBar.buttonSize = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ function() RefreshEB() end
+ ))
+
+ table.insert(controls, CreateSlider(ebSection, "按钮间距", 180, -102, 150, 0, 10, 1,
+ function() return SFramesDB.ExtraBar.buttonGap or 2 end,
+ function(value) SFramesDB.ExtraBar.buttonGap = value end,
+ function(v) return tostring(math.floor(v + 0.5)) end,
+ function() RefreshEB() end
+ ))
+
+ table.insert(controls, CreateSlider(ebSection, "对齐方式", 14, -142, 150, 1, 3, 1,
+ function()
+ local a = SFramesDB.ExtraBar.align or "center"
+ if a == "left" then return 1 elseif a == "right" then return 3 else return 2 end
+ end,
+ function(value)
+ local n = math.floor(value + 0.5)
+ if n == 1 then SFramesDB.ExtraBar.align = "left"
+ elseif n == 3 then SFramesDB.ExtraBar.align = "right"
+ else SFramesDB.ExtraBar.align = "center" end
+ end,
+ function(v) local n = math.floor(v + 0.5); if n == 1 then return "左对齐" elseif n == 3 then return "右对齐" else return "居中" end end,
+ function() RefreshEB() end
+ ))
+
+ table.insert(controls, CreateSlider(ebSection, "透明度", 180, -142, 150, 0.1, 1.0, 0.05,
+ function() return SFramesDB.ExtraBar.alpha or 1.0 end,
+ function(value) SFramesDB.ExtraBar.alpha = value end,
+ function(v) return string.format("%.0f%%", v * 100) end,
+ function() RefreshEB() end
+ ))
+
+ table.insert(controls, CreateSlider(ebSection, "起始槽位(页7=73)", 14, -182, 320, 73, 109, 1,
+ function() return SFramesDB.ExtraBar.startSlot or 73 end,
+ function(value) SFramesDB.ExtraBar.startSlot = value end,
+ function(v) local n = math.floor(v + 0.5); local pg = math.floor((n - 1) / 12) + 1; local idx = math.fmod(n - 1, 12) + 1; return n .. " (页" .. pg .. "-" .. idx .. ")" end,
+ function() RefreshEB() end
+ ))
+
+ table.insert(controls, CreateCheckBox(ebSection,
+ "显示快捷键", 12, -220,
+ function() return SFramesDB.ExtraBar.showHotkey ~= false end,
+ function(checked) SFramesDB.ExtraBar.showHotkey = checked end,
+ function() RefreshEB() end
+ ))
+
+ table.insert(controls, CreateCheckBox(ebSection,
+ "显示堆叠数", 200, -220,
+ function() return SFramesDB.ExtraBar.showCount ~= false end,
+ function(checked) SFramesDB.ExtraBar.showCount = checked end,
+ function() RefreshEB() end
+ ))
+
+ table.insert(controls, CreateCheckBox(ebSection,
+ "按钮圆角", 12, -248,
+ function() return SFramesDB.ExtraBar.buttonRounded == true end,
+ function(checked) SFramesDB.ExtraBar.buttonRounded = checked end,
+ function() RefreshEB() end
+ ))
+
+ table.insert(controls, CreateCheckBox(ebSection,
+ "按钮内阴影", 200, -248,
+ function() return SFramesDB.ExtraBar.buttonInnerShadow == true end,
+ function(checked) SFramesDB.ExtraBar.buttonInnerShadow = checked end,
+ function() RefreshEB() end
+ ))
+
+ CreateDesc(ebSection,
+ "使用布局模式 (/nui layout) 调整额外动作条位置。默认槽位 73-84 (页7) 对所有职业安全。",
+ 14, -280, font, 480)
uiScroll:UpdateRange()
self.actionBarControls = controls
@@ -3363,10 +4327,98 @@ function SFrames.ConfigUI:BuildBuffPage()
self.buffScroll = uiScroll
end
+function SFrames.ConfigUI:ShowFramesSubPage(mode)
+ local subMode = mode
+ if subMode ~= "player" and subMode ~= "target" and subMode ~= "focus" and subMode ~= "party" and subMode ~= "raid" then
+ subMode = self.activeFrameSubPage or "player"
+ end
+
+ self.activeFrameSubPage = subMode
+ self:EnsurePage(subMode)
+
+ self.playerPage:Hide()
+ self.targetPage:Hide()
+ self.focusPage:Hide()
+ self.partyPage:Hide()
+ self.raidPage:Hide()
+
+ local allSubTabs = { self.framesPlayerTab, self.framesTargetTab, self.framesFocusTab, self.framesPartyTab, self.framesRaidTab }
+ for _, tab in ipairs(allSubTabs) do
+ if tab then
+ tab.sfSoftActive = false
+ tab:Enable()
+ tab:RefreshVisual()
+ end
+ end
+
+ local activeTab, controls, scroll, titleSuffix
+ if subMode == "target" then
+ self.targetPage:Show()
+ activeTab = self.framesTargetTab
+ controls = self.targetControls
+ scroll = self.targetScroll
+ titleSuffix = "目标框体"
+ elseif subMode == "focus" then
+ self.focusPage:Show()
+ activeTab = self.framesFocusTab
+ controls = self.focusControls
+ scroll = self.focusScroll
+ titleSuffix = "焦点框体"
+ elseif subMode == "party" then
+ self.partyPage:Show()
+ activeTab = self.framesPartyTab
+ controls = self.partyControls
+ scroll = self.partyScroll
+ titleSuffix = "小队框体"
+ elseif subMode == "raid" then
+ self.raidPage:Show()
+ activeTab = self.framesRaidTab
+ controls = self.raidControls
+ scroll = self.raidScroll
+ titleSuffix = "团队框体"
+ else
+ self.playerPage:Show()
+ activeTab = self.framesPlayerTab
+ controls = self.playerControls
+ scroll = self.playerScroll
+ titleSuffix = "玩家框体"
+ end
+
+ if activeTab then
+ activeTab.sfSoftActive = true
+ activeTab:Disable()
+ activeTab:RefreshVisual()
+ end
+
+ self.title:SetText("Nanami-UI 设置 - 框体设置 / " .. titleSuffix)
+ self:RefreshControls(controls)
+ if scroll and scroll.Reset then scroll:Reset() end
+end
+
function SFrames.ConfigUI:ShowPage(mode)
- self.activePage = mode
+ local requestedMode = mode or "ui"
+ local pageMode = requestedMode
+ if requestedMode == "player" or requestedMode == "target" or requestedMode == "focus" or requestedMode == "party" or requestedMode == "raid" then
+ pageMode = "frames"
+ end
+
+ if requestedMode == "frames" then
+ requestedMode = self.activeFrameSubPage or (SFramesDB and SFramesDB.configLastFrameSubPage) or "player"
+ end
+
+ if requestedMode == "player" or requestedMode == "target" or requestedMode == "focus" or requestedMode == "party" or requestedMode == "raid" then
+ if SFramesDB then
+ SFramesDB.configLastFrameSubPage = requestedMode
+ SFramesDB.configLastPage = requestedMode
+ end
+ elseif SFramesDB then
+ SFramesDB.configLastPage = pageMode
+ end
+
+ self.activePage = pageMode
self.uiPage:Hide()
+ self.framesPage:Hide()
self.playerPage:Hide()
self.targetPage:Hide()
self.focusPage:Hide()
@@ -3382,22 +4434,22 @@ function SFrames.ConfigUI:ShowPage(mode)
self.themePage:Hide()
self.profilePage:Hide()
- local allTabs = { self.uiTab, self.playerTab, self.targetTab, self.focusTab, self.partyTab, self.raidTab, self.bagTab, self.charTab, self.actionBarTab, self.keybindsTab, self.minimapTab, self.buffTab, self.personalizeTab, self.themeTab, self.profileTab }
+ local allTabs = { self.uiTab, self.framesTab, self.bagTab, self.charTab, self.actionBarTab, self.keybindsTab, self.minimapTab, self.buffTab, self.personalizeTab, self.themeTab, self.profileTab }
for _, tab in ipairs(allTabs) do
tab.sfSoftActive = false
tab:Enable()
tab:RefreshVisual()
end
- self:EnsurePage(mode or "ui")
+ self:EnsurePage(pageMode)
+ mode = pageMode
- if mode == "player" then
- self.playerPage:Show()
- self.playerTab.sfSoftActive = true
- self.playerTab:Disable()
- self.playerTab:RefreshVisual()
- self.title:SetText("Nanami-UI 设置 - 玩家框体")
- self:RefreshControls(self.playerControls)
+ if mode == "frames" then
+ self.framesPage:Show()
+ self.framesTab.sfSoftActive = true
+ self.framesTab:Disable()
+ self.framesTab:RefreshVisual()
+ self:ShowFramesSubPage(requestedMode)
elseif mode == "target" then
self.targetPage:Show()
self.targetTab.sfSoftActive = true
@@ -3547,17 +4599,18 @@ function SFrames.ConfigUI:EnsureFrame()
panel:SetBackdropBorderColor(SOFT_THEME.panelBorder[1], SOFT_THEME.panelBorder[2], SOFT_THEME.panelBorder[3], SOFT_THEME.panelBorder[4])
end
- local titleIco = SFrames:CreateIcon(panel, "logo", 18)
- titleIco:SetDrawLayer("OVERLAY")
- titleIco:SetPoint("TOP", panel, "TOP", -52, -14)
- titleIco:SetVertexColor(SOFT_THEME.title[1], SOFT_THEME.title[2], SOFT_THEME.title[3])
-
local title = panel:CreateFontString(nil, "OVERLAY")
title:SetFont(font, 14, "OUTLINE")
- title:SetPoint("LEFT", titleIco, "RIGHT", 5, 0)
+ title:SetPoint("TOP", panel, "TOP", 0, -14)
+ title:SetJustifyH("CENTER")
title:SetTextColor(SOFT_THEME.title[1], SOFT_THEME.title[2], SOFT_THEME.title[3])
title:SetText("Nanami-UI 设置")
+ local titleIco = SFrames:CreateIcon(panel, "logo", 18)
+ titleIco:SetDrawLayer("OVERLAY")
+ titleIco:SetPoint("RIGHT", title, "LEFT", -5, 0)
+ titleIco:SetVertexColor(SOFT_THEME.title[1], SOFT_THEME.title[2], SOFT_THEME.title[3])
+
local divider = panel:CreateTexture(nil, "ARTWORK")
divider:SetWidth(1)
divider:SetPoint("TOPLEFT", panel, "TOPLEFT", 120, -40)
@@ -3600,14 +4653,24 @@ function SFrames.ConfigUI:EnsureFrame()
StyleButton(tabUI)
AddBtnIcon(tabUI, "settings", nil, "left")
+ local tabFrames = CreateFrame("Button", "SFramesConfigTabFrames", panel, "UIPanelButtonTemplate")
+ tabFrames:SetWidth(100)
+ tabFrames:SetHeight(28)
+ tabFrames:SetPoint("TOP", tabUI, "BOTTOM", 0, -4)
+ tabFrames:SetText("框体设置")
+ tabFrames:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("frames") end)
+ StyleButton(tabFrames)
+ AddBtnIcon(tabFrames, "character", nil, "left")
+
local tabPlayer = CreateFrame("Button", "SFramesConfigTabPlayer", panel, "UIPanelButtonTemplate")
tabPlayer:SetWidth(100)
tabPlayer:SetHeight(28)
- tabPlayer:SetPoint("TOP", tabUI, "BOTTOM", 0, -4)
+ tabPlayer:SetPoint("TOP", tabFrames, "BOTTOM", 0, -4)
tabPlayer:SetText("玩家框体")
tabPlayer:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("player") end)
StyleButton(tabPlayer)
AddBtnIcon(tabPlayer, "character", nil, "left")
+ tabPlayer:Hide()
local tabTarget = CreateFrame("Button", "SFramesConfigTabTarget", panel, "UIPanelButtonTemplate")
tabTarget:SetWidth(100)
@@ -3617,15 +4680,17 @@ function SFrames.ConfigUI:EnsureFrame()
tabTarget:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("target") end)
StyleButton(tabTarget)
AddBtnIcon(tabTarget, "search", nil, "left")
+ tabTarget:Hide()
local tabFocus = CreateFrame("Button", "SFramesConfigTabFocus", panel, "UIPanelButtonTemplate")
tabFocus:SetWidth(100)
tabFocus:SetHeight(28)
- tabFocus:SetPoint("TOP", tabTarget, "BOTTOM", 0, -4)
+ tabFocus:SetPoint("TOP", tabFrames, "BOTTOM", 0, -4)
tabFocus:SetText("焦点框体")
tabFocus:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("focus") end)
StyleButton(tabFocus)
AddBtnIcon(tabFocus, "search", nil, "left")
+ tabFocus:Hide()
local tabParty = CreateFrame("Button", "SFramesConfigTabParty", panel, "UIPanelButtonTemplate")
tabParty:SetWidth(100)
@@ -3635,6 +4700,7 @@ function SFrames.ConfigUI:EnsureFrame()
tabParty:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("party") end)
StyleButton(tabParty)
AddBtnIcon(tabParty, "friends", nil, "left")
+ tabParty:Hide()
local tabRaid = CreateFrame("Button", "SFramesConfigTabRaid", panel, "UIPanelButtonTemplate")
tabRaid:SetWidth(100)
@@ -3644,11 +4710,12 @@ function SFrames.ConfigUI:EnsureFrame()
tabRaid:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("raid") end)
StyleButton(tabRaid)
AddBtnIcon(tabRaid, "party", nil, "left")
+ tabRaid:Hide()
local tabBags = CreateFrame("Button", "SFramesConfigTabBags", panel, "UIPanelButtonTemplate")
tabBags:SetWidth(100)
tabBags:SetHeight(28)
- tabBags:SetPoint("TOP", tabRaid, "BOTTOM", 0, -4)
+ tabBags:SetPoint("TOP", tabFrames, "BOTTOM", 0, -4)
tabBags:SetText("背包设置")
tabBags:SetScript("OnClick", function() SFrames.ConfigUI:ShowPage("bags") end)
StyleButton(tabBags)
@@ -3734,23 +4801,75 @@ function SFrames.ConfigUI:EnsureFrame()
local uiPage = CreateFrame("Frame", "SFramesConfigUIPage", content)
uiPage:SetAllPoints(content)
- local playerPage = CreateFrame("Frame", "SFramesConfigPlayerPage", content)
- playerPage:SetAllPoints(content)
+ local framesPage = CreateFrame("Frame", "SFramesConfigFramesPage", content)
+ framesPage:SetAllPoints(content)
- local targetPage = CreateFrame("Frame", "SFramesConfigTargetPage", content)
- targetPage:SetAllPoints(content)
+ local framesTabs = CreateFrame("Frame", "SFramesConfigFramesTabs", framesPage)
+ framesTabs:SetWidth(548)
+ framesTabs:SetHeight(30)
+ framesTabs:SetPoint("TOPLEFT", framesPage, "TOPLEFT", 4, -4)
- local focusPage = CreateFrame("Frame", "SFramesConfigFocusPage", content)
- focusPage:SetAllPoints(content)
+ local framesPlayerTab = CreateFrame("Button", "SFramesConfigFramesPlayerTab", framesTabs, "UIPanelButtonTemplate")
+ framesPlayerTab:SetWidth(96)
+ framesPlayerTab:SetHeight(24)
+ framesPlayerTab:SetPoint("TOPLEFT", framesTabs, "TOPLEFT", 0, 0)
+ framesPlayerTab:SetText("玩家")
+ framesPlayerTab:SetScript("OnClick", function() SFrames.ConfigUI:ShowFramesSubPage("player") end)
+ StyleButton(framesPlayerTab)
+
+ local framesTargetTab = CreateFrame("Button", "SFramesConfigFramesTargetTab", framesTabs, "UIPanelButtonTemplate")
+ framesTargetTab:SetWidth(96)
+ framesTargetTab:SetHeight(24)
+ framesTargetTab:SetPoint("LEFT", framesPlayerTab, "RIGHT", 6, 0)
+ framesTargetTab:SetText("目标")
+ framesTargetTab:SetScript("OnClick", function() SFrames.ConfigUI:ShowFramesSubPage("target") end)
+ StyleButton(framesTargetTab)
+
+ local framesFocusTab = CreateFrame("Button", "SFramesConfigFramesFocusTab", framesTabs, "UIPanelButtonTemplate")
+ framesFocusTab:SetWidth(96)
+ framesFocusTab:SetHeight(24)
+ framesFocusTab:SetPoint("LEFT", framesTargetTab, "RIGHT", 6, 0)
+ framesFocusTab:SetText("焦点")
+ framesFocusTab:SetScript("OnClick", function() SFrames.ConfigUI:ShowFramesSubPage("focus") end)
+ StyleButton(framesFocusTab)
+
+ local framesPartyTab = CreateFrame("Button", "SFramesConfigFramesPartyTab", framesTabs, "UIPanelButtonTemplate")
+ framesPartyTab:SetWidth(96)
+ framesPartyTab:SetHeight(24)
+ framesPartyTab:SetPoint("LEFT", framesFocusTab, "RIGHT", 6, 0)
+ framesPartyTab:SetText("小队")
+ framesPartyTab:SetScript("OnClick", function() SFrames.ConfigUI:ShowFramesSubPage("party") end)
+ StyleButton(framesPartyTab)
+
+ local framesRaidTab = CreateFrame("Button", "SFramesConfigFramesRaidTab", framesTabs, "UIPanelButtonTemplate")
+ framesRaidTab:SetWidth(96)
+ framesRaidTab:SetHeight(24)
+ framesRaidTab:SetPoint("LEFT", framesPartyTab, "RIGHT", 6, 0)
+ framesRaidTab:SetText("团队")
+ framesRaidTab:SetScript("OnClick", function() SFrames.ConfigUI:ShowFramesSubPage("raid") end)
+ StyleButton(framesRaidTab)
+
+ local framesContent = CreateFrame("Frame", "SFramesConfigFramesContent", framesPage)
+ framesContent:SetPoint("TOPLEFT", framesTabs, "BOTTOMLEFT", 0, -6)
+ framesContent:SetPoint("BOTTOMRIGHT", framesPage, "BOTTOMRIGHT", 0, 0)
+
+ local playerPage = CreateFrame("Frame", "SFramesConfigPlayerPage", framesContent)
+ playerPage:SetAllPoints(framesContent)
+
+ local targetPage = CreateFrame("Frame", "SFramesConfigTargetPage", framesContent)
+ targetPage:SetAllPoints(framesContent)
+
+ local focusPage = CreateFrame("Frame", "SFramesConfigFocusPage", framesContent)
+ focusPage:SetAllPoints(framesContent)
local bagPage = CreateFrame("Frame", "SFramesConfigBagPage", content)
bagPage:SetAllPoints(content)
- local raidPage = CreateFrame("Frame", "SFramesConfigRaidPage", content)
- raidPage:SetAllPoints(content)
+ local raidPage = CreateFrame("Frame", "SFramesConfigRaidPage", framesContent)
+ raidPage:SetAllPoints(framesContent)
- local partyPage = CreateFrame("Frame", "SFramesConfigPartyPage", content)
- partyPage:SetAllPoints(content)
+ local partyPage = CreateFrame("Frame", "SFramesConfigPartyPage", framesContent)
+ partyPage:SetAllPoints(framesContent)
local charPage = CreateFrame("Frame", "SFramesConfigCharPage", content)
charPage:SetAllPoints(content)
@@ -3779,6 +4898,7 @@ function SFrames.ConfigUI:EnsureFrame()
self.frame = panel
self.title = title
self.uiTab = tabUI
+ self.framesTab = tabFrames
self.playerTab = tabPlayer
self.targetTab = tabTarget
self.focusTab = tabFocus
@@ -3795,6 +4915,7 @@ function SFrames.ConfigUI:EnsureFrame()
self.profileTab = tabProfile
self.content = content
self.uiPage = uiPage
+ self.framesPage = framesPage
self.playerPage = playerPage
self.targetPage = targetPage
self.focusPage = focusPage
@@ -3809,6 +4930,12 @@ function SFrames.ConfigUI:EnsureFrame()
self.personalizePage = personalizePage
self.themePage = themePage
self.profilePage = profilePage
+ self.framesPlayerTab = framesPlayerTab
+ self.framesTargetTab = framesTargetTab
+ self.framesFocusTab = framesFocusTab
+ self.framesPartyTab = framesPartyTab
+ self.framesRaidTab = framesRaidTab
+ self.activeFrameSubPage = "player"
local btnSaveReload = CreateFrame("Button", "SFramesConfigSaveReloadBtn", panel, "UIPanelButtonTemplate")
btnSaveReload:SetWidth(100)
@@ -3836,47 +4963,11 @@ function SFrames.ConfigUI:BuildPersonalizePage()
local page = self.personalizePage
local controls = {}
- local personalizeScroll = CreateScrollArea(page, 4, -4, 548, 458, 960)
+ local personalizeScroll = CreateScrollArea(page, 4, -4, 548, 458, 950)
local root = personalizeScroll.child
- -- ── 升级训练师提醒 ──────────────────────────────────────────
- local trainerSection = CreateSection(root, "升级技能提醒", 8, -8, 520, 100, font)
-
- table.insert(controls, CreateCheckBox(trainerSection,
- "升级时提醒可学习的新技能", 14, -34,
- function() return SFramesDB.trainerReminder ~= false end,
- function(checked) SFramesDB.trainerReminder = checked end
- ))
- CreateDesc(trainerSection, "升级后如果有新的职业技能可学习,将在屏幕和聊天窗口显示技能列表提醒", 36, -50, font, 340)
-
- CreateButton(trainerSection, "模拟提醒", 370, -34, 120, 22, function()
- if SFrames.Player and SFrames.Player.ShowTrainerReminder then
- local testLevel = UnitLevel("player")
- if mod(testLevel, 2) ~= 0 then testLevel = testLevel + 1 end
- local _, classEn = UnitClass("player")
- local cachedSkills = SFramesDB and SFramesDB.trainerCache and SFramesDB.trainerCache[classEn]
- local staticSkills = SFrames.ClassSkillData and SFrames.ClassSkillData[classEn]
- local function hasData(lv)
- if cachedSkills then
- if cachedSkills[lv] then return true end
- if lv > 1 and cachedSkills[lv - 1] then return true end
- end
- if staticSkills and staticSkills[lv] then return true end
- return false
- end
- if not hasData(testLevel) then
- while testLevel <= 60 and not hasData(testLevel) do
- testLevel = testLevel + 2
- end
- if testLevel > 60 then testLevel = 60 end
- end
- SFrames.Player:ShowTrainerReminder(testLevel)
- end
- end)
- CreateDesc(trainerSection, "点击按钮预览升级提醒效果", 392, -55, font, 100)
-
-- ── 拾取窗口 ────────────────────────────────────────────────
- local lootOptSection = CreateSection(root, "拾取窗口", 8, -118, 520, 56, font)
+ local lootOptSection = CreateSection(root, "拾取窗口", 8, -8, 520, 56, font)
table.insert(controls, CreateCheckBox(lootOptSection,
"拾取窗口跟随鼠标", 14, -30,
@@ -3892,7 +4983,7 @@ function SFrames.ConfigUI:BuildPersonalizePage()
CreateDesc(lootOptSection, "勾选后拾取窗口显示在鼠标附近,否则显示在固定位置", 36, -46, font)
-- ── 鼠标提示框 ────────────────────────────────────────────────
- local tooltipSection = CreateSection(root, "鼠标提示框", 8, -184, 520, 180, font)
+ local tooltipSection = CreateSection(root, "鼠标提示框", 8, -74, 520, 180, font)
CreateDesc(tooltipSection, "选择游戏提示框的显示位置方式(三选一)", 14, -28, font)
local function RefreshTooltipMode()
@@ -3944,7 +5035,7 @@ function SFrames.ConfigUI:BuildPersonalizePage()
-- ── AFK 待机动画 ──────────────────────────────────────────────
-- ── 血条材质选择 ──────────────────────────────────────────
- local barTexSection = CreateSection(root, "血条材质", 8, -374, 520, 120, font)
+ local barTexSection = CreateSection(root, "血条材质", 8, -264, 520, 120, font)
CreateDesc(barTexSection, "选择单位框架中血条和能量条使用的材质。修改后需要 /reload 生效。", 14, -30, font, 490)
@@ -4042,7 +5133,118 @@ function SFrames.ConfigUI:BuildPersonalizePage()
end
end
- local afkSection = CreateSection(root, "AFK 待机动画", 8, -504, 520, 146, font)
+ -- ── 框体样式预设 ──────────────────────────────────────────
+ local styleSection = CreateSection(root, "框体样式", 8, -394, 520, 90, font)
+
+ CreateDesc(styleSection, "切换单位框体的整体外观风格。即时生效,无需 /reload。", 14, -30, font, 490)
+
+ local STYLE_PRESETS = {
+ { key = "classic", label = "经典(默认)" },
+ { key = "gradient", label = "简约渐变" },
+ }
+
+ local styleBtnW = 120
+ local styleBtnH = 28
+ local styleBtnGap = 8
+ local styleBtnStartX = 14
+ local styleBtnStartY = -52
+ local styleBtns = {}
+
+ for idx, preset in ipairs(STYLE_PRESETS) do
+ local x = styleBtnStartX + (idx - 1) * (styleBtnW + styleBtnGap)
+ local btn = CreateFrame("Button", NextWidgetName("StylePreset"), styleSection)
+ btn:SetWidth(styleBtnW)
+ btn:SetHeight(styleBtnH)
+ btn:SetPoint("TOPLEFT", styleSection, "TOPLEFT", x, styleBtnStartY)
+
+ btn:SetBackdrop({
+ bgFile = "Interface\\Buttons\\WHITE8X8",
+ edgeFile = "Interface\\Buttons\\WHITE8X8",
+ tile = false, edgeSize = 1,
+ insets = { left = 1, right = 1, top = 1, bottom = 1 },
+ })
+ btn:SetBackdropColor(0.08, 0.08, 0.10, 0.9)
+ btn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
+
+ local label = btn:CreateFontString(nil, "OVERLAY")
+ label:SetFont(font, 10, "OUTLINE")
+ label:SetPoint("CENTER", btn, "CENTER", 0, 0)
+ label:SetText(preset.label)
+ label:SetTextColor(0.85, 0.85, 0.85)
+
+ btn.styleKey = preset.key
+ btn.styleLabel = preset.label
+
+ btn:SetScript("OnClick", function()
+ if not SFramesDB then SFramesDB = {} end
+ SFramesDB.frameStylePreset = this.styleKey
+ -- Gradient mode: default enable rainbow bar
+ if this.styleKey == "gradient" then
+ SFramesDB.powerRainbow = true
+ end
+ -- Update button highlights
+ for _, b in ipairs(styleBtns) do
+ if b.styleKey == this.styleKey then
+ b:SetBackdropBorderColor(1, 0.85, 0.6, 1)
+ else
+ b:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
+ end
+ end
+ -- Apply to all unit frames immediately
+ if SFrames.Player and SFrames.Player.ApplyConfig then
+ local ok, err = pcall(function() SFrames.Player:ApplyConfig() end)
+ if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Player style error: " .. tostring(err) .. "|r") end
+ end
+ if SFrames.Target and SFrames.Target.ApplyConfig then
+ local ok, err = pcall(function() SFrames.Target:ApplyConfig() end)
+ if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Target style error: " .. tostring(err) .. "|r") end
+ end
+ if SFrames.Party and SFrames.Party.ApplyConfig then
+ local ok, err = pcall(function() SFrames.Party:ApplyConfig() end)
+ if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Party style error: " .. tostring(err) .. "|r") end
+ end
+ if SFrames.Pet and SFrames.Pet.ApplyConfig then
+ local ok, err = pcall(function() SFrames.Pet:ApplyConfig() end)
+ if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Pet style error: " .. tostring(err) .. "|r") end
+ end
+ if SFrames.Raid and SFrames.Raid.ApplyConfig then
+ local ok, err = pcall(function() SFrames.Raid:ApplyConfig() end)
+ if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Raid style error: " .. tostring(err) .. "|r") end
+ end
+ if SFrames.ToT and SFrames.ToT.ApplyConfig then
+ local ok, err = pcall(function() SFrames.ToT:ApplyConfig() end)
+ if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] ToT style error: " .. tostring(err) .. "|r") end
+ end
+ if SFrames.Focus and SFrames.Focus.ApplySettings then
+ local ok, err = pcall(function() SFrames.Focus:ApplySettings() end)
+ if not ok then DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Focus style error: " .. tostring(err) .. "|r") end
+ end
+ SFrames:Print("框体样式已切换为: " .. this.styleLabel)
+ end)
+ btn:SetScript("OnEnter", function()
+ this:SetBackdropColor(0.15, 0.12, 0.18, 0.95)
+ end)
+ btn:SetScript("OnLeave", function()
+ this:SetBackdropColor(0.08, 0.08, 0.10, 0.9)
+ local cur = (SFramesDB and SFramesDB.frameStylePreset) or "classic"
+ if this.styleKey == cur then
+ this:SetBackdropBorderColor(1, 0.85, 0.6, 1)
+ else
+ this:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
+ end
+ end)
+
+ table.insert(styleBtns, btn)
+ end
+
+ local curStyle = (SFramesDB and SFramesDB.frameStylePreset) or "classic"
+ for _, b in ipairs(styleBtns) do
+ if b.styleKey == curStyle then
+ b:SetBackdropBorderColor(1, 0.85, 0.6, 1)
+ end
+ end
+
+ local afkSection = CreateSection(root, "AFK 待机动画", 8, -494, 520, 146, font)
table.insert(controls, CreateCheckBox(afkSection,
"启用 AFK 待机画面", 14, -34,
@@ -4074,7 +5276,7 @@ function SFrames.ConfigUI:BuildPersonalizePage()
end)
-- ── 字体选择 ──────────────────────────────────────────────────
- local fontSection = CreateSection(root, "全局字体", 8, -660, 520, 160, font)
+ local fontSection = CreateSection(root, "全局字体", 8, -650, 520, 160, font)
CreateDesc(fontSection, "选择 UI 全局使用的字体(聊天、框体、系统文字等),需 /reload 完全生效", 14, -28, font, 480)
local FONT_BTN_W = 150
@@ -4146,6 +5348,22 @@ function SFrames.ConfigUI:BuildPersonalizePage()
CreateLabel(fontSection, "提示:切换字体后需要 /reload 才能完全生效。", 14, -136, font, 10, 0.6, 0.6, 0.65)
+ -- ── 彩虹能量条 ──────────────────────────────────────────────────
+ local rainbowSection = CreateSection(root, "彩虹能量条", 8, -820, 520, 70, font)
+ table.insert(controls, CreateCheckBox(rainbowSection,
+ "启用彩虹色能量条", 14, -30,
+ function() return SFramesDB.powerRainbow == true end,
+ function(checked)
+ SFramesDB.powerRainbow = checked
+ if SFrames.Player then SFrames.Player:UpdatePower() end
+ if SFrames.Target then SFrames.Target:UpdatePower() end
+ if SFrames.Focus then SFrames.Focus:UpdatePower() end
+ if SFrames.Party then SFrames.Party:UpdateAll() end
+ if SFrames.Raid then SFrames.Raid:UpdateAll() end
+ end
+ ))
+ CreateDesc(rainbowSection, "所有框体(玩家/目标/焦点/队伍/团队)的能量条均使用彩虹材质", 36, -46, font)
+
personalizeScroll:UpdateRange()
self.personalizeControls = controls
self.personalizeScroll = personalizeScroll
@@ -4909,6 +6127,7 @@ local function BuildSummaryText()
table.insert(lines, "高度: " .. tostring(chat.height or "N/A"))
table.insert(lines, "缩放: " .. tostring(chat.scale or 1))
table.insert(lines, "背景透明度: " .. tostring(chat.bgAlpha or 0.45))
+ table.insert(lines, "悬停显示背景: " .. BoolStr(chat.hoverTransparent ~= false))
table.insert(lines, "边框: " .. BoolStr(chat.showBorder))
table.insert(lines, "职业色边框: " .. BoolStr(chat.borderClassColor))
table.insert(lines, "显示玩家等级: " .. BoolStr(chat.showPlayerLevel ~= false))
@@ -5012,7 +6231,6 @@ local function BuildSummaryText()
table.insert(lines, "")
table.insert(lines, "|cffffcc00== 个性化 ==|r")
- if db.trainerReminder ~= nil then table.insert(lines, "训练师提醒: " .. BoolStr(db.trainerReminder ~= false)) end
if db.Tweaks then
table.insert(lines, "微调项数: " .. CountTableKeys(db.Tweaks))
end
@@ -5232,18 +6450,18 @@ end
--------------------------------------------------------------------------------
local APPLY_CATEGORIES = {
- { key = "ui", label = "界面功能开关", keys = {"enableMerchant","enableQuestUI","enableQuestLogSkin","enableTrainer","enableSpellBook","enableTradeSkill","enableSocial","enableInspect","enableFlightMap","enablePetStable","enableMail","enableUnitFrames","enablePlayerFrame","enableTargetFrame","enablePartyFrame","enableChat","showLevel","classColorHealth","smoothBars","fontKey"} },
- { key = "player", label = "玩家框体", keys = {"playerFrameScale","playerFrameWidth","playerPortraitWidth","playerHealthHeight","playerPowerHeight","playerShowClass","playerShowClassIcon","playerShowPortrait","playerFrameAlpha","playerBgAlpha","playerNameFontSize","playerValueFontSize","castbarStandalone","castbarRainbow"} },
- { key = "target", label = "目标框体", keys = {"targetFrameScale","targetFrameWidth","targetPortraitWidth","targetHealthHeight","targetPowerHeight","targetShowClass","targetShowClassIcon","targetShowPortrait","targetFrameAlpha","targetBgAlpha","targetNameFontSize","targetValueFontSize","targetDistanceEnabled","targetDistanceScale","targetDistanceFontSize"} },
- { key = "focus", label = "焦点框体", keys = {"focusEnabled","focusFrameScale","focusFrameWidth","focusPortraitWidth","focusHealthHeight","focusPowerHeight","focusShowPortrait","focusShowCastBar","focusShowAuras","focusNameFontSize","focusValueFontSize","focusBgAlpha"} },
- { key = "party", label = "小队框架", keys = {"partyLayout","partyFrameScale","partyFrameWidth","partyFrameHeight","partyPortraitWidth","partyHealthHeight","partyPowerHeight","partyHorizontalGap","partyVerticalGap","partyNameFontSize","partyValueFontSize","partyShowBuffs","partyShowDebuffs","partyBgAlpha"} },
- { key = "raid", label = "团队框架", keys = {"raidLayout","raidFrameScale","raidFrameWidth","raidFrameHeight","raidHealthHeight","raidHorizontalGap","raidVerticalGap","raidGroupGap","raidNameFontSize","raidValueFontSize","raidShowPower","enableRaidFrames","raidHealthFormat","raidShowGroupLabel","raidBgAlpha"} },
+ { key = "ui", label = "界面功能开关", keys = {"enableMerchant","enableQuestUI","enableQuestLogSkin","enableTrainer","enableSpellBook","enableTradeSkill","enableSocial","enableInspect","enableFlightMap","enablePetStable","enableMail","enableUnitFrames","enablePlayerFrame","enableTargetFrame","enablePartyFrame","enableChat","showLevel","classColorHealth","smoothBars","fontKey","frameStylePreset"} },
+ { key = "player", label = "玩家框体", keys = {"playerFrameScale","playerFrameWidth","playerPortraitWidth","playerHealthHeight","playerPowerHeight","playerPowerWidth","playerPowerOffsetX","playerPowerOffsetY","playerPowerOnTop","playerPortraitBgAlpha","playerShowClass","playerShowClassIcon","playerShowPortrait","playerFrameAlpha","playerBgAlpha","playerNameFontSize","playerValueFontSize","playerHealthFontSize","playerPowerFontSize","playerHealthTexture","playerPowerTexture","playerNameFontKey","playerHealthFontKey","playerPowerFontKey","castbarStandalone","castbarRainbow","powerRainbow","petHealthTexture","petPowerTexture"} },
+ { key = "target", label = "目标框体", keys = {"targetFrameScale","targetFrameWidth","targetPortraitWidth","targetHealthHeight","targetPowerHeight","targetPowerWidth","targetPowerOffsetX","targetPowerOffsetY","targetPowerOnTop","targetPortraitBgAlpha","targetShowClass","targetShowClassIcon","targetShowPortrait","targetFrameAlpha","targetBgAlpha","targetNameFontSize","targetValueFontSize","targetHealthFontSize","targetPowerFontSize","targetHealthTexture","targetPowerTexture","targetNameFontKey","targetHealthFontKey","targetPowerFontKey","targetDistanceEnabled","targetDistanceOnFrame","targetDistanceScale","targetDistanceFontSize","targetDistanceFontKey","totHealthTexture"} },
+ { key = "focus", label = "焦点框体", keys = {"focusEnabled","focusFrameScale","focusFrameWidth","focusPortraitWidth","focusHealthHeight","focusPowerHeight","focusPowerWidth","focusPowerOffsetX","focusPowerOffsetY","focusPowerOnTop","focusPortraitBgAlpha","focusShowPortrait","focusShowCastBar","focusShowAuras","focusNameFontSize","focusValueFontSize","focusHealthFontSize","focusPowerFontSize","focusCastFontSize","focusDistanceFontSize","focusHealthTexture","focusPowerTexture","focusNameFontKey","focusHealthFontKey","focusPowerFontKey","focusCastFontKey","focusDistanceFontKey","focusBgAlpha"} },
+ { key = "party", label = "小队框架", keys = {"partyLayout","partyFrameScale","partyFrameWidth","partyFrameHeight","partyPortraitWidth","partyHealthHeight","partyPowerHeight","partyPowerWidth","partyPowerOffsetX","partyPowerOffsetY","partyPowerOnTop","partyPortraitBgAlpha","partyHorizontalGap","partyVerticalGap","partyNameFontSize","partyValueFontSize","partyHealthFontSize","partyPowerFontSize","partyHealthTexture","partyPowerTexture","partyNameFontKey","partyHealthFontKey","partyPowerFontKey","partyShowBuffs","partyShowDebuffs","partyBgAlpha","partyPortrait3D"} },
+ { key = "raid", label = "团队框架", keys = {"raidLayout","raidFrameScale","raidFrameWidth","raidFrameHeight","raidHealthHeight","raidPowerWidth","raidPowerHeight","raidPowerOffsetX","raidPowerOffsetY","raidPowerOnTop","raidHorizontalGap","raidVerticalGap","raidGroupGap","raidNameFontSize","raidValueFontSize","raidHealthFontSize","raidPowerFontSize","raidHealthTexture","raidPowerTexture","raidNameFontKey","raidHealthFontKey","raidPowerFontKey","raidShowPower","enableRaidFrames","raidHealthFormat","raidShowGroupLabel","raidBgAlpha"} },
{ key = "bags", label = "背包设置", table_key = "Bags" },
{ key = "actionbar",label = "动作条设置", table_key = "ActionBars" },
{ key = "chat_general", label = "聊天-通用",
chat_keys = {"enable","showBorder","borderClassColor","showPlayerLevel","chatMonitorEnabled","layoutVersion"} },
{ key = "chat_window", label = "聊天-窗口",
- chat_keys = {"width","height","scale","fontSize","bgAlpha","sidePadding","topPadding","bottomPadding","editBoxPosition","editBoxX","editBoxY"} },
+ chat_keys = {"width","height","scale","fontSize","bgAlpha","hoverTransparent","sidePadding","topPadding","bottomPadding","editBoxPosition","editBoxX","editBoxY"} },
{ key = "chat_tabs", label = "聊天-标签/过滤",
chat_keys = {"tabs","activeTab","nextTabId"} },
{ key = "chat_translate",label = "聊天-AI翻译",
@@ -5734,6 +6952,7 @@ function SFrames.ConfigUI:EnsurePage(mode)
if self._pageBuilt[mode] then return end
self._pageBuilt[mode] = true
if mode == "ui" then self:BuildUIPage()
+ elseif mode == "frames" then return
elseif mode == "player" then self:BuildPlayerPage()
elseif mode == "target" then self:BuildTargetPage()
elseif mode == "focus" then self:BuildFocusPage()
@@ -5755,11 +6974,21 @@ function SFrames.ConfigUI:Build(mode)
EnsureDB()
self:EnsureFrame()
- local page = string.lower(mode or "ui")
- local validPages = { ui = true, player = true, target = true, focus = true, bags = true, char = true, party = true, raid = true, actionbar = true, keybinds = true, minimap = true, buff = true, personalize = true, theme = true, profile = true }
+ local requested = string.lower(mode or "")
+ if requested == "" or requested == "ui" then
+ requested = ((SFramesDB and SFramesDB.configLastPage) or "ui")
+ end
+
+ local page = string.lower(requested)
+ local validPages = { ui = true, frames = true, player = true, target = true, focus = true, bags = true, char = true, party = true, raid = true, actionbar = true, keybinds = true, minimap = true, buff = true, personalize = true, theme = true, profile = true }
if not validPages[page] then page = "ui" end
- if self.frame:IsShown() and self.activePage == page then
+ local activeBucket = page
+ if page == "player" or page == "target" or page == "focus" or page == "party" or page == "raid" then
+ activeBucket = "frames"
+ end
+
+ if self.frame:IsShown() and self.activePage == activeBucket and (activeBucket ~= "frames" or self.activeFrameSubPage == page or page == "frames") then
self.frame:Hide()
return
end
diff --git a/ConsumableDB.lua b/ConsumableDB.lua
new file mode 100644
index 0000000..7c46eb5
--- /dev/null
+++ b/ConsumableDB.lua
@@ -0,0 +1,198 @@
+--------------------------------------------------------------------------------
+-- Nanami-UI: ConsumableDB.lua
+-- 食物药剂百科数据库(由导出表生成,请优先更新 Excel 后再重新生成)
+-- Source: c:\Users\rucky\Downloads\WoW_Consumables_Database.xlsx
+-- Stats : 6 roles / 9 categories / 101 entries
+--------------------------------------------------------------------------------
+SFrames = SFrames or {}
+
+SFrames.ConsumableDB = {
+ generatedAt = "2026-04-03 23:45:41",
+ summary = {
+ roleCount = 6,
+ categoryCount = 9,
+ itemCount = 101,
+ },
+ roleOrder = {
+ "坦克 (物理坦克)",
+ "坦克 (法系坦克)",
+ "法系输出",
+ "物理近战",
+ "物理远程",
+ "治疗",
+ },
+ categoryOrder = {
+ "合剂",
+ "药剂",
+ "攻强",
+ "诅咒之地buff",
+ "赞达拉",
+ "武器",
+ "食物",
+ "酒",
+ "药水",
+ },
+ groups = {
+
+ -- 1. 坦克 · 物理坦克
+ {
+ key = "tank_physical",
+ role = "坦克 (物理坦克)",
+ detail = "坦克 · 物理坦克",
+ color = { 0.40, 0.70, 1.00 },
+ items = {
+ { cat="合剂", name="泰坦合剂", effect="1200生命上限", duration="2小时", id=0 },
+ { cat="药剂", name="坚韧药剂", effect="120生命上限", duration="1小时", id=0 },
+ { cat="药剂", name="极效巨魔之血药水", effect="20生命/5秒回血", duration="1小时", id=0 },
+ { cat="药剂", name="猫鼬药剂", effect="25敏捷 / 2%暴击", duration="1小时", id=13452 },
+ { cat="药剂", name="巨人药剂", effect="25力量", duration="1小时", id=9206 },
+ { cat="药剂", name="坚甲药剂", effect="450护甲", duration="1小时", id=0 },
+ { cat="攻强", name="魂能之击", effect="30力量", duration="10分钟", id=0 },
+ { cat="攻强", name="冬泉火酒", effect="35攻强", duration="20分钟", id=0 },
+ { cat="攻强", name="魂能之力", effect="40攻强", duration="10分钟", id=0 },
+ { cat="诅咒之地buff", name="肺片鸡尾酒", effect="50耐力", duration="1小时", id=0 },
+ { cat="诅咒之地buff", name="土狼肉块", effect="40力量", duration="1小时", id=0 },
+ { cat="赞达拉", name="赞扎之魂", effect="50耐力 / 50精神", duration="2小时", id=0 },
+ { cat="武器", name="元素磨刀石", effect="2%暴击", duration="30分钟", id=0 },
+ { cat="武器", name="致密磨刀石", effect="8伤害", duration="30分钟", id=0 },
+ { cat="食物", name="迪尔格的超美味奇美拉肉片", effect="25耐力", duration="15分钟", id=0 },
+ { cat="食物", name="嫩狼肉排", effect="12耐力 / 12精神", duration="15分钟", id=0 },
+ { cat="食物", name="蜘蛛肉肠", effect="12耐力 / 12精神", duration="15分钟", id=0 },
+ { cat="酒", name="戈多克绿酒", effect="10耐力", duration="15分钟", id=0 },
+ { cat="酒", name="黑标美味朗姆酒", effect="15耐力", duration="15分钟", id=0 },
+ { cat="药水", name="极效治疗药水", effect="1050-1750生命值", duration="瞬发", id=0 },
+ },
+ },
+
+ -- 2. 坦克 · 法系坦克
+ {
+ key = "tank_caster",
+ role = "坦克 (法系坦克)",
+ detail = "坦克 · 法系坦克",
+ color = { 1.00, 0.80, 0.20 },
+ items = {
+ { cat="合剂", name="泰坦合剂", effect="1200生命上限", duration="2小时", id=0 },
+ { cat="药剂", name="坚韧药剂", effect="120生命上限", duration="1小时", id=0 },
+ { cat="药剂", name="特效巨魔之血药水", effect="20生命/5秒回血", duration="1小时", id=0 },
+ { cat="药剂", name="强效奥法药剂", effect="35法术伤害", duration="1小时", id=0 },
+ { cat="药剂", name="先知药剂", effect="18智力 / 18精神", duration="1小时", id=0 },
+ { cat="药剂", name="魔血药水", effect="12法力/5秒", duration="1小时", id=0 },
+ { cat="药剂", name="坚甲药剂", effect="450护甲", duration="1小时", id=0 },
+ { cat="诅咒之地buff", name="肺片鸡尾酒", effect="50耐力", duration="1小时", id=0 },
+ { cat="诅咒之地buff", name="脑皮层混合饮料", effect="25智力", duration="1小时", id=0 },
+ { cat="赞达拉", name="赞扎之魂", effect="50耐力 / 50精神", duration="2小时", id=0 },
+ { cat="武器", name="巫师之油", effect="24法术伤害", duration="30分钟", id=0 },
+ { cat="武器", name="卓越巫师之油", effect="36法伤 / 1%暴击", duration="30分钟", id=0 },
+ { cat="食物", name="迪尔格的超美味奇美拉肉片", effect="25耐力", duration="15分钟", id=0 },
+ { cat="食物", name="嫩狼肉排", effect="12耐力 / 12精神", duration="15分钟", id=0 },
+ { cat="食物", name="龙息红椒", effect="攻击几率喷火", duration="10分钟", id=0 },
+ { cat="酒", name="戈多克绿酒", effect="10耐力", duration="15分钟", id=0 },
+ { cat="酒", name="黑标美味朗姆酒", effect="15耐力", duration="15分钟", id=0 },
+ { cat="药水", name="极效治疗药水", effect="1050-1750生命值", duration="瞬发", id=0 },
+ { cat="药水", name="极效法力药水", effect="1350-2250法力值", duration="瞬发", id=0 },
+ { cat="药水", name="有限无敌药水", effect="物理攻击免疫", duration="6秒", id=0 },
+ },
+ },
+
+ -- 3. 输出 · 法系输出
+ {
+ key = "caster_dps",
+ role = "法系输出",
+ detail = "输出 · 法系输出",
+ color = { 0.65, 0.45, 1.00 },
+ items = {
+ { cat="合剂", name="超级能量合剂", effect="150法术伤害", duration="2小时", id=0 },
+ { cat="药剂", name="强效奥法药剂", effect="35法术伤害", duration="1小时", id=0 },
+ { cat="药剂", name="强效火力药剂", effect="40火焰法术伤害", duration="1小时", id=0 },
+ { cat="药剂", name="暗影之力药剂", effect="40暗影法术伤害", duration="1小时", id=0 },
+ { cat="药剂", name="冰霜之力药剂", effect="15冰霜法术伤害", duration="1小时", id=0 },
+ { cat="药剂", name="先知药剂", effect="18智力 / 18精神", duration="1小时", id=0 },
+ { cat="药剂", name="魔血药水", effect="12法力/5秒", duration="1小时", id=0 },
+ { cat="诅咒之地buff", name="脑皮层混合饮料", effect="25智力", duration="1小时", id=0 },
+ { cat="赞达拉", name="赞扎之魂", effect="50耐力 / 50精神", duration="2小时", id=0 },
+ { cat="武器", name="卓越巫师之油", effect="36法伤 / 1%暴击", duration="30分钟", id=0 },
+ { cat="武器", name="巫师之油", effect="24法术伤害", duration="30分钟", id=0 },
+ { cat="食物", name="洛恩塔姆薯块", effect="10智力", duration="10分钟", id=0 },
+ { cat="食物", name="黑口鱼起司", effect="10法力/5秒", duration="10分钟", id=0 },
+ { cat="食物", name="夜鳞鱼汤", effect="8法力/5秒", duration="10分钟", id=13931 },
+ { cat="酒", name="戈多克绿酒", effect="10耐力", duration="15分钟", id=0 },
+ { cat="酒", name="黑标美味朗姆酒", effect="15耐力", duration="15分钟", id=0 },
+ { cat="药水", name="极效法力药水", effect="1350-2250法力值", duration="瞬发", id=0 },
+ },
+ },
+
+ -- 4. 输出 · 物理近战
+ {
+ key = "melee_dps",
+ role = "物理近战",
+ detail = "输出 · 物理近战",
+ color = { 1.00, 0.55, 0.25 },
+ items = {
+ { cat="合剂", name="泰坦合剂", effect="1200生命上限", duration="2小时", id=0 },
+ { cat="药剂", name="猫鼬药剂", effect="25敏捷 / 2%暴击", duration="1小时", id=13452 },
+ { cat="药剂", name="巨人药剂", effect="25力量", duration="1小时", id=9206 },
+ { cat="攻强", name="魂能之击", effect="30力量", duration="10分钟", id=0 },
+ { cat="攻强", name="冬泉火酒", effect="35攻强", duration="20分钟", id=0 },
+ { cat="攻强", name="魂能之力", effect="40攻强", duration="10分钟", id=0 },
+ { cat="诅咒之地buff", name="蝎粉", effect="25敏捷", duration="1小时", id=0 },
+ { cat="诅咒之地buff", name="土狼肉块", effect="40力量", duration="1小时", id=0 },
+ { cat="赞达拉", name="赞扎之魂", effect="50耐力 / 50精神", duration="2小时", id=0 },
+ { cat="武器", name="元素磨刀石", effect="2%暴击", duration="30分钟", id=0 },
+ { cat="武器", name="致密磨刀石", effect="8伤害", duration="30分钟", id=0 },
+ { cat="食物", name="烤鱿鱼", effect="10敏捷", duration="10分钟", id=13928 },
+ { cat="食物", name="沙漠肉丸子", effect="20力量", duration="15分钟", id=0 },
+ { cat="食物", name="迪尔格的超美味奇美拉肉片", effect="25耐力", duration="15分钟", id=0 },
+ { cat="酒", name="戈多克绿酒", effect="10耐力", duration="15分钟", id=0 },
+ { cat="酒", name="黑标美味朗姆酒", effect="15耐力", duration="15分钟", id=0 },
+ { cat="药水", name="极效治疗药水", effect="1050-1750生命值", duration="瞬发", id=0 },
+ { cat="药水", name="自由行动药水", effect="免疫昏迷及移动限制", duration="30秒", id=5634 },
+ },
+ },
+
+ -- 5. 输出 · 物理远程
+ {
+ key = "ranged_dps",
+ role = "物理远程",
+ detail = "输出 · 物理远程",
+ color = { 0.55, 0.88, 0.42 },
+ items = {
+ { cat="合剂", name="泰坦合剂", effect="1200生命上限", duration="2小时", id=0 },
+ { cat="药剂", name="猫鼬药剂", effect="25敏捷 / 2%暴击", duration="1小时", id=13452 },
+ { cat="药剂", name="魔血药水", effect="12法力/5秒", duration="1小时", id=0 },
+ { cat="药剂", name="先知药剂", effect="18智力 / 18精神", duration="1小时", id=0 },
+ { cat="诅咒之地buff", name="蝎粉", effect="25敏捷", duration="1小时", id=0 },
+ { cat="诅咒之地buff", name="脑皮层混合饮料", effect="25智力", duration="1小时", id=0 },
+ { cat="赞达拉", name="赞扎之魂", effect="50耐力 / 50精神", duration="2小时", id=0 },
+ { cat="武器", name="卓越法力之油", effect="14法力/5秒", duration="30分钟", id=0 },
+ { cat="食物", name="烤鱿鱼", effect="10敏捷", duration="10分钟", id=13928 },
+ { cat="食物", name="洛恩塔姆薯块", effect="10智力", duration="10分钟", id=0 },
+ { cat="酒", name="戈多克绿酒", effect="10耐力", duration="15分钟", id=0 },
+ { cat="酒", name="黑标美味朗姆酒", effect="15耐力", duration="15分钟", id=0 },
+ { cat="药水", name="极效法力药水", effect="1350-2250法力值", duration="瞬发", id=0 },
+ },
+ },
+
+ -- 6. 治疗
+ {
+ key = "healer",
+ role = "治疗",
+ detail = "治疗",
+ color = { 0.42, 1.00, 0.72 },
+ items = {
+ { cat="合剂", name="精炼智慧合剂", effect="2000法力上限", duration="2小时", id=13511 },
+ { cat="药剂", name="先知药剂", effect="18智力 / 18精神", duration="1小时", id=0 },
+ { cat="药剂", name="魔血药水", effect="12法力/5秒", duration="1小时", id=0 },
+ { cat="诅咒之地buff", name="脑皮层混合饮料", effect="25智力", duration="1小时", id=0 },
+ { cat="赞达拉", name="赞扎之魂", effect="50耐力 / 50精神", duration="2小时", id=0 },
+ { cat="武器", name="卓越法力之油", effect="14法力/5秒 / 提升治疗效果", duration="30分钟", id=0 },
+ { cat="食物", name="洛恩塔姆薯块", effect="10智力", duration="10分钟", id=0 },
+ { cat="食物", name="夜鳞鱼汤", effect="8法力/5秒", duration="10分钟", id=13931 },
+ { cat="酒", name="戈多克绿酒", effect="10耐力", duration="15分钟", id=0 },
+ { cat="酒", name="黑标美味朗姆酒", effect="15耐力", duration="15分钟", id=0 },
+ { cat="药水", name="极效法力药水", effect="1350-2250法力值", duration="瞬发", id=0 },
+ { cat="药水", name="黑暗符文", effect="回复900-1500法力", duration="瞬发", id=0 },
+ { cat="药水", name="恶魔符文", effect="回复900-1500法力", duration="瞬发", id=0 },
+ },
+ },
+ },
+}
diff --git a/ConsumableUI.lua b/ConsumableUI.lua
new file mode 100644
index 0000000..7da78df
--- /dev/null
+++ b/ConsumableUI.lua
@@ -0,0 +1,778 @@
+--------------------------------------------------------------------------------
+-- Nanami-UI: ConsumableUI.lua
+-- 食物药剂百科窗口
+--------------------------------------------------------------------------------
+SFrames = SFrames or {}
+SFrames.ConsumableUI = SFrames.ConsumableUI or {}
+
+local CUI = SFrames.ConsumableUI
+
+local FRAME_W = 600
+local FRAME_H = 500
+local HEADER_H = 34
+local FILTER_H = 28
+local ROLE_H = 28
+local COL_H = 22
+local ROW_H = 20
+local MAX_ROWS = 18
+local SIDE_PAD = 10
+
+local COL_CAT_W = 86
+local COL_NAME_W = 168
+local COL_EFF_W = 232
+local COL_DUR_W = 76
+
+local S = {
+ frame = nil,
+ searchBox = nil,
+ searchText = "",
+ activeRole = "全部",
+ rows = {},
+ tabBtns = {},
+ displayList = {},
+ filteredCount = 0,
+ scrollOffset = 0,
+ scrollMax = 0,
+ summaryFS = nil,
+ emptyFS = nil,
+ scrollBar = nil,
+}
+
+local function GetTheme()
+ local base = {
+ panelBg = { 0.07, 0.07, 0.10, 0.96 },
+ panelBorder = { 0.35, 0.30, 0.40, 1.00 },
+ headerBg = { 0.10, 0.10, 0.14, 1.00 },
+ text = { 0.92, 0.88, 0.95 },
+ dimText = { 0.55, 0.50, 0.58 },
+ gold = { 1.00, 0.82, 0.40 },
+ slotBg = { 0.10, 0.10, 0.14, 0.90 },
+ slotBorder = { 0.30, 0.28, 0.35, 0.80 },
+ slotSelected = { 0.80, 0.60, 1.00 },
+ divider = { 0.28, 0.26, 0.32, 0.80 },
+ rowAlt = { 0.11, 0.10, 0.15, 0.60 },
+ green = { 0.40, 0.90, 0.50 },
+ }
+
+ if SFrames and SFrames.ActiveTheme then
+ for key, value in pairs(SFrames.ActiveTheme) do
+ base[key] = value
+ end
+ end
+
+ return base
+end
+
+local function GetFont()
+ return SFrames and SFrames.GetFont and SFrames:GetFont()
+ or "Fonts\\ARIALN.TTF"
+end
+
+local function ApplyBackdrop(frame, bg, border)
+ local theme = GetTheme()
+ local backdropBg = bg or theme.panelBg
+ local backdropBorder = border or theme.panelBorder
+
+ 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(backdropBg[1], backdropBg[2], backdropBg[3], backdropBg[4] or 0.96)
+ frame:SetBackdropBorderColor(
+ backdropBorder[1],
+ backdropBorder[2],
+ backdropBorder[3],
+ backdropBorder[4] or 1.00
+ )
+end
+
+local function SetDivider(parent, y, rightPadding)
+ local theme = GetTheme()
+ local divider = parent:CreateTexture(nil, "ARTWORK")
+ divider:SetTexture("Interface\\Buttons\\WHITE8X8")
+ divider:SetHeight(1)
+ divider:SetPoint("TOPLEFT", parent, "TOPLEFT", SIDE_PAD, y)
+ divider:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -(SIDE_PAD + (rightPadding or 0)), y)
+ divider:SetVertexColor(theme.divider[1], theme.divider[2], theme.divider[3], theme.divider[4] or 0.8)
+end
+
+local function Utf8DisplayWidth(value)
+ local width = 0
+ local index = 1
+ local len = string.len(value or "")
+ while index <= len do
+ local byte = string.byte(value, index)
+ if byte < 128 then
+ width = width + 7
+ index = index + 1
+ elseif byte < 224 then
+ width = width + 7
+ index = index + 2
+ elseif byte < 240 then
+ width = width + 11
+ index = index + 3
+ else
+ width = width + 11
+ index = index + 4
+ end
+ end
+ return width
+end
+
+local function GetDatabase()
+ local db = SFrames and SFrames.ConsumableDB
+ if not db then
+ return nil
+ end
+
+ if db.groups then
+ return db
+ end
+
+ return {
+ groups = db,
+ roleOrder = nil,
+ categoryOrder = nil,
+ summary = nil,
+ generatedAt = nil,
+ }
+end
+
+local function GetGroups()
+ local db = GetDatabase()
+ return db and db.groups or nil
+end
+
+local function CountAllItems(groups)
+ local count = 0
+ for _, group in ipairs(groups or {}) do
+ count = count + table.getn(group.items or {})
+ end
+ return count
+end
+
+local function CountAllCategories(groups)
+ local seen = {}
+ local count = 0
+ for _, group in ipairs(groups or {}) do
+ for _, item in ipairs(group.items or {}) do
+ if item.cat and item.cat ~= "" and not seen[item.cat] then
+ seen[item.cat] = true
+ count = count + 1
+ end
+ end
+ end
+ return count
+end
+
+local function BuildRoleList()
+ local db = GetDatabase()
+ local roles = { "全部" }
+ local seen = { ["全部"] = true }
+
+ if not db or not db.groups then
+ return roles
+ end
+
+ if db.roleOrder then
+ for _, role in ipairs(db.roleOrder) do
+ if role and role ~= "" and not seen[role] then
+ seen[role] = true
+ table.insert(roles, role)
+ end
+ end
+ end
+
+ for _, group in ipairs(db.groups) do
+ if group.role and group.role ~= "" and not seen[group.role] then
+ seen[group.role] = true
+ table.insert(roles, group.role)
+ end
+ end
+
+ return roles
+end
+
+local function RoleMatches(group)
+ if S.activeRole == "全部" then
+ return true
+ end
+ return group.role == S.activeRole
+end
+
+local function ItemMatches(item)
+ if S.searchText == "" then
+ return true
+ end
+
+ local query = string.lower(S.searchText)
+ return string.find(string.lower(item.name or ""), query, 1, true)
+ or string.find(string.lower(item.effect or ""), query, 1, true)
+ or string.find(string.lower(item.cat or ""), query, 1, true)
+end
+
+function CUI:RefreshSummary()
+ if not S.summaryFS then
+ return
+ end
+
+ local db = GetDatabase()
+ local groups = db and db.groups or {}
+ local summary = db and db.summary or nil
+ local roleCount = (summary and summary.roleCount) or table.getn(groups)
+ local categoryCount = (summary and summary.categoryCount) or CountAllCategories(groups)
+ local totalCount = (summary and summary.itemCount) or CountAllItems(groups)
+
+ local text = string.format("%d 定位 / %d 类别 / %d 条", roleCount, categoryCount, totalCount)
+ if S.activeRole ~= "全部" or S.searchText ~= "" then
+ text = string.format("当前筛出 %d 条,数据库共 %d 条", S.filteredCount or 0, totalCount)
+ end
+
+ if db and db.generatedAt then
+ text = text .. " · 更新于 " .. db.generatedAt
+ end
+
+ S.summaryFS:SetText(text)
+end
+
+function CUI:Filter()
+ S.displayList = {}
+ S.filteredCount = 0
+
+ local groups = GetGroups()
+ if not groups then
+ S.scrollOffset = 0
+ S.scrollMax = 0
+ self:RefreshSummary()
+ return
+ end
+
+ for _, group in ipairs(groups) do
+ if RoleMatches(group) then
+ local matched = {}
+ for _, item in ipairs(group.items or {}) do
+ if ItemMatches(item) then
+ table.insert(matched, item)
+ end
+ end
+
+ if table.getn(matched) > 0 then
+ table.insert(S.displayList, {
+ isHeader = true,
+ group = group,
+ count = table.getn(matched),
+ })
+ for _, item in ipairs(matched) do
+ table.insert(S.displayList, {
+ isHeader = false,
+ item = item,
+ })
+ end
+ S.filteredCount = S.filteredCount + table.getn(matched)
+ end
+ end
+ end
+
+ S.scrollOffset = 0
+ S.scrollMax = math.max(0, table.getn(S.displayList) - MAX_ROWS)
+ self:RefreshSummary()
+end
+
+local function UpdateScrollbar()
+ local scrollBar = S.scrollBar
+ if not scrollBar then
+ return
+ end
+
+ local total = table.getn(S.displayList)
+ if total <= MAX_ROWS then
+ scrollBar:Hide()
+ return
+ end
+
+ scrollBar:Show()
+
+ local trackHeight = scrollBar:GetHeight()
+ local thumbHeight = math.max(20, math.floor(trackHeight * MAX_ROWS / total + 0.5))
+ local percent = 0
+ if S.scrollMax > 0 then
+ percent = S.scrollOffset / S.scrollMax
+ end
+
+ local thumbY = math.floor((trackHeight - thumbHeight) * percent + 0.5)
+ if scrollBar.thumb then
+ scrollBar.thumb:SetHeight(thumbHeight)
+ scrollBar.thumb:SetPoint("TOP", scrollBar, "TOP", 0, -thumbY)
+ end
+end
+
+function CUI:Render()
+ local theme = GetTheme()
+ local total = table.getn(S.displayList)
+ local visibleItemIndex = 0
+
+ for i = 1, MAX_ROWS do
+ local row = S.rows[i]
+ if not row then
+ break
+ end
+
+ local displayIndex = S.scrollOffset + i
+ local entry = S.displayList[displayIndex]
+ if entry then
+ row:Show()
+
+ if entry.isHeader then
+ local headerText = entry.group.detail or entry.group.role or "未命名分组"
+ if entry.count and entry.count > 0 then
+ headerText = string.format("%s (%d)", headerText, entry.count)
+ end
+
+ row.catFS:SetText(headerText)
+ row.catFS:SetTextColor(
+ entry.group.color and entry.group.color[1] or theme.gold[1],
+ entry.group.color and entry.group.color[2] or theme.gold[2],
+ entry.group.color and entry.group.color[3] or theme.gold[3]
+ )
+ row.catFS:SetWidth(COL_CAT_W + COL_NAME_W + COL_EFF_W + COL_DUR_W - 10)
+ row.catFS:SetPoint("TOPLEFT", row, "TOPLEFT", 6, -3)
+
+ row.nameFS:SetText("")
+ row.effFS:SetText("")
+ row.durFS:SetText("")
+
+ row:SetBackdrop({
+ bgFile = "Interface\\Buttons\\WHITE8X8",
+ tile = false,
+ tileSize = 0,
+ edgeSize = 0,
+ })
+ row:SetBackdropColor(theme.headerBg[1], theme.headerBg[2], theme.headerBg[3], 0.85)
+ row.entry = nil
+ else
+ local item = entry.item
+ visibleItemIndex = visibleItemIndex + 1
+
+ row:SetBackdrop({
+ bgFile = "Interface\\Buttons\\WHITE8X8",
+ tile = false,
+ tileSize = 0,
+ edgeSize = 0,
+ })
+
+ if math.mod(visibleItemIndex, 2) == 0 then
+ row:SetBackdropColor(theme.rowAlt[1], theme.rowAlt[2], theme.rowAlt[3], theme.rowAlt[4] or 0.6)
+ else
+ row:SetBackdropColor(theme.slotBg[1], theme.slotBg[2], theme.slotBg[3], 0.0)
+ end
+
+ row.catFS:SetWidth(COL_CAT_W - 8)
+ row.catFS:SetText(item.cat or "")
+ row.catFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
+ row.catFS:SetPoint("TOPLEFT", row, "TOPLEFT", 6, -3)
+
+ if item.id and item.id > 0 then
+ row.nameFS:SetTextColor(theme.gold[1], theme.gold[2], theme.gold[3])
+ else
+ row.nameFS:SetTextColor(theme.text[1], theme.text[2], theme.text[3])
+ end
+ row.nameFS:SetText(item.name or "")
+
+ row.effFS:SetText(item.effect or "")
+ row.effFS:SetTextColor(theme.text[1], theme.text[2], theme.text[3])
+
+ row.durFS:SetText(item.duration or "")
+ row.durFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
+
+ row.entry = item
+ end
+ else
+ row:Hide()
+ row.entry = nil
+ end
+ end
+
+ if S.emptyFS then
+ if total == 0 then
+ S.emptyFS:SetText("没有匹配到条目,试试更短的关键词或切回“全部”。")
+ S.emptyFS:Show()
+ else
+ S.emptyFS:Hide()
+ end
+ end
+
+ UpdateScrollbar()
+end
+
+local function RefreshTabs()
+ local theme = GetTheme()
+ for _, button in ipairs(S.tabBtns or {}) do
+ local active = (button.roleName == S.activeRole)
+ if active then
+ button:SetBackdropBorderColor(
+ theme.slotSelected[1],
+ theme.slotSelected[2],
+ theme.slotSelected[3],
+ 1.00
+ )
+ button.fs:SetTextColor(
+ theme.slotSelected[1],
+ theme.slotSelected[2],
+ theme.slotSelected[3]
+ )
+ else
+ button:SetBackdropBorderColor(
+ theme.slotBorder[1],
+ theme.slotBorder[2],
+ theme.slotBorder[3],
+ theme.slotBorder[4] or 0.8
+ )
+ button.fs:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
+ end
+ end
+end
+
+local function BuildColumnLabel(parent, text, x, width)
+ local font = GetFont()
+ local theme = GetTheme()
+ local fs = parent:CreateFontString(nil, "OVERLAY")
+ fs:SetFont(font, 10, "OUTLINE")
+ fs:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
+ fs:SetPoint("TOPLEFT", parent, "TOPLEFT", x, -4)
+ fs:SetWidth(width)
+ fs:SetJustifyH("LEFT")
+ fs:SetText(text)
+ return fs
+end
+
+function CUI:Build()
+ local theme = GetTheme()
+ local font = GetFont()
+
+ local frame = CreateFrame("Frame", "NanamiConsumableFrame", UIParent)
+ S.frame = frame
+ frame:SetWidth(FRAME_W)
+ frame:SetHeight(FRAME_H)
+ frame:SetPoint("CENTER", UIParent, "CENTER", 60, 20)
+ frame:SetFrameStrata("HIGH")
+ frame:SetToplevel(true)
+ frame:EnableMouse(true)
+ frame:SetMovable(true)
+ frame:RegisterForDrag("LeftButton")
+ frame:SetScript("OnDragStart", function() this:StartMoving() end)
+ frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
+ frame:EnableMouseWheel(true)
+ frame:SetScript("OnMouseWheel", function()
+ if arg1 > 0 then
+ S.scrollOffset = math.max(0, S.scrollOffset - 1)
+ else
+ S.scrollOffset = math.min(S.scrollMax, S.scrollOffset + 1)
+ end
+ CUI:Render()
+ end)
+
+ ApplyBackdrop(frame)
+ tinsert(UISpecialFrames, "NanamiConsumableFrame")
+
+ local header = CreateFrame("Frame", nil, frame)
+ header:SetPoint("TOPLEFT", 0, 0)
+ header:SetPoint("TOPRIGHT", 0, 0)
+ header:SetHeight(HEADER_H)
+ header:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
+ header:SetBackdropColor(theme.headerBg[1], theme.headerBg[2], theme.headerBg[3], theme.headerBg[4] or 1.0)
+
+ local titleFS = header:CreateFontString(nil, "OVERLAY")
+ titleFS:SetFont(font, 13, "OUTLINE")
+ titleFS:SetPoint("LEFT", header, "LEFT", SIDE_PAD + 4, 0)
+ titleFS:SetText("食物药剂百科")
+ titleFS:SetTextColor(theme.gold[1], theme.gold[2], theme.gold[3])
+
+ local closeBtn = CreateFrame("Button", nil, header)
+ closeBtn:SetWidth(20)
+ closeBtn:SetHeight(20)
+ closeBtn:SetPoint("TOPRIGHT", header, "TOPRIGHT", -8, -7)
+
+ 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(theme.dimText[1], theme.dimText[2], theme.dimText[3])
+
+ closeBtn:SetScript("OnClick", function() frame:Hide() end)
+ closeBtn:SetScript("OnEnter", function() closeTex:SetVertexColor(1, 0.6, 0.7) end)
+ closeBtn:SetScript("OnLeave", function()
+ closeTex:SetVertexColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
+ end)
+
+ SetDivider(frame, -HEADER_H, 16)
+
+ local filterY = -(HEADER_H + 6)
+
+ local searchFrame = CreateFrame("Frame", nil, frame)
+ searchFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", SIDE_PAD, filterY)
+ searchFrame:SetWidth(212)
+ searchFrame:SetHeight(FILTER_H - 4)
+ searchFrame:SetBackdrop({
+ bgFile = "Interface\\Buttons\\WHITE8X8",
+ edgeFile = "Interface\\Buttons\\WHITE8X8",
+ tile = false,
+ tileSize = 0,
+ edgeSize = 1,
+ insets = { left = 1, right = 1, top = 1, bottom = 1 },
+ })
+ searchFrame:SetBackdropColor(0.05, 0.05, 0.08, 0.90)
+ searchFrame:SetBackdropBorderColor(theme.panelBorder[1], theme.panelBorder[2], theme.panelBorder[3], 0.80)
+
+ local searchBox = CreateFrame("EditBox", "NanamiConsumableSearch", searchFrame)
+ searchBox:SetPoint("TOPLEFT", searchFrame, "TOPLEFT", 4, -3)
+ searchBox:SetPoint("BOTTOMRIGHT", searchFrame, "BOTTOMRIGHT", -4, 3)
+ searchBox:SetFont(font, 11)
+ searchBox:SetAutoFocus(false)
+ searchBox:SetMaxLetters(40)
+ searchBox:EnableMouse(true)
+ S.searchBox = searchBox
+
+ local hintFS = searchFrame:CreateFontString(nil, "OVERLAY")
+ hintFS:SetFont(font, 10, "OUTLINE")
+ hintFS:SetPoint("LEFT", searchFrame, "LEFT", 6, 0)
+ hintFS:SetText("搜索名称 / 效果 / 类别")
+ hintFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
+
+ searchBox:SetScript("OnTextChanged", function()
+ local text = this:GetText() or ""
+ S.searchText = text
+ if text == "" then
+ hintFS:Show()
+ else
+ hintFS:Hide()
+ end
+ CUI:Filter()
+ CUI:Render()
+ end)
+ searchBox:SetScript("OnEditFocusGained", function() hintFS:Hide() end)
+ searchBox:SetScript("OnEditFocusLost", function()
+ if (this:GetText() or "") == "" then
+ hintFS:Show()
+ end
+ end)
+ searchBox:SetScript("OnEscapePressed", function() this:ClearFocus() end)
+
+ local summaryFS = frame:CreateFontString(nil, "OVERLAY")
+ summaryFS:SetFont(font, 10, "OUTLINE")
+ summaryFS:SetPoint("LEFT", searchFrame, "RIGHT", 12, 0)
+ summaryFS:SetPoint("RIGHT", frame, "RIGHT", -36, filterY - 12)
+ summaryFS:SetJustifyH("RIGHT")
+ summaryFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
+ summaryFS:SetText("")
+ S.summaryFS = summaryFS
+
+ local tabX = SIDE_PAD
+ local tabY = filterY - FILTER_H
+ S.tabBtns = {}
+ for _, roleName in ipairs(BuildRoleList()) do
+ local tabW = Utf8DisplayWidth(roleName) + 14
+ if tabX + tabW > FRAME_W - SIDE_PAD then
+ tabX = SIDE_PAD
+ tabY = tabY - (ROLE_H - 4) - 2
+ end
+
+ local button = CreateFrame("Button", nil, frame)
+ button:SetWidth(tabW)
+ button:SetHeight(ROLE_H - 4)
+ button:SetPoint("TOPLEFT", frame, "TOPLEFT", tabX, tabY)
+ button:SetBackdrop({
+ bgFile = "Interface\\Buttons\\WHITE8X8",
+ edgeFile = "Interface\\Buttons\\WHITE8X8",
+ tile = false,
+ tileSize = 0,
+ edgeSize = 1,
+ insets = { left = 1, right = 1, top = 1, bottom = 1 },
+ })
+ button:SetBackdropColor(theme.slotBg[1], theme.slotBg[2], theme.slotBg[3], 0.80)
+ button:SetBackdropBorderColor(theme.slotBorder[1], theme.slotBorder[2], theme.slotBorder[3], 0.80)
+
+ local buttonFS = button:CreateFontString(nil, "OVERLAY")
+ buttonFS:SetFont(font, 10, "OUTLINE")
+ buttonFS:SetPoint("CENTER", button, "CENTER", 0, 0)
+ buttonFS:SetText(roleName)
+ buttonFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
+
+ button.fs = buttonFS
+ button.roleName = roleName
+
+ button:SetScript("OnClick", function()
+ S.activeRole = this.roleName
+ RefreshTabs()
+ CUI:Filter()
+ CUI:Render()
+ end)
+ button:SetScript("OnEnter", function()
+ if this.roleName ~= S.activeRole then
+ this:SetBackdropBorderColor(theme.text[1], theme.text[2], theme.text[3], 0.60)
+ end
+ end)
+ button:SetScript("OnLeave", function()
+ if this.roleName ~= S.activeRole then
+ this:SetBackdropBorderColor(theme.slotBorder[1], theme.slotBorder[2], theme.slotBorder[3], 0.80)
+ end
+ end)
+
+ table.insert(S.tabBtns, button)
+ tabX = tabX + tabW + 3
+ end
+
+ RefreshTabs()
+
+ local listStartY = tabY - ROLE_H + 2
+ SetDivider(frame, listStartY, 16)
+
+ local colY = listStartY - 2
+ local colHeader = CreateFrame("Frame", nil, frame)
+ colHeader:SetPoint("TOPLEFT", frame, "TOPLEFT", SIDE_PAD, colY)
+ colHeader:SetWidth(FRAME_W - SIDE_PAD * 2 - 14)
+ colHeader:SetHeight(COL_H)
+ colHeader:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
+ colHeader:SetBackdropColor(theme.headerBg[1], theme.headerBg[2], theme.headerBg[3], 0.70)
+
+ BuildColumnLabel(colHeader, "类别", 4, COL_CAT_W)
+ BuildColumnLabel(colHeader, "名称", COL_CAT_W + 4, COL_NAME_W)
+ BuildColumnLabel(colHeader, "效果", COL_CAT_W + COL_NAME_W + 4, COL_EFF_W)
+ BuildColumnLabel(colHeader, "时长", COL_CAT_W + COL_NAME_W + COL_EFF_W + 4, COL_DUR_W)
+
+ local rowAreaY = colY - COL_H - 1
+ local rowAreaH = FRAME_H - (-rowAreaY) - 8
+
+ local scrollWidth = 10
+ local scrollBar = CreateFrame("Frame", nil, frame)
+ scrollBar:SetWidth(scrollWidth)
+ scrollBar:SetHeight(rowAreaH)
+ scrollBar:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, rowAreaY)
+ scrollBar:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
+ scrollBar:SetBackdropColor(theme.slotBg[1], theme.slotBg[2], theme.slotBg[3], 0.40)
+
+ local scrollThumb = scrollBar:CreateTexture(nil, "OVERLAY")
+ scrollThumb:SetTexture("Interface\\Buttons\\WHITE8X8")
+ scrollThumb:SetWidth(scrollWidth - 2)
+ scrollThumb:SetHeight(40)
+ scrollThumb:SetPoint("TOP", scrollBar, "TOP", 0, 0)
+ scrollThumb:SetVertexColor(theme.slotSelected[1], theme.slotSelected[2], theme.slotSelected[3], 0.60)
+
+ scrollBar.thumb = scrollThumb
+ S.scrollBar = scrollBar
+
+ local rowWidth = FRAME_W - SIDE_PAD * 2 - scrollWidth - 6
+ for i = 1, MAX_ROWS do
+ local rowY = rowAreaY - (i - 1) * ROW_H
+ local row = CreateFrame("Button", nil, frame)
+ row:SetWidth(rowWidth)
+ row:SetHeight(ROW_H)
+ row:SetPoint("TOPLEFT", frame, "TOPLEFT", SIDE_PAD, rowY)
+
+ local catFS = row:CreateFontString(nil, "OVERLAY")
+ catFS:SetFont(font, 9, "OUTLINE")
+ catFS:SetPoint("TOPLEFT", row, "TOPLEFT", 6, -3)
+ catFS:SetWidth(COL_CAT_W - 8)
+ catFS:SetJustifyH("LEFT")
+ row.catFS = catFS
+
+ local nameFS = row:CreateFontString(nil, "OVERLAY")
+ nameFS:SetFont(font, 9, "OUTLINE")
+ nameFS:SetPoint("TOPLEFT", row, "TOPLEFT", COL_CAT_W + 4, -3)
+ nameFS:SetWidth(COL_NAME_W - 4)
+ nameFS:SetJustifyH("LEFT")
+ row.nameFS = nameFS
+
+ local effFS = row:CreateFontString(nil, "OVERLAY")
+ effFS:SetFont(font, 9, "OUTLINE")
+ effFS:SetPoint("TOPLEFT", row, "TOPLEFT", COL_CAT_W + COL_NAME_W + 4, -3)
+ effFS:SetWidth(COL_EFF_W - 4)
+ effFS:SetJustifyH("LEFT")
+ row.effFS = effFS
+
+ local durFS = row:CreateFontString(nil, "OVERLAY")
+ durFS:SetFont(font, 9, "OUTLINE")
+ durFS:SetPoint("TOPLEFT", row, "TOPLEFT", COL_CAT_W + COL_NAME_W + COL_EFF_W + 4, -3)
+ durFS:SetWidth(COL_DUR_W - 2)
+ durFS:SetJustifyH("LEFT")
+ row.durFS = durFS
+
+ local highlight = row:CreateTexture(nil, "HIGHLIGHT")
+ highlight:SetTexture("Interface\\Buttons\\WHITE8X8")
+ highlight:SetAllPoints()
+ highlight:SetVertexColor(1, 1, 1, 0.07)
+ highlight:SetBlendMode("ADD")
+
+ row:SetScript("OnEnter", function()
+ local entry = this.entry
+ if not entry then
+ return
+ end
+
+ GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
+ if entry.id and entry.id > 0 then
+ GameTooltip:SetHyperlink("item:" .. entry.id .. ":0:0:0")
+ GameTooltip:AddLine("Shift+点击可发送物品链接", 0.55, 0.55, 0.60)
+ else
+ GameTooltip:SetText(entry.name or "", 1, 0.82, 0.40)
+ if entry.effect and entry.effect ~= "" then
+ GameTooltip:AddLine(entry.effect, 0.80, 0.80, 0.80)
+ end
+ if entry.duration and entry.duration ~= "" then
+ GameTooltip:AddLine("持续时间: " .. entry.duration, 0.55, 0.55, 0.60)
+ end
+ end
+ GameTooltip:Show()
+ end)
+ row:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ row:SetScript("OnClick", function()
+ local entry = this.entry
+ if not entry then
+ return
+ end
+
+ if IsShiftKeyDown() and entry.id and entry.id > 0 then
+ local _, link = GetItemInfo(entry.id)
+ if link and ChatFrameEditBox then
+ ChatFrameEditBox:Show()
+ ChatFrameEditBox:Insert(link)
+ end
+ end
+ end)
+
+ row:Hide()
+ S.rows[i] = row
+ end
+
+ local emptyFS = frame:CreateFontString(nil, "OVERLAY")
+ emptyFS:SetFont(font, 11, "OUTLINE")
+ emptyFS:SetPoint("CENTER", frame, "CENTER", 0, -28)
+ emptyFS:SetTextColor(theme.dimText[1], theme.dimText[2], theme.dimText[3])
+ emptyFS:SetText("")
+ emptyFS:Hide()
+ S.emptyFS = emptyFS
+end
+
+function CUI:Toggle()
+ if S.frame and S.frame:IsShown() then
+ S.frame:Hide()
+ return
+ end
+
+ if not S.frame then
+ self:Build()
+ end
+
+ self:Filter()
+ self:Render()
+ S.frame:Show()
+end
+
+function CUI:Hide()
+ if S.frame then
+ S.frame:Hide()
+ end
+end
diff --git a/Core.lua b/Core.lua
index ba38fb7..4ed59cc 100644
--- a/Core.lua
+++ b/Core.lua
@@ -32,6 +32,40 @@ do
end
end
+ -- 保护 ComboFrame,防止 fadeInfo 为 nil 导致的报错
+ -- (ComboFrame.lua:46: attempt to index local 'fadeInfo' (a nil value))
+ if ComboFrame then
+ if not ComboFrame.fadeInfo then
+ ComboFrame.fadeInfo = {}
+ end
+ local origComboScript = ComboFrame.GetScript and ComboFrame:GetScript("OnUpdate")
+ if origComboScript then
+ ComboFrame:SetScript("OnUpdate", function(elapsed)
+ if ComboFrame.fadeInfo then
+ origComboScript(elapsed)
+ end
+ end)
+ end
+ end
+ if ComboFrame_Update then
+ local _orig_ComboUpdate = ComboFrame_Update
+ ComboFrame_Update = function()
+ if ComboFrame and not ComboFrame.fadeInfo then
+ ComboFrame.fadeInfo = {}
+ end
+ return _orig_ComboUpdate()
+ end
+ end
+ if ComboFrame_OnUpdate then
+ local _orig_ComboOnUpdate = ComboFrame_OnUpdate
+ ComboFrame_OnUpdate = function(elapsed)
+ if ComboFrame and not ComboFrame.fadeInfo then
+ ComboFrame.fadeInfo = {}
+ end
+ return _orig_ComboOnUpdate(elapsed)
+ end
+ end
+
local origOnUpdate = UIParent and UIParent.GetScript and UIParent:GetScript("OnUpdate")
if origOnUpdate then
UIParent:SetScript("OnUpdate", function()
@@ -43,6 +77,11 @@ end
BINDING_HEADER_NANAMI_UI = "Nanami-UI"
BINDING_NAME_NANAMI_TOGGLE_NAV = "切换导航地图"
+BINDING_HEADER_NANAMI_EXTRABAR = "Nanami-UI 额外动作条"
+for _i = 1, 48 do
+ _G["BINDING_NAME_NANAMI_EXTRABAR" .. _i] = "额外动作条 按钮" .. _i
+end
+
SFrames.eventFrame = CreateFrame("Frame", "SFramesEventFrame", UIParent)
SFrames.events = {}
@@ -140,6 +179,52 @@ function SFrames:Print(msg)
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r " .. tostring(msg))
end
+local function IsBattlefieldMinimapVisible()
+ if BattlefieldMinimap and BattlefieldMinimap.IsVisible and BattlefieldMinimap:IsVisible() then
+ return true
+ end
+ if BattlefieldMinimapFrame and BattlefieldMinimapFrame ~= BattlefieldMinimap
+ and BattlefieldMinimapFrame.IsVisible and BattlefieldMinimapFrame:IsVisible() then
+ return true
+ end
+ return false
+end
+
+function SFrames:CaptureBattlefieldMinimapState()
+ return IsBattlefieldMinimapVisible()
+end
+
+function SFrames:RestoreBattlefieldMinimapState(wasVisible)
+ if wasVisible then
+ return
+ end
+
+ local frames = { BattlefieldMinimap, BattlefieldMinimapFrame }
+ local hidden = {}
+ for i = 1, table.getn(frames) do
+ local frame = frames[i]
+ if frame and not hidden[frame] and frame.Hide and frame.IsVisible and frame:IsVisible() then
+ hidden[frame] = true
+ pcall(frame.Hide, frame)
+ end
+ end
+end
+
+function SFrames:CallWithPreservedBattlefieldMinimap(func, a1, a2, a3, a4, a5, a6, a7, a8)
+ if type(func) ~= "function" then
+ return
+ end
+
+ local state = self:CaptureBattlefieldMinimapState()
+ local results = { pcall(func, a1, a2, a3, a4, a5, a6, a7, a8) }
+ self:RestoreBattlefieldMinimapState(state)
+
+ if not results[1] then
+ return nil, results[2]
+ end
+ return unpack(results, 2, table.getn(results))
+end
+
-- Addon Loaded Initializer
SFrames:RegisterEvent("PLAYER_LOGIN", function()
SFrames:Initialize()
@@ -299,6 +384,10 @@ function SFrames:DoFullInitialize()
SFrames.Tooltip:SetAlpha(0)
SFrames.Tooltip:Hide()
+ if SFrames.AuraTracker and SFrames.AuraTracker.Initialize then
+ SFrames.AuraTracker:Initialize()
+ end
+
-- Phase 1: Critical modules (unit frames, action bars) — must load immediately
if SFramesDB.enableUnitFrames ~= false then
if SFramesDB.enablePlayerFrame ~= false then
@@ -319,6 +408,10 @@ function SFrames:DoFullInitialize()
SFrames.ActionBars:Initialize()
end
+ if SFrames.ExtraBar and SFrames.ExtraBar.Initialize then
+ SFrames.ExtraBar:Initialize()
+ end
+
self:InitSlashCommands()
-- Phase 2: Deferred modules — spread across multiple frames to avoid memory spike
@@ -375,6 +468,13 @@ function SFrames:GetAuraTimeLeft(unit, index, isBuff)
end
end
+ if SFrames.AuraTracker and SFrames.AuraTracker.GetAuraTimeLeft then
+ local trackerTime = SFrames.AuraTracker:GetAuraTimeLeft(unit, isBuff and "buff" or "debuff", index)
+ if trackerTime and trackerTime > 0 then
+ return trackerTime
+ end
+ end
+
-- Nanami-Plates SpellDB: combat log + spell DB tracking (most accurate for debuffs)
if not isBuff and NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff then
local effect, rank, tex, stacks, dtype, duration, timeleft, isOwn = NanamiPlates_SpellDB:UnitDebuff(unit, index)
@@ -420,6 +520,64 @@ function SFrames:FormatTime(seconds)
end
end
+local POWER_RAINBOW_TEX = "Interface\\AddOns\\Nanami-UI\\img\\progress"
+
+function SFrames:UpdateRainbowBar(bar, power, maxPower, unit)
+ -- 彩虹条仅适用于法力(powerType 0),怒气/能量等跳过
+ if unit and UnitPowerType(unit) ~= 0 then
+ if bar._rainbowActive then
+ if bar.rainbowTex then bar.rainbowTex:Hide() end
+ bar._rainbowActive = nil
+ end
+ return
+ end
+ if not (SFramesDB and SFramesDB.powerRainbow) then
+ if bar._rainbowActive then
+ if bar.rainbowTex then bar.rainbowTex:Hide() end
+ bar._rainbowActive = nil
+ end
+ return
+ end
+ if not bar.rainbowTex then
+ bar.rainbowTex = bar:CreateTexture(nil, "OVERLAY")
+ bar.rainbowTex:SetTexture(POWER_RAINBOW_TEX)
+ bar.rainbowTex:Hide()
+ end
+ if maxPower and maxPower > 0 then
+ local pct = power / maxPower
+ if pct >= 1.0 then
+ -- 满条:直接铺满,不依赖 GetWidth()(两锚点定尺寸的框体 GetWidth 可能返回 0)
+ bar.rainbowTex:ClearAllPoints()
+ bar.rainbowTex:SetAllPoints(bar)
+ bar.rainbowTex:SetTexCoord(0, 1, 0, 1)
+ bar.rainbowTex:Show()
+ bar._rainbowActive = true
+ else
+ local barW = bar:GetWidth()
+ -- 双锚点定尺寸的框体(如宠物能量条)GetWidth() 可能返回 0
+ -- 回退到 GetRight()-GetLeft() 获取实际渲染宽度
+ if not barW or barW <= 0 then
+ local left = bar:GetLeft()
+ local right = bar:GetRight()
+ if left and right then
+ barW = right - left
+ end
+ end
+ if barW and barW > 0 then
+ bar.rainbowTex:ClearAllPoints()
+ bar.rainbowTex:SetPoint("TOPLEFT", bar, "TOPLEFT", 0, 0)
+ bar.rainbowTex:SetPoint("BOTTOMRIGHT", bar, "BOTTOMLEFT", barW * pct, 0)
+ bar.rainbowTex:SetTexCoord(0, pct, 0, 1)
+ bar.rainbowTex:Show()
+ bar._rainbowActive = true
+ end
+ end
+ else
+ bar.rainbowTex:Hide()
+ bar._rainbowActive = nil
+ end
+end
+
function SFrames:InitSlashCommands()
DEFAULT_CHAT_FRAME:AddMessage("SF: InitSlashCommands called.")
SLASH_SFRAMES1 = "/nanami"
@@ -808,12 +966,16 @@ function SFrames:HideBlizzardFrames()
end
if ComboFrame then
ComboFrame:UnregisterAllEvents()
+ ComboFrame:SetScript("OnUpdate", nil)
ComboFrame:Hide()
ComboFrame.Show = function() end
- ComboFrame.fadeInfo = ComboFrame.fadeInfo or {}
+ ComboFrame.fadeInfo = {}
if ComboFrame_Update then
ComboFrame_Update = function() end
end
+ if ComboFrame_OnUpdate then
+ ComboFrame_OnUpdate = function() end
+ end
end
end
diff --git a/ExtraBar.lua b/ExtraBar.lua
new file mode 100644
index 0000000..e0e3cbd
--- /dev/null
+++ b/ExtraBar.lua
@@ -0,0 +1,792 @@
+--------------------------------------------------------------------------------
+-- Nanami-UI: ExtraBar
+--
+-- A fully configurable extra action bar using action slots from page 7+
+-- (slots 73-120). Buttons are created from scratch since no spare Blizzard
+-- ActionButton frames exist.
+--
+-- Supports: grid layout, per-row count, alignment, drag-and-drop, cooldown,
+-- range coloring, tooltip, and Mover integration for position saving.
+--------------------------------------------------------------------------------
+
+SFrames.ExtraBar = {}
+
+local EB = SFrames.ExtraBar
+
+local DEFAULTS = {
+ enable = false,
+ buttonCount = 12,
+ perRow = 12,
+ buttonSize = 36,
+ buttonGap = 2,
+ align = "center",
+ startSlot = 73,
+ alpha = 1.0,
+ showHotkey = true,
+ showCount = true,
+ buttonRounded = false,
+ buttonInnerShadow = false,
+}
+
+local MAX_BUTTONS = 48
+
+function EB:GetDB()
+ if not SFramesDB then SFramesDB = {} end
+ if type(SFramesDB.ExtraBar) ~= "table" then SFramesDB.ExtraBar = {} end
+ local db = SFramesDB.ExtraBar
+ for k, v in pairs(DEFAULTS) do
+ if db[k] == nil then db[k] = v end
+ end
+ return db
+end
+
+--------------------------------------------------------------------------------
+-- Layout helper (local LayoutGrid equivalent)
+--------------------------------------------------------------------------------
+local function LayoutGrid(buttons, parent, size, gap, perRow, count)
+ if count == 0 then return end
+ local numCols = math.min(perRow, count)
+ local numRows = math.ceil(count / perRow)
+ parent:SetWidth(numCols * size + math.max(numCols - 1, 0) * gap)
+ parent:SetHeight(numRows * size + math.max(numRows - 1, 0) * gap)
+ for i = 1, count do
+ local b = buttons[i]
+ if b then
+ b:SetWidth(size)
+ b:SetHeight(size)
+ b:ClearAllPoints()
+ local col = math.fmod(i - 1, perRow)
+ local row = math.floor((i - 1) / perRow)
+ b:SetPoint("TOPLEFT", parent, "TOPLEFT", col * (size + gap), -row * (size + gap))
+ b:Show()
+ end
+ end
+ for i = count + 1, MAX_BUTTONS do
+ if buttons[i] then buttons[i]:Hide() end
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Button visual helpers (mirrors ActionBars.lua approach)
+--------------------------------------------------------------------------------
+local function CreateBackdropFor(btn)
+ if btn.sfBackdrop then return end
+ local level = btn:GetFrameLevel()
+ local bd = CreateFrame("Frame", nil, btn)
+ bd:SetFrameLevel(level > 0 and (level - 1) or 0)
+ bd:SetAllPoints(btn)
+ SFrames:CreateBackdrop(bd)
+ btn.sfBackdrop = bd
+end
+
+local function CreateInnerShadow(btn)
+ if btn.sfInnerShadow then return btn.sfInnerShadow end
+ local shadow = {}
+ local thickness = 4
+
+ local top = btn:CreateTexture(nil, "OVERLAY")
+ top:SetTexture("Interface\\Buttons\\WHITE8X8")
+ top:SetHeight(thickness)
+ top:SetGradientAlpha("VERTICAL", 0, 0, 0, 0, 0, 0, 0, 0.5)
+ shadow.top = top
+
+ local bot = btn:CreateTexture(nil, "OVERLAY")
+ bot:SetTexture("Interface\\Buttons\\WHITE8X8")
+ bot:SetHeight(thickness)
+ bot:SetGradientAlpha("VERTICAL", 0, 0, 0, 0.5, 0, 0, 0, 0)
+ shadow.bottom = bot
+
+ local left = btn:CreateTexture(nil, "OVERLAY")
+ left:SetTexture("Interface\\Buttons\\WHITE8X8")
+ left:SetWidth(thickness)
+ left:SetGradientAlpha("HORIZONTAL", 0, 0, 0, 0.5, 0, 0, 0, 0)
+ shadow.left = left
+
+ local right = btn:CreateTexture(nil, "OVERLAY")
+ right:SetTexture("Interface\\Buttons\\WHITE8X8")
+ right:SetWidth(thickness)
+ right:SetGradientAlpha("HORIZONTAL", 0, 0, 0, 0, 0, 0, 0, 0.5)
+ shadow.right = right
+
+ btn.sfInnerShadow = shadow
+ return shadow
+end
+
+local function ApplyButtonVisuals(btn, rounded, innerShadow)
+ local bd = btn.sfBackdrop
+ if not bd then return end
+
+ local inset = rounded and 3 or 2
+ btn.sfIconInset = inset
+
+ if rounded then
+ bd: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 }
+ })
+ else
+ bd:SetBackdrop({
+ bgFile = "Interface\\Buttons\\WHITE8X8",
+ edgeFile = "Interface\\Buttons\\WHITE8X8",
+ tile = false, tileSize = 0, edgeSize = 1,
+ insets = { left = 1, right = 1, top = 1, bottom = 1 }
+ })
+ end
+ local A = SFrames.ActiveTheme
+ if A and A.panelBg then
+ bd:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.9)
+ bd:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 1)
+ else
+ bd:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
+ bd:SetBackdropBorderColor(0, 0, 0, 1)
+ end
+
+ local icon = btn.sfIcon
+ if icon then
+ icon:ClearAllPoints()
+ icon:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset)
+ icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset)
+ end
+ if btn.sfCdOverlay then
+ btn.sfCdOverlay:ClearAllPoints()
+ btn.sfCdOverlay:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset)
+ btn.sfCdOverlay:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset)
+ end
+ if btn.sfRangeOverlay then
+ btn.sfRangeOverlay:ClearAllPoints()
+ btn.sfRangeOverlay:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset)
+ btn.sfRangeOverlay:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset)
+ end
+
+ if innerShadow then
+ if not btn.sfInnerShadow then CreateInnerShadow(btn) end
+ local s = btn.sfInnerShadow
+ s.top:ClearAllPoints()
+ s.top:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset)
+ s.top:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -inset, -inset)
+ s.bottom:ClearAllPoints()
+ s.bottom:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", inset, inset)
+ s.bottom:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset)
+ s.left:ClearAllPoints()
+ s.left:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset)
+ s.left:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", inset, inset)
+ s.right:ClearAllPoints()
+ s.right:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -inset, -inset)
+ s.right:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset)
+ s.top:Show(); s.bottom:Show(); s.left:Show(); s.right:Show()
+ else
+ if btn.sfInnerShadow then
+ local s = btn.sfInnerShadow
+ s.top:Hide(); s.bottom:Hide(); s.left:Hide(); s.right:Hide()
+ end
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Single-button refresh helpers
+--------------------------------------------------------------------------------
+local function RefreshButtonIcon(btn)
+ local slot = btn.sfActionSlot
+ if not slot then return end
+ if HasAction(slot) then
+ local tex = GetActionTexture(slot)
+ btn.sfIcon:SetTexture(tex)
+ btn.sfIcon:Show()
+ btn.sfIcon:SetVertexColor(1, 1, 1, 1)
+ btn:SetAlpha(1)
+ else
+ btn.sfIcon:SetTexture(nil)
+ btn.sfIcon:Hide()
+ end
+end
+
+local function RefreshButtonCooldown(btn)
+ local slot = btn.sfActionSlot
+ if not slot or not HasAction(slot) then
+ if btn.sfCdOverlay then btn.sfCdOverlay:Hide() end
+ if btn.sfCdText then btn.sfCdText:SetText("") end
+ btn.sfCdStart = 0
+ btn.sfCdDuration = 0
+ return
+ end
+ local start, duration, enable = GetActionCooldown(slot)
+ if not start or not duration or duration == 0 or enable == 0 then
+ if btn.sfCdOverlay then btn.sfCdOverlay:Hide() end
+ if btn.sfCdText then btn.sfCdText:SetText("") end
+ btn.sfCdStart = 0
+ btn.sfCdDuration = 0
+ return
+ end
+ btn.sfCdStart = start
+ btn.sfCdDuration = duration
+ local now = GetTime()
+ local remaining = (start + duration) - now
+ if remaining > 0 then
+ if btn.sfCdOverlay then btn.sfCdOverlay:Show() end
+ if btn.sfCdText then
+ if remaining >= 60 then
+ btn.sfCdText:SetText(math.floor(remaining / 60) .. "m")
+ else
+ btn.sfCdText:SetText(math.floor(remaining + 0.5) .. "")
+ end
+ end
+ else
+ if btn.sfCdOverlay then btn.sfCdOverlay:Hide() end
+ if btn.sfCdText then btn.sfCdText:SetText("") end
+ btn.sfCdStart = 0
+ btn.sfCdDuration = 0
+ end
+end
+
+local function RefreshButtonUsable(btn)
+ local slot = btn.sfActionSlot
+ if not slot or not HasAction(slot) then return end
+ local isUsable, notEnoughMana = IsUsableAction(slot)
+ if isUsable then
+ btn.sfIcon:SetVertexColor(1, 1, 1, 1)
+ elseif notEnoughMana then
+ btn.sfIcon:SetVertexColor(0.2, 0.2, 0.8, 1)
+ else
+ btn.sfIcon:SetVertexColor(0.4, 0.4, 0.4, 1)
+ end
+end
+
+local function RefreshButtonCount(btn)
+ local slot = btn.sfActionSlot
+ if not slot or not HasAction(slot) then
+ btn.sfCount:SetText("")
+ return
+ end
+ local count = GetActionCount(slot)
+ if count and count > 0 then
+ btn.sfCount:SetText(tostring(count))
+ else
+ btn.sfCount:SetText("")
+ end
+end
+
+local function RefreshButtonRange(btn)
+ local slot = btn.sfActionSlot
+ if not slot or not HasAction(slot) then
+ if btn.sfRangeOverlay then btn.sfRangeOverlay:Hide() end
+ return
+ end
+ local inRange = IsActionInRange(slot)
+ if not btn.sfRangeOverlay then
+ local inset = btn.sfIconInset or 2
+ local ov = btn:CreateTexture(nil, "OVERLAY")
+ ov:SetTexture("Interface\\Buttons\\WHITE8X8")
+ ov:SetPoint("TOPLEFT", btn, "TOPLEFT", inset, -inset)
+ ov:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -inset, inset)
+ ov:SetVertexColor(1.0, 0.1, 0.1, 0.35)
+ ov:Hide()
+ btn.sfRangeOverlay = ov
+ end
+ if inRange == 0 then
+ btn.sfRangeOverlay:Show()
+ else
+ btn.sfRangeOverlay:Hide()
+ end
+end
+
+local function RefreshButtonState(btn)
+ local slot = btn.sfActionSlot
+ if not slot or not HasAction(slot) then return end
+ if IsCurrentAction(slot) then
+ if btn.sfBackdrop then
+ btn.sfBackdrop:SetBackdropBorderColor(1, 1, 1, 0.8)
+ end
+ else
+ local A = SFrames.ActiveTheme
+ if btn.sfBackdrop then
+ if A and A.panelBorder then
+ btn.sfBackdrop:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 1)
+ else
+ btn.sfBackdrop:SetBackdropBorderColor(0, 0, 0, 1)
+ end
+ end
+ end
+end
+
+local function RefreshButtonAll(btn)
+ RefreshButtonIcon(btn)
+ RefreshButtonCooldown(btn)
+ RefreshButtonUsable(btn)
+ RefreshButtonCount(btn)
+ RefreshButtonState(btn)
+end
+
+--------------------------------------------------------------------------------
+-- Custom action button factory
+--------------------------------------------------------------------------------
+local btnIndex = 0
+
+local function CreateExtraButton(parent)
+ btnIndex = btnIndex + 1
+ local name = "SFramesExtraBarButton" .. btnIndex
+ local btn = CreateFrame("Button", name, parent)
+ btn:SetWidth(36)
+ btn:SetHeight(36)
+ btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
+ btn:RegisterForDrag("LeftButton")
+
+ CreateBackdropFor(btn)
+
+ local icon = btn:CreateTexture(name .. "Icon", "ARTWORK")
+ icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
+ icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
+ icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
+ btn.sfIcon = icon
+
+ local cdOverlay = btn:CreateTexture(nil, "OVERLAY")
+ cdOverlay:SetTexture("Interface\\Buttons\\WHITE8X8")
+ cdOverlay:SetAllPoints(icon)
+ cdOverlay:SetVertexColor(0, 0, 0, 0.6)
+ cdOverlay:Hide()
+ btn.sfCdOverlay = cdOverlay
+
+ local cdText = btn:CreateFontString(nil, "OVERLAY")
+ cdText:SetFont(SFrames:GetFont(), 12, "OUTLINE")
+ cdText:SetPoint("CENTER", btn, "CENTER", 0, 0)
+ cdText:SetTextColor(1, 1, 0.2)
+ cdText:SetText("")
+ btn.sfCdText = cdText
+
+ btn.sfCdStart = 0
+ btn.sfCdDuration = 0
+
+ local font = SFrames:GetFont()
+
+ local hotkey = btn:CreateFontString(name .. "HotKey", "OVERLAY")
+ hotkey:SetFont(font, 9, "OUTLINE")
+ hotkey:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -2, -2)
+ hotkey:SetJustifyH("RIGHT")
+ btn.sfHotKey = hotkey
+
+ local count = btn:CreateFontString(name .. "Count", "OVERLAY")
+ count:SetFont(font, 9, "OUTLINE")
+ count:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
+ count:SetJustifyH("RIGHT")
+ btn.sfCount = count
+
+ -- Highlight texture
+ local hl = btn:CreateTexture(nil, "HIGHLIGHT")
+ hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
+ hl:SetBlendMode("ADD")
+ hl:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
+ hl:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
+ hl:SetAlpha(0.3)
+
+ -- Pushed texture overlay
+ local pushed = btn:CreateTexture(nil, "OVERLAY")
+ pushed:SetTexture("Interface\\Buttons\\WHITE8X8")
+ pushed:SetPoint("TOPLEFT", btn, "TOPLEFT", 2, -2)
+ pushed:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -2, 2)
+ pushed:SetVertexColor(1, 1, 1, 0.15)
+ pushed:Hide()
+ btn.sfPushed = pushed
+
+ btn.sfActionSlot = nil
+
+ btn:SetScript("OnClick", function()
+ local slot = this.sfActionSlot
+ if not slot then return end
+ if arg1 == "LeftButton" then
+ if IsShiftKeyDown() and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
+ -- Shift-click: link to chat (fallback: pickup)
+ pcall(PickupAction, slot)
+ else
+ UseAction(slot, 0, 1)
+ end
+ elseif arg1 == "RightButton" then
+ UseAction(slot, 0, 1)
+ end
+ RefreshButtonAll(this)
+ end)
+
+ btn:SetScript("OnDragStart", function()
+ local slot = this.sfActionSlot
+ if slot then
+ pcall(PickupAction, slot)
+ RefreshButtonAll(this)
+ end
+ end)
+
+ btn:SetScript("OnReceiveDrag", function()
+ local slot = this.sfActionSlot
+ if slot then
+ pcall(PlaceAction, slot)
+ RefreshButtonAll(this)
+ end
+ end)
+
+ btn:SetScript("OnEnter", function()
+ local slot = this.sfActionSlot
+ if slot and HasAction(slot) then
+ GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
+ GameTooltip:SetAction(slot)
+ GameTooltip:Show()
+ end
+ end)
+
+ btn:SetScript("OnLeave", function()
+ GameTooltip:Hide()
+ end)
+
+ btn:SetScript("OnMouseDown", function()
+ if this.sfPushed then this.sfPushed:Show() end
+ end)
+
+ btn:SetScript("OnMouseUp", function()
+ if this.sfPushed then this.sfPushed:Hide() end
+ end)
+
+ btn:Hide()
+ return btn
+end
+
+--------------------------------------------------------------------------------
+-- Create all buttons once
+--------------------------------------------------------------------------------
+function EB:CreateButtons()
+ local db = self:GetDB()
+
+ self.holder = CreateFrame("Frame", "SFramesExtraBarHolder", UIParent)
+ self.holder:SetWidth(200)
+ self.holder:SetHeight(40)
+
+ local pos = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ExtraBar"]
+ if pos and pos.point and pos.relativePoint then
+ self.holder:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
+ else
+ self.holder:SetPoint("CENTER", UIParent, "CENTER", 0, -200)
+ end
+
+ self.buttons = {}
+ for i = 1, MAX_BUTTONS do
+ local btn = CreateExtraButton(self.holder)
+ table.insert(self.buttons, btn)
+ end
+
+ if SFrames.ActionBars and SFrames.ActionBars.RegisterBindButton then
+ for i = 1, MAX_BUTTONS do
+ local bname = self.buttons[i]:GetName()
+ SFrames.ActionBars:RegisterBindButton(bname, "NANAMI_EXTRABAR" .. i)
+ end
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Apply configuration
+--------------------------------------------------------------------------------
+function EB:ApplyConfig()
+ if not self.holder then return end
+ local db = self:GetDB()
+
+ if not db.enable then
+ self.holder:Hide()
+ return
+ end
+
+ local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
+ local perRow = db.perRow or 12
+ local size = db.buttonSize or 36
+ local gap = db.buttonGap or 2
+ local startSlot = db.startSlot or 73
+
+ -- Assign action slots
+ for i = 1, MAX_BUTTONS do
+ local btn = self.buttons[i]
+ btn.sfActionSlot = startSlot + i - 1
+ end
+
+ -- Layout
+ LayoutGrid(self.buttons, self.holder, size, gap, perRow, count)
+
+ -- Alignment via anchor
+ local positions = SFramesDB and SFramesDB.Positions
+ local pos = positions and positions["ExtraBar"]
+ if not (pos and pos.point and pos.relativePoint) then
+ self.holder:ClearAllPoints()
+ local align = db.align or "center"
+ if align == "left" then
+ self.holder:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 20, 200)
+ elseif align == "right" then
+ self.holder:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -20, 200)
+ else
+ self.holder:SetPoint("CENTER", UIParent, "CENTER", 0, -200)
+ 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
+ self.holder:SetAlpha(alpha)
+
+ -- Scale (inherit from main action bars if available)
+ local abDb = SFrames.ActionBars and SFrames.ActionBars.GetDB and SFrames.ActionBars:GetDB()
+ local scale = abDb and abDb.scale or 1.0
+ self.holder:SetScale(scale)
+
+ -- Button visuals
+ local isRounded = db.buttonRounded
+ local isShadow = db.buttonInnerShadow
+ local showHK = db.showHotkey
+ local showCt = db.showCount
+ local font = SFrames:GetFont()
+ local fontSize = math.max(6, math.floor(size * 0.25 + 0.5))
+
+ for i = 1, count do
+ local btn = self.buttons[i]
+ ApplyButtonVisuals(btn, isRounded, isShadow)
+ if btn.sfHotKey then
+ btn.sfHotKey:SetFont(font, fontSize, "OUTLINE")
+ if showHK then btn.sfHotKey:Show() else btn.sfHotKey:Hide() end
+ end
+ if btn.sfCount then
+ btn.sfCount:SetFont(font, fontSize, "OUTLINE")
+ if showCt then btn.sfCount:Show() else btn.sfCount:Hide() end
+ end
+ if btn.sfCdText then
+ local cdFontSize = math.max(8, math.floor(size * 0.35 + 0.5))
+ btn.sfCdText:SetFont(font, cdFontSize, "OUTLINE")
+ end
+ RefreshButtonAll(btn)
+ end
+
+ self.holder:Show()
+
+ -- Update mover if in layout mode
+ if SFrames.Movers and SFrames.Movers:IsLayoutMode() then
+ SFrames.Movers:SyncMoverToFrame("ExtraBar")
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Refresh all visible buttons
+--------------------------------------------------------------------------------
+function EB:RefreshAll()
+ if not self.buttons then return end
+ local db = self:GetDB()
+ if not db.enable then return end
+ local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
+ for i = 1, count do
+ RefreshButtonAll(self.buttons[i])
+ end
+end
+
+function EB:RefreshCooldowns()
+ if not self.buttons then return end
+ local db = self:GetDB()
+ if not db.enable then return end
+ local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
+ for i = 1, count do
+ RefreshButtonCooldown(self.buttons[i])
+ end
+end
+
+function EB:RefreshUsable()
+ if not self.buttons then return end
+ local db = self:GetDB()
+ if not db.enable then return end
+ local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
+ for i = 1, count do
+ RefreshButtonUsable(self.buttons[i])
+ end
+end
+
+function EB:RefreshStates()
+ if not self.buttons then return end
+ local db = self:GetDB()
+ if not db.enable then return end
+ local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
+ for i = 1, count do
+ RefreshButtonState(self.buttons[i])
+ end
+end
+
+function EB:RefreshHotkeys()
+ if not self.buttons then return end
+ local db = self:GetDB()
+ if not db.enable then return end
+ local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
+ for i = 1, count do
+ local btn = self.buttons[i]
+ local cmd = "NANAMI_EXTRABAR" .. i
+ local hotkey = btn.sfHotKey
+ if hotkey then
+ local key1 = GetBindingKey(cmd)
+ if key1 then
+ local text = key1
+ if GetBindingText then
+ text = GetBindingText(key1, "KEY_", 1) or key1
+ end
+ hotkey:SetText(text)
+ else
+ hotkey:SetText("")
+ end
+ end
+ end
+end
+
+--------------------------------------------------------------------------------
+-- OnUpdate poller for cooldown / range / usability
+--------------------------------------------------------------------------------
+function EB:SetupPoller()
+ local poller = CreateFrame("Frame", "SFramesExtraBarPoller", UIParent)
+ poller.timer = 0
+ self.poller = poller
+
+ poller:SetScript("OnUpdate", function()
+ this.timer = this.timer + arg1
+ if this.timer < 0.2 then return end
+ this.timer = 0
+
+ local db = EB:GetDB()
+ if not db.enable then return end
+ local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
+ for i = 1, count do
+ local btn = EB.buttons[i]
+ if btn and btn:IsShown() then
+ RefreshButtonCooldown(btn)
+ RefreshButtonUsable(btn)
+ RefreshButtonRange(btn)
+ RefreshButtonState(btn)
+ end
+ end
+ end)
+end
+
+--------------------------------------------------------------------------------
+-- Initialize
+--------------------------------------------------------------------------------
+function EB:Initialize()
+ local db = self:GetDB()
+ if not db.enable then return end
+
+ self:CreateButtons()
+ self:ApplyConfig()
+ self:SetupPoller()
+
+ -- Events
+ SFrames:RegisterEvent("ACTIONBAR_SLOT_CHANGED", function()
+ if not EB.buttons then return end
+ local db = EB:GetDB()
+ if not db.enable then return end
+ local slot = arg1
+ local startSlot = db.startSlot or 73
+ local count = math.min(db.buttonCount or 12, MAX_BUTTONS)
+ if slot then
+ local idx = slot - startSlot + 1
+ if idx >= 1 and idx <= count then
+ RefreshButtonAll(EB.buttons[idx])
+ end
+ else
+ EB:RefreshAll()
+ end
+ end)
+
+ SFrames:RegisterEvent("ACTIONBAR_UPDATE_COOLDOWN", function()
+ EB:RefreshCooldowns()
+ end)
+
+ SFrames:RegisterEvent("ACTIONBAR_UPDATE_USABLE", function()
+ EB:RefreshUsable()
+ end)
+
+ SFrames:RegisterEvent("ACTIONBAR_UPDATE_STATE", function()
+ EB:RefreshStates()
+ end)
+
+ SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function()
+ EB:RefreshAll()
+ EB:RefreshHotkeys()
+ end)
+
+ SFrames:RegisterEvent("UPDATE_BINDINGS", function()
+ EB:RefreshHotkeys()
+ end)
+
+ -- Register mover
+ if SFrames.Movers and SFrames.Movers.RegisterMover then
+ SFrames.Movers:RegisterMover("ExtraBar", self.holder, "额外动作条",
+ "CENTER", "UIParent", "CENTER", 0, -200)
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Late enable: called from ConfigUI when user toggles enable on
+--------------------------------------------------------------------------------
+function EB:Enable()
+ local db = self:GetDB()
+ db.enable = true
+ if not self.holder then
+ self:CreateButtons()
+ self:SetupPoller()
+
+ SFrames:RegisterEvent("ACTIONBAR_SLOT_CHANGED", function()
+ if not EB.buttons then return end
+ local db2 = EB:GetDB()
+ if not db2.enable then return end
+ local slot = arg1
+ local startSlot = db2.startSlot or 73
+ local count = math.min(db2.buttonCount or 12, MAX_BUTTONS)
+ if slot then
+ local idx = slot - startSlot + 1
+ if idx >= 1 and idx <= count then
+ RefreshButtonAll(EB.buttons[idx])
+ end
+ else
+ EB:RefreshAll()
+ end
+ end)
+ SFrames:RegisterEvent("ACTIONBAR_UPDATE_COOLDOWN", function()
+ EB:RefreshCooldowns()
+ end)
+ SFrames:RegisterEvent("ACTIONBAR_UPDATE_USABLE", function()
+ EB:RefreshUsable()
+ end)
+ SFrames:RegisterEvent("ACTIONBAR_UPDATE_STATE", function()
+ EB:RefreshStates()
+ end)
+ SFrames:RegisterEvent("UPDATE_BINDINGS", function()
+ EB:RefreshHotkeys()
+ end)
+
+ if SFrames.Movers and SFrames.Movers.RegisterMover then
+ SFrames.Movers:RegisterMover("ExtraBar", self.holder, "额外动作条",
+ "CENTER", "UIParent", "CENTER", 0, -200)
+ end
+ end
+ self:ApplyConfig()
+ self:RefreshHotkeys()
+end
+
+function EB:Disable()
+ local db = self:GetDB()
+ db.enable = false
+ if self.holder then
+ self.holder:Hide()
+ end
+ if self.holder and SFrames.Movers and SFrames.Movers.IsLayoutMode
+ and SFrames.Movers:IsLayoutMode() then
+ pcall(SFrames.Movers.SyncMoverToFrame, SFrames.Movers, "ExtraBar")
+ end
+end
+
+function EB:RunButton(index)
+ if not self.buttons then return end
+ local db = self:GetDB()
+ if not db.enable then return end
+ if index < 1 or index > math.min(db.buttonCount or 12, MAX_BUTTONS) then return end
+ local btn = self.buttons[index]
+ if not btn or not btn:IsVisible() then return end
+ local slot = btn.sfActionSlot
+ if slot and HasAction(slot) then
+ UseAction(slot)
+ RefreshButtonAll(btn)
+ end
+end
diff --git a/Factory.lua b/Factory.lua
index 82d6155..d830719 100644
--- a/Factory.lua
+++ b/Factory.lua
@@ -1,19 +1,48 @@
-- Helper function to generate ElvUI-style backdrop and shadow border
-function SFrames:CreateBackdrop(frame)
- frame:SetBackdrop({
- bgFile = "Interface\\Buttons\\WHITE8X8",
- edgeFile = "Interface\\Buttons\\WHITE8X8",
- tile = false, tileSize = 0, edgeSize = 1,
- insets = { left = 1, right = 1, top = 1, bottom = 1 }
- })
- local A = SFrames.ActiveTheme
- if A and A.panelBg then
- frame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.9)
- frame:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 1)
+function SFrames:ApplyBackdropStyle(frame, opts)
+ opts = opts or {}
+ local radius = tonumber(opts.cornerRadius) or 0
+ local showBorder = opts.showBorder ~= false
+ local useRounded = radius and radius > 0
+
+ if useRounded then
+ local edgeSize = math.max(8, math.min(18, math.floor(radius + 0.5)))
+ frame:SetBackdrop({
+ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
+ edgeFile = showBorder and "Interface\\Tooltips\\UI-Tooltip-Border" or nil,
+ tile = true, tileSize = 16, edgeSize = edgeSize,
+ insets = { left = 3, right = 3, top = 3, bottom = 3 }
+ })
else
- frame:SetBackdropColor(0.1, 0.1, 0.1, 0.9)
- frame:SetBackdropBorderColor(0, 0, 0, 1)
+ frame:SetBackdrop({
+ bgFile = "Interface\\Buttons\\WHITE8X8",
+ edgeFile = showBorder and "Interface\\Buttons\\WHITE8X8" or nil,
+ tile = false, tileSize = 0, edgeSize = 1,
+ insets = { left = 1, right = 1, top = 1, bottom = 1 }
+ })
end
+
+ local A = SFrames.ActiveTheme
+ local bgAlpha = opts.bgAlpha
+ if A and A.panelBg then
+ frame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], bgAlpha or A.panelBg[4] or 0.9)
+ if showBorder then
+ frame:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 1)
+ else
+ frame:SetBackdropBorderColor(0, 0, 0, 0)
+ end
+ else
+ frame:SetBackdropColor(0.1, 0.1, 0.1, bgAlpha or 0.9)
+ if showBorder then
+ frame:SetBackdropBorderColor(0, 0, 0, 1)
+ else
+ frame:SetBackdropBorderColor(0, 0, 0, 0)
+ end
+ end
+end
+
+function SFrames:CreateBackdrop(frame)
+ self:ApplyBackdropStyle(frame)
end
function SFrames:CreateRoundBackdrop(frame)
@@ -38,6 +67,78 @@ function SFrames:CreateUnitBackdrop(frame)
frame:SetBackdropBorderColor(0, 0, 0, 1)
end
+function SFrames:ApplyConfiguredUnitBackdrop(frame, prefix, isPortrait)
+ if not frame then return end
+ local db = SFramesDB or {}
+ local bgAlphaKey = prefix .. (isPortrait and "PortraitBgAlpha" or "BgAlpha")
+ self:ApplyBackdropStyle(frame, {
+ showBorder = false,
+ cornerRadius = 0,
+ bgAlpha = tonumber(db[bgAlphaKey]) or (isPortrait and tonumber(db[prefix .. "BgAlpha"]) or nil),
+ })
+end
+
+--------------------------------------------------------------------------------
+-- Frame Style Preset helpers
+--------------------------------------------------------------------------------
+
+function SFrames:GetFrameStylePreset()
+ return (SFramesDB and SFramesDB.frameStylePreset) or "classic"
+end
+
+function SFrames:IsGradientStyle()
+ return self:GetFrameStylePreset() == "gradient"
+end
+
+-- Apply a gradient darkening overlay on a StatusBar.
+-- Uses a separate Texture on OVERLAY layer with purple-black vertex color
+-- and alpha gradient from 0 (left) to ~0.6 (right).
+-- This approach does NOT touch the StatusBar texture itself,
+-- so it survives SetStatusBarColor calls.
+function SFrames:ApplyGradientStyle(bar)
+ if not bar then return end
+ if not bar._gradOverlay then
+ local ov = bar:CreateTexture(nil, "OVERLAY")
+ ov:SetTexture("Interface\\Buttons\\WHITE8X8")
+ bar._gradOverlay = ov
+ end
+ local ov = bar._gradOverlay
+ ov:ClearAllPoints()
+ ov:SetAllPoints(bar:GetStatusBarTexture())
+ -- Dark purple-ish tint color
+ ov:SetVertexColor(0.04, 0.0, 0.08, 1)
+ -- Alpha gradient: left fully transparent → right 65% opaque
+ if ov.SetGradientAlpha then
+ ov:SetGradientAlpha("HORIZONTAL",
+ 1, 1, 1, 0,
+ 1, 1, 1, 0.65)
+ end
+ ov:Show()
+end
+
+function SFrames:RemoveGradientStyle(bar)
+ if not bar then return end
+ if bar._gradOverlay then
+ bar._gradOverlay:Hide()
+ end
+end
+
+-- No-op wrappers kept for compatibility with unit files
+function SFrames:ApplyBarGradient(bar)
+ -- Gradient is now persistent overlay; no per-color-update needed
+end
+function SFrames:ClearBarGradient(bar)
+ self:RemoveGradientStyle(bar)
+end
+
+-- Strip backdrop from a frame (used in gradient style)
+function SFrames:ClearBackdrop(frame)
+ if not frame then return end
+ if frame.SetBackdrop then
+ frame:SetBackdrop(nil)
+ end
+end
+
-- Generator for StatusBars
function SFrames:CreateStatusBar(parent, name)
local bar = CreateFrame("StatusBar", name, parent)
@@ -50,6 +151,11 @@ function SFrames:CreateStatusBar(parent, name)
return bar
end
+function SFrames:ApplyStatusBarTexture(bar, settingKey, fallbackKey)
+ if not bar or not bar.SetStatusBarTexture then return end
+ bar:SetStatusBarTexture(self:ResolveBarTexture(settingKey, fallbackKey))
+end
+
-- Generator for FontStrings
function SFrames:CreateFontString(parent, size, justifyH)
local fs = parent:CreateFontString(nil, "OVERLAY")
@@ -59,6 +165,29 @@ function SFrames:CreateFontString(parent, size, justifyH)
return fs
end
+function SFrames:ApplyFontString(fs, size, fontSettingKey, fallbackFontKey, outlineSettingKey, fallbackOutlineKey)
+ if not fs or not fs.SetFont then return end
+ local fontPath = self:ResolveFont(fontSettingKey, fallbackFontKey)
+ local outline = self:ResolveFontOutline(outlineSettingKey, fallbackOutlineKey)
+ fs:SetFont(fontPath, size or 12, outline)
+end
+
+function SFrames:FormatCompactNumber(value)
+ local num = tonumber(value) or 0
+ local sign = num < 0 and "-" or ""
+ num = math.abs(num)
+ if num >= 1000000 then
+ return string.format("%s%.1fM", sign, num / 1000000)
+ elseif num >= 10000 then
+ return string.format("%s%.1fK", sign, num / 1000)
+ end
+ return sign .. tostring(math.floor(num + 0.5))
+end
+
+function SFrames:FormatCompactPair(currentValue, maxValue)
+ return self:FormatCompactNumber(currentValue) .. " / " .. self:FormatCompactNumber(maxValue)
+end
+
-- Generator for 3D Portraits
function SFrames:CreatePortrait(parent, name)
local portrait = CreateFrame("PlayerModel", name, parent)
diff --git a/Focus.lua b/Focus.lua
index 937e744..c41e57c 100644
--- a/Focus.lua
+++ b/Focus.lua
@@ -327,17 +327,23 @@ function SFrames.Focus:CreateFocusFrame()
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["FocusFrame"] then
local pos = SFramesDB.Positions["FocusFrame"]
- -- Validate: if GetTop would be near 0 or negative, position is bad
- f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT", pos.xOfs or 250, pos.yOfs or 0)
- -- After setting, check if visible on screen
+ local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
+ if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
+ f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT",
+ (pos.xOfs or 250) / fScale, (pos.yOfs or 0) / fScale)
+ else
+ f:SetPoint(pos.point or "LEFT", UIParent, pos.relativePoint or "LEFT",
+ pos.xOfs or 250, pos.yOfs or 0)
+ end
local top = f:GetTop()
local left = f:GetLeft()
if not top or not left or top < 50 or left < 0 then
- -- Bad position, reset
f:ClearAllPoints()
f:SetPoint("LEFT", UIParent, "LEFT", 250, 0)
SFramesDB.Positions["FocusFrame"] = nil
end
+ elseif SFramesTargetFrame then
+ f:SetPoint("TOPLEFT", SFramesTargetFrame, "BOTTOMLEFT", 0, -75)
else
f:SetPoint("LEFT", UIParent, "LEFT", 250, 0)
end
@@ -352,6 +358,11 @@ function SFrames.Focus:CreateFocusFrame()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, _, relativePoint, xOfs, yOfs = this:GetPoint()
+ local fScale = this:GetEffectiveScale() / UIParent:GetEffectiveScale()
+ if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
+ xOfs = (xOfs or 0) * fScale
+ yOfs = (yOfs or 0) * fScale
+ end
SFramesDB.Positions["FocusFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
end)
@@ -546,37 +557,27 @@ function SFrames.Focus:CreateFocusFrame()
SFrames:CreateUnitBackdrop(f)
- -- Portrait (right side) — EnableMouse(false) so clicks pass through to main Button
- local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
- f.portrait = CreateFrame("PlayerModel", nil, f)
+ -- Portrait placeholder (hidden, focus frame does not use 3D portraits)
+ f.portrait = CreateFrame("Frame", nil, f)
f.portrait:SetWidth(pWidth)
f.portrait:SetHeight(totalH - 2)
f.portrait:SetPoint("RIGHT", f, "RIGHT", -1, 0)
f.portrait:EnableMouse(false)
+ f.portrait:Hide()
local pbg = CreateFrame("Frame", nil, f)
pbg:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -1, 0)
pbg:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
pbg:SetFrameLevel(f:GetFrameLevel())
- SFrames:CreateUnitBackdrop(pbg)
f.portraitBG = pbg
pbg:EnableMouse(false)
+ pbg:Hide()
- if not showPortrait then
- f.portrait:Hide()
- pbg:Hide()
- end
-
- -- Health bar
+ -- Health bar (full width, no portrait)
f.health = SFrames:CreateStatusBar(f, "SFramesFocusHealth")
f.health:EnableMouse(false)
- if showPortrait then
- f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
- f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0)
- else
- f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
- f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
- end
+ f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
+ f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
f.health:SetHeight(hHeight)
local hbg = CreateFrame("Frame", nil, f)
@@ -596,11 +597,7 @@ function SFrames.Focus:CreateFocusFrame()
f.power = SFrames:CreateStatusBar(f, "SFramesFocusPower")
f.power:EnableMouse(false)
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
- if showPortrait then
- f.power:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMLEFT", -1, 0)
- else
- f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
- end
+ f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
local powerbg = CreateFrame("Frame", nil, f)
powerbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1)
@@ -615,9 +612,9 @@ function SFrames.Focus:CreateFocusFrame()
f.power.bg:SetTexture(SFrames:GetTexture())
f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
- -- Class icon
+ -- Class icon (anchored to frame top-right corner)
f.classIcon = SFrames:CreateClassIcon(f, 14)
- f.classIcon.overlay:SetPoint("CENTER", f.portrait, "TOPRIGHT", 0, 0)
+ f.classIcon.overlay:SetPoint("CENTER", f, "TOPRIGHT", 0, 0)
f.classIcon.overlay:EnableMouse(false)
-- Texts
@@ -747,12 +744,7 @@ function SFrames.Focus:CreateCastbar()
local cb = SFrames:CreateStatusBar(self.frame, "SFramesFocusCastbar")
cb:SetHeight(cbH)
cb:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", 0, 6)
- local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
- if showPortrait then
- cb:SetPoint("BOTTOMRIGHT", self.frame.portrait, "TOPRIGHT", -(cbH + 6), 6)
- else
- cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", -(cbH + 6), 6)
- end
+ cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", -(cbH + 6), 6)
local cbbg = CreateFrame("Frame", nil, self.frame)
cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1)
@@ -816,7 +808,6 @@ function SFrames.Focus:UpdateAll()
self.frame.power:SetMinMaxValues(0, 1)
self.frame.power:SetValue(0)
self.frame.powerText:SetText("")
- if self.frame.portrait then self.frame.portrait:Hide() end
if self.frame.classIcon then self.frame.classIcon:Hide(); if self.frame.classIcon.overlay then self.frame.classIcon.overlay:Hide() end end
self.frame.raidIcon:Hide()
self:HideAuras()
@@ -830,19 +821,6 @@ function SFrames.Focus:UpdateAll()
self:UpdateRaidIcon()
self:UpdateAuras()
- local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
- if showPortrait and self.frame.portrait then
- -- Only reset portrait model on first load / focus change (not every update)
- if not self.frame._lastPortraitUID or self.frame._lastPortraitUID ~= uid then
- self.frame.portrait:SetUnit(uid)
- self.frame.portrait:SetCamera(0)
- self.frame.portrait:Hide()
- self.frame.portrait:Show()
- self.frame.portrait:SetPosition(-1.0, 0, 0)
- self.frame._lastPortraitUID = uid
- end
- end
-
local name = UnitName(uid) or ""
local level = UnitLevel(uid)
local levelText = level
@@ -888,6 +866,7 @@ function SFrames.Focus:UpdateAll()
-- Color by class or reaction
local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false)
+ if SFrames:IsGradientStyle() then useClassColor = true end
if UnitIsPlayer(uid) and useClassColor then
local _, class = UnitClass(uid)
if class and SFrames.Config.colors.class[class] then
@@ -911,6 +890,9 @@ function SFrames.Focus:UpdateAll()
self.frame.nameText:SetText(formattedLevel .. name)
self.frame.nameText:SetTextColor(r, g, b)
end
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyBarGradient(self.frame.health)
+ end
end
function SFrames.Focus:HideAuras()
@@ -933,7 +915,7 @@ function SFrames.Focus:UpdateHealth()
self.frame.health:SetValue(hp)
if maxHp > 0 then
local pct = math.floor(hp / maxHp * 100)
- self.frame.healthText:SetText(hp .. " / " .. maxHp .. " (" .. pct .. "%)")
+ self.frame.healthText:SetText(SFrames:FormatCompactPair(hp, maxHp) .. " (" .. pct .. "%)")
else
self.frame.healthText:SetText("")
end
@@ -950,6 +932,9 @@ function SFrames.Focus:UpdatePowerType()
else
self.frame.power:SetStatusBarColor(0, 0, 1)
end
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyBarGradient(self.frame.power)
+ end
end
-- STUB: UpdatePower
@@ -961,10 +946,11 @@ function SFrames.Focus:UpdatePower()
self.frame.power:SetMinMaxValues(0, maxPower)
self.frame.power:SetValue(power)
if maxPower > 0 then
- self.frame.powerText:SetText(power .. " / " .. maxPower)
+ self.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else
self.frame.powerText:SetText("")
end
+ SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, uid)
end
-- STUB: UpdateRaidIcon
@@ -1045,8 +1031,8 @@ function SFrames.Focus:CastbarOnUpdate()
-- 1) Native UnitCastingInfo / UnitChannelInfo (if available)
if uid then
- local _UCI = UnitCastingInfo or (CastingInfo and function(u) return CastingInfo(u) end)
- local _UCH = UnitChannelInfo or (ChannelInfo and function(u) return ChannelInfo(u) end)
+ local _UCI = UnitCastingInfo or CastingInfo
+ local _UCH = UnitChannelInfo or ChannelInfo
if _UCI then
local ok, cSpell, _, _, cIcon, cStart, cEnd = pcall(_UCI, uid)
if ok and cSpell and cStart then
@@ -1177,7 +1163,6 @@ function SFrames.Focus:OnFocusChanged()
local name = self:GetFocusName()
if name then
self.frame:Show()
- self.frame._lastPortraitUID = nil -- Force portrait refresh on focus change
self:UpdateAll()
else
self.frame:Hide()
@@ -1206,9 +1191,28 @@ function SFrames.Focus:ApplySettings()
local bgAlpha = tonumber(SFramesDB and SFramesDB.focusBgAlpha) or 0.9
local nameFontSize = tonumber(SFramesDB and SFramesDB.focusNameFontSize) or 11
local valueFontSize = tonumber(SFramesDB and SFramesDB.focusValueFontSize) or 10
- local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
local showCastBar = not (SFramesDB and SFramesDB.focusShowCastBar == false)
local showAuras = not (SFramesDB and SFramesDB.focusShowAuras == false)
+ local powerOnTop = SFramesDB and SFramesDB.focusPowerOnTop == true
+ local gradientStyle = SFrames:IsGradientStyle()
+ local defaultPowerWidth = width - 2
+ if defaultPowerWidth < 60 then
+ defaultPowerWidth = 60
+ end
+ local rawPowerWidth = tonumber(SFramesDB and SFramesDB.focusPowerWidth)
+ local legacyFullWidth = tonumber(SFramesDB and SFramesDB.focusFrameWidth) or width
+ local maxPowerWidth = gradientStyle and width or (width - 2)
+ local powerWidth
+ if gradientStyle then
+ powerWidth = width
+ elseif not rawPowerWidth or math.abs(rawPowerWidth - legacyFullWidth) < 0.5 then
+ powerWidth = defaultPowerWidth
+ else
+ powerWidth = rawPowerWidth
+ end
+ powerWidth = math.floor(powerWidth + 0.5)
+ if powerWidth < 60 then powerWidth = 60 end
+ if powerWidth > maxPowerWidth then powerWidth = maxPowerWidth end
-- Main frame size & scale
f:SetWidth(width)
@@ -1222,40 +1226,75 @@ function SFrames.Focus:ApplySettings()
f:SetBackdropColor(r, g, b, bgAlpha)
end
- -- Portrait
- if f.portrait then
- f.portrait:SetWidth(pWidth)
- f.portrait:SetHeight(totalH - 2)
- if showPortrait then
- f.portrait:Show()
- if f.portraitBG then f.portraitBG:Show() end
- else
- f.portrait:Hide()
- if f.portraitBG then f.portraitBG:Hide() end
- end
- end
+ -- Portrait always hidden (focus frame uses class icon only)
+ if f.portrait then f.portrait:Hide() end
+ if f.portraitBG then f.portraitBG:Hide() end
- -- Health bar anchors
+ -- Health bar anchors (always full width, no portrait)
if f.health then
f.health:ClearAllPoints()
f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
- if showPortrait then
- f.health:SetPoint("TOPRIGHT", f.portrait, "TOPLEFT", -1, 0)
- else
- f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
- end
+ f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, 0)
f.health:SetHeight(hHeight)
end
-- Power bar anchors
if f.power then
f.power:ClearAllPoints()
- f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
- if showPortrait then
- f.power:SetPoint("BOTTOMRIGHT", f.portrait, "BOTTOMLEFT", -1, 0)
- else
- f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
+ f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", tonumber(SFramesDB and SFramesDB.focusPowerOffsetX) or 0, -1 + (tonumber(SFramesDB and SFramesDB.focusPowerOffsetY) or 0))
+ f.power:SetWidth(powerWidth)
+ f.power:SetHeight(pHeight)
+ end
+
+ if f.health and f.power then
+ local healthLevel = f:GetFrameLevel() + 2
+ local powerLevel = powerOnTop and (healthLevel + 1) or (healthLevel - 1)
+ f.health:SetFrameLevel(healthLevel)
+ f.power:SetFrameLevel(powerLevel)
+ end
+
+ if SFrames:IsGradientStyle() then
+ SFrames:ClearBackdrop(f)
+ SFrames:ClearBackdrop(f.healthBGFrame)
+ SFrames:ClearBackdrop(f.powerBGFrame)
+ if f.health then
+ f.health:ClearAllPoints()
+ f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
+ f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
+ f.health:SetHeight(hHeight)
end
+ if f.power then
+ f.power:ClearAllPoints()
+ f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", tonumber(SFramesDB and SFramesDB.focusPowerOffsetX) or 0, -2 + (tonumber(SFramesDB and SFramesDB.focusPowerOffsetY) or 0))
+ f.power:SetWidth(powerWidth)
+ f.power:SetHeight(pHeight)
+ end
+ SFrames:ApplyGradientStyle(f.health)
+ SFrames:ApplyGradientStyle(f.power)
+ if f.healthBGFrame then
+ f.healthBGFrame:ClearAllPoints()
+ f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", 0, 0)
+ f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
+ f.healthBGFrame:Hide()
+ end
+ if f.powerBGFrame then
+ f.powerBGFrame:ClearAllPoints()
+ f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", 0, 0)
+ f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 0, 0)
+ f.powerBGFrame:Hide()
+ end
+ if f.health and f.health.bg then f.health.bg:Hide() end
+ if f.power and f.power.bg then f.power.bg:Hide() end
+ else
+ SFrames:ApplyConfiguredUnitBackdrop(f, "focus")
+ if f.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.healthBGFrame, "focus") end
+ if f.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.powerBGFrame, "focus") end
+ SFrames:RemoveGradientStyle(f.health)
+ SFrames:RemoveGradientStyle(f.power)
+ if f.healthBGFrame then f.healthBGFrame:Show() end
+ if f.powerBGFrame then f.powerBGFrame:Show() end
+ if f.health and f.health.bg then f.health.bg:Show() end
+ if f.power and f.power.bg then f.power.bg:Show() end
end
-- Castbar anchors
@@ -1264,11 +1303,7 @@ function SFrames.Focus:ApplySettings()
local cbH = 12
f.castbar:SetHeight(cbH)
f.castbar:SetPoint("BOTTOMLEFT", f, "TOPLEFT", 0, 6)
- if showPortrait then
- f.castbar:SetPoint("BOTTOMRIGHT", f.portrait, "TOPRIGHT", -(cbH + 6), 6)
- else
- f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -(cbH + 6), 6)
- end
+ f.castbar:SetPoint("BOTTOMRIGHT", f, "TOPRIGHT", -(cbH + 6), 6)
if not showCastBar then
f.castbar:Hide()
if f.castbar.cbbg then f.castbar.cbbg:Hide() end
@@ -1298,8 +1333,6 @@ function SFrames.Focus:ApplySettings()
self:UpdateAuras()
end
- -- Force portrait refresh
- f._lastPortraitUID = nil
if self:GetFocusName() then
self:UpdateAll()
end
@@ -1336,7 +1369,6 @@ function SFrames.Focus:Initialize()
ef:RegisterEvent("UNIT_MAXRAGE")
ef:RegisterEvent("UNIT_DISPLAYPOWER")
ef:RegisterEvent("UNIT_AURA")
- ef:RegisterEvent("UNIT_PORTRAIT_UPDATE")
ef:RegisterEvent("UNIT_TARGET")
ef:RegisterEvent("RAID_TARGET_UPDATE")
-- Combat log events for castbar detection (non-SuperWoW fallback)
@@ -1378,7 +1410,6 @@ function SFrames.Focus:Initialize()
if event == "PLAYER_TARGET_CHANGED" then
local focusName = focusSelf:GetFocusName()
if focusName and UnitExists("target") and UnitName("target") == focusName then
- focusSelf.frame._lastPortraitUID = nil
focusSelf:UpdateAll()
-- Try to grab GUID while we have target
if UnitGUID then
@@ -1512,7 +1543,7 @@ function SFrames.Focus:Initialize()
focusSelf.frame.health:SetValue(hp)
if maxHp > 0 then
local pct = math.floor(hp / maxHp * 100)
- focusSelf.frame.healthText:SetText(hp .. " / " .. maxHp .. " (" .. pct .. "%)")
+ focusSelf.frame.healthText:SetText(SFrames:FormatCompactPair(hp, maxHp) .. " (" .. pct .. "%)")
else
focusSelf.frame.healthText:SetText("")
end
@@ -1526,7 +1557,7 @@ function SFrames.Focus:Initialize()
focusSelf.frame.power:SetMinMaxValues(0, maxPower)
focusSelf.frame.power:SetValue(power)
if maxPower > 0 then
- focusSelf.frame.powerText:SetText(power .. " / " .. maxPower)
+ focusSelf.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else
focusSelf.frame.powerText:SetText("")
end
@@ -1545,22 +1576,13 @@ function SFrames.Focus:Initialize()
focusSelf.frame.power:SetMinMaxValues(0, maxPower)
focusSelf.frame.power:SetValue(power)
if maxPower > 0 then
- focusSelf.frame.powerText:SetText(power .. " / " .. maxPower)
+ focusSelf.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else
focusSelf.frame.powerText:SetText("")
end
end
elseif event == "UNIT_AURA" then
focusSelf:UpdateAuras()
- elseif event == "UNIT_PORTRAIT_UPDATE" then
- local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
- if showPortrait and focusSelf.frame.portrait and evtUID then
- focusSelf.frame._lastPortraitUID = nil
- focusSelf.frame.portrait:SetUnit(evtUID)
- focusSelf.frame.portrait:SetCamera(0)
- focusSelf.frame.portrait:SetPosition(-1.0, 0, 0)
- focusSelf.frame._lastPortraitUID = evtUID
- end
end
end
end)
@@ -1585,8 +1607,6 @@ function SFrames.Focus:Initialize()
focusSelf.frame:Show()
end
- -- Re-scan for a valid unitID every poll cycle
- -- This catches cases where focus becomes target/party/raid dynamically
local uid = focusSelf:GetUnitID()
if uid then
focusSelf:UpdateHealth()
@@ -1594,31 +1614,19 @@ function SFrames.Focus:Initialize()
focusSelf:UpdatePower()
focusSelf:UpdateAuras()
focusSelf:UpdateRaidIcon()
-
- -- Only refresh portrait when unitID changes (prevents 3D model flicker)
- local showPortrait = not (SFramesDB and SFramesDB.focusShowPortrait == false)
- if showPortrait and focusSelf.frame.portrait then
- if uid ~= ef.lastUID then
- focusSelf.frame.portrait:SetUnit(uid)
- focusSelf.frame.portrait:SetCamera(0)
- focusSelf.frame.portrait:SetPosition(-1.0, 0, 0)
- focusSelf.frame.portrait:Show()
- ef.lastUID = uid
- end
- end
- else
- ef.lastUID = nil
end
end)
- -- Register mover
+ -- Register mover (Y aligned with pet frame, X aligned with target frame)
if SFrames.Movers and SFrames.Movers.RegisterMover then
if SFramesTargetFrame then
SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点",
- "TOPLEFT", "SFramesTargetFrame", "BOTTOMLEFT", 0, -10)
+ "TOPLEFT", "SFramesTargetFrame", "BOTTOMLEFT", 0, -75,
+ nil, { alwaysShowInLayout = true })
else
SFrames.Movers:RegisterMover("FocusFrame", self.frame, "焦点",
- "LEFT", "UIParent", "LEFT", 250, 0)
+ "LEFT", "UIParent", "LEFT", 250, 0,
+ nil, { alwaysShowInLayout = true })
end
end
diff --git a/GearScore.lua b/GearScore.lua
index 9037ea2..3c73df7 100644
--- a/GearScore.lua
+++ b/GearScore.lua
@@ -1364,20 +1364,6 @@ function GS:HookTooltips()
end
end
- local origRef = SetItemRef
- if origRef then
- SetItemRef = function(link, text, button)
- origRef(link, text, button)
- if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then return end
- pcall(function()
- local _, _, itemStr = string.find(link or "", "(item:[%-?%d:]+)")
- if itemStr then
- ItemRefTooltip._gsScoreAdded = nil
- GS:AddScoreToTooltip(ItemRefTooltip, itemStr)
- end
- end)
- end
- end
end
--------------------------------------------------------------------------------
diff --git a/LootDisplay.lua b/LootDisplay.lua
index c03b088..6289d3c 100644
--- a/LootDisplay.lua
+++ b/LootDisplay.lua
@@ -496,20 +496,31 @@ ShowLootPage = function()
row:Show()
end
- -- Let the ORIGINAL Blizzard LootFrame_Update run so that native
- -- LootButton1-4 get their IDs, slot data, and OnClick set up
- -- through the trusted native code path (required for LootSlot).
+ -- Set up the native LootFrame so it stays alive (required by the
+ -- engine) but completely invisible. We do NOT call origLootFrameUpdate
+ -- because it uses a different items-per-page (3 when paginated vs our 4)
+ -- which mis-calculates slot indices.
if LootFrame then
+ LootFrame.numLootItems = numItems
LootFrame.page = page
if not LootFrame:IsShown() then LootFrame:Show() end
end
- if origLootFrameUpdate then origLootFrameUpdate() end
- -- Now reposition the native buttons on top of our visual rows
+ -- Directly configure native LootButtons with the correct slot for
+ -- each visual row. SetSlot is the C++ binding that the native
+ -- LootButton_OnClick / LootFrameItem_OnClick reads via this.slot.
for btnIdx = 1, ITEMS_PER_PAGE do
local nb = _G["LootButton" .. btnIdx]
local row = lootRows[btnIdx]
if nb and row and row:IsShown() and row._qualColor then
+ local realSlot = row.slotIndex
+
+ -- SetSlot is the native C++ method; .slot is the Lua mirror.
+ if nb.SetSlot then nb:SetSlot(realSlot) end
+ nb.slot = realSlot
+ local _, _, _, rq = GetLootSlotInfo(realSlot)
+ nb.quality = rq or 0
+
nb:ClearAllPoints()
nb:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
nb:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
@@ -521,10 +532,10 @@ ShowLootPage = function()
nb._nanamiRow = row
nb:SetScript("OnEnter", function()
- local slot = this:GetID()
- if slot then
+ local s = this.slot
+ if s then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
- GameTooltip:SetLootItem(slot)
+ GameTooltip:SetLootItem(s)
if CursorUpdate then CursorUpdate() end
end
local r2 = this._nanamiRow
@@ -724,14 +735,15 @@ local function GetAlertFrame()
return CreateAlertFrame()
end
+-- Layout: newest item at bottom (index 1 = oldest = top, last = newest = bottom slot 0)
local function LayoutAlerts()
CreateAlertAnchor()
- for i = 1, table.getn(activeAlerts) do
+ local n = table.getn(activeAlerts)
+ for i = 1, n 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
+ -- oldest at top, newest at bottom: slot = (n - i)
+ af:ClearAllPoints()
+ af:SetPoint("BOTTOMLEFT", alertAnchor, "BOTTOMLEFT", 0, (n - i) * (ALERT_HEIGHT + ALERT_GAP))
end
end
@@ -750,36 +762,15 @@ local function RemoveAlert(frame)
LayoutAlerts()
end
-local function StartAlertFade(frame, delay)
- frame._fadeState = "waiting"
- frame._fadeElapsed = 0
- frame._fadeDelay = delay
+-- Each alert has its own independent timer; when it expires, just disappear (no float animation)
+local function StartAlertTimer(frame, delay)
+ frame._timerElapsed = 0
+ frame._timerDelay = 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
+ this._timerElapsed = (this._timerElapsed or 0) + arg1
+ if this._timerElapsed >= this._timerDelay then
+ RemoveAlert(this)
end
end)
end
@@ -794,14 +785,15 @@ local function ShowLootAlert(texture, name, quality, quantity, link)
local db = GetDB()
if not db.alertEnable then return end
+ -- Stack same item: update count and reset timer
for i = 1, table.getn(activeAlerts) do
local af = activeAlerts[i]
- if af._itemName == name and af._fadeState == "waiting" then
+ if af._itemName == name 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
+ af._timerElapsed = 0
return
end
end
@@ -815,7 +807,6 @@ local function ShowLootAlert(texture, name, quality, quantity, link)
f._quantity = quantity or 1
f._link = link
- -- Set icon texture
local iconTex = texture or "Interface\\Icons\\INV_Misc_QuestionMark"
f.icon:SetTexture(iconTex)
@@ -841,12 +832,12 @@ local function ShowLootAlert(texture, name, quality, quantity, link)
f:SetAlpha(1)
f:Show()
+ -- New item appended to end of list = bottom position
table.insert(activeAlerts, f)
LayoutAlerts()
local hold = db.alertFadeDelay or ALERT_HOLD
- local stagger = table.getn(activeAlerts) * ALERT_STAGGER
- StartAlertFade(f, hold + stagger)
+ StartAlertTimer(f, hold)
end
--------------------------------------------------------------------------------
@@ -1000,8 +991,9 @@ function LD:Initialize()
end
end
- -- After the native LootFrame_Update runs (called by the engine or
- -- by us), reposition native buttons onto our visual rows.
+ -- Replace LootFrame_Update: run the original for engine compatibility,
+ -- then re-apply the correct slot on each native button based on our
+ -- visual rows (which use ITEMS_PER_PAGE=4, not the native 3-when-paged).
LootFrame_Update = function()
if origLootFrameUpdate then origLootFrameUpdate() end
if not (lootFrame and lootFrame:IsShown()) then return end
@@ -1009,6 +1001,11 @@ function LD:Initialize()
local nb = _G["LootButton" .. i]
local row = lootRows[i]
if nb and row and row:IsShown() and row._qualColor then
+ local realSlot = row.slotIndex
+ if realSlot then
+ if nb.SetSlot then nb:SetSlot(realSlot) end
+ nb.slot = realSlot
+ end
nb:ClearAllPoints()
nb:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
nb:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
diff --git a/Mail.lua b/Mail.lua
index 7e4ea99..37bc932 100644
--- a/Mail.lua
+++ b/Mail.lua
@@ -45,6 +45,7 @@ local S = {
inboxRows = {},
currentTab = 1,
inboxPage = 1,
+ bagPage = 1,
inboxChecked = {},
collectQueue = {},
collectTimer = nil,
@@ -53,11 +54,23 @@ local S = {
isSending = false,
collectElapsed = 0,
multiSend = nil, -- active multi-send state table
+ codMode = false, -- send panel: 付款取信 mode toggle
}
local L = {
W = 360, H = 480, HEADER = 34, PAD = 12, TAB_H = 28,
BOTTOM = 46, ROWS = 8, ROW_H = 38, ICON = 30, MAX_SEND = 12,
+ BAG_SLOT = 38, BAG_GAP = 3, BAG_PER_ROW = 8, BAG_ROWS = 8,
+}
+
+StaticPopupDialogs["NANAMI_MAIL_COD_CONFIRM"] = {
+ text = "确认支付 %s 取回此物品?",
+ button1 = "确认",
+ button2 = "取消",
+ OnAccept = function() end,
+ timeout = 0,
+ whileDead = true,
+ hideOnEscape = true,
}
--------------------------------------------------------------------------------
@@ -476,18 +489,27 @@ local function UpdateInbox()
if money and money > 0 then
row.moneyFrame:SetMoney(money)
elseif CODAmount and CODAmount > 0 then
- row.codFS:SetText("COD:"); row.codFS:Show()
+ row.codFS:SetText("付款:"); row.codFS:Show()
row.moneyFrame:SetMoney(CODAmount)
else
row.moneyFrame:SetMoney(0)
end
row.expiryFS:SetText(FormatExpiry(daysLeft))
- local canTake = (hasItem or (money and money > 0)) and (not CODAmount or CODAmount == 0)
+ local canTake = hasItem or (money and money > 0)
row.takeBtn:SetDisabled(not canTake)
row.takeBtn:SetScript("OnClick", function()
if row.mailIndex then
if hasItem then
- TakeInboxItem(row.mailIndex)
+ if CODAmount and CODAmount > 0 then
+ local codStr = FormatMoneyString(CODAmount)
+ local idx = row.mailIndex
+ StaticPopupDialogs["NANAMI_MAIL_COD_CONFIRM"].OnAccept = function()
+ TakeInboxItem(idx)
+ end
+ StaticPopup_Show("NANAMI_MAIL_COD_CONFIRM", codStr)
+ else
+ TakeInboxItem(row.mailIndex)
+ end
elseif money and money > 0 then
local idx = row.mailIndex
TakeInboxMoney(idx)
@@ -543,7 +565,7 @@ end
local function StopCollecting()
S.isCollecting = false; S.collectQueue = {}; S.collectPendingDelete = nil
if S.collectTimer then S.collectTimer:SetScript("OnUpdate", nil) end
- UpdateInbox()
+ if S.currentTab == 3 then ML:UpdateMailBag() else UpdateInbox() end
end
local function ProcessCollectQueue()
@@ -683,6 +705,8 @@ end
local function ResetSendForm()
if not S.frame then return end
ClearSendItems()
+ S.codMode = false
+ if S.frame.UpdateMoneyToggle then S.frame.UpdateMoneyToggle() end
if S.frame.toEditBox then S.frame.toEditBox:SetText("") end
if S.frame.subjectEditBox then S.frame.subjectEditBox:SetText("") end
if S.frame.bodyEditBox then S.frame.bodyEditBox:SetText("") end
@@ -733,8 +757,15 @@ local function DoMultiSend(recipient, subject, body, money)
local items = {}
for i = 1, table.getn(S.sendQueue) do table.insert(items, S.sendQueue[i]) end
- -- No attachments: plain text / money mail
+ -- No attachments: plain text / money mail (付款取信 requires attachments)
if table.getn(items) == 0 then
+ if S.codMode and money and money > 0 then
+ DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 付款取信模式需要添加附件物品")
+ if S.frame and S.frame.sendBtn then
+ S.frame.sendBtn.label:SetText("发送"); S.frame.sendBtn:SetDisabled(false)
+ end
+ return
+ end
if money and money > 0 then SetSendMailMoney(money) end
SendMail(recipient, subject, body or "")
return
@@ -746,6 +777,7 @@ local function DoMultiSend(recipient, subject, body, money)
subject = subject or "",
body = body or "",
money = money,
+ codMode = S.codMode,
total = table.getn(items),
sentCount = 0,
phase = "attach", -- "attach" → "wait_send" → "cooldown" → "attach" ...
@@ -803,9 +835,13 @@ local function DoMultiSend(recipient, subject, body, money)
return
end
- -- Money only on first mail
- if ms.sentCount == 1 and ms.money and ms.money > 0 then
- SetSendMailMoney(ms.money)
+ -- Money or 付款取信
+ if ms.money and ms.money > 0 then
+ if ms.codMode then
+ SetSendMailCOD(ms.money)
+ elseif ms.sentCount == 1 then
+ SetSendMailMoney(ms.money)
+ end
end
-- Send this single-attachment mail
@@ -903,13 +939,18 @@ local function BuildMainFrame()
sep:SetPoint("TOPRIGHT", f, "TOPRIGHT", -6, -L.HEADER)
sep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
- local tabInbox = CreateTabBtn(f, "收件箱", 70)
+ local tabInbox = CreateTabBtn(f, "收件箱", 62)
tabInbox:SetPoint("TOPLEFT", f, "TOPLEFT", L.PAD, -(L.HEADER + 6))
tabInbox:SetScript("OnClick", function() S.currentTab = 1; ML:ShowInboxPanel() end)
f.tabInbox = tabInbox
- local tabSend = CreateTabBtn(f, "发送", 70)
- tabSend:SetPoint("LEFT", tabInbox, "RIGHT", 4, 0)
+ local tabBag = CreateTabBtn(f, "邮包", 50)
+ tabBag:SetPoint("LEFT", tabInbox, "RIGHT", 4, 0)
+ tabBag:SetScript("OnClick", function() S.currentTab = 3; ML:ShowMailBagPanel() end)
+ f.tabBag = tabBag
+
+ local tabSend = CreateTabBtn(f, "发送", 62)
+ tabSend:SetPoint("LEFT", tabBag, "RIGHT", 4, 0)
tabSend:SetScript("OnClick", function() S.currentTab = 2; ML:ShowSendPanel() end)
f.tabSend = tabSend
@@ -1128,16 +1169,25 @@ function ML:ShowMailDetail(mailIndex)
dp.detailMoney:SetMoney(money); dp.detailMoney:Show()
end
if CODAmount and CODAmount > 0 then
- dp.detailCodLabel:SetText("COD:"); dp.detailCodLabel:Show()
+ dp.detailCodLabel:SetText("付款取信:"); dp.detailCodLabel:Show()
dp.detailCod:SetMoney(CODAmount); dp.detailCod:Show()
end
-- Take items button
- local canTakeItem = hasItem and (not CODAmount or CODAmount == 0)
+ local canTakeItem = hasItem
dp.takeItemBtn:SetDisabled(not canTakeItem)
dp.takeItemBtn:SetScript("OnClick", function()
if S.detailMailIndex then
- TakeInboxItem(S.detailMailIndex)
+ if CODAmount and CODAmount > 0 then
+ local codStr = FormatMoneyString(CODAmount)
+ local idx = S.detailMailIndex
+ StaticPopupDialogs["NANAMI_MAIL_COD_CONFIRM"].OnAccept = function()
+ TakeInboxItem(idx)
+ end
+ StaticPopup_Show("NANAMI_MAIL_COD_CONFIRM", codStr)
+ else
+ TakeInboxItem(S.detailMailIndex)
+ end
end
end)
@@ -1205,6 +1255,294 @@ function ML:HideMailDetail()
UpdateInbox()
end
+--------------------------------------------------------------------------------
+-- BUILD: Mail Bag panel (grid view of all inbox items)
+--------------------------------------------------------------------------------
+local function BuildMailBagPanel()
+ local f = S.frame
+ local panelTop = L.HEADER + 6 + L.TAB_H + 4
+ local bp = CreateFrame("Frame", nil, f)
+ bp:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -panelTop)
+ bp:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
+ bp:Hide()
+ f.bagPanel = bp
+
+ local font = GetFont()
+ local slotsPerPage = L.BAG_PER_ROW * L.BAG_ROWS
+
+ local infoFS = bp:CreateFontString(nil, "OVERLAY")
+ infoFS:SetFont(font, 10, "OUTLINE")
+ infoFS:SetPoint("TOPLEFT", bp, "TOPLEFT", L.PAD, -2)
+ infoFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
+ f.bagInfoFS = infoFS
+
+ local codLegend = bp:CreateFontString(nil, "OVERLAY")
+ codLegend:SetFont(font, 9, "OUTLINE")
+ codLegend:SetPoint("TOPRIGHT", bp, "TOPRIGHT", -L.PAD, -2)
+ codLegend:SetText("|cFFFF5555■|r 付款取信")
+ codLegend:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
+
+ f.bagSlots = {}
+ local gridTop = 16
+ for i = 1, slotsPerPage do
+ local row = math.floor((i - 1) / L.BAG_PER_ROW)
+ local col = math.mod((i - 1), L.BAG_PER_ROW)
+ local sf = CreateFrame("Button", "SFramesMailBagSlot" .. i, bp)
+ sf:SetWidth(L.BAG_SLOT); sf:SetHeight(L.BAG_SLOT)
+ sf:SetPoint("TOPLEFT", bp, "TOPLEFT",
+ L.PAD + col * (L.BAG_SLOT + L.BAG_GAP),
+ -(gridTop + row * (L.BAG_SLOT + L.BAG_GAP)))
+ sf: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 },
+ })
+ sf:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
+ sf:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
+
+ local ico = sf:CreateTexture(nil, "ARTWORK")
+ ico:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+ ico:SetPoint("TOPLEFT", 3, -3); ico:SetPoint("BOTTOMRIGHT", -3, 3)
+ ico:Hide()
+ sf.icon = ico
+
+ local cnt = sf:CreateFontString(nil, "OVERLAY")
+ cnt:SetFont(font, 11, "OUTLINE")
+ cnt:SetPoint("BOTTOMRIGHT", sf, "BOTTOMRIGHT", -2, 2)
+ cnt:SetJustifyH("RIGHT")
+ sf.countFS = cnt
+
+ local moneyFS = sf:CreateFontString(nil, "OVERLAY")
+ moneyFS:SetFont(font, 8, "OUTLINE")
+ moneyFS:SetPoint("BOTTOM", sf, "BOTTOM", 0, 2)
+ moneyFS:SetWidth(L.BAG_SLOT)
+ moneyFS:SetJustifyH("CENTER")
+ moneyFS:Hide()
+ sf.moneyFS = moneyFS
+
+ local codFS = sf:CreateFontString(nil, "OVERLAY")
+ codFS:SetFont(font, 7, "OUTLINE")
+ codFS:SetPoint("TOP", sf, "TOP", 0, -1)
+ codFS:SetText("|cFFFF3333付款|r")
+ codFS:Hide()
+ sf.codFS = codFS
+
+ sf.mailData = nil
+ sf:RegisterForClicks("LeftButtonUp")
+ sf:SetScript("OnClick", function()
+ local data = this.mailData
+ if not data then return end
+ if data.codAmount and data.codAmount > 0 then
+ local codStr = FormatMoneyString(data.codAmount)
+ local idx = data.mailIndex
+ StaticPopupDialogs["NANAMI_MAIL_COD_CONFIRM"].OnAccept = function()
+ TakeInboxItem(idx)
+ end
+ StaticPopup_Show("NANAMI_MAIL_COD_CONFIRM", codStr)
+ elseif data.hasItem then
+ TakeInboxItem(data.mailIndex)
+ elseif data.money and data.money > 0 then
+ local idx = data.mailIndex
+ TakeInboxMoney(idx)
+ if not S.deleteTimer then S.deleteTimer = CreateFrame("Frame") end
+ S.deleteElapsed = 0
+ S.deleteTimer:SetScript("OnUpdate", function()
+ S.deleteElapsed = S.deleteElapsed + arg1
+ if S.deleteElapsed >= 0.5 then
+ this:SetScript("OnUpdate", nil)
+ if idx <= GetInboxNumItems() then DeleteInboxItem(idx) end
+ end
+ end)
+ end
+ end)
+ sf:SetScript("OnEnter", function()
+ this:SetBackdropBorderColor(T.slotHover[1], T.slotHover[2], T.slotHover[3], T.slotHover[4])
+ local data = this.mailData
+ if not data then return end
+ GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
+ if data.hasItem then
+ pcall(GameTooltip.SetInboxItem, GameTooltip, data.mailIndex)
+ else
+ GameTooltip:AddLine("金币邮件", 1, 0.84, 0)
+ end
+ GameTooltip:AddLine(" ")
+ if data.sender then
+ GameTooltip:AddLine("发件人: " .. data.sender, 0.7, 0.7, 0.7)
+ end
+ if data.subject and data.subject ~= "" then
+ GameTooltip:AddLine(data.subject, 0.5, 0.5, 0.5)
+ end
+ if data.codAmount and data.codAmount > 0 then
+ GameTooltip:AddLine(" ")
+ GameTooltip:AddLine("付款取信: " .. FormatMoneyString(data.codAmount), 1, 0.3, 0.3)
+ GameTooltip:AddLine("|cFFFFCC00点击支付并取回|r")
+ elseif data.money and data.money > 0 and not data.hasItem then
+ GameTooltip:AddLine(" ")
+ GameTooltip:AddLine("金额: " .. FormatMoneyString(data.money), 1, 0.84, 0)
+ GameTooltip:AddLine("|cFFFFCC00点击收取金币|r")
+ elseif data.hasItem then
+ GameTooltip:AddLine(" ")
+ GameTooltip:AddLine("|cFFFFCC00点击收取物品|r")
+ end
+ GameTooltip:Show()
+ end)
+ sf:SetScript("OnLeave", function()
+ local data = this.mailData
+ if data and data.codAmount and data.codAmount > 0 then
+ this:SetBackdropBorderColor(1, 0.3, 0.3, 0.8)
+ else
+ this:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
+ end
+ GameTooltip:Hide()
+ end)
+
+ f.bagSlots[i] = sf
+ end
+
+ local bsep = bp:CreateTexture(nil, "ARTWORK")
+ bsep:SetTexture("Interface\\Buttons\\WHITE8X8"); bsep:SetHeight(1)
+ bsep:SetPoint("BOTTOMLEFT", bp, "BOTTOMLEFT", 6, L.BOTTOM)
+ bsep:SetPoint("BOTTOMRIGHT", bp, "BOTTOMRIGHT", -6, L.BOTTOM)
+ bsep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
+
+ local prev = CreateActionBtn(bp, "<", 28)
+ prev:SetHeight(22); prev:SetPoint("BOTTOMLEFT", bp, "BOTTOMLEFT", L.PAD, 12)
+ prev:SetScript("OnClick", function() S.bagPage = S.bagPage - 1; ML:UpdateMailBag() end)
+ f.bagPrevBtn = prev
+
+ local nxt = CreateActionBtn(bp, ">", 28)
+ nxt:SetHeight(22); nxt:SetPoint("LEFT", prev, "RIGHT", 4, 0)
+ nxt:SetScript("OnClick", function() S.bagPage = S.bagPage + 1; ML:UpdateMailBag() end)
+ f.bagNextBtn = nxt
+
+ local pageFS = bp:CreateFontString(nil, "OVERLAY")
+ pageFS:SetFont(font, 10, "OUTLINE")
+ pageFS:SetPoint("LEFT", nxt, "RIGHT", 8, 0)
+ pageFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
+ f.bagPageFS = pageFS
+
+ local colAll = CreateActionBtn(bp, "全部收取", 80)
+ colAll:SetHeight(24); colAll:SetPoint("BOTTOMRIGHT", bp, "BOTTOMRIGHT", -L.PAD, 10)
+ colAll:SetScript("OnClick", function()
+ if S.isCollecting then StopCollecting() else CollectAll() end
+ end)
+ f.bagCollectAllBtn = colAll
+end
+
+--------------------------------------------------------------------------------
+-- Mail Bag: Update
+--------------------------------------------------------------------------------
+function ML:UpdateMailBag()
+ if not S.frame or not S.frame:IsVisible() or S.currentTab ~= 3 then return end
+ local slotsPerPage = L.BAG_PER_ROW * L.BAG_ROWS
+ local numMails = GetInboxNumItems()
+
+ local entries = {}
+ for mi = 1, numMails do
+ local _, _, sender, subject, money, CODAmount, daysLeft, hasItem = GetInboxHeaderInfo(mi)
+ if hasItem or (money and money > 0) or (CODAmount and CODAmount > 0) then
+ local itemName, itemTex
+ if hasItem then itemName, itemTex = GetInboxItem(mi) end
+ table.insert(entries, {
+ mailIndex = mi,
+ hasItem = hasItem,
+ itemName = itemName,
+ itemTexture = itemTex,
+ money = money or 0,
+ codAmount = CODAmount or 0,
+ sender = sender or "未知",
+ subject = subject or "",
+ daysLeft = daysLeft,
+ })
+ end
+ end
+
+ local totalEntries = table.getn(entries)
+ local totalPages = math.max(1, math.ceil(totalEntries / slotsPerPage))
+ if S.bagPage > totalPages then S.bagPage = totalPages end
+ if S.bagPage < 1 then S.bagPage = 1 end
+
+ S.frame.bagInfoFS:SetText(string.format("共 %d 件可收取 (%d 封邮件)", totalEntries, numMails))
+ S.frame.bagPageFS:SetText(string.format("第 %d/%d 页", S.bagPage, totalPages))
+ S.frame.bagPrevBtn:SetDisabled(S.bagPage <= 1)
+ S.frame.bagNextBtn:SetDisabled(S.bagPage >= totalPages)
+ S.frame.bagCollectAllBtn:SetDisabled(numMails == 0 and not S.isCollecting)
+ if S.isCollecting then
+ S.frame.bagCollectAllBtn.label:SetText("收取中...")
+ else
+ S.frame.bagCollectAllBtn.label:SetText("全部收取")
+ end
+
+ for i = 1, slotsPerPage do
+ local slot = S.frame.bagSlots[i]
+ local ei = (S.bagPage - 1) * slotsPerPage + i
+ local entry = entries[ei]
+ if entry then
+ slot.mailData = entry
+ if entry.hasItem and entry.itemTexture then
+ slot.icon:SetTexture(entry.itemTexture)
+ elseif entry.money > 0 then
+ slot.icon:SetTexture("Interface\\Icons\\INV_Misc_Coin_01")
+ else
+ slot.icon:SetTexture("Interface\\Icons\\INV_Misc_Note_01")
+ end
+ slot.icon:Show()
+ slot.countFS:SetText("")
+ slot.moneyFS:Hide()
+
+ if entry.money > 0 and not entry.hasItem then
+ local g = math.floor(entry.money / 10000)
+ if g > 0 then
+ slot.moneyFS:SetText("|cFFFFD700" .. g .. "g|r")
+ else
+ local sv = math.floor(math.mod(entry.money, 10000) / 100)
+ if sv > 0 then
+ slot.moneyFS:SetText("|cFFC7C7CF" .. sv .. "s|r")
+ else
+ local cv = math.mod(entry.money, 100)
+ slot.moneyFS:SetText("|cFFB87333" .. cv .. "c|r")
+ end
+ end
+ slot.moneyFS:Show()
+ end
+
+ if entry.codAmount > 0 then
+ slot.codFS:Show()
+ slot:SetBackdropBorderColor(1, 0.3, 0.3, 0.8)
+ else
+ slot.codFS:Hide()
+ slot:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
+ end
+ slot:Show()
+ else
+ slot.mailData = nil
+ slot.icon:Hide()
+ slot.countFS:SetText("")
+ slot.moneyFS:Hide()
+ slot.codFS:Hide()
+ slot:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
+ slot:Show()
+ end
+ end
+end
+
+--------------------------------------------------------------------------------
+-- Mail Bag: Panel Switching
+--------------------------------------------------------------------------------
+function ML:ShowMailBagPanel()
+ if not S.frame then return end
+ S.frame.tabInbox:SetActive(false)
+ S.frame.tabBag:SetActive(true)
+ S.frame.tabSend:SetActive(false)
+ if S.frame.detailPanel then S.frame.detailPanel:Hide() end
+ S.detailMailIndex = nil
+ S.frame.inboxPanel:Hide()
+ S.frame.sendPanel:Hide()
+ S.frame.bagPanel:Show()
+ ML:UpdateMailBag()
+end
+
--------------------------------------------------------------------------------
-- BUILD: Send panel
--------------------------------------------------------------------------------
@@ -1555,29 +1893,56 @@ local function BuildSendPanel()
bsf:SetScrollChild(bodyEB)
f.bodyEditBox = bodyEB
- -- Money row
- local mLabel = sp:CreateFontString(nil, "OVERLAY")
- mLabel:SetFont(font, 11, "OUTLINE"); mLabel:SetPoint("TOPLEFT", bsf, "BOTTOMLEFT", 0, -10)
- mLabel:SetText("附加金币:"); mLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3])
+ -- Money mode toggle (附加金币 / 付款取信)
+ local mToggle = CreateActionBtn(sp, "附加金币", 72)
+ mToggle:SetHeight(20); mToggle:SetPoint("TOPLEFT", bsf, "BOTTOMLEFT", 0, -10)
+ f.moneyToggle = mToggle
+ local function UpdateMoneyToggle()
+ if S.codMode then
+ mToggle.label:SetText("|cFFFF5555付款取信|r")
+ mToggle:SetBackdropBorderColor(1, 0.3, 0.3, 0.8)
+ else
+ mToggle.label:SetText("附加金币")
+ mToggle:SetBackdropBorderColor(T.btnBorder[1], T.btnBorder[2], T.btnBorder[3], T.btnBorder[4])
+ end
+ end
+ f.UpdateMoneyToggle = UpdateMoneyToggle
+ mToggle:SetScript("OnClick", function()
+ S.codMode = not S.codMode; UpdateMoneyToggle()
+ end)
+ mToggle:SetScript("OnEnter", function()
+ GameTooltip:SetOwner(this, "ANCHOR_TOPRIGHT")
+ if S.codMode then
+ GameTooltip:AddLine("付款取信模式", 1, 0.5, 0.5)
+ GameTooltip:AddLine("收件人需支付指定金额才能取回附件", 0.8, 0.8, 0.8)
+ else
+ GameTooltip:AddLine("附加金币模式", 1, 0.84, 0)
+ GameTooltip:AddLine("随邮件附送金币给收件人", 0.8, 0.8, 0.8)
+ end
+ GameTooltip:AddLine("|cFFFFCC00点击切换模式|r")
+ GameTooltip:Show()
+ end)
+ mToggle:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ UpdateMoneyToggle()
local gL = sp:CreateFontString(nil, "OVERLAY")
- gL:SetFont(font, 10, "OUTLINE"); gL:SetPoint("LEFT", mLabel, "RIGHT", 6, 0)
+ gL:SetFont(font, 10, "OUTLINE"); gL:SetPoint("LEFT", mToggle, "RIGHT", 6, 0)
gL:SetText("金"); gL:SetTextColor(T.moneyGold[1], T.moneyGold[2], T.moneyGold[3])
- local gEB = CreateStyledEditBox(sp, 60, 20, true); gEB:SetPoint("LEFT", gL, "RIGHT", 4, 0); gEB:SetText("0"); f.goldEB = gEB
+ local gEB = CreateStyledEditBox(sp, 50, 20, true); gEB:SetPoint("LEFT", gL, "RIGHT", 4, 0); gEB:SetText("0"); f.goldEB = gEB
local sL = sp:CreateFontString(nil, "OVERLAY")
sL:SetFont(font, 10, "OUTLINE"); sL:SetPoint("LEFT", gEB, "RIGHT", 6, 0)
sL:SetText("银"); sL:SetTextColor(T.moneySilver[1], T.moneySilver[2], T.moneySilver[3])
- local sEB = CreateStyledEditBox(sp, 40, 20, true); sEB:SetPoint("LEFT", sL, "RIGHT", 4, 0); sEB:SetText("0"); f.silverEB = sEB
+ local sEB = CreateStyledEditBox(sp, 36, 20, true); sEB:SetPoint("LEFT", sL, "RIGHT", 4, 0); sEB:SetText("0"); f.silverEB = sEB
local cL = sp:CreateFontString(nil, "OVERLAY")
cL:SetFont(font, 10, "OUTLINE"); cL:SetPoint("LEFT", sEB, "RIGHT", 6, 0)
cL:SetText("铜"); cL:SetTextColor(T.moneyCopper[1], T.moneyCopper[2], T.moneyCopper[3])
- local cEB = CreateStyledEditBox(sp, 40, 20, true); cEB:SetPoint("LEFT", cL, "RIGHT", 4, 0); cEB:SetText("0"); f.copperEB = cEB
+ local cEB = CreateStyledEditBox(sp, 36, 20, true); cEB:SetPoint("LEFT", cL, "RIGHT", 4, 0); cEB:SetText("0"); f.copperEB = cEB
-- Attachments
local aLabel = sp:CreateFontString(nil, "OVERLAY")
- aLabel:SetFont(font, 11, "OUTLINE"); aLabel:SetPoint("TOPLEFT", mLabel, "TOPLEFT", 0, -28)
+ aLabel:SetFont(font, 11, "OUTLINE"); aLabel:SetPoint("TOPLEFT", mToggle, "TOPLEFT", 0, -28)
aLabel:SetText("附件 (右击/拖放背包物品添加):"); aLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3])
local clrBtn = CreateActionBtn(sp, "清空", 50)
@@ -1679,7 +2044,7 @@ local function SetupEvents()
f:SetScript("OnEvent", function()
if event == "MAIL_SHOW" then
if SFramesDB and SFramesDB.enableMail == false then return end
- S.currentTab = 1; S.inboxPage = 1; S.inboxChecked = {}
+ S.currentTab = 1; S.inboxPage = 1; S.bagPage = 1; S.inboxChecked = {}
CheckInbox(); f:Show(); ML:ShowInboxPanel()
elseif event == "MAIL_INBOX_UPDATE" then
if f:IsVisible() then
@@ -1689,10 +2054,28 @@ local function SetupEvents()
else
ML:HideMailDetail()
end
+ elseif S.currentTab == 3 then
+ ML:UpdateMailBag()
else
UpdateInbox()
end
end
+ -- 收件箱清空后同步小地图信件图标状态
+ if MiniMapMailFrame then
+ if HasNewMail and HasNewMail() then
+ MiniMapMailFrame:Show()
+ elseif GetInboxNumItems() == 0 then
+ MiniMapMailFrame:Hide()
+ end
+ end
+ elseif event == "UPDATE_PENDING_MAIL" then
+ if MiniMapMailFrame then
+ if HasNewMail and HasNewMail() then
+ MiniMapMailFrame:Show()
+ else
+ MiniMapMailFrame:Hide()
+ end
+ end
elseif event == "MAIL_CLOSED" then
if S.multiSend then AbortMultiSend("邮箱已关闭") end
f:Hide()
@@ -1721,6 +2104,7 @@ local function SetupEvents()
f:RegisterEvent("MAIL_SHOW"); f:RegisterEvent("MAIL_INBOX_UPDATE")
f:RegisterEvent("MAIL_CLOSED"); f:RegisterEvent("MAIL_SEND_SUCCESS")
f:RegisterEvent("MAIL_SEND_INFO_UPDATE"); f:RegisterEvent("MAIL_FAILED")
+ f:RegisterEvent("UPDATE_PENDING_MAIL")
if MailFrame then
local origMailOnShow = MailFrame:GetScript("OnShow")
@@ -1728,6 +2112,7 @@ local function SetupEvents()
if origMailOnShow then origMailOnShow() end
this:ClearAllPoints()
this:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -10000, 10000)
+ this:SetAlpha(0)
this:EnableMouse(false)
end)
for i = table.getn(UISpecialFrames), 1, -1 do
@@ -1772,6 +2157,7 @@ function ML:Initialize()
BuildMainFrame()
BuildInboxPanel()
BuildDetailPanel()
+ BuildMailBagPanel()
BuildSendPanel()
SetupEvents()
end
@@ -1781,19 +2167,19 @@ end
--------------------------------------------------------------------------------
function ML:ShowInboxPanel()
if not S.frame then return end
- S.frame.tabInbox:SetActive(true); S.frame.tabSend:SetActive(false)
+ S.frame.tabInbox:SetActive(true); S.frame.tabBag:SetActive(false); S.frame.tabSend:SetActive(false)
if S.frame.detailPanel then S.frame.detailPanel:Hide() end
- S.frame.inboxPanel:Show(); S.frame.sendPanel:Hide()
+ S.frame.inboxPanel:Show(); S.frame.sendPanel:Hide(); S.frame.bagPanel:Hide()
S.detailMailIndex = nil
UpdateInbox()
end
function ML:ShowSendPanel()
if not S.frame then return end
- S.frame.tabInbox:SetActive(false); S.frame.tabSend:SetActive(true)
+ S.frame.tabInbox:SetActive(false); S.frame.tabBag:SetActive(false); S.frame.tabSend:SetActive(true)
if S.frame.detailPanel then S.frame.detailPanel:Hide() end
S.detailMailIndex = nil
- S.frame.inboxPanel:Hide(); S.frame.sendPanel:Show()
+ S.frame.inboxPanel:Hide(); S.frame.sendPanel:Show(); S.frame.bagPanel:Hide()
if S.frame.sendStatus then S.frame.sendStatus:SetText("") end
if S.statusFadeTimer then S.statusFadeTimer:SetScript("OnUpdate", nil) end
ML:UpdateSendPanel()
diff --git a/MapIcons.lua b/MapIcons.lua
index d776770..377d703 100644
--- a/MapIcons.lua
+++ b/MapIcons.lua
@@ -127,6 +127,64 @@ local function GetZoneYards()
return nil, nil
end
+local function IsMapStateProtected()
+ if WorldMapFrame and WorldMapFrame:IsVisible() then
+ return true
+ end
+ if BattlefieldMinimap and BattlefieldMinimap:IsVisible() then
+ return true
+ end
+ if BattlefieldMinimapFrame and BattlefieldMinimapFrame:IsVisible() then
+ return true
+ end
+ return false
+end
+
+local function SafeSetMapToCurrentZone()
+ if not SetMapToCurrentZone then
+ return
+ end
+ if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
+ return SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
+ end
+ return pcall(SetMapToCurrentZone)
+end
+
+local function SafeSetMapZoom(continent, zone)
+ if not SetMapZoom then
+ return
+ end
+ if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
+ return SFrames:CallWithPreservedBattlefieldMinimap(SetMapZoom, continent, zone)
+ end
+ return pcall(SetMapZoom, continent, zone)
+end
+
+local function WithPlayerZoneMap(func)
+ if type(func) ~= "function" then
+ return
+ end
+ if IsMapStateProtected() or not SetMapToCurrentZone then
+ return func()
+ end
+
+ local savedC = GetCurrentMapContinent and GetCurrentMapContinent() or 0
+ local savedZ = GetCurrentMapZone and GetCurrentMapZone() or 0
+
+ SafeSetMapToCurrentZone()
+ local results = { func() }
+
+ if SetMapZoom then
+ if savedZ and savedZ > 0 and savedC and savedC > 0 then
+ SafeSetMapZoom(savedC, savedZ)
+ elseif savedC and savedC > 0 then
+ SafeSetMapZoom(savedC, 0)
+ end
+ end
+
+ return unpack(results)
+end
+
--------------------------------------------------------------------------------
-- 1. World Map + Battlefield Minimap: class icon overlays
--------------------------------------------------------------------------------
@@ -224,97 +282,95 @@ local function UpdateMinimapDots()
return
end
- if WorldMapFrame and WorldMapFrame:IsVisible() then
+ if IsMapStateProtected() then
return
end
- if SetMapToCurrentZone then
- pcall(SetMapToCurrentZone)
- end
+ WithPlayerZoneMap(function()
+ local px, py = GetPlayerMapPosition("player")
+ if not px or not py or (px == 0 and py == 0) then
+ for i = 1, MAX_PARTY do
+ if mmDots[i] then mmDots[i]:Hide() end
+ end
+ return
+ end
+
+ local zw, zh = GetZoneYards()
+ if not zw or not zh or zw == 0 or zh == 0 then
+ for i = 1, MAX_PARTY do
+ if mmDots[i] then mmDots[i]:Hide() end
+ end
+ return
+ end
+
+ local now = GetTime()
+ if now - indoorCheckTime > 3 then
+ indoorCheckTime = now
+ cachedIndoor = DetectIndoor()
+ end
+
+ local zoom = Minimap:GetZoom()
+ local mmYards = MM_ZOOM[cachedIndoor] and MM_ZOOM[cachedIndoor][zoom]
+ or MM_ZOOM[1][zoom] or 466.67
+ local mmHalfYards = mmYards / 2
+ local mmHalfPx = Minimap:GetWidth() / 2
+
+ local facing = 0
+ local doRotate = false
+ local okCvar, rotateVal = pcall(GetCVar, "rotateMinimap")
+ if okCvar and rotateVal == "1" and GetPlayerFacing then
+ local ok2, f = pcall(GetPlayerFacing)
+ if ok2 and f then
+ facing = f
+ doRotate = true
+ end
+ end
- local px, py = GetPlayerMapPosition("player")
- if not px or not py or (px == 0 and py == 0) then
for i = 1, MAX_PARTY do
- if mmDots[i] then mmDots[i]:Hide() end
- end
- return
- end
+ local unit = "party" .. i
+ if i <= numParty and UnitExists(unit) and UnitIsConnected(unit) then
+ local mx, my = GetPlayerMapPosition(unit)
+ if mx and my and (mx ~= 0 or my ~= 0) then
+ local dx = (mx - px) * zw
+ local dy = (py - my) * zh
- local zw, zh = GetZoneYards()
- if not zw or not zh or zw == 0 or zh == 0 then
- for i = 1, MAX_PARTY do
- if mmDots[i] then mmDots[i]:Hide() end
- end
- return
- end
-
- local now = GetTime()
- if now - indoorCheckTime > 3 then
- indoorCheckTime = now
- cachedIndoor = DetectIndoor()
- end
-
- local zoom = Minimap:GetZoom()
- local mmYards = MM_ZOOM[cachedIndoor] and MM_ZOOM[cachedIndoor][zoom]
- or MM_ZOOM[1][zoom] or 466.67
- local mmHalfYards = mmYards / 2
- local mmHalfPx = Minimap:GetWidth() / 2
-
- local facing = 0
- local doRotate = false
- local okCvar, rotateVal = pcall(GetCVar, "rotateMinimap")
- if okCvar and rotateVal == "1" and GetPlayerFacing then
- local ok2, f = pcall(GetPlayerFacing)
- if ok2 and f then
- facing = f
- doRotate = true
- end
- end
-
- for i = 1, MAX_PARTY do
- local unit = "party" .. i
- if i <= numParty and UnitExists(unit) and UnitIsConnected(unit) then
- local mx, my = GetPlayerMapPosition(unit)
- if mx and my and (mx ~= 0 or my ~= 0) then
- local dx = (mx - px) * zw
- local dy = (py - my) * zh
-
- if doRotate then
- local s = math.sin(facing)
- local c = math.cos(facing)
- dx, dy = dx * c + dy * s, -dx * s + dy * c
- end
-
- local dist = math.sqrt(dx * dx + dy * dy)
- if dist < mmHalfYards * 0.92 then
- local scale = mmHalfPx / mmHalfYards
-
- if not mmDots[i] then
- mmDots[i] = CreateMinimapDot(i)
+ if doRotate then
+ local s = math.sin(facing)
+ local c = math.cos(facing)
+ dx, dy = dx * c + dy * s, -dx * s + dy * c
end
- local dot = mmDots[i]
- local _, class = UnitClass(unit)
- local cc = class and CLASS_COLORS and CLASS_COLORS[class]
- if cc then
- dot.icon:SetVertexColor(cc.r, cc.g, cc.b, 1)
+ local dist = math.sqrt(dx * dx + dy * dy)
+ if dist < mmHalfYards * 0.92 then
+ local scale = mmHalfPx / mmHalfYards
+
+ if not mmDots[i] then
+ mmDots[i] = CreateMinimapDot(i)
+ end
+ local dot = mmDots[i]
+
+ local _, class = UnitClass(unit)
+ local cc = class and CLASS_COLORS and CLASS_COLORS[class]
+ if cc then
+ dot.icon:SetVertexColor(cc.r, cc.g, cc.b, 1)
+ else
+ dot.icon:SetVertexColor(1, 0.82, 0, 1)
+ end
+
+ dot:ClearAllPoints()
+ dot:SetPoint("CENTER", Minimap, "CENTER", dx * scale, dy * scale)
+ dot:Show()
else
- dot.icon:SetVertexColor(1, 0.82, 0, 1)
+ if mmDots[i] then mmDots[i]:Hide() end
end
-
- dot:ClearAllPoints()
- dot:SetPoint("CENTER", Minimap, "CENTER", dx * scale, dy * scale)
- dot:Show()
else
if mmDots[i] then mmDots[i]:Hide() end
end
else
if mmDots[i] then mmDots[i]:Hide() end
end
- else
- if mmDots[i] then mmDots[i]:Hide() end
end
- end
+ end)
end
--------------------------------------------------------------------------------
diff --git a/MapReveal.lua b/MapReveal.lua
index 0bf6340..74e40f7 100644
--- a/MapReveal.lua
+++ b/MapReveal.lua
@@ -34,6 +34,26 @@ local function GetOverlayDB()
return MapOverlayData or LibMapOverlayData or zMapOverlayData or mapOverlayData
end
+local function SafeSetMapToCurrentZone()
+ if not SetMapToCurrentZone then
+ return
+ end
+ if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
+ return SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
+ end
+ return pcall(SetMapToCurrentZone)
+end
+
+local function SafeSetMapZoom(continent, zone)
+ if not SetMapZoom then
+ return
+ end
+ if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
+ return SFrames:CallWithPreservedBattlefieldMinimap(SetMapZoom, continent, zone)
+ end
+ return pcall(SetMapZoom, continent, zone)
+end
+
--------------------------------------------------------------------------------
-- Persistence: save/load discovered overlay data to SFramesDB
--------------------------------------------------------------------------------
@@ -428,7 +448,7 @@ local function ProcessScanZone()
end
local entry = scanQueue[scanIndex]
- SetMapZoom(entry.cont, entry.zone)
+ SafeSetMapZoom(entry.cont, entry.zone)
local mapFile = GetMapInfo and GetMapInfo() or ""
if mapFile == "" then
@@ -490,11 +510,11 @@ function MapReveal:FinishScan()
end
if savedMapZ > 0 then
- SetMapZoom(savedMapC, savedMapZ)
+ SafeSetMapZoom(savedMapC, savedMapZ)
elseif savedMapC > 0 then
- SetMapZoom(savedMapC, 0)
+ SafeSetMapZoom(savedMapC, 0)
else
- if SetMapToCurrentZone then SetMapToCurrentZone() end
+ SafeSetMapToCurrentZone()
end
local cf = DEFAULT_CHAT_FRAME
diff --git a/Media.lua b/Media.lua
index 8105309..8e4dcb2 100644
--- a/Media.lua
+++ b/Media.lua
@@ -62,6 +62,27 @@ function SFrames:GetTexture()
return self.Media.statusbar
end
+function SFrames:ResolveBarTexture(settingKey, fallbackKey)
+ local key
+ if SFramesDB and settingKey then
+ key = SFramesDB[settingKey]
+ end
+ if (not key or key == "") and SFramesDB and fallbackKey then
+ key = SFramesDB[fallbackKey]
+ end
+ if key and key ~= "" then
+ local builtin = self._barTextureLookup[key]
+ if builtin then return builtin end
+
+ local LSM = self:GetSharedMedia()
+ if LSM then
+ local path = LSM:Fetch("statusbar", key, true)
+ if path then return path end
+ end
+ end
+ return self:GetTexture()
+end
+
function SFrames:GetFont()
-- 1. Check built-in font key
if SFramesDB and SFramesDB.fontKey then
@@ -79,6 +100,40 @@ function SFrames:GetFont()
return self.Media.font
end
+function SFrames:ResolveFont(settingKey, fallbackKey)
+ local key
+ if SFramesDB and settingKey then
+ key = SFramesDB[settingKey]
+ end
+ if (not key or key == "") and SFramesDB and fallbackKey then
+ key = SFramesDB[fallbackKey]
+ end
+ if key and key ~= "" then
+ local builtin = self._fontLookup[key]
+ if builtin then return builtin end
+ local LSM = self:GetSharedMedia()
+ if LSM then
+ local path = LSM:Fetch("font", key, true)
+ if path then return path end
+ end
+ end
+ return self:GetFont()
+end
+
+function SFrames:ResolveFontOutline(settingKey, fallbackKey)
+ local outline
+ if SFramesDB and settingKey then
+ outline = SFramesDB[settingKey]
+ end
+ if (not outline or outline == "") and SFramesDB and fallbackKey then
+ outline = SFramesDB[fallbackKey]
+ end
+ if outline and outline ~= "" then
+ return outline
+ end
+ return self.Media.fontOutline or "OUTLINE"
+end
+
function SFrames:GetSharedMediaList(mediaType)
local LSM = self:GetSharedMedia()
if LSM and LSM.List then return LSM:List(mediaType) end
diff --git a/Minimap.lua b/Minimap.lua
index 5ce5cf4..3f95e54 100644
--- a/Minimap.lua
+++ b/Minimap.lua
@@ -93,6 +93,19 @@ local function GetDB()
return db
end
+local function IsMapStateProtected()
+ if WorldMapFrame and WorldMapFrame:IsVisible() then
+ return true
+ end
+ if BattlefieldMinimap and BattlefieldMinimap:IsVisible() then
+ return true
+ end
+ if BattlefieldMinimapFrame and BattlefieldMinimapFrame:IsVisible() then
+ return true
+ end
+ return false
+end
+
local function ResolveStyleKey()
local key = GetDB().mapStyle or "auto"
if key == "auto" then
@@ -152,6 +165,34 @@ local function ApplyPosition()
end
end
+local function HideTooltipIfOwned(frame)
+ if not frame or not GameTooltip or not GameTooltip:IsVisible() then
+ return
+ end
+ if GameTooltip.IsOwned and GameTooltip:IsOwned(frame) then
+ GameTooltip:Hide()
+ return
+ end
+ if GameTooltip.GetOwner and GameTooltip:GetOwner() == frame then
+ GameTooltip:Hide()
+ end
+end
+
+local function ForceHideMinimapFrame(frame)
+ if not frame then return end
+ HideTooltipIfOwned(frame)
+ if frame.EnableMouse then
+ frame:EnableMouse(false)
+ end
+ if frame.Hide then
+ frame:Hide()
+ end
+ if frame.SetAlpha then
+ frame:SetAlpha(0)
+ end
+ frame.Show = function() end
+end
+
--------------------------------------------------------------------------------
-- Hide default Blizzard minimap chrome
-- MUST be called AFTER BuildFrame (Minimap is already reparented)
@@ -165,15 +206,13 @@ local function HideDefaultElements()
MinimapToggleButton,
MiniMapWorldMapButton,
GameTimeFrame,
+ TimeManagerClockButton,
MinimapZoneTextButton,
MiniMapTracking,
MinimapBackdrop,
}
for _, f in ipairs(kill) do
- if f then
- f:Hide()
- f.Show = function() end
- end
+ ForceHideMinimapFrame(f)
end
-- Hide all tracking-related frames (Turtle WoW dual tracking, etc.)
@@ -183,10 +222,7 @@ local function HideDefaultElements()
}
for _, name in ipairs(trackNames) do
local f = _G[name]
- if f and f.Hide then
- f:Hide()
- f.Show = function() end
- end
+ ForceHideMinimapFrame(f)
end
-- Also hide any tracking textures that are children of Minimap
@@ -195,8 +231,7 @@ local function HideDefaultElements()
for _, child in ipairs(children) do
local n = child.GetName and child:GetName()
if n and string.find(n, "Track") then
- child:Hide()
- child.Show = function() end
+ ForceHideMinimapFrame(child)
end
end
end
@@ -455,8 +490,15 @@ local function UpdateZoneText()
end
local function SetZoneMap()
+ if IsMapStateProtected() then
+ return
+ end
if SetMapToCurrentZone then
- pcall(SetMapToCurrentZone)
+ if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
+ SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
+ else
+ pcall(SetMapToCurrentZone)
+ end
end
end
diff --git a/MinimapBuffs.lua b/MinimapBuffs.lua
index e9800bb..e91a3eb 100644
--- a/MinimapBuffs.lua
+++ b/MinimapBuffs.lua
@@ -78,6 +78,43 @@ local DEBUFF_TYPE_COLORS = {
local DEBUFF_DEFAULT_COLOR = { r = 0.80, g = 0.00, b = 0.00 }
local WEAPON_ENCHANT_COLOR = { r = 0.58, g = 0.22, b = 0.82 }
+local function IsTooltipOwnedBy(frame)
+ if not frame or not GameTooltip or not GameTooltip:IsVisible() then
+ return false
+ end
+ if GameTooltip.IsOwned then
+ return GameTooltip:IsOwned(frame)
+ end
+ if GameTooltip.GetOwner then
+ return GameTooltip:GetOwner() == frame
+ end
+ return false
+end
+
+local function HideOwnedTooltip(frame)
+ if IsTooltipOwnedBy(frame) then
+ GameTooltip:Hide()
+ end
+end
+
+local function SetSlotTooltipKey(btn, key)
+ if btn._sfTooltipKey ~= key then
+ HideOwnedTooltip(btn)
+ btn._sfTooltipKey = key
+ end
+end
+
+local function ClearSlotState(btn)
+ HideOwnedTooltip(btn)
+ btn.buffIndex = -1
+ btn._sfSimulated = false
+ btn._sfSimLabel = nil
+ btn._sfSimDesc = nil
+ btn._isWeaponEnchant = false
+ btn._weaponSlotID = nil
+ btn._sfTooltipKey = nil
+end
+
local function HideBlizzardBuffs()
for i = 0, 23 do
local btn = _G["BuffButton" .. i]
@@ -170,18 +207,19 @@ local function CreateSlot(parent, namePrefix, index, isBuff)
btn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
+ btn:SetScript("OnHide", function()
+ HideOwnedTooltip(this)
+ end)
btn:SetScript("OnClick", function()
if this._sfSimulated then return end
if this._isWeaponEnchant then return end
if this.isBuff and this.buffIndex and this.buffIndex >= 0 then
+ HideOwnedTooltip(this)
CancelPlayerBuff(this.buffIndex)
end
end)
- btn.buffIndex = -1
- btn._sfSimulated = false
- btn._isWeaponEnchant = false
- btn._weaponSlotID = nil
+ ClearSlotState(btn)
btn:Hide()
return btn
end
@@ -339,9 +377,12 @@ function MB:UpdateBuffs()
local btn = self.buffSlots[slotIdx]
local texture = GetPlayerBuffTexture(buffIndex)
if texture then
+ SetSlotTooltipKey(btn, "buff:" .. tostring(buffIndex))
btn.icon:SetTexture(texture)
btn.buffIndex = buffIndex
btn._sfSimulated = false
+ btn._sfSimLabel = nil
+ btn._sfSimDesc = nil
btn._isWeaponEnchant = false
btn._weaponSlotID = nil
@@ -360,10 +401,8 @@ function MB:UpdateBuffs()
btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1)
btn:Show()
else
+ ClearSlotState(btn)
btn:Hide()
- btn.buffIndex = -1
- btn._isWeaponEnchant = false
- btn._weaponSlotID = nil
end
end
end
@@ -378,9 +417,12 @@ function MB:UpdateBuffs()
local btn = self.buffSlots[slotIdx]
local texture = GetInventoryItemTexture("player", 16)
if texture then
+ SetSlotTooltipKey(btn, "weapon:16")
btn.icon:SetTexture(texture)
btn.buffIndex = -1
btn._sfSimulated = false
+ btn._sfSimLabel = nil
+ btn._sfSimDesc = nil
btn._isWeaponEnchant = true
btn._weaponSlotID = 16
@@ -407,9 +449,12 @@ function MB:UpdateBuffs()
local btn = self.buffSlots[slotIdx]
local texture = GetInventoryItemTexture("player", 17)
if texture then
+ SetSlotTooltipKey(btn, "weapon:17")
btn.icon:SetTexture(texture)
btn.buffIndex = -1
btn._sfSimulated = false
+ btn._sfSimLabel = nil
+ btn._sfSimDesc = nil
btn._isWeaponEnchant = true
btn._weaponSlotID = 17
@@ -432,10 +477,8 @@ function MB:UpdateBuffs()
for j = slotIdx + 1, MAX_BUFFS do
local btn = self.buffSlots[j]
+ ClearSlotState(btn)
btn:Hide()
- btn.buffIndex = -1
- btn._isWeaponEnchant = false
- btn._weaponSlotID = nil
end
self:UpdateDebuffs()
@@ -450,6 +493,7 @@ function MB:UpdateDebuffs()
if db.showDebuffs == false then
for i = 1, MAX_DEBUFFS do
+ ClearSlotState(self.debuffSlots[i])
self.debuffSlots[i]:Hide()
end
if self.debuffContainer then self.debuffContainer:Hide() end
@@ -467,9 +511,14 @@ function MB:UpdateDebuffs()
local btn = self.debuffSlots[slotIdx]
local texture = GetPlayerBuffTexture(buffIndex)
if texture then
+ SetSlotTooltipKey(btn, "debuff:" .. tostring(buffIndex))
btn.icon:SetTexture(texture)
btn.buffIndex = buffIndex
btn._sfSimulated = false
+ btn._sfSimLabel = nil
+ btn._sfSimDesc = nil
+ btn._isWeaponEnchant = false
+ btn._weaponSlotID = nil
local apps = GetPlayerBuffApplications(buffIndex)
if apps and apps > 1 then
@@ -495,15 +544,15 @@ function MB:UpdateDebuffs()
btn:Show()
else
+ ClearSlotState(btn)
btn:Hide()
- btn.buffIndex = -1
end
end
end
for j = slotIdx + 1, MAX_DEBUFFS do
+ ClearSlotState(self.debuffSlots[j])
self.debuffSlots[j]:Hide()
- self.debuffSlots[j].buffIndex = -1
end
end
@@ -556,6 +605,7 @@ function MB:SimulateBuffs()
local btn = self.buffSlots[i]
local sim = SIM_BUFFS[i]
if sim then
+ SetSlotTooltipKey(btn, "sim-buff:" .. tostring(i))
btn.icon:SetTexture(sim.tex)
btn.buffIndex = -1
btn._sfSimulated = true
@@ -578,6 +628,7 @@ function MB:SimulateBuffs()
btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1)
btn:Show()
else
+ ClearSlotState(btn)
btn:Hide()
end
end
@@ -586,11 +637,14 @@ function MB:SimulateBuffs()
local btn = self.debuffSlots[i]
local sim = SIM_DEBUFFS[i]
if sim then
+ SetSlotTooltipKey(btn, "sim-debuff:" .. tostring(i))
btn.icon:SetTexture(sim.tex)
btn.buffIndex = -1
btn._sfSimulated = true
btn._sfSimLabel = sim.label
btn._sfSimDesc = sim.desc
+ btn._isWeaponEnchant = false
+ btn._weaponSlotID = nil
btn.timer:SetText(sim.time)
ApplyTimerColor(btn, sim.time)
@@ -601,6 +655,7 @@ function MB:SimulateBuffs()
btn:SetBackdropBorderColor(c.r, c.g, c.b, 1)
btn:Show()
else
+ ClearSlotState(btn)
btn:Hide()
end
end
diff --git a/Movers.lua b/Movers.lua
index b0ae333..ca7b198 100644
--- a/Movers.lua
+++ b/Movers.lua
@@ -207,8 +207,12 @@ local function SyncMoverToFrame(name)
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 minW = 72
+ local minH = 40
+ if h > w * 3 then minW = 40 end
+ if w > h * 3 then minH = 24 end
+ mover:SetWidth(math.max(w, minW))
+ mover:SetHeight(math.max(h, minH))
local l = frame:GetLeft()
local b = frame:GetBottom()
@@ -256,17 +260,12 @@ local function SyncFrameToMover(name)
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))
+ local fScale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
+ if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
+ frame:SetPoint(pos.point, UIParent, pos.relativePoint,
+ (pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
+ else
+ frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
end
end
@@ -720,11 +719,14 @@ local function CreateControlBar()
local accent = th.accent or { 1, 0.5, 0.8, 0.98 }
local titleC = th.title or { 1, 0.88, 1 }
+ local ROW_Y_TOP = 12
+ local ROW_Y_BOT = -12
+
controlBar = CreateFrame("Frame", "SFramesLayoutControlBar", UIParent)
controlBar:SetFrameStrata("FULLSCREEN_DIALOG")
controlBar:SetFrameLevel(200)
controlBar:SetWidth(480)
- controlBar:SetHeight(44)
+ controlBar:SetHeight(72)
controlBar:SetPoint("TOP", UIParent, "TOP", 0, -8)
controlBar:SetClampedToScreen(true)
controlBar:SetMovable(true)
@@ -739,7 +741,7 @@ local function CreateControlBar()
local title = controlBar:CreateFontString(nil, "OVERLAY")
title:SetFont(Font(), 13, "OUTLINE")
- title:SetPoint("LEFT", controlBar, "LEFT", 14, 0)
+ title:SetPoint("LEFT", controlBar, "LEFT", 14, ROW_Y_TOP)
title:SetText("Nanami 布局")
title:SetTextColor(titleC[1], titleC[2], titleC[3], 1)
@@ -747,17 +749,19 @@ local function CreateControlBar()
sep:SetTexture("Interface\\Buttons\\WHITE8x8")
sep:SetWidth(1)
sep:SetHeight(24)
- sep:SetPoint("LEFT", controlBar, "LEFT", 118, 0)
+ sep:SetPoint("LEFT", controlBar, "LEFT", 118, ROW_Y_TOP)
sep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.5)
local bx = 128
- -- Snap toggle
+ -- Snap toggle (row 1)
local snapBtn = MakeControlButton(controlBar, "", 76, bx, function()
local cfg = GetLayoutCfg()
cfg.snapEnabled = not cfg.snapEnabled
if controlBar._updateSnap then controlBar._updateSnap() end
end)
+ snapBtn:ClearAllPoints()
+ snapBtn:SetPoint("LEFT", controlBar, "LEFT", bx, ROW_Y_TOP)
controlBar.snapBtn = snapBtn
local function UpdateSnapBtnText()
@@ -773,7 +777,7 @@ local function CreateControlBar()
end
controlBar._updateSnap = UpdateSnapBtnText
- -- Grid toggle
+ -- Grid toggle (row 1)
local gridBtn = MakeControlButton(controlBar, "", 76, bx + 84, function()
local cfg = GetLayoutCfg()
cfg.showGrid = not cfg.showGrid
@@ -782,6 +786,8 @@ local function CreateControlBar()
if cfg.showGrid then gridFrame:Show() else gridFrame:Hide() end
end
end)
+ gridBtn:ClearAllPoints()
+ gridBtn:SetPoint("LEFT", controlBar, "LEFT", bx + 84, ROW_Y_TOP)
controlBar.gridBtn = gridBtn
local function UpdateGridBtnText()
@@ -797,18 +803,20 @@ local function CreateControlBar()
end
controlBar._updateGrid = UpdateGridBtnText
- -- Reset all
+ -- Reset all (row 1)
local resetBtn = MakeControlButton(controlBar, "全部重置", 76, bx + 176, function()
M:ResetAllMovers()
end)
+ resetBtn:ClearAllPoints()
+ resetBtn:SetPoint("LEFT", controlBar, "LEFT", bx + 176, ROW_Y_TOP)
local wbGold = th.wbGold or { 1, 0.88, 0.55 }
resetBtn._text:SetTextColor(wbGold[1], wbGold[2], wbGold[3], 1)
- -- Close
+ -- Close (row 1)
local closeBtn = CreateFrame("Button", nil, controlBar)
closeBtn:SetWidth(60)
closeBtn:SetHeight(26)
- closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, 0)
+ closeBtn:SetPoint("RIGHT", controlBar, "RIGHT", -10, ROW_Y_TOP)
closeBtn:SetBackdrop(ROUND_BACKDROP)
closeBtn:SetBackdropColor(0.35, 0.08, 0.10, 0.95)
closeBtn:SetBackdropBorderColor(0.65, 0.20, 0.25, 0.90)
@@ -837,6 +845,64 @@ local function CreateControlBar()
end)
controlBar.closeBtn = closeBtn
+ -- Row separator
+ local rowSep = controlBar:CreateTexture(nil, "ARTWORK")
+ rowSep:SetTexture("Interface\\Buttons\\WHITE8x8")
+ rowSep:SetHeight(1)
+ rowSep:SetPoint("LEFT", controlBar, "LEFT", 10, 0)
+ rowSep:SetPoint("RIGHT", controlBar, "RIGHT", -10, 0)
+ rowSep:SetVertexColor(panelBd[1], panelBd[2], panelBd[3], 0.35)
+
+ -- Row 2: Preset buttons
+ local presetLabel = controlBar:CreateFontString(nil, "OVERLAY")
+ presetLabel:SetFont(Font(), 10, "OUTLINE")
+ presetLabel:SetPoint("LEFT", controlBar, "LEFT", 14, ROW_Y_BOT)
+ presetLabel:SetText("预设:")
+ presetLabel:SetTextColor(0.7, 0.68, 0.78, 1)
+
+ controlBar._presetBtns = {}
+ local AB = SFrames.ActionBars
+ local presets = AB and AB.PRESETS or {}
+ local px = 56
+ for idx = 1, 3 do
+ local p = presets[idx]
+ local pName = p and p.name or ("方案" .. idx)
+ local pDesc = p and p.desc or ""
+ local pId = idx
+ local pbtn = MakeControlButton(controlBar, pName, 80, px + (idx - 1) * 88, function()
+ if AB and AB.ApplyPreset then
+ AB:ApplyPreset(pId)
+ end
+ end)
+ pbtn:ClearAllPoints()
+ pbtn:SetPoint("LEFT", controlBar, "LEFT", px + (idx - 1) * 88, ROW_Y_BOT)
+ pbtn._text:SetFont(Font(), 9, "OUTLINE")
+ pbtn._presetId = pId
+
+ pbtn:SetScript("OnEnter", function()
+ local a2 = T().accent or { 1, 0.5, 0.8 }
+ this:SetBackdropBorderColor(a2[1], a2[2], a2[3], 0.95)
+ this._text:SetTextColor(1, 1, 1, 1)
+ GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
+ GameTooltip:AddLine(pName, 1, 0.85, 0.55)
+ GameTooltip:AddLine(pDesc, 0.75, 0.75, 0.85, true)
+ GameTooltip:Show()
+ end)
+ pbtn:SetScript("OnLeave", function()
+ local th2 = T()
+ local bd2 = th2.buttonBorder or { 0.35, 0.30, 0.50, 0.90 }
+ local tc2 = th2.buttonText or { 0.85, 0.82, 0.92 }
+ this:SetBackdropColor(th2.buttonBg and th2.buttonBg[1] or 0.16,
+ th2.buttonBg and th2.buttonBg[2] or 0.12,
+ th2.buttonBg and th2.buttonBg[3] or 0.22, 0.94)
+ this:SetBackdropBorderColor(bd2[1], bd2[2], bd2[3], bd2[4] or 0.90)
+ this._text:SetTextColor(tc2[1], tc2[2], tc2[3], 1)
+ GameTooltip:Hide()
+ end)
+
+ controlBar._presetBtns[idx] = pbtn
+ end
+
controlBar:Hide()
return controlBar
end
@@ -844,7 +910,7 @@ end
--------------------------------------------------------------------------------
-- Register mover
--------------------------------------------------------------------------------
-function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, defaultRelPoint, defaultX, defaultY, onMoved)
+function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, defaultRelPoint, defaultX, defaultY, onMoved, opts)
if not name or not frame then return end
registry[name] = {
@@ -856,6 +922,7 @@ function M:RegisterMover(name, frame, label, defaultPoint, defaultRelativeTo, de
defaultX = defaultX or 0,
defaultY = defaultY or 0,
onMoved = onMoved,
+ alwaysShowInLayout = opts and opts.alwaysShowInLayout or false,
}
CreateMoverFrame(name, registry[name])
@@ -890,10 +957,19 @@ function M:EnterLayoutMode()
UIParent:GetWidth(), UIParent:GetHeight(),
UIParent:GetRight() or 0, UIParent:GetTop() or 0))
- for name, _ in pairs(registry) do
+ for name, entry in pairs(registry) do
+ local frame = entry and entry.frame
+ local shouldShow = entry.alwaysShowInLayout
+ or (frame and frame.IsShown and frame:IsShown())
SyncMoverToFrame(name)
local mover = moverFrames[name]
- if mover then mover:Show() end
+ if mover then
+ if shouldShow then
+ mover:Show()
+ else
+ mover:Hide()
+ end
+ end
end
SFrames:Print("布局模式已开启 - 拖拽移动 | 箭头微调 | 右键重置 | Shift禁用磁吸")
@@ -924,6 +1000,22 @@ function M:IsLayoutMode()
return isLayoutMode
end
+function M:SetMoverAlwaysShow(name, alwaysShow)
+ local entry = registry[name]
+ if entry then
+ entry.alwaysShowInLayout = alwaysShow
+ end
+ local mover = moverFrames[name]
+ if mover and isLayoutMode then
+ if alwaysShow then
+ SyncMoverToFrame(name)
+ mover:Show()
+ else
+ mover:Hide()
+ end
+ end
+end
+
--------------------------------------------------------------------------------
-- Reset movers
--------------------------------------------------------------------------------
@@ -979,13 +1071,30 @@ function M:ApplyPosition(name, frame, defaultPoint, defaultRelTo, defaultRelPoin
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)
+ local fScale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
+ if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
+ frame:SetPoint(pos.point, UIParent, pos.relativePoint,
+ (pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
+ else
+ frame:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
+ end
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)
+ if relFrame == UIParent then
+ local fScale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale()
+ if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
+ frame:SetPoint(defaultPoint or "CENTER", UIParent, defaultRelPoint or "CENTER",
+ (defaultX or 0) / fScale, (defaultY or 0) / fScale)
+ else
+ frame:SetPoint(defaultPoint or "CENTER", UIParent, defaultRelPoint or "CENTER",
+ defaultX or 0, defaultY or 0)
+ end
+ else
+ frame:SetPoint(defaultPoint or "CENTER", relFrame, defaultRelPoint or "CENTER",
+ defaultX or 0, defaultY or 0)
+ end
return false
end
end
@@ -996,3 +1105,7 @@ end
function M:GetRegistry()
return registry
end
+
+function M:SyncMoverToFrame(name)
+ SyncMoverToFrame(name)
+end
diff --git a/Nanami-UI.toc b/Nanami-UI.toc
index f7b0af5..c4723fd 100644
--- a/Nanami-UI.toc
+++ b/Nanami-UI.toc
@@ -9,6 +9,7 @@
Bindings.xml
Core.lua
Config.lua
+AuraTracker.lua
Movers.lua
Media.lua
IconMap.lua
@@ -27,7 +28,6 @@ MapIcons.lua
Tweaks.lua
MinimapBuffs.lua
Focus.lua
-ClassSkillData.lua
Units\Player.lua
Units\Pet.lua
Units\Target.lua
@@ -39,6 +39,7 @@ GearScore.lua
Tooltip.lua
Units\Raid.lua
ActionBars.lua
+ExtraBar.lua
KeyBindManager.lua
Bags\Offline.lua
@@ -57,6 +58,8 @@ QuestLogSkin.lua
TrainerUI.lua
TradeSkillDB.lua
BeastTrainingUI.lua
+ConsumableDB.lua
+ConsumableUI.lua
TradeSkillUI.lua
CharacterPanel.lua
StatSummary.lua
diff --git a/SetupWizard.lua b/SetupWizard.lua
index 0cc745d..0461b16 100644
--- a/SetupWizard.lua
+++ b/SetupWizard.lua
@@ -444,6 +444,10 @@ local function ApplyChoices()
SFramesDB.enableChat = c.enableChat
if type(SFramesDB.Chat) ~= "table" then SFramesDB.Chat = {} end
SFramesDB.Chat.translateEnabled = c.translateEnabled
+ -- 翻译关闭时,聊天监控也自动关闭
+ if c.translateEnabled == false then
+ SFramesDB.Chat.chatMonitorEnabled = false
+ end
SFramesDB.Chat.hcGlobalDisable = c.hcGlobalDisable
SFramesDB.enableUnitFrames = c.enableUnitFrames
@@ -1316,9 +1320,14 @@ function SW:DoSkip()
self:Hide()
return
end
- -- First-run: apply defaults
- if not SFramesDB then SFramesDB = {} end
- SFramesDB.setupComplete = true
+ -- First-run: apply defaults then initialize
+ choices = GetDefaultChoices()
+ local ok, err = pcall(ApplyChoices)
+ if not ok then
+ if not SFramesDB then SFramesDB = {} end
+ SFramesDB.setupComplete = true
+ DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami-UI] Wizard skip apply error: "..tostring(err).."|r")
+ end
self:Hide()
if completeCb then completeCb() end
end
diff --git a/Tooltip.lua b/Tooltip.lua
index c6930cc..c179ccd 100644
--- a/Tooltip.lua
+++ b/Tooltip.lua
@@ -115,6 +115,23 @@ local function TT_DifficultyColor(unitLevel)
end
end
+local function TT_GetClassificationText(unit)
+ if not UnitExists(unit) then return nil end
+
+ local classif = UnitClassification(unit)
+ if classif == "rareelite" then
+ return "|cffc57cff[稀有 精英]|r"
+ elseif classif == "rare" then
+ return "|cffc57cff[稀有]|r"
+ elseif classif == "elite" then
+ return "|cffffa500[精英]|r"
+ elseif classif == "worldboss" then
+ return "|cffff4040[首领]|r"
+ end
+
+ return nil
+end
+
--------------------------------------------------------------------------------
-- Initialize
--------------------------------------------------------------------------------
@@ -680,6 +697,29 @@ function SFrames.FloatingTooltip:FormatLines(tooltip)
GameTooltipStatusBar._origSetColor(GameTooltipStatusBar, color.r, color.g, color.b)
end
end
+
+ local classificationText = TT_GetClassificationText(unit)
+ if classificationText then
+ local numLines = tooltip:NumLines()
+ local appended = false
+ for i = 2, numLines do
+ local left = getglobal("GameTooltipTextLeft" .. i)
+ if left then
+ local txt = left:GetText()
+ if txt and (string.find(txt, "^Level ") or string.find(txt, "^等级 ")) then
+ if not string.find(txt, "%[") then
+ left:SetText(txt .. " " .. classificationText)
+ end
+ appended = true
+ break
+ end
+ end
+ end
+
+ if not appended then
+ tooltip:AddLine(classificationText)
+ end
+ end
end
--------------------------------------------------------------------------
@@ -1268,27 +1308,27 @@ function IC:HookTooltips()
---------------------------------------------------------------------------
-- SetItemRef (chat item links)
---------------------------------------------------------------------------
- local orig_SetItemRef = SetItemRef
- if orig_SetItemRef then
- SetItemRef = function(link, text, button)
- orig_SetItemRef(link, text, button)
- if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then return end
+ if ItemRefTooltip and ItemRefTooltip.SetHyperlink then
+ local orig_ItemRef_SetHyperlink = ItemRefTooltip.SetHyperlink
+ ItemRefTooltip.SetHyperlink = function(self, link)
+ self._nanamiSellPriceAdded = nil
+ self._gsScoreAdded = nil
+
+ local r1, r2, r3, r4 = orig_ItemRef_SetHyperlink(self, link)
+
+ if IsAltKeyDown() or IsShiftKeyDown() or IsControlKeyDown() then
+ return r1, r2, r3, r4
+ end
+
pcall(function()
local _, _, itemStr = string.find(link or "", "(item:[%-?%d:]+)")
if itemStr then
- ItemRefTooltip._nanamiSellPriceAdded = nil
- local itemId = IC_GetItemIdFromLink(itemStr)
- local price = IC_QueryAndLearnPrice(itemStr)
- if price and price > 0 and not ItemRefTooltip.hasMoney then
- SetTooltipMoney(ItemRefTooltip, price)
- ItemRefTooltip:Show()
- end
- if itemId then
- ItemRefTooltip:AddLine("物品ID: " .. itemId, 0.55, 0.55, 0.70)
- ItemRefTooltip:Show()
- end
+ local moneyAlreadyShown = self.hasMoney
+ IC_EnhanceTooltip(self, itemStr, nil, moneyAlreadyShown)
end
end)
+
+ return r1, r2, r3, r4
end
end
diff --git a/TradeSkillUI.lua b/TradeSkillUI.lua
index 1a4abdd..22cac88 100644
--- a/TradeSkillUI.lua
+++ b/TradeSkillUI.lua
@@ -962,12 +962,7 @@ function TSUI.CreateReagentSlot(parent, i)
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
local ok
if S.currentMode == "craft" then
- local link = GetCraftItemLink and GetCraftItemLink(S.selectedIndex)
- if link then
- ok = pcall(GameTooltip.SetCraftItem, GameTooltip, S.selectedIndex, this.reagentIndex)
- else
- ok = pcall(GameTooltip.SetCraftSpell, GameTooltip, S.selectedIndex)
- end
+ ok = pcall(GameTooltip.SetCraftItem, GameTooltip, S.selectedIndex, this.reagentIndex)
else
ok = pcall(GameTooltip.SetTradeSkillItem, GameTooltip, S.selectedIndex, this.reagentIndex)
end
@@ -1521,6 +1516,7 @@ end
function TSUI.UpdateProfTabs()
TSUI.ScanProfessions()
local currentSkillName = API.GetSkillLineName()
+ local numVisible = 0
for i = 1, 10 do
local tab = S.profTabs[i]
if not tab then break end
@@ -1544,10 +1540,16 @@ function TSUI.UpdateProfTabs()
tab.glow:Hide(); tab.checked:Hide()
end
tab:Show()
+ numVisible = numVisible + 1
else
tab.profName = nil; tab.active = false; tab:Hide()
end
end
+ if S.encBtn and S.MainFrame then
+ S.encBtn:ClearAllPoints()
+ S.encBtn:SetPoint("TOPLEFT", S.MainFrame, "TOPRIGHT", 2,
+ -(6 + numVisible * (42 + 4) + 10))
+ end
end
function TSUI.IsTabSwitching()
@@ -1932,6 +1934,24 @@ function TSUI:Initialize()
end
end)
dIF:SetScript("OnLeave", function() GameTooltip:Hide() end)
+ dIF:SetScript("OnMouseUp", function()
+ if IsShiftKeyDown() and S.selectedIndex then
+ local link = API.GetItemLink(S.selectedIndex)
+ if link then
+ if ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
+ ChatFrameEditBox:Insert(link)
+ else
+ if ChatFrame_OpenChat then
+ ChatFrame_OpenChat(link)
+ elseif ChatFrameEditBox then
+ ChatFrameEditBox:Show()
+ ChatFrameEditBox:SetText(link)
+ ChatFrameEditBox:SetFocus()
+ end
+ end
+ end
+ end
+ end)
local dName = det:CreateFontString(nil, "OVERLAY")
dName:SetFont(font, 13, "OUTLINE")
@@ -2128,6 +2148,52 @@ function TSUI:Initialize()
end)
TSUI.CreateProfTabs(MF)
+ -- ── 食物药剂百科 按钮(职业图标条底部)───────────────────────────────
+ do
+ local TAB_SZ, TAB_GAP, TAB_TOP = 42, 4, 6
+ local encBtn = CreateFrame("Button", nil, MF)
+ encBtn:SetWidth(TAB_SZ); encBtn:SetHeight(TAB_SZ)
+ encBtn:SetPoint("TOPLEFT", MF, "TOPRIGHT", 2, -(TAB_TOP + 10))
+ encBtn:SetFrameStrata("HIGH")
+ encBtn: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 },
+ })
+ encBtn:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
+ encBtn:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
+
+ local ico = encBtn:CreateTexture(nil, "ARTWORK")
+ ico:SetTexture("Interface\\Icons\\INV_Potion_97")
+ ico:SetTexCoord(0.08, 0.92, 0.08, 0.92)
+ ico:SetPoint("TOPLEFT", encBtn, "TOPLEFT", 4, -4)
+ ico:SetPoint("BOTTOMRIGHT", encBtn, "BOTTOMRIGHT", -4, 4)
+
+ local hl = encBtn:CreateTexture(nil, "HIGHLIGHT")
+ hl:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
+ hl:SetBlendMode("ADD"); hl:SetAlpha(0.3); hl:SetAllPoints(ico)
+
+ encBtn:SetScript("OnEnter", function()
+ this:SetBackdropBorderColor(T.slotSelected[1], T.slotSelected[2], T.slotSelected[3], 1)
+ GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
+ GameTooltip:SetText("食物药剂百科", 1, 0.82, 0.60)
+ GameTooltip:AddLine("查看各职业消耗品推荐列表", 0.8, 0.8, 0.8)
+ GameTooltip:AddLine("Shift+点击物品可插入聊天", 0.6, 0.6, 0.65)
+ GameTooltip:Show()
+ end)
+ encBtn:SetScript("OnLeave", function()
+ this:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
+ GameTooltip:Hide()
+ end)
+ encBtn:SetScript("OnClick", function()
+ if SFrames.ConsumableUI then SFrames.ConsumableUI:Toggle() end
+ end)
+ encBtn:Show()
+ S.encBtn = encBtn
+ end
+ -- ────────────────────────────────────────────────────────────────────────
+
MF:Hide()
tinsert(UISpecialFrames, "SFramesTradeSkillFrame")
end
diff --git a/TrainerUI.lua b/TrainerUI.lua
index 45ebcb8..fe50dd1 100644
--- a/TrainerUI.lua
+++ b/TrainerUI.lua
@@ -61,10 +61,11 @@ local currentFilter = "all"
local displayList = {}
local rowButtons = {}
local collapsedCats = {}
-local isTradeskillTrainerCached = false -- Cache to avoid repeated API calls
+local isTradeskillTrainerCached = false
local function HideBlizzardTrainer()
if not ClassTrainerFrame then return end
ClassTrainerFrame:SetScript("OnHide", function() end)
+ ClassTrainerFrame:UnregisterAllEvents()
if ClassTrainerFrame:IsVisible() then
if HideUIPanel then
pcall(HideUIPanel, ClassTrainerFrame)
@@ -161,123 +162,20 @@ end
local function GetVerifiedCategory(index)
local name, _, category = GetTrainerServiceInfo(index)
if not name then return nil end
-
- -- "used" is always reliable - player already knows this skill
- if category == "used" then
- return "used"
+ if category == "available" or category == "unavailable" or category == "used" then
+ return category
end
-
- -- "unavailable" from API should be trusted - it considers:
- -- - Level requirements
- -- - Skill rank prerequisites (e.g., need Fireball Rank 1 before Rank 2)
- -- - Profession skill requirements
- if category == "unavailable" then
- return "unavailable"
- end
-
- -- For "available", do extra verification only for tradeskill trainers
- -- Class trainers' "available" is already accurate
- if category == "available" then
- -- Additional check for tradeskill trainers (use cached value)
- if isTradeskillTrainerCached then
- local playerLevel = UnitLevel("player") or 1
-
- -- Check level requirement
- if GetTrainerServiceLevelReq then
- local ok, levelReq = pcall(GetTrainerServiceLevelReq, index)
- if ok and levelReq and levelReq > 0 and playerLevel < levelReq then
- return "unavailable"
- end
- end
-
- -- Check skill requirement
- if GetTrainerServiceSkillReq then
- local ok, skillName, skillRank, hasReq = pcall(GetTrainerServiceSkillReq, index)
- if ok and skillName and skillName ~= "" and not hasReq then
- return "unavailable"
- end
- end
- end
- return "available"
- end
-
- -- Fallback: unknown category, treat as unavailable
- return category or "unavailable"
+ return "unavailable"
end
local scanTip = nil
local function GetServiceTooltipInfo(index)
- if not scanTip then
- scanTip = CreateFrame("GameTooltip", "SFramesTrainerScanTip", nil, "GameTooltipTemplate")
- end
- scanTip:SetOwner(WorldFrame, "ANCHOR_NONE")
- scanTip:ClearLines()
- local ok = pcall(scanTip.SetTrainerService, scanTip, index)
- if not ok then return "", "" end
-
- local infoLines = {}
- local descLines = {}
- local numLines = scanTip:NumLines()
- local foundDesc = false
-
- for i = 2, numLines do
- local leftFS = _G["SFramesTrainerScanTipTextLeft" .. i]
- local rightFS = _G["SFramesTrainerScanTipTextRight" .. i]
- local leftText = leftFS and leftFS:GetText() or ""
- local rightText = rightFS and rightFS:GetText() or ""
-
- if leftText == "" and rightText == "" then
- if not foundDesc and table.getn(infoLines) > 0 then
- foundDesc = true
- end
- else
- local line
- if rightText ~= "" and leftText ~= "" then
- line = leftText .. " " .. rightText
- elseif leftText ~= "" then
- line = leftText
- else
- line = rightText
- end
-
- local isYellow = leftFS and leftFS.GetTextColor and true
- local r, g, b
- if leftFS and leftFS.GetTextColor then
- r, g, b = leftFS:GetTextColor()
- end
- local isWhiteOrYellow = r and (r > 0.9 and g > 0.75)
-
- if not foundDesc and rightText ~= "" then
- table.insert(infoLines, line)
- elseif not foundDesc and not isWhiteOrYellow and string.len(leftText) < 30 then
- table.insert(infoLines, line)
- else
- foundDesc = true
- table.insert(descLines, line)
- end
- end
- end
- scanTip:Hide()
- return table.concat(infoLines, "\n"), table.concat(descLines, "\n")
+ return "", ""
end
local function GetServiceQuality(index)
- if not scanTip then
- scanTip = CreateFrame("GameTooltip", "SFramesTrainerScanTip", nil, "GameTooltipTemplate")
- end
- scanTip:SetOwner(WorldFrame, "ANCHOR_NONE")
- scanTip:ClearLines()
- local ok = pcall(scanTip.SetTrainerService, scanTip, index)
- if not ok then scanTip:Hide() return nil end
- local firstLine = _G["SFramesTrainerScanTipTextLeft1"]
- if not firstLine or not firstLine.GetTextColor then
- scanTip:Hide()
- return nil
- end
- local r, g, b = firstLine:GetTextColor()
- scanTip:Hide()
- return ColorToQuality(r, g, b)
+ return nil
end
--------------------------------------------------------------------------------
@@ -392,10 +290,7 @@ end
local qualityCache = {}
local function GetCachedServiceQuality(index)
- if qualityCache[index] ~= nil then return qualityCache[index] end
- local q = GetServiceQuality(index)
- qualityCache[index] = q or false
- return q
+ return nil
end
--------------------------------------------------------------------------------
@@ -604,22 +499,8 @@ local function CreateListRow(parent, idx)
self.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
end
- -- Skip quality scan for tradeskill trainers (performance optimization)
- if not isTradeskillTrainerCached then
- local quality = GetCachedServiceQuality(svc.index)
- local qc = QUALITY_COLORS[quality]
- if qc and quality and quality >= 2 then
- self.qualGlow:SetVertexColor(qc[1], qc[2], qc[3])
- self.qualGlow:Show()
- self.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1)
- else
- self.qualGlow:Hide()
- self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
- end
- else
- self.qualGlow:Hide()
- self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
- end
+ self.qualGlow:Hide()
+ self.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
local ok, cost = pcall(GetTrainerServiceCost, svc.index)
if ok and cost and cost > 0 then
@@ -838,13 +719,7 @@ local function UpdateDetail()
detail.icon:SetTexture(iconTex)
detail.iconFrame:Show()
- local quality = GetServiceQuality(selectedIndex)
- local qc = QUALITY_COLORS[quality]
- if qc and quality and quality >= 2 then
- detail.iconFrame:SetBackdropBorderColor(qc[1], qc[2], qc[3], 1)
- else
- detail.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
- end
+ detail.iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
detail.nameFS:SetText(name or "")
@@ -873,13 +748,10 @@ local function UpdateDetail()
end
detail.reqFS:SetText(table.concat(reqParts, " "))
- local spellInfo, descText = GetServiceTooltipInfo(selectedIndex)
- detail.infoFS:SetText(spellInfo)
- detail.descFS:SetText(descText)
- detail.descDivider:Show()
-
- local textH = detail.descFS:GetHeight() or 40
- detail.descScroll:GetScrollChild():SetHeight(math.max(1, textH))
+ detail.infoFS:SetText("")
+ detail.descFS:SetText("")
+ detail.descDivider:Hide()
+ detail.descScroll:GetScrollChild():SetHeight(1)
detail.descScroll:SetVerticalScroll(0)
local canTrain = (category == "available") and cost and (GetMoney() >= cost)
@@ -1342,6 +1214,7 @@ function TUI:Initialize()
local function CleanupBlizzardTrainer()
if not ClassTrainerFrame then return end
ClassTrainerFrame:SetScript("OnHide", function() end)
+ ClassTrainerFrame:UnregisterAllEvents()
if HideUIPanel then pcall(HideUIPanel, ClassTrainerFrame) end
if ClassTrainerFrame:IsVisible() then ClassTrainerFrame:Hide() end
ClassTrainerFrame:SetAlpha(0)
@@ -1361,6 +1234,7 @@ function TUI:Initialize()
if event == "TRAINER_SHOW" then
if ClassTrainerFrame then
ClassTrainerFrame:SetScript("OnHide", function() end)
+ ClassTrainerFrame:UnregisterAllEvents()
ClassTrainerFrame:SetAlpha(0)
ClassTrainerFrame:EnableMouse(false)
end
diff --git a/Tweaks.lua b/Tweaks.lua
index ea4715e..2a34d61 100644
--- a/Tweaks.lua
+++ b/Tweaks.lua
@@ -1148,11 +1148,9 @@ end
-- unit under the mouse cursor without changing current target.
--
-- Strategy:
--- UseAction hook: temporarily TargetUnit(moUnit) before the real UseAction,
--- then restore previous target afterwards. This preserves the hardware
--- event callstack so the client doesn't reject the action.
--- CastSpellByName hook (SuperWoW): pass moUnit as 2nd arg directly.
--- CastSpellByName hook (no SuperWoW): same target-swap trick.
+-- Temporarily try the mouseover unit first while preserving the original
+-- target. If the spell cannot resolve on mouseover, stop the pending target
+-- mode, restore the original target, and retry there.
--------------------------------------------------------------------------------
local mouseoverCastEnabled = false
local origUseAction = nil
@@ -1176,6 +1174,70 @@ local function GetMouseoverUnit()
return nil
end
+local function CaptureTargetState()
+ local hadTarget = UnitExists("target")
+ return {
+ hadTarget = hadTarget,
+ name = hadTarget and UnitName("target") or nil,
+ }
+end
+
+local function RestoreTargetState(state)
+ if not state then return end
+ if state.hadTarget and state.name then
+ TargetLastTarget()
+ if not UnitExists("target") or UnitName("target") ~= state.name then
+ TargetByName(state.name, true)
+ end
+ else
+ ClearTarget()
+ end
+end
+
+local function ResolvePendingSpellTarget(unit)
+ if not (SpellIsTargeting and SpellIsTargeting()) then
+ return true
+ end
+
+ if unit then
+ if SpellCanTargetUnit then
+ if SpellCanTargetUnit(unit) then
+ SpellTargetUnit(unit)
+ end
+ else
+ SpellTargetUnit(unit)
+ end
+ end
+
+ if SpellIsTargeting and SpellIsTargeting() then
+ return false
+ end
+
+ return true
+end
+
+local function TryActionOnUnit(unit, action, cursor, onSelf)
+ if not unit then return false end
+
+ if not (UnitIsUnit and UnitExists("target") and UnitIsUnit(unit, "target")) then
+ TargetUnit(unit)
+ end
+
+ origUseAction(action, cursor, onSelf)
+ return ResolvePendingSpellTarget(unit)
+end
+
+local function TryCastSpellOnUnit(unit, spell)
+ if not unit then return false end
+
+ if not (UnitIsUnit and UnitExists("target") and UnitIsUnit(unit, "target")) then
+ TargetUnit(unit)
+ end
+
+ origCastSpellByName(spell)
+ return ResolvePendingSpellTarget(unit)
+end
+
local function MouseoverUseAction(action, cursor, onSelf)
-- Don't interfere: picking up action, or re-entrant call
if cursor == 1 or inMouseoverAction then
@@ -1187,42 +1249,24 @@ local function MouseoverUseAction(action, cursor, onSelf)
return origUseAction(action, cursor, onSelf)
end
- -- Skip if mouseover IS current target (no swap needed)
- if UnitIsUnit and UnitExists("target") and UnitIsUnit(moUnit, "target") then
- return origUseAction(action, cursor, onSelf)
- end
+ local prevTarget = CaptureTargetState()
- -- Remember current target state
- local hadTarget = UnitExists("target")
- local prevTargetName = hadTarget and UnitName("target") or nil
-
- -- Temporarily target the mouseover unit
inMouseoverAction = true
- TargetUnit(moUnit)
+ local castOnMouseover = TryActionOnUnit(moUnit, action, cursor, onSelf)
- -- Execute the real UseAction on the now-targeted mouseover unit
- origUseAction(action, cursor, onSelf)
-
- -- Handle ground-targeted spells (Blizzard, Flamestrike, etc.)
- if SpellIsTargeting and SpellIsTargeting() then
- SpellTargetUnit(moUnit)
- end
- if SpellIsTargeting and SpellIsTargeting() then
+ if not castOnMouseover and SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting()
end
- -- Restore previous target
- if hadTarget and prevTargetName then
- -- Target back the previous unit
- TargetLastTarget()
- -- Verify restoration worked
- if not UnitExists("target") or UnitName("target") ~= prevTargetName then
- -- TargetLastTarget failed, try by name
- TargetByName(prevTargetName, true)
+ RestoreTargetState(prevTarget)
+
+ if not castOnMouseover and prevTarget.hadTarget then
+ origUseAction(action, cursor, onSelf)
+ if SpellIsTargeting and SpellIsTargeting() then
+ if not ResolvePendingSpellTarget("target") then
+ SpellStopTargeting()
+ end
end
- else
- -- Had no target before, clear
- ClearTarget()
end
inMouseoverAction = false
@@ -1244,43 +1288,26 @@ local function MouseoverCastSpellByName(spell, arg2)
return origCastSpellByName(spell)
end
- -- SuperWoW: direct unit parameter, no target swap needed
- if SUPERWOW_VERSION then
- origCastSpellByName(spell, moUnit)
- if SpellIsTargeting and SpellIsTargeting() then
- SpellTargetUnit(moUnit)
- end
- if SpellIsTargeting and SpellIsTargeting() then
- SpellStopTargeting()
- end
- return
- end
+ local prevTarget = CaptureTargetState()
- -- No SuperWoW: target-swap
- local hadTarget = UnitExists("target")
- local prevTargetName = hadTarget and UnitName("target") or nil
+ inMouseoverAction = true
+ local castOnMouseover = TryCastSpellOnUnit(moUnit, spell)
- if not (hadTarget and UnitIsUnit and UnitIsUnit(moUnit, "target")) then
- TargetUnit(moUnit)
- end
-
- origCastSpellByName(spell)
-
- if SpellIsTargeting and SpellIsTargeting() then
- SpellTargetUnit("target")
- end
- if SpellIsTargeting and SpellIsTargeting() then
+ if not castOnMouseover and SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting()
end
- if hadTarget and prevTargetName then
- TargetLastTarget()
- if not UnitExists("target") or UnitName("target") ~= prevTargetName then
- TargetByName(prevTargetName, true)
+ RestoreTargetState(prevTarget)
+
+ if not castOnMouseover and prevTarget.hadTarget then
+ origCastSpellByName(spell)
+ if SpellIsTargeting and SpellIsTargeting() then
+ if not ResolvePendingSpellTarget("target") then
+ SpellStopTargeting()
+ end
end
- elseif not hadTarget then
- ClearTarget()
end
+ inMouseoverAction = false
end
local function InitMouseoverCast()
diff --git a/Units/Party.lua b/Units/Party.lua
index 5b458b8..5d6ae87 100644
--- a/Units/Party.lua
+++ b/Units/Party.lua
@@ -9,6 +9,9 @@ local PARTY_HORIZONTAL_GAP = 8
local PARTY_UNIT_LOOKUP = { party1 = true, party2 = true, party3 = true, party4 = true }
local PARTYPET_UNIT_LOOKUP = { partypet1 = true, partypet2 = true, partypet3 = true, partypet4 = true }
+-- Pre-allocated table reused every UpdateAuras call
+local _partyDebuffColor = { r = 0, g = 0, b = 0 }
+
local function GetIncomingHeals(unit)
return SFrames:GetIncomingHeals(unit)
end
@@ -36,6 +39,12 @@ local function Clamp(value, minValue, maxValue)
return value
end
+local function SetTextureIfPresent(region, texturePath)
+ if region and region.SetTexture and texturePath then
+ region:SetTexture(texturePath)
+ end
+end
+
function SFrames.Party:GetMetrics()
local db = SFramesDB or {}
@@ -57,6 +66,34 @@ function SFrames.Party:GetMetrics()
local powerHeight = tonumber(db.partyPowerHeight) or (height - healthHeight - 3)
powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, height - 6)
+ local gradientStyle = SFrames:IsGradientStyle()
+ local availablePowerWidth = width - portraitWidth - 5
+ if availablePowerWidth < 40 then
+ availablePowerWidth = 40
+ end
+
+ local rawPowerWidth = tonumber(db.partyPowerWidth)
+ local legacyFullWidth = tonumber(db.partyFrameWidth) or width
+ local legacyPowerWidth = width - portraitWidth - 3
+ local defaultPowerWidth = gradientStyle and width or availablePowerWidth
+ local maxPowerWidth = gradientStyle and width or availablePowerWidth
+ local powerWidth
+ if gradientStyle then
+ -- 渐变风格:能量条始终与血条等宽(全宽)
+ powerWidth = width
+ elseif not rawPowerWidth
+ or math.abs(rawPowerWidth - legacyFullWidth) < 0.5
+ or math.abs(rawPowerWidth - legacyPowerWidth) < 0.5
+ or math.abs(rawPowerWidth - availablePowerWidth) < 0.5 then
+ powerWidth = defaultPowerWidth
+ else
+ powerWidth = rawPowerWidth
+ end
+ powerWidth = Clamp(math.floor(powerWidth + 0.5), 40, maxPowerWidth)
+
+ local powerOffsetX = Clamp(math.floor((tonumber(db.partyPowerOffsetX) or 0) + 0.5), -120, 120)
+ local powerOffsetY = Clamp(math.floor((tonumber(db.partyPowerOffsetY) or 0) + 0.5), -80, 80)
+
if healthHeight + powerHeight + 3 > height then
powerHeight = height - healthHeight - 3
if powerHeight < 6 then
@@ -81,16 +118,30 @@ function SFrames.Party:GetMetrics()
local valueFont = tonumber(db.partyValueFontSize) or 10
valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18)
+ local healthFont = tonumber(db.partyHealthFontSize) or valueFont
+ healthFont = Clamp(math.floor(healthFont + 0.5), 8, 18)
+
+ local powerFont = tonumber(db.partyPowerFontSize) or valueFont
+ powerFont = Clamp(math.floor(powerFont + 0.5), 8, 18)
+
return {
width = width,
height = height,
portraitWidth = portraitWidth,
healthHeight = healthHeight,
powerHeight = powerHeight,
+ powerWidth = powerWidth,
+ powerOffsetX = powerOffsetX,
+ powerOffsetY = powerOffsetY,
+ powerOnTop = db.partyPowerOnTop == true,
horizontalGap = hgap,
verticalGap = vgap,
nameFont = nameFont,
valueFont = valueFont,
+ healthFont = healthFont,
+ powerFont = powerFont,
+ healthTexture = SFrames:ResolveBarTexture("partyHealthTexture", "barTexture"),
+ powerTexture = SFrames:ResolveBarTexture("partyPowerTexture", "barTexture"),
}
end
@@ -142,8 +193,8 @@ function SFrames.Party:ApplyFrameStyle(frame, metrics)
if frame.power then
frame.power:ClearAllPoints()
- frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1)
- frame.power:SetPoint("TOPRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
+ frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
+ frame.power:SetWidth(metrics.powerWidth)
frame.power:SetHeight(metrics.powerHeight)
end
@@ -153,14 +204,114 @@ function SFrames.Party:ApplyFrameStyle(frame, metrics)
frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 1, -1)
end
- local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
- local fontPath = SFrames:GetFont()
+ SFrames:ApplyStatusBarTexture(frame.health, "partyHealthTexture", "barTexture")
+ SFrames:ApplyStatusBarTexture(frame.power, "partyPowerTexture", "barTexture")
+ if frame.health and frame.power then
+ local healthLevel = frame:GetFrameLevel() + 2
+ local powerLevel = metrics.powerOnTop and (healthLevel + 1) or (healthLevel - 1)
+ frame.health:SetFrameLevel(healthLevel)
+ frame.power:SetFrameLevel(powerLevel)
+ end
+ SFrames:ApplyConfiguredUnitBackdrop(frame, "party")
+ if frame.pbg then SFrames:ApplyConfiguredUnitBackdrop(frame.pbg, "party", true) end
+ if frame.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.healthBGFrame, "party") end
+ if frame.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.powerBGFrame, "party") end
+ SetTextureIfPresent(frame.health and frame.health.bg, metrics.healthTexture)
+ SetTextureIfPresent(frame.health and frame.health.healPredMine, metrics.healthTexture)
+ SetTextureIfPresent(frame.health and frame.health.healPredOther, metrics.healthTexture)
+ SetTextureIfPresent(frame.health and frame.health.healPredOver, metrics.healthTexture)
+ SetTextureIfPresent(frame.power and frame.power.bg, metrics.powerTexture)
+
+ -- Gradient style preset
+ if SFrames:IsGradientStyle() then
+ -- Hide portrait & its backdrop
+ if frame.portrait then frame.portrait:Hide() end
+ if frame.pbg then SFrames:ClearBackdrop(frame.pbg); frame.pbg:Hide() end
+ -- Strip backdrops
+ SFrames:ClearBackdrop(frame)
+ SFrames:ClearBackdrop(frame.healthBGFrame)
+ SFrames:ClearBackdrop(frame.powerBGFrame)
+ -- Health bar full width
+ if frame.health then
+ frame.health:ClearAllPoints()
+ frame.health:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
+ frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
+ frame.health:SetHeight(metrics.healthHeight)
+ end
+ -- Power bar full width
+ if frame.power then
+ frame.power:ClearAllPoints()
+ frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -2 + metrics.powerOffsetY)
+ frame.power:SetWidth(metrics.powerWidth)
+ frame.power:SetHeight(metrics.powerHeight)
+ end
+ -- Apply gradient overlays
+ SFrames:ApplyGradientStyle(frame.health)
+ SFrames:ApplyGradientStyle(frame.power)
+ -- Flush BG frames
+ if frame.healthBGFrame then
+ frame.healthBGFrame:ClearAllPoints()
+ frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", 0, 0)
+ frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
+ end
+ if frame.powerBGFrame then
+ frame.powerBGFrame:ClearAllPoints()
+ frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", 0, 0)
+ frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 0, 0)
+ end
+ -- Hide bar backgrounds (transparent)
+ if frame.healthBGFrame then frame.healthBGFrame:Hide() end
+ if frame.powerBGFrame then frame.powerBGFrame:Hide() end
+ if frame.health and frame.health.bg then frame.health.bg:Hide() end
+ if frame.power and frame.power.bg then frame.power.bg:Hide() end
+ else
+ SFrames:RemoveGradientStyle(frame.health)
+ SFrames:RemoveGradientStyle(frame.power)
+ -- Restore bar backgrounds
+ if frame.healthBGFrame then frame.healthBGFrame:Show() end
+ if frame.powerBGFrame then frame.powerBGFrame:Show() end
+ if frame.health and frame.health.bg then frame.health.bg:Show() end
+ if frame.power and frame.power.bg then frame.power.bg:Show() end
+
+ local use3D = not (SFramesDB and SFramesDB.partyPortrait3D == false)
+ if use3D then
+ if frame.portrait then frame.portrait:Show() end
+ if frame.pbg then frame.pbg:Show() end
+ else
+ -- Hide portrait area and extend health/power bars to full width
+ if frame.portrait then frame.portrait:Hide() end
+ if frame.pbg then frame.pbg:Hide() end
+ local fullWidth = metrics.width - 2
+ if frame.health then
+ frame.health:ClearAllPoints()
+ frame.health:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1)
+ frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -1, -1)
+ frame.health:SetHeight(metrics.healthHeight)
+ end
+ if frame.healthBGFrame then
+ frame.healthBGFrame:ClearAllPoints()
+ frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", -1, 1)
+ frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 1, -1)
+ end
+ if frame.power then
+ frame.power:SetWidth(fullWidth)
+ end
+ if frame.powerBGFrame then
+ frame.powerBGFrame:ClearAllPoints()
+ frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", -1, 1)
+ frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 1, -1)
+ end
+ end
+ end
if frame.nameText then
- frame.nameText:SetFont(fontPath, metrics.nameFont, outline)
+ SFrames:ApplyFontString(frame.nameText, metrics.nameFont, "partyNameFontKey", "fontKey")
end
if frame.healthText then
- frame.healthText:SetFont(fontPath, metrics.valueFont, outline)
+ SFrames:ApplyFontString(frame.healthText, metrics.healthFont, "partyHealthFontKey", "fontKey")
+ end
+ if frame.powerText then
+ SFrames:ApplyFontString(frame.powerText, metrics.powerFont, "partyPowerFontKey", "fontKey")
end
end
@@ -250,12 +401,13 @@ function SFrames.Party:ApplyLayout()
end
end
+ local auraRowHeight = 24 -- 20px icon + 2px gap above + 2px padding
if mode == "horizontal" then
self.parent:SetWidth((metrics.width * 4) + (metrics.horizontalGap * 3))
- self.parent:SetHeight(metrics.height)
+ self.parent:SetHeight(metrics.height + auraRowHeight)
else
self.parent:SetWidth(metrics.width)
- self.parent:SetHeight(metrics.height + ((metrics.height + metrics.verticalGap) * 3))
+ self.parent:SetHeight(metrics.height + ((metrics.height + metrics.verticalGap) * 3) + auraRowHeight)
end
end
@@ -432,11 +584,16 @@ function SFrames.Party:Initialize()
f.healthText = SFrames:CreateFontString(f.health, 10, "RIGHT")
f.healthText:SetPoint("RIGHT", f.health, "RIGHT", -4, 0)
+
+ f.powerText = SFrames:CreateFontString(f.power, 9, "RIGHT")
+ f.powerText:SetPoint("RIGHT", f.power, "RIGHT", -4, 0)
f.nameText:SetShadowColor(0, 0, 0, 1)
f.nameText:SetShadowOffset(1, -1)
f.healthText:SetShadowColor(0, 0, 0, 1)
f.healthText:SetShadowOffset(1, -1)
+ f.powerText:SetShadowColor(0, 0, 0, 1)
+ f.powerText:SetShadowOffset(1, -1)
-- Leader / Master Looter overlay (high frame level so icons aren't hidden by portrait)
local roleOvr = CreateFrame("Frame", nil, f)
@@ -621,74 +778,79 @@ end
function SFrames.Party:CreateAuras(index)
local f = self.frames[index].frame
+ -- Use self.parent (plain Frame) as parent for aura buttons so they are
+ -- never clipped by the party Button frame's boundaries.
+ local auraParent = self.parent
f.buffs = {}
f.debuffs = {}
local size = 20
local spacing = 2
-
+
-- Party Buffs
for i = 1, 4 do
- local b = CreateFrame("Button", "SFramesParty"..index.."Buff"..i, f)
+ local b = CreateFrame("Button", "SFramesParty"..index.."Buff"..i, auraParent)
b:SetWidth(size)
b:SetHeight(size)
+ b:SetFrameLevel((f:GetFrameLevel() or 0) + 3)
SFrames:CreateUnitBackdrop(b)
-
+
b.icon = b:CreateTexture(nil, "ARTWORK")
b.icon:SetAllPoints()
b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
-
+
b.cdText = SFrames:CreateFontString(b, 9, "CENTER")
b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
b.cdText:SetTextColor(1, 0.82, 0)
b.cdText:SetShadowColor(0, 0, 0, 1)
b.cdText:SetShadowOffset(1, -1)
-
+
b:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
GameTooltip:SetUnitBuff(f.unit, this:GetID())
end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
-
- -- Anchored BELOW the frame on the left side
+
+ -- Anchored BELOW the party frame on the left side
if i == 1 then
b:SetPoint("TOPLEFT", f, "BOTTOMLEFT", 0, -2)
else
b:SetPoint("LEFT", f.buffs[i-1], "RIGHT", spacing, 0)
end
-
+
b:Hide()
f.buffs[i] = b
end
-
+
-- Debuffs (Starting right after Buffs to remain linear)
for i = 1, 4 do
- local b = CreateFrame("Button", "SFramesParty"..index.."Debuff"..i, f)
+ local b = CreateFrame("Button", "SFramesParty"..index.."Debuff"..i, auraParent)
b:SetWidth(size)
b:SetHeight(size)
+ b:SetFrameLevel((f:GetFrameLevel() or 0) + 3)
SFrames:CreateUnitBackdrop(b)
-
+
b.icon = b:CreateTexture(nil, "ARTWORK")
b.icon:SetAllPoints()
b.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
-
+
b.cdText = SFrames:CreateFontString(b, 9, "CENTER")
b.cdText:SetPoint("BOTTOM", b, "BOTTOM", 0, 1)
b.cdText:SetTextColor(1, 0.82, 0)
b.cdText:SetShadowColor(0, 0, 0, 1)
b.cdText:SetShadowOffset(1, -1)
-
+
b:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
GameTooltip:SetUnitDebuff(f.unit, this:GetID())
end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
-
+
if i == 1 then
b:SetPoint("LEFT", f.buffs[4], "RIGHT", spacing * 4, 0)
else
b:SetPoint("LEFT", f.debuffs[i-1], "RIGHT", spacing, 0)
end
-
+
b:Hide()
f.debuffs[i] = b
end
@@ -726,6 +888,7 @@ end
function SFrames.Party:TickAuras(unit)
+ if self.testing then return end
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
@@ -772,7 +935,10 @@ function SFrames.Party:UpdateAll()
if inRaid and raidFramesEnabled then
for i = 1, 4 do
if self.frames[i] and self.frames[i].frame then
- self.frames[i].frame:Hide()
+ local f = self.frames[i].frame
+ f:Hide()
+ if f.buffs then for j = 1, 4 do f.buffs[j]:Hide() end end
+ if f.debuffs then for j = 1, 4 do f.debuffs[j]:Hide() end end
end
end
if self._globalUpdateFrame then
@@ -793,6 +959,8 @@ function SFrames.Party:UpdateAll()
hasVisible = true
else
f:Hide()
+ if f.buffs then for j = 1, 4 do f.buffs[j]:Hide() end end
+ if f.debuffs then for j = 1, 4 do f.debuffs[j]:Hide() end end
end
end
if self._globalUpdateFrame then
@@ -812,11 +980,16 @@ function SFrames.Party:UpdateFrame(unit)
if not data then return end
local f = data.frame
- f.portrait:SetUnit(unit)
- f.portrait:SetCamera(0)
- f.portrait:Hide()
- f.portrait:Show()
-
+ local use3D = not (SFramesDB and SFramesDB.partyPortrait3D == false)
+ if use3D then
+ f.portrait:SetUnit(unit)
+ f.portrait:SetCamera(0)
+ f.portrait:Hide()
+ f.portrait:Show()
+ else
+ f.portrait:Hide()
+ end
+
local name = UnitName(unit) or ""
local level = UnitLevel(unit)
if level == -1 then level = "??" end
@@ -841,6 +1014,10 @@ function SFrames.Party:UpdateFrame(unit)
f.nameText:SetTextColor(1, 1, 1)
end
end
+ -- Re-apply gradient after color change
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyBarGradient(f.health)
+ end
-- Update Leader/Master Looter
if GetPartyLeaderIndex() == data.index then
@@ -870,6 +1047,8 @@ function SFrames.Party:UpdatePortrait(unit)
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
+ local use3D = not (SFramesDB and SFramesDB.partyPortrait3D == false)
+ if not use3D then return end
f.portrait:SetUnit(unit)
f.portrait:SetCamera(0)
f.portrait:Hide()
@@ -917,14 +1096,8 @@ function SFrames.Party:UpdateHealPrediction(unit)
local predOther = f.health.healPredOther
local predOver = f.health.healPredOver
- local function HidePredictions()
- predMine:Hide()
- predOther:Hide()
- predOver:Hide()
- end
-
if not UnitExists(unit) or not UnitIsConnected(unit) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -946,7 +1119,7 @@ function SFrames.Party:UpdateHealPrediction(unit)
end
if maxHp <= 0 then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -963,7 +1136,7 @@ function SFrames.Party:UpdateHealPrediction(unit)
end
local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -971,13 +1144,19 @@ function SFrames.Party:UpdateHealPrediction(unit)
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineIncoming <= 0 and othersIncoming <= 0 then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
- local barWidth = f:GetWidth() - (f.portrait:GetWidth() + 4)
+ local use3DForPred = not (SFramesDB and SFramesDB.partyPortrait3D == false)
+ local barWidth
+ if use3DForPred then
+ barWidth = f:GetWidth() - (f.portrait:GetWidth() + 4)
+ else
+ barWidth = f:GetWidth() - 2
+ end
if barWidth <= 0 then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1054,6 +1233,9 @@ function SFrames.Party:UpdatePowerType(unit)
else
f.power:SetStatusBarColor(0, 0, 1)
end
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyBarGradient(f.power)
+ end
end
function SFrames.Party:UpdatePower(unit)
@@ -1064,6 +1246,7 @@ function SFrames.Party:UpdatePower(unit)
if not UnitIsConnected(unit) then
f.power:SetMinMaxValues(0, 100)
f.power:SetValue(0)
+ if f.powerText then f.powerText:SetText("") end
return
end
@@ -1071,6 +1254,14 @@ function SFrames.Party:UpdatePower(unit)
local maxPower = UnitManaMax(unit)
f.power:SetMinMaxValues(0, maxPower)
f.power:SetValue(power)
+ if f.powerText then
+ if maxPower and maxPower > 0 then
+ f.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
+ else
+ f.powerText:SetText("")
+ end
+ end
+ SFrames:UpdateRainbowBar(f.power, power, maxPower, unit)
end
function SFrames.Party:UpdateRaidIcons()
@@ -1106,6 +1297,7 @@ function SFrames.Party:UpdateRaidIcon(unit)
end
function SFrames.Party:UpdateAuras(unit)
+ if self.testing then return end
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
@@ -1114,7 +1306,9 @@ function SFrames.Party:UpdateAuras(unit)
local showBuffs = not (SFramesDB and SFramesDB.partyShowBuffs == false)
local hasDebuff = false
- local debuffColor = {r=_A.slotBg[1], g=_A.slotBg[2], b=_A.slotBg[3]}
+ _partyDebuffColor.r = _A.slotBg[1]
+ _partyDebuffColor.g = _A.slotBg[2]
+ _partyDebuffColor.b = _A.slotBg[3]
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
@@ -1128,10 +1322,10 @@ function SFrames.Party:UpdateAuras(unit)
if texture then
if debuffType then
hasDebuff = true
- if debuffType == "Magic" then debuffColor = {r=0.2, g=0.6, b=1}
- elseif debuffType == "Curse" then debuffColor = {r=0.6, g=0, b=1}
- elseif debuffType == "Disease" then debuffColor = {r=0.6, g=0.4, b=0}
- elseif debuffType == "Poison" then debuffColor = {r=0, g=0.6, b=0}
+ if debuffType == "Magic" then _partyDebuffColor.r = 0.2; _partyDebuffColor.g = 0.6; _partyDebuffColor.b = 1
+ elseif debuffType == "Curse" then _partyDebuffColor.r = 0.6; _partyDebuffColor.g = 0; _partyDebuffColor.b = 1
+ elseif debuffType == "Disease" then _partyDebuffColor.r = 0.6; _partyDebuffColor.g = 0.4; _partyDebuffColor.b = 0
+ elseif debuffType == "Poison" then _partyDebuffColor.r = 0; _partyDebuffColor.g = 0.6; _partyDebuffColor.b = 0
end
end
@@ -1174,7 +1368,7 @@ function SFrames.Party:UpdateAuras(unit)
end
if hasDebuff then
- f.health.bg:SetVertexColor(debuffColor.r, debuffColor.g, debuffColor.b, 1)
+ f.health.bg:SetVertexColor(_partyDebuffColor.r, _partyDebuffColor.g, _partyDebuffColor.b, 1)
else
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
end
@@ -1254,10 +1448,38 @@ function SFrames.Party:TestMode()
f.masterIcon:Show()
end
- -- Show one dummy debuff to test positioning
- f.debuffs[1].icon:SetTexture("Interface\\Icons\\Spell_Shadow_ShadowWordPain")
- f.debuffs[1]:Show()
-
+ -- Show test buffs (all 4)
+ local testBuffIcons = {
+ "Interface\\Icons\\Spell_Holy_PowerWordFortitude",
+ "Interface\\Icons\\Spell_Holy_Renew",
+ "Interface\\Icons\\Spell_Holy_GreaterHeal",
+ "Interface\\Icons\\Spell_Nature_Abolishmagic",
+ }
+ for j = 1, 4 do
+ local fakeTime = math.random(30, 300)
+ f.buffs[j].icon:SetTexture(testBuffIcons[j])
+ f.buffs[j].expirationTime = GetTime() + fakeTime
+ f.buffs[j].cdText:SetText(SFrames:FormatTime(fakeTime))
+ f.buffs[j]:Show()
+ end
+
+ -- Show test debuffs (all 4, different types for color test)
+ local testDebuffIcons = {
+ "Interface\\Icons\\Spell_Shadow_ShadowWordPain", -- Magic (blue)
+ "Interface\\Icons\\Spell_Shadow_Curse", -- Curse (purple)
+ "Interface\\Icons\\Ability_Rogue_FeignDeath", -- Disease (brown)
+ "Interface\\Icons\\Ability_Poisoning", -- Poison (green)
+ }
+ for j = 1, 4 do
+ local debuffTime = math.random(5, 25)
+ f.debuffs[j].icon:SetTexture(testDebuffIcons[j])
+ f.debuffs[j].expirationTime = GetTime() + debuffTime
+ f.debuffs[j].cdText:SetText(SFrames:FormatTime(debuffTime))
+ f.debuffs[j]:Show()
+ end
+ -- Magic debuff background color
+ f.health.bg:SetVertexColor(0.2, 0.6, 1, 1)
+
-- Test pet
if f.petFrame then
f.petFrame:Show()
@@ -1268,9 +1490,18 @@ function SFrames.Party:TestMode()
end
end
else
- self:UpdateAll()
for i = 1, 4 do
- self.frames[i].frame.debuffs[1]:Hide()
+ local f = self.frames[i].frame
+ f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
+ for j = 1, 4 do
+ f.buffs[j].expirationTime = nil
+ f.buffs[j].cdText:SetText("")
+ f.buffs[j]:Hide()
+ f.debuffs[j].expirationTime = nil
+ f.debuffs[j].cdText:SetText("")
+ f.debuffs[j]:Hide()
+ end
end
+ self:UpdateAll()
end
end
diff --git a/Units/Pet.lua b/Units/Pet.lua
index 33ddc14..386fb31 100644
--- a/Units/Pet.lua
+++ b/Units/Pet.lua
@@ -225,16 +225,22 @@ function SFrames.Pet:Initialize()
local f = CreateFrame("Button", "SFramesPetFrame", UIParent)
f:SetWidth(150)
f:SetHeight(30)
-
- if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PetFrame"] then
- local pos = SFramesDB.Positions["PetFrame"]
- f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs)
- else
- f:SetPoint("TOPLEFT", SFramesPlayerFrame, "BOTTOMLEFT", 10, -55)
- end
-
+
local frameScale = (SFramesDB and type(SFramesDB.petFrameScale) == "number") and SFramesDB.petFrameScale or 1
f:SetScale(Clamp(frameScale, 0.7, 1.8))
+
+ if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PetFrame"] then
+ local pos = SFramesDB.Positions["PetFrame"]
+ local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
+ if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
+ f:SetPoint(pos.point, UIParent, pos.relativePoint,
+ (pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
+ else
+ f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
+ end
+ else
+ f:SetPoint("TOPLEFT", SFramesPlayerFrame, "BOTTOMLEFT", 0, -75)
+ end
f:SetMovable(true)
f:EnableMouse(true)
@@ -245,6 +251,11 @@ function SFrames.Pet:Initialize()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint()
+ local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
+ if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
+ xOfs = (xOfs or 0) * fScale
+ yOfs = (yOfs or 0) * fScale
+ end
SFramesDB.Positions["PetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
end)
@@ -294,6 +305,8 @@ function SFrames.Pet:Initialize()
hbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(hbg)
+ f.healthBGFrame = hbg
+
f.health.bg = f.health:CreateTexture(nil, "BACKGROUND")
f.health.bg:SetAllPoints()
f.health.bg:SetTexture(SFrames:GetTexture())
@@ -328,6 +341,7 @@ function SFrames.Pet:Initialize()
pbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
pbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(pbg)
+ f.powerBGFrame = pbg
f.power.bg = f.power:CreateTexture(nil, "BACKGROUND")
f.power.bg:SetAllPoints()
@@ -394,11 +408,13 @@ function SFrames.Pet:Initialize()
SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function() self:UpdateAll() end)
self:InitFoodFeature()
+ self:ApplyConfig()
self:UpdateAll()
if SFrames.Movers and SFrames.Movers.RegisterMover and self.frame then
SFrames.Movers:RegisterMover("PetFrame", self.frame, "宠物",
- "TOPLEFT", "SFramesPlayerFrame", "BOTTOMLEFT", 10, -55)
+ "TOPLEFT", "SFramesPlayerFrame", "BOTTOMLEFT", 0, -75,
+ nil, { alwaysShowInLayout = true })
end
if StaticPopup_Show then
@@ -413,6 +429,90 @@ function SFrames.Pet:Initialize()
end
end
+function SFrames.Pet:ApplyConfig()
+ if not self.frame then return end
+ local f = self.frame
+
+ -- Apply bar textures
+ SFrames:ApplyStatusBarTexture(f.health, "petHealthTexture", "barTexture")
+ SFrames:ApplyStatusBarTexture(f.power, "petPowerTexture", "barTexture")
+ local healthTex = SFrames:ResolveBarTexture("petHealthTexture", "barTexture")
+ local powerTex = SFrames:ResolveBarTexture("petPowerTexture", "barTexture")
+ if f.health and f.health.bg then f.health.bg:SetTexture(healthTex) end
+ if f.power and f.power.bg then f.power.bg:SetTexture(powerTex) end
+
+ if SFrames:IsGradientStyle() then
+ -- Strip backdrops
+ SFrames:ClearBackdrop(f)
+ SFrames:ClearBackdrop(f.healthBGFrame)
+ SFrames:ClearBackdrop(f.powerBGFrame)
+ -- Health bar full width
+ if f.health then
+ f.health:ClearAllPoints()
+ f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
+ f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
+ f.health:SetHeight(18)
+ end
+ -- Power bar full width
+ if f.power then
+ f.power:ClearAllPoints()
+ f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -2)
+ f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 0)
+ end
+ -- Apply gradient overlays
+ SFrames:ApplyGradientStyle(f.health)
+ SFrames:ApplyGradientStyle(f.power)
+ -- Flush BG frames
+ if f.healthBGFrame then
+ f.healthBGFrame:ClearAllPoints()
+ f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", 0, 0)
+ f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
+ end
+ if f.powerBGFrame then
+ f.powerBGFrame:ClearAllPoints()
+ f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", 0, 0)
+ f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 0, 0)
+ end
+ -- Hide bar backgrounds (transparent)
+ if f.healthBGFrame then f.healthBGFrame:Hide() end
+ if f.powerBGFrame then f.powerBGFrame:Hide() end
+ if f.health and f.health.bg then f.health.bg:Hide() end
+ if f.power and f.power.bg then f.power.bg:Hide() end
+ else
+ -- Classic style: restore backdrops
+ SFrames:CreateUnitBackdrop(f)
+ if f.health and f.health.bg then f.health.bg:Show() end
+ if f.power and f.power.bg then f.power.bg:Show() end
+ if f.healthBGFrame then
+ SFrames:CreateUnitBackdrop(f.healthBGFrame)
+ f.healthBGFrame:Show()
+ f.healthBGFrame:ClearAllPoints()
+ f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1)
+ f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1)
+ end
+ if f.powerBGFrame then
+ SFrames:CreateUnitBackdrop(f.powerBGFrame)
+ f.powerBGFrame:Show()
+ f.powerBGFrame:ClearAllPoints()
+ f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1)
+ f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
+ 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(18)
+ end
+ if f.power then
+ f.power:ClearAllPoints()
+ f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
+ f.power:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
+ end
+ SFrames:RemoveGradientStyle(f.health)
+ SFrames:RemoveGradientStyle(f.power)
+ end
+end
+
function SFrames.Pet:UpdateAll()
if UnitExists("pet") then
if SFramesDB and SFramesDB.showPetFrame == false then
@@ -437,6 +537,9 @@ function SFrames.Pet:UpdateAll()
local r, g, b = 0.33, 0.59, 0.33
self.frame.health:SetStatusBarColor(r, g, b)
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyBarGradient(self.frame.health)
+ end
else
self.frame:Hide()
if self.foodPanel then self.foodPanel:Hide() end
@@ -465,12 +568,6 @@ function SFrames.Pet:UpdateHealPrediction()
local predOther = self.frame.health.healPredOther
local predOver = self.frame.health.healPredOver
- local function HidePredictions()
- predMine:Hide()
- predOther:Hide()
- predOver:Hide()
- end
-
local hp = UnitHealth("pet") or 0
local maxHp = UnitHealthMax("pet") or 0
@@ -485,7 +582,7 @@ function SFrames.Pet:UpdateHealPrediction()
end
if maxHp <= 0 or UnitIsDeadOrGhost("pet") then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -497,7 +594,7 @@ function SFrames.Pet:UpdateHealPrediction()
local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -505,13 +602,13 @@ function SFrames.Pet:UpdateHealPrediction()
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
local barWidth = self.frame.health:GetWidth()
if barWidth <= 0 then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -521,7 +618,7 @@ function SFrames.Pet:UpdateHealPrediction()
local availableWidth = barWidth - currentWidth
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -587,6 +684,9 @@ function SFrames.Pet:UpdatePowerType()
else
self.frame.power:SetStatusBarColor(0, 0, 1)
end
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyBarGradient(self.frame.power)
+ end
end
function SFrames.Pet:UpdatePower()
@@ -594,6 +694,7 @@ function SFrames.Pet:UpdatePower()
local maxPower = UnitManaMax("pet")
self.frame.power:SetMinMaxValues(0, maxPower)
self.frame.power:SetValue(power)
+ SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, "pet")
end
function SFrames.Pet:UpdateHappiness()
diff --git a/Units/Player.lua b/Units/Player.lua
index cb41dd5..ca99e97 100644
--- a/Units/Player.lua
+++ b/Units/Player.lua
@@ -34,6 +34,17 @@ local function Clamp(value, minValue, maxValue)
return value
end
+local function SetTextureIfPresent(region, texturePath)
+ if region and region.SetTexture and texturePath then
+ region:SetTexture(texturePath)
+ end
+end
+
+local function ApplyFontIfPresent(fs, size, fontKey, fallbackFontKey)
+ if not fs then return end
+ SFrames:ApplyFontString(fs, size, fontKey, fallbackFontKey)
+end
+
function SFrames.Player:GetConfig()
local db = SFramesDB or {}
@@ -52,6 +63,33 @@ function SFrames.Player:GetConfig()
local powerHeight = tonumber(db.playerPowerHeight) or 9
powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, 40)
+ local showPortrait = db.playerShowPortrait ~= false
+ local gradientStyle = SFrames:IsGradientStyle()
+ local classicDefaultPowerWidth = width - (showPortrait and portraitWidth or 0) - 2
+ if classicDefaultPowerWidth < 60 then
+ classicDefaultPowerWidth = 60
+ end
+
+ local rawPowerWidth = tonumber(db.playerPowerWidth)
+ local legacyFullWidth = tonumber(db.playerFrameWidth) or width
+ local defaultPowerWidth = gradientStyle and width or classicDefaultPowerWidth
+ local maxPowerWidth = gradientStyle and width or (width - 2)
+ local powerWidth
+ if gradientStyle then
+ -- 渐变风格:能量条始终与血条等宽(全宽)
+ powerWidth = width
+ elseif not rawPowerWidth
+ or math.abs(rawPowerWidth - legacyFullWidth) < 0.5
+ or math.abs(rawPowerWidth - classicDefaultPowerWidth) < 0.5 then
+ powerWidth = defaultPowerWidth
+ else
+ powerWidth = rawPowerWidth
+ end
+ powerWidth = Clamp(math.floor(powerWidth + 0.5), 60, maxPowerWidth)
+
+ local powerOffsetX = Clamp(math.floor((tonumber(db.playerPowerOffsetX) or 0) + 0.5), -120, 120)
+ local powerOffsetY = Clamp(math.floor((tonumber(db.playerPowerOffsetY) or 0) + 0.5), -80, 80)
+
local height = healthHeight + powerHeight + 4
height = Clamp(height, 30, 140)
@@ -61,6 +99,12 @@ function SFrames.Player:GetConfig()
local valueFont = tonumber(db.playerValueFontSize) or 10
valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18)
+ local healthFont = tonumber(db.playerHealthFontSize) or valueFont
+ healthFont = Clamp(math.floor(healthFont + 0.5), 8, 18)
+
+ local powerFont = tonumber(db.playerPowerFontSize) or valueFont
+ powerFont = Clamp(math.floor(powerFont + 0.5), 8, 18)
+
local frameScale = tonumber(db.playerFrameScale) or 1
frameScale = Clamp(frameScale, 0.7, 1.8)
@@ -70,8 +114,16 @@ function SFrames.Player:GetConfig()
portraitWidth = portraitWidth,
healthHeight = healthHeight,
powerHeight = powerHeight,
+ powerWidth = powerWidth,
+ powerOffsetX = powerOffsetX,
+ powerOffsetY = powerOffsetY,
+ powerOnTop = db.playerPowerOnTop == true,
nameFont = nameFont,
valueFont = valueFont,
+ healthFont = healthFont,
+ powerFont = powerFont,
+ healthTexture = SFrames:ResolveBarTexture("playerHealthTexture", "barTexture"),
+ powerTexture = SFrames:ResolveBarTexture("playerPowerTexture", "barTexture"),
scale = frameScale,
}
end
@@ -146,8 +198,8 @@ function SFrames.Player:ApplyConfig()
if f.power then
f.power:ClearAllPoints()
- f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
- f.power:SetPoint("TOPRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
+ f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", cfg.powerOffsetX, -1 + cfg.powerOffsetY)
+ f.power:SetWidth(cfg.powerWidth)
f.power:SetHeight(cfg.powerHeight)
end
@@ -161,6 +213,18 @@ function SFrames.Player:ApplyConfig()
if showPortrait then f.restOverlay:SetAlpha(1) else f.restOverlay:SetAlpha(0) end
end
+ if f.health and f.power then
+ local healthLevel = f:GetFrameLevel() + 2
+ local powerLevel = cfg.powerOnTop and (healthLevel + 1) or (healthLevel - 1)
+ f.health:SetFrameLevel(healthLevel)
+ f.power:SetFrameLevel(powerLevel)
+ end
+
+ SFrames:ApplyConfiguredUnitBackdrop(f, "player")
+ if f.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.healthBGFrame, "player") end
+ if f.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.powerBGFrame, "player") end
+ if f.portraitBG then SFrames:ApplyConfiguredUnitBackdrop(f.portraitBG, "player", true) end
+
if f.castbar then
f.castbar:ClearAllPoints()
if showPortrait then
@@ -172,49 +236,134 @@ function SFrames.Player:ApplyConfig()
end
end
- local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
- local fontPath = SFrames:GetFont()
+ SFrames:ApplyStatusBarTexture(f.health, "playerHealthTexture", "barTexture")
+ SFrames:ApplyStatusBarTexture(f.power, "playerPowerTexture", "barTexture")
+ if f.manaBar then
+ SFrames:ApplyStatusBarTexture(f.manaBar, "playerPowerTexture", "barTexture")
+ end
+ SetTextureIfPresent(f.health and f.health.bg, cfg.healthTexture)
+ SetTextureIfPresent(f.health and f.health.healPredMine, cfg.healthTexture)
+ SetTextureIfPresent(f.health and f.health.healPredOther, cfg.healthTexture)
+ SetTextureIfPresent(f.health and f.health.healPredOver, cfg.healthTexture)
+ SetTextureIfPresent(f.power and f.power.bg, cfg.powerTexture)
+ SetTextureIfPresent(f.manaBar and f.manaBar.bg, cfg.powerTexture)
- if f.nameText then
- f.nameText:SetFont(fontPath, cfg.nameFont, outline)
- end
- if f.healthText then
- f.healthText:SetFont(fontPath, cfg.valueFont, outline)
- end
- if f.powerText then
- f.powerText:SetFont(fontPath, cfg.valueFont, outline)
+ -- Gradient style preset
+ if SFrames:IsGradientStyle() then
+ -- Hide portrait, its backdrop, and class icon (no portrait in gradient mode)
+ if f.portrait then f.portrait:Hide() end
+ if f.portraitBG then f.portraitBG:Hide() end
+ if f.restOverlay then f.restOverlay:SetAlpha(0) end
+ if f.classIcon then f.classIcon:Hide(); if f.classIcon.overlay then f.classIcon.overlay:Hide() end end
+ -- Strip backdrops
+ SFrames:ClearBackdrop(f)
+ SFrames:ClearBackdrop(f.healthBGFrame)
+ SFrames:ClearBackdrop(f.powerBGFrame)
+ -- Health bar full width
+ if f.health then
+ f.health:ClearAllPoints()
+ f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
+ f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
+ f.health:SetHeight(cfg.healthHeight)
+ end
+ -- Power bar full width, below health
+ if f.power then
+ f.power:ClearAllPoints()
+ f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", cfg.powerOffsetX, -2 + cfg.powerOffsetY)
+ f.power:SetWidth(cfg.powerWidth)
+ f.power:SetHeight(cfg.powerHeight)
+ end
+ -- Apply gradient overlays
+ SFrames:ApplyGradientStyle(f.health)
+ SFrames:ApplyGradientStyle(f.power)
+ if f.manaBar then SFrames:ApplyGradientStyle(f.manaBar) end
+ -- Reposition healthBGFrame / powerBGFrame flush (no border padding)
+ if f.healthBGFrame then
+ f.healthBGFrame:ClearAllPoints()
+ f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", 0, 0)
+ f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
+ end
+ if f.powerBGFrame then
+ f.powerBGFrame:ClearAllPoints()
+ f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", 0, 0)
+ f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 0, 0)
+ end
+ -- Hide bar backgrounds (transparent)
+ if f.healthBGFrame then f.healthBGFrame:Hide() end
+ if f.powerBGFrame then f.powerBGFrame:Hide() end
+ if f.health and f.health.bg then f.health.bg:Hide() end
+ if f.power and f.power.bg then f.power.bg:Hide() end
+ if f.manaBar and f.manaBar.bg then f.manaBar.bg:Hide() end
+ else
+ -- Classic style: remove gradient overlays if they exist
+ SFrames:RemoveGradientStyle(f.health)
+ SFrames:RemoveGradientStyle(f.power)
+ if f.manaBar then SFrames:RemoveGradientStyle(f.manaBar) end
+ -- Restore bar backgrounds
+ if f.healthBGFrame then f.healthBGFrame:Show() end
+ if f.powerBGFrame then f.powerBGFrame:Show() end
+ if f.health and f.health.bg then f.health.bg:Show() end
+ if f.power and f.power.bg then f.power.bg:Show() end
+ if f.manaBar and f.manaBar.bg then f.manaBar.bg:Show() end
end
+
+ ApplyFontIfPresent(f.nameText, cfg.nameFont, "playerNameFontKey")
+ ApplyFontIfPresent(f.healthText, cfg.healthFont, "playerHealthFontKey")
+ ApplyFontIfPresent(f.powerText, cfg.powerFont, "playerPowerFontKey")
if f.manaText then
- local manaFont = cfg.valueFont - 1
+ local manaFont = cfg.powerFont - 1
if manaFont < 8 then manaFont = 8 end
- f.manaText:SetFont(fontPath, manaFont, outline)
+ ApplyFontIfPresent(f.manaText, manaFont, "playerPowerFontKey")
end
if f.zLetters then
for i = 1, 3 do
if f.zLetters[i] and f.zLetters[i].text then
- f.zLetters[i].text:SetFont(fontPath, 8 + (i - 1) * 3, "OUTLINE")
+ SFrames:ApplyFontString(f.zLetters[i].text, 8 + (i - 1) * 3, nil, "fontKey")
end
end
end
+ -- Icon positions
+ if f.leaderIcon then
+ local ox = tonumber(db.playerLeaderIconOffsetX) or 0
+ local oy = tonumber(db.playerLeaderIconOffsetY) or 0
+ f.leaderIcon:ClearAllPoints()
+ f.leaderIcon:SetPoint("TOPLEFT", f, "TOPLEFT", ox, oy)
+ end
+ if f.raidIconOverlay then
+ local ox = tonumber(db.playerRaidIconOffsetX) or 0
+ local oy = tonumber(db.playerRaidIconOffsetY) or 0
+ f.raidIconOverlay:ClearAllPoints()
+ f.raidIconOverlay:SetPoint("CENTER", f.health, "TOP", ox, oy)
+ end
+
self:UpdateAll()
end
+local RAINBOW_TEX_PATH = "Interface\\AddOns\\Nanami-UI\\img\\progress"
+
function SFrames.Player:Initialize()
local f = CreateFrame("Button", "SFramesPlayerFrame", UIParent)
f:SetWidth(SFrames.Config.width)
f:SetHeight(SFrames.Config.height)
+
+ local frameScale = (SFramesDB and type(SFramesDB.playerFrameScale) == "number") and SFramesDB.playerFrameScale or 1
+ f:SetScale(frameScale)
+
if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["PlayerFrame"] then
local pos = SFramesDB.Positions["PlayerFrame"]
- f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs)
+ local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
+ if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
+ f:SetPoint(pos.point, UIParent, pos.relativePoint,
+ (pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
+ else
+ f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
+ end
else
f:SetPoint("CENTER", UIParent, "CENTER", -200, -100)
end
- local frameScale = (SFramesDB and type(SFramesDB.playerFrameScale) == "number") and SFramesDB.playerFrameScale or 1
- f:SetScale(frameScale)
-
- -- Make it movable
+
f:SetMovable(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
@@ -224,6 +373,11 @@ function SFrames.Player:Initialize()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint()
+ local fSc = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
+ if fSc > 0.01 and math.abs(fSc - 1) > 0.001 then
+ xOfs = (xOfs or 0) * fSc
+ yOfs = (yOfs or 0) * fSc
+ end
SFramesDB.Positions["PlayerFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
end)
@@ -269,7 +423,7 @@ function SFrames.Player:Initialize()
local hbg = CreateFrame("Frame", nil, f)
hbg:SetPoint("TOPLEFT", f.health, "TOPLEFT", -1, 1)
hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1)
- hbg:SetFrameLevel(f:GetFrameLevel() - 1)
+ hbg:SetFrameLevel(math.max(0, f:GetFrameLevel() - 1))
SFrames:CreateUnitBackdrop(hbg)
f.healthBGFrame = hbg
@@ -308,7 +462,7 @@ function SFrames.Player:Initialize()
local powerbg = CreateFrame("Frame", nil, f)
powerbg:SetPoint("TOPLEFT", f.power, "TOPLEFT", -1, 1)
powerbg:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
- powerbg:SetFrameLevel(f:GetFrameLevel() - 1)
+ powerbg:SetFrameLevel(math.max(0, f:GetFrameLevel() - 1))
SFrames:CreateUnitBackdrop(powerbg)
f.powerBGFrame = powerbg
@@ -318,6 +472,12 @@ function SFrames.Player:Initialize()
f.power.bg:SetTexture(SFrames:GetTexture())
f.power.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
+ -- Rainbow overlay for power bar
+ f.power.rainbowTex = f.power:CreateTexture(nil, "OVERLAY")
+ f.power.rainbowTex:SetTexture(RAINBOW_TEX_PATH)
+ f.power.rainbowTex:SetAllPoints()
+ f.power.rainbowTex:Hide()
+
-- Five-second rule ticker (mana regen delay indicator)
f.power.fsrGlow = f.power:CreateTexture(nil, "OVERLAY")
f.power.fsrGlow:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
@@ -433,7 +593,7 @@ function SFrames.Player:Initialize()
f.leaderIcon:SetTexture("Interface\\GroupFrame\\UI-Group-LeaderIcon")
f.leaderIcon:SetWidth(16)
f.leaderIcon:SetHeight(16)
- f.leaderIcon:SetPoint("TOPLEFT", f.portrait, "TOPLEFT", -4, 4)
+ f.leaderIcon:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
f.leaderIcon:Hide()
-- Raid Target Icon (top center of health bar, half outside frame)
@@ -467,39 +627,6 @@ function SFrames.Player:Initialize()
SFrames:RegisterEvent("PLAYER_LEVEL_UP", function()
if arg1 then self.currentLevel = arg1 end
self:UpdateAll()
- if arg1 and mod(arg1, 2) == 0 and (not SFramesDB or SFramesDB.trainerReminder ~= false) then
- self:ShowTrainerReminder(arg1)
- end
- end)
- SFrames:RegisterEvent("TRAINER_SHOW", function()
- SFrames.Player.trainerScannedThisVisit = nil
- SFrames.Player.trainerShowPending = true
- SFrames.Player.trainerRetryCount = 0
- if not SFrames.Player.trainerRetryFrame then
- SFrames.Player.trainerRetryFrame = CreateFrame("Frame")
- end
- SFrames.Player.trainerRetryFrame:SetScript("OnUpdate", function()
- if not this.elapsed then this.elapsed = 0 end
- this.elapsed = this.elapsed + arg1
- if this.elapsed < 0.3 then return end
- this.elapsed = 0
- if SFrames.Player.trainerScannedThisVisit then
- this:SetScript("OnUpdate", nil)
- return
- end
- SFrames.Player.trainerRetryCount = (SFrames.Player.trainerRetryCount or 0) + 1
- if SFrames.Player.trainerRetryCount > 10 then
- SFrames.Player:ScanTrainer()
- this:SetScript("OnUpdate", nil)
- return
- end
- SFrames.Player:ScanTrainer()
- end)
- end)
- SFrames:RegisterEvent("TRAINER_UPDATE", function()
- if not SFrames.Player.trainerScannedThisVisit then
- SFrames.Player:ScanTrainer()
- end
end)
SFrames:RegisterEvent("PARTY_MEMBERS_CHANGED", function() self:UpdateLeaderIcon() end)
SFrames:RegisterEvent("PARTY_LEADER_CHANGED", function() self:UpdateLeaderIcon() end)
@@ -528,471 +655,6 @@ function SFrames.Player:Initialize()
end)
end
-function SFrames.Player:HasSpellInBook(spellName)
- local baseName = string.gsub(spellName, " 等级 %d+$", "")
- baseName = string.gsub(baseName, " %d+级$", "")
- local i = 1
- while true do
- local name = GetSpellName(i, BOOKTYPE_SPELL)
- if not name then return false end
- if name == spellName or name == baseName then return true end
- i = i + 1
- end
-end
-
-function SFrames.Player:GetSpellIcon(skillDisplayName)
- local baseName = string.gsub(skillDisplayName, " %d+级$", "")
- local altName1 = string.gsub(baseName, ":", ":")
- local altName2 = string.gsub(baseName, ":", ":")
- local i = 1
- while true do
- local name = GetSpellName(i, BOOKTYPE_SPELL)
- if not name then break end
- if name == baseName or name == altName1 or name == altName2 then
- return GetSpellTexture(i, BOOKTYPE_SPELL)
- end
- i = i + 1
- end
- return nil
-end
-
-function SFrames.Player:ParseTrainerTooltipLevel(serviceIndex)
- if not self.trainerScanTip then
- local tt = CreateFrame("GameTooltip", "NanamiTrainerScanTip", nil, "GameTooltipTemplate")
- tt:SetOwner(UIParent, "ANCHOR_NONE")
- self.trainerScanTip = tt
- end
- local tt = self.trainerScanTip
- tt:ClearLines()
- if not tt.SetTrainerService then return nil end
- tt:SetTrainerService(serviceIndex)
- for j = 2, tt:NumLines() do
- local textObj = getglobal("NanamiTrainerScanTipTextLeft" .. j)
- if textObj then
- local text = textObj:GetText()
- if text then
- local _, _, lvl = string.find(text, "需要等级%s*(%d+)")
- if not lvl then
- _, _, lvl = string.find(text, "Requires Level (%d+)")
- end
- if lvl then return tonumber(lvl) end
- end
- end
- end
- return nil
-end
-
-function SFrames.Player:FindSkillLevelInStaticData(classEn, skillName)
- local staticData = SFrames.ClassSkillData and SFrames.ClassSkillData[classEn]
- if not staticData then return nil end
- local baseName = string.gsub(skillName, " %d+级$", "")
- baseName = string.gsub(baseName, "(等级 %d+)$", "")
- baseName = string.gsub(baseName, "%s+$", "")
- for level, skills in pairs(staticData) do
- for _, s in ipairs(skills) do
- local sBase = string.gsub(s, " %d+级$", "")
- sBase = string.gsub(sBase, "(等级 %d+)$", "")
- sBase = string.gsub(sBase, "%s+$", "")
- if baseName == sBase or skillName == s then
- return level
- end
- end
- end
- local talentData = SFrames.TalentTrainerSkills and SFrames.TalentTrainerSkills[classEn]
- if talentData then
- for level, entries in pairs(talentData) do
- for _, entry in ipairs(entries) do
- local tBase = string.gsub(entry[1], " %d+级$", "")
- if baseName == tBase or skillName == entry[1] then
- return level
- end
- end
- end
- end
- return nil
-end
-
-function SFrames.Player:ScanTrainer()
- if self.scanningTrainer then return end
- self.scanningTrainer = true
-
- local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9"
-
- if not GetNumTrainerServices then
- DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff6666训练师扫描失败: GetNumTrainerServices 不存在|r")
- self.scanningTrainer = nil
- return
- end
- if IsTradeskillTrainer and IsTradeskillTrainer() then
- self.scanningTrainer = nil
- 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
-
- local cache = {}
- local numServices = GetNumTrainerServices()
- if not numServices or numServices == 0 then
- DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff6666训练师扫描: 未检测到技能列表 (0项),将在数据加载后重试|r")
- self.scanningTrainer = nil
- return
- end
-
- local hasLevelAPI = (GetTrainerServiceLevelReq ~= nil)
- local noLevelCount = 0
- local totalAdded = 0
-
- local iconMissCount = 0
-
- for i = 1, numServices do
- local name, subText, serviceType = GetTrainerServiceInfo(i)
-
- if name and subText and subText ~= "" then
- local icon = GetTrainerServiceIcon and GetTrainerServiceIcon(i)
-
- if (not icon or icon == "") and ClassTrainerFrame then
- local skillButton = getglobal("ClassTrainerSkill" .. i)
- if skillButton then
- local iconTex = getglobal("ClassTrainerSkill" .. i .. "Icon")
- if iconTex and iconTex.GetTexture then
- icon = iconTex:GetTexture()
- end
- end
- end
-
- if not icon or icon == "" then
- iconMissCount = iconMissCount + 1
- end
-
- local reqLevel
- if hasLevelAPI then
- reqLevel = GetTrainerServiceLevelReq(i)
- end
- if not reqLevel or reqLevel == 0 then
- reqLevel = self:ParseTrainerTooltipLevel(i)
- end
- if not reqLevel or reqLevel == 0 then
- local lookupName = name .. " " .. subText
- reqLevel = self:FindSkillLevelInStaticData(classEn, lookupName)
- if not reqLevel then
- reqLevel = self:FindSkillLevelInStaticData(classEn, name)
- end
- end
- if not reqLevel or reqLevel == 0 then
- noLevelCount = noLevelCount + 1
- end
-
- if reqLevel and reqLevel > 0 then
- local displayName = name .. " " .. subText
- if not cache[reqLevel] then
- cache[reqLevel] = {}
- end
- table.insert(cache[reqLevel], {
- name = displayName,
- icon = icon or "",
- })
- totalAdded = totalAdded + 1
- end
- end
- end
-
- if not SFramesDB.trainerCache then
- SFramesDB.trainerCache = {}
- end
- SFramesDB.trainerCache[classEn] = cache
- self.trainerScannedThisVisit = true
-
- local levelCount = 0
- for _ in pairs(cache) do levelCount = levelCount + 1 end
- local iconOk = totalAdded - iconMissCount
- DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r 已从训练师更新技能数据(" .. levelCount .. " 个等级," .. totalAdded .. " 项技能," .. iconOk .. " 个图标)")
- if noLevelCount > 0 then
- DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff9900有 " .. noLevelCount .. " 项技能无法确定等级要求,已跳过|r")
- end
- if iconMissCount > 0 then
- DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffff9900有 " .. iconMissCount .. " 项技能未获取到图标|r")
- end
-
- self.scanningTrainer = nil
-end
-
-function SFrames.Player:ShowTrainerReminder(newLevel)
- local _, classEn = UnitClass("player")
- local classNames = {
- WARRIOR = "战士", PALADIN = "圣骑士", HUNTER = "猎人",
- ROGUE = "盗贼", PRIEST = "牧师", SHAMAN = "萨满祭司",
- MAGE = "法师", WARLOCK = "术士", DRUID = "德鲁伊",
- }
- local className = classNames[classEn] or UnitClass("player")
-
- local classFallbackIcons = {
- WARRIOR = "Interface\\Icons\\Ability_Warrior_OffensiveStance",
- PALADIN = "Interface\\Icons\\Spell_Holy_HolyBolt",
- HUNTER = "Interface\\Icons\\Ability_Marksmanship",
- ROGUE = "Interface\\Icons\\Ability_BackStab",
- PRIEST = "Interface\\Icons\\Spell_Holy_HolyBolt",
- SHAMAN = "Interface\\Icons\\Spell_Nature_Lightning",
- MAGE = "Interface\\Icons\\Spell_Frost_IceStorm",
- WARLOCK = "Interface\\Icons\\Spell_Shadow_DeathCoil",
- DRUID = "Interface\\Icons\\Spell_Nature_Regeneration",
- }
- local fallbackIcon = classFallbackIcons[classEn] or "Interface\\Icons\\Trade_Engraving"
-
- local allSkills = {}
- local allIcons = {}
- local talentSkills = {}
- local usedCache = false
-
- local classCache = SFramesDB and SFramesDB.trainerCache and SFramesDB.trainerCache[classEn]
- if classCache then
- local lowLevel = newLevel - 1
- if lowLevel < 1 then lowLevel = 1 end
- for lv = lowLevel, newLevel do
- if classCache[lv] then
- usedCache = true
- for _, entry in ipairs(classCache[lv]) do
- if not self:HasSpellInBook(entry.name) then
- table.insert(allSkills, entry.name)
- local ico = entry.icon
- if not ico or ico == "" then
- ico = self:GetSpellIcon(entry.name) or fallbackIcon
- end
- table.insert(allIcons, ico)
- end
- end
- end
- end
- end
-
- if not usedCache then
- local baseSkills = SFrames.ClassSkillData and SFrames.ClassSkillData[classEn] and SFrames.ClassSkillData[classEn][newLevel]
- if baseSkills then
- for _, s in ipairs(baseSkills) do
- table.insert(allSkills, s)
- table.insert(allIcons, self:GetSpellIcon(s) or fallbackIcon)
- end
- end
-
- local talentData = SFrames.TalentTrainerSkills and SFrames.TalentTrainerSkills[classEn] and SFrames.TalentTrainerSkills[classEn][newLevel]
- if talentData then
- for _, entry in ipairs(talentData) do
- local displayName = entry[1]
- local requiredSpell = entry[2]
- if self:HasSpellInBook(requiredSpell) then
- table.insert(allSkills, displayName)
- table.insert(allIcons, self:GetSpellIcon(displayName) or fallbackIcon)
- table.insert(talentSkills, displayName)
- end
- end
- end
- end
-
- local mountQuest = SFrames.ClassMountQuests and SFrames.ClassMountQuests[classEn] and SFrames.ClassMountQuests[classEn][newLevel]
-
- local skillCount = table.getn(allSkills)
-
- if skillCount == 0 and not mountQuest then
- return
- end
-
- local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9"
-
- SFrames:Print(string.format("已达到 %d 级!你的%s训练师有新技能可以学习。", newLevel, className))
- if skillCount > 0 then
- DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r 可学习的技能(共 " .. skillCount .. " 项):")
- local line = " "
- local lineCount = 0
- for i = 1, skillCount do
- if lineCount > 0 then line = line .. ", " end
- line = line .. "|cffffd100" .. allSkills[i] .. "|r"
- lineCount = lineCount + 1
- if i == skillCount or lineCount == 4 then
- DEFAULT_CHAT_FRAME:AddMessage(line)
- line = " "
- lineCount = 0
- end
- end
- end
- if not usedCache and table.getn(talentSkills) > 0 then
- DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cff00ff00(天赋)|r " .. table.concat(talentSkills, ", "))
- end
- if mountQuest then
- DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r |cffffff00★|r " .. mountQuest)
- end
-
- local bannerMsg = string.format("Lv.%d - %s训练师有 %d 项新技能可学习", newLevel, className, skillCount)
- if mountQuest then
- bannerMsg = bannerMsg .. " + " .. mountQuest
- end
- if skillCount == 0 and not mountQuest then
- bannerMsg = string.format("Lv.%d - 前往%s训练师查看可学技能", newLevel, className)
- elseif skillCount == 0 and mountQuest then
- bannerMsg = string.format("Lv.%d - %s", newLevel, mountQuest)
- end
- UIErrorsFrame:AddMessage(bannerMsg, 1.0, 0.82, 0.0, 1, 5)
- PlaySound("LEVELUP")
-
- if not self.trainerReminderFrame then
- local fr = CreateFrame("Frame", "NanamiTrainerReminder", UIParent)
- fr:SetWidth(440)
- fr:SetHeight(106)
- fr:SetPoint("TOP", UIParent, "TOP", 0, -120)
- fr:SetFrameStrata("DIALOG")
-
- local bg = fr:CreateTexture(nil, "BACKGROUND")
- bg:SetAllPoints(fr)
- bg:SetTexture(0, 0, 0, 0.78)
-
- local border = fr:CreateTexture(nil, "BORDER")
- border:SetPoint("TOPLEFT", fr, "TOPLEFT", -1, 1)
- border:SetPoint("BOTTOMRIGHT", fr, "BOTTOMRIGHT", 1, -1)
- border:SetTexture(1, 0.82, 0, 0.3)
-
- local icon = fr:CreateTexture(nil, "ARTWORK")
- icon:SetWidth(36)
- icon:SetHeight(36)
- icon:SetPoint("TOPLEFT", fr, "TOPLEFT", 10, -6)
- icon:SetTexture("Interface\\Icons\\INV_Misc_Book_11")
- fr.icon = icon
-
- local title = fr:CreateFontString(nil, "OVERLAY", "GameFontNormal")
- title:SetPoint("TOPLEFT", icon, "TOPRIGHT", 10, -2)
- title:SetPoint("RIGHT", fr, "RIGHT", -10, 0)
- title:SetJustifyH("LEFT")
- title:SetTextColor(1, 0.82, 0)
- fr.title = title
-
- local subtitle = fr:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
- subtitle:SetPoint("TOPLEFT", icon, "TOPRIGHT", 10, -18)
- subtitle:SetPoint("RIGHT", fr, "RIGHT", -10, 0)
- subtitle:SetJustifyH("LEFT")
- subtitle:SetTextColor(0.75, 0.75, 0.75)
- fr.subtitle = subtitle
-
- fr.skillIcons = {}
- fr.skillBorders = {}
- local maxIcons = 13
- for idx = 1, maxIcons do
- local bdr = fr:CreateTexture(nil, "BORDER")
- bdr:SetWidth(30)
- bdr:SetHeight(30)
- bdr:SetPoint("TOPLEFT", fr, "TOPLEFT", 9 + (idx - 1) * 32, -45)
- bdr:SetTexture(1, 0.82, 0, 0.25)
- bdr:Hide()
- fr.skillBorders[idx] = bdr
-
- local si = fr:CreateTexture(nil, "ARTWORK")
- si:SetWidth(28)
- si:SetHeight(28)
- si:SetPoint("CENTER", bdr, "CENTER", 0, 0)
- si:SetTexCoord(0.07, 0.93, 0.07, 0.93)
- si:Hide()
- fr.skillIcons[idx] = si
- end
-
- local detail = fr:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
- detail:SetPoint("BOTTOMLEFT", fr, "BOTTOMLEFT", 10, 8)
- detail:SetPoint("RIGHT", fr, "RIGHT", -10, 0)
- detail:SetJustifyH("LEFT")
- detail:SetTextColor(0.85, 0.85, 0.85)
- fr.detail = detail
-
- fr:SetAlpha(0)
- fr:Hide()
- self.trainerReminderFrame = fr
- end
-
- local fr = self.trainerReminderFrame
-
- for idx = 1, 13 do
- fr.skillIcons[idx]:Hide()
- fr.skillBorders[idx]:Hide()
- end
-
- local iconCount = 0
-
- for i = 1, skillCount do
- if iconCount >= 13 then break end
- iconCount = iconCount + 1
- fr.skillIcons[iconCount]:SetTexture(allIcons[i])
- fr.skillIcons[iconCount]:Show()
- fr.skillBorders[iconCount]:Show()
- end
- if mountQuest and iconCount < 13 then
- iconCount = iconCount + 1
- fr.skillIcons[iconCount]:SetTexture("Interface\\Icons\\Spell_Nature_Swiftness")
- fr.skillIcons[iconCount]:Show()
- fr.skillBorders[iconCount]:Show()
- end
-
- fr.title:SetText(string.format("已达到 |cffffffff%d|r 级 — %s训练师有新技能", newLevel, className))
-
- if skillCount > 0 or mountQuest then
- local preview = ""
- if skillCount > 0 then
- preview = "|cffffd100" .. allSkills[1] .. "|r"
- if skillCount > 1 then preview = preview .. ", |cffffd100" .. allSkills[2] .. "|r" end
- if skillCount > 2 then preview = preview .. ", |cffffd100" .. allSkills[3] .. "|r" end
- if skillCount > 3 then preview = preview .. " 等 " .. skillCount .. " 项" end
- end
- if mountQuest then
- if preview ~= "" then preview = preview .. " | " end
- preview = preview .. "|cffffff00" .. mountQuest .. "|r"
- end
- fr.subtitle:SetText(preview)
- fr.detail:SetText("详见聊天窗口")
- else
- fr.subtitle:SetText("")
- fr.detail:SetText("前往职业训练师查看可学习的技能")
- end
-
- if iconCount > 0 then
- fr:SetHeight(106)
- else
- fr:SetHeight(80)
- end
- fr:Show()
- fr:SetAlpha(0)
-
- fr.fadeState = "in"
- fr.fadeTimer = 0
- fr.holdTimer = 0
- fr:SetScript("OnUpdate", function()
- local dt = arg1
- if fr.fadeState == "in" then
- fr.fadeTimer = fr.fadeTimer + dt
- local a = fr.fadeTimer / 0.5
- if a >= 1 then
- a = 1
- fr.fadeState = "hold"
- end
- fr:SetAlpha(a)
- elseif fr.fadeState == "hold" then
- fr.holdTimer = fr.holdTimer + dt
- if fr.holdTimer >= 8 then
- fr.fadeState = "out"
- fr.fadeTimer = 0
- end
- elseif fr.fadeState == "out" then
- fr.fadeTimer = fr.fadeTimer + dt
- local a = 1 - fr.fadeTimer / 1.0
- if a <= 0 then
- a = 0
- fr:Hide()
- fr:SetScript("OnUpdate", nil)
- end
- fr:SetAlpha(a)
- end
- end)
-end
-
function SFrames.Player:UpdateAll()
if not self.frame then return end
self:UpdateHealth()
@@ -1029,7 +691,7 @@ function SFrames.Player:UpdateAll()
nameLine = nameLine .. " " .. className
end
- if not (SFramesDB and SFramesDB.playerShowClassIcon == false) then
+ if not SFrames:IsGradientStyle() and not (SFramesDB and SFramesDB.playerShowClassIcon == false) then
SFrames:SetClassIcon(self.frame.classIcon, class)
else
self.frame.classIcon:Hide()
@@ -1037,6 +699,8 @@ function SFrames.Player:UpdateAll()
end
local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false)
+ -- Gradient style always uses class colors
+ if SFrames:IsGradientStyle() then useClassColor = true end
if useClassColor and class and SFrames.Config.colors.class[class] then
local color = SFrames.Config.colors.class[class]
@@ -1050,6 +714,10 @@ function SFrames.Player:UpdateAll()
self.frame.nameText:SetText(nameLine)
self.frame.nameText:SetTextColor(1, 1, 1)
end
+ -- Re-apply gradient after color change (SetStatusBarColor resets SetGradientAlpha)
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyBarGradient(self.frame.health)
+ end
end
function SFrames.Player:UpdateRestingStatus()
@@ -1108,9 +776,9 @@ function SFrames.Player:UpdateHealth()
self.frame.health:SetValue(hp)
if maxHp > 0 then
- self.frame.healthText:SetText(hp .. " / " .. maxHp)
+ self.frame.healthText:SetText(SFrames:FormatCompactPair(hp, maxHp))
else
- self.frame.healthText:SetText(hp)
+ self.frame.healthText:SetText(SFrames:FormatCompactNumber(hp))
end
self:UpdateHealPrediction()
@@ -1122,12 +790,6 @@ function SFrames.Player:UpdateHealPrediction()
local predOther = self.frame.health.healPredOther
local predOver = self.frame.health.healPredOver
- local function HidePredictions()
- predMine:Hide()
- predOther:Hide()
- predOver:Hide()
- end
-
local hp = UnitHealth("player") or 0
local maxHp = UnitHealthMax("player") or 0
@@ -1146,7 +808,7 @@ function SFrames.Player:UpdateHealPrediction()
end
if maxHp <= 0 or UnitIsDeadOrGhost("player") then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1158,7 +820,7 @@ function SFrames.Player:UpdateHealPrediction()
end
local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1166,14 +828,14 @@ function SFrames.Player:UpdateHealPrediction()
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
local showPortrait = SFramesDB and SFramesDB.playerShowPortrait ~= false
local barWidth = self.frame:GetWidth() - (showPortrait and (self.frame.portrait:GetWidth() + 2) or 2)
if barWidth <= 0 then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1183,7 +845,7 @@ function SFrames.Player:UpdateHealPrediction()
local availableWidth = barWidth - currentWidth
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1249,6 +911,9 @@ function SFrames.Player:UpdatePowerType()
else
self.frame.power:SetStatusBarColor(0, 0, 1)
end
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyBarGradient(self.frame.power)
+ end
end
function SFrames.Player:GetDruidAltMana(currentPower, currentMaxPower)
@@ -1393,7 +1058,9 @@ function SFrames.Player:UpdatePower()
local maxPower = UnitManaMax("player")
self.frame.power:SetMinMaxValues(0, maxPower)
self.frame.power:SetValue(power)
- self.frame.powerText:SetText(power .. " / " .. maxPower)
+ self.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
+
+ SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, "player")
local _, class = UnitClass("player")
local powerType = UnitPowerType("player")
@@ -1464,7 +1131,7 @@ function SFrames.Player:UpdatePower()
if maxMana and maxMana > 0 then
local pct = math.floor(mana / maxMana * 100 + 0.5)
- self.frame.manaText:SetText(pct .. "% " .. mana)
+ self.frame.manaText:SetText(pct .. "% " .. SFrames:FormatCompactNumber(mana))
self.frame.manaText:Show()
if self.frame.manaBar then
self.frame.manaBar:SetMinMaxValues(0, maxMana)
@@ -1633,38 +1300,16 @@ local function HSVtoRGB(h, s, v)
else return v, p, q end
end
-local RAINBOW_TEX_PATH = "Interface\\AddOns\\Nanami-UI\\img\\progress"
-local RAINBOW_SEG_COORDS = {
- {40/512, 473/512, 45/512, 169/512},
- {40/512, 473/512, 194/512, 318/512},
- {40/512, 473/512, 343/512, 467/512},
-}
-
local function UpdateRainbowProgress(cb, progress)
- if not cb.rainbowSegs then return end
+ if not cb.rainbowTex then return end
local barWidth = cb:GetWidth()
if barWidth <= 0 then return end
local fillW = progress * barWidth
- for i = 1, 3 do
- local seg = cb.rainbowSegs[i]
- local segL = barWidth * (i - 1) / 3
- local segR = barWidth * i / 3
- if fillW <= segL then
- seg:Hide()
- else
- local visR = math.min(fillW, segR)
- local frac = (visR - segL) / (segR - segL)
- if visR >= segR and i < 3 then
- visR = segR + 2
- end
- seg:ClearAllPoints()
- seg:SetPoint("TOPLEFT", cb, "TOPLEFT", segL, 0)
- seg:SetPoint("BOTTOMRIGHT", cb, "BOTTOMLEFT", visR, 0)
- local c = seg.fullCoords
- seg:SetTexCoord(c[1], c[1] + (c[2] - c[1]) * frac, c[3], c[4])
- seg:Show()
- end
- end
+ cb.rainbowTex:ClearAllPoints()
+ cb.rainbowTex:SetPoint("TOPLEFT", cb, "TOPLEFT", 0, 0)
+ cb.rainbowTex:SetPoint("BOTTOMRIGHT", cb, "BOTTOMLEFT", fillW, 0)
+ cb.rainbowTex:SetTexCoord(0, progress, 0, 1)
+ cb.rainbowTex:Show()
end
function SFrames.Player:CreateCastbar()
@@ -1710,16 +1355,9 @@ function SFrames.Player:CreateCastbar()
lagTex:Hide()
cb.lagTex = lagTex
- cb.rainbowSegs = {}
- for i = 1, 3 do
- local tex = cb:CreateTexture(nil, "ARTWORK")
- tex:SetTexture(RAINBOW_TEX_PATH)
- local c = RAINBOW_SEG_COORDS[i]
- tex:SetTexCoord(c[1], c[2], c[3], c[4])
- tex.fullCoords = c
- tex:Hide()
- cb.rainbowSegs[i] = tex
- end
+ cb.rainbowTex = cb:CreateTexture(nil, "ARTWORK")
+ cb.rainbowTex:SetTexture(RAINBOW_TEX_PATH)
+ cb.rainbowTex:Hide()
cb:Hide()
cbbg:Hide()
@@ -1759,15 +1397,27 @@ function SFrames.Player:ApplyCastbarPosition()
cb:SetParent(UIParent)
cb.cbbg:SetParent(UIParent)
cb.ibg:SetParent(UIParent)
- cb:SetWidth(280)
- cb:SetHeight(20)
- cb:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, 120)
+ local cbW = db.castbarWidth or 280
+ local cbH = db.castbarHeight or 20
+ cb:SetWidth(cbW)
+ cb:SetHeight(cbH)
cb:SetFrameStrata("HIGH")
+ if SFrames.Movers and SFrames.Movers.ApplyPosition then
+ SFrames.Movers:ApplyPosition("PlayerCastbar", cb,
+ "BOTTOM", "SFramesPetHolder", "TOP", 0, 6)
+ else
+ local petHolder = _G["SFramesPetHolder"]
+ if petHolder then
+ cb:SetPoint("BOTTOM", petHolder, "TOP", 0, 6)
+ else
+ cb:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, 120)
+ end
+ end
cb.cbbg:SetPoint("TOPLEFT", cb, "TOPLEFT", -1, 1)
cb.cbbg:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 1, -1)
cb.cbbg:SetFrameLevel(math.max(1, cb:GetFrameLevel() - 1))
- cb.icon:SetWidth(22)
- cb.icon:SetHeight(22)
+ cb.icon:SetWidth(cbH + 2)
+ cb.icon:SetHeight(cbH + 2)
cb.icon:SetPoint("RIGHT", cb, "LEFT", -4, 0)
cb.ibg:SetPoint("TOPLEFT", cb.icon, "TOPLEFT", -1, 1)
cb.ibg:SetPoint("BOTTOMRIGHT", cb.icon, "BOTTOMRIGHT", 1, -1)
@@ -1775,7 +1425,11 @@ function SFrames.Player:ApplyCastbarPosition()
if SFrames.Movers and SFrames.Movers.RegisterMover then
SFrames.Movers:RegisterMover("PlayerCastbar", cb, "施法条",
- "BOTTOM", "UIParent", "BOTTOM", 0, 120)
+ "BOTTOM", "SFramesPetHolder", "TOP", 0, 6,
+ nil, { alwaysShowInLayout = true })
+ end
+ if SFrames.Movers and SFrames.Movers.SetMoverAlwaysShow then
+ SFrames.Movers:SetMoverAlwaysShow("PlayerCastbar", true)
end
else
cb:SetParent(self.frame)
@@ -1796,6 +1450,9 @@ function SFrames.Player:ApplyCastbarPosition()
cb.ibg:SetPoint("TOPLEFT", cb.icon, "TOPLEFT", -1, 1)
cb.ibg:SetPoint("BOTTOMRIGHT", cb.icon, "BOTTOMRIGHT", 1, -1)
cb.ibg:SetFrameLevel(math.max(1, cb:GetFrameLevel() - 1))
+ if SFrames.Movers and SFrames.Movers.SetMoverAlwaysShow then
+ SFrames.Movers:SetMoverAlwaysShow("PlayerCastbar", false)
+ end
end
end
@@ -1836,11 +1493,12 @@ function SFrames.Player:CastbarStart(spellName, duration)
cb.ibg:Hide()
end
- cb:SetAlpha(1)
- cb.cbbg:SetAlpha(1)
+ local cbAlpha = tonumber((SFramesDB or {}).castbarAlpha) or 1
+ cb:SetAlpha(cbAlpha)
+ cb.cbbg:SetAlpha(cbAlpha)
if texture then
- cb.icon:SetAlpha(1)
- cb.ibg:SetAlpha(1)
+ cb.icon:SetAlpha(cbAlpha)
+ cb.ibg:SetAlpha(cbAlpha)
end
cb:Show()
cb.cbbg:Show()
@@ -1901,11 +1559,12 @@ function SFrames.Player:CastbarChannelStart(duration, spellName)
cb.ibg:Hide()
end
- cb:SetAlpha(1)
- cb.cbbg:SetAlpha(1)
+ local cbAlpha = tonumber((SFramesDB or {}).castbarAlpha) or 1
+ cb:SetAlpha(cbAlpha)
+ cb.cbbg:SetAlpha(cbAlpha)
if texture then
- cb.icon:SetAlpha(1)
- cb.ibg:SetAlpha(1)
+ cb.icon:SetAlpha(cbAlpha)
+ cb.ibg:SetAlpha(cbAlpha)
end
cb:Show()
cb.cbbg:Show()
@@ -1961,6 +1620,9 @@ function SFrames.Player:CastbarOnUpdate()
cb.casting = nil
cb.fadeOut = true
cb:SetValue(cb.maxValue)
+ if db.castbarRainbow and cb.rainbowActive then
+ UpdateRainbowProgress(cb, 1)
+ end
return
end
cb:SetValue(elapsed)
@@ -1973,9 +1635,7 @@ function SFrames.Player:CastbarOnUpdate()
UpdateRainbowProgress(cb, elapsed / cb.maxValue)
elseif cb.rainbowActive then
cb:SetStatusBarColor(1, 0.7, 0)
- if cb.rainbowSegs then
- for i = 1, 3 do cb.rainbowSegs[i]:Hide() end
- end
+ if cb.rainbowTex then cb.rainbowTex:Hide() end
cb.rainbowActive = nil
end
if not cb.icon:IsShown() then
@@ -1987,6 +1647,9 @@ function SFrames.Player:CastbarOnUpdate()
cb.channeling = nil
cb.fadeOut = true
cb:SetValue(0)
+ if db.castbarRainbow and cb.rainbowActive then
+ UpdateRainbowProgress(cb, 0)
+ end
return
end
cb:SetValue(timeRemaining)
@@ -1999,20 +1662,13 @@ function SFrames.Player:CastbarOnUpdate()
UpdateRainbowProgress(cb, timeRemaining / cb.maxValue)
elseif cb.rainbowActive then
cb:SetStatusBarColor(1, 0.7, 0)
- if cb.rainbowSegs then
- for i = 1, 3 do cb.rainbowSegs[i]:Hide() end
- end
+ if cb.rainbowTex then cb.rainbowTex:Hide() end
cb.rainbowActive = nil
end
if not cb.icon:IsShown() then
self:CastbarTryResolveIcon()
end
elseif cb.fadeOut then
- if cb.rainbowActive then
- for i = 1, 3 do cb.rainbowSegs[i]:Hide() end
- cb:SetStatusBarColor(1, 0.7, 0)
- cb.rainbowActive = nil
- end
local alpha = cb:GetAlpha() - 0.05
if alpha > 0 then
cb:SetAlpha(alpha)
@@ -2021,6 +1677,11 @@ function SFrames.Player:CastbarOnUpdate()
cb.ibg:SetAlpha(alpha)
else
cb.fadeOut = nil
+ if cb.rainbowActive then
+ if cb.rainbowTex then cb.rainbowTex:Hide() end
+ cb:SetStatusBarColor(1, 0.7, 0)
+ cb.rainbowActive = nil
+ end
cb:Hide()
cb.cbbg:Hide()
cb.icon:Hide()
@@ -2059,9 +1720,10 @@ function SFrames.Player:CastbarTryResolveIcon()
end
if texture then
+ local cbAlpha = tonumber((SFramesDB or {}).castbarAlpha) or 1
cb.icon:SetTexture(texture)
- cb.icon:SetAlpha(1)
- cb.ibg:SetAlpha(1)
+ cb.icon:SetAlpha(cbAlpha)
+ cb.ibg:SetAlpha(cbAlpha)
cb.icon:Show()
cb.ibg:Show()
end
diff --git a/Units/Raid.lua b/Units/Raid.lua
index e5677af..1dd9ad3 100644
--- a/Units/Raid.lua
+++ b/Units/Raid.lua
@@ -9,10 +9,76 @@ local UNIT_PADDING = 2
local RAID_UNIT_LOOKUP = {}
for i = 1, 40 do RAID_UNIT_LOOKUP["raid" .. i] = true end
+-- Pre-allocated tables reused every UpdateAuras call to avoid per-call garbage
+local _foundIndicators = { [1] = false, [2] = false, [3] = false, [4] = false }
+local _debuffColor = { r = 0, g = 0, b = 0 }
+
+-- Module-level helper: match aura name against a list (no closure allocation)
+local function MatchesList(auraName, list)
+ for _, name in ipairs(list) do
+ if string.find(auraName, name) then
+ return true
+ end
+ end
+ return false
+end
+
+-- Module-level helper: get buff name via SuperWoW aura ID or tooltip scan
+local function RaidGetBuffName(unit, index)
+ if SFrames.superwow_active and SpellInfo then
+ local texture, auraID = UnitBuff(unit, index)
+ if auraID and SpellInfo then
+ local spellName = SpellInfo(auraID)
+ if spellName and spellName ~= "" then
+ return spellName, texture
+ end
+ end
+ end
+ SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
+ SFrames.Tooltip:SetUnitBuff(unit, index)
+ local buffName = SFramesScanTooltipTextLeft1:GetText()
+ SFrames.Tooltip:Hide()
+ return buffName, UnitBuff(unit, index)
+end
+
+local function RaidGetDebuffName(unit, index)
+ if SFrames.superwow_active and SpellInfo then
+ local texture, count, dtype, auraID = UnitDebuff(unit, index)
+ if auraID and SpellInfo then
+ local spellName = SpellInfo(auraID)
+ if spellName and spellName ~= "" then
+ return spellName, texture, count, dtype
+ end
+ end
+ end
+ SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
+ SFrames.Tooltip:SetUnitDebuff(unit, index)
+ local debuffName = SFramesScanTooltipTextLeft1:GetText()
+ SFrames.Tooltip:Hide()
+ local texture, count, dtype = UnitDebuff(unit, index)
+ return debuffName, texture, count, dtype
+end
+
local function GetIncomingHeals(unit)
return SFrames:GetIncomingHeals(unit)
end
+local function Clamp(value, minValue, maxValue)
+ if value < minValue then
+ return minValue
+ end
+ if value > maxValue then
+ return maxValue
+ end
+ return value
+end
+
+local function SetTextureIfPresent(region, texturePath)
+ if region and region.SetTexture and texturePath then
+ region:SetTexture(texturePath)
+ end
+end
+
function SFrames.Raid:GetMetrics()
local db = SFramesDB or {}
@@ -25,29 +91,66 @@ function SFrames.Raid:GetMetrics()
local healthHeight = tonumber(db.raidHealthHeight) or math.floor((height - 3) * 0.8)
healthHeight = math.max(10, math.min(height - 6, healthHeight))
- local powerHeight = height - healthHeight - 3
+ local powerHeight = tonumber(db.raidPowerHeight) or (height - healthHeight - 3)
+ powerHeight = math.max(0, math.min(height - 3, powerHeight))
if not db.raidShowPower then
powerHeight = 0
healthHeight = height - 2
end
+ local gradientStyle = SFrames:IsGradientStyle()
+ local availablePowerWidth = width - 2
+ if availablePowerWidth < 20 then
+ availablePowerWidth = 20
+ end
+
+ local rawPowerWidth = tonumber(db.raidPowerWidth)
+ local legacyFullWidth = tonumber(db.raidFrameWidth) or width
+ local defaultPowerWidth = gradientStyle and width or availablePowerWidth
+ local maxPowerWidth = gradientStyle and width or availablePowerWidth
+ local powerWidth
+ if gradientStyle then
+ -- 渐变风格:能量条始终与血条等宽(全宽)
+ powerWidth = width
+ elseif not rawPowerWidth
+ or math.abs(rawPowerWidth - legacyFullWidth) < 0.5
+ or math.abs(rawPowerWidth - availablePowerWidth) < 0.5 then
+ powerWidth = defaultPowerWidth
+ else
+ powerWidth = rawPowerWidth
+ end
+ powerWidth = Clamp(math.floor(powerWidth + 0.5), 20, maxPowerWidth)
+
+ local powerOffsetX = Clamp(math.floor((tonumber(db.raidPowerOffsetX) or 0) + 0.5), -120, 120)
+ local powerOffsetY = Clamp(math.floor((tonumber(db.raidPowerOffsetY) or 0) + 0.5), -80, 80)
+
local hgap = tonumber(db.raidHorizontalGap) or UNIT_PADDING
local vgap = tonumber(db.raidVerticalGap) or UNIT_PADDING
local groupGap = tonumber(db.raidGroupGap) or GROUP_PADDING
local nameFont = tonumber(db.raidNameFontSize) or 10
local valueFont = tonumber(db.raidValueFontSize) or 9
+ local healthFont = tonumber(db.raidHealthFontSize) or valueFont
+ local powerFont = tonumber(db.raidPowerFontSize) or valueFont
return {
width = width,
height = height,
healthHeight = healthHeight,
powerHeight = powerHeight,
+ powerWidth = powerWidth,
+ powerOffsetX = powerOffsetX,
+ powerOffsetY = powerOffsetY,
+ powerOnTop = db.raidPowerOnTop == true,
horizontalGap = hgap,
verticalGap = vgap,
groupGap = groupGap,
nameFont = nameFont,
valueFont = valueFont,
+ healthFont = healthFont,
+ powerFont = powerFont,
+ healthTexture = SFrames:ResolveBarTexture("raidHealthTexture", "barTexture"),
+ powerTexture = SFrames:ResolveBarTexture("raidPowerTexture", "barTexture"),
showPower = db.raidShowPower ~= false,
}
end
@@ -87,8 +190,8 @@ function SFrames.Raid:ApplyFrameStyle(frame, metrics)
frame.power:Show()
if frame.powerBGFrame then frame.powerBGFrame:Show() end
frame.power:ClearAllPoints()
- frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1)
- frame.power:SetPoint("TOPRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
+ frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
+ frame.power:SetWidth(metrics.powerWidth)
frame.power:SetHeight(metrics.powerHeight)
else
frame.power:Hide()
@@ -96,14 +199,80 @@ function SFrames.Raid:ApplyFrameStyle(frame, metrics)
end
end
- local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
- local fontPath = SFrames:GetFont()
+ SFrames:ApplyStatusBarTexture(frame.health, "raidHealthTexture", "barTexture")
+ SFrames:ApplyStatusBarTexture(frame.power, "raidPowerTexture", "barTexture")
+ if frame.health and frame.power then
+ local healthLevel = frame:GetFrameLevel() + 2
+ local powerLevel = metrics.powerOnTop and (healthLevel + 1) or (healthLevel - 1)
+ frame.health:SetFrameLevel(healthLevel)
+ frame.power:SetFrameLevel(powerLevel)
+ end
+ SFrames:ApplyConfiguredUnitBackdrop(frame, "raid")
+ if frame.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.healthBGFrame, "raid") end
+ if frame.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(frame.powerBGFrame, "raid") end
+ SetTextureIfPresent(frame.health and frame.health.bg, metrics.healthTexture)
+ SetTextureIfPresent(frame.health and frame.health.healPredMine, metrics.healthTexture)
+ SetTextureIfPresent(frame.health and frame.health.healPredOther, metrics.healthTexture)
+ SetTextureIfPresent(frame.health and frame.health.healPredOver, metrics.healthTexture)
+ SetTextureIfPresent(frame.power and frame.power.bg, metrics.powerTexture)
+
+ -- Gradient style preset
+ if SFrames:IsGradientStyle() then
+ -- Strip backdrops
+ SFrames:ClearBackdrop(frame)
+ SFrames:ClearBackdrop(frame.healthBGFrame)
+ SFrames:ClearBackdrop(frame.powerBGFrame)
+ -- Health bar full width
+ if frame.health then
+ frame.health:ClearAllPoints()
+ frame.health:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
+ frame.health:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
+ frame.health:SetHeight(metrics.healthHeight)
+ end
+ -- Power bar full width
+ if frame.power and metrics.showPower then
+ frame.power:ClearAllPoints()
+ frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", metrics.powerOffsetX, -1 + metrics.powerOffsetY)
+ frame.power:SetWidth(metrics.powerWidth)
+ frame.power:SetHeight(metrics.powerHeight)
+ end
+ -- Apply gradient overlays
+ SFrames:ApplyGradientStyle(frame.health)
+ SFrames:ApplyGradientStyle(frame.power)
+ -- Flush BG frames
+ if frame.healthBGFrame then
+ frame.healthBGFrame:ClearAllPoints()
+ frame.healthBGFrame:SetPoint("TOPLEFT", frame.health, "TOPLEFT", 0, 0)
+ frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 0, 0)
+ end
+ if frame.powerBGFrame then
+ frame.powerBGFrame:ClearAllPoints()
+ frame.powerBGFrame:SetPoint("TOPLEFT", frame.power, "TOPLEFT", 0, 0)
+ frame.powerBGFrame:SetPoint("BOTTOMRIGHT", frame.power, "BOTTOMRIGHT", 0, 0)
+ end
+ -- Hide bar backgrounds (transparent)
+ if frame.healthBGFrame then frame.healthBGFrame:Hide() end
+ if frame.powerBGFrame then frame.powerBGFrame:Hide() end
+ if frame.health and frame.health.bg then frame.health.bg:Hide() end
+ if frame.power and frame.power.bg then frame.power.bg:Hide() end
+ else
+ SFrames:RemoveGradientStyle(frame.health)
+ SFrames:RemoveGradientStyle(frame.power)
+ -- Restore bar backgrounds
+ if frame.healthBGFrame then frame.healthBGFrame:Show() end
+ if frame.powerBGFrame then frame.powerBGFrame:Show() end
+ if frame.health and frame.health.bg then frame.health.bg:Show() end
+ if frame.power and frame.power.bg then frame.power.bg:Show() end
+ end
if frame.nameText then
- frame.nameText:SetFont(fontPath, metrics.nameFont, outline)
+ SFrames:ApplyFontString(frame.nameText, metrics.nameFont, "raidNameFontKey", "fontKey")
end
if frame.healthText then
- frame.healthText:SetFont(fontPath, metrics.valueFont, outline)
+ SFrames:ApplyFontString(frame.healthText, metrics.healthFont, "raidHealthFontKey", "fontKey")
+ end
+ if frame.powerText then
+ SFrames:ApplyFontString(frame.powerText, metrics.powerFont, "raidPowerFontKey", "fontKey")
end
end
@@ -724,6 +893,9 @@ function SFrames.Raid:UpdateFrame(unit)
f.nameText:SetTextColor(1, 1, 1)
end
end
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyBarGradient(f.health)
+ end
self:UpdateHealth(unit)
self:UpdatePower(unit)
@@ -794,11 +966,10 @@ function SFrames.Raid:UpdateHealth(unit)
txt = percent .. "%"
elseif db.raidHealthFormat == "deficit" then
if maxHp - hp > 0 then
- txt = "-" .. (maxHp - hp)
+ txt = "-" .. SFrames:FormatCompactNumber(maxHp - hp)
end
else
- txt = (math.floor(hp/100)/10).."k" -- default compact e.g. 4.5k
- if hp < 1000 then txt = tostring(hp) end
+ txt = SFrames:FormatCompactNumber(hp)
end
f.healthText:SetText(txt)
@@ -826,14 +997,8 @@ function SFrames.Raid:UpdateHealPrediction(unit)
local predOther = f.health.healPredOther
local predOver = f.health.healPredOver
- local function HidePredictions()
- predMine:Hide()
- predOther:Hide()
- predOver:Hide()
- end
-
if not UnitExists(unit) or not UnitIsConnected(unit) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -855,7 +1020,7 @@ function SFrames.Raid:UpdateHealPrediction(unit)
end
if maxHp <= 0 then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -872,7 +1037,7 @@ function SFrames.Raid:UpdateHealPrediction(unit)
end
local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -880,13 +1045,13 @@ function SFrames.Raid:UpdateHealPrediction(unit)
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineIncoming <= 0 and othersIncoming <= 0 then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
local barWidth = f:GetWidth() - 2
if barWidth <= 0 then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -896,7 +1061,7 @@ function SFrames.Raid:UpdateHealPrediction(unit)
local availableWidth = barWidth - currentPosition
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -972,6 +1137,10 @@ function SFrames.Raid:UpdatePower(unit)
local pType = UnitPowerType(unit)
local color = SFrames.Config.colors.power[pType] or SFrames.Config.colors.power[0]
f.power:SetStatusBarColor(color.r, color.g, color.b)
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyBarGradient(f.power)
+ end
+ SFrames:UpdateRainbowBar(f.power, power, maxPower, unit)
break
end
end
@@ -1072,7 +1241,11 @@ function SFrames.Raid:UpdateAuras(unit)
local f = frameData.frame
local buffsNeeded = self:GetClassBuffs()
- local foundIndicators = { [1] = false, [2] = false, [3] = false, [4] = false }
+ -- Reuse pre-allocated table
+ _foundIndicators[1] = false
+ _foundIndicators[2] = false
+ _foundIndicators[3] = false
+ _foundIndicators[4] = false
-- Hide all first
for i = 1, 4 do
@@ -1085,70 +1258,22 @@ function SFrames.Raid:UpdateAuras(unit)
return
end
- local function MatchesList(auraName, list)
- for _, name in ipairs(list) do
- if string.find(auraName, name) then
- return true
- end
- end
- return false
- end
-
- -- Helper: get buff name via SuperWoW aura ID (fast) or tooltip scan (fallback)
- local hasSuperWoW = SFrames.superwow_active and SpellInfo
- local function GetBuffName(unit, index)
- if hasSuperWoW then
- local texture, auraID = UnitBuff(unit, index)
- if auraID and SpellInfo then
- local spellName = SpellInfo(auraID)
- if spellName and spellName ~= "" then
- return spellName, texture
- end
- end
- end
- -- Fallback: tooltip scan
- SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
- SFrames.Tooltip:SetUnitBuff(unit, index)
- local buffName = SFramesScanTooltipTextLeft1:GetText()
- SFrames.Tooltip:Hide()
- return buffName, UnitBuff(unit, index)
- end
-
- local function GetDebuffName(unit, index)
- if hasSuperWoW then
- local texture, count, dtype, auraID = UnitDebuff(unit, index)
- if auraID and SpellInfo then
- local spellName = SpellInfo(auraID)
- if spellName and spellName ~= "" then
- return spellName, texture, count, dtype
- end
- end
- end
- -- Fallback: tooltip scan
- SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
- SFrames.Tooltip:SetUnitDebuff(unit, index)
- local debuffName = SFramesScanTooltipTextLeft1:GetText()
- SFrames.Tooltip:Hide()
- local texture, count, dtype = UnitDebuff(unit, index)
- return debuffName, texture, count, dtype
- end
-
- -- Check Buffs
+ -- Check Buffs (using module-level helpers, no closures)
for i = 1, 32 do
local texture, applications = UnitBuff(unit, i)
if not texture then break end
- local buffName = GetBuffName(unit, i)
+ local buffName = RaidGetBuffName(unit, i)
if buffName then
for pos, listData in pairs(buffsNeeded) do
- if pos <= 4 and not listData.isDebuff and not foundIndicators[pos] then
+ if pos <= 4 and not listData.isDebuff and not _foundIndicators[pos] then
if MatchesList(buffName, listData) then
f.indicators[pos].icon:SetTexture(texture)
f.indicators[pos].index = i
f.indicators[pos].isDebuff = false
f.indicators[pos]:Show()
- foundIndicators[pos] = true
+ _foundIndicators[pos] = true
end
end
end
@@ -1156,7 +1281,10 @@ function SFrames.Raid:UpdateAuras(unit)
end
local hasDebuff = false
- local debuffColor = {r=_A.slotBg[1], g=_A.slotBg[2], b=_A.slotBg[3]}
+ -- Reuse pre-allocated table
+ _debuffColor.r = _A.slotBg[1]
+ _debuffColor.g = _A.slotBg[2]
+ _debuffColor.b = _A.slotBg[3]
-- Check Debuffs
for i = 1, 16 do
@@ -1165,24 +1293,24 @@ function SFrames.Raid:UpdateAuras(unit)
if dispelType then
hasDebuff = true
- if dispelType == "Magic" then debuffColor = {r=0.2, g=0.6, b=1}
- elseif dispelType == "Curse" then debuffColor = {r=0.6, g=0, b=1}
- elseif dispelType == "Disease" then debuffColor = {r=0.6, g=0.4, b=0}
- elseif dispelType == "Poison" then debuffColor = {r=0, g=0.6, b=0}
+ if dispelType == "Magic" then _debuffColor.r = 0.2; _debuffColor.g = 0.6; _debuffColor.b = 1
+ elseif dispelType == "Curse" then _debuffColor.r = 0.6; _debuffColor.g = 0; _debuffColor.b = 1
+ elseif dispelType == "Disease" then _debuffColor.r = 0.6; _debuffColor.g = 0.4; _debuffColor.b = 0
+ elseif dispelType == "Poison" then _debuffColor.r = 0; _debuffColor.g = 0.6; _debuffColor.b = 0
end
end
- local debuffName = GetDebuffName(unit, i)
+ local debuffName = RaidGetDebuffName(unit, i)
if debuffName then
for pos, listData in pairs(buffsNeeded) do
- if pos <= 4 and listData.isDebuff and not foundIndicators[pos] then
+ if pos <= 4 and listData.isDebuff and not _foundIndicators[pos] then
if MatchesList(debuffName, listData) then
f.indicators[pos].icon:SetTexture(texture)
f.indicators[pos].index = i
f.indicators[pos].isDebuff = true
f.indicators[pos]:Show()
- foundIndicators[pos] = true
+ _foundIndicators[pos] = true
end
end
end
@@ -1190,9 +1318,8 @@ function SFrames.Raid:UpdateAuras(unit)
end
if hasDebuff then
- f.health.bg:SetVertexColor(debuffColor.r, debuffColor.g, debuffColor.b, 1)
+ f.health.bg:SetVertexColor(_debuffColor.r, _debuffColor.g, _debuffColor.b, 1)
else
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
end
end
-
diff --git a/Units/Target.lua b/Units/Target.lua
index a51386e..6c940f7 100644
--- a/Units/Target.lua
+++ b/Units/Target.lua
@@ -182,6 +182,17 @@ local function Clamp(value, minValue, maxValue)
return value
end
+local function SetTextureIfPresent(region, texturePath)
+ if region and region.SetTexture and texturePath then
+ region:SetTexture(texturePath)
+ end
+end
+
+local function ApplyFontIfPresent(fs, size, fontKey, fallbackFontKey)
+ if not fs then return end
+ SFrames:ApplyFontString(fs, size, fontKey, fallbackFontKey)
+end
+
local DIST_BASE_WIDTH = 80
local DIST_BASE_HEIGHT = 24
local DIST_BASE_FONTSIZE = 14
@@ -232,6 +243,33 @@ function SFrames.Target:GetConfig()
local powerHeight = tonumber(db.targetPowerHeight) or 9
powerHeight = Clamp(math.floor(powerHeight + 0.5), 6, 40)
+ local showPortrait = db.targetShowPortrait ~= false
+ local gradientStyle = SFrames:IsGradientStyle()
+ local classicDefaultPowerWidth = width - (showPortrait and portraitWidth or 0) - 2
+ if classicDefaultPowerWidth < 60 then
+ classicDefaultPowerWidth = 60
+ end
+
+ local rawPowerWidth = tonumber(db.targetPowerWidth)
+ local legacyFullWidth = tonumber(db.targetFrameWidth) or width
+ local defaultPowerWidth = gradientStyle and width or classicDefaultPowerWidth
+ local maxPowerWidth = gradientStyle and width or (width - 2)
+ local powerWidth
+ if gradientStyle then
+ -- 渐变风格:能量条始终与血条等宽(全宽)
+ powerWidth = width
+ elseif not rawPowerWidth
+ or math.abs(rawPowerWidth - legacyFullWidth) < 0.5
+ or math.abs(rawPowerWidth - classicDefaultPowerWidth) < 0.5 then
+ powerWidth = defaultPowerWidth
+ else
+ powerWidth = rawPowerWidth
+ end
+ powerWidth = Clamp(math.floor(powerWidth + 0.5), 60, maxPowerWidth)
+
+ local powerOffsetX = Clamp(math.floor((tonumber(db.targetPowerOffsetX) or 0) + 0.5), -120, 120)
+ local powerOffsetY = Clamp(math.floor((tonumber(db.targetPowerOffsetY) or 0) + 0.5), -80, 80)
+
local height = healthHeight + powerHeight + 4
height = Clamp(height, 30, 140)
@@ -241,6 +279,12 @@ function SFrames.Target:GetConfig()
local valueFont = tonumber(db.targetValueFontSize) or 10
valueFont = Clamp(math.floor(valueFont + 0.5), 8, 18)
+ local healthFont = tonumber(db.targetHealthFontSize) or valueFont
+ healthFont = Clamp(math.floor(healthFont + 0.5), 8, 18)
+
+ local powerFont = tonumber(db.targetPowerFontSize) or valueFont
+ powerFont = Clamp(math.floor(powerFont + 0.5), 8, 18)
+
local frameScale = tonumber(db.targetFrameScale) or 1
frameScale = Clamp(frameScale, 0.7, 1.8)
@@ -250,8 +294,16 @@ function SFrames.Target:GetConfig()
portraitWidth = portraitWidth,
healthHeight = healthHeight,
powerHeight = powerHeight,
+ powerWidth = powerWidth,
+ powerOffsetX = powerOffsetX,
+ powerOffsetY = powerOffsetY,
+ powerOnTop = db.targetPowerOnTop == true,
nameFont = nameFont,
valueFont = valueFont,
+ healthFont = healthFont,
+ powerFont = powerFont,
+ healthTexture = SFrames:ResolveBarTexture("targetHealthTexture", "barTexture"),
+ powerTexture = SFrames:ResolveBarTexture("targetPowerTexture", "barTexture"),
scale = frameScale,
}
end
@@ -334,8 +386,8 @@ function SFrames.Target:ApplyConfig()
if f.power then
f.power:ClearAllPoints()
- f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
- f.power:SetPoint("TOPRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
+ f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", cfg.powerOffsetX, -1 + cfg.powerOffsetY)
+ f.power:SetWidth(cfg.powerWidth)
f.power:SetHeight(cfg.powerHeight)
end
@@ -345,19 +397,71 @@ function SFrames.Target:ApplyConfig()
f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 1, -1)
end
- local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
- local fontPath = SFrames:GetFont()
+ SFrames:ApplyStatusBarTexture(f.health, "targetHealthTexture", "barTexture")
+ SFrames:ApplyStatusBarTexture(f.power, "targetPowerTexture", "barTexture")
+ SetTextureIfPresent(f.health and f.health.bg, cfg.healthTexture)
+ SetTextureIfPresent(f.health and f.health.healPredMine, cfg.healthTexture)
+ SetTextureIfPresent(f.health and f.health.healPredOther, cfg.healthTexture)
+ SetTextureIfPresent(f.health and f.health.healPredOver, cfg.healthTexture)
+ SetTextureIfPresent(f.power and f.power.bg, cfg.powerTexture)
- if f.nameText then
- f.nameText:SetFont(fontPath, cfg.nameFont, outline)
- end
- if f.healthText then
- f.healthText:SetFont(fontPath, cfg.valueFont, outline)
- end
- if f.powerText then
- f.powerText:SetFont(fontPath, cfg.valueFont, outline)
+ -- Gradient style preset
+ if SFrames:IsGradientStyle() then
+ -- Hide portrait & its backdrop
+ if f.portrait then f.portrait:Hide() end
+ if f.portraitBG then f.portraitBG:Hide() end
+ -- Strip backdrops
+ SFrames:ClearBackdrop(f)
+ SFrames:ClearBackdrop(f.healthBGFrame)
+ SFrames:ClearBackdrop(f.powerBGFrame)
+ -- Health bar full width
+ if f.health then
+ f.health:ClearAllPoints()
+ f.health:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
+ f.health:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, 0)
+ f.health:SetHeight(cfg.healthHeight)
+ end
+ -- Power bar full width, below health
+ if f.power then
+ f.power:ClearAllPoints()
+ f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", cfg.powerOffsetX, -2 + cfg.powerOffsetY)
+ f.power:SetWidth(cfg.powerWidth)
+ f.power:SetHeight(cfg.powerHeight)
+ end
+ -- Apply gradient overlays
+ SFrames:ApplyGradientStyle(f.health)
+ SFrames:ApplyGradientStyle(f.power)
+ -- Flush BG frames (no border padding)
+ if f.healthBGFrame then
+ f.healthBGFrame:ClearAllPoints()
+ f.healthBGFrame:SetPoint("TOPLEFT", f.health, "TOPLEFT", 0, 0)
+ f.healthBGFrame:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 0, 0)
+ end
+ if f.powerBGFrame then
+ f.powerBGFrame:ClearAllPoints()
+ f.powerBGFrame:SetPoint("TOPLEFT", f.power, "TOPLEFT", 0, 0)
+ f.powerBGFrame:SetPoint("BOTTOMRIGHT", f.power, "BOTTOMRIGHT", 0, 0)
+ end
+ -- Hide bar backgrounds (transparent)
+ if f.healthBGFrame then f.healthBGFrame:Hide() end
+ if f.powerBGFrame then f.powerBGFrame:Hide() end
+ if f.health and f.health.bg then f.health.bg:Hide() end
+ if f.power and f.power.bg then f.power.bg:Hide() end
+ else
+ -- Classic style: remove gradient overlays if they exist
+ SFrames:RemoveGradientStyle(f.health)
+ SFrames:RemoveGradientStyle(f.power)
+ -- Restore bar backgrounds
+ if f.healthBGFrame then f.healthBGFrame:Show() end
+ if f.powerBGFrame then f.powerBGFrame:Show() end
+ if f.health and f.health.bg then f.health.bg:Show() end
+ if f.power and f.power.bg then f.power.bg:Show() end
end
+ ApplyFontIfPresent(f.nameText, cfg.nameFont, "targetNameFontKey")
+ ApplyFontIfPresent(f.healthText, cfg.healthFont, "targetHealthFontKey")
+ ApplyFontIfPresent(f.powerText, cfg.powerFont, "targetPowerFontKey")
+
if f.castbar then
f.castbar:ClearAllPoints()
if showPortrait then
@@ -374,6 +478,24 @@ function SFrames.Target:ApplyConfig()
self:ApplyDistanceScale(dScale)
end
+ if f.distText then
+ local dfs = (db.targetDistanceFontSize and tonumber(db.targetDistanceFontSize)) or 10
+ dfs = Clamp(dfs, 8, 24)
+ SFrames:ApplyFontString(f.distText, dfs, "targetDistanceFontKey", "fontKey")
+ end
+
+ if f.health and f.power then
+ local healthLevel = f:GetFrameLevel() + 2
+ local powerLevel = cfg.powerOnTop and (healthLevel + 1) or (healthLevel - 1)
+ f.health:SetFrameLevel(healthLevel)
+ f.power:SetFrameLevel(powerLevel)
+ end
+
+ SFrames:ApplyConfiguredUnitBackdrop(f, "target")
+ if f.healthBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.healthBGFrame, "target") end
+ if f.powerBGFrame then SFrames:ApplyConfiguredUnitBackdrop(f.powerBGFrame, "target") end
+ if f.portraitBG then SFrames:ApplyConfiguredUnitBackdrop(f.portraitBG, "target", true) end
+
if UnitExists("target") then
self:UpdateAll()
end
@@ -386,8 +508,6 @@ function SFrames.Target:ApplyDistanceScale(scale)
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 customSize = SFramesDB and tonumber(SFramesDB.targetDistanceFontSize)
local fontSize
if customSize and customSize >= 8 and customSize <= 24 then
@@ -395,7 +515,7 @@ function SFrames.Target:ApplyDistanceScale(scale)
else
fontSize = math.max(8, math.floor(DIST_BASE_FONTSIZE * scale + 0.5))
end
- f.text:SetFont(fontPath, fontSize, outline)
+ SFrames:ApplyFontString(f.text, fontSize, "targetDistanceFontKey", "fontKey")
end
end
@@ -437,54 +557,46 @@ function SFrames.Target:InitializeDistanceFrame()
f.text:SetShadowColor(0, 0, 0, 1)
f.text:SetShadowOffset(1, -1)
- -- Behind indicator text (shown next to distance)
- f.behindText = SFrames:CreateFontString(f, fontSize, "LEFT")
- f.behindText:SetPoint("LEFT", f.text, "RIGHT", 4, 0)
- f.behindText:SetShadowColor(0, 0, 0, 1)
- f.behindText:SetShadowOffset(1, -1)
- f.behindText:Hide()
-
SFrames.Target.distanceFrame = f
f:Hide()
- f.timer = 0
- f:SetScript("OnUpdate", function()
- if SFramesDB and SFramesDB.targetDistanceEnabled == false then
- if this:IsShown() then this:Hide() end
+ local ticker = CreateFrame("Frame", nil, UIParent)
+ ticker:SetWidth(1)
+ ticker:SetHeight(1)
+ ticker.timer = 0
+ ticker:Show()
+ ticker:SetScript("OnUpdate", function()
+ local distFrame = SFrames.Target.distanceFrame
+ if not distFrame then return end
+ local disabled = SFramesDB and SFramesDB.targetDistanceEnabled == false
+ local onFrame = not SFramesDB or SFramesDB.targetDistanceOnFrame ~= false
+ local tgtFrame = SFrames.Target and SFrames.Target.frame
+ local embeddedText = tgtFrame and tgtFrame.distText
+
+ if disabled then
+ if distFrame:IsShown() then distFrame:Hide() end
+ if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end
return
end
if not UnitExists("target") then
- if this:IsShown() then this:Hide() end
- if this.behindText then this.behindText:Hide() end
+ if distFrame:IsShown() then distFrame:Hide() end
+ if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end
return
end
this.timer = this.timer + (arg1 or 0)
if this.timer >= 0.4 then
this.timer = 0
local dist = SFrames.Target:GetDistance("target")
- this.text:SetText(dist or "---")
- if not this:IsShown() then this:Show() end
+ local distStr = dist or "---"
- -- Behind indicator
- if this.behindText then
- local showBehind = not SFramesDB or SFramesDB.Tweaks == nil
- or SFramesDB.Tweaks.behindIndicator ~= false
- if showBehind and IsUnitXPAvailable() then
- local ok, isBehind = pcall(UnitXP, "behind", "player", "target")
- if ok and isBehind then
- this.behindText:SetText("背后")
- this.behindText:SetTextColor(0.2, 1.0, 0.3)
- this.behindText:Show()
- elseif ok then
- this.behindText:SetText("正面")
- this.behindText:SetTextColor(1.0, 0.35, 0.3)
- this.behindText:Show()
- else
- this.behindText:Hide()
- end
- else
- this.behindText:Hide()
- end
+ if onFrame and embeddedText then
+ embeddedText:SetText(distStr)
+ if not embeddedText:IsShown() then embeddedText:Show() end
+ if distFrame:IsShown() then distFrame:Hide() end
+ else
+ distFrame.text:SetText(distStr)
+ if not distFrame:IsShown() then distFrame:Show() end
+ if embeddedText and embeddedText:IsShown() then embeddedText:Hide() end
end
end
end)
@@ -515,15 +627,23 @@ function SFrames.Target:Initialize()
local f = CreateFrame("Button", "SFramesTargetFrame", UIParent)
f:SetWidth(SFrames.Config.width)
f:SetHeight(SFrames.Config.height)
- if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["TargetFrame"] then
- local pos = SFramesDB.Positions["TargetFrame"]
- f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs, pos.yOfs)
- else
- f:SetPoint("CENTER", UIParent, "CENTER", 200, -100) -- Mirrored from player
- end
+
local frameScale = (SFramesDB and type(SFramesDB.targetFrameScale) == "number") and SFramesDB.targetFrameScale or 1
f:SetScale(frameScale)
-
+
+ if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["TargetFrame"] then
+ local pos = SFramesDB.Positions["TargetFrame"]
+ local fScale = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
+ if fScale > 0.01 and math.abs(fScale - 1) > 0.001 then
+ f:SetPoint(pos.point, UIParent, pos.relativePoint,
+ (pos.xOfs or 0) / fScale, (pos.yOfs or 0) / fScale)
+ else
+ f:SetPoint(pos.point, UIParent, pos.relativePoint, pos.xOfs or 0, pos.yOfs or 0)
+ end
+ else
+ f:SetPoint("CENTER", UIParent, "CENTER", 200, -100)
+ end
+
f:SetMovable(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
@@ -533,25 +653,21 @@ function SFrames.Target:Initialize()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, relativeTo, relativePoint, xOfs, yOfs = f:GetPoint()
+ local fSc = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
+ if fSc > 0.01 and math.abs(fSc - 1) > 0.001 then
+ xOfs = (xOfs or 0) * fSc
+ yOfs = (yOfs or 0) * fSc
+ end
SFramesDB.Positions["TargetFrame"] = { point = point, relativePoint = relativePoint, xOfs = xOfs, yOfs = yOfs }
end)
f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
f:SetScript("OnClick", function()
- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] OnClick fired: " .. tostring(arg1) .. "|r")
if arg1 == "LeftButton" then
-- Shift+左键 = 设为焦点
if IsShiftKeyDown() then
- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Shift+LeftButton -> SetFocus|r")
if SFrames.Focus and SFrames.Focus.SetFromTarget then
- local ok, err = pcall(SFrames.Focus.SetFromTarget, SFrames.Focus)
- if ok then
- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Focus set OK|r")
- else
- DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] Focus error: " .. tostring(err) .. "|r")
- end
- else
- DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] SFrames.Focus missing!|r")
+ pcall(SFrames.Focus.SetFromTarget, SFrames.Focus)
end
return
end
@@ -562,31 +678,25 @@ function SFrames.Target:Initialize()
SpellTargetUnit(this.unit)
end
elseif arg1 == "RightButton" then
- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] RightButton hit|r")
if SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting()
return
end
if not UnitExists("target") then
- DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] No target, abort|r")
return
end
if not SFrames.Target.dropDown then
- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Creating dropdown...|r")
local ok1, err1 = pcall(function()
SFrames.Target.dropDown = CreateFrame("Frame", "SFramesTargetDropDown", UIParent, "UIDropDownMenuTemplate")
end)
if not ok1 then
- DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] CreateFrame failed: " .. tostring(err1) .. "|r")
return
end
SFrames.Target.dropDown.displayMode = "MENU"
SFrames.Target.dropDown.initialize = function()
- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] initialize() called|r")
local dd = SFrames.Target.dropDown
local name = dd.targetName
if not name then
- DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] initialize: no targetName|r")
return
end
@@ -678,16 +788,9 @@ function SFrames.Target:Initialize()
-- 取消按钮不添加,点击菜单外部即可关闭(节省按钮位)
end
- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Dropdown created OK|r")
end
SFrames.Target.dropDown.targetName = UnitName("target")
- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] Calling ToggleDropDownMenu...|r")
- local ok3, err3 = pcall(ToggleDropDownMenu, 1, nil, SFrames.Target.dropDown, "cursor")
- if not ok3 then
- DEFAULT_CHAT_FRAME:AddMessage("|cffff4444[Nanami] ToggleDropDownMenu failed: " .. tostring(err3) .. "|r")
- else
- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] ToggleDropDownMenu OK|r")
- end
+ pcall(ToggleDropDownMenu, 1, nil, SFrames.Target.dropDown, "cursor")
end
end)
f:SetScript("OnReceiveDrag", function()
@@ -799,6 +902,19 @@ function SFrames.Target:Initialize()
f.comboText:SetTextColor(1, 0.8, 0)
f.comboText:SetText("")
+ -- Embedded distance text (high-level overlay so it's never covered)
+ local distOverlay = CreateFrame("Frame", nil, f)
+ distOverlay:SetFrameLevel((f:GetFrameLevel() or 0) + 20)
+ distOverlay:SetAllPoints(f.health)
+ local distFS = (SFramesDB and tonumber(SFramesDB.targetDistanceFontSize)) or 10
+ f.distText = SFrames:CreateFontString(distOverlay, distFS, "CENTER")
+ f.distText:SetPoint("CENTER", f.health, "TOP", 0, 0)
+ f.distText:SetTextColor(1, 0.82, 0.25)
+ f.distText:SetShadowColor(0, 0, 0, 1)
+ f.distText:SetShadowOffset(1, -1)
+ f.distText:SetText("")
+ f.distText:Hide()
+
-- Raid Target Icon (top center of health bar, half outside frame)
local raidIconSize = 22
local raidIconOvr = CreateFrame("Frame", nil, f)
@@ -864,7 +980,8 @@ function SFrames.Target:Initialize()
-- Register movers
if SFrames.Movers and SFrames.Movers.RegisterMover then
SFrames.Movers:RegisterMover("TargetFrame", f, "目标",
- "CENTER", "UIParent", "CENTER", 200, -100)
+ "CENTER", "UIParent", "CENTER", 200, -100,
+ nil, { alwaysShowInLayout = true })
if SFrames.Target.distanceFrame then
SFrames.Movers:RegisterMover("TargetDistanceFrame", SFrames.Target.distanceFrame, "目标距离",
"CENTER", "UIParent", "CENTER", 0, 100)
@@ -1047,18 +1164,24 @@ function SFrames.Target:OnTargetChanged()
if UnitExists("target") then
self.frame:Show()
self:UpdateAll()
+ local enabled = not (SFramesDB and SFramesDB.targetDistanceEnabled == false)
+ local onFrame = not SFramesDB or SFramesDB.targetDistanceOnFrame ~= false
if SFrames.Target.distanceFrame then
local dist = self:GetDistance("target")
- SFrames.Target.distanceFrame.text:SetText(dist or "---")
- if not (SFramesDB and SFramesDB.targetDistanceEnabled == false) then
- SFrames.Target.distanceFrame:Show()
- else
+ if onFrame and self.frame.distText then
+ self.frame.distText:SetText(dist or "---")
+ if enabled then self.frame.distText:Show() else self.frame.distText:Hide() end
SFrames.Target.distanceFrame:Hide()
+ else
+ SFrames.Target.distanceFrame.text:SetText(dist or "---")
+ if enabled then SFrames.Target.distanceFrame:Show() else SFrames.Target.distanceFrame:Hide() end
+ if self.frame.distText then self.frame.distText:Hide() end
end
end
else
self.frame:Hide()
if SFrames.Target.distanceFrame then SFrames.Target.distanceFrame:Hide() end
+ if self.frame and self.frame.distText then self.frame.distText:Hide() end
end
end
@@ -1153,6 +1276,8 @@ function SFrames.Target:UpdateAll()
end
local useClassColor = not (SFramesDB and SFramesDB.classColorHealth == false)
+ -- Gradient style always uses class colors
+ if SFrames:IsGradientStyle() then useClassColor = true end
if UnitIsPlayer("target") and useClassColor then
local _, class = UnitClass("target")
@@ -1184,6 +1309,10 @@ function SFrames.Target:UpdateAll()
self.frame.nameText:SetText(formattedLevel .. name)
self.frame.nameText:SetTextColor(r, g, b)
end
+ -- Re-apply gradient after color change
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyBarGradient(self.frame.health)
+ end
end
function SFrames.Target:UpdateHealth()
@@ -1208,9 +1337,9 @@ function SFrames.Target:UpdateHealth()
end
if displayMax > 0 then
- self.frame.healthText:SetText(displayHp .. " / " .. displayMax)
+ self.frame.healthText:SetText(SFrames:FormatCompactPair(displayHp, displayMax))
else
- self.frame.healthText:SetText(displayHp)
+ self.frame.healthText:SetText(SFrames:FormatCompactNumber(displayHp))
end
self:UpdateHealPrediction()
@@ -1222,14 +1351,8 @@ function SFrames.Target:UpdateHealPrediction()
local predOther = self.frame.health.healPredOther
local predOver = self.frame.health.healPredOver
- local function HidePredictions()
- predMine:Hide()
- predOther:Hide()
- predOver:Hide()
- end
-
if not UnitExists("target") then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1251,7 +1374,7 @@ function SFrames.Target:UpdateHealPrediction()
end
if maxHp <= 0 then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1268,7 +1391,7 @@ function SFrames.Target:UpdateHealPrediction()
end
local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1276,14 +1399,14 @@ function SFrames.Target:UpdateHealPrediction()
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
local showPortrait = SFramesDB and SFramesDB.targetShowPortrait ~= false
local barWidth = self.frame:GetWidth() - (showPortrait and (self.frame.portrait:GetWidth() + 2) or 2)
if barWidth <= 0 then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1293,7 +1416,7 @@ function SFrames.Target:UpdateHealPrediction()
local availableWidth = barWidth - currentWidth
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
- HidePredictions()
+ predMine:Hide(); predOther:Hide(); predOver:Hide()
return
end
@@ -1359,6 +1482,9 @@ function SFrames.Target:UpdatePowerType()
else
self.frame.power:SetStatusBarColor(0, 0, 1)
end
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyBarGradient(self.frame.power)
+ end
end
function SFrames.Target:UpdatePower()
@@ -1367,10 +1493,11 @@ function SFrames.Target:UpdatePower()
self.frame.power:SetMinMaxValues(0, maxPower)
self.frame.power:SetValue(power)
if maxPower > 0 then
- self.frame.powerText:SetText(power .. " / " .. maxPower)
+ self.frame.powerText:SetText(SFrames:FormatCompactPair(power, maxPower))
else
self.frame.powerText:SetText("")
end
+ SFrames:UpdateRainbowBar(self.frame.power, power, maxPower, "target")
end
function SFrames.Target:UpdateComboPoints()
@@ -1501,23 +1628,15 @@ end
function SFrames.Target:TickAuras()
if not UnitExists("target") then return end
- local timeNow = GetTime()
+ local tracker = SFrames.AuraTracker
local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime
- local hasNP = NanamiPlates_SpellDB and NanamiPlates_SpellDB.FindEffectData
-
- local targetName, targetLevel, targetGUID
- if hasNP then
- targetName = UnitName("target")
- targetLevel = UnitLevel("target") or 0
- targetGUID = UnitGUID and UnitGUID("target")
- end
-- Buffs
for i = 1, 32 do
local b = self.frame.buffs[i]
- if b:IsShown() and b.expirationTime then
- local timeLeft = b.expirationTime - timeNow
- if timeLeft > 0 and timeLeft < 3600 then
+ if b:IsShown() then
+ local timeLeft = tracker and tracker:GetAuraTimeLeft("target", "buff", i)
+ if timeLeft and timeLeft > 0 and timeLeft < 3600 then
if npFormat then
local text, r, g, bc, a = npFormat(timeLeft)
b.cdText:SetText(text)
@@ -1531,30 +1650,11 @@ function SFrames.Target:TickAuras()
end
end
- -- Debuffs: re-query SpellDB for live-accurate timers
+ -- Debuffs
for i = 1, 32 do
local b = self.frame.debuffs[i]
if b:IsShown() then
- local timeLeft = nil
-
- if hasNP and b.effectName then
- local data = targetGUID and NanamiPlates_SpellDB:FindEffectData(targetGUID, targetLevel, b.effectName)
- if not data and targetName then
- data = NanamiPlates_SpellDB:FindEffectData(targetName, targetLevel, b.effectName)
- end
- if data and data.start and data.duration then
- local remaining = data.duration + data.start - timeNow
- if remaining > 0 then
- timeLeft = remaining
- b.expirationTime = timeNow + remaining
- end
- end
- end
-
- if not timeLeft and b.expirationTime then
- timeLeft = b.expirationTime - timeNow
- end
-
+ local timeLeft = tracker and tracker:GetAuraTimeLeft("target", "debuff", i)
if timeLeft and timeLeft > 0 and timeLeft < 3600 then
if npFormat then
local text, r, g, bc, a = npFormat(timeLeft)
@@ -1573,6 +1673,11 @@ end
function SFrames.Target:UpdateAuras()
if not UnitExists("target") then return end
+ local tracker = SFrames.AuraTracker
+ if tracker and tracker.HandleAuraSnapshot then
+ tracker:HandleAuraSnapshot("target")
+ end
+
local hasSuperWoW = SFrames.superwow_active and SpellInfo
local numBuffs = 0
-- Buffs
@@ -1584,12 +1689,9 @@ function SFrames.Target:UpdateAuras()
b.icon:SetTexture(texture)
-- Store aura ID when SuperWoW is available
b.auraID = hasSuperWoW and swAuraID or nil
-
- SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
- SFrames.Tooltip:ClearLines()
- SFrames.Tooltip:SetUnitBuff("target", i)
- local timeLeft = SFrames:GetAuraTimeLeft("target", i, true)
- SFrames.Tooltip:Hide()
+
+ local state = tracker and tracker:GetAuraState("target", "buff", i)
+ local timeLeft = state and tracker:GetAuraTimeLeft("target", "buff", i)
if timeLeft and timeLeft > 0 then
b.expirationTime = GetTime() + timeLeft
b.cdText:SetText(SFrames:FormatTime(timeLeft))
@@ -1621,7 +1723,6 @@ function SFrames.Target:UpdateAuras()
end
-- Debuffs
- local hasNP = NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff
local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime
for i = 1, 32 do
@@ -1633,39 +1734,12 @@ function SFrames.Target:UpdateAuras()
-- Store aura ID when SuperWoW is available
b.auraID = hasSuperWoW and swDebuffAuraID or nil
- local timeLeft = 0
- local effectName = nil
-
- if hasNP then
- local effect, rank, _, stacks, dtype, duration, npTimeLeft, isOwn = NanamiPlates_SpellDB:UnitDebuff("target", i)
- effectName = effect
- if npTimeLeft and npTimeLeft > 0 then
- timeLeft = npTimeLeft
- elseif effect and effect ~= "" and duration and duration > 0
- and NanamiPlates_Auras and NanamiPlates_Auras.timers then
- local unitKey = (UnitGUID and UnitGUID("target")) or UnitName("target") or ""
- local cached = NanamiPlates_Auras.timers[unitKey .. "_" .. effect]
- if not cached and UnitName("target") then
- cached = NanamiPlates_Auras.timers[UnitName("target") .. "_" .. effect]
- end
- if cached and cached.startTime and cached.duration then
- local remaining = cached.duration - (GetTime() - cached.startTime)
- if remaining > 0 then timeLeft = remaining end
- end
- end
- end
-
- if timeLeft <= 0 then
- SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
- SFrames.Tooltip:ClearLines()
- SFrames.Tooltip:SetUnitDebuff("target", i)
- timeLeft = SFrames:GetAuraTimeLeft("target", i, false)
- SFrames.Tooltip:Hide()
- end
+ local state = tracker and tracker:GetAuraState("target", "debuff", i)
+ local timeLeft = state and tracker:GetAuraTimeLeft("target", "debuff", i)
if timeLeft and timeLeft > 0 then
b.expirationTime = GetTime() + timeLeft
- b.effectName = effectName
+ b.effectName = state and state.name or nil
if npFormat then
local text, r, g, bc, a = npFormat(timeLeft)
b.cdText:SetText(text)
diff --git a/Units/ToT.lua b/Units/ToT.lua
index 0519d0d..92f7027 100644
--- a/Units/ToT.lua
+++ b/Units/ToT.lua
@@ -1,12 +1,36 @@
SFrames.ToT = {}
local _A = SFrames.ActiveTheme
+function SFrames.ToT:ApplyConfig()
+ local f = self.frame
+ if not f then return end
+ SFrames:ApplyStatusBarTexture(f.health, "totHealthTexture", "barTexture")
+ local tex = SFrames:ResolveBarTexture("totHealthTexture", "barTexture")
+ if f.health and f.health.bg then f.health.bg:SetTexture(tex) end
+ if SFrames:IsGradientStyle() then
+ SFrames:ApplyGradientStyle(f.health)
+ if f.hbg then f.hbg:Hide() end
+ if f.health and f.health.bg then f.health.bg:Hide() end
+ else
+ SFrames:RemoveGradientStyle(f.health)
+ if f.hbg then f.hbg:Show() end
+ if f.health and f.health.bg then f.health.bg:Show() end
+ end
+end
+
function SFrames.ToT:Initialize()
local f = CreateFrame("Button", "SFramesToTFrame", UIParent)
f:SetWidth(120)
f:SetHeight(25)
- f:SetPoint("BOTTOMLEFT", SFramesTargetFrame, "BOTTOMRIGHT", 5, 0)
-
+
+ if SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ToTFrame"] then
+ local pos = SFramesDB.Positions["ToTFrame"]
+ f:SetPoint(pos.point or "BOTTOMLEFT", UIParent, pos.relativePoint or "BOTTOMLEFT",
+ pos.xOfs or 0, pos.yOfs or 0)
+ else
+ f:SetPoint("BOTTOMLEFT", SFramesTargetFrame, "BOTTOMRIGHT", 5, 0)
+ end
+
f:RegisterForClicks("LeftButtonUp", "RightButtonUp")
f:SetScript("OnClick", function()
if arg1 == "LeftButton" then
@@ -24,6 +48,7 @@ function SFrames.ToT:Initialize()
hbg:SetPoint("BOTTOMRIGHT", f.health, "BOTTOMRIGHT", 1, -1)
hbg:SetFrameLevel(f:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(hbg)
+ f.hbg = hbg
f.health.bg = f.health:CreateTexture(nil, "BACKGROUND")
f.health.bg:SetAllPoints()
@@ -35,7 +60,15 @@ function SFrames.ToT:Initialize()
self.frame = f
f:Hide()
-
+
+ self:ApplyConfig()
+
+ if SFrames.Movers and SFrames.Movers.RegisterMover then
+ SFrames.Movers:RegisterMover("ToTFrame", f, "目标的目标",
+ "BOTTOMLEFT", "SFramesTargetFrame", "BOTTOMRIGHT", 5, 0,
+ nil, { alwaysShowInLayout = true })
+ end
+
-- Update loop since targettarget changes don't fire precise events in Vanilla
self.updater = CreateFrame("Frame")
self.updater.timer = 0
diff --git a/WorldMap.lua b/WorldMap.lua
index 24238e0..1cd4471 100644
--- a/WorldMap.lua
+++ b/WorldMap.lua
@@ -52,6 +52,26 @@ local function HookScript(frame, script, fn)
end)
end
+local function SafeSetMapToCurrentZone()
+ if not SetMapToCurrentZone then
+ return
+ end
+ if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
+ return SFrames:CallWithPreservedBattlefieldMinimap(SetMapToCurrentZone)
+ end
+ return pcall(SetMapToCurrentZone)
+end
+
+local function SafeSetMapZoom(continent, zone)
+ if not SetMapZoom then
+ return
+ end
+ if SFrames and SFrames.CallWithPreservedBattlefieldMinimap then
+ return SFrames:CallWithPreservedBattlefieldMinimap(SetMapZoom, continent, zone)
+ end
+ return pcall(SetMapZoom, continent, zone)
+end
+
--------------------------------------------------------------------------------
-- 1. Hide Blizzard Decorations
-- Called at init AND on every WorldMapFrame:OnShow to counter Blizzard resets
@@ -951,12 +971,18 @@ local function CreateWaypointPin()
pinLabel:SetText("")
local btnW, btnH = 52, 18
+ local PIN_BTN_BACKDROP = {
+ bgFile = "Interface\\Buttons\\WHITE8X8",
+ edgeFile = "Interface\\Buttons\\WHITE8X8",
+ edgeSize = 1,
+ insets = { left = 1, right = 1, top = 1, bottom = 1 },
+ }
pinShareBtn = CreateFrame("Button", nil, pinFrame)
pinShareBtn:SetWidth(btnW)
pinShareBtn:SetHeight(btnH)
- pinShareBtn:SetPoint("TOPLEFT", pinFrame, "BOTTOMLEFT", -10, -8)
- pinShareBtn:SetBackdrop(PANEL_BACKDROP)
+ pinShareBtn:SetPoint("TOP", pinLabel, "BOTTOM", -(btnW / 2 + 2), -4)
+ pinShareBtn:SetBackdrop(PIN_BTN_BACKDROP)
pinShareBtn:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
pinShareBtn:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
local shareFS = pinShareBtn:CreateFontString(nil, "OVERLAY")
@@ -966,9 +992,11 @@ local function CreateWaypointPin()
shareFS:SetTextColor(_A.btnText[1], _A.btnText[2], _A.btnText[3])
pinShareBtn:SetScript("OnEnter", function()
this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4])
+ this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4])
end)
pinShareBtn:SetScript("OnLeave", function()
this:SetBackdropColor(_A.btnBg[1], _A.btnBg[2], _A.btnBg[3], _A.btnBg[4])
+ this:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
end)
pinShareBtn:SetScript("OnClick", function()
WM:ShareWaypoint()
@@ -978,7 +1006,7 @@ local function CreateWaypointPin()
pinClearBtn:SetWidth(btnW)
pinClearBtn:SetHeight(btnH)
pinClearBtn:SetPoint("LEFT", pinShareBtn, "RIGHT", 4, 0)
- pinClearBtn:SetBackdrop(PANEL_BACKDROP)
+ pinClearBtn:SetBackdrop(PIN_BTN_BACKDROP)
pinClearBtn:SetBackdropColor(_A.buttonDownBg[1], _A.buttonDownBg[2], _A.buttonDownBg[3], _A.buttonDownBg[4])
pinClearBtn:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
local clearFS = pinClearBtn:CreateFontString(nil, "OVERLAY")
@@ -988,9 +1016,11 @@ local function CreateWaypointPin()
clearFS:SetTextColor(_A.dimText[1], _A.dimText[2], _A.dimText[3])
pinClearBtn:SetScript("OnEnter", function()
this:SetBackdropColor(_A.btnHoverBg[1], _A.btnHoverBg[2], _A.btnHoverBg[3], _A.btnHoverBg[4])
+ this:SetBackdropBorderColor(_A.btnHoverBd[1], _A.btnHoverBd[2], _A.btnHoverBd[3], _A.btnHoverBd[4])
end)
pinClearBtn:SetScript("OnLeave", function()
this:SetBackdropColor(_A.buttonDownBg[1], _A.buttonDownBg[2], _A.buttonDownBg[3], _A.buttonDownBg[4])
+ this:SetBackdropBorderColor(_A.btnBorder[1], _A.btnBorder[2], _A.btnBorder[3], _A.btnBorder[4])
end)
pinClearBtn:SetScript("OnClick", function()
WM:ClearWaypoint()
@@ -1099,7 +1129,7 @@ function WM:HandleWaypointLink(data)
if waited >= 0.05 then
timer:SetScript("OnUpdate", nil)
if SetMapZoom then
- SetMapZoom(pending.continent, pending.zone)
+ SafeSetMapZoom(pending.continent, pending.zone)
end
WM:SetWaypoint(pending.continent, pending.zone, pending.x, pending.y, pending.name)
end
@@ -1138,7 +1168,7 @@ local function DiscoverDmfZoneIndices()
local target = string.lower(loc.zone)
local zones = { GetMapZones(loc.cont) }
for idx = 1, table.getn(zones) do
- SetMapZoom(loc.cont, idx)
+ SafeSetMapZoom(loc.cont, idx)
local mf = GetMapInfo and GetMapInfo() or ""
if mf ~= "" then
if mf == loc.zone then
@@ -1158,11 +1188,11 @@ local function DiscoverDmfZoneIndices()
end
end
if savedZ > 0 then
- SetMapZoom(savedC, savedZ)
+ SafeSetMapZoom(savedC, savedZ)
elseif savedC > 0 then
- SetMapZoom(savedC, 0)
+ SafeSetMapZoom(savedC, 0)
else
- if SetMapToCurrentZone then SetMapToCurrentZone() end
+ SafeSetMapToCurrentZone()
end
end
@@ -1390,7 +1420,7 @@ SlashCmdList["DMFMAP"] = function(msg)
cf:AddMessage("|cffffcc66[DMF Scan] " .. cname .. " (cont=" .. c .. "):|r")
local zones = { GetMapZones(c) }
for idx = 1, table.getn(zones) do
- SetMapZoom(c, idx)
+ SafeSetMapZoom(c, idx)
local mf = GetMapInfo and GetMapInfo() or "(nil)"
local zname = zones[idx] or "?"
if string.find(string.lower(mf), "elwynn") or string.find(string.lower(zname), "elwynn")
@@ -1401,9 +1431,9 @@ SlashCmdList["DMFMAP"] = function(msg)
end
end
end
- if savedZ > 0 then SetMapZoom(savedC, savedZ)
- elseif savedC > 0 then SetMapZoom(savedC, 0)
- else if SetMapToCurrentZone then SetMapToCurrentZone() end end
+ if savedZ > 0 then SafeSetMapZoom(savedC, savedZ)
+ elseif savedC > 0 then SafeSetMapZoom(savedC, 0)
+ else SafeSetMapToCurrentZone() end
return
end
local ai, iw, dl, ds = GetDmfSchedule()
@@ -1931,7 +1961,7 @@ local function UpdateNavMap()
if not N.frame or not N.tiles or not N.frame:IsVisible() then return end
if WorldMapFrame and WorldMapFrame:IsVisible() then return end
- if SetMapToCurrentZone then SetMapToCurrentZone() end
+ SafeSetMapToCurrentZone()
local mapFile = GetMapInfo and GetMapInfo() or ""
if mapFile ~= "" and mapFile ~= N.curMap then
diff --git a/agent-tools/generate_consumable_db.py b/agent-tools/generate_consumable_db.py
new file mode 100644
index 0000000..10f56c7
--- /dev/null
+++ b/agent-tools/generate_consumable_db.py
@@ -0,0 +1,426 @@
+from __future__ import annotations
+
+import argparse
+import re
+import textwrap
+import xml.etree.ElementTree as ET
+import zipfile
+from collections import defaultdict
+from dataclasses import dataclass
+from datetime import datetime
+from pathlib import Path, PurePosixPath
+
+
+MAIN_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
+DOC_REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
+PKG_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
+NS = {
+ "main": MAIN_NS,
+}
+
+ROLE_META = {
+ "坦克 (物理坦克)": {
+ "key": "tank_physical",
+ "detail": "坦克 · 物理坦克",
+ "color": (0.40, 0.70, 1.00),
+ },
+ "坦克 (法系坦克)": {
+ "key": "tank_caster",
+ "detail": "坦克 · 法系坦克",
+ "color": (1.00, 0.80, 0.20),
+ },
+ "法系输出": {
+ "key": "caster_dps",
+ "detail": "输出 · 法系输出",
+ "color": (0.65, 0.45, 1.00),
+ },
+ "物理近战": {
+ "key": "melee_dps",
+ "detail": "输出 · 物理近战",
+ "color": (1.00, 0.55, 0.25),
+ },
+ "物理远程": {
+ "key": "ranged_dps",
+ "detail": "输出 · 物理远程",
+ "color": (0.55, 0.88, 0.42),
+ },
+ "治疗": {
+ "key": "healer",
+ "detail": "治疗",
+ "color": (0.42, 1.00, 0.72),
+ },
+}
+
+CATEGORY_ORDER = [
+ "合剂",
+ "药剂",
+ "攻强",
+ "诅咒之地buff",
+ "赞达拉",
+ "武器",
+ "食物",
+ "酒",
+ "药水",
+]
+
+# The exported sheet does not include item IDs. We only preserve a very small
+# set of IDs that already existed in the addon and can be matched confidently.
+ITEM_ID_OVERRIDES = {
+ "巨人药剂": 9206,
+ "猫鼬药剂": 13452,
+ "精炼智慧合剂": 13511,
+ "夜鳞鱼汤": 13931,
+ "烤鱿鱼": 13928,
+ "炎夏火水": 12820,
+ "自由行动药水": 5634,
+}
+
+HEADER_MAP = {
+ "A": "role",
+ "B": "category",
+ "C": "name",
+ "D": "effect",
+ "E": "duration",
+}
+
+
+@dataclass
+class Row:
+ role: str
+ category: str
+ name: str
+ effect: str
+ duration: str
+ row_number: int
+
+
+def read_xlsx_rows(path: Path) -> list[Row]:
+ with zipfile.ZipFile(path) as archive:
+ shared_strings = load_shared_strings(archive)
+ workbook = ET.fromstring(archive.read("xl/workbook.xml"))
+ relationships = ET.fromstring(archive.read("xl/_rels/workbook.xml.rels"))
+ rel_map = {
+ rel.attrib["Id"]: rel.attrib["Target"]
+ for rel in relationships.findall(f"{{{PKG_REL_NS}}}Relationship")
+ }
+
+ sheet = workbook.find("main:sheets", NS)[0]
+ target = rel_map[sheet.attrib[f"{{{DOC_REL_NS}}}id"]]
+ sheet_path = normalize_sheet_path(target)
+ root = ET.fromstring(archive.read(sheet_path))
+ sheet_rows = root.find("main:sheetData", NS).findall("main:row", NS)
+
+ rows: list[Row] = []
+ for row in sheet_rows[1:]:
+ values = {}
+ for cell in row.findall("main:c", NS):
+ ref = cell.attrib.get("r", "")
+ col_match = re.match(r"([A-Z]+)", ref)
+ if not col_match:
+ continue
+ col = col_match.group(1)
+ values[col] = read_cell_value(cell, shared_strings).strip()
+
+ if not any(values.values()):
+ continue
+
+ normalized = {
+ field: normalize_text(values.get(col, ""))
+ for col, field in HEADER_MAP.items()
+ }
+ rows.append(
+ Row(
+ role=normalized["role"],
+ category=normalized["category"],
+ name=normalized["name"],
+ effect=normalized["effect"],
+ duration=normalized["duration"],
+ row_number=int(row.attrib.get("r", "0")),
+ )
+ )
+
+ return rows
+
+
+def load_shared_strings(archive: zipfile.ZipFile) -> list[str]:
+ if "xl/sharedStrings.xml" not in archive.namelist():
+ return []
+
+ root = ET.fromstring(archive.read("xl/sharedStrings.xml"))
+ values: list[str] = []
+ for string_item in root.findall("main:si", NS):
+ text_parts = [
+ text_node.text or ""
+ for text_node in string_item.iter(f"{{{MAIN_NS}}}t")
+ ]
+ values.append("".join(text_parts))
+ return values
+
+
+def normalize_sheet_path(target: str) -> str:
+ if target.startswith("/"):
+ normalized = PurePosixPath("xl") / PurePosixPath(target).relative_to("/")
+ else:
+ normalized = PurePosixPath("xl") / PurePosixPath(target)
+ return str(normalized).replace("xl/xl/", "xl/")
+
+
+def read_cell_value(cell: ET.Element, shared_strings: list[str]) -> str:
+ cell_type = cell.attrib.get("t")
+ value_node = cell.find("main:v", NS)
+ if cell_type == "s" and value_node is not None:
+ return shared_strings[int(value_node.text)]
+ if cell_type == "inlineStr":
+ inline_node = cell.find("main:is", NS)
+ if inline_node is None:
+ return ""
+ return "".join(
+ text_node.text or ""
+ for text_node in inline_node.iter(f"{{{MAIN_NS}}}t")
+ )
+ return value_node.text if value_node is not None else ""
+
+
+def normalize_text(value: str) -> str:
+ value = (value or "").strip()
+ value = value.replace("\u3000", " ")
+ value = re.sub(r"\s+", " ", value)
+ value = value.replace("(", "(").replace(")", ")")
+ return value
+
+
+def normalize_rows(rows: list[Row]) -> list[Row]:
+ normalized: list[Row] = []
+ for row in rows:
+ role = row.role
+ category = row.category
+ name = row.name
+ effect = normalize_effect(row.effect)
+ duration = normalize_duration(row.duration)
+
+ if not role or not category or not name:
+ raise ValueError(
+ f"存在不完整数据行,Excel 行号 {row.row_number}: "
+ f"{role!r}, {category!r}, {name!r}"
+ )
+
+ normalized.append(
+ Row(
+ role=role,
+ category=category,
+ name=name,
+ effect=effect,
+ duration=duration,
+ row_number=row.row_number,
+ )
+ )
+
+ return normalized
+
+
+def normalize_effect(value: str) -> str:
+ value = normalize_text(value)
+ replacements = {
+ "、": " / ",
+ ",": " / ",
+ }
+ for old, new in replacements.items():
+ value = value.replace(old, new)
+ value = value.replace("提升治疗", "提升治疗效果")
+ return value
+
+
+def normalize_duration(value: str) -> str:
+ value = normalize_text(value)
+ replacements = {
+ "2小时1": "2小时",
+ "瞬发 ": "瞬发",
+ }
+ return replacements.get(value, value)
+
+
+def build_groups(rows: list[Row]) -> list[dict]:
+ buckets: dict[str, list[Row]] = defaultdict(list)
+ for row in rows:
+ buckets[row.role].append(row)
+
+ role_order = [
+ role
+ for role in ROLE_META
+ if role in buckets
+ ]
+ unknown_roles = sorted(role for role in buckets if role not in ROLE_META)
+ role_order.extend(unknown_roles)
+
+ category_index = {name: index for index, name in enumerate(CATEGORY_ORDER)}
+ groups = []
+ for role in role_order:
+ meta = ROLE_META.get(role, {})
+ role_rows = sorted(
+ buckets[role],
+ key=lambda item: (
+ category_index.get(item.category, len(CATEGORY_ORDER)),
+ item.row_number,
+ item.name,
+ ),
+ )
+ items = []
+ for row in role_rows:
+ items.append(
+ {
+ "cat": row.category,
+ "name": row.name,
+ "effect": row.effect,
+ "duration": row.duration,
+ "id": ITEM_ID_OVERRIDES.get(row.name, 0),
+ }
+ )
+
+ groups.append(
+ {
+ "key": meta.get("key", slugify(role)),
+ "role": role,
+ "detail": meta.get("detail", role),
+ "color": meta.get("color", (0.85, 0.75, 0.90)),
+ "items": items,
+ }
+ )
+
+ return groups
+
+
+def slugify(value: str) -> str:
+ value = re.sub(r"[^0-9A-Za-z\u4e00-\u9fff]+", "_", value)
+ value = value.strip("_")
+ return value.lower() or "role"
+
+
+def render_lua(groups: list[dict], source_path: Path, item_count: int) -> str:
+ role_order = [group["role"] for group in groups]
+ category_order = sorted(
+ {item["cat"] for group in groups for item in group["items"]},
+ key=lambda name: CATEGORY_ORDER.index(name)
+ if name in CATEGORY_ORDER
+ else len(CATEGORY_ORDER),
+ )
+
+ generated_at = datetime.fromtimestamp(source_path.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
+
+ lines = [
+ "-" * 80,
+ "-- Nanami-UI: ConsumableDB.lua",
+ "-- 食物药剂百科数据库(由导出表生成,请优先更新 Excel 后再重新生成)",
+ f"-- Source: {source_path}",
+ f"-- Stats : {len(groups)} roles / {len(category_order)} categories / {item_count} entries",
+ "-" * 80,
+ "SFrames = SFrames or {}",
+ "",
+ "SFrames.ConsumableDB = {",
+ f' generatedAt = "{generated_at}",',
+ " summary = {",
+ f" roleCount = {len(groups)},",
+ f" categoryCount = {len(category_order)},",
+ f" itemCount = {item_count},",
+ " },",
+ " roleOrder = {",
+ ]
+
+ for role in role_order:
+ lines.append(f' "{lua_escape(role)}",')
+ lines.extend(
+ [
+ " },",
+ " categoryOrder = {",
+ ]
+ )
+ for category in category_order:
+ lines.append(f' "{lua_escape(category)}",')
+ lines.extend(
+ [
+ " },",
+ " groups = {",
+ ]
+ )
+
+ for index, group in enumerate(groups, start=1):
+ color = ", ".join(f"{component:.2f}" for component in group["color"])
+ lines.extend(
+ [
+ "",
+ f" -- {index}. {group['detail']}",
+ " {",
+ f' key = "{lua_escape(group["key"])}",',
+ f' role = "{lua_escape(group["role"])}",',
+ f' detail = "{lua_escape(group["detail"])}",',
+ f" color = {{ {color} }},",
+ " items = {",
+ ]
+ )
+ for item in group["items"]:
+ lines.append(
+ ' {{ cat="{cat}", name="{name}", effect="{effect}", duration="{duration}", id={item_id} }},'.format(
+ cat=lua_escape(item["cat"]),
+ name=lua_escape(item["name"]),
+ effect=lua_escape(item["effect"]),
+ duration=lua_escape(item["duration"]),
+ item_id=item["id"],
+ )
+ )
+ lines.extend(
+ [
+ " },",
+ " },",
+ ]
+ )
+
+ lines.extend(
+ [
+ " },",
+ "}",
+ "",
+ ]
+ )
+ return "\n".join(lines)
+
+
+def lua_escape(value: str) -> str:
+ value = value.replace("\\", "\\\\").replace('"', '\\"')
+ return value
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Generate ConsumableDB.lua from an exported WoW consumables XLSX."
+ )
+ parser.add_argument("xlsx", type=Path, help="Path to the exported XLSX file")
+ parser.add_argument(
+ "-o",
+ "--output",
+ type=Path,
+ default=Path("ConsumableDB.lua"),
+ help="Output Lua file path",
+ )
+ return parser.parse_args()
+
+
+def main() -> None:
+ args = parse_args()
+ rows = normalize_rows(read_xlsx_rows(args.xlsx))
+ groups = build_groups(rows)
+ lua = render_lua(groups, args.xlsx, len(rows))
+ args.output.write_text(lua, encoding="utf-8", newline="\n")
+ print(
+ textwrap.dedent(
+ f"""\
+ Generated {args.output}
+ source : {args.xlsx}
+ roles : {len(groups)}
+ items : {len(rows)}
+ categories: {len({item['cat'] for group in groups for item in group['items']})}
+ """
+ ).strip()
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/MapReveal-Standalone.md b/docs/MapReveal-Standalone.md
new file mode 100644
index 0000000..779f907
--- /dev/null
+++ b/docs/MapReveal-Standalone.md
@@ -0,0 +1,126 @@
+# MapReveal Standalone
+
+## 目标
+
+把 `Nanami-UI` 里的地图迷雾揭示功能单独提纯出来,方便其他插件作者复用,而不依赖整套 `SFrames` / `Nanami-UI` 框架。
+
+## 当前实现包含什么
+
+原始实现位于 `MapReveal.lua`,核心能力有 4 部分:
+
+1. 基于 `LibMapOverlayData` / `MapOverlayData` 补全地图 overlay 数据。
+2. Hook `WorldMapFrame_Update`,在世界地图刷新后把未探索区域重新画出来,并降低亮度显示。
+3. 被动采集当前角色已经探索过的 overlay,持久化到 SavedVariables。
+4. 主动扫描全部大陆与区域,把当前角色已经探索到的 overlay 批量收集出来。
+
+## 与 Nanami-UI 的耦合点
+
+真正的强耦合不多,主要只有这些:
+
+1. `SFrames:Print`
+ 用于聊天框输出提示。
+2. `SFramesDB` / `SFramesGlobalDB`
+ 分别保存角色配置和账号级扫描数据。
+3. `SFrames:CallWithPreservedBattlefieldMinimap`
+ 用来保护战场小地图状态,避免 `SetMapZoom` / `SetMapToCurrentZone` 带来副作用。
+4. `/nui ...`
+ 命令入口挂在 `Nanami-UI` 的统一 Slash Command 里。
+5. `ConfigUI.lua`
+ 只是配置面板入口,不影响核心迷雾逻辑。
+
+## 提纯后的最小依赖
+
+独立插件只需要:
+
+1. `LibMapOverlayData` 或兼容的 overlay 数据表。
+2. `WorldMapFrame_Update`
+3. `GetMapInfo` / `GetNumMapOverlays` / `GetMapOverlayInfo`
+4. `SetMapZoom` / `SetMapToCurrentZone`
+5. SavedVariables
+
+也就是说,这个功能本质上可以完全脱离 UI 框架。
+
+## 推荐拆分方式
+
+建议拆成一个独立插件:
+
+1. `Nanami-MapReveal.toc`
+2. `Core.lua`
+3. `MapReveal.lua`
+
+其中:
+
+1. `Core.lua` 负责初始化、打印、Slash 命令、配置默认值。
+2. `MapReveal.lua` 只保留地图迷雾逻辑、扫描逻辑、数据持久化。
+3. 独立版会在世界地图右上角放一个 `Reveal` 勾选框,方便用户直接开关迷雾揭示。
+
+## 数据结构建议
+
+角色级配置:
+
+```lua
+NanamiMapRevealDB = {
+ enabled = true,
+ unexploredAlpha = 0.7,
+}
+```
+
+账号级扫描数据:
+
+```lua
+NanamiMapRevealGlobalDB = {
+ scanned = {
+ ["ZoneName"] = {
+ "OVERLAYNAME:width:height:offsetX:offsetY",
+ },
+ },
+}
+```
+
+## Hook 策略
+
+最稳妥的方式仍然是包裹 `WorldMapFrame_Update`:
+
+1. 先隐藏现有 `WorldMapOverlay1..N`
+2. 调用原始 `WorldMapFrame_Update`
+3. 被动收集当前可见的 explored overlays
+4. 如果开关开启,再自行补画 unexplored overlays
+
+这样兼容原版地图逻辑,也不需要重做整张世界地图。
+
+## 兼容注意点
+
+1. `NUM_WORLDMAP_OVERLAYS` 不够时要动态扩容。
+2. 最后一行/列贴图可能不是 256,必须重新计算 `TexCoord`。
+3. 个别地图 overlay 偏移有 errata,需要单独修正。
+4. 扫描地图时可能影响当前地图上下文,结束后要恢复原地图层级。
+5. 如果服务端或整合包已经补过 `MapOverlayData`,合并时要按 overlay 名去重。
+
+## 已提取的独立插件样板
+
+仓库里已经附带一个可复用版本:
+
+- [standalone/Nanami-MapReveal/Nanami-MapReveal.toc](e:/Game/trutle%20wow/Interface/AddOns/Nanami-UI/standalone/Nanami-MapReveal/Nanami-MapReveal.toc)
+- [standalone/Nanami-MapReveal/Core.lua](e:/Game/trutle%20wow/Interface/AddOns/Nanami-UI/standalone/Nanami-MapReveal/Core.lua)
+- [standalone/Nanami-MapReveal/MapReveal.lua](e:/Game/trutle%20wow/Interface/AddOns/Nanami-UI/standalone/Nanami-MapReveal/MapReveal.lua)
+
+## 给其他作者的接入建议
+
+如果对方已经有自己的框架:
+
+1. 保留 `MapReveal.lua` 主体逻辑。
+2. 把打印函数替换成自己的日志函数。
+3. 把 DB 名称换成自己的 SavedVariables。
+4. 把 Slash 命令合并进自己的命令系统。
+
+如果对方只想直接用:
+
+1. 复制 `standalone/Nanami-MapReveal` 整个目录。
+2. 放进 `Interface/AddOns/`
+3. 确保客户端有 `LibMapOverlayData` 或兼容数据源。
+
+## 后续可继续提纯的方向
+
+1. 把 `TurtleWoW_Zones` 再单独拆成数据文件。
+2. 增加一个纯 API 层,只暴露 `Toggle` / `Refresh` / `ScanAllMaps` / `ExportScannedData`。
+3. 为不同端做兼容层,比如 Turtle WoW、Vanilla、1.12 私服整合端分别适配。
diff --git a/img/progress.tga b/img/progress.tga
index 0ab42d6..88f60bc 100644
Binary files a/img/progress.tga and b/img/progress.tga differ