Files
moon-well-ui/data/addon/ForeverUI/ActionBar.lua
T
Claude 39fbfc8c10 ForeverUI : barres, la barre de forme reste à l'écran en combat
Les boutons bonus sont ancrés à la glissière, ce qui la rend protégée :
en combat, ses SetPoint étaient bloqués (action bloquée par image). Après
un retrait hors combat elle reposait 45 px plus bas, et un druide prenant
une forme en combat gardait sa barre à moitié sous l'écran tout le combat.
Elle ne bouge plus en combat, attend au repos, y revient avant le
verrouillage, et suit une barre réaffichée pendant son propre retrait.
Durée : BONUSACTIONBAR_SLIDETIME du client (0,15 s).

Familier et postures : le porteur, protégé par les boutons qu'on y ancre,
était masqué avant le test de combat (familier mort ou expiré en combat :
action bloquée). Le test passe d'abord ; la sortie de combat le replace.

Choix d'icône d'un ensemble : une icône portée deux fois (deux anneaux,
bijoux ou armes identiques) ne compte qu'une fois, comme
RefreshEquipmentSetIconInfo, sinon la grille gagnait des cases vides au
delà de la liste du client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxRZNHkq453dyUMcWsyUf1
2026-10-01 23:19:58 +00:00

489 lines
17 KiB
Lua

-- Main action bar: the client's secure action buttons, kept and reskinned (camelot
-- ActionButtonTemplate, MainActionBar), with the bar border, page arrows and gryphon end caps.
-- Recreating the buttons would mean rewriting spells, macros and drag and drop, and risk taint.
-- Nothing is resized or moved in combat, where the client forbids it.
-- camelot ActionButtonTemplate: 45 x 45; shared/ActionBar.lua minButtonPadding = 2
local BUTTON_SIZE = 45
local BUTTON_PADDING = 2
local BUTTON_PITCH = BUTTON_SIZE + BUTTON_PADDING
local FRAME_WIDTH, FRAME_HEIGHT = 46, 45 -- size of the four state textures
local BUTTON_COUNT = 12
local END_CAP_WIDTH, END_CAP_HEIGHT = 154, 95
local END_CAP_DROP = -2 -- measured on screen: the art bottom drops 2 px below the bar
local L = ForeverUI.L
local ATLAS = {
normal = "ui-hud-actionbar-iconframe",
pushed = "ui-hud-actionbar-iconframe-down",
highlight = "ui-hud-actionbar-iconframe-mouseover",
border = "ui-hud-actionbar-iconframe-border",
slot = "ui-hud-actionbar-iconframe-slot",
background = "ui-hud-actionbar-iconframe-background",
flash = "ui-hud-actionbar-iconframe-flash",
}
local BARS = {
"ActionButton", "MultiBarBottomLeftButton", "MultiBarBottomRightButton",
"MultiBarRightButton", "MultiBarLeftButton", "BonusActionButton",
}
-- The frame is not the button's NormalTexture: ActionButton_Update sets UI-Quickslot2 on
-- every refresh and ActionButton_ShowGrid recolors it, erasing any skin. So the client's
-- texture is silenced and we draw our own frame. add: use the ADD blend mode.
local function setStateTexture(texture, atlas, add)
if not texture then
return
end
ForeverUI.SetAtlas(texture, atlas, true)
texture:SetWidth(FRAME_WIDTH)
texture:SetHeight(FRAME_HEIGHT)
texture:ClearAllPoints()
texture:SetPoint("TOPLEFT", 0, 0)
texture:SetBlendMode(add and "ADD" or "BLEND")
end
-- Hide again what the client just rewrote.
local function silenceNormalTexture(button)
local normal = button:GetNormalTexture()
if normal then
normal:SetAlpha(0)
normal:SetVertexColor(1, 1, 1, 0)
end
end
-- 3.3.5 trap: ActionButton_Update and ActionButton_HideGrid hide the WHOLE empty button while
-- its "showgrid" ATTRIBUTE is 0, and it only rises during a drag, so empty slots vanish, in
-- combat too (a stance change updates every button). Keeping the attribute at 1 makes the
-- client keep empty buttons shown. A secure frame changes only out of combat;
-- PLAYER_REGEN_ENABLED brings back anything hidden meanwhile.
local function keepGrid(button)
if not button or InCombatLockdown() or button:GetAttribute("statehidden") then
return
end
if (button:GetAttribute("showgrid") or 0) < 1 then
button:SetAttribute("showgrid", 1)
end
if not button:IsShown() then
button:Show()
end
end
local function skinButton(button)
if not button or button.foreverSkinned then
return
end
local name = button:GetName()
if not name then
return
end
local icon = _G[name .. "Icon"]
local border = _G[name .. "Border"]
local hotkey = _G[name .. "HotKey"]
local count = _G[name .. "Count"]
local macro = _G[name .. "Name"]
local cooldown = _G[name .. "Cooldown"]
local flash = _G[name .. "Flash"]
local floatingBG = _G[name .. "FloatingBG"]
button:SetWidth(BUTTON_SIZE)
button:SetHeight(BUTTON_SIZE)
-- Empty slot, under the icon.
local background = button:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(background, ATLAS.background, true)
background:SetAllPoints(button)
local slot = button:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(slot, ATLAS.slot, true)
slot:SetAllPoints(button)
button.foreverBackground = background
button.foreverSlot = slot
if icon then
-- The icon fills the button, as in camelot; 3.3.5 has no MaskTexture, so the frame's
-- solid corners cover the icon's corners.
icon:ClearAllPoints()
icon:SetAllPoints(button)
icon:SetTexCoord(0, 1, 0, 1)
icon:SetDrawLayer("BORDER")
end
-- Our frame, above the icon and out of the client's reach.
local frame = button:CreateTexture(nil, "ARTWORK")
setStateTexture(frame, ATLAS.normal)
button.foreverFrame = frame
silenceNormalTexture(button)
setStateTexture(button:GetPushedTexture(), ATLAS.pushed)
setStateTexture(button:GetHighlightTexture(), ATLAS.highlight)
setStateTexture(button:GetCheckedTexture(), ATLAS.highlight, true)
if flash then
ForeverUI.SetAtlas(flash, ATLAS.flash)
flash:ClearAllPoints()
flash:SetPoint("TOPLEFT", 0, 0)
end
if border then
-- The client's equipped border is green and square; camelot uses its own, hidden by default.
ForeverUI.SetAtlas(border, ATLAS.border)
border:ClearAllPoints()
border:SetPoint("TOPLEFT", 0, 0)
end
if floatingBG then
floatingBG:SetAlpha(0)
end
-- SetFontObject RESETS the justification, so set it AFTER. The number fonts have no
-- justifyH (client FontStyles.xml), so they are centered by default.
if hotkey then
hotkey:SetWidth(32)
hotkey:SetHeight(10)
hotkey:ClearAllPoints()
hotkey:SetPoint("TOPRIGHT", -5, -5)
hotkey:SetFontObject(NumberFontNormalSmallGray)
hotkey:SetJustifyH("RIGHT")
end
if count then
count:ClearAllPoints()
count:SetPoint("BOTTOMRIGHT", -5, 5)
count:SetFontObject(NumberFontNormal)
count:SetJustifyH("RIGHT")
end
if macro then
macro:SetWidth(36)
macro:SetHeight(10)
macro:ClearAllPoints()
macro:SetPoint("BOTTOM", 0, 2)
macro:SetFontObject(GameFontHighlightSmallOutline)
end
if cooldown and icon then
cooldown:ClearAllPoints()
cooldown:SetPoint("TOPLEFT", icon, "TOPLEFT", 3, -3)
cooldown:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -3, 3)
end
keepGrid(button)
button.foreverSkinned = true
end
-- ---------- Bar
local holder = CreateFrame("Frame", "ForeverUIActionBarHolder", UIParent)
holder:SetWidth(BUTTON_COUNT * BUTTON_SIZE + (BUTTON_COUNT - 1) * BUTTON_PADDING)
holder:SetHeight(BUTTON_SIZE)
-- Bar border: camelot's UI-HUD-ActionBar-Frame, one octagonal image, from TOPLEFT (-6, 6)
-- to BOTTOMRIGHT (4, -5). Nine-sliced so the corner bevels keep their size.
local border = CreateFrame("Frame", nil, holder)
border:SetPoint("TOPLEFT", -6, 6)
border:SetPoint("BOTTOMRIGHT", 4, -5)
border:SetFrameLevel(holder:GetFrameLevel())
ForeverUI.SetBarFrameArt(border)
-- Page block (mainline/MainActionBar.xml ActionBarPageNumber): BOTTOMRIGHT on the bar's
-- BOTTOMLEFT at (-4, 9); number 17 x 10 centered at -1, two 17 x 14 arrows at +10 and -10;
-- sized by its children: 17 x 34.
local page = CreateFrame("Frame", "ForeverUIActionBarPage", holder)
page:SetWidth(17)
page:SetHeight(34)
page:SetPoint("BOTTOMRIGHT", holder, "BOTTOMLEFT", -4, 9)
page:SetFrameLevel(holder:GetFrameLevel() + 11)
-- Dividers between buttons: RIGHT on the button's LEFT, offset 5.
local dividers = {}
-- End caps (gryphons): two 154 x 95 frames filled by their texture, movable on their own
-- (Customize UI); BottomBar.lua gives their default positions.
local END_CAP_INSET = 30 -- each cap overlaps its bar by 30 px
local function createEndCap(name, atlas)
local cap = CreateFrame("Frame", name, UIParent)
cap:SetWidth(END_CAP_WIDTH)
cap:SetHeight(END_CAP_HEIGHT)
-- Behind every element of the playing screen (levels 1 and up of the same strata): a cap
-- never covers anything.
cap:SetFrameStrata(holder:GetFrameStrata())
cap:SetFrameLevel(0)
local texture = cap:CreateTexture(nil, "OVERLAY")
texture:SetAllPoints(cap)
if not ForeverUI.SetAtlas(texture, atlas, true) then
cap:Hide()
end
cap.texture = texture
return cap
end
-- mainline/EditModePresetLayouts.lua: the left cap sits on the bar's LEFT edge, the right
-- cap on the bag bar's RIGHT edge, each 30 px inward (BottomBar.lua re-anchors it to the
-- bags). Camelot drops the cap 20 px below the bar, off screen for a bar 2 px above the
-- screen bottom, so the art bottom is aligned on the bar bottom, then 2 px lower.
local leftCap = createEndCap("ForeverUIActionBarLeftCap", "ui-hud-actionbar-gryphon-left")
local rightCap = createEndCap("ForeverUIActionBarRightCap", "ui-hud-actionbar-gryphon-right")
-- The bonus bar takes the same place. On a stance change 3.3.5 shows BonusActionBarFrame
-- over the main bar at its own position, so its buttons are anchored like the main ones and
-- follow the holder. Nothing is reparented: buttons follow the client bar's visibility.
-- 3.3.5 slides this bar in, but secure buttons cannot move every frame in combat, where
-- stances change: they are anchored ONCE to a track (our frame), and the track slides, out
-- of combat only (see below).
local PLACED_BARS = { "ActionButton", "BonusActionButton" }
-- The track covers the holder and shifts vertically while sliding. The duration comes from
-- the client when it declares it.
local track = CreateFrame("Frame", "ForeverUIBonusSlide", holder)
local SLIDE_DURATION = BONUSACTIONBAR_SLIDETIME or 0.2
local SLIDE_DISTANCE = BUTTON_SIZE
local function placeTrack(progress)
local offset = -SLIDE_DISTANCE * (1 - progress)
track:ClearAllPoints()
track:SetPoint("TOPLEFT", holder, "TOPLEFT", 0, offset)
track:SetPoint("BOTTOMRIGHT", holder, "BOTTOMRIGHT", 0, offset)
end
placeTrack(1)
-- The slide goes both ways. When hiding, the client sets mode = "hide" and keeps the bar
-- shown until the move ends, so the mode is read rather than the visibility alone. Without
-- that field the hide is instant: hidden buttons cannot be animated.
-- The bonus buttons are anchored to the track, which makes it protected (as the crafting page,
-- ProfessionsBook.lua): it never moves in combat. It waits at rest while the bar is hidden and
-- goes back to rest when combat starts, so a bar shown in combat appears in place, unslid.
track.progress = 1
track.target = 1
track:SetScript("OnUpdate", function(self, elapse)
local bar = BonusActionBarFrame
local visible = bar and bar:IsShown()
if InCombatLockdown() then
self.visible = visible
return
end
if visible and not self.visible then
self.progress, self.target = 0, 1 -- shown: slides up
elseif visible then
-- the client is hiding it, or shows it again before the end of its own slide
self.target = (bar.mode == "hide") and 0 or 1
else
if self.progress ~= 1 then
placeTrack(1) -- hidden: back at rest
end
self.progress, self.target = 1, 1
end
self.visible = visible
if self.progress ~= self.target then
local step = (elapse or 0) / SLIDE_DURATION
if self.target > self.progress then
self.progress = math.min(self.target, self.progress + step)
else
self.progress = math.max(self.target, self.progress - step)
end
placeTrack(self.progress)
end
end)
-- PLAYER_REGEN_DISABLED fires before the lockdown: last moment to put the track at rest
track:RegisterEvent("PLAYER_REGEN_DISABLED")
track:SetScript("OnEvent", function(self)
self.progress, self.target = 1, 1
placeTrack(1)
end)
ForeverUI.ActionBarSlide = track
local function layoutButtons()
if InCombatLockdown() then
return
end
for _, prefix in ipairs(PLACED_BARS) do
local anchor = (prefix == "BonusActionButton") and track or holder
for index = 1, BUTTON_COUNT do
local button = _G[prefix .. index]
if button then
button:ClearAllPoints()
button:SetPoint("LEFT", anchor, "LEFT", (index - 1) * BUTTON_PITCH, 0)
-- Dividers belong to the holder: one set, placed on the main bar.
if prefix == "ActionButton" and index > 1 and not dividers[index] then
local divider = ForeverUI.CreateDivider(holder, holder:GetFrameLevel() + 1)
divider:SetPoint("TOP", button, "TOP", 0, 0)
divider:SetPoint("BOTTOM", button, "BOTTOM", 0, 0)
divider:SetPoint("RIGHT", button, "LEFT", 5, 0)
dividers[index] = divider
end
end
end
end
end
-- Hide the client's bar art. 3.3.5 MainMenuBar.xml has FOUR sets of pieces, and missing one
-- leaves the old dwarf bar behind ours: MainMenuBarTexture0..3 (bar body),
-- MainMenuXPBarTexture0..3 (XP bar frame), MainMenuMaxLevelBar0..3 (shown at 80), the
-- MainMenuBarLeftEndCap / RightEndCap end caps, and MainMenuBarExpText.
local OLD_ART = {
"MainMenuBarTexture%d", "MainMenuXPBarTexture%d", "MainMenuMaxLevelBar%d",
}
-- Bonus bar art: all the frame's own texture regions are hidden (buttons are child frames,
-- not regions). The frame also stops taking the mouse: BonusActionBarFrame is 505 x 43, HIGH
-- strata, toplevel, enableMouse; invisible, it would still swallow clicks on the micro-menu
-- below it. Its buttons keep their own mouse, being child frames.
local function clearBonusArt()
local bar = BonusActionBarFrame
if not bar or not bar.GetRegions then
return
end
if bar.EnableMouse then
bar:EnableMouse(false)
end
local regions = { bar:GetRegions() }
for _, region in ipairs(regions) do
if region and region.GetObjectType and region:GetObjectType() == "Texture" then
region:SetAlpha(0)
end
end
end
local function hideOldBarArt()
clearBonusArt()
for _, model in ipairs(OLD_ART) do
for index = 0, 3 do
local texture = _G[string.format(model, index)]
if texture then
texture:SetAlpha(0)
end
end
end
for _, name in ipairs({ "MainMenuBarLeftEndCap", "MainMenuBarRightEndCap", "MainMenuBarExpText" }) do
local region = _G[name]
if region then
region:SetAlpha(0)
end
end
if MainMenuBarPageNumber then
MainMenuBarPageNumber:SetAlpha(1)
MainMenuBarPageNumber:SetWidth(17)
MainMenuBarPageNumber:SetHeight(10)
MainMenuBarPageNumber:SetJustifyH("CENTER")
MainMenuBarPageNumber:ClearAllPoints()
MainMenuBarPageNumber:SetPoint("CENTER", page, "CENTER", -1, 0)
end
-- Page arrows: camelot has its own in the bar atlas.
local arrows = {
{ button = ActionBarUpButton, prefix = "ui-hud-actionbar-pageuparrow", y = 10 },
{ button = ActionBarDownButton, prefix = "ui-hud-actionbar-pagedownarrow", y = -10 },
}
for _, entry in ipairs(arrows) do
local button = entry.button
if button and not button.foreverSkinned then
ForeverUI.SetAtlas(button:GetNormalTexture(), entry.prefix .. "-up")
ForeverUI.SetAtlas(button:GetPushedTexture(), entry.prefix .. "-down")
ForeverUI.SetAtlas(button:GetHighlightTexture(), entry.prefix .. "-mouseover")
if button.GetDisabledTexture and button:GetDisabledTexture() then
ForeverUI.SetAtlas(button:GetDisabledTexture(), entry.prefix .. "-disabled")
end
button:SetWidth(17)
button:SetHeight(14)
button:ClearAllPoints()
button:SetPoint("CENTER", page, "CENTER", 0, entry.y)
button.foreverSkinned = true
end
end
end
local function skinAll()
for _, prefix in ipairs(BARS) do
for index = 1, BUTTON_COUNT do
local button = _G[prefix .. index]
skinButton(button)
keepGrid(button)
end
end
hideOldBarArt()
end
-- The client rewrites the normal texture on every button update: silence it again after.
if hooksecurefunc then
hooksecurefunc("ActionButton_Update", function(self)
if self and self.foreverSkinned then
silenceNormalTexture(self)
keepGrid(self)
end
end)
hooksecurefunc("ActionButton_ShowGrid", function(self)
if self and self.foreverSkinned then
silenceNormalTexture(self)
end
end)
-- Here the client hides empty buttons.
hooksecurefunc("ActionButton_HideGrid", function(self)
if self and self.foreverSkinned then
silenceNormalTexture(self)
keepGrid(self)
end
end)
end
local watcher = CreateFrame("Frame")
watcher:RegisterEvent("PLAYER_ENTERING_WORLD")
watcher:RegisterEvent("PLAYER_REGEN_ENABLED")
watcher:RegisterEvent("ACTIONBAR_PAGE_CHANGED")
watcher:RegisterEvent("UPDATE_BONUS_ACTIONBAR")
watcher:SetScript("OnEvent", function()
skinAll()
layoutButtons()
end)
skinAll()
layoutButtons()
ForeverUI.ActionBarDebug = function()
local button = _G["ActionButton1"]
if not button then
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r : " .. L.ACTIONBAR_BUTTON1_MISSING)
return
end
local point, relativeTo, _, x, y = button:GetPoint(1)
local normal = button:GetNormalTexture()
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"|cff66ccffForeverUI|r " .. L.ACTIONBAR_DEBUG,
button:GetWidth(), button:GetHeight(),
tostring(button:GetParent() and button:GetParent():GetName() or "?"),
tostring(point), tostring(relativeTo and relativeTo:GetName() or "?"), x or 0, y or 0,
tostring(button:IsShown()),
tostring(button.foreverFrame and button.foreverFrame:GetTexture() or NONE),
normal and normal:GetAlpha() or -1))
end
ForeverUI.ActionBarHolder = holder
ForeverUI.ActionBarBorder = border
ForeverUI.ActionBarEndCaps = { left = leftCap, right = rightCap, inset = END_CAP_INSET, drop = END_CAP_DROP }
-- camelot/EditModePresetLayoutConstants.lua: BOTTOMRIGHT on the micro-menu's BOTTOMLEFT at
-- (-4.5, -4); the micro-menu at BOTTOM (116.5, 6), 275 wide, puts the bar's right edge at
-- -25.5 from the screen center, 2 from the bottom. BottomBar.lua redoes this with the real
-- micro-menu width.
ForeverUI.Layout.Register(holder, "actionbar", L.ACTIONBAR_EDIT_LABEL, "BOTTOMRIGHT", "BOTTOM", -25.5, 2)
ForeverUI.Layout.Register(leftCap, "leftgryphon", L.ACTIONBAR_EDIT_LABEL_LEFT_GRYPHON, "BOTTOMRIGHT", "BOTTOM",
-25.5 - holder:GetWidth() + END_CAP_INSET, 2 + END_CAP_DROP)
ForeverUI.Layout.Register(rightCap, "rightgryphon", L.ACTIONBAR_EDIT_LABEL_RIGHT_GRYPHON, "BOTTOMLEFT", "BOTTOM",
0, 2 + END_CAP_DROP)