-------------------------------------------------------------------------------- -- 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, } 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 -- Let the ORIGINAL Blizzard LootFrame_Update run so that native -- LootButton1-4 get their IDs, slot data, and OnClick set up -- through the trusted native code path (required for LootSlot). if LootFrame then LootFrame.page = page if not LootFrame:IsShown() then LootFrame:Show() end end if origLootFrameUpdate then origLootFrameUpdate() end -- Now reposition the native buttons on top of our visual rows 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 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 slot = this:GetID() if slot then GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:SetLootItem(slot) 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 not lootFrame._posApplied then local hasSaved = false if SFrames.Movers and SFrames.Movers.ApplyPosition then hasSaved = SFrames.Movers:ApplyPosition("LootFrame", lootFrame, "TOPLEFT", "UIParent", "TOPLEFT", 50, -200) end if hasSaved then lootFrame._posApplied = true end end if not lootFrame._posApplied then local cx, cy = GetCursorPosition() local uiS = UIParent:GetEffectiveScale() lootFrame:ClearAllPoints() lootFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", cx / uiS - 30, cy / uiS + 16) end lootFrame:Show() end local function CloseLootFrame() 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 local function LayoutAlerts() CreateAlertAnchor() for i = 1, table.getn(activeAlerts) do local af = activeAlerts[i] if af._fadeState ~= "fading" then af:ClearAllPoints() af:SetPoint("BOTTOMLEFT", alertAnchor, "BOTTOMLEFT", 0, (i - 1) * (ALERT_HEIGHT + ALERT_GAP)) end end end local function RemoveAlert(frame) frame._inUse = false frame:SetAlpha(0) frame:Hide() frame:SetScript("OnUpdate", nil) local newActive = {} for i = 1, table.getn(activeAlerts) do if activeAlerts[i] ~= frame then table.insert(newActive, activeAlerts[i]) end end activeAlerts = newActive LayoutAlerts() end local function StartAlertFade(frame, delay) frame._fadeState = "waiting" frame._fadeElapsed = 0 frame._fadeDelay = delay frame:SetScript("OnUpdate", function() this._fadeElapsed = (this._fadeElapsed or 0) + arg1 if this._fadeState == "waiting" then if this._fadeElapsed >= this._fadeDelay then this._fadeState = "fading" this._fadeElapsed = 0 this._baseY = 0 for idx = 1, table.getn(activeAlerts) do if activeAlerts[idx] == this then this._baseY = (idx - 1) * (ALERT_HEIGHT + ALERT_GAP) break end end end elseif this._fadeState == "fading" then local p = this._fadeElapsed / ALERT_FADE_DUR if p >= 1 then RemoveAlert(this) else this:SetAlpha(1 - p) this:ClearAllPoints() this:SetPoint("BOTTOMLEFT", alertAnchor, "BOTTOMLEFT", 0, this._baseY + p * ALERT_FLOAT) end end end) end local function PruneAlerts() while table.getn(activeAlerts) > MAX_ALERTS do RemoveAlert(activeAlerts[1]) end end local function ShowLootAlert(texture, name, quality, quantity, link) local db = GetDB() if not db.alertEnable then return end for i = 1, table.getn(activeAlerts) do local af = activeAlerts[i] if af._itemName == name and af._fadeState == "waiting" then af._quantity = (af._quantity or 1) + (quantity or 1) if af._quantity > 1 then af.countFS:SetText("x" .. af._quantity) end af._fadeElapsed = 0 return end end PruneAlerts() CreateAlertAnchor() local f = GetAlertFrame() f._inUse = true f._itemName = name f._quantity = quantity or 1 f._link = link -- Set icon texture local iconTex = texture or "Interface\\Icons\\INV_Misc_QuestionMark" f.icon:SetTexture(iconTex) local r, g, b = QColor(quality) f.qBar:SetVertexColor(r, g, b, 0.90) f.iconFrame:SetBackdropBorderColor(r, g, b, 0.60) f.nameFS:SetText("|cff" .. ColorHex(r, g, b) .. (name or "???") .. "|r") if f._quantity > 1 then f.countFS:SetText("x" .. f._quantity) else f.countFS:SetText("") end -- Re-apply theme backdrop colors (pool reuse) local th = T() local bg = th.panelBg or { 0.06, 0.05, 0.09, 0.92 } local bd = th.panelBorder or { 0.30, 0.25, 0.42, 0.80 } f:SetBackdropColor(bg[1], bg[2], bg[3], bg[4] or 0.92) f:SetBackdropBorderColor(bd[1], bd[2], bd[3], bd[4] or 0.80) f:SetAlpha(1) f:Show() table.insert(activeAlerts, f) LayoutAlerts() local hold = db.alertFadeDelay or ALERT_HOLD local stagger = table.getn(activeAlerts) * ALERT_STAGGER StartAlertFade(f, hold + stagger) end -------------------------------------------------------------------------------- -- Parse CHAT_MSG_LOOT -------------------------------------------------------------------------------- local function ParseLootMessage(msg) if not msg then return end local _, _, link, countStr = string.find(msg, "(|c%x+|Hitem.-%[.-%]|h|r)x(%d+)") if not link then _, _, link = string.find(msg, "(|c%x+|Hitem.-%[.-%]|h|r)") end if link then local itemName, quality, itemId = ParseItemLink(link) if itemName then local count = tonumber(countStr) or 1 -- Try full link first (most reliable), then itemId, then itemString local tex = GetItemTexture(link) if not tex and itemId and itemId > 0 then tex = GetItemTexture(itemId) end if not tex and itemId and itemId > 0 then tex = GetItemTexture("item:" .. itemId .. ":0:0:0") end ShowLootAlert(tex, itemName, quality, count, link) end return end -- Money local isMoney = false if string.find(msg, "铜") or string.find(msg, "银") or string.find(msg, "金") or string.find(msg, "[Cc]opper") or string.find(msg, "[Ss]ilver") or string.find(msg, "[Gg]old") then if string.find(msg, "拾取") or string.find(msg, "获得") or string.find(msg, "loot") or string.find(msg, "receive") then isMoney = true end end if isMoney then local cleanMsg = string.gsub(msg, "|c%x+", "") cleanMsg = string.gsub(cleanMsg, "|r", "") local _, _, moneyPart = string.find(cleanMsg, "[::]%s*(.+)$") if not moneyPart then _, _, moneyPart = string.find(cleanMsg, "loot%s+(.+)$") end if not moneyPart then _, _, moneyPart = string.find(cleanMsg, "获得了?%s*(.+)$") end if moneyPart then moneyPart = string.gsub(moneyPart, "[%.。]+$", "") ShowLootAlert("Interface\\Icons\\INV_Misc_Coin_01", moneyPart, 1, 1, nil) end end end -------------------------------------------------------------------------------- -- Initialize -------------------------------------------------------------------------------- function LD:Initialize() local db = GetDB() if not db.enable then return end CreateLootFrame() CreateAlertAnchor() 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 -- After the native LootFrame_Update runs (called by the engine or -- by us), reposition native buttons onto our visual rows. 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 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