Files
Nanami-UI/LootDisplay.lua
2026-04-09 09:46:47 +08:00

1020 lines
36 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--------------------------------------------------------------------------------
-- Nanami-UI: Loot Display (拾取界面 + 已拾取提示)
--------------------------------------------------------------------------------
SFrames = SFrames or {}
SFrames.LootDisplay = SFrames.LootDisplay or {}
local LD = SFrames.LootDisplay
--------------------------------------------------------------------------------
-- Constants
--------------------------------------------------------------------------------
local ROW_HEIGHT = 32
local ROW_WIDTH = 180
local ROW_GAP = 2
local ICON_SIZE = 26
local TITLE_HEIGHT = 22
local ALERT_WIDTH = 240
local ALERT_HEIGHT = 32
local ALERT_GAP = 3
local ALERT_ICON = 24
local ALERT_FADE_DUR = 0.6
local ALERT_FLOAT = 22
local ALERT_HOLD = 3.5
local ALERT_STAGGER = 0.25
local MAX_ALERTS = 10
local QUALITY_COLORS = {
[0] = { 0.62, 0.62, 0.62 },
[1] = { 0.92, 0.92, 0.88 },
[2] = { 0.12, 1.00, 0.00 },
[3] = { 0.00, 0.44, 0.87 },
[4] = { 0.64, 0.21, 0.93 },
[5] = { 1.00, 0.50, 0.00 },
[6] = { 0.90, 0.80, 0.50 },
}
-- Shared rounded backdrop template (matches rest of Nanami-UI)
local ROUND_BACKDROP = {
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
}
local ROUND_BACKDROP_SMALL = {
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
}
local ROUND_BACKDROP_SHADOW = {
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
}
local ICON_BACKDROP = {
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 10,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
}
local ITEMS_PER_PAGE = 4
local PAGE_BAR_H = 20
local lootRows = {}
local activeAlerts = {}
local alertAnchor = nil
local alertPool = {}
local origLootFrameUpdate = nil
local ShowLootPage
local HideBagFullWarning
--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------
local function T()
return SFrames.ActiveTheme or {}
end
local function Font()
return (SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
end
local function GetDB()
if not SFramesDB then SFramesDB = {} end
if type(SFramesDB.lootDisplay) ~= "table" then
SFramesDB.lootDisplay = {
enable = true,
alertEnable = true,
alertFadeDelay = ALERT_HOLD,
scale = 1.0,
followCursor = false,
}
end
if SFramesDB.lootDisplay.followCursor == nil then
SFramesDB.lootDisplay.followCursor = false
end
return SFramesDB.lootDisplay
end
local function QColor(quality)
local q = tonumber(quality) or 1
local c = QUALITY_COLORS[q]
if c then return c[1], c[2], c[3] end
return 0.92, 0.92, 0.88
end
local function ColorHex(r, g, b)
return string.format("%02x%02x%02x", r * 255, g * 255, b * 255)
end
local function GetItemTexture(itemIdOrLink)
if not itemIdOrLink then return nil end
local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 = GetItemInfo(itemIdOrLink)
if a10 and type(a10) == "string" then return a10 end
if a9 and type(a9) == "string" and string.find(a9, "Interface") then return a9 end
if a8 and type(a8) == "string" and string.find(a8, "Interface") then return a8 end
return nil
end
local function ParseItemLink(link)
if not link then return nil end
local _, _, colorHex, itemId, itemName = string.find(link, "|c(%x+)|Hitem:(%d+).-|h%[(.-)%]|h|r")
if not itemName then return nil end
local id = tonumber(itemId) or 0
local _, _, quality = GetItemInfo(id)
return itemName, quality or 1, id
end
--------------------------------------------------------------------------------
-- ▸ LOOT FRAME (拾取窗口)
--------------------------------------------------------------------------------
local lootFrame = nil
local function CreateLootFrame()
if lootFrame then return lootFrame end
local th = T()
local bg = th.panelBg or { 0.06, 0.05, 0.09, 0.95 }
local bd = th.panelBorder or { 0.30, 0.25, 0.42, 0.90 }
local acc = th.accent or { 1, 0.5, 0.8, 0.98 }
lootFrame = CreateFrame("Frame", "NanamiLootFrame", UIParent)
lootFrame:SetFrameStrata("FULLSCREEN_DIALOG")
lootFrame:SetFrameLevel(50)
lootFrame:SetWidth(ROW_WIDTH + 14)
lootFrame:SetHeight(TITLE_HEIGHT + 3 * (ROW_HEIGHT + ROW_GAP) + 10)
lootFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 50, -200)
lootFrame:SetClampedToScreen(true)
lootFrame:SetMovable(true)
lootFrame:EnableMouse(false)
-- Shadow
local shadow = CreateFrame("Frame", nil, lootFrame)
shadow:SetPoint("TOPLEFT", lootFrame, "TOPLEFT", -5, 5)
shadow:SetPoint("BOTTOMRIGHT", lootFrame, "BOTTOMRIGHT", 5, -5)
shadow:SetFrameLevel(math.max(lootFrame:GetFrameLevel() - 1, 0))
shadow:SetBackdrop(ROUND_BACKDROP_SHADOW)
shadow:SetBackdropColor(0, 0, 0, 0.55)
shadow:SetBackdropBorderColor(0, 0, 0, 0.40)
shadow:EnableMouse(false)
-- Background blocker: prevents clicks passing through to frames behind the loot window
local blocker = CreateFrame("Frame", nil, lootFrame)
blocker:SetAllPoints(lootFrame)
blocker:SetFrameLevel(lootFrame:GetFrameLevel())
blocker:EnableMouse(true)
-- Main panel (rounded)
lootFrame:SetBackdrop(ROUND_BACKDROP)
lootFrame:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 0.95)
lootFrame:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 0.90)
-- Top accent line
local acLine = lootFrame:CreateTexture(nil, "ARTWORK")
acLine:SetTexture("Interface\\Buttons\\WHITE8x8")
acLine:SetHeight(2)
acLine:SetPoint("TOPLEFT", lootFrame, "TOPLEFT", 5, -5)
acLine:SetPoint("TOPRIGHT", lootFrame, "TOPRIGHT", -5, -5)
acLine:SetVertexColor(acc[1], acc[2], acc[3], 0.65)
-- Title bar: handles dragging the entire loot frame
local titleBar = CreateFrame("Frame", nil, lootFrame)
titleBar:SetHeight(TITLE_HEIGHT)
titleBar:SetPoint("TOPLEFT", lootFrame, "TOPLEFT", 0, 0)
titleBar:SetPoint("TOPRIGHT", lootFrame, "TOPRIGHT", 0, 0)
titleBar:SetFrameLevel(lootFrame:GetFrameLevel() + 1)
titleBar:EnableMouse(true)
titleBar:RegisterForDrag("LeftButton")
titleBar:SetScript("OnDragStart", function() lootFrame:StartMoving() end)
titleBar:SetScript("OnDragStop", function() lootFrame:StopMovingOrSizing() end)
-- Title text
local titleFS = titleBar:CreateFontString(nil, "OVERLAY")
titleFS:SetFont(Font(), 11, "OUTLINE")
titleFS:SetPoint("TOPLEFT", lootFrame, "TOPLEFT", 10, -8)
local tc = th.title or { 1, 0.88, 1 }
titleFS:SetTextColor(tc[1], tc[2], tc[3], 0.95)
titleFS:SetText("拾取")
lootFrame._title = titleFS
-- Close button
local closeBtn = CreateFrame("Button", nil, lootFrame)
closeBtn:SetWidth(18)
closeBtn:SetHeight(18)
closeBtn:SetPoint("TOPRIGHT", lootFrame, "TOPRIGHT", -5, -5)
closeBtn:SetFrameLevel(lootFrame:GetFrameLevel() + 5)
closeBtn:RegisterForClicks("LeftButtonUp")
closeBtn:SetBackdrop(ROUND_BACKDROP_SMALL)
local cbg = th.buttonBg or { 0.35, 0.08, 0.12, 0.85 }
local cbd = th.buttonBorder or { 0.50, 0.18, 0.22, 0.80 }
closeBtn:SetBackdropColor(cbg[1], cbg[2], cbg[3], cbg[4] or 0.85)
closeBtn:SetBackdropBorderColor(cbd[1], cbd[2], cbd[3], cbd[4] or 0.80)
local closeFS = closeBtn:CreateFontString(nil, "OVERLAY")
closeFS:SetFont(Font(), 10, "OUTLINE")
closeFS:SetPoint("CENTER", 0, 0)
closeFS:SetText("×")
closeFS:SetTextColor(0.9, 0.65, 0.65, 1)
closeBtn:SetScript("OnClick", function() CloseLoot() end)
closeBtn:SetScript("OnEnter", function()
this:SetBackdropColor(0.55, 0.12, 0.15, 0.95)
this:SetBackdropBorderColor(0.9, 0.30, 0.35, 1)
closeFS:SetTextColor(1, 1, 1, 1)
end)
closeBtn:SetScript("OnLeave", function()
this:SetBackdropColor(cbg[1], cbg[2], cbg[3], cbg[4] or 0.85)
this:SetBackdropBorderColor(cbd[1], cbd[2], cbd[3], cbd[4] or 0.80)
closeFS:SetTextColor(0.9, 0.65, 0.65, 1)
end)
-- Page controls (visible only when > ITEMS_PER_PAGE items)
local pageBar = CreateFrame("Frame", nil, lootFrame)
pageBar:SetHeight(PAGE_BAR_H)
pageBar:SetPoint("BOTTOMLEFT", lootFrame, "BOTTOMLEFT", 7, 4)
pageBar:SetPoint("BOTTOMRIGHT", lootFrame, "BOTTOMRIGHT", -7, 4)
pageBar:SetFrameLevel(lootFrame:GetFrameLevel() + 3)
pageBar:EnableMouse(false)
pageBar:Hide()
lootFrame._pageBar = pageBar
local pageFS = pageBar:CreateFontString(nil, "OVERLAY")
pageFS:SetFont(Font(), 9, "OUTLINE")
pageFS:SetPoint("CENTER", pageBar, "CENTER", 0, 0)
pageFS:SetTextColor(0.75, 0.75, 0.80, 0.95)
lootFrame._pageText = pageFS
local dim = th.dimText or { 0.55, 0.55, 0.60 }
local prevBtn = CreateFrame("Button", nil, pageBar)
prevBtn:SetWidth(22)
prevBtn:SetHeight(16)
prevBtn:SetPoint("RIGHT", pageFS, "LEFT", -8, 0)
prevBtn:SetFrameLevel(pageBar:GetFrameLevel() + 1)
prevBtn:RegisterForClicks("LeftButtonUp")
prevBtn:SetBackdrop(ROUND_BACKDROP_SMALL)
prevBtn:SetBackdropColor(0.10, 0.09, 0.14, 0.80)
prevBtn:SetBackdropBorderColor(0.25, 0.22, 0.35, 0.60)
local prevFS2 = prevBtn:CreateFontString(nil, "OVERLAY")
prevFS2:SetFont(Font(), 10, "OUTLINE")
prevFS2:SetPoint("CENTER", 0, 0)
prevFS2:SetText("<")
prevFS2:SetTextColor(dim[1], dim[2], dim[3], 0.90)
prevBtn:SetScript("OnClick", function()
if lootFrame._page and lootFrame._page > 1 then
lootFrame._page = lootFrame._page - 1
ShowLootPage()
end
end)
prevBtn:SetScript("OnEnter", function()
this:SetBackdropBorderColor(acc[1], acc[2], acc[3], 0.70)
end)
prevBtn:SetScript("OnLeave", function()
this:SetBackdropBorderColor(0.25, 0.22, 0.35, 0.60)
end)
lootFrame._prevBtn = prevBtn
local nextBtn = CreateFrame("Button", nil, pageBar)
nextBtn:SetWidth(22)
nextBtn:SetHeight(16)
nextBtn:SetPoint("LEFT", pageFS, "RIGHT", 8, 0)
nextBtn:SetFrameLevel(pageBar:GetFrameLevel() + 1)
nextBtn:RegisterForClicks("LeftButtonUp")
nextBtn:SetBackdrop(ROUND_BACKDROP_SMALL)
nextBtn:SetBackdropColor(0.10, 0.09, 0.14, 0.80)
nextBtn:SetBackdropBorderColor(0.25, 0.22, 0.35, 0.60)
local nextFS2 = nextBtn:CreateFontString(nil, "OVERLAY")
nextFS2:SetFont(Font(), 10, "OUTLINE")
nextFS2:SetPoint("CENTER", 0, 0)
nextFS2:SetText(">")
nextFS2:SetTextColor(dim[1], dim[2], dim[3], 0.90)
nextBtn:SetScript("OnClick", function()
if lootFrame._page and lootFrame._totalPages and lootFrame._page < lootFrame._totalPages then
lootFrame._page = lootFrame._page + 1
ShowLootPage()
end
end)
nextBtn:SetScript("OnEnter", function()
this:SetBackdropBorderColor(acc[1], acc[2], acc[3], 0.70)
end)
nextBtn:SetScript("OnLeave", function()
this:SetBackdropBorderColor(0.25, 0.22, 0.35, 0.60)
end)
lootFrame._nextBtn = nextBtn
-- Bag-full warning (hidden by default)
local bagFullFS = lootFrame:CreateFontString(nil, "OVERLAY")
bagFullFS:SetFont(Font(), 9, "OUTLINE")
bagFullFS:SetPoint("LEFT", titleFS, "RIGHT", 6, 0)
bagFullFS:SetTextColor(1.0, 0.30, 0.30, 1.0)
bagFullFS:Hide()
lootFrame._bagFullText = bagFullFS
-- Escape key closes our loot frame
table.insert(UISpecialFrames, "NanamiLootFrame")
lootFrame:SetScript("OnHide", function()
if not this._closingLoot then
CloseLoot()
end
end)
lootFrame:Hide()
return lootFrame
end
--------------------------------------------------------------------------------
-- Loot row
--------------------------------------------------------------------------------
local function CreateLootRow(parent, index)
local th = T()
local slotBg = th.slotBg or { 0.07, 0.06, 0.10, 0.85 }
local slotBd = th.slotBorder or { 0.18, 0.16, 0.28, 0.60 }
local hoverBd = th.slotHover or { 0.38, 0.40, 0.90 }
local acc = th.accent or { 1, 0.5, 0.8 }
local row = CreateFrame("Button", "NanamiLootRow" .. index, parent)
row:SetWidth(ROW_WIDTH)
row:SetHeight(ROW_HEIGHT)
row:SetFrameLevel(parent:GetFrameLevel() + 2)
row:RegisterForClicks("LeftButtonUp")
row:SetBackdrop(ROUND_BACKDROP_SMALL)
row:SetBackdropColor(slotBg[1], slotBg[2], slotBg[3], slotBg[4] or 0.85)
row:SetBackdropBorderColor(slotBd[1], slotBd[2], slotBd[3], slotBd[4] or 0.60)
-- Left quality accent bar
local qBar = row:CreateTexture(nil, "OVERLAY")
qBar:SetTexture("Interface\\Buttons\\WHITE8X8")
qBar:SetWidth(2)
qBar:SetPoint("TOPLEFT", row, "TOPLEFT", 3, -3)
qBar:SetPoint("BOTTOMLEFT", row, "BOTTOMLEFT", 3, 3)
qBar:SetVertexColor(1, 1, 1, 0.6)
row.qBar = qBar
-- Icon frame (rounded border)
local iconFrame = CreateFrame("Frame", nil, row)
iconFrame:SetWidth(ICON_SIZE + 4)
iconFrame:SetHeight(ICON_SIZE + 4)
iconFrame:SetPoint("LEFT", row, "LEFT", 7, 0)
iconFrame:SetFrameLevel(row:GetFrameLevel() + 1)
iconFrame:SetBackdrop(ICON_BACKDROP)
iconFrame:SetBackdropColor(0, 0, 0, 0.60)
iconFrame:SetBackdropBorderColor(slotBd[1], slotBd[2], slotBd[3], 0.70)
iconFrame:EnableMouse(false)
row.iconFrame = iconFrame
local icon = iconFrame:CreateTexture(nil, "ARTWORK")
icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 2, -2)
icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -2, 2)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
row.icon = icon
-- Name
local nameFS = row:CreateFontString(nil, "OVERLAY")
nameFS:SetFont(Font(), 10, "OUTLINE")
nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 4, 0)
nameFS:SetPoint("RIGHT", row, "RIGHT", -6, 0)
nameFS:SetJustifyH("LEFT")
row.nameFS = nameFS
-- Count (bottom-right of icon)
local countFS = row:CreateFontString(nil, "OVERLAY")
countFS:SetFont(Font(), 8, "OUTLINE")
countFS:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -2, 2)
countFS:SetJustifyH("RIGHT")
countFS:SetTextColor(1, 1, 1, 0.95)
row.countFS = countFS
row._slotBg = slotBg
row._slotBd = slotBd
row._hoverBd = hoverBd
row._acc = acc
row:EnableMouse(false)
row:Hide()
return row
end
--------------------------------------------------------------------------------
-- Bag-full warning helpers
--------------------------------------------------------------------------------
local function ShowBagFullWarning()
if not lootFrame or not lootFrame:IsShown() then return end
if lootFrame._bagFullText then
lootFrame._bagFullText:SetText("背包已满")
lootFrame._bagFullText:Show()
end
end
HideBagFullWarning = function()
if lootFrame and lootFrame._bagFullText then
lootFrame._bagFullText:Hide()
end
end
--------------------------------------------------------------------------------
-- Show current page
--------------------------------------------------------------------------------
ShowLootPage = function()
if not lootFrame then return end
local numItems = lootFrame._numItems or 0
local page = lootFrame._page or 1
local totalPages = lootFrame._totalPages or 1
local startSlot = (page - 1) * ITEMS_PER_PAGE + 1
local endSlot = startSlot + ITEMS_PER_PAGE - 1
if endSlot > numItems then endSlot = numItems end
local slotsOnPage = endSlot - startSlot + 1
if slotsOnPage < 0 then slotsOnPage = 0 end
while table.getn(lootRows) < ITEMS_PER_PAGE do
local idx = table.getn(lootRows) + 1
lootRows[idx] = CreateLootRow(lootFrame, idx)
end
for i = 1, table.getn(lootRows) do lootRows[i]:Hide() end
for i = 1, ITEMS_PER_PAGE do
local nb = _G["LootButton" .. i]
if nb then nb:Hide() end
end
local hasPages = totalPages > 1
local bottomPad = hasPages and (PAGE_BAR_H + 6) or 6
local totalH = TITLE_HEIGHT + (slotsOnPage * (ROW_HEIGHT + ROW_GAP)) + bottomPad + 4
lootFrame:SetHeight(totalH)
-- Build visual rows
for btnIdx = 1, slotsOnPage do
local slotIdx = startSlot + btnIdx - 1
local row = lootRows[btnIdx]
if not row then break end
row:ClearAllPoints()
row:SetPoint("TOPLEFT", lootFrame, "TOPLEFT", 7,
-(TITLE_HEIGHT + 2 + (btnIdx - 1) * (ROW_HEIGHT + ROW_GAP)))
row:SetWidth(ROW_WIDTH)
row.slotIndex = slotIdx
local texture, itemName, quantity, quality = GetLootSlotInfo(slotIdx)
if texture then
row.icon:SetTexture(texture)
local r, g, b = QColor(quality)
row._qualColor = { r, g, b }
row.qBar:SetVertexColor(r, g, b, 0.90)
row.iconFrame:SetBackdropBorderColor(r, g, b, 0.65)
row:SetBackdropBorderColor(r, g, b, 0.30)
row:SetBackdropColor(row._slotBg[1], row._slotBg[2], row._slotBg[3], row._slotBg[4] or 0.85)
row.iconFrame:SetAlpha(1)
row.nameFS:SetText("|cff" .. ColorHex(r, g, b) .. (itemName or "") .. "|r")
if quantity and quantity > 1 then
row.countFS:SetText(tostring(quantity))
else
row.countFS:SetText("")
end
else
row._qualColor = nil
row.icon:SetTexture("")
row.iconFrame:SetAlpha(0.25)
row.qBar:SetVertexColor(0.3, 0.3, 0.3, 0.30)
row.nameFS:SetText("")
row.countFS:SetText("")
row:SetBackdropColor(0.04, 0.04, 0.06, 0.40)
row:SetBackdropBorderColor(0.12, 0.12, 0.18, 0.25)
row.iconFrame:SetBackdropBorderColor(0.15, 0.15, 0.20, 0.30)
end
row:Show()
end
-- Set up the native LootFrame so it stays alive (required by the
-- engine) but completely invisible. We do NOT call origLootFrameUpdate
-- because it uses a different items-per-page (3 when paginated vs our 4)
-- which mis-calculates slot indices.
if LootFrame then
LootFrame.numLootItems = numItems
LootFrame.page = page
if not LootFrame:IsShown() then LootFrame:Show() end
end
-- Directly configure native LootButtons with the correct slot for
-- each visual row. SetSlot is the C++ binding that the native
-- LootButton_OnClick / LootFrameItem_OnClick reads via this.slot.
for btnIdx = 1, ITEMS_PER_PAGE do
local nb = _G["LootButton" .. btnIdx]
local row = lootRows[btnIdx]
if nb and row and row:IsShown() and row._qualColor then
local realSlot = row.slotIndex
-- SetSlot is the native C++ method; .slot is the Lua mirror.
if nb.SetSlot then nb:SetSlot(realSlot) end
nb.slot = realSlot
local _, _, _, rq = GetLootSlotInfo(realSlot)
nb.quality = rq or 0
nb:ClearAllPoints()
nb:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
nb:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
nb:SetFrameStrata("FULLSCREEN_DIALOG")
nb:SetFrameLevel(row:GetFrameLevel() + 10)
nb:SetAlpha(0)
nb:EnableMouse(true)
nb:Show()
nb._nanamiRow = row
nb:SetScript("OnEnter", function()
local s = this.slot
if s then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetLootItem(s)
if CursorUpdate then CursorUpdate() end
end
local r2 = this._nanamiRow
if r2 and r2._qualColor then
local qc = r2._qualColor
r2:SetBackdropBorderColor(qc[1], qc[2], qc[3], 0.70)
r2:SetBackdropColor(qc[1]*0.15, qc[2]*0.15, qc[3]*0.15, 0.90)
end
end)
nb:SetScript("OnLeave", function()
GameTooltip:Hide()
local r2 = this._nanamiRow
if r2 then
if r2._qualColor then
local qc = r2._qualColor
r2:SetBackdropBorderColor(qc[1], qc[2], qc[3], 0.30)
else
r2:SetBackdropBorderColor(r2._slotBd[1], r2._slotBd[2],
r2._slotBd[3], r2._slotBd[4] or 0.60)
end
r2:SetBackdropColor(r2._slotBg[1], r2._slotBg[2],
r2._slotBg[3], r2._slotBg[4] or 0.85)
end
end)
else
if nb then nb:Hide() end
end
end
if hasPages then
lootFrame._pageText:SetText(page .. "/" .. totalPages)
lootFrame._pageBar:Show()
else
lootFrame._pageBar:Hide()
end
end
--------------------------------------------------------------------------------
-- Update loot frame (direct slot mapping, no compaction)
--------------------------------------------------------------------------------
local function UpdateLootFrame()
local db = GetDB()
if not db.enable then return end
local numItems = GetNumLootItems()
if not numItems or numItems == 0 then
if lootFrame then lootFrame:Hide() end
return
end
CreateLootFrame()
lootFrame._numItems = numItems
lootFrame._totalPages = math.ceil(numItems / ITEMS_PER_PAGE)
if not lootFrame._page or lootFrame._page > lootFrame._totalPages then
lootFrame._page = 1
end
lootFrame:SetWidth(ROW_WIDTH + 14)
lootFrame:SetScale(db.scale or 1.0)
ShowLootPage()
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()
end
local function CloseLootFrame()
if lootFrame then
lootFrame._closingLoot = true
lootFrame:Hide()
lootFrame._closingLoot = nil
end
for i = 1, ITEMS_PER_PAGE do
local nb = _G["LootButton" .. i]
if nb then nb:Hide() end
end
for i = 1, table.getn(lootRows) do lootRows[i]:Hide() end
HideBagFullWarning()
if LootFrame then LootFrame:Hide() end
end
--------------------------------------------------------------------------------
-- ▸ LOOT ALERTS (已拾取提示)
--------------------------------------------------------------------------------
local function CreateAlertAnchor()
if alertAnchor then return alertAnchor end
alertAnchor = CreateFrame("Frame", "NanamiLootAlertAnchor", UIParent)
alertAnchor:SetWidth(ALERT_WIDTH)
alertAnchor:SetHeight(ALERT_HEIGHT)
alertAnchor:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 40, 340)
alertAnchor:EnableMouse(false)
if SFrames.Movers and SFrames.Movers.ApplyPosition then
SFrames.Movers:ApplyPosition("LootAlert", alertAnchor,
"BOTTOMLEFT", "UIParent", "BOTTOMLEFT", 40, 340)
end
return alertAnchor
end
local function CreateAlertFrame()
local th = T()
local idx = table.getn(alertPool) + 1
local bg = th.panelBg or { 0.06, 0.05, 0.09, 0.92 }
local bd = th.panelBorder or { 0.30, 0.25, 0.42, 0.80 }
local f = CreateFrame("Frame", "NanamiLootAlert" .. idx, UIParent)
f:SetFrameStrata("HIGH")
f:SetFrameLevel(20)
f:SetWidth(ALERT_WIDTH)
f:SetHeight(ALERT_HEIGHT)
-- Rounded backdrop matching UI theme
f:SetBackdrop(ROUND_BACKDROP_SMALL)
f:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 0.92)
f:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 0.80)
-- Left quality accent bar
local qBar = f:CreateTexture(nil, "OVERLAY")
qBar:SetTexture("Interface\\Buttons\\WHITE8X8")
qBar:SetWidth(2)
qBar:SetPoint("TOPLEFT", f, "TOPLEFT", 3, -3)
qBar:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 3, 3)
qBar:SetVertexColor(1, 1, 1, 0.7)
f.qBar = qBar
-- Icon with rounded border
local iconFrame = CreateFrame("Frame", nil, f)
iconFrame:SetWidth(ALERT_ICON + 4)
iconFrame:SetHeight(ALERT_ICON + 4)
iconFrame:SetPoint("LEFT", f, "LEFT", 7, 0)
iconFrame:SetFrameLevel(f:GetFrameLevel() + 1)
iconFrame:SetBackdrop(ICON_BACKDROP)
iconFrame:SetBackdropColor(0, 0, 0, 0.55)
iconFrame:SetBackdropBorderColor(0.25, 0.22, 0.30, 0.6)
f.iconFrame = iconFrame
local icon = iconFrame:CreateTexture(nil, "ARTWORK")
icon:SetPoint("TOPLEFT", iconFrame, "TOPLEFT", 2, -2)
icon:SetPoint("BOTTOMRIGHT", iconFrame, "BOTTOMRIGHT", -2, 2)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
f.icon = icon
-- Name
local nameFS = f:CreateFontString(nil, "OVERLAY")
nameFS:SetFont(Font(), 10, "OUTLINE")
nameFS:SetPoint("LEFT", iconFrame, "RIGHT", 5, 0)
nameFS:SetPoint("RIGHT", f, "RIGHT", -30, 0)
nameFS:SetJustifyH("LEFT")
f.nameFS = nameFS
-- Count (right side)
local countFS = f:CreateFontString(nil, "OVERLAY")
countFS:SetFont(Font(), 10, "OUTLINE")
countFS:SetPoint("RIGHT", f, "RIGHT", -6, 0)
countFS:SetJustifyH("RIGHT")
local dim = th.dimText or { 0.55, 0.55, 0.60 }
countFS:SetTextColor(dim[1], dim[2], dim[3], 0.95)
f.countFS = countFS
f:Hide()
table.insert(alertPool, f)
return f
end
local function GetAlertFrame()
for i = 1, table.getn(alertPool) do
if not alertPool[i]._inUse then return alertPool[i] end
end
return CreateAlertFrame()
end
-- Layout: newest item at bottom (index 1 = oldest = top, last = newest = bottom slot 0)
local function LayoutAlerts()
CreateAlertAnchor()
local n = table.getn(activeAlerts)
for i = 1, n do
local af = activeAlerts[i]
-- oldest at top, newest at bottom: slot = (n - i)
af:ClearAllPoints()
af:SetPoint("BOTTOMLEFT", alertAnchor, "BOTTOMLEFT", 0, (n - i) * (ALERT_HEIGHT + ALERT_GAP))
end
end
local function RemoveAlert(frame)
frame._inUse = false
frame:SetAlpha(0)
frame:Hide()
frame:SetScript("OnUpdate", nil)
local newActive = {}
for i = 1, table.getn(activeAlerts) do
if activeAlerts[i] ~= frame then
table.insert(newActive, activeAlerts[i])
end
end
activeAlerts = newActive
LayoutAlerts()
end
-- Each alert has its own independent timer; when it expires, just disappear (no float animation)
local function StartAlertTimer(frame, delay)
frame._timerElapsed = 0
frame._timerDelay = delay
frame:SetScript("OnUpdate", function()
this._timerElapsed = (this._timerElapsed or 0) + arg1
if this._timerElapsed >= this._timerDelay then
RemoveAlert(this)
end
end)
end
local function PruneAlerts()
while table.getn(activeAlerts) > MAX_ALERTS do
RemoveAlert(activeAlerts[1])
end
end
local function ShowLootAlert(texture, name, quality, quantity, link)
local db = GetDB()
if not db.alertEnable then return end
-- Stack same item: update count and reset timer
for i = 1, table.getn(activeAlerts) do
local af = activeAlerts[i]
if af._itemName == name then
af._quantity = (af._quantity or 1) + (quantity or 1)
if af._quantity > 1 then
af.countFS:SetText("x" .. af._quantity)
end
af._timerElapsed = 0
return
end
end
PruneAlerts()
CreateAlertAnchor()
local f = GetAlertFrame()
f._inUse = true
f._itemName = name
f._quantity = quantity or 1
f._link = link
local iconTex = texture or "Interface\\Icons\\INV_Misc_QuestionMark"
f.icon:SetTexture(iconTex)
local r, g, b = QColor(quality)
f.qBar:SetVertexColor(r, g, b, 0.90)
f.iconFrame:SetBackdropBorderColor(r, g, b, 0.60)
f.nameFS:SetText("|cff" .. ColorHex(r, g, b) .. (name or "???") .. "|r")
if f._quantity > 1 then
f.countFS:SetText("x" .. f._quantity)
else
f.countFS:SetText("")
end
-- Re-apply theme backdrop colors (pool reuse)
local th = T()
local bg = th.panelBg or { 0.06, 0.05, 0.09, 0.92 }
local bd = th.panelBorder or { 0.30, 0.25, 0.42, 0.80 }
f:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 0.92)
f:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 0.80)
f:SetAlpha(1)
f:Show()
-- New item appended to end of list = bottom position
table.insert(activeAlerts, f)
LayoutAlerts()
local hold = db.alertFadeDelay or ALERT_HOLD
StartAlertTimer(f, hold)
end
--------------------------------------------------------------------------------
-- Parse CHAT_MSG_LOOT
--------------------------------------------------------------------------------
local function ParseLootMessage(msg)
if not msg then return end
local _, _, link, countStr = string.find(msg, "(|c%x+|Hitem.-%[.-%]|h|r)x(%d+)")
if not link then
_, _, link = string.find(msg, "(|c%x+|Hitem.-%[.-%]|h|r)")
end
if link then
local itemName, quality, itemId = ParseItemLink(link)
if itemName then
local count = tonumber(countStr) or 1
-- Try full link first (most reliable), then itemId, then itemString
local tex = GetItemTexture(link)
if not tex and itemId and itemId > 0 then
tex = GetItemTexture(itemId)
end
if not tex and itemId and itemId > 0 then
tex = GetItemTexture("item:" .. itemId .. ":0:0:0")
end
ShowLootAlert(tex, itemName, quality, count, link)
end
return
end
-- Money
local isMoney = false
if string.find(msg, "") or string.find(msg, "") or string.find(msg, "")
or string.find(msg, "[Cc]opper") or string.find(msg, "[Ss]ilver") or string.find(msg, "[Gg]old") then
if string.find(msg, "拾取") or string.find(msg, "获得") or string.find(msg, "loot") or string.find(msg, "receive") then
isMoney = true
end
end
if isMoney then
local cleanMsg = string.gsub(msg, "|c%x+", "")
cleanMsg = string.gsub(cleanMsg, "|r", "")
local _, _, moneyPart = string.find(cleanMsg, "[:]%s*(.+)$")
if not moneyPart then
_, _, moneyPart = string.find(cleanMsg, "loot%s+(.+)$")
end
if not moneyPart then
_, _, moneyPart = string.find(cleanMsg, "获得了?%s*(.+)$")
end
if moneyPart then
moneyPart = string.gsub(moneyPart, "[%.。]+$", "")
ShowLootAlert("Interface\\Icons\\INV_Misc_Coin_01", moneyPart, 1, 1, nil)
end
end
end
--------------------------------------------------------------------------------
-- Initialize
--------------------------------------------------------------------------------
function LD:Initialize()
local db = GetDB()
if not db.enable then return end
CreateLootFrame()
CreateAlertAnchor()
if SFrames.Movers and SFrames.Movers.ApplyPosition then
local applied = SFrames.Movers:ApplyPosition("LootFrame", lootFrame,
"TOPLEFT", "UIParent", "TOPLEFT", 50, -200)
if applied then lootFrame._posApplied = true end
SFrames.Movers:ApplyPosition("LootAlert", alertAnchor,
"BOTTOMLEFT", "UIParent", "BOTTOMLEFT", 40, 340)
end
if SFrames.Movers and SFrames.Movers.RegisterMover then
SFrames.Movers:RegisterMover("LootFrame", lootFrame, "拾取窗口",
"TOPLEFT", "UIParent", "TOPLEFT", 50, -200)
SFrames.Movers:RegisterMover("LootAlert", alertAnchor, "已拾取提示",
"BOTTOMLEFT", "UIParent", "BOTTOMLEFT", 40, 340)
end
SFrames:RegisterEvent("LOOT_OPENED", function()
if GetDB().enable then
if lootFrame then lootFrame._page = 1 end
HideBagFullWarning()
UpdateLootFrame()
end
end)
SFrames:RegisterEvent("LOOT_SLOT_CLEARED", function()
if GetDB().enable and lootFrame and lootFrame:IsShown() then
HideBagFullWarning()
UpdateLootFrame()
end
end)
SFrames:RegisterEvent("UI_ERROR_MESSAGE", function()
if lootFrame and lootFrame:IsShown() then
local msg = arg1
if msg == ERR_INV_FULL
or (INVENTORY_FULL and msg == INVENTORY_FULL)
or (msg and (string.find(msg, "背包已满")
or string.find(msg, "Inventory is full"))) then
ShowBagFullWarning()
end
end
end)
SFrames:RegisterEvent("LOOT_CLOSED", function()
CloseLootFrame()
end)
SFrames:RegisterEvent("LOOT_BIND_CONFIRM", function()
local slot = arg1
if slot then ConfirmLootSlot(slot) end
end)
SFrames:RegisterEvent("CHAT_MSG_LOOT", function()
local playerName = UnitName("player")
if not playerName then return end
local msg = arg1 or ""
if string.find(msg, playerName) or string.find(msg, "你获得") or string.find(msg, "你拾取") or string.find(msg, "You receive") then
ParseLootMessage(msg)
end
end)
-- Save the original LootFrame_Update so ShowLootPage can call it
-- to let native code set up LootButton IDs and OnClick handlers.
origLootFrameUpdate = LootFrame_Update
if LootFrame then
-- Prevent the XML-defined OnHide from calling CloseLoot()
LootFrame:SetScript("OnHide", function() end)
-- Keep LootFrame shown but invisible while our UI is active
local origShow = LootFrame.Show
LootFrame.Show = function(self)
origShow(self)
self:SetAlpha(0)
self:EnableMouse(false)
end
-- Block native LootFrame from hiding while we are looting
local origHide = LootFrame.Hide
LootFrame.Hide = function(self)
if lootFrame and lootFrame:IsShown() then
return
end
origHide(self)
end
end
-- Replace LootFrame_Update: run the original for engine compatibility,
-- then re-apply the correct slot on each native button based on our
-- visual rows (which use ITEMS_PER_PAGE=4, not the native 3-when-paged).
LootFrame_Update = function()
if origLootFrameUpdate then origLootFrameUpdate() end
if not (lootFrame and lootFrame:IsShown()) then return end
for i = 1, ITEMS_PER_PAGE do
local nb = _G["LootButton" .. i]
local row = lootRows[i]
if nb and row and row:IsShown() and row._qualColor then
local realSlot = row.slotIndex
if realSlot then
if nb.SetSlot then nb:SetSlot(realSlot) end
nb.slot = realSlot
end
nb:ClearAllPoints()
nb:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
nb:SetPoint("BOTTOMRIGHT", row, "BOTTOMRIGHT", 0, 0)
nb:SetFrameStrata("FULLSCREEN_DIALOG")
nb:SetFrameLevel(row:GetFrameLevel() + 10)
nb:SetAlpha(0)
nb:EnableMouse(true)
end
end
end
end