完成焦点等开发
This commit is contained in:
126
ActionBars.lua
126
ActionBars.lua
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
118
Bags/Sort.lua
118
Bags/Sort.lua
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
48
Chat.lua
48
Chat.lua
@@ -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*")
|
||||
|
||||
1891
ConfigUI.lua
1891
ConfigUI.lua
File diff suppressed because it is too large
Load Diff
183
Core.lua
183
Core.lua
@@ -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()
|
||||
|
||||
@@ -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
235
Mail.lua
@@ -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
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -333,5 +333,4 @@ function MI:Initialize()
|
||||
UpdateMinimapDots()
|
||||
end)
|
||||
|
||||
SFrames:Print("地图职业图标模块已加载")
|
||||
end
|
||||
|
||||
39
Media.lua
39
Media.lua
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,7 +35,6 @@ Units\ToT.lua
|
||||
Units\Party.lua
|
||||
TalentDefaultDB.lua
|
||||
Units\TalentTree.lua
|
||||
SellPriceDB.lua
|
||||
GearScore.lua
|
||||
Tooltip.lua
|
||||
Units\Raid.lua
|
||||
|
||||
30
QuestUI.lua
30
QuestUI.lua
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
118
Tooltip.lua
118
Tooltip.lua
@@ -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()
|
||||
|
||||
428
TrainerUI.lua
428
TrainerUI.lua
@@ -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)
|
||||
|
||||
250
Tweaks.lua
250
Tweaks.lua
@@ -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
|
||||
|
||||
103
Units/Party.lua
103
Units/Party.lua
@@ -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)
|
||||
|
||||
144
Units/Pet.lua
144
Units/Pet.lua
@@ -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)
|
||||
|
||||
366
Units/Player.lua
366
Units/Player.lua
@@ -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)
|
||||
|
||||
175
Units/Raid.lua
175
Units/Raid.lua
@@ -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
|
||||
|
||||
332
Units/Target.lua
332
Units/Target.lua
@@ -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
|
||||
|
||||
@@ -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
333
docs/插件功能概览.md
Normal 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
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
BIN
img/bar/bar_gradient.tga
Normal file
Binary file not shown.
BIN
img/bar/bar_tukui.tga
Normal file
BIN
img/bar/bar_tukui.tga
Normal file
Binary file not shown.
BIN
img/progress.tga
Normal file
BIN
img/progress.tga
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Reference in New Issue
Block a user