Files
Adrestie 7e742d094e ForeverUI : animations de la fenêtre de butin
Les animations de camelot, rejouées image par image comme dans le suivi
des quêtes (les groupes de 3.3.5 n'ont ni fromAlpha / toAlpha ni
setToFinalAlpha), avec leurs durées et leur lissage IN : ouverture (monte
de 10 et apparaît en 0,1 s), fermeture (descend de 10 et s'efface en
0,1 s), apparition des cartes (0,1 s), et glissement de 100 vers la droite
en 0,3 s avec fondu sur les 0,2 dernières à chaque ramassage, clic compris
(camelot : butin automatique seulement). À la fermeture, toute animation
en cours s'arrête et chaque carte reprend sa place : rien ne reste à
l'écran quand le dernier objet ferme le butin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 04:00:30 +02:00

535 lines
19 KiB
Lua

-- ForeverUI: the loot window with camelot's LootFrame (blizzard_uipanels_game/mainline/
-- lootframe.xml / .lua, scrollingflatpanel.xml / .lua): a flat panel titled ITEMS, 220 wide and
-- 290 high at most, listing one card per loot slot (looting_itemcard_*: background tinted by the
-- quality, stroke, rarity tag with the quality name, 37 x 37 item button, name).
-- The client's LootFrame stays the host: its events, the master loot list (anchored to the
-- button named in LootFrame.selectedLootButton) and CloseLoot when it hides. It stays shown while
-- the loot is open, invisible and mouse-less; our panel lives on UIParent and follows it.
-- 3.3.5 does not tell quest items (no quest border) nor currencies.
local ForeverUI = ForeverUI or {}
_G.ForeverUI = ForeverUI
local Tpl = ForeverUI.Templates
local P = {}
ForeverUI.Loot = P
local N = {
width = 220, maxHeight = 290, barWidth = 16, -- panelWidth, panelMaxHeight, ScrollBarWidth
anchors = 26, extra = 20, -- ScrollingFlatPanelMixin:Resize
list = { 4, -22, 4 }, -- ScrollBox: TOPLEFT (4, -22), BOTTOM (0, 4)
pad = 6, spacing = 2, card = 46, -- ScrollBoxPad, ScrollBoxSpacing, element height
bar = { -16, -28, 6 }, -- ScrollBar at the panel's TOPRIGHT / BOTTOMRIGHT
background = { 6, -20, -2, 2 }, -- DefaultPanelFlatTemplate Bg
title = { 30, -1, -24, 20, -5 }, -- TitleContainer, its text at TOP (0, -5)
item = { 5, -4, 37, -157 }, -- ItemButton TOPLEFT (5, -4), hit rect to the right
stripe = { 100, 13 }, -- QualityStripe, TOPRIGHT
quality = { -4, -2 }, -- QualityText, TOPRIGHT
text = { 8, -8, 150, 30 }, -- Text at the item's TOPRIGHT
money = { 8, 0, 93, 38 }, -- money Text at the item's RIGHT
highlight = 0.7, -- HighlightNameFrame alpha
shadow = { 30, 0.2 }, -- ScrollBox shadows: 30 in from each side, scale 0.2
cursor = { -30, 50, 350 }, -- lootUnderMouse: x - 30, max(y + 50, 350)
default = { 16, -116 }, -- EditModePresetLayouts: TOPLEFT of UIParent
}
-- GameFontWhiteTiny2 (SystemFont_Tiny2: FRIZQT 8, white), missing in 3.3.5
local TINY = CreateFont("ForeverUIFontWhiteTiny2")
TINY:SetFont("Fonts" .. string.char(92) .. "FRIZQT__.TTF", 8, "")
TINY:SetTextColor(1, 1, 1)
local COMMON = 1
local function atlas(t, name)
return ForeverUI.SetAtlas(t, name, true)
end
-- ------------------------------------------------------------ animations
-- camelot's animation groups, replayed by hand as in ObjectiveTracker.lua: 3.3.5 groups have no
-- fromAlpha / toAlpha nor setToFinalAlpha. One step per frame (the panel, a card): a new step
-- replaces the running one, stop drops it. fn(progress) runs each frame; finish once done,
-- after the loop.
local A = {
open = 0.1, close = 0.1, drop = 10, -- ScrollingFlatPanel ShowAnim / HideAnim
card = 0.1, -- element ShowAnim
slide = 0.3, slideX = 100, fade = 0.2, fadeDelay = 0.1, -- SlideOutRightAnim
}
local steps = {}
local runner = CreateFrame("Frame")
runner:Hide()
runner:SetScript("OnUpdate", function(self, elapsed)
local finished
for key, e in pairs(steps) do
e.t = e.t + elapsed
if e.t >= e.delay then
local p = (e.duration > 0) and math.min(1, (e.t - e.delay) / e.duration) or 1
e.fn(p)
if p >= 1 then
steps[key] = nil
if e.finish then
finished = finished or {}
table.insert(finished, e.finish)
end
end
end
end
if finished then
for _, finish in ipairs(finished) do finish() end
end
if not next(steps) then self:Hide() end
end)
P.steps, P.runner = steps, runner
local function play(key, duration, fn, finish)
steps[key] = { t = 0, delay = 0, duration = duration, fn = fn, finish = finish }
runner:Show()
end
local function stop(key)
steps[key] = nil
end
-- smoothing="IN"
local function easeIn(p) return p * p end
-- ------------------------------------------------------------ panel
local panel = CreateFrame("Frame", "ForeverUILootFrame", UIParent)
panel:SetWidth(N.width)
panel:SetHeight(N.maxHeight)
panel:SetFrameStrata("HIGH")
panel:SetToplevel(true)
panel:SetClampedToScreen(true)
panel:EnableMouse(true)
panel:Hide()
P.panel = panel
-- FlatPanelBackgroundTemplate: two 16 x 16 rounded bottom corners, the edge between them and the
-- rest flat, tinted by PANEL_BACKGROUND_COLOR
do
local B = N.background
local c = ForeverUI.PanelBackground
local bottomLeft = panel:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(bottomLeft, "uiframebackground-nineslice-cornerbottomleft")
bottomLeft:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", B[1], B[4])
local bottomRight = panel:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(bottomRight, "uiframebackground-nineslice-cornerbottomright")
bottomRight:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", B[3], B[4])
local edge = panel:CreateTexture(nil, "BACKGROUND")
edge:SetTexture(c[1], c[2], c[3], c[4])
edge:SetPoint("TOPLEFT", bottomLeft, "TOPRIGHT")
edge:SetPoint("BOTTOMRIGHT", bottomRight, "BOTTOMLEFT")
local body = panel:CreateTexture(nil, "BACKGROUND")
body:SetTexture(c[1], c[2], c[3], c[4])
body:SetPoint("TOPLEFT", panel, "TOPLEFT", B[1], B[2])
body:SetPoint("BOTTOMRIGHT", bottomRight, "TOPRIGHT")
for _, t in ipairs({ bottomLeft, bottomRight }) do t:SetVertexColor(c[1], c[2], c[3], c[4]) end
end
-- Bronze metal without portrait (DefaultPanelBaseTemplate's NineSlice), above the cards
local metal = CreateFrame("Frame", nil, panel)
metal:SetAllPoints(panel)
metal:SetFrameLevel(panel:GetFrameLevel() + 10)
Tpl.NineSlice(metal, "ButtonFrameTemplateNoPortrait", panel)
-- Title (TitleContainer, frame level 510 in the source: above the metal)
do
local T = N.title
local container = CreateFrame("Frame", nil, panel)
container:SetHeight(T[4])
container:SetPoint("TOPLEFT", panel, "TOPLEFT", T[1], T[2])
container:SetPoint("TOPRIGHT", panel, "TOPRIGHT", T[3], T[2])
container:SetFrameLevel(panel:GetFrameLevel() + 12)
local title = container:CreateFontString(nil, "OVERLAY", "GameFontNormal")
title:SetPoint("TOP", container, "TOP", 0, T[5])
title:SetPoint("LEFT", container, "LEFT")
title:SetPoint("RIGHT", container, "RIGHT")
title:SetText(ITEMS)
P.title = title
end
-- ClosePanelButton: closes the host, as 3.3.5's LootCloseButton does
local close = CreateFrame("Button", "ForeverUILootFrameCloseButton", panel)
Tpl.CloseButton(close, panel)
close:SetFrameLevel(panel:GetFrameLevel() + 12)
close:SetScript("OnClick", function() HideUIPanel(LootFrame) end)
-- ScrollBox: a clipping scroll frame; the cards are laid out in its child
local zone = CreateFrame("ScrollFrame", "ForeverUILootList", panel)
zone:SetPoint("TOPLEFT", panel, "TOPLEFT", N.list[1], N.list[2])
zone:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", N.list[1], N.list[3])
zone:SetWidth(N.width - N.pad)
local child = CreateFrame("Frame", nil, zone)
child:SetWidth(N.width - N.pad)
child:SetHeight(1)
zone:SetScrollChild(child)
P.zone, P.child = zone, child
local step = N.card + N.spacing
-- Upper and lower shadows, shown when there are cards beyond the view. As the ScrollBox's Shadows
-- frame: 15 levels above the cards, at scale 0.2; _looting_itemcard_shadow-center at its size,
-- 30 in from each side in that scale, the upper one flipped.
local shadows = CreateFrame("Frame", nil, zone)
shadows:SetAllPoints(zone)
shadows:SetScale(N.shadow[2])
shadows:SetFrameLevel(child:GetFrameLevel() + 15)
P.shadows = shadows
local function shadow(top)
local t = shadows:CreateTexture(nil, "OVERLAY")
local e = ForeverUI.AtlasEntry("_looting_itemcard_shadow-center")
atlas(t, "_looting_itemcard_shadow-center")
t:SetHeight(e and e[7] or 150)
if top then
if e then t:SetTexCoord(e[2], e[3], e[5], e[4]) end
t:SetPoint("TOPLEFT", shadows, "TOPLEFT", N.shadow[1], 0)
t:SetPoint("TOPRIGHT", shadows, "TOPRIGHT", -N.shadow[1], 0)
else
t:SetPoint("BOTTOMLEFT", shadows, "BOTTOMLEFT", N.shadow[1], 0)
t:SetPoint("BOTTOMRIGHT", shadows, "BOTTOMRIGHT", -N.shadow[1], 0)
end
t:Hide()
return t
end
local upperShadow, lowerShadow = shadow(true), shadow(false)
P.upperShadow, P.lowerShadow = upperShadow, lowerShadow
local bar = ForeverUI.CreateScrollBar("ForeverUILootScrollBar", panel, zone)
bar:ClearAllPoints()
bar:SetPoint("TOPLEFT", panel, "TOPRIGHT", N.bar[1], N.bar[2])
bar:SetPoint("BOTTOMLEFT", panel, "BOTTOMRIGHT", N.bar[1], N.bar[3])
bar:SetFrameLevel(panel:GetFrameLevel() + 12)
bar:Hide()
P.bar = bar
local function scrollTo(offset)
local range = math.max(0, child:GetHeight() - zone:GetHeight())
local y = math.min(offset * step, range)
zone:SetVerticalScroll(y)
Tpl.SetShown(upperShadow, y > 0)
Tpl.SetShown(lowerShadow, y < range)
end
bar.onScroll = scrollTo
zone:EnableMouseWheel(true)
zone:SetScript("OnMouseWheel", function(_, delta)
if bar:IsShown() then bar:MoveTo((bar.offset or 0) - delta) end
end)
-- ------------------------------------------------------------ cards
local cards = {}
P.cards = cards
local function quality(slot)
if LootSlotIsCoin(slot) then return COMMON end
local _, _, _, q = GetLootSlotInfo(slot)
return q or COMMON
end
local function onEnter(card)
local enabled = card.item:IsEnabled()
if not enabled or enabled == 0 then return end
card.highlight:Show()
if LootSlotIsItem(card.slot) then
GameTooltip:SetOwner(card, "ANCHOR_NONE")
GameTooltip:ClearAllPoints()
GameTooltip:SetPoint("LEFT", card, "RIGHT")
GameTooltip:SetLootItem(card.slot)
CursorUpdate(card)
end
end
local function onLeave(card)
if GameTooltip:IsOwned(card) then GameTooltip:Hide() end
ResetCursor()
card.highlight:Hide()
end
-- LootButton_OnClick: the host keeps what GroupLootDropDown and the master loot confirmation
-- read (the button name anchors the master loot list)
local function onClick(button)
local card = button.card
if IsModifiedClick() then
HandleModifiedItemClick(GetLootSlotLink(card.slot))
return
end
StaticPopup_Hide("CONFIRM_LOOT_DISTRIBUTION")
LootFrame.selectedLootButton = button:GetName()
LootFrame.selectedSlot = card.slot
LootFrame.selectedQuality = card.quality
LootFrame.selectedItemName = card.text:GetText()
LootSlot(card.slot)
end
-- the card at its place in the list, moved right by dx (slide out)
local function placeCard(card, dx)
card:ClearAllPoints()
card:SetPoint("TOPLEFT", child, "TOPLEFT", N.pad + dx, card.top)
card:SetPoint("TOPRIGHT", child, "TOPRIGHT", -N.pad + dx, card.top)
end
-- LootFrameElementTemplate (+ LootFrameItemElementTemplate's stripe and quality name)
local function createCard(i)
local card = CreateFrame("Frame", nil, child)
card:SetHeight(N.card)
card.top = -(N.pad + (i - 1) * step)
placeCard(card, 0)
local nameFrame = card:CreateTexture(nil, "BACKGROUND")
atlas(nameFrame, "looting_itemcard_bg")
nameFrame:SetAllPoints(card)
local stripe = card:CreateTexture(nil, "BACKGROUND")
atlas(stripe, "looting_raritytag_frame")
stripe:SetWidth(N.stripe[1])
stripe:SetHeight(N.stripe[2])
stripe:SetPoint("TOPRIGHT", card, "TOPRIGHT", 0, 0)
local border = card:CreateTexture(nil, "BORDER")
atlas(border, "looting_itemcard_stroke_normal")
border:SetAllPoints(card)
local highlight = card:CreateTexture(nil, "OVERLAY")
atlas(highlight, "looting_itemcard_stroke_clickstate")
highlight:SetBlendMode("ADD")
highlight:SetAlpha(N.highlight)
highlight:SetAllPoints(card)
highlight:Hide()
local pushed = card:CreateTexture(nil, "OVERLAY")
atlas(pushed, "looting_itemcard_stroke_clickstate")
pushed:SetBlendMode("ADD")
pushed:SetAllPoints(card)
pushed:Hide()
local qualityText = card:CreateFontString(nil, "ARTWORK")
qualityText:SetFontObject(TINY)
qualityText:SetJustifyH("RIGHT")
qualityText:SetPoint("TOPRIGHT", card, "TOPRIGHT", N.quality[1], N.quality[2])
local I = N.item
local item = CreateFrame("Button", "ForeverUILootItem" .. i, card, "ItemButtonTemplate")
item:SetWidth(I[3])
item:SetHeight(I[3])
item:SetPoint("TOPLEFT", card, "TOPLEFT", I[1], I[2])
item:SetHitRectInsets(0, I[4], 0, 0)
item:RegisterForClicks("LeftButtonUp", "RightButtonUp")
item.card = card
local text = card:CreateFontString(nil, "ARTWORK", "GameFontNormal")
text:SetJustifyH("LEFT")
text:SetJustifyV("TOP")
card.item, card.text, card.qualityText, card.stripe = item, text, qualityText, stripe
card.nameFrame, card.highlight, card.pushed = nameFrame, highlight, pushed
item:SetScript("OnEnter", function() onEnter(card) end)
item:SetScript("OnLeave", function() onLeave(card) end)
item:SetScript("OnMouseDown", function() pushed:Show() end)
item:SetScript("OnMouseUp", function() pushed:Hide() end)
item:SetScript("OnUpdate", function()
if GameTooltip:IsOwned(card) then onEnter(card) end
CursorOnUpdate(card)
end)
item:SetScript("OnClick", onClick)
return card
end
-- LootFrameElementMixin:Init on the card of slot; money and empty slots as camelot's element
-- factory sorts them
local function fill(card, slot)
card.slot = slot
stop(card)
placeCard(card, 0)
card:SetAlpha(1)
local isCoin, isItem = LootSlotIsCoin(slot), LootSlotIsItem(slot)
if not (isCoin or isItem) then
card:Hide()
return
end
local texture, name, quantity, _, locked = GetLootSlotInfo(slot)
local q = quality(slot)
card.quality = q
local c = ITEM_QUALITY_COLORS[q] or ITEM_QUALITY_COLORS[COMMON]
card.text:SetText(name)
card.text:SetVertexColor(c.r, c.g, c.b)
card.nameFrame:SetVertexColor(c.r, c.g, c.b)
card.text:ClearAllPoints()
if isCoin then
local M = N.money
card.text:SetWidth(M[3])
card.text:SetHeight(M[4])
card.text:SetJustifyV("MIDDLE")
card.text:SetPoint("LEFT", card.item, "RIGHT", M[1], M[2])
card.stripe:Hide()
card.qualityText:Hide()
Tpl.QualityOutline(card.item, nil)
else
local T = N.text
card.text:SetWidth(T[3])
card.text:SetHeight(T[4])
card.text:SetJustifyV("TOP")
card.text:SetPoint("TOPLEFT", card.item, "TOPRIGHT", T[1], T[2])
card.stripe:Show()
card.qualityText:SetText(_G["ITEM_QUALITY" .. q .. "_DESC"])
card.qualityText:Show()
Tpl.QualityOutline(card.item, q)
end
local icon = _G[card.item:GetName() .. "IconTexture"]
icon:SetTexture(texture)
local r, g, b = 1, 1, 1
if locked then r, g, b = 0.9, 0, 0 end
SetItemButtonTextureVertexColor(card.item, r, g, b)
SetItemButtonNormalTextureVertexColor(card.item, r, g, b)
local count = _G[card.item:GetName() .. "Count"]
if (quantity or 0) > 1 then
count:SetText(quantity)
count:Show()
else
count:Hide()
end
card.item:Enable()
-- ShowAnim: fades in over 0.1 s
card:SetAlpha(0)
card:Show()
play(card, A.card, function(p) card:SetAlpha(easeIn(p)) end)
end
-- SlideOutRightAnim, on every looted slot: 100 right over 0.3 s, fading out over the last 0.2 s,
-- then the card hides
local function slideOut(card)
if GameTooltip:IsOwned(card) then GameTooltip:Hide() end
card.highlight:Hide()
card.pushed:Hide()
card.item:Disable()
card:SetAlpha(1)
play(card, A.slide, function(p)
placeCard(card, A.slideX * easeIn(p))
local f = math.min(1, math.max(0, (p * A.slide - A.fadeDelay) / A.fade))
card:SetAlpha(1 - easeIn(f))
end, function()
card:Hide()
placeCard(card, 0)
card:SetAlpha(1)
end)
end
-- ScrollingFlatPanelMixin:Resize: the panel fits its cards up to 290, wider by the bar when
-- they do not fit
local function resize(n)
local cardsHeight = math.max(0, n * N.card + (n - 1) * N.spacing)
local height = math.min(cardsHeight + N.anchors + N.extra, N.maxHeight)
panel:SetHeight(height)
child:SetHeight(math.max(1, cardsHeight + 2 * N.pad))
local view = height - N.anchors
local hasBar = cardsHeight + 2 * N.pad > view
panel:SetWidth(N.width + (hasBar and N.barWidth or 0))
Tpl.SetShown(bar, hasBar)
local visibleCount = math.floor((view - N.pad) / step)
bar:Configure(n, visibleCount, 0)
scrollTo(0)
end
-- the panel at its place (P.anchor), moved down by dy
local function placePanel(dy)
local a = P.anchor
panel:ClearAllPoints()
panel:SetPoint(a[1], a[2], a[3], a[4], a[5] + dy)
end
function P.Open()
local n = GetNumLootItems() or 0
for i = 1, n do
cards[i] = cards[i] or createCard(i)
fill(cards[i], i)
end
for i = n + 1, #cards do cards[i]:Hide() end
resize(n)
if GetCVar("lootUnderMouse") == "1" then
local C = N.cursor
local x, y = GetCursorPosition()
local scale = panel:GetEffectiveScale()
P.anchor = { "TOPLEFT", nil, "BOTTOMLEFT", x / scale + C[1], math.max(y / scale + C[2], C[3]) }
else
P.anchor = { "TOPLEFT", UIParent, "TOPLEFT", N.default[1], N.default[2] }
end
-- ShowAnim played backwards: comes up 10 and fades in over 0.1 s
stop(panel)
placePanel(-A.drop)
panel:SetAlpha(0)
panel:Show()
panel:Raise()
play(panel, A.open, function(p)
local back = easeIn(1 - p)
placePanel(-A.drop * back)
panel:SetAlpha(1 - back)
end)
end
-- HideAnim: goes down 10 and fades out over 0.1 s, then the panel hides (and its OnHide stops
-- every animation, so nothing stays on screen when the last item closes the loot)
function P.Close()
if GameTooltip:GetOwner() and GameTooltip:GetOwner():GetParent() == child then GameTooltip:Hide() end
if not panel:IsShown() then return end
play(panel, A.close, function(p)
local e = easeIn(p)
placePanel(-A.drop * e)
panel:SetAlpha(1 - e)
end, function() panel:Hide() end)
end
panel:SetScript("OnHide", function()
stop(panel)
panel:SetAlpha(1)
if P.anchor then placePanel(0) end
for _, card in ipairs(cards) do
stop(card)
placeCard(card, 0)
card:SetAlpha(1)
end
end)
-- ------------------------------------------------------------ host
-- The client's LootFrame: invisible and mouse-less, shown while the loot is open
local function suppress()
LootFrame:SetAlpha(0)
LootFrame:EnableMouse(false)
for _, name in ipairs({ "LootCloseButton", "LootFrameUpButton", "LootFrameDownButton" }) do
local b = _G[name]
if b then b:EnableMouse(false) end
end
for i = 1, LOOTFRAME_NUMBUTTONS or 4 do
local b = _G["LootButton" .. i]
if b then b:EnableMouse(false) end
end
end
if LootFrame then
suppress()
hooksecurefunc("LootFrame_Show", P.Open)
LootFrame:HookScript("OnHide", P.Close)
end
-- The master loot list: GetMasterLootCandidate gives no name until the name cache has it (often
-- the master looter's own); the client then queries it and sends UPDATE_MASTER_LOOT_LIST, which
-- 3.3.5's LootFrame answers with UIDropDownMenu_Refresh, which does not rebuild the list. Opened,
-- the list is opened again, so it lists the candidates whose names came.
local function reopenMasterLootList()
if GroupLootDropDown and UIDROPDOWNMENU_OPEN_MENU == GroupLootDropDown and DropDownList1:IsShown() then
CloseDropDownMenus()
ToggleDropDownMenu(1, nil, GroupLootDropDown, LootFrame.selectedLootButton, 0, 0)
end
end
local events = CreateFrame("Frame")
events:RegisterEvent("LOOT_SLOT_CLEARED")
events:RegisterEvent("LOOT_SLOT_CHANGED")
events:RegisterEvent("UPDATE_MASTER_LOOT_LIST")
events:SetScript("OnEvent", function(_, event, slot)
if event == "UPDATE_MASTER_LOOT_LIST" then
reopenMasterLootList()
return
end
local card = slot and cards[slot]
if not (card and panel:IsShown()) then return end
if event == "LOOT_SLOT_CLEARED" then
slideOut(card)
else
fill(card, slot)
end
end)