完成焦点等开发

This commit is contained in:
rucky
2026-03-31 18:03:23 +08:00
parent c7dd0f4848
commit 6e18269bfd
34 changed files with 6803 additions and 542 deletions

View File

@@ -26,6 +26,7 @@ local DEFAULTS = {
showHotkey = true,
showMacroName = false,
rangeColoring = true,
behindGlow = true,
showPetBar = true,
showStanceBar = true,
showRightBars = true,
@@ -1149,6 +1150,91 @@ 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
@@ -1160,7 +1246,18 @@ function AB:SetupRangeCheck()
this.timer = 0
local db = AB:GetDB()
if not db.rangeColoring then return end
-- 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
@@ -1168,15 +1265,30 @@ function AB:SetupRangeCheck()
for _, b in ipairs(buttons) do
local ok, action = pcall(getID, b)
if ok and action and HasAction(action) then
local inRange = IsActionInRange(action)
local ov = GetOrCreateRangeOverlay(b)
if inRange == 0 then
ov:Show()
else
ov:Hide()
-- Range coloring
if db.rangeColoring then
local inRange = IsActionInRange(action)
local ov = GetOrCreateRangeOverlay(b)
if inRange == 0 then
ov:Show()
else
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

View File

@@ -1700,17 +1700,20 @@ function SFrames.Bags.Bank:Initialize()
local sortBtn = CreateHeaderIconButton("SFramesBankSortBtn", SFBankFrame, "Interface\\Icons\\INV_Misc_Note_05")
sortBtn:SetPoint("LEFT", searchEB, "RIGHT", 6, 0)
SFrames:SetIcon(sortBtn.icon, "gold")
sortBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
sortBtn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(TEXT_SORT, 1, 1, 1)
GameTooltip:AddLine("\229\183\166\233\148\174\230\149\180\231\144\134 | \229\143\179\233\148\174\229\143\141\229\186\143\230\149\180\231\144\134", 0.7, 0.7, 0.7)
GameTooltip:Show()
end)
sortBtn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
sortBtn:SetScript("OnClick", function()
local reverse = (arg1 == "RightButton")
if SFrames.Bags.Sort and SFrames.Bags.Sort.StartBank then
SFrames.Bags.Sort:StartBank()
SFrames.Bags.Sort:StartBank(reverse)
return
end

View File

@@ -920,11 +920,12 @@ function SFrames.Bags.Container:Initialize()
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
local _A = SFrames.ActiveTheme
local _bagBgA = (SFramesDB and SFramesDB.Bags and type(SFramesDB.Bags.bgAlpha) == "number") and SFramesDB.Bags.bgAlpha or 0.95
if _A and _A.panelBg then
BagFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _A.panelBg[4] or 0.95)
BagFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], _bagBgA)
BagFrame:SetBackdropBorderColor(_A.panelBorder[1], _A.panelBorder[2], _A.panelBorder[3], _A.panelBorder[4] or 0.9)
else
BagFrame:SetBackdropColor(0.12, 0.06, 0.10, 0.95)
BagFrame:SetBackdropColor(0.12, 0.06, 0.10, _bagBgA)
BagFrame:SetBackdropBorderColor(0.55, 0.30, 0.42, 0.9)
end
local bagShadow = CreateFrame("Frame", nil, BagFrame)
@@ -1418,16 +1419,21 @@ function SFrames.Bags.Container:Initialize()
local sortBtn = CreateHeaderIconButton("SFramesBagSortBtn", BagFrame, "Interface\\Icons\\INV_Misc_Note_05")
sortBtn:SetPoint("LEFT", eb, "RIGHT", 6, 0)
SFrames:SetIcon(sortBtn.icon, "backpack")
sortBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
sortBtn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(TEXT_SORT, 1, 1, 1)
GameTooltip:AddLine("\229\183\166\233\148\174\230\149\180\231\144\134 | \229\143\179\233\148\174\229\143\141\229\186\143\230\149\180\231\144\134", 0.7, 0.7, 0.7)
GameTooltip:Show()
end)
sortBtn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
sortBtn:SetScript("OnClick", function()
if SFrames.Bags.Sort then SFrames.Bags.Sort:Start() end
if SFrames.Bags.Sort then
local reverse = (arg1 == "RightButton")
SFrames.Bags.Sort:Start(reverse)
end
end)
local hsBtn = CreateHeaderIconButton("SFramesBagHSBtn", BagFrame, "Interface\\Icons\\INV_Misc_Rune_01")

View File

@@ -108,16 +108,30 @@ local function GetSpecialType(link, isBag)
return "Normal"
end
local activeReverse = false
local function SortItemsByRule(items)
table.sort(items, function(a, b)
if a.score ~= b.score then
return a.score > b.score
elseif a.name ~= b.name then
return a.name < b.name
else
return a.count > b.count
end
end)
if activeReverse then
table.sort(items, function(a, b)
if a.score ~= b.score then
return a.score < b.score
elseif a.name ~= b.name then
return a.name > b.name
else
return a.count < b.count
end
end)
else
table.sort(items, function(a, b)
if a.score ~= b.score then
return a.score > b.score
elseif a.name ~= b.name then
return a.name < b.name
else
return a.count > b.count
end
end)
end
end
local function ResetSortState()
@@ -127,6 +141,7 @@ local function ResetSortState()
activeCompleteUpdate = nil
activeBagOrder = nil
activePhase = nil
activeReverse = false
timeSinceLast = 0
sortTimer:Hide()
end
@@ -178,13 +193,22 @@ function SFrames.Bags.Sort:ExecuteSimpleSort(items, bagOrder)
end
-- Now assign target slot to each item
-- When reverse sorting, fill from the last slot (bottom-right) instead of first
for _, item in ipairs(items) do
local targetSlot = nil
if item.specialType ~= "Normal" and slotPool[item.specialType] and table.getn(slotPool[item.specialType]) > 0 then
targetSlot = table.remove(slotPool[item.specialType], 1)
if activeReverse then
targetSlot = table.remove(slotPool[item.specialType])
else
targetSlot = table.remove(slotPool[item.specialType], 1)
end
elseif table.getn(slotPool.Normal) > 0 then
targetSlot = table.remove(slotPool.Normal, 1)
if activeReverse then
targetSlot = table.remove(slotPool.Normal)
else
targetSlot = table.remove(slotPool.Normal, 1)
end
end
if targetSlot then
@@ -356,9 +380,73 @@ function SFrames.Bags.Sort:StartPlacementPhase(ignoreLocked)
end
function SFrames.Bags.Sort:StartForBags(bagOrder, completeMessage, completeUpdate)
if isSorting then return end
-- If a previous sort is still running, force-cancel it first
if isSorting then
-- Drop cursor item if held
if CursorHasItem() then
for _, bag in ipairs(activeBagOrder or bagOrder) do
local bagSlots = GetContainerNumSlots(bag) or 0
for slot = 1, bagSlots do
local _, _, locked = GetContainerItemInfo(bag, slot)
if locked then
PickupContainerItem(bag, slot)
break
end
end
if not CursorHasItem() then break end
end
end
ResetSortState()
end
-- Save reverse flag before any potential defer (ResetSortState clears it)
local savedReverse = activeReverse
-- Wait for any lingering locked items before starting
local hasLocked = false
for _, bag in ipairs(bagOrder) do
local bagSlots = GetContainerNumSlots(bag) or 0
for slot = 1, bagSlots do
local _, _, locked = GetContainerItemInfo(bag, slot)
if locked then
hasLocked = true
break
end
end
if hasLocked then break end
end
if hasLocked then
-- Defer start until locks clear
local waitTimer = CreateFrame("Frame")
local waitElapsed = 0
local selfRef = self
waitTimer:SetScript("OnUpdate", function()
waitElapsed = waitElapsed + arg1
if waitElapsed < 0.05 then return end
waitElapsed = 0
local still = false
for _, bag in ipairs(bagOrder) do
local bagSlots = GetContainerNumSlots(bag) or 0
for slot = 1, bagSlots do
local _, _, locked = GetContainerItemInfo(bag, slot)
if locked then still = true; break end
end
if still then break end
end
if not still then
this:SetScript("OnUpdate", nil)
activeReverse = savedReverse
selfRef:StartForBags(bagOrder, completeMessage, completeUpdate)
end
end)
return
end
isSorting = true
activeReverse = savedReverse
sortQueue = {}
timeSinceLast = 0
activeCompleteMessage = completeMessage
@@ -384,12 +472,13 @@ function SFrames.Bags.Sort:StartForBags(bagOrder, completeMessage, completeUpdat
self:StartPlacementPhase(false)
end
function SFrames.Bags.Sort:Start()
function SFrames.Bags.Sort:Start(reverse)
if SFrames.Bags.Container and SFrames.Bags.Container.isOffline then
SFrames:Print(TEXT_OFFLINE_BAGS)
return
end
activeReverse = reverse or false
self:StartForBags(
{0, 1, 2, 3, 4},
TEXT_BAG_DONE,
@@ -401,12 +490,13 @@ function SFrames.Bags.Sort:Start()
)
end
function SFrames.Bags.Sort:StartBank()
function SFrames.Bags.Sort:StartBank(reverse)
if SFrames.Bags.Bank and SFrames.Bags.Bank.isOffline then
SFrames:Print(TEXT_OFFLINE_BANK)
return
end
activeReverse = reverse or false
self:StartForBags(
{-1, 5, 6, 7, 8, 9, 10, 11},
TEXT_BANK_DONE,

View File

@@ -54,7 +54,7 @@ L.LIST_ROW_W = L.LEFT_W - L.SIDE_PAD * 2 - L.SCROLLBAR_W - 4
local S = {
MainFrame = nil,
selectedIndex = nil,
currentFilter = "all",
currentFilter = "available",
displayList = {},
rowButtons = {},
collapsedCats = {},
@@ -67,9 +67,17 @@ local scanTip = nil
function BTUI.GetCraftExtendedInfo(index)
local name, rank, skillType, v4, _, tpCost, reqLevel = GetCraftInfo(index)
local canLearn = (tonumber(reqLevel) or 0) > 0
tpCost = tonumber(tpCost) or 0
return name, rank, skillType, canLearn, tpCost
local canLearn = false
local isLearned = false
if skillType ~= "header" then
if skillType == "used" then
isLearned = true
else
canLearn = (tonumber(reqLevel) or 0) > 0
end
end
return name, rank, skillType, canLearn, tpCost, isLearned
end
function BTUI.GetSkillTooltipLines(index)
@@ -356,7 +364,11 @@ function BTUI.CreateListRow(parent, idx)
self.rankFS:Hide()
end
if skill.canLearn then
if skill.isLearned then
self.nameFS:SetTextColor(T.learned[1], T.learned[2], T.learned[3])
self.icon:SetVertexColor(0.5, 0.5, 0.5)
self.tpFS:Hide()
elseif skill.canLearn then
self.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3])
self.icon:SetVertexColor(1, 1, 1)
local tp = skill.tpCost or 0
@@ -373,8 +385,8 @@ function BTUI.CreateListRow(parent, idx)
self.tpFS:Hide()
end
else
self.nameFS:SetTextColor(T.learned[1], T.learned[2], T.learned[3])
self.icon:SetVertexColor(0.5, 0.5, 0.5)
self.nameFS:SetTextColor(T.unavailable[1], T.unavailable[2], T.unavailable[3])
self.icon:SetVertexColor(0.6, 0.3, 0.3)
self.tpFS:Hide()
end
end
@@ -392,17 +404,47 @@ end
--------------------------------------------------------------------------------
-- Logic
--------------------------------------------------------------------------------
-- Strip rank suffix to get base skill name (e.g. "自然抗性 2" -> "自然抗性")
function BTUI.GetBaseSkillName(name)
if not name then return "" end
-- Remove trailing " Rank X" / " X" patterns (both EN and CN)
local base = string.gsub(name, "%s+%d+$", "")
base = string.gsub(base, "%s+[Rr]ank%s+%d+$", "")
return base
end
-- Build a lookup: baseName -> highest tpCost among learned ranks
function BTUI.BuildLearnedCostMap()
local map = {} -- baseName -> highest learned tpCost
local numCrafts = GetNumCrafts and GetNumCrafts() or 0
for i = 1, numCrafts do
local name, rank, skillType, canLearn, tpCost, isLearned = BTUI.GetCraftExtendedInfo(i)
if name and skillType ~= "header" and isLearned then
local base = BTUI.GetBaseSkillName(name)
local cost = tpCost or 0
if not map[base] or cost > map[base] then
map[base] = cost
end
end
end
return map
end
function BTUI.BuildDisplayList()
S.displayList = {}
local numCrafts = GetNumCrafts and GetNumCrafts() or 0
if numCrafts == 0 then return end
-- Pre-compute learned cost for each base skill name
local learnedCostMap = BTUI.BuildLearnedCostMap()
local currentCat = nil
local catSkills = {}
local catOrder = {}
for i = 1, numCrafts do
local name, rank, skillType, canLearn, tpCost = BTUI.GetCraftExtendedInfo(i)
local name, rank, skillType, canLearn, tpCost, isLearned = BTUI.GetCraftExtendedInfo(i)
if name then
if skillType == "header" then
currentCat = name
@@ -418,11 +460,18 @@ function BTUI.BuildDisplayList()
table.insert(catOrder, currentCat)
end
end
-- Calculate effective (incremental) TP cost
local effectiveCost = tpCost or 0
if not isLearned and effectiveCost > 0 then
local base = BTUI.GetBaseSkillName(name)
local prevCost = learnedCostMap[base] or 0
effectiveCost = math.max(0, effectiveCost - prevCost)
end
local show = true
if S.currentFilter == "available" then
show = canLearn
elseif S.currentFilter == "used" then
show = (not canLearn)
show = isLearned
end
if show then
table.insert(catSkills[currentCat], {
@@ -430,7 +479,8 @@ function BTUI.BuildDisplayList()
name = name,
rank = rank or "",
canLearn = canLearn,
tpCost = tpCost,
tpCost = effectiveCost,
isLearned = isLearned,
})
end
end
@@ -524,16 +574,18 @@ function BTUI.UpdateDetail()
return
end
local name, rank, skillType, canLearn, tpCost = BTUI.GetCraftExtendedInfo(S.selectedIndex)
local name, rank, skillType, canLearn, tpCost, isLearned = BTUI.GetCraftExtendedInfo(S.selectedIndex)
local iconTex = GetCraftIcon and GetCraftIcon(S.selectedIndex)
detail.icon:SetTexture(iconTex); detail.iconFrame:Show()
detail.nameFS:SetText(name or "")
if canLearn then
if isLearned then
detail.nameFS:SetTextColor(T.learned[1], T.learned[2], T.learned[3])
elseif canLearn then
detail.nameFS:SetTextColor(T.available[1], T.available[2], T.available[3])
else
detail.nameFS:SetTextColor(T.learned[1], T.learned[2], T.learned[3])
detail.nameFS:SetTextColor(T.unavailable[1], T.unavailable[2], T.unavailable[3])
end
if rank and rank ~= "" then
@@ -554,10 +606,23 @@ function BTUI.UpdateDetail()
detail.descScroll:GetScrollChild():SetHeight(math.max(1, descH))
detail.descScroll:SetVerticalScroll(0)
if tpCost > 0 then
if isLearned then
detail.costFS:SetText("|cff808080已学会|r")
elseif tpCost > 0 then
-- Calculate effective (incremental) cost
local effectiveCost = tpCost
local base = BTUI.GetBaseSkillName(name)
local learnedMap = BTUI.BuildLearnedCostMap()
local prevCost = learnedMap[base] or 0
effectiveCost = math.max(0, tpCost - prevCost)
local remaining = BTUI.GetRemainingTP()
local costColor = remaining >= tpCost and "|cff40ff40" or "|cffff4040"
detail.costFS:SetText("训练点数: " .. costColor .. tpCost .. "|r (剩余: " .. remaining .. ")")
local costColor = remaining >= effectiveCost and "|cff40ff40" or "|cffff4040"
if prevCost > 0 then
detail.costFS:SetText("训练点数: " .. costColor .. effectiveCost .. "|r (总" .. tpCost .. " - 已学" .. prevCost .. ") 剩余: " .. remaining)
else
detail.costFS:SetText("训练点数: " .. costColor .. effectiveCost .. "|r (剩余: " .. remaining .. ")")
end
elseif canLearn then
detail.costFS:SetText("训练点数: |cff40ff40免费|r")
else
@@ -706,13 +771,8 @@ function BTUI:Initialize()
fb:SetPoint("TOPLEFT", MF, "TOPLEFT", L.SIDE_PAD, -(L.HEADER_H + 4))
fb:SetWidth(L.LEFT_W - L.SIDE_PAD * 2); fb:SetHeight(22)
local fAll = BTUI.CreateFilterBtn(fb, "全部", 52)
fAll:SetPoint("LEFT", fb, "LEFT", 0, 0)
fAll:SetScript("OnClick", function() S.currentFilter = "all"; BTUI.FullUpdate() end)
MF.filterAll = fAll
local fAvail = BTUI.CreateFilterBtn(fb, "可学", 52)
fAvail:SetPoint("LEFT", fAll, "RIGHT", 3, 0)
fAvail:SetPoint("LEFT", fb, "LEFT", 0, 0)
fAvail:SetScript("OnClick", function() S.currentFilter = "available"; BTUI.FullUpdate() end)
MF.filterAvail = fAvail
@@ -721,11 +781,16 @@ function BTUI:Initialize()
fUsed:SetScript("OnClick", function() S.currentFilter = "used"; BTUI.FullUpdate() end)
MF.filterUsed = fUsed
local fAll = BTUI.CreateFilterBtn(fb, "全部", 52)
fAll:SetPoint("LEFT", fUsed, "RIGHT", 3, 0)
fAll:SetScript("OnClick", function() S.currentFilter = "all"; BTUI.FullUpdate() end)
MF.filterAll = fAll
-- List scroll area
local listTop = L.HEADER_H + L.FILTER_H + 8
local ls = CreateFrame("ScrollFrame", "SFramesBTListScroll", MF)
ls:SetPoint("TOPLEFT", MF, "TOPLEFT", L.SIDE_PAD, -listTop)
ls:SetPoint("BOTTOMRIGHT", MF, "BOTTOMLEFT", L.SIDE_PAD + L.LIST_ROW_W, 6)
ls:SetPoint("BOTTOMRIGHT", MF, "BOTTOMLEFT", L.SIDE_PAD + L.LIST_ROW_W, L.BOTTOM_H + 4)
local lc = CreateFrame("Frame", "SFramesBTListContent", ls)
lc:SetWidth(L.LIST_ROW_W); lc:SetHeight(1)
@@ -807,10 +872,18 @@ function BTUI:Initialize()
function BTUI.UpdateScrollbar()
if not MF.sbThumb or not MF.sbTrack then return end
local scrollMax = BTUI.GetScrollMax()
if scrollMax <= 0 then MF.sbThumb:Hide(); return end
if scrollMax <= 0 then
MF.sbThumb:Hide()
if ls:GetVerticalScroll() > 0 then ls:SetVerticalScroll(0) end
return
end
MF.sbThumb:Show()
local trackH = MF.sbTrack:GetHeight()
local curScroll = ls:GetVerticalScroll()
if curScroll > scrollMax then
curScroll = scrollMax
ls:SetVerticalScroll(scrollMax)
end
local ratio = curScroll / scrollMax
ratio = math.max(0, math.min(1, ratio))
local thumbH = math.max(20, trackH * (trackH / (trackH + scrollMax)))
@@ -1016,7 +1089,7 @@ function BTUI:Initialize()
CraftFrame:SetScript("OnHide", function() end)
CraftFrame:SetAlpha(0); CraftFrame:EnableMouse(false)
end
S.selectedIndex = nil; S.currentFilter = "all"; S.collapsedCats = {}
S.selectedIndex = nil; S.currentFilter = "available"; S.collapsedCats = {}
local petName = UnitName("pet") or ""
if petName ~= "" then
MF.titleFS:SetText("训练野兽 - " .. petName)

View File

@@ -70,7 +70,7 @@ local EQUIP_SLOTS_RIGHT = {
}
local EQUIP_SLOTS_BOTTOM = {
{ id = 16, name = "MainHandSlot" }, { id = 17, name = "SecondaryHandSlot" },
{ id = 18, name = "RangedSlot" }, { id = 0, name = "AmmoSlot" },
{ id = 18, name = "RangedSlot" }, { name = "AmmoSlot" },
}
local TAB_NAMES = { "装备", "声望", "技能", "荣誉" }
@@ -438,11 +438,15 @@ local TALENT_DB = {
meleeCrit = {
{ names = {"Cruelty", "残忍"}, perRank = 1 },
{ names = {"Malice", "恶意"}, perRank = 1 },
{ names = {"Lethal Shots", "致命射击"}, perRank = 1 },
{ names = {"Killer Instinct", "杀戮本能", "致命本能"}, perRank = 1 },
{ names = {"Conviction", "信念", "坚定信念"}, perRank = 1 },
{ names = {"Sharpened Claws", "利爪强化", "尖锐之爪"}, perRank = 2 },
{ names = {"Thundering Strikes", "雷霆一击", "雷霆之击"}, perRank = 1 },
},
rangedCrit = {
{ names = {"Lethal Shots", "夺命射击"}, perRank = 1 },
{ names = {"Killer Instinct", "杀戮本能", "致命本能"}, perRank = 1 },
},
spellCrit = {
{ names = {"Arcane Instability", "奥术不稳定"}, perRank = 1 },
{ names = {"Critical Mass", "临界质量"}, perRank = 2 },
@@ -458,7 +462,7 @@ local function ScanTalentBonuses()
if _talentCache and _talentCacheTime and (now - _talentCacheTime) < 5 then
return _talentCache
end
local r = { meleeHit = 0, spellHit = 0, meleeCrit = 0, spellCrit = 0,
local r = { meleeHit = 0, spellHit = 0, meleeCrit = 0, rangedCrit = 0, spellCrit = 0,
talentDetails = {} }
if not GetNumTalentTabs then _talentCache = r; _talentCacheTime = now; return r end
@@ -532,7 +536,7 @@ local function FullRangedCrit()
local _, agi = UnitStat("player", 2); agiCrit = (agi or 0) / ratio
end
local gearCrit = GetGearBonus("RANGEDCRIT") + GetGearBonus("CRIT")
local talentCrit = GetTalentBonus("meleeCrit")
local talentCrit = GetTalentBonus("rangedCrit")
return baseCrit + agiCrit + gearCrit + talentCrit,
baseCrit, agiCrit, gearCrit, talentCrit
end
@@ -1320,6 +1324,14 @@ function CP:BuildEquipmentPage()
if page.built then return end
page.built = true
-- Resolve AmmoSlot ID dynamically (may vary by client)
for _, si in ipairs(EQUIP_SLOTS_BOTTOM) do
if si.name == "AmmoSlot" and not si.id then
local slotId = GetInventorySlotInfo("AmmoSlot")
si.id = slotId or 0
end
end
local contentH = FRAME_H - (HEADER_H + TAB_BAR_H) - INNER_PAD - 4
local scrollArea = CreateScrollFrame(page, CONTENT_W, contentH)
scrollArea:SetPoint("TOPLEFT", page, "TOPLEFT", 0, 0)
@@ -1547,6 +1559,72 @@ function CP:BuildEquipmentPage()
end)
page.rotRight = rotRight
-- Gear action toolbar: small icon buttons next to model toolbar
local gearTbW = segW * 3 + 2
local gearToolbar = CreateFrame("Frame", nil, page)
gearToolbar:SetWidth(gearTbW)
gearToolbar:SetHeight(tbH)
gearToolbar:SetPoint("BOTTOM", modelBg, "BOTTOM", 0, 5 + tbH + 2)
gearToolbar:SetFrameLevel(page:GetFrameLevel() + 6)
SetPixelBackdrop(gearToolbar, { 0.04, 0.04, 0.06, 0.75 }, { 0.22, 0.22, 0.28, 0.5 })
page.gearToolbar = gearToolbar
local function MakeGearToolBtn(parent, w, text, ox, tooltip, onClick)
local btn = CreateFrame("Button", nil, parent)
btn:SetWidth(w)
btn:SetHeight(tbH - 2)
btn:SetPoint("LEFT", parent, "LEFT", ox, 0)
btn:SetFrameLevel(parent:GetFrameLevel() + 1)
btn:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
tile = false, tileSize = 0, edgeSize = 0,
})
btn:SetBackdropColor(0, 0, 0, 0)
local fs = MakeFS(btn, 8, "CENTER", { 0.5, 0.5, 0.55 })
fs:SetPoint("CENTER", btn, "CENTER", 0, 0)
fs:SetText(text)
btn.label = fs
btn:SetScript("OnEnter", function()
this:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4] or 0.5)
this.label:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
if tooltip then
GameTooltip:SetOwner(this, "ANCHOR_BOTTOM")
GameTooltip:AddLine(tooltip, 1, 1, 1)
GameTooltip:Show()
end
end)
btn:SetScript("OnLeave", function()
this:SetBackdropColor(0, 0, 0, 0)
this.label:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
GameTooltip:Hide()
end)
btn:SetScript("OnClick", onClick)
return btn
end
local gStripBtn = MakeGearToolBtn(gearToolbar, segW, "", 1, "一键脱装", function() CP:StripAllGear() end)
page.gStripBtn = gStripBtn
local gSep1 = gearToolbar:CreateTexture(nil, "OVERLAY")
gSep1:SetTexture("Interface\\Buttons\\WHITE8X8")
gSep1:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4] or 0.4)
gSep1:SetWidth(1)
gSep1:SetHeight(tbH - 4)
gSep1:SetPoint("LEFT", gearToolbar, "LEFT", segW + 1, 0)
local gEquipBtn = MakeGearToolBtn(gearToolbar, segW, "穿", segW + 1, "一键穿装", function() CP:QuickEquipLastSet() end)
page.gEquipBtn = gEquipBtn
local gSep2 = gearToolbar:CreateTexture(nil, "OVERLAY")
gSep2:SetTexture("Interface\\Buttons\\WHITE8X8")
gSep2:SetVertexColor(T.sepColor[1], T.sepColor[2], T.sepColor[3], T.sepColor[4] or 0.4)
gSep2:SetWidth(1)
gSep2:SetHeight(tbH - 4)
gSep2:SetPoint("LEFT", gearToolbar, "LEFT", segW * 2 + 1, 0)
local gSetsBtn = MakeGearToolBtn(gearToolbar, segW, "", segW * 2 + 1, "装备方案管理", function() CP:ToggleGearSetPopup() end)
page.gSetsBtn = gSetsBtn
-- Equipment slots
local slotY = -6
page.equipSlots = {}
@@ -2064,7 +2142,7 @@ function CP:BuildEquipmentPage()
if gearC > 0 then CS.TipKV(" 装备暴击:", string.format("+%d%%", gearC)) end
if talC > 0 then
CS.TipKV(" 天赋暴击:", string.format("+%d%%", talC))
for _, d in ipairs(CS.GetTalentDetailsFor("meleeCrit")) do
for _, d in ipairs(CS.GetTalentDetailsFor("rangedCrit")) do
CS.TipLine(string.format(" %s (%d/%d) +%d%%",
d.name, d.rank, d.maxRank, d.bonus), 0.55,0.55,0.6)
end
@@ -2765,6 +2843,13 @@ function CP:CreateEquipSlot(parent, slotID, slotName)
ilvlText:SetText("")
frame.ilvlText = ilvlText
local durText = frame:CreateFontString(nil, "OVERLAY")
durText:SetFont(GetFont(), 7, "OUTLINE")
durText:SetPoint("TOPLEFT", frame, "TOPLEFT", 2, -2)
durText:SetTextColor(0.4, 1, 0.4)
durText:SetText("")
frame.durText = durText
local glow = frame:CreateTexture(nil, "OVERLAY")
glow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
glow:SetBlendMode("ADD")
@@ -2822,7 +2907,7 @@ function CP:CreateEquipSlot(parent, slotID, slotName)
CP:ShowSwapPopup(this)
else
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
if GetInventoryItemLink("player", this.slotID) then
if GetInventoryItemLink("player", this.slotID) or GetInventoryItemTexture("player", this.slotID) then
GameTooltip:SetInventoryItem("player", this.slotID)
else
local label = SLOT_LABEL[this.slotName] or this.slotName
@@ -2926,6 +3011,34 @@ function CP:UpdateEquipment()
if slot then
local tex = GetInventoryItemTexture("player", si.id)
local link = GetInventoryItemLink("player", si.id)
-- Ammo slot special: try tooltip-based detection when texture/link fail
if not tex and si.name == "AmmoSlot" then
if not CP._ammoTip then
CP._ammoTip = CreateFrame("GameTooltip", "SFramesCPAmmoTip", UIParent, "GameTooltipTemplate")
CP._ammoTip:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -300, -300)
end
local tip = CP._ammoTip
tip:SetOwner(UIParent, "ANCHOR_NONE")
tip:ClearLines()
tip:SetInventoryItem("player", si.id)
local nLines = tip:NumLines() or 0
if nLines > 0 then
local txtObj = getglobal("SFramesCPAmmoTipTextLeft1")
if txtObj then
local ammoName = txtObj:GetText()
if ammoName and ammoName ~= "" then
-- Ammo is equipped but API can't return texture; use a generic icon
tex = "Interface\\Icons\\INV_Ammo_Arrow_02"
-- Try to get the real icon via GetItemInfo if available
if GetItemInfo then
local _, _, _, _, _, _, _, _, _, itemTex = GetItemInfo(ammoName)
if itemTex then tex = itemTex end
end
end
end
end
tip:Hide()
end
if tex then
slot.icon:SetTexture(tex)
slot.icon:SetVertexColor(1, 1, 1)
@@ -2939,6 +3052,54 @@ function CP:UpdateEquipment()
slot.ilvlText:SetText("")
end
end
-- Durability display (tooltip scan for 1.12 clients)
if slot.durText then
local dur, durMax
-- Try direct API first (pcall for safety)
if GetInventoryItemDurability then
local ok, c, m = pcall(GetInventoryItemDurability, si.id)
if ok then dur, durMax = c, m end
end
-- Fallback: scan tooltip
if not dur and link then
if not CP._durTip then
CP._durTip = CreateFrame("GameTooltip", "SFramesCPDurTip", UIParent, "GameTooltipTemplate")
CP._durTip:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -300, -300)
end
local tip = CP._durTip
tip:SetOwner(UIParent, "ANCHOR_NONE")
tip:ClearLines()
tip:SetInventoryItem("player", si.id)
local nLines = tip:NumLines() or 0
for li = 1, nLines do
local txtObj = getglobal("SFramesCPDurTipTextLeft" .. li)
if txtObj then
local line = txtObj:GetText()
if line then
local _, _, c, m = string.find(line, "(%d+)%s*/%s*(%d+)")
if c and m then
dur, durMax = tonumber(c), tonumber(m)
break
end
end
end
end
tip:Hide()
end
if dur and durMax and durMax > 0 then
local pct = math.floor(dur / durMax * 100)
slot.durText:SetText(pct .. "%")
if pct <= 25 then
slot.durText:SetTextColor(1, 0.2, 0.2)
elseif pct <= 50 then
slot.durText:SetTextColor(1, 0.8, 0.2)
else
slot.durText:SetTextColor(0.4, 1, 0.4)
end
else
slot.durText:SetText("")
end
end
local quality = GetItemQualityFromLink(link)
if quality and quality >= 2 and QUALITY_COLORS[quality] then
local qc = QUALITY_COLORS[quality]
@@ -2960,6 +3121,7 @@ function CP:UpdateEquipment()
slot.currentBorderColor = nil
slot.qualGlow:Hide()
if slot.ilvlText then slot.ilvlText:SetText("") end
if slot.durText then slot.durText:SetText("") end
end
end
end
@@ -3848,6 +4010,430 @@ function CP:UpdatePet()
end
end
--------------------------------------------------------------------------------
-- Gear Set Management: Strip / Equip / Save / Load / Delete
--------------------------------------------------------------------------------
-- All equippable slot IDs (excluding shirt=4, tabard=19, ammo=0)
local GEAR_SLOT_IDS = { 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }
-- One-click strip: unequip all gear into bags
function CP:StripAllGear()
-- Save current gear as "_lastStripped" before stripping
self:SaveGearSetInternal("_lastStripped")
local count = 0
for _, sid in ipairs(GEAR_SLOT_IDS) do
if GetInventoryItemLink("player", sid) then
PickupInventoryItem(sid)
PutItemInBackpack()
count = count + 1
end
end
if count > 0 then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[Nanami-UI]|r 已脱下 " .. count .. " 件装备")
else
DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[Nanami-UI]|r 当前没有可脱下的装备")
end
CP:ScheduleEquipUpdate()
end
-- Internal: save current gear to DB
function CP:SaveGearSetInternal(name)
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.gearSets then SFramesDB.gearSets = {} end
local set = {}
for _, sid in ipairs(GEAR_SLOT_IDS) do
local link = GetInventoryItemLink("player", sid)
if link then
set[sid] = link
end
end
SFramesDB.gearSets[name] = set
end
-- Save gear set with user-provided name
function CP:SaveGearSet(name)
if not name or name == "" then return end
self:SaveGearSetInternal(name)
DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[Nanami-UI]|r 装备方案 |cffffd100" .. name .. "|r 已保存")
end
-- Load gear set by name
function CP:LoadGearSet(name)
if not SFramesDB or not SFramesDB.gearSets or not SFramesDB.gearSets[name] then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[Nanami-UI]|r 找不到装备方案: " .. (name or "nil"))
return
end
local set = SFramesDB.gearSets[name]
local equipped = 0
-- Find item in bags by link matching (item:ID portion)
local function FindInBags(targetLink)
if not targetLink then return nil, nil end
local _, _, targetId = string.find(targetLink, "item:(%d+)")
if not targetId then return nil, nil end
for bag = 0, 4 do
local slots = GetContainerNumSlots(bag)
if slots and slots > 0 then
for slot = 1, slots do
local bagLink = GetContainerItemLink(bag, slot)
if bagLink then
local _, _, bagId = string.find(bagLink, "item:(%d+)")
if bagId == targetId then
return bag, slot
end
end
end
end
end
return nil, nil
end
for _, sid in ipairs(GEAR_SLOT_IDS) do
local targetLink = set[sid]
if targetLink then
-- Check if already wearing this item
local currentLink = GetInventoryItemLink("player", sid)
if currentLink then
local _, _, curId = string.find(currentLink, "item:(%d+)")
local _, _, tarId = string.find(targetLink, "item:(%d+)")
if curId == tarId then
-- Already wearing, skip
else
local bag, slot = FindInBags(targetLink)
if bag and slot then
PickupContainerItem(bag, slot)
PickupInventoryItem(sid)
equipped = equipped + 1
end
end
else
local bag, slot = FindInBags(targetLink)
if bag and slot then
PickupContainerItem(bag, slot)
PickupInventoryItem(sid)
equipped = equipped + 1
end
end
end
end
if equipped > 0 then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[Nanami-UI]|r 已装备 " .. equipped .. " 件 (方案: |cffffd100" .. name .. "|r)")
else
DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[Nanami-UI]|r 方案 |cffffd100" .. name .. "|r 中的装备已全部穿戴或不在背包中")
end
CP:ScheduleEquipUpdate()
end
-- Delete gear set
function CP:DeleteGearSet(name)
if not SFramesDB or not SFramesDB.gearSets then return end
SFramesDB.gearSets[name] = nil
DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[Nanami-UI]|r 装备方案 |cffffd100" .. name .. "|r 已删除")
end
-- Quick equip: load the "_lastStripped" auto-saved set
function CP:QuickEquipLastSet()
if SFramesDB and SFramesDB.gearSets and SFramesDB.gearSets["_lastStripped"] then
self:LoadGearSet("_lastStripped")
else
DEFAULT_CHAT_FRAME:AddMessage("|cff00ccff[Nanami-UI]|r 没有最近脱下的装备记录,请先使用一键脱装或保存方案")
end
end
-- Get sorted list of user-created gear set names
function CP:GetGearSetNames()
local names = {}
if SFramesDB and SFramesDB.gearSets then
for k, _ in pairs(SFramesDB.gearSets) do
if string.sub(k, 1, 1) ~= "_" then
table.insert(names, k)
end
end
end
table.sort(names)
return names
end
--------------------------------------------------------------------------------
-- Gear Set Popup UI
--------------------------------------------------------------------------------
local gearSetPopup
function CP:ToggleGearSetPopup()
if gearSetPopup and gearSetPopup:IsShown() then
gearSetPopup:Hide()
return
end
self:ShowGearSetPopup()
end
function CP:ShowGearSetPopup()
if not panel then return end
local POPUP_W = 200
local ROW_H = 20
local PAD = 6
if not gearSetPopup then
gearSetPopup = CreateFrame("Frame", "SFramesCPGearSetPopup", panel)
gearSetPopup:SetWidth(POPUP_W)
gearSetPopup:SetFrameStrata("TOOLTIP")
gearSetPopup:SetFrameLevel(200)
SetRoundBackdrop(gearSetPopup, T.bg, T.border)
gearSetPopup:Hide()
gearSetPopup:EnableMouse(true)
-- Title
local title = MakeFS(gearSetPopup, 10, "CENTER", T.gold)
title:SetPoint("TOP", gearSetPopup, "TOP", 0, -PAD)
title:SetText("装备方案管理")
gearSetPopup.title = title
-- Save current button
local saveBtn = CreateFrame("Button", NextName("GSave"), gearSetPopup)
saveBtn:SetWidth(POPUP_W - PAD * 2)
saveBtn:SetHeight(ROW_H)
saveBtn:SetPoint("TOP", gearSetPopup, "TOP", 0, -(PAD + 16))
SetPixelBackdrop(saveBtn, { 0.15, 0.35, 0.15, 0.8 }, T.tabBorder)
local saveTxt = MakeFS(saveBtn, 9, "CENTER", { 0.6, 1, 0.6 })
saveTxt:SetPoint("CENTER", saveBtn, "CENTER", 0, 0)
saveTxt:SetText("+ 保存当前装备为新方案")
saveBtn.label = saveTxt
saveBtn:SetScript("OnEnter", function()
this:SetBackdropColor(0.2, 0.45, 0.2, 0.9)
this.label:SetTextColor(0.8, 1, 0.8)
end)
saveBtn:SetScript("OnLeave", function()
this:SetBackdropColor(0.15, 0.35, 0.15, 0.8)
this.label:SetTextColor(0.6, 1, 0.6)
end)
saveBtn:SetScript("OnClick", function()
CP:ShowGearSetNameInput()
end)
gearSetPopup.saveBtn = saveBtn
gearSetPopup.rows = {}
end
-- Rebuild set list
local names = self:GetGearSetNames()
local numSets = table.getn(names)
-- Hide old rows
for _, row in ipairs(gearSetPopup.rows) do
row:Hide()
end
local contentTop = -(PAD + 16 + ROW_H + 4)
for i = 1, numSets do
local row = gearSetPopup.rows[i]
if not row then
row = CreateFrame("Frame", nil, gearSetPopup)
row:SetWidth(POPUP_W - PAD * 2)
row:SetHeight(ROW_H)
local bg = row:CreateTexture(nil, "BACKGROUND")
bg:SetTexture("Interface\\Buttons\\WHITE8X8")
bg:SetVertexColor(0.12, 0.12, 0.16, 0.6)
bg:SetAllPoints(row)
row.bg = bg
local nameText = MakeFS(row, 9, "LEFT", T.nameText)
nameText:SetPoint("LEFT", row, "LEFT", 6, 0)
nameText:SetWidth(POPUP_W - PAD * 2 - 60)
row.nameText = nameText
-- Load button
local loadBtn = CreateFrame("Button", nil, row)
loadBtn:SetWidth(24)
loadBtn:SetHeight(ROW_H - 4)
loadBtn:SetPoint("RIGHT", row, "RIGHT", -28, 0)
loadBtn:SetFrameLevel(row:GetFrameLevel() + 1)
SetPixelBackdrop(loadBtn, T.tabBg, T.tabBorder)
local loadTxt = MakeFS(loadBtn, 8, "CENTER", { 0.5, 0.8, 1 })
loadTxt:SetPoint("CENTER", loadBtn, "CENTER", 0, 0)
loadTxt:SetText("穿")
loadBtn.label = loadTxt
loadBtn:SetScript("OnEnter", function()
this:SetBackdropColor(T.tabActiveBg[1], T.tabActiveBg[2], T.tabActiveBg[3], 0.7)
this.label:SetTextColor(0.7, 0.95, 1)
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:AddLine("加载此方案", 1, 1, 1)
GameTooltip:Show()
end)
loadBtn:SetScript("OnLeave", function()
this:SetBackdropColor(T.tabBg[1], T.tabBg[2], T.tabBg[3], T.tabBg[4])
this.label:SetTextColor(0.5, 0.8, 1)
GameTooltip:Hide()
end)
row.loadBtn = loadBtn
-- Delete button
local delBtn = CreateFrame("Button", nil, row)
delBtn:SetWidth(24)
delBtn:SetHeight(ROW_H - 4)
delBtn:SetPoint("RIGHT", row, "RIGHT", -2, 0)
delBtn:SetFrameLevel(row:GetFrameLevel() + 1)
SetPixelBackdrop(delBtn, { 0.35, 0.12, 0.12, 0.8 }, T.tabBorder)
local delTxt = MakeFS(delBtn, 8, "CENTER", { 1, 0.5, 0.5 })
delTxt:SetPoint("CENTER", delBtn, "CENTER", 0, 0)
delTxt:SetText("X")
delBtn.label = delTxt
delBtn:SetScript("OnEnter", function()
this:SetBackdropColor(0.5, 0.15, 0.15, 0.9)
this.label:SetTextColor(1, 0.7, 0.7)
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:AddLine("删除此方案", 1, 0.3, 0.3)
GameTooltip:Show()
end)
delBtn:SetScript("OnLeave", function()
this:SetBackdropColor(0.35, 0.12, 0.12, 0.8)
this.label:SetTextColor(1, 0.5, 0.5)
GameTooltip:Hide()
end)
row.delBtn = delBtn
gearSetPopup.rows[i] = row
end
row:SetPoint("TOPLEFT", gearSetPopup, "TOPLEFT", PAD, contentTop - (i - 1) * (ROW_H + 2))
row.nameText:SetText(names[i])
row.setName = names[i]
row.loadBtn:SetScript("OnClick", function()
CP:LoadGearSet(this:GetParent().setName)
gearSetPopup:Hide()
end)
row.delBtn:SetScript("OnClick", function()
CP:DeleteGearSet(this:GetParent().setName)
CP:ShowGearSetPopup()
end)
row:Show()
end
-- Hide extra rows
for i = numSets + 1, table.getn(gearSetPopup.rows) do
gearSetPopup.rows[i]:Hide()
end
local totalH = PAD + 16 + ROW_H + 4 + numSets * (ROW_H + 2) + PAD
if totalH < 70 then totalH = 70 end
gearSetPopup:SetHeight(totalH)
-- Position popup to the right of the panel
gearSetPopup:ClearAllPoints()
gearSetPopup:SetPoint("TOPLEFT", panel, "TOPRIGHT", 4, 0)
gearSetPopup:Show()
end
--------------------------------------------------------------------------------
-- Gear Set Name Input Dialog
--------------------------------------------------------------------------------
local gearSetNameInput
function CP:ShowGearSetNameInput()
if not panel then return end
if not gearSetNameInput then
local f = CreateFrame("Frame", "SFramesCPGearSetNameInput", UIParent)
f:SetWidth(240)
f:SetHeight(90)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
f:SetFrameStrata("DIALOG")
f:SetFrameLevel(250)
SetRoundBackdrop(f, T.bg, T.border)
f:EnableMouse(true)
f:SetMovable(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
f:Hide()
local title = MakeFS(f, 10, "CENTER", T.gold)
title:SetPoint("TOP", f, "TOP", 0, -8)
title:SetText("输入方案名称")
local editBox = CreateFrame("EditBox", "SFramesCPGearSetEditBox", f)
editBox:SetWidth(200)
editBox:SetHeight(22)
editBox:SetPoint("TOP", f, "TOP", 0, -28)
editBox:SetFontObject(ChatFontNormal)
editBox:SetAutoFocus(true)
editBox:SetMaxLetters(30)
SetPixelBackdrop(editBox, { 0.08, 0.08, 0.1, 0.9 }, T.tabBorder)
editBox:SetTextInsets(6, 6, 2, 2)
f.editBox = editBox
local okBtn = CreateFrame("Button", nil, f)
okBtn:SetWidth(80)
okBtn:SetHeight(20)
okBtn:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 20, 8)
SetPixelBackdrop(okBtn, { 0.15, 0.35, 0.15, 0.8 }, T.tabBorder)
local okTxt = MakeFS(okBtn, 9, "CENTER", { 0.6, 1, 0.6 })
okTxt:SetPoint("CENTER", okBtn, "CENTER", 0, 0)
okTxt:SetText("保存")
okBtn:SetScript("OnEnter", function()
this:SetBackdropColor(0.2, 0.45, 0.2, 0.9)
end)
okBtn:SetScript("OnLeave", function()
this:SetBackdropColor(0.15, 0.35, 0.15, 0.8)
end)
okBtn:SetScript("OnClick", function()
local name = f.editBox:GetText()
if name and name ~= "" then
CP:SaveGearSet(name)
f:Hide()
if gearSetPopup and gearSetPopup:IsShown() then
CP:ShowGearSetPopup()
end
end
end)
f.okBtn = okBtn
local cancelBtn = CreateFrame("Button", nil, f)
cancelBtn:SetWidth(80)
cancelBtn:SetHeight(20)
cancelBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -20, 8)
SetPixelBackdrop(cancelBtn, { 0.35, 0.12, 0.12, 0.8 }, T.tabBorder)
local cancelTxt = MakeFS(cancelBtn, 9, "CENTER", { 1, 0.6, 0.6 })
cancelTxt:SetPoint("CENTER", cancelBtn, "CENTER", 0, 0)
cancelTxt:SetText("取消")
cancelBtn:SetScript("OnEnter", function()
this:SetBackdropColor(0.45, 0.15, 0.15, 0.9)
end)
cancelBtn:SetScript("OnLeave", function()
this:SetBackdropColor(0.35, 0.12, 0.12, 0.8)
end)
cancelBtn:SetScript("OnClick", function()
f:Hide()
end)
f.cancelBtn = cancelBtn
editBox:SetScript("OnEnterPressed", function()
local name = this:GetText()
if name and name ~= "" then
CP:SaveGearSet(name)
f:Hide()
if gearSetPopup and gearSetPopup:IsShown() then
CP:ShowGearSetPopup()
end
end
end)
editBox:SetScript("OnEscapePressed", function()
f:Hide()
end)
gearSetNameInput = f
end
gearSetNameInput.editBox:SetText("")
gearSetNameInput:Show()
gearSetNameInput.editBox:SetFocus()
end
--------------------------------------------------------------------------------
-- Events
--------------------------------------------------------------------------------

View File

@@ -1502,6 +1502,16 @@ local function EnsureProtectedTabs(db, maxId)
tab.kind = "general"
tab.locked = true
if Trim(tab.name) == "" then tab.name = GENERAL or "General" end
-- General tab: ensure all channel alias groups default to enabled
-- except lfg/lft group which should stay off by default
if type(tab.channelFilters) ~= "table" then tab.channelFilters = {} end
local LFG_GROUP_CANONICAL = ChannelKey("lfg")
for _, group in ipairs(CHANNEL_ALIAS_GROUPS) do
local canonicalKey = ChannelKey(group[1] or "")
if canonicalKey ~= "" and canonicalKey ~= LFG_GROUP_CANONICAL and tab.channelFilters[canonicalKey] == nil then
tab.channelFilters[canonicalKey] = true
end
end
elseif i == combatIdx then
tab.kind = "combat"
tab.locked = true
@@ -1524,17 +1534,17 @@ local function EnsureDB()
if db.showBorder == nil then db.showBorder = DEFAULTS.showBorder end
if db.borderClassColor == nil then db.borderClassColor = DEFAULTS.borderClassColor end
if db.showPlayerLevel == nil then db.showPlayerLevel = DEFAULTS.showPlayerLevel end
if type(db.width) ~= "number" then db.width = DEFAULTS.width end
if type(db.height) ~= "number" then db.height = DEFAULTS.height end
if type(db.scale) ~= "number" then db.scale = DEFAULTS.scale end
if type(db.fontSize) ~= "number" then db.fontSize = DEFAULTS.fontSize end
if type(db.sidePadding) ~= "number" then db.sidePadding = DEFAULTS.sidePadding end
if type(db.topPadding) ~= "number" then db.topPadding = DEFAULTS.topPadding end
if type(db.bottomPadding) ~= "number" then db.bottomPadding = DEFAULTS.bottomPadding end
if type(db.bgAlpha) ~= "number" then db.bgAlpha = DEFAULTS.bgAlpha end
if type(db.width) ~= "number" then db.width = tonumber(db.width) or DEFAULTS.width end
if type(db.height) ~= "number" then db.height = tonumber(db.height) or DEFAULTS.height end
if type(db.scale) ~= "number" then db.scale = tonumber(db.scale) or DEFAULTS.scale end
if type(db.fontSize) ~= "number" then db.fontSize = tonumber(db.fontSize) or DEFAULTS.fontSize end
if type(db.sidePadding) ~= "number" then db.sidePadding = tonumber(db.sidePadding) or DEFAULTS.sidePadding end
if type(db.topPadding) ~= "number" then db.topPadding = tonumber(db.topPadding) or DEFAULTS.topPadding end
if type(db.bottomPadding) ~= "number" then db.bottomPadding = tonumber(db.bottomPadding) or DEFAULTS.bottomPadding end
if type(db.bgAlpha) ~= "number" then db.bgAlpha = tonumber(db.bgAlpha) or DEFAULTS.bgAlpha end
if type(db.editBoxPosition) ~= "string" then db.editBoxPosition = DEFAULTS.editBoxPosition end
if type(db.editBoxX) ~= "number" then db.editBoxX = DEFAULTS.editBoxX end
if type(db.editBoxY) ~= "number" then db.editBoxY = DEFAULTS.editBoxY end
if type(db.editBoxX) ~= "number" then db.editBoxX = tonumber(db.editBoxX) or DEFAULTS.editBoxX end
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 type(db.layoutVersion) ~= "number" then db.layoutVersion = 1 end
@@ -2076,6 +2086,9 @@ function SFrames.Chat:FindTabIndexById(tabId)
end
function SFrames.Chat:ShouldAutoTranslateForTab(index, filterKey, channelName)
if SFramesDB and SFramesDB.Chat and SFramesDB.Chat.translateEnabled == false then
return false
end
local tab = self:GetTab(index)
if not tab then return false end
if filterKey == "channel" then
@@ -3719,7 +3732,14 @@ function SFrames.Chat:EnsureConfigFrame()
end)
local filterSection = CreateCfgSection(filtersPage, "消息与频道接收", 0, -108, 584, 380, fontPath)
local filterHintText = filterSection:CreateFontString(nil, "OVERLAY")
filterHintText:SetFont(fontPath, 10, "OUTLINE")
filterHintText:SetPoint("TOPRIGHT", filterSection, "TOPRIGHT", -16, -8)
filterHintText:SetJustifyH("RIGHT")
filterHintText:SetText("勾选即等于接收该频道或类型消息")
filterHintText:SetTextColor(0.78, 0.72, 0.58)
CreateCfgButton(filterSection, "全选", 16, -26, 60, 20, function()
for _, def in ipairs(FILTER_DEFS) do
SFrames.Chat:SetActiveTabFilter(def.key, true)
@@ -4130,6 +4150,7 @@ function SFrames.Chat:EnsureConfigFrame()
end)
self:ShowConfigPage(self.configActivePage or "window")
panel:Hide()
end
function SFrames.Chat:OpenConfigFrame(pageKey)
@@ -6465,6 +6486,8 @@ function SFrames.Chat:Initialize()
end
translateEvFrame:SetScript("OnEvent", function()
if not (SFrames and SFrames.Chat) then return end
-- 翻译总开关关闭时直接跳过,避免无用开销
if SFramesDB and SFramesDB.Chat and SFramesDB.Chat.translateEnabled == false then return end
local filterKey = GetTranslateFilterKeyForEvent(event)
if not filterKey then return end
local messageText = arg1
@@ -6712,8 +6735,7 @@ function SFrames.Chat:Initialize()
if not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then
return
end
local shouldTranslate = SFrames.Chat:GetTabChannelTranslateFilter(matchedTabIdx, chanName)
if shouldTranslate then
if SFrames.Chat:ShouldAutoTranslateForTab(matchedTabIdx, "channel", chanName) then
local cleanText = CleanTextForTranslation(text)
cleanText = string.gsub(cleanText, "^%[.-%]%s*", "")
local _, _, senderName = string.find(cleanText, "^%[([^%]]+)%]:%s*")

File diff suppressed because it is too large Load Diff

183
Core.lua
View File

@@ -47,12 +47,31 @@ SFrames.eventFrame = CreateFrame("Frame", "SFramesEventFrame", UIParent)
SFrames.events = {}
function SFrames:GetIncomingHeals(unit)
-- Resolve pet unitIds: if "target" points to a pet in our group, use the
-- concrete petN / partypetN id so heal-prediction libraries can match it.
local resolvedUnit = unit
if unit == "target" or unit == "targettarget" then
if UnitExists(unit) and UnitPlayerControlled(unit) and not UnitIsPlayer(unit) then
-- It's a player-controlled non-player unit (pet). Find the real unitId.
if UnitIsUnit(unit, "pet") then
resolvedUnit = "pet"
else
for i = 1, 4 do
if UnitIsUnit(unit, "partypet" .. i) then
resolvedUnit = "partypet" .. i
break
end
end
end
end
end
-- Source 1: ShaguTweaks libpredict
if ShaguTweaks and ShaguTweaks.libpredict and ShaguTweaks.libpredict.UnitGetIncomingHeals then
local lp = ShaguTweaks.libpredict
if lp.UnitGetIncomingHealsBreakdown then
local ok, total, mine, others = pcall(function()
return lp:UnitGetIncomingHealsBreakdown(unit, UnitName("player"))
return lp:UnitGetIncomingHealsBreakdown(resolvedUnit, UnitName("player"))
end)
if ok then
return math.max(0, tonumber(total) or 0),
@@ -60,7 +79,7 @@ function SFrames:GetIncomingHeals(unit)
math.max(0, tonumber(others) or 0)
end
end
local ok, amount = pcall(function() return lp:UnitGetIncomingHeals(unit) end)
local ok, amount = pcall(function() return lp:UnitGetIncomingHeals(resolvedUnit) end)
if ok then
amount = math.max(0, tonumber(amount) or 0)
return amount, 0, amount
@@ -70,7 +89,7 @@ function SFrames:GetIncomingHeals(unit)
if AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("HealComm-1.0") then
local ok, HC = pcall(function() return AceLibrary("HealComm-1.0") end)
if ok and HC and HC.getHeal then
local name = UnitName(unit)
local name = UnitName(resolvedUnit)
if name then
local total = HC:getHeal(name) or 0
total = math.max(0, tonumber(total) or 0)
@@ -124,8 +143,127 @@ end
-- Addon Loaded Initializer
SFrames:RegisterEvent("PLAYER_LOGIN", function()
SFrames:Initialize()
SFrames:SaveCharProfile()
end)
SFrames:RegisterEvent("PLAYER_LOGOUT", function()
SFrames:SaveCharProfile()
end)
function SFrames:GetCharKey()
local name = UnitName("player") or "Unknown"
local realm = GetRealmName and GetRealmName() or "Unknown"
return realm, name
end
function SFrames:SaveCharProfile()
if not SFramesDB then return end
if not SFramesGlobalDB then SFramesGlobalDB = {} end
if not SFramesGlobalDB.CharProfiles then SFramesGlobalDB.CharProfiles = {} end
local realm, name = self:GetCharKey()
if not SFramesGlobalDB.CharProfiles[realm] then SFramesGlobalDB.CharProfiles[realm] = {} end
local snapshot = {}
for k, v in pairs(SFramesDB) do
snapshot[k] = v
end
snapshot._savedAt = time and time() or 0
snapshot._class = select and select(2, UnitClass("player")) or "UNKNOWN"
if UnitClass then
local _, classEn = UnitClass("player")
snapshot._class = classEn or "UNKNOWN"
end
snapshot._level = UnitLevel and UnitLevel("player") or 0
-- Nanami-QT
if QuickToolboxDB then
snapshot._qtDB = QuickToolboxDB
end
if QuickToolboxSharedDB then
snapshot._qtSharedDB = QuickToolboxSharedDB
end
-- Nanami-Plates
if NanamiPlatesDB then
snapshot._platesDB = NanamiPlatesDB
end
-- Nanami-DPS
if NanamiDPS_DB then
snapshot._dpsDB = NanamiDPS_DB
end
SFramesGlobalDB.CharProfiles[realm][name] = snapshot
end
function SFrames:GetAllCharProfiles()
if not SFramesGlobalDB or not SFramesGlobalDB.CharProfiles then return {} end
local list = {}
for realm, chars in pairs(SFramesGlobalDB.CharProfiles) do
for charName, data in pairs(chars) do
table.insert(list, {
realm = realm,
name = charName,
class = data._class or "UNKNOWN",
level = data._level or 0,
savedAt = data._savedAt or 0,
data = data,
})
end
end
return list
end
function SFrames:ApplyCharProfile(profileData)
if not profileData or type(profileData) ~= "table" then return false end
for k, v in pairs(profileData) do
if k ~= "_savedAt" and k ~= "_class" and k ~= "_level" and k ~= "Positions"
and k ~= "_qtDB" and k ~= "_qtSharedDB" and k ~= "_platesDB" and k ~= "_dpsDB" then
SFramesDB[k] = v
end
end
-- Nanami-QT
if profileData._qtDB and type(profileData._qtDB) == "table" then
if not QuickToolboxDB then QuickToolboxDB = {} end
for k, v in pairs(profileData._qtDB) do
QuickToolboxDB[k] = v
end
end
if profileData._qtSharedDB and type(profileData._qtSharedDB) == "table" then
if not QuickToolboxSharedDB then QuickToolboxSharedDB = {} end
for k, v in pairs(profileData._qtSharedDB) do
QuickToolboxSharedDB[k] = v
end
end
-- Nanami-Plates
if profileData._platesDB and type(profileData._platesDB) == "table" then
if not NanamiPlatesDB then NanamiPlatesDB = {} end
for k, v in pairs(profileData._platesDB) do
NanamiPlatesDB[k] = v
end
end
-- Nanami-DPS
if profileData._dpsDB and type(profileData._dpsDB) == "table" then
if not NanamiDPS_DB then NanamiDPS_DB = {} end
for k, v in pairs(profileData._dpsDB) do
NanamiDPS_DB[k] = v
end
end
return true
end
function SFrames:DeleteCharProfile(realm, name)
if not SFramesGlobalDB or not SFramesGlobalDB.CharProfiles then return end
if SFramesGlobalDB.CharProfiles[realm] then
SFramesGlobalDB.CharProfiles[realm][name] = nil
end
end
function SFrames:SafeInit(name, initFn)
local ok, err = pcall(initFn)
if not ok then
@@ -208,7 +346,6 @@ function SFrames:DoFullInitialize()
deferFrame:SetScript("OnUpdate", function()
if idx > table.getn(deferred) then
this:SetScript("OnUpdate", nil)
SFrames:Print("所有模块加载完成 =^_^=")
return
end
local batchEnd = idx + batchSize - 1
@@ -421,6 +558,35 @@ function SFrames:InitSlashCommands()
else
SFrames:Print("StatSummary module unavailable.")
end
elseif cmd == "iteminfo" or cmd == "ii" then
-- 打印鼠标悬停物品或指定itemID的GetItemInfo全部返回值
local itemId = tonumber(args)
local link
if itemId and itemId > 0 then
link = "item:" .. itemId .. ":0:0:0:0:0:0:0"
else
local ttName = GameTooltip:GetName()
local left1 = ttName and _G[ttName .. "TextLeft1"]
local name = left1 and left1:GetText()
if name and name ~= "" and GetItemLinkByName then
link = GetItemLinkByName(name)
end
if not link then
SFrames:Print("用法: /nui iteminfo <itemID> 或悬停物品后输入 /nui iteminfo")
return
end
end
-- 打印所有返回值(不预设数量,逐个检查)
local results = { GetItemInfo(link) }
SFrames:Print("|cff88ccff=== GetItemInfo 结果 ===|r")
SFrames:Print(" link: " .. tostring(link))
SFrames:Print(" 返回值数量: " .. table.getn(results))
for i = 1, table.getn(results) do
SFrames:Print(" [" .. i .. "] " .. tostring(results[i]))
end
if table.getn(results) == 0 then
SFrames:Print(" |cffff4444(无返回值,物品可能不在本地缓存中)|r")
end
elseif cmd == "afk" then
if SFrames.AFKScreen and SFrames.AFKScreen.Toggle then
SFrames.AFKScreen:Toggle()
@@ -567,11 +733,13 @@ function SFrames:InitSlashCommands()
DEFAULT_CHAT_FRAME:AddMessage(" |cffffd100" .. name .. "|r (用户自定义)")
end
end
elseif cmd == "profile" then
if SFrames.ConfigUI and SFrames.ConfigUI.Build then SFrames.ConfigUI:Build("profile") end
elseif cmd == "config" or cmd == "" then
if SFrames.ConfigUI and SFrames.ConfigUI.Build then SFrames.ConfigUI:Build("ui") end
else
local hex = SFrames.Theme and SFrames.Theme:GetAccentHex() or "ffffb3d9"
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r Commands: /nui, /nui ui, /nui bags, /nui chat, /nui layout, /nui unlock, /nui lock, /nui test, /nui partyh, /nui partyv, /nui focushelp, /nui mapreveal, /nui mapscan, /nui stats, /nui afk, /nui pin, /nui bind, /nui keybinds")
DEFAULT_CHAT_FRAME:AddMessage("|c" .. hex .. "[Nanami-UI]|r Commands: /nui, /nui ui, /nui bags, /nui chat, /nui layout, /nui unlock, /nui lock, /nui test, /nui partyh, /nui partyv, /nui focushelp, /nui mapreveal, /nui mapscan, /nui stats, /nui afk, /nui pin, /nui bind, /nui keybinds, /nui profile")
end
end
end
@@ -633,6 +801,11 @@ function SFrames:HideBlizzardFrames()
TargetFrame:Hide()
TargetFrame.Show = function() end
end
-- 禁用原版目标框体右键菜单
if TargetFrameDropDown then
TargetFrameDropDown:Hide()
TargetFrameDropDown.initialize = function() end
end
if ComboFrame then
ComboFrame:UnregisterAllEvents()
ComboFrame:Hide()

1631
Focus.lua

File diff suppressed because it is too large Load Diff

View File

@@ -96,8 +96,12 @@ local function GetDB()
alertEnable = true,
alertFadeDelay = ALERT_HOLD,
scale = 1.0,
followCursor = false,
}
end
if SFramesDB.lootDisplay.followCursor == nil then
SFramesDB.lootDisplay.followCursor = false
end
return SFramesDB.lootDisplay
end
@@ -585,21 +589,30 @@ local function UpdateLootFrame()
ShowLootPage()
if not lootFrame._posApplied then
local hasSaved = false
if SFrames.Movers and SFrames.Movers.ApplyPosition then
hasSaved = SFrames.Movers:ApplyPosition("LootFrame", lootFrame,
"TOPLEFT", "UIParent", "TOPLEFT", 50, -200)
end
if hasSaved then
lootFrame._posApplied = true
end
end
if not lootFrame._posApplied then
if db.followCursor then
-- 跟随鼠标:每次打开都定位到鼠标附近
local cx, cy = GetCursorPosition()
local uiS = UIParent:GetEffectiveScale()
lootFrame:ClearAllPoints()
lootFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", cx / uiS - 30, cy / uiS + 16)
else
-- 固定位置:使用保存的位置或默认位置
if not lootFrame._posApplied then
local hasSaved = false
if SFrames.Movers and SFrames.Movers.ApplyPosition then
hasSaved = SFrames.Movers:ApplyPosition("LootFrame", lootFrame,
"TOPLEFT", "UIParent", "TOPLEFT", 50, -200)
end
if hasSaved then
lootFrame._posApplied = true
end
end
if not lootFrame._posApplied then
local cx, cy = GetCursorPosition()
local uiS = UIParent:GetEffectiveScale()
lootFrame:ClearAllPoints()
lootFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", cx / uiS - 30, cy / uiS + 16)
end
end
lootFrame:Show()

235
Mail.lua
View File

@@ -10,6 +10,9 @@ SFrames = SFrames or {}
SFrames.Mail = {}
local ML = SFrames.Mail
SFramesDB = SFramesDB or {}
if SFramesDB.mailContacts == nil then
SFramesDB.mailContacts = {}
end
-- Save original Blizzard functions BEFORE other addons (TurtleMail etc.) hook them.
-- File-level locals are captured at load time, which is before later-alphabetical addons.
@@ -219,6 +222,8 @@ local function CreateActionBtn(parent, text, w)
end
end)
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
return btn
end
@@ -1223,13 +1228,178 @@ local function BuildSendPanel()
local labelW = 50
local ebW = L.W - L.PAD * 2 - labelW - 6
local toLabel = sp:CreateFontString(nil, "OVERLAY")
toLabel:SetFont(font, 11, "OUTLINE"); toLabel:SetPoint("TOPLEFT", sp, "TOPLEFT", L.PAD, -6)
toLabel:SetFont(font, 11, "OUTLINE"); toLabel:SetPoint("TOPLEFT", sp, "TOPLEFT", L.PAD, -32)
toLabel:SetWidth(labelW); toLabel:SetJustifyH("RIGHT")
toLabel:SetText("收件人:"); toLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3])
local toEB = CreateStyledEditBox(sp, ebW, 22)
toEB:SetPoint("LEFT", toLabel, "RIGHT", 6, 0)
f.toEditBox = toEB
-- Contact buttons
local addContactBtn = CreateActionBtn(sp, "添加", 30)
addContactBtn:SetHeight(22); addContactBtn:SetPoint("TOPRIGHT", sp, "TOPRIGHT", -L.PAD, -6)
addContactBtn:SetScript("OnClick", function()
local name = f.toEditBox:GetText()
if name and name ~= "" then
if not SFramesDB then
SFramesDB = {}
end
if not SFramesDB.mailContacts then
SFramesDB.mailContacts = {}
end
SFramesDB.mailContacts[name] = true
DEFAULT_CHAT_FRAME:AddMessage("|cFF66FF88[Nanami-Mail]|r 已添加常用联系人: " .. name)
end
end)
-- Contact management frame
local contactFrame
local function CreateContactFrame()
if contactFrame then return end
contactFrame = CreateFrame("Frame", "SFramesMailContactFrame", UIParent)
if not contactFrame then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 创建联系人管理窗口失败")
return
end
contactFrame:SetWidth(250)
contactFrame:SetHeight(300)
contactFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
contactFrame: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 }
})
contactFrame:SetBackdropColor(0.1, 0.1, 0.1, 0.95)
contactFrame:SetBackdropBorderColor(0.5, 0.5, 0.5, 1)
contactFrame:SetFrameStrata("DIALOG")
contactFrame:SetMovable(true)
contactFrame:EnableMouse(true)
contactFrame:RegisterForDrag("LeftButton")
contactFrame:SetScript("OnDragStart", function() this:StartMoving() end)
contactFrame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
contactFrame:Hide()
-- Title
local title = contactFrame:CreateFontString(nil, "OVERLAY")
title:SetFont(GetFont(), 12, "OUTLINE")
title:SetPoint("TOP", contactFrame, "TOP", 0, -10)
title:SetText("|cFFFFCC00常用联系人管理|r")
-- Close button
local closeBtn = CreateActionBtn(contactFrame, "X", 20)
closeBtn:SetHeight(20)
closeBtn:SetPoint("TOPRIGHT", contactFrame, "TOPRIGHT", -5, -5)
closeBtn:SetScript("OnClick", function() contactFrame:Hide() end)
-- Scroll frame
local scrollFrame = CreateFrame("ScrollFrame", nil, contactFrame)
if not scrollFrame then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 创建滚动框架失败")
return
end
scrollFrame:SetWidth(230)
scrollFrame:SetHeight(240)
scrollFrame:SetPoint("TOPLEFT", contactFrame, "TOPLEFT", 10, -30)
local scrollChild = CreateFrame("Frame", nil, scrollFrame)
if not scrollChild then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 创建滚动子框架失败")
return
end
scrollChild:SetWidth(210)
scrollChild:SetHeight(1)
scrollFrame:SetScrollChild(scrollChild)
-- Refresh function
contactFrame.Refresh = function()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.mailContacts then SFramesDB.mailContacts = {} end
-- Clear existing buttons
for _, child in ipairs({scrollChild:GetChildren()}) do
child:Hide()
child:SetParent(nil)
child = nil
end
-- Create contact buttons
local contacts = SFramesDB.mailContacts
local y = 0
for name in pairs(contacts) do
local contactBtn = CreateActionBtn(scrollChild, name, 180)
contactBtn:SetHeight(20)
contactBtn:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 0, -y)
contactBtn.contactName = name
contactBtn:SetScript("OnClick", function()
if this.contactName and f.toEditBox then
f.toEditBox:SetText(this.contactName)
contactFrame:Hide()
end
end)
local deleteBtn = CreateActionBtn(scrollChild, "×", 20)
deleteBtn:SetHeight(20)
deleteBtn:SetPoint("LEFT", contactBtn, "RIGHT", 5, 0)
deleteBtn.contactName = name
deleteBtn:SetScript("OnClick", function()
if this.contactName then
SFramesDB.mailContacts[this.contactName] = nil
contactFrame:Refresh()
end
end)
y = y + 25
end
scrollChild:SetHeight(math.max(1, y))
-- No contacts message
if not next(contacts) then
local noContacts = scrollChild:CreateFontString(nil, "OVERLAY")
noContacts:SetFont(GetFont(), 11, "OUTLINE")
noContacts:SetPoint("TOP", scrollChild, "TOP", 0, -10)
noContacts:SetText("|cFF999999常用联系人列表为空|r")
end
end
-- Add contact section
local addLabel = contactFrame:CreateFontString(nil, "OVERLAY")
addLabel:SetFont(GetFont(), 11, "OUTLINE")
addLabel:SetPoint("BOTTOMLEFT", contactFrame, "BOTTOMLEFT", 10, 40)
addLabel:SetText("添加新联系人:")
local addEditBox = CreateStyledEditBox(contactFrame, 150, 20)
addEditBox:SetPoint("BOTTOMLEFT", contactFrame, "BOTTOMLEFT", 10, 15)
local addBtn = CreateActionBtn(contactFrame, "添加", 60)
addBtn:SetHeight(20)
addBtn:SetPoint("BOTTOMRIGHT", contactFrame, "BOTTOMRIGHT", -10, 15)
addBtn:SetScript("OnClick", function()
local name = addEditBox:GetText()
if name and name ~= "" then
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.mailContacts then SFramesDB.mailContacts = {} end
SFramesDB.mailContacts[name] = true
addEditBox:SetText("")
contactFrame:Refresh()
end
end)
end
local manageContactBtn = CreateActionBtn(sp, "管理", 30)
manageContactBtn:SetHeight(22); manageContactBtn:SetPoint("RIGHT", addContactBtn, "LEFT", -4, 0)
manageContactBtn:SetScript("OnClick", function()
CreateContactFrame()
if contactFrame then
contactFrame:Refresh()
contactFrame:Show()
else
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 无法创建联系人管理窗口")
end
end)
-- Autocomplete dropdown for recipient
local AC_MAX = 8
local acBox = CreateFrame("Frame", "SFramesMailAutoComplete", f)
@@ -1297,6 +1467,13 @@ local function BuildSendPanel()
seen[name] = true
end
-- Add mail contacts first
if SFramesDB and SFramesDB.mailContacts then
for name in pairs(SFramesDB.mailContacts) do
addName(name, "contact")
end
end
for fi = 1, GetNumFriends() do
local name = GetFriendInfo(fi)
addName(name, "friend")
@@ -1318,7 +1495,9 @@ local function BuildSendPanel()
local r = results[ai]
local col = T.whoColor
local tag = ""
if r.source == "friend" then
if r.source == "contact" then
col = { 1, 1, 0.3 }; tag = " |cFFFFCC00[常用]|r"
elseif r.source == "friend" then
col = T.friendColor; tag = " |cFF66FF88[好友]|r"
elseif r.source == "guild" then
col = T.guildColor; tag = " |cFF66CCFF[公会]|r"
@@ -1343,7 +1522,7 @@ local function BuildSendPanel()
-- Subject (second line, aligned with recipient)
local subLabel = sp:CreateFontString(nil, "OVERLAY")
subLabel:SetFont(font, 11, "OUTLINE"); subLabel:SetPoint("TOPLEFT", sp, "TOPLEFT", L.PAD, -32)
subLabel:SetFont(font, 11, "OUTLINE"); subLabel:SetPoint("TOPLEFT", sp, "TOPLEFT", L.PAD, -60)
subLabel:SetWidth(labelW); subLabel:SetJustifyH("RIGHT")
subLabel:SetText("主题:"); subLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3])
local subEB = CreateStyledEditBox(sp, ebW, 22)
@@ -1355,7 +1534,7 @@ local function BuildSendPanel()
-- Body
local bodyLabel = sp:CreateFontString(nil, "OVERLAY")
bodyLabel:SetFont(font, 11, "OUTLINE"); bodyLabel:SetPoint("TOPLEFT", sp, "TOPLEFT", L.PAD, -58)
bodyLabel:SetFont(font, 11, "OUTLINE"); bodyLabel:SetPoint("TOPLEFT", sp, "TOPLEFT", L.PAD, -88)
bodyLabel:SetText("正文:"); bodyLabel:SetTextColor(T.labelText[1], T.labelText[2], T.labelText[3])
local bsf = CreateFrame("ScrollFrame", "SFramesMailBodyScroll", sp, "UIPanelScrollFrameTemplate")
@@ -1620,6 +1799,54 @@ function ML:ShowSendPanel()
ML:UpdateSendPanel()
end
--------------------------------------------------------------------------------
-- Slash commands for mail contacts
--------------------------------------------------------------------------------
SLASH_MAILCONTACT1 = "/mailcontact"
SLASH_MAILCONTACT2 = "/mailcontacts"
SlashCmdList["MAILCONTACT"] = function(msg)
if not SFramesDB then
SFramesDB = {}
end
if not SFramesDB.mailContacts then
SFramesDB.mailContacts = {}
end
local cmd, arg = string.match(msg, "^(%S*)%s*(.-)$")
cmd = string.lower(cmd)
if cmd == "add" and arg ~= "" then
SFramesDB.mailContacts[arg] = true
DEFAULT_CHAT_FRAME:AddMessage("|cFF66FF88[Nanami-Mail]|r 已添加常用联系人: " .. arg)
elseif cmd == "remove" and arg ~= "" then
if SFramesDB.mailContacts[arg] then
SFramesDB.mailContacts[arg] = nil
DEFAULT_CHAT_FRAME:AddMessage("|cFF66FF88[Nanami-Mail]|r 已删除常用联系人: " .. arg)
else
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF6666[Nanami-Mail]|r 常用联系人中不存在: " .. arg)
end
elseif cmd == "list" then
local contacts = SFramesDB.mailContacts
if next(contacts) then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFCC00[Nanami-Mail]|r 常用联系人列表:")
for name in pairs(contacts) do
DEFAULT_CHAT_FRAME:AddMessage("|cFF66FF88- |r" .. name)
end
else
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFCC00[Nanami-Mail]|r 常用联系人列表为空")
end
elseif cmd == "clear" then
SFramesDB.mailContacts = {}
DEFAULT_CHAT_FRAME:AddMessage("|cFF66FF88[Nanami-Mail]|r 已清空常用联系人列表")
else
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFCC00[Nanami-Mail]|r 常用联系人命令:")
DEFAULT_CHAT_FRAME:AddMessage("|cFF66FF88/mailcontact add <name>|r - 添加常用联系人")
DEFAULT_CHAT_FRAME:AddMessage("|cFF66FF88/mailcontact remove <name>|r - 删除常用联系人")
DEFAULT_CHAT_FRAME:AddMessage("|cFF66FF88/mailcontact list|r - 查看常用联系人列表")
DEFAULT_CHAT_FRAME:AddMessage("|cFF66FF88/mailcontact clear|r - 清空常用联系人列表")
end
end
--------------------------------------------------------------------------------
-- Bootstrap
--------------------------------------------------------------------------------

View File

@@ -333,5 +333,4 @@ function MI:Initialize()
UpdateMinimapDots()
end)
SFrames:Print("地图职业图标模块已加载")
end

View File

@@ -10,6 +10,23 @@ SFrames.Media = {
fontOutline = "OUTLINE",
}
-- Built-in font choices (includes Nanami-Plates bundled fonts)
SFrames.FontChoices = {
{ key = "default", label = "默认 (ARIALN)", path = "Fonts\\ARIALN.TTF" },
{ key = "frizqt", label = "FRIZQT", path = "Fonts\\FRIZQT__.TTF" },
{ key = "morpheus", label = "Morpheus", path = "Fonts\\MORPHEUS.TTF" },
{ key = "skurri", label = "Skurri", path = "Fonts\\SKURRI.TTF" },
{ key = "np_handwrite", label = "手写DF+DB", path = "Interface\\AddOns\\Nanami-Plates\\fonts\\手写DF+DB.ttf" },
{ key = "np_pangwa", label = "胖娃方正英文数字", path = "Interface\\AddOns\\Nanami-Plates\\fonts\\胖娃方正英文数字.ttf" },
{ key = "np_yahei_star", label = "雅黑+一星球", path = "Interface\\AddOns\\Nanami-Plates\\fonts\\雅黑+一星球.ttf" },
{ key = "np_yahei_hand", label = "雅黑手写英文", path = "Interface\\AddOns\\Nanami-Plates\\fonts\\雅黑手写英文.ttf" },
}
SFrames._fontLookup = {}
for _, entry in ipairs(SFrames.FontChoices) do
SFrames._fontLookup[entry.key] = entry.path
end
function SFrames:GetSharedMedia()
if LibStub then
local ok, LSM = pcall(function() return LibStub("LibSharedMedia-3.0", true) end)
@@ -18,8 +35,24 @@ function SFrames:GetSharedMedia()
return nil
end
SFrames.BarTextures = {
{ key = "default", label = "默认", path = "Interface\\TargetingFrame\\UI-StatusBar" },
{ key = "bar_elvui", label = "ElvUI", path = "Interface\\AddOns\\Nanami-UI\\img\\bar\\bar_elvui.tga" },
{ key = "bar_gradient", label = "渐变", path = "Interface\\AddOns\\Nanami-UI\\img\\bar\\bar_gradient.tga" },
{ key = "bar_tukui", label = "TukUI", path = "Interface\\AddOns\\Nanami-UI\\img\\bar\\bar_tukui.tga" },
{ key = "flat", label = "纯色", path = "Interface\\Buttons\\WHITE8X8" },
}
SFrames._barTextureLookup = {}
for _, entry in ipairs(SFrames.BarTextures) do
SFrames._barTextureLookup[entry.key] = entry.path
end
function SFrames:GetTexture()
if SFramesDB and SFramesDB.barTexture then
local builtin = self._barTextureLookup[SFramesDB.barTexture]
if builtin then return builtin end
local LSM = self:GetSharedMedia()
if LSM then
local path = LSM:Fetch("statusbar", SFramesDB.barTexture, true)
@@ -30,6 +63,12 @@ function SFrames:GetTexture()
end
function SFrames:GetFont()
-- 1. Check built-in font key
if SFramesDB and SFramesDB.fontKey then
local builtin = self._fontLookup[SFramesDB.fontKey]
if builtin then return builtin end
end
-- 2. Fallback: LibSharedMedia font name
if SFramesDB and SFramesDB.fontName then
local LSM = self:GetSharedMedia()
if LSM then

View File

@@ -109,7 +109,8 @@ end
local function ApplySlotBackdrop(btn, isBuff)
btn:SetBackdrop(ROUND_BACKDROP)
btn:SetBackdropColor(0.06, 0.06, 0.08, 0.92)
local bgA = (SFramesDB and SFramesDB.MinimapBuffs and type(SFramesDB.MinimapBuffs.bgAlpha) == "number") and SFramesDB.MinimapBuffs.bgAlpha or 0.92
btn:SetBackdropColor(0.06, 0.06, 0.08, bgA)
if isBuff then
btn:SetBackdropBorderColor(0.25, 0.25, 0.30, 1)
else

View File

@@ -35,7 +35,6 @@ Units\ToT.lua
Units\Party.lua
TalentDefaultDB.lua
Units\TalentTree.lua
SellPriceDB.lua
GearScore.lua
Tooltip.lua
Units\Raid.lua

View File

@@ -143,6 +143,7 @@ local function ForwardScrollWheel(frame)
local p = this:GetParent()
while p do
if p:GetObjectType() == "ScrollFrame" then
if p.UpdateScrollChildRect then p:UpdateScrollChildRect() end
local cur = p:GetVerticalScroll()
local maxVal = p:GetVerticalScrollRange()
if arg1 > 0 then
@@ -208,8 +209,16 @@ local function CreateScrollArea(parent, name)
content:SetHeight(1)
scroll:SetScrollChild(content)
-- Hook content:SetHeight to auto-refresh scroll range
local origSetHeight = content.SetHeight
content.SetHeight = function(self, h)
origSetHeight(self, h)
if scroll.UpdateScrollChildRect then scroll:UpdateScrollChildRect() end
end
scroll:EnableMouseWheel(true)
scroll:SetScript("OnMouseWheel", function()
if this.UpdateScrollChildRect then this:UpdateScrollChildRect() end
local cur = this:GetVerticalScroll()
local maxVal = this:GetVerticalScrollRange()
if arg1 > 0 then
@@ -705,6 +714,9 @@ local function ShowPage(name)
if pages[name] then
pages[name]:Show()
if pages[name].scroll then
if pages[name].scroll.UpdateScrollChildRect then
pages[name].scroll:UpdateScrollChildRect()
end
pages[name].scroll:SetVerticalScroll(0)
end
end
@@ -1555,6 +1567,19 @@ function QUI:Initialize()
end
end
end
-- Deferred scroll refresh: re-layout after 1 frame so FontString heights are valid
if MainFrame._deferredPage then
MainFrame._deferredTick = (MainFrame._deferredTick or 0) + 1
if MainFrame._deferredTick >= 2 then
local pg = pages[MainFrame._deferredPage]
if pg and pg.Update then pg.Update() end
if pg and pg.scroll and pg.scroll.UpdateScrollChildRect then
pg.scroll:UpdateScrollChildRect()
end
MainFrame._deferredPage = nil
MainFrame._deferredTick = nil
end
end
end)
MainFrame:Hide()
@@ -1578,6 +1603,7 @@ function QUI:Initialize()
pages.gossip.Update()
UpdateBottomButtons()
MainFrame:Show()
MainFrame._deferredPage = "gossip"; MainFrame._deferredTick = 0
elseif event == "GOSSIP_CLOSED" then
if currentPage == "gossip" then
@@ -1593,6 +1619,7 @@ function QUI:Initialize()
pages.greeting.Update()
UpdateBottomButtons()
MainFrame:Show()
MainFrame._deferredPage = "greeting"; MainFrame._deferredTick = 0
elseif event == "QUEST_DETAIL" then
pendingClose = false
@@ -1601,6 +1628,7 @@ function QUI:Initialize()
pages.detail.Update()
UpdateBottomButtons()
MainFrame:Show()
MainFrame._deferredPage = "detail"; MainFrame._deferredTick = 0
elseif event == "QUEST_PROGRESS" then
pendingClose = false
@@ -1609,6 +1637,7 @@ function QUI:Initialize()
pages.progress.Update()
UpdateBottomButtons()
MainFrame:Show()
MainFrame._deferredPage = "progress"; MainFrame._deferredTick = 0
elseif event == "QUEST_COMPLETE" then
pendingClose = false
@@ -1618,6 +1647,7 @@ function QUI:Initialize()
pages.complete.Update()
UpdateBottomButtons()
MainFrame:Show()
MainFrame._deferredPage = "complete"; MainFrame._deferredTick = 0
elseif event == "QUEST_FINISHED" then
if previousPage then

View File

@@ -1,4 +1,4 @@
-- Nanami-UI Embedded Sell Price Database
-- Nanami-UI Embedded Sell Price Database
-- Merged: ShaguTweaks Turtle WoW (priority) + Informant + SellValue wowhead data
-- [itemID] = vendorSellPrice (in copper)
-- Total entries: 23853

View File

@@ -832,6 +832,19 @@ local function BuildOptions(f, pageY)
function() return SFramesDB.spellBookAutoReplace == true end,
function(v) SFramesDB.spellBookAutoReplace = v end
)
f.optMouseover = MakeCheckOption(f, "鼠标指向施法", L.SIDE_PAD + 280, optY,
function()
return SFramesDB.Tweaks and SFramesDB.Tweaks.mouseoverCast == true
end,
function(v)
if not SFramesDB.Tweaks then SFramesDB.Tweaks = {} end
SFramesDB.Tweaks.mouseoverCast = v
if SFrames.Tweaks and SFrames.Tweaks.SetMouseoverCast then
SFrames.Tweaks:SetMouseoverCast(v)
end
end
)
end
--------------------------------------------------------------------------------

View File

@@ -357,7 +357,7 @@ local function BuildPanel()
f:SetWidth(PANEL_W)
f:SetHeight(PANEL_H)
f:SetPoint("CENTER", UIParent, "CENTER", 200, 0)
f:SetFrameStrata("DIALOG")
f:SetFrameStrata("HIGH")
f:SetFrameLevel(100)
f:EnableMouse(true)
f:SetMovable(true)

View File

@@ -682,6 +682,41 @@ function SFrames.FloatingTooltip:FormatLines(tooltip)
end
end
--------------------------------------------------------------------------
-- Armor & Resistances for non-player units
--------------------------------------------------------------------------
if not UnitIsPlayer(unit) and UnitExists(unit) then
local armor = UnitResistance(unit, 0) or 0
if armor > 0 then
tooltip:AddLine(" ")
tooltip:AddLine(string.format("护甲: %d", armor), 0.78, 0.61, 0.43)
end
local resInfo = {
{ 2, "火焰", 1.0, 0.3, 0.3 },
{ 3, "自然", 0.3, 1.0, 0.3 },
{ 4, "冰霜", 0.3, 0.3, 1.0 },
{ 5, "暗影", 0.6, 0.2, 0.9 },
{ 6, "奥术", 1.0, 1.0, 1.0 },
{ 1, "神圣", 1.0, 0.9, 0.5 },
}
local hasRes = false
local resLine = ""
for i = 1, table.getn(resInfo) do
local ri = resInfo[i]
local val = UnitResistance(unit, ri[1]) or 0
if val > 0 then
if hasRes then resLine = resLine .. " " end
resLine = resLine .. ri[2] .. ":" .. val
hasRes = true
end
end
if hasRes then
if armor <= 0 then tooltip:AddLine(" ") end
tooltip:AddLine("抗性: " .. resLine, 0.7, 0.7, 0.75)
end
end
--------------------------------------------------------------------------
-- Target of mouseover (all units)
--------------------------------------------------------------------------
@@ -817,7 +852,7 @@ local function IC_AppendItemLevel(tooltip, link)
end
--------------------------------------------------------------------------------
-- Sell Price Infrastructure
-- Sell Price Infrastructure (GetItemInfo API + runtime cache)
--------------------------------------------------------------------------------
local function IC_GetItemIdFromLink(link)
if not link then return nil end
@@ -835,26 +870,44 @@ end
local function IC_GetSellPrice(itemId)
if not itemId then return nil end
if NanamiSellPriceDB then
local price = NanamiSellPriceDB[itemId]
if price and price > 0 then return price end
end
if ShaguTweaks and ShaguTweaks.SellValueDB then
local price = ShaguTweaks.SellValueDB[itemId]
if price and price > 0 then return price end
end
-- Runtime cache (persisted in SavedVariables)
if SFramesGlobalDB and SFramesGlobalDB.sellPriceCache then
local price = SFramesGlobalDB.sellPriceCache[itemId]
if price and price > 0 then return price end
end
-- Fallback: aux addon
if aux and aux.account_data and aux.account_data.merchant_sell then
local price = aux.account_data.merchant_sell[itemId]
if price and price > 0 then return price end
end
-- Fallback: ShaguTweaks
if ShaguTweaks and ShaguTweaks.SellValueDB then
local price = ShaguTweaks.SellValueDB[itemId]
if price and price > 0 then return price end
end
return nil
end
local function IC_TryGetItemInfoSellPrice(link)
---------------------------------------------------------------------------
-- GetItemInfo API: query sell price and learn/cache it
-- Returns: sellPrice (copper) or nil
---------------------------------------------------------------------------
local function IC_QueryAndLearnPrice(link)
if not link then return nil end
local r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11 = GetItemInfo(link)
if r11 and type(r11) == "number" and r11 > 0 then
return r11
local itemId = IC_GetItemIdFromLink(link)
-- Check cache first
local cached = IC_GetSellPrice(itemId)
if cached then return cached end
-- Query via GetItemInfo (11th return = sellPrice)
local name, _, quality, ilvl, minLvl, itemType, subType, stackCount, equipLoc, texture, sellPrice = GetItemInfo(link)
if name then
DEFAULT_CHAT_FRAME:AddMessage(
"|cff88ccff[Nanami-SellPrice]|r GetItemInfo(" .. (itemId or "?") .. ") = "
.. (name or "nil") .. ", sellPrice=" .. tostring(sellPrice or "nil"))
end
if sellPrice and type(sellPrice) == "number" and sellPrice > 0 then
if itemId then IC_CacheSellPrice(itemId, sellPrice) end
return sellPrice
end
return nil
end
@@ -880,12 +933,7 @@ local function IC_AddSellPrice(tooltip, link, count)
if not link then return end
if tooltip._nanamiSellPriceAdded then return end
if MerchantFrame and MerchantFrame:IsShown() then return end
local itemId = IC_GetItemIdFromLink(link)
local price = IC_GetSellPrice(itemId)
if not price then
price = IC_TryGetItemInfoSellPrice(link)
if price and itemId then IC_CacheSellPrice(itemId, price) end
end
local price = IC_QueryAndLearnPrice(link)
if price and price > 0 then
count = count or 1
if count < 1 then count = 1 end
@@ -954,7 +1002,7 @@ function IC:HookTooltips()
orig_SetTooltipMoney(frame, money, a1, a2, a3)
end
if IC_PendingBagLink and money and money > 0 then
if frame == GameTooltip or frame == SFramesScanTooltip then
if frame == GameTooltip then
local itemId = IC_GetItemIdFromLink(IC_PendingBagLink)
local count = IC_PendingBagCount or 1
if count < 1 then count = 1 end
@@ -966,7 +1014,8 @@ function IC:HookTooltips()
end
---------------------------------------------------------------------------
-- MERCHANT_SHOW: proactively scan all bag items to cache sell prices.
-- MERCHANT_SHOW: proactively scan all bag items to learn sell prices
-- via GetItemInfo API and cache them.
---------------------------------------------------------------------------
local scanFrame = CreateFrame("Frame")
scanFrame:RegisterEvent("MERCHANT_SHOW")
@@ -976,26 +1025,7 @@ function IC:HookTooltips()
for slot = 1, numSlots do
local link = GetContainerItemLink(bag, slot)
if link then
local itemId = IC_GetItemIdFromLink(link)
if itemId and not IC_GetSellPrice(itemId) then
local infoPrice = IC_TryGetItemInfoSellPrice(link)
if infoPrice then
IC_CacheSellPrice(itemId, infoPrice)
elseif SFramesScanTooltip then
local _, cnt = GetContainerItemInfo(bag, slot)
cnt = cnt or 1
IC_PendingBagLink = link
IC_PendingBagCount = cnt
pcall(function()
SFramesScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFramesScanTooltip:ClearLines()
SFramesScanTooltip:SetBagItem(bag, slot)
SFramesScanTooltip:Hide()
end)
IC_PendingBagLink = nil
IC_PendingBagCount = nil
end
end
IC_QueryAndLearnPrice(link)
end
end
end
@@ -1248,11 +1278,7 @@ function IC:HookTooltips()
if itemStr then
ItemRefTooltip._nanamiSellPriceAdded = nil
local itemId = IC_GetItemIdFromLink(itemStr)
local price = IC_GetSellPrice(itemId)
if not price then
price = IC_TryGetItemInfoSellPrice(itemStr)
if price and itemId then IC_CacheSellPrice(itemId, price) end
end
local price = IC_QueryAndLearnPrice(itemStr)
if price and price > 0 and not ItemRefTooltip.hasMoney then
SetTooltipMoney(ItemRefTooltip, price)
ItemRefTooltip:Show()

View File

@@ -37,18 +37,18 @@ local function ColorToQuality(r, g, b)
end
--------------------------------------------------------------------------------
-- Layout
-- Layout (Left-Right structure like TradeSkill frame)
--------------------------------------------------------------------------------
local FRAME_W = 380
local FRAME_H = 540
local FRAME_W = 620
local FRAME_H = 480
local HEADER_H = 34
local SIDE_PAD = 14
local CONTENT_W = FRAME_W - SIDE_PAD * 2
local SIDE_PAD = 10
local FILTER_H = 28
local LIST_ROW_H = 32
local LIST_W = 320
local DETAIL_W = FRAME_W - LIST_W - SIDE_PAD * 3
local LIST_ROW_H = 28
local CAT_ROW_H = 20
local DETAIL_H = 160
local BOTTOM_H = 52
local BOTTOM_H = 48
local SCROLL_STEP = 40
local MAX_ROWS = 60
@@ -61,6 +61,7 @@ local currentFilter = "all"
local displayList = {}
local rowButtons = {}
local collapsedCats = {}
local isTradeskillTrainerCached = false -- Cache to avoid repeated API calls
local function HideBlizzardTrainer()
if not ClassTrainerFrame then return end
ClassTrainerFrame:SetScript("OnHide", function() end)
@@ -152,6 +153,58 @@ local function IsServiceHeader(index)
return false
end
--------------------------------------------------------------------------------
-- Enhanced category detection for better filtering
-- Trust Blizzard API for class trainers (considers spell rank requirements)
-- Only do extra verification for tradeskill trainers
--------------------------------------------------------------------------------
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"
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"
end
local scanTip = nil
local function GetServiceTooltipInfo(index)
@@ -350,7 +403,7 @@ end
--------------------------------------------------------------------------------
local function CreateListRow(parent, idx)
local row = CreateFrame("Button", nil, parent)
row:SetWidth(CONTENT_W)
row:SetWidth(LIST_W - 30) -- Account for scrollbar
row:SetHeight(LIST_ROW_H)
local iconFrame = CreateFrame("Frame", nil, row)
@@ -365,6 +418,7 @@ local function CreateListRow(parent, idx)
})
iconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
iconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
iconFrame:EnableMouse(false) -- Don't block mouse events
row.iconFrame = iconFrame
local icon = iconFrame:CreateTexture(nil, "ARTWORK")
@@ -384,16 +438,17 @@ local function CreateListRow(parent, idx)
row.qualGlow = qualGlow
local nameFS = row:CreateFontString(nil, "OVERLAY")
nameFS:SetFont(GetFont(), 12, "OUTLINE")
nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 6, 2)
nameFS:SetFont(GetFont(), 11, "OUTLINE")
nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 6, 5)
nameFS:SetPoint("RIGHT", row, "RIGHT", -90, 0)
nameFS:SetJustifyH("LEFT")
nameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
row.nameFS = nameFS
local subFS = row:CreateFontString(nil, "OVERLAY")
subFS:SetFont(GetFont(), 10, "OUTLINE")
subFS:SetPoint("TOPLEFT", nameFS, "BOTTOMLEFT", 0, -1)
subFS:SetFont(GetFont(), 9, "OUTLINE")
subFS:SetPoint("LEFT", iconFrame, "RIGHT", 6, -6)
subFS:SetPoint("RIGHT", row, "RIGHT", -90, 0)
subFS:SetJustifyH("LEFT")
subFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
row.subFS = subFS
@@ -414,6 +469,13 @@ local function CreateListRow(parent, idx)
costMoney:UnregisterAllEvents()
costMoney:SetScript("OnEvent", nil)
costMoney:SetScript("OnShow", nil)
costMoney:EnableMouse(false) -- Don't block mouse events
-- Disable mouse on child buttons too
local moneyName = "SFTRow" .. idx .. "Money"
for _, suffix in ipairs({"GoldButton", "SilverButton", "CopperButton"}) do
local child = _G[moneyName .. suffix]
if child and child.EnableMouse then child:EnableMouse(false) end
end
costMoney.moneyType = nil
costMoney.hasPickup = nil
costMoney.small = 1
@@ -542,12 +604,18 @@ local function CreateListRow(parent, idx)
self.icon:SetVertexColor(T.passive[1], T.passive[2], T.passive[3])
end
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)
-- 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])
@@ -574,6 +642,21 @@ local function CreateListRow(parent, idx)
self:Hide()
end
-- Enable mouse wheel scrolling on rows to pass through to scrollbar
row:EnableMouseWheel(true)
row:SetScript("OnMouseWheel", function()
local scrollBar = MainFrame and MainFrame.scrollBar
if scrollBar then
local cur = scrollBar:GetValue()
local min, max = scrollBar:GetMinMaxValues()
if arg1 > 0 then
scrollBar:SetValue(math.max(min, cur - SCROLL_STEP))
else
scrollBar:SetValue(math.min(max, cur + SCROLL_STEP))
end
end
end)
return row
end
@@ -586,8 +669,8 @@ local function BuildDisplayList()
if numServices == 0 then return end
local currentCat = nil
local catServices = {}
local categories = {}
-- Each category stores three buckets: available, used, unavailable
local catBuckets = {}
local catOrder = {}
for i = 1, numServices do
@@ -596,30 +679,28 @@ local function BuildDisplayList()
local isHdr = IsServiceHeader(i)
if isHdr then
currentCat = name
if not catServices[name] then
catServices[name] = {}
if not catBuckets[name] then
catBuckets[name] = { available = {}, used = {}, unavailable = {} }
table.insert(catOrder, name)
end
else
if not currentCat then
currentCat = "技能"
if not catServices[currentCat] then
catServices[currentCat] = {}
if not catBuckets[currentCat] then
catBuckets[currentCat] = { available = {}, used = {}, unavailable = {} }
table.insert(catOrder, currentCat)
end
end
local show = false
if currentFilter == "all" then
show = true
elseif currentFilter == (category or "") then
show = true
end
-- Use verified category for more accurate filtering
local cat = GetVerifiedCategory(i) or "unavailable"
local show = (currentFilter == "all") or (currentFilter == cat)
if show then
table.insert(catServices[currentCat], {
local bucket = catBuckets[currentCat][cat] or catBuckets[currentCat]["unavailable"]
table.insert(bucket, {
index = i,
name = name,
subText = subText or "",
category = category or "unavailable",
category = cat,
})
end
end
@@ -629,8 +710,9 @@ local function BuildDisplayList()
local hasCats = table.getn(catOrder) > 1
for _, catName in ipairs(catOrder) do
local svcs = catServices[catName]
if table.getn(svcs) > 0 then
local buckets = catBuckets[catName]
local totalCount = table.getn(buckets.available) + table.getn(buckets.used) + table.getn(buckets.unavailable)
if totalCount > 0 then
if hasCats then
table.insert(displayList, {
type = "header",
@@ -639,23 +721,19 @@ local function BuildDisplayList()
})
end
if not collapsedCats[catName] then
for _, svc in ipairs(svcs) do
table.insert(displayList, {
type = "service",
data = svc,
})
-- available → used → unavailable
for _, svc in ipairs(buckets.available) do
table.insert(displayList, { type = "service", data = svc })
end
for _, svc in ipairs(buckets.used) do
table.insert(displayList, { type = "service", data = svc })
end
for _, svc in ipairs(buckets.unavailable) do
table.insert(displayList, { type = "service", data = svc })
end
end
end
end
if table.getn(catOrder) <= 1 and table.getn(displayList) == 0 then
local allCat = catOrder[1] or "技能"
local svcs = catServices[allCat] or {}
for _, svc in ipairs(svcs) do
table.insert(displayList, { type = "service", data = svc })
end
end
end
--------------------------------------------------------------------------------
@@ -712,6 +790,18 @@ local function UpdateList()
end
content:SetHeight(math.max(1, y))
-- Update scrollbar range
if MainFrame.scrollBar then
local visibleHeight = MainFrame.listScroll:GetHeight() or 1
local maxScroll = math.max(0, y - visibleHeight)
MainFrame.scrollBar:SetMinMaxValues(0, maxScroll)
if maxScroll <= 0 then
MainFrame.scrollBar:Hide()
else
MainFrame.scrollBar:Show()
end
end
end
local function UpdateDetail()
@@ -734,7 +824,8 @@ local function UpdateDetail()
return
end
local name, subText, category = GetTrainerServiceInfo(selectedIndex)
local name, subText, _ = GetTrainerServiceInfo(selectedIndex)
local category = GetVerifiedCategory(selectedIndex)
local iconTex = GetTrainerServiceIcon and GetTrainerServiceIcon(selectedIndex)
local ok, cost = pcall(GetTrainerServiceCost, selectedIndex)
if not ok then cost = 0 end
@@ -904,57 +995,133 @@ function TUI:Initialize()
headerSep:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -6, -HEADER_H)
headerSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
-- Filter bar
-- Filter bar (above left list)
local filterBar = CreateFrame("Frame", nil, MainFrame)
filterBar:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -(HEADER_H + 4))
filterBar:SetPoint("TOPRIGHT", MainFrame, "TOPRIGHT", -SIDE_PAD, -(HEADER_H + 4))
filterBar:SetWidth(LIST_W)
filterBar:SetHeight(FILTER_H)
local fAll = CreateFilterBtn(filterBar, "全部", 52)
fAll:SetPoint("LEFT", filterBar, "LEFT", 0, 0)
fAll:SetScript("OnClick", function() currentFilter = "all"; FullUpdate() end)
MainFrame.filterAll = fAll
local fAvail = CreateFilterBtn(filterBar, "可学习", 60)
fAvail:SetPoint("LEFT", fAll, "RIGHT", 4, 0)
local fAvail = CreateFilterBtn(filterBar, "可学习", 55)
fAvail:SetPoint("LEFT", filterBar, "LEFT", 0, 0)
fAvail:SetScript("OnClick", function() currentFilter = "available"; FullUpdate() end)
MainFrame.filterAvail = fAvail
local fUnavail = CreateFilterBtn(filterBar, "不可学", 60)
fUnavail:SetPoint("LEFT", fAvail, "RIGHT", 4, 0)
local fUnavail = CreateFilterBtn(filterBar, "不可学", 55)
fUnavail:SetPoint("LEFT", fAvail, "RIGHT", 2, 0)
fUnavail:SetScript("OnClick", function() currentFilter = "unavailable"; FullUpdate() end)
MainFrame.filterUnavail = fUnavail
local fUsed = CreateFilterBtn(filterBar, "已学会", 60)
fUsed:SetPoint("LEFT", fUnavail, "RIGHT", 4, 0)
local fUsed = CreateFilterBtn(filterBar, "已学会", 55)
fUsed:SetPoint("LEFT", fUnavail, "RIGHT", 2, 0)
fUsed:SetScript("OnClick", function() currentFilter = "used"; FullUpdate() end)
MainFrame.filterUsed = fUsed
-- Scrollable list area
local listTop = HEADER_H + FILTER_H + 8
local listBottom = DETAIL_H + BOTTOM_H + 8
local fAll = CreateFilterBtn(filterBar, "全部", 45)
fAll:SetPoint("LEFT", fUsed, "RIGHT", 2, 0)
fAll:SetScript("OnClick", function() currentFilter = "all"; FullUpdate() end)
MainFrame.filterAll = fAll
local listScroll = CreateFrame("ScrollFrame", "SFramesTrainerListScroll", MainFrame)
listScroll:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -listTop)
listScroll:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, listBottom)
-- Left side: Scrollable list area
local listTop = HEADER_H + FILTER_H + 8
local listBg = CreateFrame("Frame", nil, MainFrame)
listBg:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD, -listTop)
listBg:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, BOTTOM_H + 4)
listBg:SetWidth(LIST_W)
listBg: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 },
})
listBg:SetBackdropColor(0, 0, 0, 0.3)
listBg:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.5)
-- Scrollbar
local scrollBar = CreateFrame("Slider", "SFramesTrainerScrollBar", listBg)
scrollBar:SetWidth(16)
scrollBar:SetPoint("TOPRIGHT", listBg, "TOPRIGHT", -4, -4)
scrollBar:SetPoint("BOTTOMRIGHT", listBg, "BOTTOMRIGHT", -4, 4)
scrollBar:SetOrientation("VERTICAL")
scrollBar:SetThumbTexture("Interface\\Buttons\\UI-ScrollBar-Knob")
scrollBar:SetMinMaxValues(0, 1)
scrollBar:SetValue(0)
scrollBar:SetValueStep(SCROLL_STEP)
scrollBar:EnableMouseWheel(true)
local scrollBg = scrollBar:CreateTexture(nil, "BACKGROUND")
scrollBg:SetAllPoints()
scrollBg:SetTexture(0, 0, 0, 0.3)
local scrollUp = CreateFrame("Button", nil, scrollBar)
scrollUp:SetWidth(16); scrollUp:SetHeight(16)
scrollUp:SetPoint("BOTTOM", scrollBar, "TOP", 0, 0)
scrollUp:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up")
scrollUp:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Down")
scrollUp:SetDisabledTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Disabled")
scrollUp:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Highlight")
scrollUp:SetScript("OnClick", function()
local val = scrollBar:GetValue()
scrollBar:SetValue(val - SCROLL_STEP)
end)
local scrollDown = CreateFrame("Button", nil, scrollBar)
scrollDown:SetWidth(16); scrollDown:SetHeight(16)
scrollDown:SetPoint("TOP", scrollBar, "BOTTOM", 0, 0)
scrollDown:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up")
scrollDown:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Down")
scrollDown:SetDisabledTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Disabled")
scrollDown:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Highlight")
scrollDown:SetScript("OnClick", function()
local val = scrollBar:GetValue()
scrollBar:SetValue(val + SCROLL_STEP)
end)
local listScroll = CreateFrame("ScrollFrame", "SFramesTrainerListScroll", listBg)
listScroll:SetPoint("TOPLEFT", listBg, "TOPLEFT", 4, -4)
listScroll:SetPoint("BOTTOMRIGHT", scrollBar, "BOTTOMLEFT", -2, 0)
local listContent = CreateFrame("Frame", "SFramesTrainerListContent", listScroll)
listContent:SetWidth(CONTENT_W)
listContent:SetWidth(LIST_W - 30)
listContent:SetHeight(1)
listScroll:SetScrollChild(listContent)
listScroll:EnableMouseWheel(true)
listScroll:SetScript("OnMouseWheel", function()
local cur = this:GetVerticalScroll()
local maxVal = this:GetVerticalScrollRange()
-- Sync scrollbar with scroll frame
scrollBar:SetScript("OnValueChanged", function()
listScroll:SetVerticalScroll(this:GetValue())
end)
scrollBar:SetScript("OnMouseWheel", function()
local cur = this:GetValue()
local min, max = this:GetMinMaxValues()
if arg1 > 0 then
this:SetVerticalScroll(math.max(0, cur - SCROLL_STEP))
this:SetValue(math.max(min, cur - SCROLL_STEP))
else
this:SetVerticalScroll(math.min(maxVal, cur + SCROLL_STEP))
this:SetValue(math.min(max, cur + SCROLL_STEP))
end
end)
-- Mouse wheel on list area
local function OnListMouseWheel()
local cur = scrollBar:GetValue()
local min, max = scrollBar:GetMinMaxValues()
if arg1 > 0 then
scrollBar:SetValue(math.max(min, cur - SCROLL_STEP))
else
scrollBar:SetValue(math.min(max, cur + SCROLL_STEP))
end
end
listBg:EnableMouseWheel(true)
listBg:SetScript("OnMouseWheel", OnListMouseWheel)
listScroll:EnableMouseWheel(true)
listScroll:SetScript("OnMouseWheel", OnListMouseWheel)
listScroll.content = listContent
MainFrame.listScroll = listScroll
MainFrame.listBg = listBg
MainFrame.scrollBar = scrollBar
for i = 1, MAX_ROWS do
local row = CreateListRow(listContent, i)
@@ -968,24 +1135,24 @@ function TUI:Initialize()
rowButtons[i] = row
end
-- Detail separator
local detailSep = MainFrame:CreateTexture(nil, "ARTWORK")
detailSep:SetTexture("Interface\\Buttons\\WHITE8X8")
detailSep:SetHeight(1)
detailSep:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", 6, DETAIL_H + BOTTOM_H + 4)
detailSep:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -6, DETAIL_H + BOTTOM_H + 4)
detailSep:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], T.divider[4])
-- Right side: Detail panel
local detailPanel = CreateFrame("Frame", nil, MainFrame)
detailPanel:SetPoint("TOPLEFT", MainFrame, "TOPLEFT", SIDE_PAD + LIST_W + SIDE_PAD, -listTop)
detailPanel:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, BOTTOM_H + 4)
detailPanel: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 },
})
detailPanel:SetBackdropColor(0, 0, 0, 0.3)
detailPanel:SetBackdropBorderColor(T.panelBorder[1], T.panelBorder[2], T.panelBorder[3], 0.5)
MainFrame.detail = detailPanel
-- Detail area
local detail = CreateFrame("Frame", nil, MainFrame)
detail:SetPoint("BOTTOMLEFT", MainFrame, "BOTTOMLEFT", SIDE_PAD, BOTTOM_H + 4)
detail:SetPoint("BOTTOMRIGHT", MainFrame, "BOTTOMRIGHT", -SIDE_PAD, BOTTOM_H + 4)
detail:SetHeight(DETAIL_H)
MainFrame.detail = detail
local dIconFrame = CreateFrame("Frame", nil, detail)
dIconFrame:SetWidth(42); dIconFrame:SetHeight(42)
dIconFrame:SetPoint("TOPLEFT", detail, "TOPLEFT", 2, -6)
-- Icon in detail panel
local dIconFrame = CreateFrame("Frame", nil, detailPanel)
dIconFrame:SetWidth(48); dIconFrame:SetHeight(48)
dIconFrame:SetPoint("TOPLEFT", detailPanel, "TOPLEFT", 10, -10)
dIconFrame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
@@ -995,55 +1162,61 @@ function TUI:Initialize()
dIconFrame:SetBackdropColor(T.slotBg[1], T.slotBg[2], T.slotBg[3], T.slotBg[4])
dIconFrame:SetBackdropBorderColor(T.slotBorder[1], T.slotBorder[2], T.slotBorder[3], T.slotBorder[4])
dIconFrame:Hide()
detail.iconFrame = dIconFrame
detailPanel.iconFrame = dIconFrame
local dIcon = dIconFrame:CreateTexture(nil, "ARTWORK")
dIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
dIcon:SetPoint("TOPLEFT", dIconFrame, "TOPLEFT", 3, -3)
dIcon:SetPoint("BOTTOMRIGHT", dIconFrame, "BOTTOMRIGHT", -3, 3)
detail.icon = dIcon
detailPanel.icon = dIcon
local dNameFS = detail:CreateFontString(nil, "OVERLAY")
dNameFS:SetFont(GetFont(), 13, "OUTLINE")
dNameFS:SetPoint("TOPLEFT", dIconFrame, "TOPRIGHT", 8, -2)
dNameFS:SetPoint("RIGHT", detail, "RIGHT", -4, 0)
-- Name next to icon
local dNameFS = detailPanel:CreateFontString(nil, "OVERLAY")
dNameFS:SetFont(GetFont(), 14, "OUTLINE")
dNameFS:SetPoint("TOPLEFT", dIconFrame, "TOPRIGHT", 8, -4)
dNameFS:SetPoint("RIGHT", detailPanel, "RIGHT", -10, 0)
dNameFS:SetJustifyH("LEFT")
dNameFS:SetTextColor(T.nameText[1], T.nameText[2], T.nameText[3])
detail.nameFS = dNameFS
detailPanel.nameFS = dNameFS
local dSubFS = detail:CreateFontString(nil, "OVERLAY")
-- Subtext (rank)
local dSubFS = detailPanel:CreateFontString(nil, "OVERLAY")
dSubFS:SetFont(GetFont(), 11, "OUTLINE")
dSubFS:SetPoint("TOPLEFT", dNameFS, "BOTTOMLEFT", 0, -2)
dSubFS:SetJustifyH("LEFT")
dSubFS:SetTextColor(T.dimText[1], T.dimText[2], T.dimText[3])
detail.subFS = dSubFS
detailPanel.subFS = dSubFS
local dReqFS = detail:CreateFontString(nil, "OVERLAY")
-- Requirements
local dReqFS = detailPanel:CreateFontString(nil, "OVERLAY")
dReqFS:SetFont(GetFont(), 11, "OUTLINE")
dReqFS:SetPoint("TOPLEFT", dIconFrame, "BOTTOMLEFT", 0, -6)
dReqFS:SetPoint("RIGHT", detail, "RIGHT", -4, 0)
dReqFS:SetPoint("TOPLEFT", dIconFrame, "BOTTOMLEFT", 0, -10)
dReqFS:SetPoint("RIGHT", detailPanel, "RIGHT", -10, 0)
dReqFS:SetJustifyH("LEFT")
detail.reqFS = dReqFS
detailPanel.reqFS = dReqFS
local dInfoFS = detail:CreateFontString(nil, "OVERLAY")
-- Spell info
local dInfoFS = detailPanel:CreateFontString(nil, "OVERLAY")
dInfoFS:SetFont(GetFont(), 11)
dInfoFS:SetPoint("TOPLEFT", dReqFS, "BOTTOMLEFT", 0, -4)
dInfoFS:SetPoint("RIGHT", detail, "RIGHT", -4, 0)
dInfoFS:SetPoint("TOPLEFT", dReqFS, "BOTTOMLEFT", 0, -6)
dInfoFS:SetPoint("RIGHT", detailPanel, "RIGHT", -10, 0)
dInfoFS:SetJustifyH("LEFT")
dInfoFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3])
detail.infoFS = dInfoFS
detailPanel.infoFS = dInfoFS
local descDivider = detail:CreateTexture(nil, "ARTWORK")
-- Divider line
local descDivider = detailPanel:CreateTexture(nil, "ARTWORK")
descDivider:SetTexture("Interface\\Buttons\\WHITE8X8")
descDivider:SetHeight(1)
descDivider:SetPoint("TOPLEFT", dInfoFS, "BOTTOMLEFT", 0, -5)
descDivider:SetPoint("RIGHT", detail, "RIGHT", -4, 0)
descDivider:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.3)
detail.descDivider = descDivider
descDivider:SetPoint("TOPLEFT", dInfoFS, "BOTTOMLEFT", 0, -8)
descDivider:SetPoint("RIGHT", detailPanel, "RIGHT", -10, 0)
descDivider:SetVertexColor(T.divider[1], T.divider[2], T.divider[3], 0.5)
detailPanel.descDivider = descDivider
local descScroll = CreateFrame("ScrollFrame", nil, detail)
descScroll:SetPoint("TOPLEFT", descDivider, "BOTTOMLEFT", 0, -4)
descScroll:SetPoint("BOTTOMRIGHT", detail, "BOTTOMRIGHT", -4, 2)
-- Description scroll area
local descScroll = CreateFrame("ScrollFrame", nil, detailPanel)
descScroll:SetPoint("TOPLEFT", descDivider, "BOTTOMLEFT", 0, -6)
descScroll:SetPoint("BOTTOMRIGHT", detailPanel, "BOTTOMRIGHT", -10, 8)
descScroll:EnableMouseWheel(true)
descScroll:SetScript("OnMouseWheel", function()
local cur = this:GetVerticalScroll()
@@ -1054,20 +1227,20 @@ function TUI:Initialize()
this:SetVerticalScroll(math.min(maxVal, cur + 14))
end
end)
detail.descScroll = descScroll
detailPanel.descScroll = descScroll
local descContent = CreateFrame("Frame", nil, descScroll)
descContent:SetWidth(CONTENT_W - 8)
descContent:SetWidth(DETAIL_W - 20)
descContent:SetHeight(1)
descScroll:SetScrollChild(descContent)
local dDescFS = descContent:CreateFontString(nil, "OVERLAY")
dDescFS:SetFont(GetFont(), 11)
dDescFS:SetPoint("TOPLEFT", descContent, "TOPLEFT", 0, 0)
dDescFS:SetWidth(CONTENT_W - 8)
dDescFS:SetWidth(DETAIL_W - 20)
dDescFS:SetJustifyH("LEFT")
dDescFS:SetTextColor(T.bodyText[1], T.bodyText[2], T.bodyText[3])
detail.descFS = dDescFS
detailPanel.descFS = dDescFS
-- Bottom bar
local bottomSep = MainFrame:CreateTexture(nil, "ARTWORK")
@@ -1125,8 +1298,9 @@ function TUI:Initialize()
local numServices = GetNumTrainerServices and GetNumTrainerServices() or 0
local gold = GetMoney()
for i = 1, numServices do
local name, _, category = GetTrainerServiceInfo(i)
if name and category == "available" and not IsServiceHeader(i) then
local name = GetTrainerServiceInfo(i)
local cat = GetVerifiedCategory(i)
if name and cat == "available" and not IsServiceHeader(i) then
local ok, cost = pcall(GetTrainerServiceCost, i)
if ok and cost and gold >= cost then
pcall(BuyTrainerService, i)
@@ -1192,11 +1366,15 @@ function TUI:Initialize()
end
selectedIndex = nil
currentFilter = "all"
currentFilter = "available"
collapsedCats = {}
qualityCache = {}
-- Cache tradeskill trainer status once (avoid repeated API calls)
isTradeskillTrainerCached = IsTradeskillTrainer and IsTradeskillTrainer() or false
local npcName = UnitName("npc") or "训练师"
if IsTradeskillTrainer and IsTradeskillTrainer() then
if isTradeskillTrainerCached then
npcName = npcName .. " - 专业训练"
end
MainFrame.npcNameFS:SetText(npcName)

View File

@@ -7,18 +7,21 @@
-- 5. Dark UI - darken the entire interface
-- 6. WorldMap Window - turn fullscreen map into a movable/scalable window
-- 7. Hunter Aspect Guard - cancel Cheetah/Pack when taking damage in combat (avoid OOC false positives)
-- 8. Mouseover Cast - cast on mouseover unit without changing target
--------------------------------------------------------------------------------
SFrames.Tweaks = SFrames.Tweaks or {}
local Tweaks = SFrames.Tweaks
SFrames.castdb = SFrames.castdb or {}
SFrames.guidToName = SFrames.guidToName or {}
SFrames.castByName = SFrames.castByName or {} -- [unitName] = { spell, start, casttime, icon, channel }
local function GetTweaksCfg()
if not SFramesDB or type(SFramesDB.Tweaks) ~= "table" then
return { autoStance = true, superWoW = true, turtleCompat = true,
cooldownNumbers = true, darkUI = false, worldMapWindow = false,
hunterAspectGuard = true }
hunterAspectGuard = true, mouseoverCast = false }
end
return SFramesDB.Tweaks
end
@@ -104,6 +107,49 @@ local function InitSuperWoW()
castdb[guid].icon = icon
castdb[guid].channel = (arg3 == "CHANNEL") or false
-- Build GUID -> name mapping and castByName
-- Try UnitName(guid) directly (SuperWoW allows GUID as unitID)
local castName
local ok_n, gname = pcall(UnitName, guid)
if ok_n and gname and gname ~= "" and gname ~= UNKNOWN then
castName = gname
SFrames.guidToName[guid] = gname
end
-- Fallback: scan known unitIDs with UnitGUID
if not castName and UnitGUID then
local scanUnits = {"target","targettarget","party1","party2","party3","party4",
"party1target","party2target","party3target","party4target"}
local rn = GetNumRaidMembers and GetNumRaidMembers() or 0
for ri = 1, rn do
table.insert(scanUnits, "raid"..ri)
table.insert(scanUnits, "raid"..ri.."target")
end
for _, u in ipairs(scanUnits) do
local ok1, exists = pcall(UnitExists, u)
if ok1 and exists then
local ok2, ug = pcall(UnitGUID, u)
if ok2 and ug and ug == guid then
local ok3, uname = pcall(UnitName, u)
if ok3 and uname then
castName = uname
SFrames.guidToName[guid] = uname
end
break
end
end
end
end
-- Write castByName for name-based consumers (Focus castbar)
if castName then
SFrames.castByName[castName] = {
cast = spell,
start = GetTime(),
casttime = arg5,
icon = icon,
channel = (arg3 == "CHANNEL") or false,
}
end
SFrames.superwow_active = true
elseif arg3 == "FAIL" then
local guid = arg1
@@ -115,6 +161,15 @@ local function InitSuperWoW()
castdb[guid].icon = nil
castdb[guid].channel = nil
end
-- Clear castByName
local failName = SFrames.guidToName[guid]
if not failName then
local ok_n, gname = pcall(UnitName, guid)
if ok_n and gname and gname ~= "" and gname ~= UNKNOWN then failName = gname end
end
if failName and SFrames.castByName[failName] then
SFrames.castByName[failName] = nil
end
end
end)
end
@@ -1074,6 +1129,185 @@ local function InitHunterAspectGuard()
end)
end
--------------------------------------------------------------------------------
-- Combat Background Notify
-- Flash the Windows taskbar icon when entering combat (UnitXP SP3 required).
--------------------------------------------------------------------------------
local function InitCombatNotify()
if not (type(UnitXP) == "function" and pcall(UnitXP, "nop", "nop")) then return end
local f = CreateFrame("Frame", "NanamiCombatNotify")
f:RegisterEvent("PLAYER_REGEN_DISABLED")
f:SetScript("OnEvent", function()
pcall(UnitXP, "notify", "taskbarIcon")
end)
end
--------------------------------------------------------------------------------
-- Mouseover Cast
-- When enabled, action bar presses and CastSpellByName calls will target the
-- 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.
--------------------------------------------------------------------------------
local mouseoverCastEnabled = false
local origUseAction = nil
local origCastSpellByName = nil
local inMouseoverAction = false -- re-entrancy guard
local function GetMouseoverUnit()
local focus = GetMouseFocus and GetMouseFocus()
if focus then
if focus.unit and UnitExists(focus.unit) then
return focus.unit
end
local parent = focus:GetParent()
if parent and parent.unit and UnitExists(parent.unit) then
return parent.unit
end
end
if UnitExists("mouseover") then
return "mouseover"
end
return nil
end
local function MouseoverUseAction(action, cursor, onSelf)
-- Don't interfere: picking up action, or re-entrant call
if cursor == 1 or inMouseoverAction then
return origUseAction(action, cursor, onSelf)
end
local moUnit = GetMouseoverUnit()
if not moUnit then
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
-- 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)
-- 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
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)
end
else
-- Had no target before, clear
ClearTarget()
end
inMouseoverAction = false
end
local function MouseoverCastSpellByName(spell, arg2)
-- Already has explicit target arg, pass through
if arg2 then
return origCastSpellByName(spell, arg2)
end
-- Re-entrancy guard (e.g. called from within MouseoverUseAction's chain)
if inMouseoverAction then
return origCastSpellByName(spell)
end
local moUnit = GetMouseoverUnit()
if not moUnit then
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
-- No SuperWoW: target-swap
local hadTarget = UnitExists("target")
local prevTargetName = hadTarget and UnitName("target") or nil
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
SpellStopTargeting()
end
if hadTarget and prevTargetName then
TargetLastTarget()
if not UnitExists("target") or UnitName("target") ~= prevTargetName then
TargetByName(prevTargetName, true)
end
elseif not hadTarget then
ClearTarget()
end
end
local function InitMouseoverCast()
if UseAction then
origUseAction = UseAction
UseAction = MouseoverUseAction
end
if CastSpellByName then
origCastSpellByName = CastSpellByName
CastSpellByName = MouseoverCastSpellByName
end
mouseoverCastEnabled = true
DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami] 鼠标指向施法已启用" ..
(SUPERWOW_VERSION and "SuperWoW 模式)" or "(目标切换模式)") .. "|r")
end
function Tweaks:SetMouseoverCast(enabled)
if enabled and not mouseoverCastEnabled then
InitMouseoverCast()
elseif not enabled and mouseoverCastEnabled then
if origUseAction then UseAction = origUseAction end
if origCastSpellByName then CastSpellByName = origCastSpellByName end
mouseoverCastEnabled = false
DEFAULT_CHAT_FRAME:AddMessage("|cff88ccff[Nanami] 鼠标指向施法已关闭|r")
end
end
--------------------------------------------------------------------------------
-- Module API
--------------------------------------------------------------------------------
@@ -1131,6 +1365,20 @@ function Tweaks:Initialize()
end
end
if cfg.combatNotify ~= false then
local ok, err = pcall(InitCombatNotify)
if not ok then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: CombatNotify init failed: " .. tostring(err) .. "|r")
end
end
if cfg.mouseoverCast then
local ok, err = pcall(InitMouseoverCast)
if not ok then
DEFAULT_CHAT_FRAME:AddMessage("|cffff4444Nanami-UI: MouseoverCast init failed: " .. tostring(err) .. "|r")
end
end
if cfg.darkUI then
local ok, err = pcall(InitDarkUI)
if not ok then

View File

@@ -129,6 +129,17 @@ function SFrames.Party:ApplyFrameStyle(frame, metrics)
frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 1, -1)
end
local bgA = (SFramesDB and type(SFramesDB.partyBgAlpha) == "number") and SFramesDB.partyBgAlpha or 0.9
local A = SFrames.ActiveTheme
if A and A.panelBg and bgA < 0.89 then
if frame.pbg and frame.pbg.SetBackdropColor then
frame.pbg:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], bgA)
end
if frame.healthBGFrame and frame.healthBGFrame.SetBackdropColor then
frame.healthBGFrame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], bgA)
end
end
if frame.power then
frame.power:ClearAllPoints()
frame.power:SetPoint("TOPLEFT", frame.health, "BOTTOMLEFT", 0, -1)
@@ -302,6 +313,10 @@ function SFrames.Party:Initialize()
end)
f:SetScript("OnClick", function()
if arg1 == "LeftButton" then
if IsShiftKeyDown() and SFrames.Focus and SFrames.Focus.SetFromUnit then
pcall(SFrames.Focus.SetFromUnit, SFrames.Focus, this.unit)
return
end
if TryDropCursorOnUnit(this.unit) then
return
end
@@ -324,10 +339,12 @@ function SFrames.Party:Initialize()
end
end)
f:SetScript("OnEnter", function()
if SetMouseoverUnit then SetMouseoverUnit(this.unit) end
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit(this.unit)
end)
f:SetScript("OnLeave", function()
if SetMouseoverUnit then SetMouseoverUnit() end
GameTooltip:Hide()
end)
f.unit = unit
@@ -376,13 +393,21 @@ function SFrames.Party:Initialize()
f.health.healPredMine = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredMine:SetTexture(SFrames:GetTexture())
f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78)
f.health.healPredMine:SetDrawLayer("OVERLAY", 7)
f.health.healPredMine:Hide()
f.health.healPredOther = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredOther:SetTexture(SFrames:GetTexture())
f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5)
f.health.healPredOther:SetDrawLayer("OVERLAY", 7)
f.health.healPredOther:Hide()
f.health.healPredOver = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredOver:SetTexture(SFrames:GetTexture())
f.health.healPredOver:SetVertexColor(1.0, 0.3, 0.3, 0.6)
f.health.healPredOver:SetDrawLayer("OVERLAY", 7)
f.health.healPredOver:Hide()
-- Power Bar
f.power = SFrames:CreateStatusBar(f, "SFramesPartyFrame"..i.."Power")
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
@@ -470,10 +495,14 @@ function SFrames.Party:Initialize()
pf:RegisterForClicks("LeftButtonUp", "RightButtonUp")
pf:SetScript("OnClick", function() TargetUnit(this.unit) end)
pf:SetScript("OnEnter", function()
if SetMouseoverUnit then SetMouseoverUnit(this.unit) end
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit(this.unit)
end)
pf:SetScript("OnLeave", function() GameTooltip:Hide() end)
pf:SetScript("OnLeave", function()
if SetMouseoverUnit then SetMouseoverUnit() end
GameTooltip:Hide()
end)
pf:Hide()
f.petFrame = pf
@@ -882,14 +911,16 @@ function SFrames.Party:UpdateHealPrediction(unit)
local data = self:GetFrameByUnit(unit)
if not data then return end
local f = data.frame
if not (f.health and f.health.healPredMine and f.health.healPredOther) then return end
if not (f.health and f.health.healPredMine and f.health.healPredOther and f.health.healPredOver) then return end
local predMine = f.health.healPredMine
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
@@ -899,14 +930,39 @@ function SFrames.Party:UpdateHealPrediction(unit)
local hp = UnitHealth(unit) or 0
local maxHp = UnitHealthMax(unit) or 0
if maxHp <= 0 or hp >= maxHp then
if CheckSuperWow then
local ok, hasSW = pcall(CheckSuperWow)
if ok and hasSW then
local ok2, realHp = pcall(UnitHealth, unit)
if ok2 then
hp = realHp or hp
end
local ok3, realMaxHp = pcall(UnitHealthMax, unit)
if ok3 then
maxHp = realMaxHp or maxHp
end
end
end
if maxHp <= 0 then
HidePredictions()
return
end
local _, mineIncoming, othersIncoming = GetIncomingHeals(unit)
if CheckSuperWow then
local ok, hasSW = pcall(CheckSuperWow)
if ok and hasSW then
local ok2, _, realMine, realOther = pcall(GetIncomingHeals, unit)
if ok2 then
mineIncoming = realMine or mineIncoming
othersIncoming = realOther or othersIncoming
end
end
end
local missing = maxHp - hp
if missing <= 0 then
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
return
end
@@ -914,29 +970,30 @@ function SFrames.Party:UpdateHealPrediction(unit)
local mineShown = math.min(math.max(0, mineIncoming), missing)
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineShown <= 0 and otherShown <= 0 then
if mineIncoming <= 0 and othersIncoming <= 0 then
HidePredictions()
return
end
local barWidth = f.health:GetWidth() or 0
local barWidth = f:GetWidth() - (f.portrait:GetWidth() + 4)
if barWidth <= 0 then
HidePredictions()
return
end
local currentWidth = math.floor((hp / maxHp) * barWidth + 0.5)
local currentWidth = (hp / maxHp) * barWidth
if currentWidth < 0 then currentWidth = 0 end
if currentWidth > barWidth then currentWidth = barWidth end
local availableWidth = barWidth - currentWidth
if availableWidth <= 0 then
HidePredictions()
return
end
if availableWidth < 0 then availableWidth = 0 end
local mineWidth = math.floor((mineShown / maxHp) * barWidth + 0.5)
local otherWidth = math.floor((otherShown / maxHp) * barWidth + 0.5)
local mineWidth = 0
local otherWidth = 0
if missing > 0 then
mineWidth = (mineShown / missing) * availableWidth
otherWidth = (otherShown / missing) * availableWidth
end
if mineWidth < 0 then mineWidth = 0 end
if otherWidth < 0 then otherWidth = 0 end
if mineWidth > availableWidth then mineWidth = availableWidth end
@@ -949,6 +1006,7 @@ function SFrames.Party:UpdateHealPrediction(unit)
predMine:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentWidth, 0)
predMine:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth, 0)
predMine:SetWidth(mineWidth)
predMine:SetHeight(f.health:GetHeight())
predMine:Show()
else
predMine:Hide()
@@ -959,10 +1017,29 @@ function SFrames.Party:UpdateHealPrediction(unit)
predOther:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentWidth + mineWidth, 0)
predOther:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth + mineWidth, 0)
predOther:SetWidth(otherWidth)
predOther:SetHeight(f.health:GetHeight())
predOther:Show()
else
predOther:Hide()
end
local totalIncoming = mineIncoming + othersIncoming
local overHeal = totalIncoming - missing
if overHeal > 0 then
local overWidth = (overHeal / maxHp) * barWidth
if overWidth > 0 then
predOver:ClearAllPoints()
predOver:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentWidth + mineWidth + otherWidth, 0)
predOver:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth + mineWidth + otherWidth, 0)
predOver:SetWidth(overWidth)
predOver:SetHeight(f.health:GetHeight())
predOver:Show()
else
predOver:Hide()
end
else
predOver:Hide()
end
end
function SFrames.Party:UpdatePowerType(unit)

View File

@@ -270,11 +270,13 @@ function SFrames.Pet:Initialize()
end)
f:SetScript("OnEnter", function()
if SetMouseoverUnit then SetMouseoverUnit("pet") end
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit("pet")
GameTooltip:Show()
end)
f:SetScript("OnLeave", function()
if SetMouseoverUnit then SetMouseoverUnit() end
GameTooltip:Hide()
end)
@@ -297,6 +299,25 @@ function SFrames.Pet:Initialize()
f.health.bg:SetTexture(SFrames:GetTexture())
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
-- Heal prediction overlay (incoming heals)
f.health.healPredMine = f.health:CreateTexture(nil, "ARTWORK")
f.health.healPredMine:SetTexture(SFrames:GetTexture())
f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78)
f.health.healPredMine:SetDrawLayer("ARTWORK", 2)
f.health.healPredMine:Hide()
f.health.healPredOther = f.health:CreateTexture(nil, "ARTWORK")
f.health.healPredOther:SetTexture(SFrames:GetTexture())
f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5)
f.health.healPredOther:SetDrawLayer("ARTWORK", 2)
f.health.healPredOther:Hide()
f.health.healPredOver = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredOver:SetTexture(SFrames:GetTexture())
f.health.healPredOver:SetVertexColor(1.0, 0.3, 0.3, 0.6)
f.health.healPredOver:SetDrawLayer("OVERLAY", 7)
f.health.healPredOver:Hide()
-- Power Bar
f.power = SFrames:CreateStatusBar(f, "SFramesPetPower")
f.power:SetPoint("TOPLEFT", f.health, "BOTTOMLEFT", 0, -1)
@@ -434,6 +455,128 @@ function SFrames.Pet:UpdateHealth()
else
self.frame.healthText:SetText("")
end
self:UpdateHealPrediction()
end
function SFrames.Pet:UpdateHealPrediction()
if not (self.frame and self.frame.health and self.frame.health.healPredMine and self.frame.health.healPredOther and self.frame.health.healPredOver) then return end
local predMine = self.frame.health.healPredMine
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
if CheckSuperWow then
local ok, hasSW = pcall(CheckSuperWow)
if ok and hasSW then
local ok2, realHp = pcall(UnitHealth, "pet")
if ok2 then hp = realHp or hp end
local ok3, realMaxHp = pcall(UnitHealthMax, "pet")
if ok3 then maxHp = realMaxHp or maxHp end
end
end
if maxHp <= 0 or UnitIsDeadOrGhost("pet") then
HidePredictions()
return
end
local totalIncoming, mineIncoming, othersIncoming = 0, 0, 0
local ok, t, m, o = pcall(function() return SFrames:GetIncomingHeals("pet") end)
if ok then
totalIncoming, mineIncoming, othersIncoming = t or 0, m or 0, o or 0
end
local missing = maxHp - hp
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
return
end
local mineShown = math.min(math.max(0, mineIncoming), missing)
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()
return
end
local barWidth = self.frame.health:GetWidth()
if barWidth <= 0 then
HidePredictions()
return
end
local currentWidth = (hp / maxHp) * barWidth
if currentWidth < 0 then currentWidth = 0 end
if currentWidth > barWidth then currentWidth = barWidth end
local availableWidth = barWidth - currentWidth
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
return
end
local mineWidth = 0
local otherWidth = 0
if missing > 0 then
mineWidth = (mineShown / missing) * availableWidth
otherWidth = (otherShown / missing) * availableWidth
if mineWidth < 0 then mineWidth = 0 end
if otherWidth < 0 then otherWidth = 0 end
if mineWidth > availableWidth then mineWidth = availableWidth end
if otherWidth > (availableWidth - mineWidth) then
otherWidth = availableWidth - mineWidth
end
end
if mineWidth > 0 then
predMine:ClearAllPoints()
predMine:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth, 0)
predMine:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth, 0)
predMine:SetWidth(mineWidth)
predMine:SetHeight(self.frame.health:GetHeight())
predMine:Show()
else
predMine:Hide()
end
if otherWidth > 0 then
predOther:ClearAllPoints()
predOther:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth + mineWidth, 0)
predOther:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth + mineWidth, 0)
predOther:SetWidth(otherWidth)
predOther:SetHeight(self.frame.health:GetHeight())
predOther:Show()
else
predOther:Hide()
end
local totalIncomingValue = mineIncoming + othersIncoming
local overHeal = totalIncomingValue - missing
if overHeal > 0 then
local overWidth = math.floor((overHeal / maxHp) * barWidth + 0.5)
if overWidth > 0 then
predOver:ClearAllPoints()
predOver:SetPoint("TOPLEFT", self.frame.health, "TOPRIGHT", 0, 0)
predOver:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMRIGHT", 0, 0)
predOver:SetWidth(overWidth)
predOver:SetHeight(self.frame.health:GetHeight())
predOver:Show()
else
predOver:Hide()
end
else
predOver:Hide()
end
end
function SFrames.Pet:UpdatePowerType()
@@ -1118,6 +1261,7 @@ function SFrames.Pet:CreateAuras()
this.timer = this.timer + arg1
if this.timer >= 0.25 then
SFrames.Pet:TickAuras()
SFrames.Pet:UpdateHealPrediction()
this.timer = 0
end
end)

View File

@@ -21,31 +21,7 @@ local function GetChineseClassName(classToken, localizedClass)
end
local function GetIncomingHeals(unit)
if not (ShaguTweaks and ShaguTweaks.libpredict and ShaguTweaks.libpredict.UnitGetIncomingHeals) then
return 0, 0, 0
end
local libpredict = ShaguTweaks.libpredict
if libpredict.UnitGetIncomingHealsBreakdown then
local ok, total, mine, others = pcall(function()
return libpredict:UnitGetIncomingHealsBreakdown(unit, UnitName("player"))
end)
if ok then
total = math.max(0, tonumber(total) or 0)
mine = math.max(0, tonumber(mine) or 0)
others = math.max(0, tonumber(others) or 0)
return total, mine, others
end
end
local ok, amount = pcall(function()
return libpredict:UnitGetIncomingHeals(unit)
end)
if not ok then return 0, 0, 0 end
amount = tonumber(amount) or 0
if amount < 0 then amount = 0 end
return amount, 0, amount
return SFrames:GetIncomingHeals(unit)
end
local function Clamp(value, minValue, maxValue)
@@ -116,6 +92,15 @@ function SFrames.Player:ApplyConfig()
f:SetHeight(cfg.height)
f:SetAlpha(frameAlpha)
local bgA = tonumber(db.playerBgAlpha) or 0.9
local _A = SFrames.ActiveTheme
if _A and _A.panelBg and bgA < 0.89 then
if f.SetBackdropColor then f:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end
if f.healthBGFrame and f.healthBGFrame.SetBackdropColor then f.healthBGFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end
if f.powerBGFrame and f.powerBGFrame.SetBackdropColor then f.powerBGFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end
if f.portraitBG and f.portraitBG.SetBackdropColor then f.portraitBG:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end
end
if showPortrait then
if f.portrait then
f.portrait:SetWidth(cfg.portraitWidth)
@@ -295,15 +280,23 @@ function SFrames.Player:Initialize()
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
-- Heal prediction overlay (incoming heals)
f.health.healPredMine = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredMine = f.health:CreateTexture(nil, "ARTWORK")
f.health.healPredMine:SetTexture(SFrames:GetTexture())
f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78)
f.health.healPredMine:SetDrawLayer("ARTWORK", 2)
f.health.healPredMine:Hide()
f.health.healPredOther = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredOther = f.health:CreateTexture(nil, "ARTWORK")
f.health.healPredOther:SetTexture(SFrames:GetTexture())
f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5)
f.health.healPredOther:SetDrawLayer("ARTWORK", 2)
f.health.healPredOther:Hide()
f.health.healPredOver = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredOver:SetTexture(SFrames:GetTexture())
f.health.healPredOver:SetVertexColor(1.0, 0.3, 0.3, 0.6)
f.health.healPredOver:SetDrawLayer("OVERLAY", 7)
f.health.healPredOver:Hide()
-- Power Bar
f.power = SFrames:CreateStatusBar(f, "SFramesPlayerPower")
@@ -524,11 +517,13 @@ function SFrames.Player:Initialize()
f.unit = "player"
f:SetScript("OnEnter", function()
if SetMouseoverUnit then SetMouseoverUnit(this.unit) end
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit(this.unit)
GameTooltip:Show()
end)
f:SetScript("OnLeave", function()
if SetMouseoverUnit then SetMouseoverUnit() end
GameTooltip:Hide()
end)
end
@@ -1094,6 +1089,21 @@ end
function SFrames.Player:UpdateHealth()
local hp = UnitHealth("player")
local maxHp = UnitHealthMax("player")
if CheckSuperWow then
local ok, hasSW = pcall(CheckSuperWow)
if ok and hasSW then
local ok2, realHp = pcall(UnitHealth, "player")
if ok2 then
hp = realHp or hp
end
local ok3, realMaxHp = pcall(UnitHealthMax, "player")
if ok3 then
maxHp = realMaxHp or maxHp
end
end
end
self.frame.health:SetMinMaxValues(0, maxHp)
self.frame.health:SetValue(hp)
@@ -1107,25 +1117,47 @@ function SFrames.Player:UpdateHealth()
end
function SFrames.Player:UpdateHealPrediction()
if not (self.frame and self.frame.health and self.frame.health.healPredMine and self.frame.health.healPredOther) then return end
if not (self.frame and self.frame.health and self.frame.health.healPredMine and self.frame.health.healPredOther and self.frame.health.healPredOver) then return end
local predMine = self.frame.health.healPredMine
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
if maxHp <= 0 or hp >= maxHp or UnitIsDeadOrGhost("player") then
if CheckSuperWow then
local ok, hasSW = pcall(CheckSuperWow)
if ok and hasSW then
local ok2, realHp = pcall(UnitHealth, "player")
if ok2 then
hp = realHp or hp
end
local ok3, realMaxHp = pcall(UnitHealthMax, "player")
if ok3 then
maxHp = realMaxHp or maxHp
end
end
end
if maxHp <= 0 or UnitIsDeadOrGhost("player") then
HidePredictions()
return
end
local _, mineIncoming, othersIncoming = GetIncomingHeals("player")
local totalIncoming, mineIncoming, othersIncoming = 0, 0, 0
local ok, t, m, o = pcall(function() return GetIncomingHeals("player") end)
if ok then
totalIncoming, mineIncoming, othersIncoming = t or 0, m or 0, o or 0
end
local missing = maxHp - hp
if missing <= 0 then
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
return
end
@@ -1133,34 +1165,39 @@ function SFrames.Player:UpdateHealPrediction()
local mineShown = math.min(math.max(0, mineIncoming), missing)
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineShown <= 0 and otherShown <= 0 then
if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
return
end
local barWidth = self.frame.health:GetWidth() or 0
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()
return
end
local currentWidth = math.floor((hp / maxHp) * barWidth + 0.5)
local currentWidth = (hp / maxHp) * barWidth
if currentWidth < 0 then currentWidth = 0 end
if currentWidth > barWidth then currentWidth = barWidth end
local availableWidth = barWidth - currentWidth
if availableWidth <= 0 then
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
return
end
local mineWidth = math.floor((mineShown / maxHp) * barWidth + 0.5)
local otherWidth = math.floor((otherShown / maxHp) * barWidth + 0.5)
if mineWidth < 0 then mineWidth = 0 end
if otherWidth < 0 then otherWidth = 0 end
if mineWidth > availableWidth then mineWidth = availableWidth end
if otherWidth > (availableWidth - mineWidth) then
otherWidth = availableWidth - mineWidth
local mineWidth = 0
local otherWidth = 0
if missing > 0 then
mineWidth = (mineShown / missing) * availableWidth
otherWidth = (otherShown / missing) * availableWidth
if mineWidth < 0 then mineWidth = 0 end
if otherWidth < 0 then otherWidth = 0 end
if mineWidth > availableWidth then mineWidth = availableWidth end
if otherWidth > (availableWidth - mineWidth) then
otherWidth = availableWidth - mineWidth
end
end
if mineWidth > 0 then
@@ -1168,6 +1205,7 @@ function SFrames.Player:UpdateHealPrediction()
predMine:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth, 0)
predMine:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth, 0)
predMine:SetWidth(mineWidth)
predMine:SetHeight(self.frame.health:GetHeight())
predMine:Show()
else
predMine:Hide()
@@ -1178,10 +1216,29 @@ function SFrames.Player:UpdateHealPrediction()
predOther:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth + mineWidth, 0)
predOther:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth + mineWidth, 0)
predOther:SetWidth(otherWidth)
predOther:SetHeight(self.frame.health:GetHeight())
predOther:Show()
else
predOther:Hide()
end
local totalIncomingValue = mineIncoming + othersIncoming
local overHeal = totalIncomingValue - missing
if overHeal > 0 then
local overWidth = math.floor((overHeal / maxHp) * barWidth + 0.5)
if overWidth > 0 then
predOver:ClearAllPoints()
predOver:SetPoint("TOPLEFT", self.frame.health, "TOPRIGHT", 0, 0)
predOver:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMRIGHT", 0, 0)
predOver:SetWidth(overWidth)
predOver:SetHeight(self.frame.health:GetHeight())
predOver:Show()
else
predOver:Hide()
end
else
predOver:Hide()
end
end
function SFrames.Player:UpdatePowerType()
@@ -1426,7 +1483,7 @@ end
--------------------------------------------------------------------------------
function SFrames.Player:CreateAuras()
-- Create 16 Buff Slots
-- Create 32 Buff Slots
self.frame.buffs = {}
self.frame.debuffs = {}
local size = 24
@@ -1434,7 +1491,7 @@ function SFrames.Player:CreateAuras()
local rowSpacing = 1
local buffsPerRow = 9
for i = 1, 16 do
for i = 1, 32 do
local b = CreateFrame("Button", "SFramesPlayerBuff"..i, self.frame)
b:SetWidth(size)
b:SetHeight(size)
@@ -1474,17 +1531,25 @@ end
function SFrames.Player:UpdateAuras()
local slotIdx = 0
local hasGetPlayerBuffID = SFrames.superwow_active and type(GetPlayerBuffID) == "function"
for i = 0, 31 do
local buffIndex, untilCancelled = GetPlayerBuff(i, "HELPFUL")
if buffIndex and buffIndex >= 0 then
if not SFrames:IsBuffHidden(buffIndex) then
slotIdx = slotIdx + 1
if slotIdx > 16 then break end
if slotIdx > 32 then break end
local b = self.frame.buffs[slotIdx]
local texture = GetPlayerBuffTexture(buffIndex)
if texture then
b.icon:SetTexture(texture)
b.buffIndex = buffIndex
-- Store aura ID when SuperWoW is available
if hasGetPlayerBuffID then
local ok, auraID = pcall(GetPlayerBuffID, buffIndex)
b.auraID = ok and auraID or nil
else
b.auraID = nil
end
b:Show()
local timeLeft = GetPlayerBuffTimeLeft(buffIndex)
@@ -1499,8 +1564,9 @@ function SFrames.Player:UpdateAuras()
end
end
end
for j = slotIdx + 1, 16 do
for j = slotIdx + 1, 32 do
self.frame.buffs[j]:Hide()
self.frame.buffs[j].auraID = nil
end
end
@@ -1544,6 +1610,63 @@ end
-- Player Castbar
--------------------------------------------------------------------------------
local function GetLatencySeconds()
if GetNetStats then
local _, _, latency = GetNetStats()
if latency and latency > 0 then return latency / 1000 end
end
return 0
end
local function HSVtoRGB(h, s, v)
local i = math.floor(h * 6)
local f = h * 6 - i
local p = v * (1 - s)
local q = v * (1 - f * s)
local t = v * (1 - (1 - f) * s)
local m = math.mod(i, 6)
if m == 0 then return v, t, p
elseif m == 1 then return q, v, p
elseif m == 2 then return p, v, t
elseif m == 3 then return p, q, v
elseif m == 4 then return t, p, 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
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
end
function SFrames.Player:CreateCastbar()
local cb = SFrames:CreateStatusBar(self.frame, "SFramesPlayerCastbar")
cb:SetHeight(SFrames.Config.castbarHeight)
@@ -1581,6 +1704,23 @@ function SFrames.Player:CreateCastbar()
ibg:SetFrameLevel(cb:GetFrameLevel() - 1)
SFrames:CreateUnitBackdrop(ibg)
local lagTex = cb:CreateTexture(nil, "OVERLAY")
lagTex:SetTexture(SFrames:GetTexture())
lagTex:SetVertexColor(1, 0.2, 0.2, 0.5)
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:Hide()
cbbg:Hide()
cb.icon:Hide()
@@ -1591,7 +1731,9 @@ function SFrames.Player:CreateCastbar()
self.frame.castbar.ibg = ibg
cb:SetScript("OnUpdate", function() SFrames.Player:CastbarOnUpdate() end)
self:ApplyCastbarPosition()
-- Hook events
SFrames:RegisterEvent("SPELLCAST_START", function() self:CastbarStart(arg1, arg2) end)
SFrames:RegisterEvent("SPELLCAST_STOP", function() self:CastbarStop() end)
@@ -1603,6 +1745,60 @@ function SFrames.Player:CreateCastbar()
SFrames:RegisterEvent("SPELLCAST_CHANNEL_STOP", function() self:CastbarStop() end)
end
function SFrames.Player:ApplyCastbarPosition()
local cb = self.frame.castbar
if not cb then return end
local db = SFramesDB or {}
cb:ClearAllPoints()
cb.cbbg:ClearAllPoints()
cb.icon:ClearAllPoints()
cb.ibg:ClearAllPoints()
if db.castbarStandalone then
cb:SetParent(UIParent)
cb.cbbg:SetParent(UIParent)
cb.ibg:SetParent(UIParent)
cb:SetWidth(280)
cb:SetHeight(20)
cb:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, 120)
cb:SetFrameStrata("HIGH")
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:SetPoint("RIGHT", cb, "LEFT", -4, 0)
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.RegisterMover then
SFrames.Movers:RegisterMover("PlayerCastbar", cb, "施法条",
"BOTTOM", "UIParent", "BOTTOM", 0, 120)
end
else
cb:SetParent(self.frame)
cb.cbbg:SetParent(self.frame)
cb.ibg:SetParent(self.frame)
local cbH = SFrames.Config.castbarHeight
cb:SetWidth(0) -- 清除独立模式的显式宽度,让双锚点自动计算
cb:SetHeight(cbH)
cb:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", 0, 6)
cb:SetPoint("BOTTOMLEFT", self.frame.portrait, "TOPLEFT", cbH + 6, 6)
cb:SetFrameStrata("MEDIUM")
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(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)
cb.ibg:SetFrameLevel(math.max(1, cb:GetFrameLevel() - 1))
end
end
function SFrames.Player:CastbarStart(spellName, duration)
local cb = self.frame.castbar
cb.casting = true
@@ -1648,6 +1844,23 @@ function SFrames.Player:CastbarStart(spellName, duration)
end
cb:Show()
cb.cbbg:Show()
local lag = GetLatencySeconds()
if lag > 0 and cb.maxValue > 0 then
local barW = cb:GetWidth()
local lagW = math.min((lag / cb.maxValue) * barW, barW * 0.5)
if lagW >= 1 then
cb.lagTex:ClearAllPoints()
cb.lagTex:SetPoint("TOPRIGHT", cb, "TOPRIGHT", 0, 0)
cb.lagTex:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", 0, 0)
cb.lagTex:SetWidth(lagW)
cb.lagTex:Show()
else
cb.lagTex:Hide()
end
else
cb.lagTex:Hide()
end
end
function SFrames.Player:CastbarChannelStart(duration, spellName)
@@ -1696,6 +1909,23 @@ function SFrames.Player:CastbarChannelStart(duration, spellName)
end
cb:Show()
cb.cbbg:Show()
local lag = GetLatencySeconds()
if lag > 0 and cb.maxValue > 0 then
local barW = cb:GetWidth()
local lagW = math.min((lag / cb.maxValue) * barW, barW * 0.5)
if lagW >= 1 then
cb.lagTex:ClearAllPoints()
cb.lagTex:SetPoint("TOPLEFT", cb, "TOPLEFT", 0, 0)
cb.lagTex:SetPoint("BOTTOMLEFT", cb, "BOTTOMLEFT", 0, 0)
cb.lagTex:SetWidth(lagW)
cb.lagTex:Show()
else
cb.lagTex:Hide()
end
else
cb.lagTex:Hide()
end
end
function SFrames.Player:CastbarStop()
@@ -1703,7 +1933,7 @@ function SFrames.Player:CastbarStop()
cb.casting = nil
cb.channeling = nil
cb.fadeOut = true
-- keep showing for a short fade out
if cb.lagTex then cb.lagTex:Hide() end
end
function SFrames.Player:CastbarDelayed(delay)
@@ -1714,18 +1944,17 @@ function SFrames.Player:CastbarDelayed(delay)
end
end
function SFrames.Player:CastbarChannelUpdate(delay)
function SFrames.Player:CastbarChannelUpdate(remainingMs)
local cb = self.frame.castbar
if cb.channeling then
local add = delay / 1000
cb.maxValue = cb.maxValue + add
cb.endTime = cb.endTime + add
cb:SetMinMaxValues(0, cb.maxValue)
cb.endTime = GetTime() + remainingMs / 1000
end
end
function SFrames.Player:CastbarOnUpdate()
local cb = self.frame.castbar
local db = SFramesDB or {}
if cb.casting then
local elapsed = GetTime() - cb.startTime
if elapsed >= cb.maxValue then
@@ -1736,6 +1965,19 @@ function SFrames.Player:CastbarOnUpdate()
end
cb:SetValue(elapsed)
cb.time:SetText(string.format("%.1f", math.max(cb.maxValue - elapsed, 0)))
if db.castbarRainbow then
if not cb.rainbowActive then
cb:SetStatusBarColor(0, 0, 0, 0)
cb.rainbowActive = true
end
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
cb.rainbowActive = nil
end
if not cb.icon:IsShown() then
self:CastbarTryResolveIcon()
end
@@ -1749,10 +1991,28 @@ function SFrames.Player:CastbarOnUpdate()
end
cb:SetValue(timeRemaining)
cb.time:SetText(string.format("%.1f", timeRemaining))
if db.castbarRainbow then
if not cb.rainbowActive then
cb:SetStatusBarColor(0, 0, 0, 0)
cb.rainbowActive = true
end
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
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)

View File

@@ -71,6 +71,17 @@ function SFrames.Raid:ApplyFrameStyle(frame, metrics)
frame.healthBGFrame:SetPoint("BOTTOMRIGHT", frame.health, "BOTTOMRIGHT", 1, -1)
end
local bgA = (SFramesDB and type(SFramesDB.raidBgAlpha) == "number") and SFramesDB.raidBgAlpha or 0.9
local A = SFrames.ActiveTheme
if A and A.panelBg and bgA < 0.89 then
if frame.healthBGFrame and frame.healthBGFrame.SetBackdropColor then
frame.healthBGFrame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], bgA)
end
if frame.powerBGFrame and frame.powerBGFrame.SetBackdropColor then
frame.powerBGFrame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], bgA)
end
end
if frame.power then
if metrics.showPower then
frame.power:Show()
@@ -290,6 +301,10 @@ function SFrames.Raid:EnsureFrames()
end)
f:SetScript("OnClick", function()
if arg1 == "LeftButton" then
if IsShiftKeyDown() and SFrames.Focus and SFrames.Focus.SetFromUnit then
pcall(SFrames.Focus.SetFromUnit, SFrames.Focus, this.unit)
return
end
if TryDropCursorOnUnit(this.unit) then return end
if SpellIsTargeting and SpellIsTargeting() then
SpellTargetUnit(this.unit)
@@ -375,10 +390,14 @@ function SFrames.Raid:EnsureFrames()
end
end)
f:SetScript("OnEnter", function()
if SetMouseoverUnit then SetMouseoverUnit(this.unit) end
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit(this.unit)
end)
f:SetScript("OnLeave", function() GameTooltip:Hide() end)
f:SetScript("OnLeave", function()
if SetMouseoverUnit then SetMouseoverUnit() end
GameTooltip:Hide()
end)
f.unit = unit
-- Health Bar
@@ -395,16 +414,24 @@ function SFrames.Raid:EnsureFrames()
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
-- Heal prediction overlay (incoming heals)
f.health.healPredMine = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredMine = f.health:CreateTexture(nil, "ARTWORK")
f.health.healPredMine:SetTexture(SFrames:GetTexture())
f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78)
f.health.healPredMine:SetDrawLayer("ARTWORK", 2)
f.health.healPredMine:Hide()
f.health.healPredOther = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredOther = f.health:CreateTexture(nil, "ARTWORK")
f.health.healPredOther:SetTexture(SFrames:GetTexture())
f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5)
f.health.healPredOther:SetDrawLayer("ARTWORK", 2)
f.health.healPredOther:Hide()
f.health.healPredOver = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredOver:SetTexture(SFrames:GetTexture())
f.health.healPredOver:SetVertexColor(1.0, 0.3, 0.3, 0.6)
f.health.healPredOver:SetDrawLayer("OVERLAY", 7)
f.health.healPredOver:Hide()
-- Power Bar
f.power = SFrames:CreateStatusBar(f, "SFramesRaidFrame"..i.."Power")
f.power:SetMinMaxValues(0, 100)
@@ -793,14 +820,16 @@ function SFrames.Raid:UpdateHealPrediction(unit)
if not frameData then return end
local f = frameData.frame
if not (f.health and f.health.healPredMine and f.health.healPredOther) then return end
if not (f.health and f.health.healPredMine and f.health.healPredOther and f.health.healPredOver) then return end
local predMine = f.health.healPredMine
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
@@ -810,14 +839,39 @@ function SFrames.Raid:UpdateHealPrediction(unit)
local hp = UnitHealth(unit) or 0
local maxHp = UnitHealthMax(unit) or 0
if maxHp <= 0 or hp >= maxHp then
if CheckSuperWow then
local ok, hasSW = pcall(CheckSuperWow)
if ok and hasSW then
local ok2, realHp = pcall(UnitHealth, unit)
if ok2 then
hp = realHp or hp
end
local ok3, realMaxHp = pcall(UnitHealthMax, unit)
if ok3 then
maxHp = realMaxHp or maxHp
end
end
end
if maxHp <= 0 then
HidePredictions()
return
end
local _, mineIncoming, othersIncoming = GetIncomingHeals(unit)
if CheckSuperWow then
local ok, hasSW = pcall(CheckSuperWow)
if ok and hasSW then
local ok2, _, realMine, realOther = pcall(GetIncomingHeals, unit)
if ok2 then
mineIncoming = realMine or mineIncoming
othersIncoming = realOther or othersIncoming
end
end
end
local missing = maxHp - hp
if missing <= 0 then
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
return
end
@@ -825,41 +879,46 @@ function SFrames.Raid:UpdateHealPrediction(unit)
local mineShown = math.min(math.max(0, mineIncoming), missing)
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineShown <= 0 and otherShown <= 0 then
if mineIncoming <= 0 and othersIncoming <= 0 then
HidePredictions()
return
end
local barWidth = f.health:GetWidth() or 0
local barWidth = f:GetWidth() - 2
if barWidth <= 0 then
HidePredictions()
return
end
local currentWidth = math.floor((hp / maxHp) * barWidth + 0.5)
if currentWidth < 0 then currentWidth = 0 end
if currentWidth > barWidth then currentWidth = barWidth end
local currentPosition = (hp / maxHp) * barWidth
if currentPosition < 0 then currentPosition = 0 end
if currentPosition > barWidth then currentPosition = barWidth end
local availableWidth = barWidth - currentWidth
if availableWidth <= 0 then
local availableWidth = barWidth - currentPosition
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
return
end
local mineWidth = math.floor((mineShown / maxHp) * barWidth + 0.5)
local otherWidth = math.floor((otherShown / maxHp) * barWidth + 0.5)
if mineWidth < 0 then mineWidth = 0 end
if otherWidth < 0 then otherWidth = 0 end
if mineWidth > availableWidth then mineWidth = availableWidth end
if otherWidth > (availableWidth - mineWidth) then
otherWidth = availableWidth - mineWidth
local mineWidth = 0
local otherWidth = 0
if missing > 0 then
mineWidth = (mineShown / missing) * availableWidth
otherWidth = (otherShown / missing) * availableWidth
if mineWidth < 0 then mineWidth = 0 end
if otherWidth < 0 then otherWidth = 0 end
if mineWidth > availableWidth then mineWidth = availableWidth end
if otherWidth > (availableWidth - mineWidth) then
otherWidth = availableWidth - mineWidth
end
end
if mineWidth > 0 then
predMine:ClearAllPoints()
predMine:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentWidth, 0)
predMine:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth, 0)
predMine:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentPosition, 0)
predMine:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentPosition, 0)
predMine:SetWidth(mineWidth)
predMine:SetHeight(f.health:GetHeight())
predMine:Show()
else
predMine:Hide()
@@ -867,13 +926,32 @@ function SFrames.Raid:UpdateHealPrediction(unit)
if otherWidth > 0 then
predOther:ClearAllPoints()
predOther:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentWidth + mineWidth, 0)
predOther:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentWidth + mineWidth, 0)
predOther:SetPoint("TOPLEFT", f.health, "TOPLEFT", currentPosition + mineWidth, 0)
predOther:SetPoint("BOTTOMLEFT", f.health, "BOTTOMLEFT", currentPosition + mineWidth, 0)
predOther:SetWidth(otherWidth)
predOther:SetHeight(f.health:GetHeight())
predOther:Show()
else
predOther:Hide()
end
local totalIncoming = mineIncoming + othersIncoming
local overHeal = totalIncoming - missing
if overHeal > 0 then
local overWidth = (overHeal / maxHp) * barWidth
if overWidth > 0 then
predOver:ClearAllPoints()
predOver:SetPoint("TOPLEFT", f.health, "TOPRIGHT", 0, 0)
predOver:SetPoint("BOTTOMLEFT", f.health, "BOTTOMRIGHT", 0, 0)
predOver:SetWidth(overWidth)
predOver:SetHeight(f.health:GetHeight())
predOver:Show()
else
predOver:Hide()
end
else
predOver:Hide()
end
end
function SFrames.Raid:UpdatePower(unit)
@@ -1016,15 +1094,51 @@ function SFrames.Raid:UpdateAuras(unit)
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
for i = 1, 32 do
local texture, applications = UnitBuff(unit, i)
if not texture then break end
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFrames.Tooltip:SetUnitBuff(unit, i)
local buffName = SFramesScanTooltipTextLeft1:GetText()
SFrames.Tooltip:Hide()
local buffName = GetBuffName(unit, i)
if buffName then
for pos, listData in pairs(buffsNeeded) do
@@ -1058,10 +1172,7 @@ function SFrames.Raid:UpdateAuras(unit)
end
end
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
SFrames.Tooltip:SetUnitDebuff(unit, i)
local debuffName = SFramesScanTooltipTextLeft1:GetText()
SFrames.Tooltip:Hide()
local debuffName = GetDebuffName(unit, i)
if debuffName then
for pos, listData in pairs(buffsNeeded) do

View File

@@ -186,13 +186,30 @@ local DIST_BASE_WIDTH = 80
local DIST_BASE_HEIGHT = 24
local DIST_BASE_FONTSIZE = 14
-- Check if UnitXP SP3 distance API is available
local hasUnitXP = nil -- nil = not checked yet, true/false = cached result
local function IsUnitXPAvailable()
if hasUnitXP ~= nil then return hasUnitXP end
hasUnitXP = (type(UnitXP) == "function") and (pcall(UnitXP, "nop", "nop"))
return hasUnitXP
end
function SFrames.Target:GetDistance(unit)
if not UnitExists(unit) then return nil end
if UnitIsUnit(unit, "player") then return "0 码" end
-- Using multiple "scale rulers" (rungs) for better precision in 1.12
if CheckInteractDistance(unit, 2) then return "< 8 码" -- Trade
elseif CheckInteractDistance(unit, 3) then return "8-10 码" -- Duel
elseif CheckInteractDistance(unit, 4) then return "10-28 码" -- Follow
-- Prefer UnitXP precise distance when available
if IsUnitXPAvailable() then
local ok, dist = pcall(UnitXP, "distanceBetween", "player", unit)
if ok and type(dist) == "number" and dist >= 0 then
return string.format("%.1f 码", dist)
end
end
-- Fallback: CheckInteractDistance rough ranges
if CheckInteractDistance(unit, 2) then return "< 8 码"
elseif CheckInteractDistance(unit, 3) then return "8-10 码"
elseif CheckInteractDistance(unit, 4) then return "10-28 码"
elseif UnitIsVisible(unit) then return "28-100 码"
else return "> 100 码" end
end
@@ -255,6 +272,15 @@ function SFrames.Target:ApplyConfig()
f:SetHeight(cfg.height)
f:SetAlpha(frameAlpha)
local bgA = tonumber(db.targetBgAlpha) or 0.9
local _A = SFrames.ActiveTheme
if _A and _A.panelBg and bgA < 0.89 then
if f.SetBackdropColor then f:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end
if f.healthBGFrame and f.healthBGFrame.SetBackdropColor then f.healthBGFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end
if f.powerBGFrame and f.powerBGFrame.SetBackdropColor then f.powerBGFrame:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end
if f.portraitBG and f.portraitBG.SetBackdropColor then f.portraitBG:SetBackdropColor(_A.panelBg[1], _A.panelBg[2], _A.panelBg[3], bgA) end
end
if showPortrait then
if f.portrait then
f.portrait:SetWidth(cfg.portraitWidth)
@@ -362,7 +388,13 @@ function SFrames.Target:ApplyDistanceScale(scale)
if f.text then
local fontPath = SFrames:GetFont()
local outline = (SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
local fontSize = math.max(8, math.floor(DIST_BASE_FONTSIZE * scale + 0.5))
local customSize = SFramesDB and tonumber(SFramesDB.targetDistanceFontSize)
local fontSize
if customSize and customSize >= 8 and customSize <= 24 then
fontSize = math.floor(customSize + 0.5)
else
fontSize = math.max(8, math.floor(DIST_BASE_FONTSIZE * scale + 0.5))
end
f.text:SetFont(fontPath, fontSize, outline)
end
end
@@ -405,6 +437,13 @@ 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()
@@ -416,6 +455,7 @@ function SFrames.Target:InitializeDistanceFrame()
end
if not UnitExists("target") then
if this:IsShown() then this:Hide() end
if this.behindText then this.behindText:Hide() end
return
end
this.timer = this.timer + (arg1 or 0)
@@ -424,6 +464,28 @@ function SFrames.Target:InitializeDistanceFrame()
local dist = SFrames.Target:GetDistance("target")
this.text:SetText(dist or "---")
if not this:IsShown() then this:Show() end
-- 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
end
end
end)
end
@@ -476,7 +538,23 @@ function SFrames.Target:Initialize()
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")
end
return
end
if TryDropCursorOnUnit(this.unit) then
return
end
@@ -484,15 +562,132 @@ function SFrames.Target:Initialize()
SpellTargetUnit(this.unit)
end
elseif arg1 == "RightButton" then
if SpellIsTargeting() then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nanami] RightButton hit|r")
if SpellIsTargeting and SpellIsTargeting() then
SpellStopTargeting()
return
end
HideDropDownMenu(1)
TargetFrameDropDown.unit = "target"
TargetFrameDropDown.name = UnitName("target")
TargetFrameDropDown.initialize = TargetFrameDropDown_Initialize
ToggleDropDownMenu(1, nil, TargetFrameDropDown, "SFramesTargetFrame", 120, 10)
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
local info = {}
info.text = name
info.isTitle = 1
info.notCheckable = 1
UIDropDownMenu_AddButton(info)
if UnitIsPlayer("target") and UnitIsFriend("player", "target") and not UnitIsUnit("target", "player") then
-- 悄悄话
info = {}
info.text = "悄悄话"
info.notCheckable = 1
info.func = function() ChatFrame_SendTell(name) end
UIDropDownMenu_AddButton(info)
-- 组队相关(动态)
local inParty = UnitInParty("target")
local isLeader = IsPartyLeader()
if inParty and isLeader then
info = {}
info.text = "提升为队长"
info.notCheckable = 1
info.func = function() PromoteToLeader(name) end
UIDropDownMenu_AddButton(info)
info = {}
info.text = "取消邀请"
info.notCheckable = 1
info.func = function() UninviteByName(name) end
UIDropDownMenu_AddButton(info)
end
-- 观察
info = {}
info.text = "观察"
info.notCheckable = 1
info.func = function() InspectUnit("target") end
UIDropDownMenu_AddButton(info)
-- 交易
info = {}
info.text = "交易"
info.notCheckable = 1
info.func = function() InitiateTrade("target") end
UIDropDownMenu_AddButton(info)
-- 邀请组队(不在队伍中时显示)
if not inParty then
info = {}
info.text = "邀请组队"
info.notCheckable = 1
info.func = function() InviteByName(name) end
UIDropDownMenu_AddButton(info)
end
-- 跟随
info = {}
info.text = "跟随"
info.notCheckable = 1
info.func = function() FollowUnit("target") end
UIDropDownMenu_AddButton(info)
-- 决斗(不在队伍中时显示,节省按钮数)
if not inParty then
info = {}
info.text = "决斗"
info.notCheckable = 1
info.func = function() StartDuel("target") end
UIDropDownMenu_AddButton(info)
end
end
if SFrames.Focus and SFrames.Focus.GetFocusName then
info = {}
local ok2, curFocus = pcall(SFrames.Focus.GetFocusName, SFrames.Focus)
local isSameTarget = ok2 and curFocus and curFocus == name
if isSameTarget then
info.text = "取消焦点"
info.func = function() pcall(SFrames.Focus.Clear, SFrames.Focus) end
else
info.text = "设为焦点"
info.func = function() pcall(SFrames.Focus.SetFromTarget, SFrames.Focus) end
end
info.notCheckable = 1
UIDropDownMenu_AddButton(info)
end
-- 取消按钮不添加,点击菜单外部即可关闭(节省按钮位)
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
end
end)
f:SetScript("OnReceiveDrag", function()
@@ -540,15 +735,23 @@ function SFrames.Target:Initialize()
f.health.bg:SetVertexColor(_A.slotBg[1], _A.slotBg[2], _A.slotBg[3], _A.slotBg[4] or 1)
-- Heal prediction overlay (incoming heals)
f.health.healPredMine = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredMine = f.health:CreateTexture(nil, "ARTWORK")
f.health.healPredMine:SetTexture(SFrames:GetTexture())
f.health.healPredMine:SetVertexColor(0.4, 1.0, 0.55, 0.78)
f.health.healPredMine:SetDrawLayer("ARTWORK", 2)
f.health.healPredMine:Hide()
f.health.healPredOther = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredOther = f.health:CreateTexture(nil, "ARTWORK")
f.health.healPredOther:SetTexture(SFrames:GetTexture())
f.health.healPredOther:SetVertexColor(0.2, 0.9, 0.35, 0.5)
f.health.healPredOther:SetDrawLayer("ARTWORK", 2)
f.health.healPredOther:Hide()
f.health.healPredOver = f.health:CreateTexture(nil, "OVERLAY")
f.health.healPredOver:SetTexture(SFrames:GetTexture())
f.health.healPredOver:SetVertexColor(1.0, 0.3, 0.3, 0.6)
f.health.healPredOver:SetDrawLayer("OVERLAY", 7)
f.health.healPredOver:Hide()
-- Power Bar
f.power = SFrames:CreateStatusBar(f, "SFramesTargetPower")
@@ -645,11 +848,13 @@ function SFrames.Target:Initialize()
f.unit = "target"
f:SetScript("OnEnter", function()
if SetMouseoverUnit then SetMouseoverUnit(this.unit) end
GameTooltip_SetDefaultAnchor(GameTooltip, this)
GameTooltip:SetUnit(this.unit)
GameTooltip:Show()
end)
f:SetScript("OnLeave", function()
if SetMouseoverUnit then SetMouseoverUnit() end
GameTooltip:Hide()
end)
@@ -1012,13 +1217,15 @@ function SFrames.Target:UpdateHealth()
end
function SFrames.Target:UpdateHealPrediction()
if not (self.frame and self.frame.health and self.frame.health.healPredMine and self.frame.health.healPredOther) then return end
if not (self.frame and self.frame.health and self.frame.health.healPredMine and self.frame.health.healPredOther and self.frame.health.healPredOver) then return end
local predMine = self.frame.health.healPredMine
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
@@ -1028,14 +1235,39 @@ function SFrames.Target:UpdateHealPrediction()
local hp = UnitHealth("target") or 0
local maxHp = UnitHealthMax("target") or 0
if maxHp <= 0 or hp >= maxHp then
if CheckSuperWow then
local ok, hasSW = pcall(CheckSuperWow)
if ok and hasSW then
local ok2, realHp = pcall(UnitHealth, "target")
if ok2 then
hp = realHp or hp
end
local ok3, realMaxHp = pcall(UnitHealthMax, "target")
if ok3 then
maxHp = realMaxHp or maxHp
end
end
end
if maxHp <= 0 then
HidePredictions()
return
end
local _, mineIncoming, othersIncoming = GetIncomingHeals("target")
if CheckSuperWow then
local ok, hasSW = pcall(CheckSuperWow)
if ok and hasSW then
local ok2, _, realMine, realOther = pcall(GetIncomingHeals, "target")
if ok2 then
mineIncoming = realMine or mineIncoming
othersIncoming = realOther or othersIncoming
end
end
end
local missing = maxHp - hp
if missing <= 0 then
if missing <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
return
end
@@ -1043,34 +1275,39 @@ function SFrames.Target:UpdateHealPrediction()
local mineShown = math.min(math.max(0, mineIncoming), missing)
local remaining = missing - mineShown
local otherShown = math.min(math.max(0, othersIncoming), remaining)
if mineShown <= 0 and otherShown <= 0 then
if mineShown <= 0 and otherShown <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
return
end
local barWidth = self.frame.health:GetWidth() or 0
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()
return
end
local currentWidth = math.floor((hp / maxHp) * barWidth + 0.5)
local currentWidth = (hp / maxHp) * barWidth
if currentWidth < 0 then currentWidth = 0 end
if currentWidth > barWidth then currentWidth = barWidth end
local availableWidth = barWidth - currentWidth
if availableWidth <= 0 then
if availableWidth <= 0 and (mineIncoming <= 0 and othersIncoming <= 0) then
HidePredictions()
return
end
local mineWidth = math.floor((mineShown / maxHp) * barWidth + 0.5)
local otherWidth = math.floor((otherShown / maxHp) * barWidth + 0.5)
if mineWidth < 0 then mineWidth = 0 end
if otherWidth < 0 then otherWidth = 0 end
if mineWidth > availableWidth then mineWidth = availableWidth end
if otherWidth > (availableWidth - mineWidth) then
otherWidth = availableWidth - mineWidth
local mineWidth = 0
local otherWidth = 0
if missing > 0 then
mineWidth = (mineShown / missing) * availableWidth
otherWidth = (otherShown / missing) * availableWidth
if mineWidth < 0 then mineWidth = 0 end
if otherWidth < 0 then otherWidth = 0 end
if mineWidth > availableWidth then mineWidth = availableWidth end
if otherWidth > (availableWidth - mineWidth) then
otherWidth = availableWidth - mineWidth
end
end
if mineWidth > 0 then
@@ -1078,6 +1315,7 @@ function SFrames.Target:UpdateHealPrediction()
predMine:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth, 0)
predMine:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth, 0)
predMine:SetWidth(mineWidth)
predMine:SetHeight(self.frame.health:GetHeight())
predMine:Show()
else
predMine:Hide()
@@ -1088,10 +1326,29 @@ function SFrames.Target:UpdateHealPrediction()
predOther:SetPoint("TOPLEFT", self.frame.health, "TOPLEFT", currentWidth + mineWidth, 0)
predOther:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMLEFT", currentWidth + mineWidth, 0)
predOther:SetWidth(otherWidth)
predOther:SetHeight(self.frame.health:GetHeight())
predOther:Show()
else
predOther:Hide()
end
local totalIncoming = mineIncoming + othersIncoming
local overHeal = totalIncoming - missing
if overHeal > 0 then
local overWidth = math.floor((overHeal / maxHp) * barWidth + 0.5)
if overWidth > 0 then
predOver:ClearAllPoints()
predOver:SetPoint("TOPLEFT", self.frame.health, "TOPRIGHT", 0, 0)
predOver:SetPoint("BOTTOMLEFT", self.frame.health, "BOTTOMRIGHT", 0, 0)
predOver:SetWidth(overWidth)
predOver:SetHeight(self.frame.health:GetHeight())
predOver:Show()
else
predOver:Hide()
end
else
predOver:Hide()
end
end
function SFrames.Target:UpdatePowerType()
@@ -1155,7 +1412,7 @@ function SFrames.Target:CreateAuras()
self.frame.debuffs = {}
-- Target Buffs (Top left to right)
for i = 1, 16 do
for i = 1, 32 do
local b = CreateFrame("Button", "SFramesTargetBuff"..i, self.frame)
b:SetWidth(AURA_SIZE)
b:SetHeight(AURA_SIZE)
@@ -1191,7 +1448,7 @@ function SFrames.Target:CreateAuras()
end
-- Target Debuffs (Bottom left to right)
for i = 1, 16 do
for i = 1, 32 do
local b = CreateFrame("Button", "SFramesTargetDebuff"..i, self.frame)
b:SetWidth(AURA_SIZE)
b:SetHeight(AURA_SIZE)
@@ -1256,7 +1513,7 @@ function SFrames.Target:TickAuras()
end
-- Buffs
for i = 1, 16 do
for i = 1, 32 do
local b = self.frame.buffs[i]
if b:IsShown() and b.expirationTime then
local timeLeft = b.expirationTime - timeNow
@@ -1275,7 +1532,7 @@ function SFrames.Target:TickAuras()
end
-- Debuffs: re-query SpellDB for live-accurate timers
for i = 1, 16 do
for i = 1, 32 do
local b = self.frame.debuffs[i]
if b:IsShown() then
local timeLeft = nil
@@ -1316,14 +1573,17 @@ end
function SFrames.Target:UpdateAuras()
if not UnitExists("target") then return end
local hasSuperWoW = SFrames.superwow_active and SpellInfo
local numBuffs = 0
-- Buffs
for i = 1, 16 do
local texture = UnitBuff("target", i)
for i = 1, 32 do
local texture, swAuraID = UnitBuff("target", i)
local b = self.frame.buffs[i]
b:SetID(i) -- Ensure ID is set for tooltips
if texture then
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()
@@ -1364,12 +1624,14 @@ function SFrames.Target:UpdateAuras()
local hasNP = NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff
local npFormat = NanamiPlates_Auras and NanamiPlates_Auras.FormatTime
for i = 1, 16 do
local texture = UnitDebuff("target", i)
for i = 1, 32 do
local texture, dbCount, dbType, swDebuffAuraID = UnitDebuff("target", i)
local b = self.frame.debuffs[i]
b:SetID(i)
if texture then
b.icon:SetTexture(texture)
-- Store aura ID when SuperWoW is available
b.auraID = hasSuperWoW and swDebuffAuraID or nil
local timeLeft = 0
local effectName = nil

View File

@@ -2117,6 +2117,4 @@ function WM:Initialize()
DiscoverDmfZoneIndices()
self.initialized = true
local _hex = (SFrames.Theme and SFrames.Theme:GetAccentHex()) or "ffffb3d9"
DEFAULT_CHAT_FRAME:AddMessage("|c" .. _hex .. "[Nanami-UI]|r 世界地图模块已加载 (Nanami主题 | Ctrl+左键放置标记 | /nui nav 导航地图)")
end

333
docs/插件功能概览.md Normal file
View File

@@ -0,0 +1,333 @@
# Nanami-UI 插件功能概览
本文基于当前仓库代码结构整理,目标是快速回答两个问题:
1. 这个乌龟服插件当前做了什么。
2. 每块功能主要落在哪些代码文件里。
## 1. 插件定位
`Nanami-UI` 是一个面向 Turtle WoW 1.12 环境的大型整合型 UI 插件。整体风格以 `SFrames` 为统一命名空间,核心能力主要包括:
- 自定义单位框体:玩家、宠物、目标、目标的目标、队伍、团队、焦点、天赋树。
- 动作条与按键:动作条重排、绑定管理、绑定导入导出。
- 地图与导航:小地图/世界地图换肤、地图迷雾揭示、区域等级、队友图标、暗月马戏团辅助。
- 聊天与社交:聊天框接管、私聊容器、社交面板重做。
- 背包与交易:背包/银行整合、离线库存、商人、交易、邮件、拾取窗口。
- 任务与书籍:任务对话、任务日志皮肤、书籍阅读界面。
- 职业与养成:角色面板、装备统计、观察面板、法术书、训练师、专业/制造、野兽训练。
- 辅助与增强Tooltip、Tweaks、AFK 屏保、Setup Wizard、配置面板等。
## 2. 加载主线
### 2.1 入口文件
- [Nanami-UI.toc](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Nanami-UI.toc)
负责定义加载顺序、依赖和 SavedVariables。
- [Core.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Core.lua)
定义全局表 `SFrames`、事件分发、初始化总流程、角色配置快照、斜杠命令。
- [Config.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Config.lua)
提供默认配置、主题预设、颜色与样式生成逻辑。
### 2.2 初始化方式
当前插件的模块大致分成两种启动方式:
- `Core.lua` 集中初始化
通过 `SFrames:DoFullInitialize()` 分批启动关键模块与延迟模块,避免登录瞬间内存尖峰。
- 模块自举初始化
一部分界面文件自己监听 `PLAYER_LOGIN` / `ADDON_LOADED` / 专用事件,在对应窗口可用时初始化。
### 2.3 Core 直接管理的模块
`Core.lua` 当前显式初始化的模块包括:
- 立即加载:`Player``Pet``Target``ToT``Party``FloatingTooltip``ActionBars`
- 延迟批量加载:`Raid``Bags``Focus``TalentTree``Minimap``MinimapBuffs``MinimapButton``Chat``MapReveal``WorldMap``ZoneLevelRange``MapIcons``Tweaks``AFKScreen``LootDisplay`
### 2.4 自举型模块
这类文件通常在自身内部注册事件并完成初始化:
- [QuestUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\QuestUI.lua)
- [TrainerUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\TrainerUI.lua)
- [TradeSkillUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\TradeSkillUI.lua)
- [BeastTrainingUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\BeastTrainingUI.lua)
- [BookUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\BookUI.lua)
- [Merchant.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Merchant.lua)
- [Mail.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Mail.lua)
- [SocialUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\SocialUI.lua)
- [SpellBookUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\SpellBookUI.lua)
- [FlightMap.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\FlightMap.lua)
- [QuestLogSkin.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\QuestLogSkin.lua)
## 3. 核心框架与基础设施
### 3.1 核心与公共能力
- [Core.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Core.lua)
事件总线、统一初始化、角色配置快照、斜杠命令、若干兼容性保护。
- [Config.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Config.lua)
默认配置、主题生成、颜色方案、类职业主题映射。
- [Media.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Media.lua)
字体与状态条材质列表,兼容 LibSharedMedia。
- [Factory.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Factory.lua)
通用边框、背景、按钮、下拉、输入框等 UI 工厂方法。
- [Movers.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Movers.lua)
布局模式、拖拽锚点、网格吸附、统一解锁/锁定框体。
- [IconMap.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\IconMap.lua)
`img/icon.tga` 的图标坐标表,供多个界面复用。
- [Bindings.xml](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Bindings.xml)
插件按键绑定入口。
- [ConfigUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\ConfigUI.lua)
综合配置面板,覆盖 UI、背包、动作条、小地图、角色等多页设置。
- [SetupWizard.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\SetupWizard.lua)
首次安装向导,也可作为二次配置入口。
- [GameMenu.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\GameMenu.lua)
游戏菜单增强,提供 Nanami-UI 相关入口。
- [MinimapButton.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\MinimapButton.lua)
小地图按钮,用于打开主要面板或功能入口。
### 3.2 斜杠命令
[Core.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Core.lua) 中已确认的主命令入口为:
- `/nanami`
- `/nui`
从当前代码可见,至少包含以下子命令:
- `unlock` / `move`
- `lock`
- `chatreset`
- `test`
- `partyh`
- `partyv`
- `partylayout`
- `focus`
- `clearfocus`
- `targetfocus`
- `fcast`
- `focushelp`
此外还有若干模块自己的调试或快捷命令:
- [DarkmoonGuide.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\DarkmoonGuide.lua)`/dmf``/darkmoon`
- [FlightMap.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\FlightMap.lua)`/ftcdebug``/ftcexport`
- [GearScore.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\GearScore.lua)`/gsdebug`
- [BeastTrainingUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\BeastTrainingUI.lua)`/btdebug`
## 4. 功能与代码文件对应
### 4.1 单位框体与战斗信息
- [Units\Player.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Units\Player.lua)
玩家框体,包含血量/能量、头像、施法条、buff/debuff、移动锚点等。
- [Units\Pet.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Units\Pet.lua)
宠物框体。
- [Units\Target.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Units\Target.lua)
目标框体,含施法识别与法术图标映射。
- [Units\ToT.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Units\ToT.lua)
目标的目标框体。
- [Units\Party.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Units\Party.lua)
队伍框体,支持布局切换、拖拽物品到队友等逻辑。
- [Units\Raid.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Units\Raid.lua)
团队框体。
- [Focus.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Focus.lua)
焦点系统,支持设置焦点、目标焦点、对焦点施法。
- [Tooltip.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Tooltip.lua)
浮动 Tooltip、单位信息扩展。
- [MinimapBuffs.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\MinimapBuffs.lua)
小地图附近的 buff/debuff 重新排布。
- [ActionBars.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\ActionBars.lua)
主动作条、姿态条、宠物条、键位显示、范围检测、布局修正。
- [KeyBindManager.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\KeyBindManager.lua)
按键绑定查看、导入导出、编辑。
### 4.2 地图与导航
- [Minimap.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Minimap.lua)
小地图换肤,支持不同边框样式与职业主题皮肤。
- [WorldMap.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\WorldMap.lua)
世界地图整体重绘与窗口化,是地图类功能的主要承载文件。
- [MapReveal.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\MapReveal.lua)
迷雾揭示与 overlay 数据补丁。
- [ZoneLevelRange.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\ZoneLevelRange.lua)
世界地图区域等级、阵营归属、副本/团队本信息提示。
- [MapIcons.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\MapIcons.lua)
世界地图、小地图、战场地图上的队伍/团队职业图标显示。
- [DarkmoonGuide.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\DarkmoonGuide.lua)
暗月马戏团 Sayge Buff 指引面板。
- [DarkmoonMapMarker.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\DarkmoonMapMarker.lua)
现在只保留兼容占位,实际标记逻辑已并入 `WorldMap.lua`
- [FlightMap.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\FlightMap.lua)
飞行点目的地列表、飞行进度条与路径可视化。
- [FlightData.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\FlightData.lua)
飞行路线耗时数据库。
### 4.3 聊天与社交
- [Chat.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Chat.lua)
聊天框全面换肤与接管,保留原生聊天后端。
- [Whisper.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Whisper.lua)
私聊联系人和聊天记录容器。
- [SocialUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\SocialUI.lua)
好友、忽略、Who、公会、团队管理界面重做。
### 4.4 背包、银行、商人、交易、邮件、拾取
- [Bags\Offline.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Bags\Offline.lua)
跨角色离线背包/银行数据采集。
- [Bags\Core.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Bags\Core.lua)
背包模块总入口。
- [Bags\Container.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Bags\Container.lua)
背包容器 UI 与交互。
- [Bags\Bank.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Bags\Bank.lua)
银行界面与银行容器管理。
- [Bags\Sort.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Bags\Sort.lua)
背包整理逻辑。
- [Bags\Features.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Bags\Features.lua)
自动卖灰、自动开包等附加功能。
- [Merchant.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Merchant.lua)
商人面板重做,含回购、批量购买、修理。
- [Trade.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Trade.lua)
交易窗口重做。
- [Mail.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Mail.lua)
邮件面板重做,支持收件管理与多物品发送队列。
- [LootDisplay.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\LootDisplay.lua)
接管原生拾取窗口视觉层,同时尽量保留原生按钮交互。
- [SellPriceDB.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\SellPriceDB.lua)
物品售卖价格数据库,服务于商人/背包场景。
### 4.5 任务、书籍、日志
- [QuestUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\QuestUI.lua)
NPC 任务对话、详情、进度、奖励等流程界面。
- [QuestLogSkin.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\QuestLogSkin.lua)
任务日志窗口换肤。
- [BookUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\BookUI.lua)
书籍/卷轴阅读窗口重做。
### 4.6 角色、法术、训练、专业
- [CharacterPanel.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\CharacterPanel.lua)
角色面板核心文件,体量很大,集成装备、属性、模型、称号、套装等内容。
- [StatSummary.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\StatSummary.lua)
装备统计摘要与独立统计面板。
- [InspectPanel.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\InspectPanel.lua)
观察面板与目标装备信息展示。
- [SpellBookUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\SpellBookUI.lua)
法术书重做。
- [TrainerUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\TrainerUI.lua)
职业训练师界面重做。
- [TradeSkillUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\TradeSkillUI.lua)
专业制造/制作界面重做。
- [BeastTrainingUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\BeastTrainingUI.lua)
猎人宠物训练界面重做。
- [PetStableSkin.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\PetStableSkin.lua)
宠物管理员/兽栏界面皮肤化。
- [Units\TalentTree.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Units\TalentTree.lua)
天赋树显示模块。
- [TalentDefaultDB.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\TalentDefaultDB.lua)
默认天赋数据库。
- [TradeSkillDB.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\TradeSkillDB.lua)
专业/配方静态数据库。
- [ClassSkillData.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\ClassSkillData.lua)
职业训练技能数据。
- [GearScore.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\GearScore.lua)
装等/装备评分能力。
### 4.7 其他增强
- [Tweaks.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Tweaks.lua)
各种杂项增强,当前能看到包括自动下马/取消变形、冷却文本、世界地图 Tooltip 处理、战斗提示等。
- [AFKScreen.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\AFKScreen.lua)
AFK 全屏界面,显示模型、时间、世界 Buff 等信息。
- [Roll.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Roll.lua)
与掷骰/需求贪婪相关的界面或逻辑扩展。
## 5. 数据、资源与文档文件
### 5.1 图片与材质资源
- `img/`
存放小地图底图、职业风格贴图、动作条材质、图标图集等资源。
- [IconMap.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\IconMap.lua)
`img/icon.tga` 提供图标索引。
### 5.2 现有文档
- [docs\LootDisplay-技术要点.md](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\docs\LootDisplay-技术要点.md)
记录接管拾取窗口时的关键实现思路。
- [docs\AutoDismount-技术总结.md](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\docs\AutoDismount-技术总结.md)
记录自动下马/取消变形的实现方案。
### 5.3 辅助脚本与研究资料
- `agent-tools/`
看起来用于生成或整理职业训练相关数据。
- `*.pdf`
当前仓库中包含乌龟服属性收益研究资料,更偏设计/数值参考,不属于插件运行时代码。
## 6. SavedVariables 与依赖
### 6.1 SavedVariables
根据 [Nanami-UI.toc](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Nanami-UI.toc)
- `SFramesDB`
角色级配置。
- `SFramesGlobalDB`
全局配置和跨角色数据。
### 6.2 可选依赖
`toc` 中声明的可选依赖包括:
- `ShaguTweaks`
- `Blizzard_CombatLog`
- `HealComm-1.0`
- `DruidManaLib-1.0`
- `!Libs`
- `pfQuest`
- `RDbItems`
从代码上看,这些依赖主要用于:
- 治疗预测、Debuff/Overlay 数据读取。
- 地图尺寸与覆盖层补全。
- 额外物品/任务数据支持。
## 7. 维护视角下的重点文件
如果后续要继续分析或改功能,优先关注下面这些“枢纽文件”:
- [Core.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Core.lua)
初始化顺序、模块开关、斜杠命令、配置快照。
- [Config.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Config.lua)
默认值和主题系统。
- [ConfigUI.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\ConfigUI.lua)
用户可见设置入口。
- [CharacterPanel.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\CharacterPanel.lua)
当前仓库里体量最大、耦合也较高的角色面板核心。
- [Chat.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Chat.lua)
体量大、接管深,聊天相关问题通常会从这里开始查。
- [WorldMap.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\WorldMap.lua)
地图增强的主要承载点。
- [Bags\Container.lua](e:\Game\trutle wow\Interface\AddOns\Nanami-UI\Bags\Container.lua)
背包显示与交互核心。
## 8. 当前文档的边界
这份文档是“基础概览版”,目前更偏结构和职责映射,尚未逐项展开:
- 每个模块的详细事件流。
- 每个配置项对应的具体字段。
- 模块之间的调用关系图。
- Turtle WoW 私有 API 或兼容处理的逐点说明。
如果继续往下做,比较自然的下一步是:
1. 补一份“初始化时序图”。
2. 补一份“配置字段与模块对应表”。
3. 针对地图、角色面板、聊天三个大模块分别拆详细设计文档。

BIN
img/bar/bar_elvui.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
img/bar/bar_gradient.tga Normal file

Binary file not shown.

BIN
img/bar/bar_tukui.tga Normal file

Binary file not shown.

BIN
img/progress.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB