From c6f61c1ada0d492fca852e51fd1ee0c26f558a23 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:45:58 +0100 Subject: [PATCH 1/6] Stage 1 Migration Inventory frame to ace3 --- Core/MultiBotEngine.lua | 4 + Core/MultiBotEvery.lua | 17 +- Core/MultiBotHandler.lua | 46 +- Core/MultiBotInit.lua | 155 +--- Core/MultiBotLocale.lua | 2 +- Locales/MultiBotAceLocale-deDE.lua | 1 + Locales/MultiBotAceLocale-enGB.lua | 1 + Locales/MultiBotAceLocale-enUS.lua | 1 + Locales/MultiBotAceLocale-esES.lua | 1 + Locales/MultiBotAceLocale-frFR.lua | 1 + Locales/MultiBotAceLocale-koKR.lua | 1 + Locales/MultiBotAceLocale-ruRU.lua | 1 + Locales/MultiBotAceLocale-zhCN.lua | 1 + MultiBot.toc | 2 + TODO.md | 2 + UI/MultiBotInventoryFrame.lua | 941 +++++++++++++++++++++++ UI/MultiBotInventoryItem.lua | 254 ++++++ UI/MultiBotItem.lua | 106 +-- UI/MultiBotRewardFrame.lua | 13 +- docs/ace3-inventory-migration-tracker.md | 212 +++++ 20 files changed, 1474 insertions(+), 288 deletions(-) create mode 100644 UI/MultiBotInventoryFrame.lua create mode 100644 UI/MultiBotInventoryItem.lua create mode 100644 docs/ace3-inventory-migration-tracker.md diff --git a/Core/MultiBotEngine.lua b/Core/MultiBotEngine.lua index 6600259..59859fc 100644 --- a/Core/MultiBotEngine.lua +++ b/Core/MultiBotEngine.lua @@ -1552,6 +1552,10 @@ end -- Rafraîchit l’inventaire du bot actuellement affiché dans la frame Inventory -- en rejouant le même flux que le bouton "Inventory" (waitFor = "INVENTORY" + "items"). MultiBot.RefreshInventory = function(delay) + if MultiBot.inventory and MultiBot.inventory.refresh then + return MultiBot.inventory:refresh(delay) + end + -- Si la frame d’inventaire n’est pas visible ou pas encore initialisée, on ne fait rien if not MultiBot.inventory or not MultiBot.inventory:IsVisible() then return false diff --git a/Core/MultiBotEvery.lua b/Core/MultiBotEvery.lua index 8e389f9..d784d8e 100644 --- a/Core/MultiBotEvery.lua +++ b/Core/MultiBotEvery.lua @@ -144,19 +144,14 @@ MultiBot.addEvery = function(pFrame, pCombat, pNormal) if(pButton.state) then MultiBot.inventory:Hide() pButton.setDisable() - else - local tUnits = MultiBot.frames["MultiBar"].frames["Units"] - for key, value in pairs(MultiBot.index.actives) do - if(tUnits.buttons[value].name ~= UnitName("player")) then - tUnits.frames[value].getButton("Inventory").setDisable() - end - end + return + end - pButton.setEnable() - MultiBot.inventory.name = pButton.getName() - tUnits.buttons[MultiBot.inventory.name].waitFor = "INVENTORY" - SendChatMessage("items", "WHISPER", nil, pButton.getName()) + if(MultiBot.RequestBotInventory and MultiBot.RequestBotInventory(pButton.getName())) then + return end + + pButton.setEnable() end pFrame.addButton("Spellbook", 274, 0, "inv_misc_book_09", MultiBot.L("tips.every.spellbook")).setDisable() diff --git a/Core/MultiBotHandler.lua b/Core/MultiBotHandler.lua index 76e7cc7..fcb0dbd 100644 --- a/Core/MultiBotHandler.lua +++ b/Core/MultiBotHandler.lua @@ -246,6 +246,14 @@ local function setSavedLayoutValue(key, value) return value end +MultiBot.GetSavedLayoutValue = function(key) + return getSavedLayoutValue(key) +end + +MultiBot.SetSavedLayoutValue = function(key, value) + return setSavedLayoutValue(key, value) +end + -- HANDLER -- @@ -1496,12 +1504,20 @@ function MultiBot.HandleMultiBotEvent(event, ...) -- Inventory -- if(tButton.waitFor == "INVENTORY" and MultiBot.isInside(arg1, "Inventory", "背包")) then - local tItems = MultiBot.inventory.frames["Items"] - for key, value in pairs(tItems.buttons) do value:Hide() end - for key in pairs(tItems.buttons) do tItems.buttons[key] = nil end - MultiBot.inventory.setText("Title", MultiBot.doReplace(MultiBot.L("info.inventory"), "NAME", arg2)) - MultiBot.inventory.name = arg2 - tItems.index = 0 + if(MultiBot.inventory and MultiBot.inventory.beginPayload) then + MultiBot.inventory:beginPayload(arg2) + else + local tItems = MultiBot.inventory.frames["Items"] + if(tItems.clear) then + tItems:clear() + else + for key, value in pairs(tItems.buttons) do value:Hide() end + for key in pairs(tItems.buttons) do tItems.buttons[key] = nil end + end + MultiBot.inventory.setText("Title", MultiBot.doReplace(MultiBot.L("info.inventory"), "NAME", arg2)) + MultiBot.inventory.name = arg2 + tItems.index = 0 + end tButton.waitFor = "ITEM" SendChatMessage("stats", "WHISPER", nil, arg2) return @@ -1516,7 +1532,11 @@ function MultiBot.HandleMultiBotEvent(event, ...) if(tButton.waitFor == "ITEM") then if(string.sub(arg1, 1, 3) == "---") then return end - MultiBot.addItem(MultiBot.inventory.frames["Items"], arg1) + if(MultiBot.inventory and MultiBot.inventory.appendItem) then + MultiBot.inventory:appendItem(arg1) + else + MultiBot.addItem(MultiBot.inventory.frames["Items"], arg1) + end return end @@ -1542,7 +1562,11 @@ function MultiBot.HandleMultiBotEvent(event, ...) end if(MultiBot.inventory:IsVisible() and MultiBot.isInside(string.lower(arg1), "opened")) then - tButton.waitFor = "LOOT" + if(MultiBot.inventory and MultiBot.inventory.markLootPending) then + MultiBot.inventory:markLootPending(tButton.name) + else + tButton.waitFor = "LOOT" + end return end end @@ -1564,6 +1588,12 @@ function MultiBot.HandleMultiBotEvent(event, ...) tButton = MultiBot.frames["MultiBar"].frames["Units"].buttons[tName] end + if(tButton ~= nil and MultiBot.inventory and MultiBot.inventory.handleLootReceived + and MultiBot.inventory:handleLootReceived(tButton.name)) then + tButton.waitFor = "" + return + end + if(tButton ~= nil and tButton.waitFor == "LOOT" and tButton ~= nil) then tButton.waitFor = "INVENTORY" SendChatMessage("items", "WHISPER", nil, tButton.name) diff --git a/Core/MultiBotInit.lua b/Core/MultiBotInit.lua index 33feeab..b2dd8f4 100644 --- a/Core/MultiBotInit.lua +++ b/Core/MultiBotInit.lua @@ -3162,158 +3162,7 @@ end -- INVENTORY -- -MultiBot.inventory = MultiBot.newFrame(MultiBot, -700, -144, 32, 442, 884) -MultiBot.inventory.addTexture("Interface\\AddOns\\MultiBot\\Textures\\Inventory.blp") -MultiBot.inventory.addText("Title", MB_INVENTORY_LABEL, "CENTER", -58, 429, 12) -MultiBot.inventory.action = "s" -MultiBot.inventory:SetMovable(true) -MultiBot.inventory:Hide() - -MultiBot.inventory.movButton("Move", -406, 849, 34, MultiBot.L("tips.move.inventory")) - -MultiBot.inventory.wowButton("X", -126, 862, 15, 18, 13) -.doLeft = function(pButton) - local tUnits = MultiBot.frames and MultiBot.frames["MultiBar"] and MultiBot.frames["MultiBar"].frames and MultiBot.frames["MultiBar"].frames["Units"] - local tName = MultiBot.inventory and MultiBot.inventory.name - if(tUnits == nil or tName == nil or tUnits.buttons == nil or tUnits.buttons[tName] == nil) then - MultiBot.inventory:Hide() - return - end - - local tButton = tUnits.buttons[tName].buttons and tUnits.buttons[tName].buttons["Inventory"] - if(tButton ~= nil and tButton.doLeft ~= nil) then - tButton.doLeft(tButton) - else - MultiBot.inventory:Hide() - end -end - -MultiBot.inventory.addButton("Sell", -94, 806, "inv_misc_coin_16", MultiBot.L("tips.inventory.sell")).setEnable() -.doLeft = function(pButton) - if(pButton.state) then - MultiBot.inventory.action = "" - pButton.setDisable() - else - CancelTrade() - MultiBot.inventory.action = "s" - pButton.getButton("Destroy").setDisable() - pButton.getButton("Equip").setDisable() - pButton.getButton("Trade").setDisable() - pButton.getButton("Use").setDisable() - pButton.setEnable() - end -end - --- Bouton vendre tous les objets gris (s *) -MultiBot.inventory.addButton("SellGrey", -94, 768, "inv_misc_coin_03", MultiBot.L("tips.inventory.sellgrey")) -.doLeft = function(pButton) - if not MultiBot.isTarget() then - return - end - CancelTrade() - MultiBot.inventory.action = "" - pButton.getButton("Destroy").setDisable() - pButton.getButton("Equip").setDisable() - pButton.getButton("Trade").setDisable() - pButton.getButton("Sell").setDisable() - pButton.getButton("Use").setDisable() - SendChatMessage("s *", "WHISPER", nil, pButton.getName()) - if MultiBot.RefreshInventory then - MultiBot.RefreshInventory(0.5) - end -end - --- Bouton vendre tous les objets vendables (s vendor) -MultiBot.inventory.addButton("SellVendor", -94, 731, "inv_misc_coin_04", MultiBot.L("tips.inventory.sellvendor")) -.doLeft = function(pButton) - if not MultiBot.isTarget() then - return - end - CancelTrade() - MultiBot.inventory.action = "" - pButton.getButton("Destroy").setDisable() - pButton.getButton("Equip").setDisable() - pButton.getButton("Trade").setDisable() - pButton.getButton("Sell").setDisable() - pButton.getButton("Use").setDisable() - SendChatMessage("s vendor", "WHISPER", nil, pButton.getName()) - if MultiBot.RefreshInventory then - MultiBot.RefreshInventory() - end -end - -MultiBot.inventory.addButton("Equip", -94, 694, "inv_helmet_22", MultiBot.L("tips.inventory.equip")).setDisable() -.doLeft = function(pButton) - if(pButton.state) then - MultiBot.inventory.action = "" - pButton.setDisable() - else - CancelTrade() - MultiBot.inventory.action = "e" - pButton.getButton("Destroy").setDisable() - pButton.getButton("Trade").setDisable() - pButton.getButton("Sell").setDisable() - pButton.getButton("Use").setDisable() - pButton.setEnable() - end -end - -MultiBot.inventory.addButton("Use", -94, 657, "inv_gauntlets_25", MultiBot.L("tips.inventory.use")).setDisable() -.doLeft = function(pButton) - if(pButton.state) then - MultiBot.inventory.action = "" - pButton.setDisable() - else - CancelTrade() - MultiBot.inventory.action = "u" - pButton.getButton("Destroy").setDisable() - pButton.getButton("Equip").setDisable() - pButton.getButton("Trade").setDisable() - pButton.getButton("Sell").setDisable() - pButton.setEnable() - end -end - -MultiBot.inventory.addButton("Trade", -94, 620, "achievement_reputation_01", MultiBot.L("tips.inventory.trade")).setDisable() -.doLeft = function(pButton) - if(pButton.state) then - MultiBot.inventory.action = "" - pButton.setDisable() - CancelTrade() - else - InitiateTrade(pButton.getName()) - MultiBot.inventory.action = "give" - pButton.getButton("Destroy").setDisable() - pButton.getButton("Equip").setDisable() - pButton.getButton("Sell").setDisable() - pButton.getButton("Use").setDisable() - pButton.setEnable() - end -end - -MultiBot.inventory.addButton("Destroy", -94, 583, "inv_hammer_15", MultiBot.L("tips.inventory.drop")).setDisable() -.doLeft = function(pButton) - if(pButton.state) then - MultiBot.inventory.action = "" - pButton.setDisable() - else - CancelTrade() - MultiBot.inventory.action = "destroy" - pButton.getButton("Equip").setDisable() - pButton.getButton("Trade").setDisable() - pButton.getButton("Sell").setDisable() - pButton.getButton("Use").setDisable() - pButton.setEnable() - end -end - -MultiBot.inventory.addButton("Open", -94, 322.5, "inv_misc_gift_05", MultiBot.L("tips.inventory.open")) -.doLeft = function(pButton) - SendChatMessage("open items", "WHISPER", nil, pButton.getName()) -end - -local tFrame = MultiBot.inventory.addFrame("Items", -397, 807, 32) -tFrame:Show() +MultiBot.InitializeInventoryFrame() -- STATS -- @@ -4795,8 +4644,6 @@ if MultiBot.TimerAfter then end) end --- Minimap bootstrap is handled by OnEnable via LIFECYCLE_ENABLE_STEPS. - -- FINISH -- MultiBot.state = true diff --git a/Core/MultiBotLocale.lua b/Core/MultiBotLocale.lua index 3bc8b33..8920184 100644 --- a/Core/MultiBotLocale.lua +++ b/Core/MultiBotLocale.lua @@ -65,7 +65,7 @@ function MultiBot.GetLocaleString(key, fallback) if aceLocale then local activeLocale = aceLocale:GetLocale(LOCALE_NAMESPACE, true) - local activeValue = activeLocale and activeLocale[key] + local activeValue = type(activeLocale) == "table" and rawget(activeLocale, key) or nil if type(activeValue) == "string" then return activeValue end diff --git a/Locales/MultiBotAceLocale-deDE.lua b/Locales/MultiBotAceLocale-deDE.lua index 29a767f..e825df8 100644 --- a/Locales/MultiBotAceLocale-deDE.lua +++ b/Locales/MultiBotAceLocale-deDE.lua @@ -7,6 +7,7 @@ local deDEValues = { ["info.itemdestroyalert"] = "Möchtest du diesen Gegenstand WIRKLICH zerstören?\n%s", ["info.keydestroyalert"] = "Ich verkaufe keine Schlüssel.", ["info.itemsellalert"] = "Ich kann diesen Gegenstand nicht verkaufen.", + ["info.inventoryvendortarget"] = "Sie müssen zuerst einen Händler auswählen.", ["info.butttitle"] = "|cffffd100MultiBot|r", ["info.buttontoggle"] = "|cff00ff00Linksklick: UI umschalten|r", ["info.buttonoptions"] = "|cffff0000Rechtsklick: Optionen|r", diff --git a/Locales/MultiBotAceLocale-enGB.lua b/Locales/MultiBotAceLocale-enGB.lua index 07e8b5b..2dc51ad 100644 --- a/Locales/MultiBotAceLocale-enGB.lua +++ b/Locales/MultiBotAceLocale-enGB.lua @@ -7,6 +7,7 @@ local enGBValues = { ["info.itemdestroyalert"] = "Do you REALLY want to destroy this item?\n%s", ["info.keydestroyalert"] = "I will not sell Keys.", ["info.itemsellalert"] = "I cant sell this Item.", + ["info.inventoryvendortarget"] = "You must select a vendor first.", ["info.butttitle"] = "|cffffd100MultiBot|r", ["info.buttontoggle"] = "|cff00ff00Left-click: toggle UI|r", ["info.buttonoptions"] = "|cffff0000Right-click: options|r", diff --git a/Locales/MultiBotAceLocale-enUS.lua b/Locales/MultiBotAceLocale-enUS.lua index 49ad18e..538e103 100644 --- a/Locales/MultiBotAceLocale-enUS.lua +++ b/Locales/MultiBotAceLocale-enUS.lua @@ -7,6 +7,7 @@ local enUSValues = { ["info.itemdestroyalert"] = "Do you REALLY want to destroy this item?\n%s", ["info.keydestroyalert"] = "I will not sell Keys.", ["info.itemsellalert"] = "I cant sell this Item.", + ["info.inventoryvendortarget"] = "You must select a vendor first.", ["info.butttitle"] = "|cffffd100MultiBot|r", ["info.buttontoggle"] = "|cff00ff00Left-click: toggle UI|r", ["info.buttonoptions"] = "|cffff0000Right-click: options|r", diff --git a/Locales/MultiBotAceLocale-esES.lua b/Locales/MultiBotAceLocale-esES.lua index 1295283..a24810f 100644 --- a/Locales/MultiBotAceLocale-esES.lua +++ b/Locales/MultiBotAceLocale-esES.lua @@ -7,6 +7,7 @@ local esESValues = { ["info.itemdestroyalert"] = "¿REALMENTE quieres destruir este objeto?\n%s", ["info.keydestroyalert"] = "No venderé llaves.", ["info.itemsellalert"] = "No puedo vender este objeto.", + ["info.inventoryvendortarget"] = "Primero debes seleccionar un vendedor.", ["info.butttitle"] = "|cffffd100MultiBot|r", ["info.buttontoggle"] = "|cff00ff00Clic izquierdo: alternar la interfaz|r", ["info.buttonoptions"] = "|cffff0000Clic derecho: opciones|r", diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index da0f2f8..b731e0f 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -7,6 +7,7 @@ local frFRValues = { ["info.itemdestroyalert"] = "Voulez-vous VRAIMENT détruire cet objet ?\n%s", ["info.keydestroyalert"] = "Je ne peux pas vendre des clés.", ["info.itemsellalert"] = "Je ne peux pas vendre cet item.", + ["info.inventoryvendortarget"] = "Vous devez dabord sélectionner un vendeur.", ["info.butttitle"] = "|cffffd100MultiBot|r", ["info.buttontoggle"] = "|cff00ff00Clic gauche : afficher/masquer l’interface|r", ["info.buttonoptions"] = "|cffff0000Clic droit : options|r", diff --git a/Locales/MultiBotAceLocale-koKR.lua b/Locales/MultiBotAceLocale-koKR.lua index a0285dc..5d43c0e 100644 --- a/Locales/MultiBotAceLocale-koKR.lua +++ b/Locales/MultiBotAceLocale-koKR.lua @@ -7,6 +7,7 @@ local koKRValues = { ["info.itemdestroyalert"] = "이 아이템을 정말로 파기하시겠습니까?\n%s", ["info.keydestroyalert"] = "열쇠는 판매하지 않습니다.", ["info.itemsellalert"] = "이 아이템은 판매할 수 없습니다.", + ["info.inventoryvendortarget"] = "먼저 판매자를 선택해야 합니다.", ["info.butttitle"] = "|cffffd100MultiBot|r", ["info.buttontoggle"] = "|cff00ff00좌클릭: UI 전환|r", ["info.buttonoptions"] = "|cffff0000우클릭: 옵션|r", diff --git a/Locales/MultiBotAceLocale-ruRU.lua b/Locales/MultiBotAceLocale-ruRU.lua index 2d54ed9..9eb65cf 100644 --- a/Locales/MultiBotAceLocale-ruRU.lua +++ b/Locales/MultiBotAceLocale-ruRU.lua @@ -7,6 +7,7 @@ local ruRUValues = { ["info.itemdestroyalert"] = "Вы ДЕЙСТВИТЕЛЬНО хотите уничтожить этот предмет?\n%s", ["info.keydestroyalert"] = "Я не продаю ключи.", ["info.itemsellalert"] = "Я не могу продать этот предмет.", + ["info.inventoryvendortarget"] = "Сначала вы должны выбрать продавца.", ["info.butttitle"] = "|cffffd100MultiBot|r", ["info.buttontoggle"] = "|cff00ff00ЛКМ: переключить интерфейс|r", ["info.buttonoptions"] = "|cffff0000ПКМ: настройки|r", diff --git a/Locales/MultiBotAceLocale-zhCN.lua b/Locales/MultiBotAceLocale-zhCN.lua index 1306af0..cf00cbf 100644 --- a/Locales/MultiBotAceLocale-zhCN.lua +++ b/Locales/MultiBotAceLocale-zhCN.lua @@ -7,6 +7,7 @@ local zhCNValues = { ["info.itemdestroyalert"] = "你真的要销毁这个物品吗?\n%s", ["info.keydestroyalert"] = "我不会出售钥匙。", ["info.itemsellalert"] = "我无法出售该物品。", + ["info.inventoryvendortarget"] = "您必须先选择一位商人。", ["info.butttitle"] = "|cffffd100MultiBot|r", ["info.buttontoggle"] = "|cff00ff00左键:切换界面|r", ["info.buttonoptions"] = "|cffff0000右键:选项|r", diff --git a/MultiBot.toc b/MultiBot.toc index 970bfc3..5e6640a 100644 --- a/MultiBot.toc +++ b/MultiBot.toc @@ -62,6 +62,8 @@ UI\MultiBotStats.lua UI\MultiBotSpell.lua UI\MultiBotSpellBookFrame.lua UI\MultiBotRewardFrame.lua +UI\MultiBotInventoryFrame.lua +UI\MultiBotInventoryItem.lua UI\MultiBotItem.lua UI\MultiBotTalentFrame.lua Core\MultiBotInit.lua diff --git a/TODO.md b/TODO.md index 087050d..5489a71 100644 --- a/TODO.md +++ b/TODO.md @@ -6,3 +6,5 @@ TODO * Quand on deplace ou fait quelque chose dans l'ui il faudrait que ça se sauvegarde tout de suite dans les variables dans deco reco * Raidus doit se rafraichir à l'ouverture et fermeture * dans la liste des quêtes des fois c'est l'ID de la queête qui apparait et pas le tritre +* Afficher le pognon et les places de sacs dans la frame inventaire +* La fenêtre inventaire doit se rafraichir par exemple quand on fait le bot bouffer il faut que ce qu'il a bouffé se décompte diff --git a/UI/MultiBotInventoryFrame.lua b/UI/MultiBotInventoryFrame.lua new file mode 100644 index 0000000..6abdf6f --- /dev/null +++ b/UI/MultiBotInventoryFrame.lua @@ -0,0 +1,941 @@ +if not MultiBot then return end + +local INVENTORY_WINDOW_DEFAULTS = { + width = 520, + height = 470, + pointX = -700, + pointY = -144, + actionsWidth = 172, + buttonSize = 32, + buttonSpacing = 38, + labelOffsetX = 42, + itemSize = 32, + itemSpacingX = 38, + itemSpacingY = 37, + itemsPerRow = 8, +} +local INVENTORY_LAYOUT_KEY = "InventoryPoint" + +local ACTION_ORDER = { "Sell", "Equip", "Use", "Trade", "Destroy" } +local ACTION_MODE_CONFIG = { + Sell = { value = "s", cancelTradeOnActivate = true }, + Equip = { value = "e", cancelTradeOnActivate = true }, + Use = { value = "u", cancelTradeOnActivate = true }, + Trade = { value = "give", cancelTradeOnActivate = false }, + Destroy = { value = "destroy", cancelTradeOnActivate = true }, +} + +local function getInventoryAceGUI() + if MultiBot.GetAceGUI then + local ace = MultiBot.GetAceGUI() + if type(ace) == "table" and type(ace.Create) == "function" then + return ace + end + end + + if type(LibStub) == "table" then + local ok, aceGUI = pcall(LibStub.GetLibrary, LibStub, "AceGUI-3.0", true) + if ok and type(aceGUI) == "table" and type(aceGUI.Create) == "function" then + return aceGUI + end + end + + return nil +end + +local inventoryEscapeIndex = 0 +local function registerInventoryEscapeClose(window, namePrefix) + if not window or not window.frame or type(UISpecialFrames) ~= "table" then + return + end + + if window.__mbEscapeName then + return + end + + inventoryEscapeIndex = inventoryEscapeIndex + 1 + local safePrefix = tostring(namePrefix or "Inventory"):gsub("[^%w_]", "") + local frameName = string.format("MultiBotAce%s_%d", safePrefix, inventoryEscapeIndex) + + window.__mbEscapeName = frameName + _G[frameName] = window.frame + + for _, existing in ipairs(UISpecialFrames) do + if existing == frameName then + return + end + end + + table.insert(UISpecialFrames, frameName) +end + +local function persistInventoryWindowPosition(frame) + if not frame or not MultiBot.SetSavedLayoutValue or not MultiBot.toPoint then + return + end + + local offsetX, offsetY = MultiBot.toPoint(frame) + MultiBot.SetSavedLayoutValue(INVENTORY_LAYOUT_KEY, offsetX .. ", " .. offsetY) +end + +local function bindInventoryWindowPosition(window) + if not window or not window.frame then + return + end + + local savedPoint = MultiBot.GetSavedLayoutValue and MultiBot.GetSavedLayoutValue(INVENTORY_LAYOUT_KEY) or nil + if type(savedPoint) == "string" and savedPoint ~= "" then + local splitPoint = MultiBot.doSplit(savedPoint, ", ") + local offsetX = tonumber(splitPoint[1]) + local offsetY = tonumber(splitPoint[2]) + if offsetX and offsetY then + window.frame:ClearAllPoints() + window.frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", offsetX, offsetY) + end + end + + if window.__mbPositionHooked then + return + end + + window.__mbPositionHooked = true + window.frame:HookScript("OnDragStop", function(frame) + persistInventoryWindowPosition(frame) + end) +end + +local function addSimpleBackdrop(frame, bgAlpha) + if not frame or not frame.SetBackdrop then + return + end + + frame:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + + if frame.SetBackdropColor then + frame:SetBackdropColor(0.06, 0.06, 0.08, bgAlpha or 0.92) + end + + if frame.SetBackdropBorderColor then + frame:SetBackdropBorderColor(0.35, 0.35, 0.35, 0.95) + end +end + +local function makeActionButton(parent, key, iconTexture, tooltipText, yOffset) + local size = INVENTORY_WINDOW_DEFAULTS.buttonSize + local button = CreateFrame("Button", nil, parent) + button:SetSize(size, size) + button:SetPoint("TOPLEFT", parent, "TOPLEFT", 12, yOffset) + button:RegisterForClicks("LeftButtonDown", "RightButtonDown") + button:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD") + button:SetPushedTexture("Interface\\Buttons\\UI-Quickslot-Depress") + + button.icon = button:CreateTexture(nil, "ARTWORK") + button.icon:SetAllPoints(button) + button.icon:SetTexture(MultiBot.SafeTexturePath(iconTexture)) + + button.border = button:CreateTexture(nil, "OVERLAY") + button.border:SetTexture("Interface\\AddOns\\MultiBot\\Icons\\border.blp") + button.border:SetPoint("TOPLEFT", button, "TOPLEFT", -2, 2) + button.border:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 2, -2) + button.border:Hide() + + button.state = false + button.tip = tooltipText + button.parent = parent.inventoryRef + button.actionKey = key + + function button.setDisable(_) + button.state = false + if button.icon and button.icon.SetDesaturated then + button.icon:SetDesaturated(true) + end + if button.border then + button.border:Hide() + end + return button + end + + function button.setEnable(_) + button.state = true + if button.icon and button.icon.SetDesaturated then + button.icon:SetDesaturated(false) + end + if button.border then + button.border:Show() + end + return button + end + + function button.getButton(_, index) + return button.parent and button.parent.getButton and button.parent.getButton(index) or nil + end + + function button.getName() + return MultiBot.inventory and MultiBot.inventory.name or nil + end + + button:SetScript("OnEnter", function(self) + if not self.tip or not GameTooltip then return end + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:SetText(self.tip, 1, 1, 1, true) + GameTooltip:Show() + end) + + button:SetScript("OnLeave", function() + if GameTooltip and GameTooltip.Hide then + GameTooltip:Hide() + end + end) + + button:SetScript("OnClick", function(self, mouseButton) + if mouseButton == "LeftButton" and self.doLeft then + self.doLeft(self) + return + end + + if mouseButton == "RightButton" and self.doRight then + self.doRight(self) + end + end) + + return button +end + +local function makeItemsContainer(parent, scrollChild) + local items = { + host = parent, + child = scrollChild, + buttons = {}, + index = 0, + iconSize = INVENTORY_WINDOW_DEFAULTS.itemSize, + spacingX = INVENTORY_WINDOW_DEFAULTS.itemSpacingX, + spacingY = INVENTORY_WINDOW_DEFAULTS.itemSpacingY, + itemsPerRow = INVENTORY_WINDOW_DEFAULTS.itemsPerRow, + } + + function items:getName() + return MultiBot.inventory and MultiBot.inventory.name or nil + end + + function items:get() + return MultiBot.inventory + end + + function items.getButton(index) + return MultiBot.inventory and MultiBot.inventory.getButton and MultiBot.inventory.getButton(index) or nil + end + + function items.catButton(_) + return items + end + + function items:getNextSlotPosition() + local perRow = math.max(1, self.itemsPerRow or 1) + local posX = (self.index % perRow) * (self.spacingX or 0) + local posY = math.floor(self.index / perRow) * -(self.spacingY or 0) + return posX, posY + end + + function items:addChatItem(itemInfo) + if not itemInfo or itemInfo == "" or not MultiBot.InventoryAddItem then + return nil + end + + return MultiBot.InventoryAddItem(self, itemInfo) + end + + function items:clear() + for key, button in pairs(self.buttons) do + if button and button.Hide then + button:Hide() + end + self.buttons[key] = nil + end + self.index = 0 + self:updateCanvas() + end + + function items:updateCanvas() + local count = 0 + for _ in pairs(self.buttons) do + count = count + 1 + end + local rows = math.max(1, math.ceil(count / (self.itemsPerRow or 1))) + local height = math.max(260, 20 + (rows * self.spacingY)) + self.child:SetHeight(height) + end + + function items.addButton(pName, pX, pY, pTexture, pTip) + local button = CreateFrame("Button", nil, items.child) + button:SetSize(items.iconSize, items.iconSize) + button:SetPoint("TOPLEFT", items.child, "TOPLEFT", pX, pY) + button:RegisterForClicks("LeftButtonDown", "RightButtonDown") + button:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD") + button:SetPushedTexture("Interface\\Buttons\\UI-Quickslot-Depress") + + button.icon = button:CreateTexture(nil, "ARTWORK") + button.icon:SetAllPoints(button) + button.icon:SetTexture(MultiBot.SafeTexturePath(pTexture)) + + button.border = button:CreateTexture(nil, "OVERLAY") + button.border:SetTexture("Interface\\AddOns\\MultiBot\\Icons\\border.blp") + button.border:SetPoint("TOPLEFT", button, "TOPLEFT", -2, 2) + button.border:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 2, -2) + + button.parent = items + button.name = pName + button.tip = pTip + button.texture = MultiBot.SafeTexturePath(pTexture) + button.size = items.iconSize + button.x = pX + button.y = pY + + function button.setAmount(pAmount) + if button.amount and button.amount.Hide then + button.amount:Hide() + end + button.amount = button:CreateFontString(nil, "OVERLAY", "NumberFontNormal") + button.amount:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 1, 1) + button.amount:SetText(pAmount) + return button + end + + function button.getButton(_, index) + return button.parent and button.parent.getButton and button.parent.getButton(index) or nil + end + + function button.getName() + return items:getName() + end + + button:SetScript("OnEnter", function(self) + if not self.tip or not GameTooltip then return end + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + if type(self.tip) == "string" and string.sub(self.tip, 1, 1) == "|" then + GameTooltip:SetHyperlink(self.tip) + else + GameTooltip:SetText(self.tip, 1, 1, 1, true) + end + GameTooltip:Show() + end) + + button:SetScript("OnLeave", function() + if GameTooltip and GameTooltip.Hide then + GameTooltip:Hide() + end + end) + + button:SetScript("OnClick", function(self, mouseButton) + if mouseButton == "LeftButton" and self.doLeft then + self.doLeft(self) + return + end + + if mouseButton == "RightButton" and self.doRight then + self.doRight(self) + end + end) + + items.buttons[pName] = button + items:updateCanvas() + return button + end + + return items +end + +local function updateModeLabel() + local inventory = MultiBot.inventory + if not inventory or not inventory.modeLabel then + return + end + + local labels = { + [""] = MultiBot.L("info.action", "Action") .. ": -", + s = MultiBot.L("info.action", "Action") .. ": Sell", + e = MultiBot.L("info.action", "Action") .. ": Equip", + u = MultiBot.L("info.action", "Action") .. ": Use", + give = MultiBot.L("info.action", "Action") .. ": Trade", + destroy = MultiBot.L("info.action", "Action") .. ": Destroy", + } + + inventory.modeLabel:SetText(labels[inventory.action or ""] or (MultiBot.L("info.action", "Action") .. ": -")) +end + +local function getInventoryWindowTitle(botName) + local defaultTitle = MB_INVENTORY_LABEL or INVENTORY_TOOLTIP or BAGSLOT or "Inventory" + if not botName or botName == "" then + return defaultTitle + end + + return MultiBot.doReplace(MultiBot.L("info.inventory", defaultTitle), "NAME", botName) +end + +local function disableActionModes(exceptKey) + local inventory = MultiBot.inventory + if not inventory or not inventory.buttons then + return + end + + for _, key in ipairs(ACTION_ORDER) do + if key ~= exceptKey then + local button = inventory.buttons[key] + if button and button.setDisable then + button.setDisable() + end + end + end +end + +local function syncInventoryButtonState(enabled) + local inventory = MultiBot.inventory + if not inventory or not inventory.name then + return + end + + local units = MultiBot.frames + and MultiBot.frames["MultiBar"] + and MultiBot.frames["MultiBar"].frames + and MultiBot.frames["MultiBar"].frames["Units"] + + local unitFrame = units and units.frames and units.frames[inventory.name] or nil + local sourceButton = unitFrame and unitFrame.getButton and unitFrame.getButton("Inventory") or nil + if not sourceButton then + return + end + + if enabled then + if sourceButton.setEnable then sourceButton.setEnable() end + else + if sourceButton.setDisable then sourceButton.setDisable() end + end +end + +local function getInventoryUnitsFrame() + return MultiBot.frames + and MultiBot.frames["MultiBar"] + and MultiBot.frames["MultiBar"].frames + and MultiBot.frames["MultiBar"].frames["Units"] + or nil +end + +local function getInventorySourceButton(botName) + if not botName or botName == "" then + return nil + end + + local units = getInventoryUnitsFrame() + local unitFrame = units and units.frames and units.frames[botName] or nil + return unitFrame and unitFrame.getButton and unitFrame.getButton("Inventory") or nil +end + +local function getInventoryWaitButton(botName) + if not botName or botName == "" then + return nil + end + + local units = getInventoryUnitsFrame() + return units and units.buttons and units.buttons[botName] or nil +end + +local function disableOtherInventoryButtons(activeBotName) + local units = getInventoryUnitsFrame() + if not units or not MultiBot.index or not MultiBot.index.actives then + return + end + + for _, botName in pairs(MultiBot.index.actives) do + if botName ~= UnitName("player") then + local button = units.frames + and units.frames[botName] + and units.frames[botName].getButton + and units.frames[botName].getButton("Inventory") + or nil + + if button and button.setDisable and botName ~= activeBotName then + button.setDisable() + end + end + end +end + +local function setInventoryBotName(botName) + local inventory = MultiBot.inventory + if not inventory then + return + end + + inventory.name = botName or "" + + if inventory.window and inventory.window.SetTitle then + inventory.window:SetTitle(getInventoryWindowTitle(inventory.name)) + end + + if inventory.helperText then + inventory.helperText:SetText(botName or "") + end +end + +local function resetInventoryViewState() + local inventory = MultiBot.inventory + if not inventory then + return + end + + setInventoryBotName("") + inventory.pendingLootBot = nil + + if inventory.resetItems then + inventory:resetItems() + end +end + +local function requestInventoryForBot(botName) + local waitButton = getInventoryWaitButton(botName) + if waitButton then + waitButton.waitFor = "INVENTORY" + end + + if botName and botName ~= "" then + SendChatMessage("items", "WHISPER", nil, botName) + end +end + +MultiBot.RequestBotInventory = function(botName) + if not botName or botName == "" then + return false + end + + local inventory = MultiBot.inventory + if (not inventory or not inventory.requestBotInventory) and MultiBot.InitializeInventoryFrame then + inventory = MultiBot.InitializeInventoryFrame() + end + + if inventory and inventory.requestBotInventory then + return inventory:requestBotInventory(botName) + end + + requestInventoryForBot(botName) + return true +end + +local function closeInventoryWindow() + local inventory = MultiBot.inventory + if not inventory then return end + if inventory.window then + inventory.window:Hide() + end + syncInventoryButtonState(false) + resetInventoryViewState() +end + +local function openInventoryWindow() + local inventory = MultiBot.inventory + if inventory and inventory.window then + inventory.window:Show() + syncInventoryButtonState(true) + end +end + +local function prepareInventoryForBot(botName) + if not botName or botName == "" then + return false + end + + disableOtherInventoryButtons(botName) + setInventoryBotName(botName) + + local sourceButton = getInventorySourceButton(botName) + if sourceButton and sourceButton.setEnable then + sourceButton.setEnable() + end + + requestInventoryForBot(botName) + return true +end + +local function setInventoryActionState(buttonKey, options) + local inventory = MultiBot.inventory + if not inventory then + return + end + + options = options or {} + + local nextState = buttonKey and ACTION_MODE_CONFIG[buttonKey] or nil + local previousAction = inventory.action or "" + local shouldCancelTrade = options.cancelTrade + + if shouldCancelTrade == nil then + shouldCancelTrade = previousAction == ACTION_MODE_CONFIG.Trade.value + and (not nextState or nextState.value ~= ACTION_MODE_CONFIG.Trade.value) + end + + if shouldCancelTrade then + CancelTrade() + end + + inventory.action = nextState and nextState.value or "" + disableActionModes(buttonKey) + + if nextState then + local button = inventory.buttons and inventory.buttons[buttonKey] or nil + if button and button.setEnable then + button.setEnable() + end + end + + updateModeLabel() +end + +local function toggleInventoryAction(buttonKey, button) + local inventory = MultiBot.inventory + local state = ACTION_MODE_CONFIG[buttonKey] + if not inventory or not state or not button then + return + end + + if button.state then + setInventoryActionState(nil, { + cancelTrade = state.value == ACTION_MODE_CONFIG.Trade.value, + }) + return + end + + if buttonKey == "Trade" then + InitiateTrade(button.getName()) + end + + setInventoryActionState(buttonKey, { + cancelTrade = state.cancelTradeOnActivate, + }) +end + +local function runInventoryInstantAction(botName, command, options) + options = options or {} + + if not botName or botName == "" or not command or command == "" then + return false + end + + if options.requiresTarget and not MultiBot.isTarget() then + return false + end + + if options.clearActionState then + CancelTrade() + setInventoryActionState(nil, { cancelTrade = false }) + end + + SendChatMessage(command, "WHISPER", nil, botName) + + if options.refreshDelay ~= nil and MultiBot.RefreshInventory then + MultiBot.RefreshInventory(options.refreshDelay) + elseif options.refresh and MultiBot.RefreshInventory then + MultiBot.RefreshInventory() + end + + return true +end + +local function createInventoryContent(window) + local content = window.content + content:SetPoint("TOPLEFT", window.frame, "TOPLEFT", 10, -30) + content:SetPoint("BOTTOMRIGHT", window.frame, "BOTTOMRIGHT", -10, 10) + + local root = CreateFrame("Frame", nil, content) + root:SetAllPoints(content) + addSimpleBackdrop(root, 0.90) + + local leftPanel = CreateFrame("Frame", nil, root) + leftPanel:SetPoint("TOPLEFT", root, "TOPLEFT", 8, -8) + leftPanel:SetPoint("BOTTOMLEFT", root, "BOTTOMLEFT", 8, 8) + leftPanel:SetWidth(INVENTORY_WINDOW_DEFAULTS.actionsWidth) + addSimpleBackdrop(leftPanel, 0.55) + + local itemsPanel = CreateFrame("Frame", nil, root) + itemsPanel:SetPoint("TOPLEFT", leftPanel, "TOPRIGHT", 10, 0) + itemsPanel:SetPoint("BOTTOMRIGHT", root, "BOTTOMRIGHT", -8, 8) + addSimpleBackdrop(itemsPanel, 0.55) + + local modeLabel = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal") + modeLabel:SetPoint("TOPLEFT", leftPanel, "TOPLEFT", 12, -14) + modeLabel:SetPoint("TOPRIGHT", leftPanel, "TOPRIGHT", -12, -14) + modeLabel:SetJustifyH("LEFT") + modeLabel:SetText(MultiBot.L("info.action", "Action") .. ": Sell") + + local helperText = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + helperText:SetPoint("TOPLEFT", modeLabel, "BOTTOMLEFT", 0, -8) + helperText:SetPoint("TOPRIGHT", leftPanel, "TOPRIGHT", -12, -8) + helperText:SetJustifyH("LEFT") + helperText:SetJustifyV("TOP") + helperText:SetText("") + + local scrollFrame = CreateFrame("ScrollFrame", "MultiBotInventoryScrollFrame", itemsPanel, "UIPanelScrollFrameTemplate") + scrollFrame:SetPoint("TOPLEFT", itemsPanel, "TOPLEFT", 8, -8) + scrollFrame:SetPoint("BOTTOMRIGHT", itemsPanel, "BOTTOMRIGHT", -28, 8) + + local scrollChild = CreateFrame("Frame", nil, scrollFrame) + scrollChild:SetWidth(304) + scrollChild:SetHeight(260) + scrollFrame:SetScrollChild(scrollChild) + + local actionHost = { inventoryRef = nil } + local buttons = {} + local buttonDefs = { + { key = "Sell", texture = "inv_misc_coin_16", tip = MultiBot.L("tips.inventory.sell") }, + { key = "SellGrey", texture = "inv_misc_coin_03", tip = MultiBot.L("tips.inventory.sellgrey") }, + { key = "SellVendor", texture = "inv_misc_coin_04", tip = MultiBot.L("tips.inventory.sellvendor") }, + { key = "Equip", texture = "inv_helmet_22", tip = MultiBot.L("tips.inventory.equip") }, + { key = "Use", texture = "inv_gauntlets_25", tip = MultiBot.L("tips.inventory.use") }, + { key = "Trade", texture = "achievement_reputation_01", tip = MultiBot.L("tips.inventory.trade") }, + { key = "Destroy", texture = "inv_hammer_15", tip = MultiBot.L("tips.inventory.drop") }, + { key = "Open", texture = "inv_misc_gift_05", tip = MultiBot.L("tips.inventory.open") }, + } + + for index, definition in ipairs(buttonDefs) do + local yOffset = -54 - ((index - 1) * INVENTORY_WINDOW_DEFAULTS.buttonSpacing) + buttons[definition.key] = makeActionButton(leftPanel, definition.key, definition.texture, definition.tip, yOffset) + end + + local items = makeItemsContainer(itemsPanel, scrollChild) + + return { + root = root, + leftPanel = leftPanel, + itemsPanel = itemsPanel, + items = items, + modeLabel = modeLabel, + helperText = helperText, + actionHost = actionHost, + buttons = buttons, + } +end + +function MultiBot.InitializeInventoryFrame() + if MultiBot.inventory and MultiBot.inventory.__aceInitialized then + return MultiBot.inventory + end + + local aceGUI = getInventoryAceGUI() + if not aceGUI then + UIErrorsFrame:AddMessage("AceGUI-3.0 is required for Inventory", 1, 0.2, 0.2, 1) + return nil + end + + local window = aceGUI:Create("Window") + window:SetTitle(getInventoryWindowTitle(nil)) + window:SetLayout("Manual") + window:SetWidth(INVENTORY_WINDOW_DEFAULTS.width) + window:SetHeight(INVENTORY_WINDOW_DEFAULTS.height) + window:EnableResize(false) + window.frame:SetClampedToScreen(true) + window.frame:SetFrameStrata("HIGH") + window.frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", INVENTORY_WINDOW_DEFAULTS.pointX, INVENTORY_WINDOW_DEFAULTS.pointY) + window:SetCallback("OnClose", function(widget) + closeInventoryWindow() + widget:Hide() + end) + window:Hide() + window.frame:HookScript("OnHide", function() + syncInventoryButtonState(false) + end) + + registerInventoryEscapeClose(window, "Inventory") + bindInventoryWindowPosition(window) + + local content = createInventoryContent(window) + + local inventory = { + __aceInitialized = true, + window = window, + root = content.root, + buttons = content.buttons, + frames = { Items = content.items }, + texts = { Title = content.modeLabel }, + modeLabel = content.modeLabel, + helperText = content.helperText, + name = "", + action = "s", + pendingLootBot = nil, + } + + MultiBot.inventory = inventory + + content.actionHost.inventoryRef = inventory + for _, button in pairs(content.buttons) do + button.parent = inventory + end + + function inventory.setText(key, value) + if key == "Title" then + if inventory.window and inventory.window.SetTitle then + inventory.window:SetTitle(value or getInventoryWindowTitle(inventory.name)) + end + return inventory + end + + if key == "Mode" and inventory.modeLabel then + inventory.modeLabel:SetText(value or "") + end + return inventory + end + + function inventory.getButton(index) + return inventory.buttons and inventory.buttons[index] or nil + end + + function inventory.getFrame(index) + return inventory.frames and inventory.frames[index] or nil + end + + function inventory:Show() + openInventoryWindow() + end + + function inventory:Hide() + closeInventoryWindow() + end + + function inventory:IsVisible() + return self.window and self.window.frame and self.window.frame:IsShown() or false + end + + function inventory:GetRight() + return self.window and self.window.frame and self.window.frame:GetRight() or 0 + end + + function inventory:GetBottom() + return self.window and self.window.frame and self.window.frame:GetBottom() or 0 + end + + function inventory.setPoint(x, y) + if type(x) ~= "number" or type(y) ~= "number" then return end + window.frame:ClearAllPoints() + window.frame:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", x, y) + persistInventoryWindowPosition(window.frame) + end + + function inventory:resetItems() + local items = self.frames and self.frames.Items + if items and items.clear then + items:clear() + end + if items then + items.index = 0 + end + end + + function inventory:setBotName(botName) + setInventoryBotName(botName) + return inventory + end + + function inventory:requestBotInventory(botName) + return prepareInventoryForBot(botName) + end + + function inventory:refresh(delay, botName) + local targetBotName = botName or self.name + if not targetBotName or targetBotName == "" or not self:IsVisible() then + return false + end + + local function doRefresh() + if not self:IsVisible() then + return false + end + + return prepareInventoryForBot(targetBotName) + end + + if type(delay) == "number" and delay > 0 and MultiBot.TimerAfter then + MultiBot.TimerAfter(delay, doRefresh) + return true + end + + return doRefresh() + end + + function inventory:markLootPending(botName) + local targetBotName = botName or self.name + if not targetBotName or targetBotName == "" then + return false + end + + self.pendingLootBot = targetBotName + return true + end + + function inventory:handleLootReceived(botName) + local targetBotName = botName or self.pendingLootBot + if not targetBotName or targetBotName == "" then + return false + end + + if self.pendingLootBot and self.pendingLootBot ~= targetBotName then + return false + end + + self.pendingLootBot = nil + return self:refresh(nil, targetBotName) + end + + function inventory:beginPayload(botName) + setInventoryBotName(botName or "") + self.pendingLootBot = nil + self:resetItems() + return self + end + + function inventory:appendItem(itemInfo) + local items = self.frames and self.frames.Items + if items and items.addChatItem then + return items:addChatItem(itemInfo) + end + + if MultiBot.addItem and items then + return MultiBot.addItem(items, itemInfo) + end + + return nil + end + + for _, key in ipairs(ACTION_ORDER) do + inventory.buttons[key].doLeft = function(pButton) + toggleInventoryAction(key, pButton) + end + end + + inventory.buttons.SellGrey.doLeft = function(pButton) + runInventoryInstantAction(pButton.getName(), "s *", { + requiresTarget = true, + clearActionState = true, + refreshDelay = 0.5, + }) + end + + inventory.buttons.SellVendor.doLeft = function(pButton) + runInventoryInstantAction(pButton.getName(), "s vendor", { + requiresTarget = true, + clearActionState = true, + refresh = true, + }) + end + + inventory.buttons.Open.doLeft = function(pButton) + runInventoryInstantAction(pButton.getName(), "open items") + end + + setInventoryActionState("Sell", { cancelTrade = false }) + resetInventoryViewState() + + return inventory +end \ No newline at end of file diff --git a/UI/MultiBotInventoryItem.lua b/UI/MultiBotInventoryItem.lua new file mode 100644 index 0000000..5a827c4 --- /dev/null +++ b/UI/MultiBotInventoryItem.lua @@ -0,0 +1,254 @@ +if not MultiBot then return end + +local function inventoryItemL(key, fallback) + return MultiBot.L("info." .. key, fallback) +end + +local function buildInventoryButtonKey(frame, itemName) + return string.format("%s_%d", itemName or "Item", frame.index or 0) +end + +local function buildInventoryItemLink(parts) + return "|" .. parts[2] .. "|" .. parts[3] .. "|" .. parts[4] .. "|h|r" +end + +local function splitInventoryItemPayload(itemInfo) + local parts = MultiBot.doSplit(itemInfo or "", "|") + local itemData = parts[3] and MultiBot.doSplit(parts[3], ":") or {} + return parts, itemData +end + +local function extractInventoryItemCount(parts) + local amountInfo = parts and parts[6] or nil + if type(amountInfo) ~= "string" or string.sub(amountInfo, 1, 2) ~= "rx" then + return nil + end + + local amountToken = MultiBot.doSplit(amountInfo, " ")[1] + local amount = tonumber(string.sub(amountToken or "", 3)) + return amount and amount > 1 and amount or nil +end + +local function resolveInventoryItemName(parts, itemName) + if itemName ~= nil then + return itemName + end + + local rawLinkText = parts and parts[4] or nil + if type(rawLinkText) ~= "string" or string.len(rawLinkText) < 4 then + return "Item" + end + + return string.sub(rawLinkText, 3, string.len(rawLinkText) - 1) +end + +local function resolveInventoryItemLink(parts, itemLink) + if itemLink ~= nil then + return itemLink + end + + return buildInventoryItemLink(parts) +end + +local function resolveInventoryItemRarity(itemRare) + if itemRare ~= nil then + return itemRare + end + + return 4 +end + +local function getInventoryItemPosition(frame) + if frame and frame.getNextSlotPosition then + return frame:getNextSlotPosition() + end + + local index = (frame and frame.index) or 0 + local itemsPerRow = (frame and frame.itemsPerRow) or 8 + local spacingX = (frame and frame.spacingX) or 38 + local spacingY = (frame and frame.spacingY) or 37 + return (index % itemsPerRow) * spacingX, math.floor(index / itemsPerRow) * -spacingY +end + +local function buildInventoryItemRecord(itemInfo) + local parts, itemData = splitInventoryItemPayload(itemInfo) + local itemId = itemData[2] + if not itemId or itemId == "" then + return nil + end + + local itemIcon = GetItemIcon(itemId) + local itemName, itemLink, itemRare = GetItemInfo(itemId) + + return { + id = itemId, + icon = itemIcon, + name = resolveInventoryItemName(parts, itemName), + link = resolveInventoryItemLink(parts, itemLink), + rare = resolveInventoryItemRarity(itemRare), + count = extractInventoryItemCount(parts), + info = itemInfo, + parts = parts, + } +end + +local function getInventoryItemActionState() + local inventory = MultiBot.inventory or {} + return inventory.action or "", inventory.name or "" +end + +local function requestInventoryRefresh(delay) + if MultiBot.RefreshInventory then + MultiBot.RefreshInventory(delay) + end +end + +local function bindInventoryDestroyConfirm(button, botName) + if not StaticPopupDialogs["MULTIBOT_CONFIRM_DESTROY"] then + StaticPopupDialogs["MULTIBOT_CONFIRM_DESTROY"] = { + text = inventoryItemL("itemdestroyalert", "Are you sure you want to destroy this item?"), + button1 = OKAY, + button2 = CANCEL, + timeout = 0, + whileDead = 1, + hideOnEscape = 1, + OnAccept = function(_, data) + if not data or not data.button then return end + sendInventoryItemCommand("destroy", data.button, data.botName, { + hideButton = true, + }) + end, + } + end + + StaticPopup_Show("MULTIBOT_CONFIRM_DESTROY", button.item.link, nil, { + button = button, + botName = botName, + }) +end + +local function sendInventoryFeedback(key, fallback) + SendChatMessage(inventoryItemL(key, fallback), "SAY") +end + +local function isInventoryProtectedKey(item) + return MultiBot.isInside(item and item.info or "", "%f[%a][Kk]ey%f[%A]") +end + +local function isInventoryProtectedHearthstone(item) + return item and item.id == "6948" +end + +local function needsInventoryDestroyConfirmation(item) + return isInventoryProtectedHearthstone(item) + or isInventoryProtectedKey(item) + or ((item and item.rare or 0) > 3) +end + +local function sendInventoryItemCommand(command, button, botName, options) + options = options or {} + + if not command or command == "" or not button or not botName or botName == "" then + return false + end + + SendChatMessage(command .. " " .. button.tip, "WHISPER", nil, botName) + + if options.hideButton and button.Hide then + button:Hide() + end + + if options.refreshDelay ~= nil then + requestInventoryRefresh(options.refreshDelay) + elseif options.refresh then + requestInventoryRefresh() + end + + return true +end + +local function handleInventoryItemClick(button) + local action, botName = getInventoryItemActionState() + local item = button and button.item or nil + + if action == "" then + sendInventoryFeedback("action", "Choose an action first") + return + end + + if action == "s" then + if not MultiBot.isTarget() then + sendInventoryFeedback("inventoryvendortarget", "Target a vendor first") + return + end + + if isInventoryProtectedHearthstone(item) then + sendInventoryFeedback("itemsellalert", "You cannot sell this item") + return + end + + if isInventoryProtectedKey(item) then + sendInventoryFeedback("keydestroyalert", "I will not sell Keys.") + return + end + + sendInventoryItemCommand(action, button, botName, { + hideButton = true, + refreshDelay = 0.3, + }) + return + end + + if action == "e" or action == "u" or action == "give" then + sendInventoryItemCommand(action, button, botName) + return + end + + if action ~= "destroy" then + return + end + + if needsInventoryDestroyConfirmation(item) then + bindInventoryDestroyConfirm(button, botName) + return + end + + sendInventoryItemCommand(action, button, botName, { + hideButton = true, + }) +end + +MultiBot.InventoryAddItem = function(frame, itemInfo) + if not frame then + return nil + end + + local item = buildInventoryItemRecord(itemInfo) + if not item then + return nil + end + + local itemX, itemY = getInventoryItemPosition(frame) + local itemIndex = frame.index or 0 + local buttonKey = buildInventoryButtonKey(frame, item.name) + local button = frame.addButton(buttonKey, itemX, itemY, item.icon, item.link) + if frame.catButton ~= nil then + frame.catButton("Catecher", 270, -490, 308, 524) + end + + item.index = itemIndex + item.x = itemX + item.y = itemY + button.item = item + + button.doLeft = handleInventoryItemClick + + if item.count then + button.setAmount(item.count) + end + + frame.index = itemIndex + 1 + return button +end + +MultiBot.addItem = MultiBot.InventoryAddItem \ No newline at end of file diff --git a/UI/MultiBotItem.lua b/UI/MultiBotItem.lua index 1a2cd34..11f5399 100644 --- a/UI/MultiBotItem.lua +++ b/UI/MultiBotItem.lua @@ -1,105 +1,3 @@ -local function itemL(key, fallback) - return MultiBot.L("info." .. key, fallback) -end +if not MultiBot then return end -MultiBot.addItem = function(pFrame, pInfo) - local tInfo = MultiBot.doSplit(pInfo, "|") - local tID = MultiBot.doSplit(tInfo[3], ":")[2] - - local tIcon = GetItemIcon(tID) - local tName, tLink, tRare = GetItemInfo(tID) - - local tX = (pFrame.index%8) * 38 - local tY = math.floor(pFrame.index/8) * -37.1 - - if(tName == nil) then tName = string.sub(tInfo[4], 3, string.len(tInfo[4]) - 1) end - if(tLink == nil) then tLink = "|" .. tInfo[2] .. "|" .. tInfo[3] .. "|" .. tInfo[4] .. "|h|r" end - if(tRare == nil) then tRare = 4 end -- for Security - - local tButton = pFrame.addButton(tName, tX, tY, tIcon, tLink) - pFrame.catButton("Catecher", 270, -490, 308, 524) - - tButton.item = {} - tButton.item.id = tID - tButton.item.link = tLink - tButton.item.name = tName - tButton.item.info = pInfo - tButton.item.rare = tRare - - tButton.doLeft = function(pButton) - local tAction = MultiBot.inventory.action - -- Nom du bot cible (destinataire du whisper), on évite de masquer tName (nom de l'objet) - local botName = MultiBot.inventory.name - - if(tAction == "") then - SendChatMessage(itemL("action", "Choose an action first"), "SAY") - return - end - - if(tAction == "s" and MultiBot.isTarget()) then - if(pButton.item.id == "6948") then - return SendChatMessage(itemL("itemsellalert", "You cannot sell this item"), "SAY") - end - - if(MultiBot.isInside(pButton.item.info or "", "%f[%a][Kk]ey%f[%A]")) then - return SendChatMessage(itemL("keydestroyalert", "You cannot destroy a key"), "SAY") - end - - -- Envoi de la commande "s [item]" au bot dont l’inventaire est ouvert - SendChatMessage(tAction .. " " .. pButton.tip, "WHISPER", nil, botName) - - -- On masque l’item cliqué pour un feedback immédiat - pButton:Hide() - - -- Puis on relit proprement l’inventaire du bot pour se resynchroniser - if MultiBot.RefreshInventory then - MultiBot.RefreshInventory(0.3) - end - - return - end - - if(tAction == "e" or tAction == "u" or tAction == "give") then - SendChatMessage(tAction .. " " .. pButton.tip, "WHISPER", nil, botName) - return - end - - if(tAction == "destroy") then - local needsConfirm = false - if(pButton.item.id == "6948") then needsConfirm = true end -- Hearthstone - if(MultiBot.isInside(pButton.item.info, "%f[%a][Kk]ey%f[%A]")) then needsConfirm = true end - if(pButton.item.rare > 3) then needsConfirm = true end -- Épique ou mieux - if needsConfirm then - if not StaticPopupDialogs["MULTIBOT_CONFIRM_DESTROY"] then - StaticPopupDialogs["MULTIBOT_CONFIRM_DESTROY"] = { - text = itemL("itemdestroyalert", "Are you sure you want to destroy this item?"), - button1 = OKAY, - button2 = CANCEL, - timeout = 0, - whileDead = 1, - hideOnEscape = 1, - OnAccept = function(self, data) - if not data or not data.button then return end - SendChatMessage("destroy" .. " " .. data.button.tip, "WHISPER", nil, data.tName) - data.button:Hide() - end, - } - end - --local data = { button = pButton, tName = tName } - local data = { button = pButton, tName = botName } - StaticPopup_Show("MULTIBOT_CONFIRM_DESTROY", pButton.item.link, nil, data) - return - end - -- Pas de confirmation requise - SendChatMessage(tAction .. " " .. pButton.tip, "WHISPER", nil, botName) - pButton:Hide() - return - end - end - - if(string.sub(tInfo[6], 1, 2) == "rx") then - tButton.setAmount(string.sub(MultiBot.doSplit(tInfo[6], " ")[1], 3)) - end - - pFrame.index = pFrame.index + 1 -end \ No newline at end of file +MultiBot.addItem = MultiBot.InventoryAddItem \ No newline at end of file diff --git a/UI/MultiBotRewardFrame.lua b/UI/MultiBotRewardFrame.lua index 9550e68..4850c35 100644 --- a/UI/MultiBotRewardFrame.lua +++ b/UI/MultiBotRewardFrame.lua @@ -33,16 +33,9 @@ end local function requestBotInventory(botName) if(botName == nil) then return end - local unitsButtons = MultiBot.frames - and MultiBot.frames["MultiBar"] - and MultiBot.frames["MultiBar"].frames - and MultiBot.frames["MultiBar"].frames["Units"] - and MultiBot.frames["MultiBar"].frames["Units"].buttons - - local botButton = unitsButtons and unitsButtons[botName] or nil - if(botButton ~= nil) then botButton.waitFor = "INVENTORY" end - - SendChatMessage("items", "WHISPER", nil, botName) + if(MultiBot.RequestBotInventory) then + MultiBot.RequestBotInventory(botName) + end end local function buildRow(parent, yOffset) diff --git a/docs/ace3-inventory-migration-tracker.md b/docs/ace3-inventory-migration-tracker.md new file mode 100644 index 0000000..f23dfc6 --- /dev/null +++ b/docs/ace3-inventory-migration-tracker.md @@ -0,0 +1,212 @@ +# Ace3 Inventory Migration Tracker (Milestone 8) + +Dedicated tracking document for the full migration of the bot **INVENTORY** frame from the legacy `MultiBot.newFrame(...)` path to a native AceGUI/Ace3 implementation. + +> Scope: migrate the bot inventory screen to a standalone UI module under `UI/`, remove the legacy frame shell for this screen, preserve all gameplay behavior, and modernize the Lua structure without regressing the playerbots command flow. + +--- + +## 1) Current legacy scope + +### Source-of-truth files +- `Core/MultiBotInit.lua` +- `Core/MultiBotEvery.lua` +- `Core/MultiBotHandler.lua` +- `Core/MultiBotEngine.lua` +- `UI/MultiBotItem.lua` +- `UI/MultiBotRewardFrame.lua` + +### Legacy responsibilities currently coupled together +- Window creation and static layout. +- Inventory mode/action state (`sell`, `equip`, `use`, `trade`, `destroy`). +- Item grid population from bot chat lines. +- Refresh orchestration (`items` request replay). +- Close/open synchronization with the bot button in the main MultiBar. +- Follow-up flows after trade close and loot/open events. + +--- + +## 2) Migration goals + +### Functional goals +- [ ] Preserve the exact bot command protocol (`items`, `stats`, `open items`, `s *`, `s vendor`, item actions by whisper). +- [ ] Preserve close/open parity with the per-bot `Inventory` button. +- [ ] Preserve the current action model: exclusive action modes plus instant actions. +- [ ] Preserve refresh behavior after sell, trade close, loot/open, and bulk vendor actions. +- [ ] Preserve safeguards around Hearthstone, keys, and epic+ destruction confirmation. +- [ ] Preserve title updates and selected bot state. +- [ ] Preserve compatibility with callers outside the main Inventory button flow (notably reward/inspect helpers). + +### Technical goals +- [ ] Remove the legacy visual shell for `MultiBot.inventory`. +- [ ] Rebuild the screen as a native AceGUI window, not a legacy frame hosted inside AceGUI. +- [ ] Move the screen implementation into a dedicated file under `UI/`. +- [ ] Reduce UI/protocol coupling by introducing a clearer controller boundary. +- [ ] Modernize local helpers/state handling in Lua while keeping the existing addon architecture stable. +- [ ] Keep position persistence behavior aligned with the existing `InventoryPoint` expectation, or migrate it safely. + +--- + +## 3) Recommended target structure + +### Planned UI/module split +- [x] `UI/MultiBotInventoryFrame.lua` + - AceGUI host window creation. + - Layout composition. + - Widget lifecycle. + - Public open/hide/refresh hooks for the inventory screen. +- [x] `UI/MultiBotInventoryItem.lua` *(chosen split: inventory item renderer extracted from the generic legacy file for this migration)* + - Item widget creation/binding. + - Tooltip binding. + - Item click dispatch to the active inventory action. +- [ ] Existing handler integration in `Core/MultiBotHandler.lua` + - Chat-driven data intake remains here unless a later refactor extracts protocol dispatch more broadly. +- [ ] Existing request/refresh integration in `Core/MultiBotEvery.lua` and `Core/MultiBotEngine.lua` + - Keep the external entrypoints stable while redirecting them to the new module behavior. + +### Target responsibilities +- [ ] Window/controller state. +- [ ] Action-mode state. +- [ ] Item collection/render state. +- [ ] Refresh/request bridge. +- [ ] Legacy compatibility shims kept only where needed during transition. + +--- + +## 4) Feature parity checklist + +### A. Window lifecycle +- [x] Opening inventory from a bot button selects the correct bot. +- [x] Opening inventory disables other bots' inventory toggles as before. +- [x] Closing from the window close button synchronizes the source bot button state. +- [x] Reopening the same bot inventory after close works without recreating stale state. +- [x] Escape/close behavior is consistent with other migrated AceGUI windows if applicable. + +### B. Header/state +- [x] Window title reflects `NAME's Inventory` / localized equivalent. +- [x] Current bot name is stored centrally and reused by refresh/actions. +- [x] Default state when no bot is active is safe and inert. + +### C. Action controls +- [x] `Sell` is an exclusive toggle mode. +- [x] `Equip` is an exclusive toggle mode. +- [x] `Use` is an exclusive toggle mode. +- [x] `Trade` is an exclusive toggle mode and still initiates trade correctly. +- [x] `Destroy` is an exclusive toggle mode. +- [x] `SellGrey` remains an instant action (`s *`). +- [x] `SellVendor` remains an instant action (`s vendor`). +- [x] `Open` remains an instant action (`open items`). +- [x] Switching modes still cancels incompatible trade state where required. +- [x] The UI clearly exposes which action mode is active. + +### D. Item grid/content +- [x] The item list is cleared before a new inventory payload is rendered. +- [x] Items are added incrementally as chat lines arrive. +- [x] Tooltips show the item hyperlink correctly. +- [x] Clicking an item with no selected action still gives user feedback. +- [ ] Layout supports the full inventory payload without depending on the legacy background texture shell. + +### E. Item action rules +- [x] Selling still requires a valid vendor target. +- [x] Selling Hearthstone is blocked. +- [x] Selling keys is blocked. +- [x] Destroying Hearthstone asks for confirmation. +- [x] Destroying keys asks for confirmation. +- [x] Destroying epic-or-better items asks for confirmation. +- [x] Equip/use/trade actions still send the correct whisper command. +- [x] Immediate local feedback after destructive/vendor actions remains coherent. + +### F. Refresh/event flows +- [x] `RefreshInventory(delay)` still works against the new window/controller state. +- [x] Selling an item triggers a delayed refresh. +- [x] `SellGrey` triggers a refresh. +- [x] `SellVendor` triggers a refresh. +- [x] `TRADE_CLOSED` still refreshes the currently open inventory. +- [x] Opening loot containers still triggers the `LOOT -> INVENTORY` recovery path. + +### G. Cross-feature integration +- [x] Reward/inspect helpers can still request inventory data for a bot. +- [x] The main MultiBar button flow remains the canonical entrypoint. +- [x] No regression is introduced for stats/inspect follow-up triggered during inventory loading. + +--- + +## 5) Proposed migration sequence + +### Phase 1 — Extraction prep +- [ ] Document all current entrypoints and side effects. +- [x] Inventory module file introduced under `UI/` (`UI/MultiBotInventoryFrame.lua`). +- [x] Item rendering moved to `UI/MultiBotInventoryItem.lua` with `UI/MultiBotItem.lua` kept as a thin compatibility shim. + +### Phase 2 — AceGUI window host +- [x] AceGUI inventory host window created. +- [x] Recreate header/title/close behavior natively. +- [x] Visibility/open/close state wired to the existing bot-button flow. +- [x] Preserve persisted position semantics. + +### Phase 3 — Action layer +- [x] Recreate the left action column with AceGUI/native widgets as needed. +- [x] Replace manual button-to-button exclusivity with centralized action-state logic. +- [x] Preserve instant action commands and target checks. + +### Phase 4 — Item rendering +- [x] Rebuild the item area in the new screen. +- [x] Rebind tooltips and click handlers. +- [x] Preserve item metadata storage required by existing action rules. + +### Phase 5 — Handler/refresh integration +- [x] Redirect inventory population to the new view/controller. +- [x] Validate `INVENTORY -> ITEM -> LOOT` transitions. +- [x] Validate trade-close refresh and delayed refresh paths. + +### Phase 6 — Legacy removal +- [x] Remove the legacy Inventory frame construction from `Core/MultiBotInit.lua`. +- [x] Remove obsolete helper assumptions tied only to the old shell. +- [ ] Update the Milestone 8 docs/checklists to mark the screen as migrated. + +--- + +## 6) Non-regression test matrix + +### Manual gameplay checks +- [ ] Open bot A inventory from MultiBar. +- [ ] Switch to bot B inventory from MultiBar. +- [ ] Close inventory from the window close button. +- [ ] Activate `Sell`, click a normal item at vendor. +- [ ] Activate `Sell`, click Hearthstone. +- [ ] Run `SellGrey` at vendor. +- [ ] Run `SellVendor` at vendor. +- [ ] Activate `Equip`, click equippable item. +- [ ] Activate `Use`, click usable item. +- [ ] Activate `Trade`, click item, then close trade. +- [ ] Activate `Destroy`, click a normal item. +- [ ] Activate `Destroy`, click a protected item needing confirmation. +- [ ] Use `Open` on a loot container item. +- [ ] Trigger inventory request from reward/inspect path. + +### Static/code checks for the PR +- [ ] TOC load order updated if new UI file is added. +- [ ] No remaining user-facing dependency on the legacy Inventory texture shell. +- [ ] Inventory migration is documented in the milestone checklist files. +- [ ] No dead references to removed legacy inventory widgets remain. + +--- + +## 7) Open design decisions + +- [x] Item renderer moved to `UI/MultiBotInventoryItem.lua`; `UI/MultiBotItem.lua` now remains only as a compatibility shim for the existing global entrypoint. +- [ ] Should the new inventory host use pure AceGUI widgets for the item grid, or a hybrid AceGUI host plus native scroll child for dense icon rendering? +- [x] `InventoryPoint` is preserved as-is with backward-compatible layout persistence wiring for the AceGUI host. +- [ ] Should close-button parity be handled by calling the source MultiBar button behavior directly, or by centralizing open/close state in an inventory controller API? + +--- + +## 8) PR checklist for the future migration patch + +- [x] New dedicated inventory UI file added under `UI/`. +- [x] Legacy inventory shell removed from `Core/MultiBotInit.lua`. +- [ ] Existing inventory flows verified against the parity checklist above. +- [ ] `docs/ace3-ui-frame-inventory.md` updated. +- [ ] `docs/ace3-expansion-checklist.md` updated. +- [ ] Screenshot captured if the final UI change is visually testable in this environment. +- [ ] Final PR summary explicitly calls out preserved behaviors and any intentional UX improvements. \ No newline at end of file From 50eefb0e0a71b9c96ab5d390e3e0307b2876aa12 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:05:52 +0100 Subject: [PATCH 2/6] Some adjustements and checks --- .luacheckrc | 4 +- Locales/MultiBotAceLocale-deDE.lua | 2 +- Locales/MultiBotAceLocale-ruRU.lua | 2 +- UI/MultiBotInventoryFrame.lua | 67 +++++++++++++++++++----- UI/MultiBotInventoryItem.lua | 5 +- docs/ace3-inventory-migration-tracker.md | 6 +-- 6 files changed, 63 insertions(+), 23 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 88285e5..660d8a8 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -35,8 +35,8 @@ globals = { "GameFontNormal", "_G", "SetDesaturation", "WOW_PROJECT_ID", "WOW_PROJECT_MAINLINE", "ColorPickerFrame", "fixstrata", "UIDropDownMenu_HandleGlobalMouseEvent", "EventRegistry", "BackdropTemplateMixin", "TooltipBackdropTemplateMixin", "NORMAL_FONT_COLOR", "HIGHLIGHT_FONT_COLOR", "GameTooltip_SetDefaultAnchor", "ChatFrame1", "UISpecialFrames", "GetMouseFocus", "ShowUIPanel", "tremove", "min", "max", "GetMinimapShape", "GetMinimapShape", "PanelTemplates_TabResize", "GetGuildRosterShowOffline", "SetGuildRosterShowOffline", "IsInGuild", "GetGuildInfo", "SetGuildRosterShowOffline", "PLAYER", "INVENTORY_TOOLTIP", - "BAGSLOT", "UNKNOWN", "UnitIsDead", "ShowPrompt", "_MB_GetOrCreateShamanPos", "ensureHiddenTooltip", "MB_TAB_TITLE_DEFAULT", "SPELLBOOK", "MB_PAGE_DEFAULT", "SPELLBOOK_END_NON_SPELL_STREAK", - "RAID_CLASS_COLORS", "INSPECT" + "BAGSLOT", "UNKNOWN", "UnitIsDead", "ShowPrompt", "_MB_GetOrCreateShamanPos", "ensureHiddenTooltip", "MB_TAB_TITLE_DEFAULT", "SPELLBOOK", "MB_PAGE_DEFAULT", "SPELLBOOK_END_NON_SPELL_STREAK", "sendInventoryItemCommand", + "RAID_CLASS_COLORS", "INSPECT", "MB_INVENTORY_LABEL" } read_globals = { diff --git a/Locales/MultiBotAceLocale-deDE.lua b/Locales/MultiBotAceLocale-deDE.lua index e825df8..5216c71 100644 --- a/Locales/MultiBotAceLocale-deDE.lua +++ b/Locales/MultiBotAceLocale-deDE.lua @@ -7,7 +7,7 @@ local deDEValues = { ["info.itemdestroyalert"] = "Möchtest du diesen Gegenstand WIRKLICH zerstören?\n%s", ["info.keydestroyalert"] = "Ich verkaufe keine Schlüssel.", ["info.itemsellalert"] = "Ich kann diesen Gegenstand nicht verkaufen.", - ["info.inventoryvendortarget"] = "Sie müssen zuerst einen Händler auswählen.", + ["info.inventoryvendortarget"] = "Sie müssen zuerst einen Händler auswählen.", ["info.butttitle"] = "|cffffd100MultiBot|r", ["info.buttontoggle"] = "|cff00ff00Linksklick: UI umschalten|r", ["info.buttonoptions"] = "|cffff0000Rechtsklick: Optionen|r", diff --git a/Locales/MultiBotAceLocale-ruRU.lua b/Locales/MultiBotAceLocale-ruRU.lua index 9eb65cf..954d0d7 100644 --- a/Locales/MultiBotAceLocale-ruRU.lua +++ b/Locales/MultiBotAceLocale-ruRU.lua @@ -7,7 +7,7 @@ local ruRUValues = { ["info.itemdestroyalert"] = "Вы ДЕЙСТВИТЕЛЬНО хотите уничтожить этот предмет?\n%s", ["info.keydestroyalert"] = "Я не продаю ключи.", ["info.itemsellalert"] = "Я не могу продать этот предмет.", - ["info.inventoryvendortarget"] = "Сначала вы должны выбрать продавца.", + ["info.inventoryvendortarget"] = "Сначала вы должны выбрать продавца.", ["info.butttitle"] = "|cffffd100MultiBot|r", ["info.buttontoggle"] = "|cff00ff00ЛКМ: переключить интерфейс|r", ["info.buttonoptions"] = "|cffff0000ПКМ: настройки|r", diff --git a/UI/MultiBotInventoryFrame.lua b/UI/MultiBotInventoryFrame.lua index 6abdf6f..de9a65c 100644 --- a/UI/MultiBotInventoryFrame.lua +++ b/UI/MultiBotInventoryFrame.lua @@ -13,6 +13,9 @@ local INVENTORY_WINDOW_DEFAULTS = { itemSpacingX = 38, itemSpacingY = 37, itemsPerRow = 8, + itemsPanelPadding = 8, + scrollBarAllowance = 28, + minCanvasHeight = 260, } local INVENTORY_LAYOUT_KEY = "InventoryPoint" @@ -232,11 +235,22 @@ local function makeItemsContainer(parent, scrollChild) return MultiBot.inventory and MultiBot.inventory.getButton and MultiBot.inventory.getButton(index) or nil end - function items.catButton(_) - return items + function items:getAvailableWidth() + local hostWidth = self.host and self.host.GetWidth and self.host:GetWidth() or 0 + local horizontalPadding = (INVENTORY_WINDOW_DEFAULTS.itemsPanelPadding * 2) + INVENTORY_WINDOW_DEFAULTS.scrollBarAllowance + return math.max(self.iconSize, hostWidth - horizontalPadding) end + function items:refreshLayoutMetrics() + local stepX = math.max(self.iconSize, self.spacingX or self.iconSize) + local usableWidth = self:getAvailableWidth() + local additionalSlots = math.floor(math.max(0, usableWidth - self.iconSize) / stepX) + self.itemsPerRow = math.max(1, additionalSlots + 1) + self.child:SetWidth(math.max(usableWidth, self.itemsPerRow * stepX)) + end + function items:getNextSlotPosition() + self:refreshLayoutMetrics() local perRow = math.max(1, self.itemsPerRow or 1) local posX = (self.index % perRow) * (self.spacingX or 0) local posY = math.floor(self.index / perRow) * -(self.spacingY or 0) @@ -263,15 +277,38 @@ local function makeItemsContainer(parent, scrollChild) end function items:updateCanvas() - local count = 0 - for _ in pairs(self.buttons) do - count = count + 1 + self:refreshLayoutMetrics() + + local count = math.max(self.index or 0, 0) + if count == 0 then + for _ in pairs(self.buttons) do + count = count + 1 + end end - local rows = math.max(1, math.ceil(count / (self.itemsPerRow or 1))) - local height = math.max(260, 20 + (rows * self.spacingY)) + + local rows = math.max(1, math.ceil(count / math.max(1, self.itemsPerRow or 1))) + local height = math.max(INVENTORY_WINDOW_DEFAULTS.minCanvasHeight, 20 + (rows * self.spacingY)) self.child:SetHeight(height) end + function items:updateLayout() + self:refreshLayoutMetrics() + + for _, button in pairs(self.buttons) do + if button and button.ClearAllPoints then + local layoutIndex = button.layoutIndex or 0 + local posX = (layoutIndex % self.itemsPerRow) * (self.spacingX or 0) + local posY = math.floor(layoutIndex / self.itemsPerRow) * -(self.spacingY or 0) + button:ClearAllPoints() + button:SetPoint("TOPLEFT", self.child, "TOPLEFT", posX, posY) + button.x = posX + button.y = posY + end + end + + self:updateCanvas() + end + function items.addButton(pName, pX, pY, pTexture, pTip) local button = CreateFrame("Button", nil, items.child) button:SetSize(items.iconSize, items.iconSize) @@ -294,6 +331,7 @@ local function makeItemsContainer(parent, scrollChild) button.tip = pTip button.texture = MultiBot.SafeTexturePath(pTexture) button.size = items.iconSize + button.layoutIndex = items.index or 0 button.x = pX button.y = pY @@ -344,7 +382,7 @@ local function makeItemsContainer(parent, scrollChild) end) items.buttons[pName] = button - items:updateCanvas() + items:updateLayout() return button end @@ -679,12 +717,12 @@ local function createInventoryContent(window) helperText:SetText("") local scrollFrame = CreateFrame("ScrollFrame", "MultiBotInventoryScrollFrame", itemsPanel, "UIPanelScrollFrameTemplate") - scrollFrame:SetPoint("TOPLEFT", itemsPanel, "TOPLEFT", 8, -8) - scrollFrame:SetPoint("BOTTOMRIGHT", itemsPanel, "BOTTOMRIGHT", -28, 8) + scrollFrame:SetPoint("TOPLEFT", itemsPanel, "TOPLEFT", INVENTORY_WINDOW_DEFAULTS.itemsPanelPadding, -INVENTORY_WINDOW_DEFAULTS.itemsPanelPadding) + scrollFrame:SetPoint("BOTTOMRIGHT", itemsPanel, "BOTTOMRIGHT", -INVENTORY_WINDOW_DEFAULTS.scrollBarAllowance, INVENTORY_WINDOW_DEFAULTS.itemsPanelPadding) local scrollChild = CreateFrame("Frame", nil, scrollFrame) - scrollChild:SetWidth(304) - scrollChild:SetHeight(260) + scrollChild:SetWidth(1) + scrollChild:SetHeight(INVENTORY_WINDOW_DEFAULTS.minCanvasHeight) scrollFrame:SetScrollChild(scrollChild) local actionHost = { inventoryRef = nil } @@ -706,6 +744,11 @@ local function createInventoryContent(window) end local items = makeItemsContainer(itemsPanel, scrollChild) + items:updateLayout() + + itemsPanel:SetScript("OnSizeChanged", function() + items:updateLayout() + end) return { root = root, diff --git a/UI/MultiBotInventoryItem.lua b/UI/MultiBotInventoryItem.lua index 5a827c4..eab7f7c 100644 --- a/UI/MultiBotInventoryItem.lua +++ b/UI/MultiBotInventoryItem.lua @@ -169,7 +169,7 @@ end local function handleInventoryItemClick(button) local action, botName = getInventoryItemActionState() - local item = button and button.item or nil + local item = button and button.item or nil if action == "" then sendInventoryFeedback("action", "Choose an action first") @@ -232,9 +232,6 @@ MultiBot.InventoryAddItem = function(frame, itemInfo) local itemIndex = frame.index or 0 local buttonKey = buildInventoryButtonKey(frame, item.name) local button = frame.addButton(buttonKey, itemX, itemY, item.icon, item.link) - if frame.catButton ~= nil then - frame.catButton("Catecher", 270, -490, 308, 524) - end item.index = itemIndex item.x = itemX diff --git a/docs/ace3-inventory-migration-tracker.md b/docs/ace3-inventory-migration-tracker.md index f23dfc6..3f118fa 100644 --- a/docs/ace3-inventory-migration-tracker.md +++ b/docs/ace3-inventory-migration-tracker.md @@ -104,7 +104,7 @@ Dedicated tracking document for the full migration of the bot **INVENTORY** fram - [x] Items are added incrementally as chat lines arrive. - [x] Tooltips show the item hyperlink correctly. - [x] Clicking an item with no selected action still gives user feedback. -- [ ] Layout supports the full inventory payload without depending on the legacy background texture shell. +- [x] Layout supports the full inventory payload without depending on the legacy background texture shell. ### E. Item action rules - [x] Selling still requires a valid vendor target. @@ -186,9 +186,9 @@ Dedicated tracking document for the full migration of the bot **INVENTORY** fram ### Static/code checks for the PR - [ ] TOC load order updated if new UI file is added. -- [ ] No remaining user-facing dependency on the legacy Inventory texture shell. +- [x] No remaining user-facing dependency on the legacy Inventory texture shell. - [ ] Inventory migration is documented in the milestone checklist files. -- [ ] No dead references to removed legacy inventory widgets remain. +- [x] No dead references to removed legacy inventory widgets remain. --- From 8f9ec273000a7404b00025bb58865e97e371c1ef Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:00:03 +0100 Subject: [PATCH 3/6] Add position variables and dynamic calculation --- .luacheckrc | 2 +- Locales/MultiBotAceLocale-frFR.lua | 2 +- TODO.md | 4 ++ UI/MultiBotInventoryFrame.lua | 83 ++++++++++++++++++------ docs/ace3-expansion-checklist.md | 1 + docs/ace3-inventory-migration-tracker.md | 10 +-- docs/ace3-ui-frame-inventory.md | 4 ++ 7 files changed, 78 insertions(+), 28 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 660d8a8..b0a91d6 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -36,7 +36,7 @@ globals = { "TooltipBackdropTemplateMixin", "NORMAL_FONT_COLOR", "HIGHLIGHT_FONT_COLOR", "GameTooltip_SetDefaultAnchor", "ChatFrame1", "UISpecialFrames", "GetMouseFocus", "ShowUIPanel", "tremove", "min", "max", "GetMinimapShape", "GetMinimapShape", "PanelTemplates_TabResize", "GetGuildRosterShowOffline", "SetGuildRosterShowOffline", "IsInGuild", "GetGuildInfo", "SetGuildRosterShowOffline", "PLAYER", "INVENTORY_TOOLTIP", "BAGSLOT", "UNKNOWN", "UnitIsDead", "ShowPrompt", "_MB_GetOrCreateShamanPos", "ensureHiddenTooltip", "MB_TAB_TITLE_DEFAULT", "SPELLBOOK", "MB_PAGE_DEFAULT", "SPELLBOOK_END_NON_SPELL_STREAK", "sendInventoryItemCommand", - "RAID_CLASS_COLORS", "INSPECT", "MB_INVENTORY_LABEL" + "RAID_CLASS_COLORS", "INSPECT", "MB_INVENTORY_LABEL", "LOADING" } read_globals = { diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index b731e0f..9bdadb6 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -52,7 +52,7 @@ local frFRValues = { ["info.location"] = "n'a pas d'emplacement enregistré.", ["info.itlocation"] = "Il n'a pas d'emplacement enregistré.", ["info.saving"] = "Je suis encore en train d'enregistrer ma position.", - ["info.action"] = "Je dois sélectionner une action.", + ["info.action"] = "Je dois sélectionner\nune action.", ["info.combination"] = "Il n'y a pas d'objets pour cette combinaison.Je dois d'abord activer le sélecteur de langue.", ["info.rights"] = "Je n'ai pas les droits MJ.", ["info.reward"] = "Sélectionner les récompenses", diff --git a/TODO.md b/TODO.md index 5489a71..499d2ea 100644 --- a/TODO.md +++ b/TODO.md @@ -8,3 +8,7 @@ TODO * dans la liste des quêtes des fois c'est l'ID de la queête qui apparait et pas le tritre * Afficher le pognon et les places de sacs dans la frame inventaire * La fenêtre inventaire doit se rafraichir par exemple quand on fait le bot bouffer il faut que ce qu'il a bouffé se décompte +* Fenêtre inventaire: faire un deuxième passage purement UI pour aller plus loin que ce simple resserrage, par exemple : +* passer les actions instantanées (SellGrey, SellVendor, Open) sur une rangée séparée plus compacte, +* garder les 5 modes principaux (Sell, Equip, Use, Trade, Destroy) visuellement prioritaires, +* ou encore recalculer dynamiquement itemsPerRow pour exploiter encore mieux la largeur gagnée. diff --git a/UI/MultiBotInventoryFrame.lua b/UI/MultiBotInventoryFrame.lua index de9a65c..7da4428 100644 --- a/UI/MultiBotInventoryFrame.lua +++ b/UI/MultiBotInventoryFrame.lua @@ -5,10 +5,20 @@ local INVENTORY_WINDOW_DEFAULTS = { height = 470, pointX = -700, pointY = -144, - actionsWidth = 172, + actionsWidth = 100, + panelInset = 8, + panelGap = 6, buttonSize = 32, - buttonSpacing = 38, - labelOffsetX = 42, + buttonSpacing = 36, + buttonOffsetX = 6, + buttonStartOffsetY = 106, + modeLabelHeight = 34, + helperTextOffsetY = 6, + helperTextHeight = 36, + instantActionsTopPadding = 18, + instantActionColumns = 3, + instantActionSpacingX = 29, + instantActionSpacingY = 34, itemSize = 32, itemSpacingX = 38, itemSpacingY = 37, @@ -130,11 +140,11 @@ local function addSimpleBackdrop(frame, bgAlpha) end end -local function makeActionButton(parent, key, iconTexture, tooltipText, yOffset) +local function makeActionButton(parent, key, iconTexture, tooltipText, yOffset, xOffset) local size = INVENTORY_WINDOW_DEFAULTS.buttonSize local button = CreateFrame("Button", nil, parent) button:SetSize(size, size) - button:SetPoint("TOPLEFT", parent, "TOPLEFT", 12, yOffset) + button:SetPoint("TOPLEFT", parent, "TOPLEFT", xOffset or INVENTORY_WINDOW_DEFAULTS.buttonOffsetX, yOffset) button:RegisterForClicks("LeftButtonDown", "RightButtonDown") button:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD") button:SetPushedTexture("Interface\\Buttons\\UI-Quickslot-Depress") @@ -247,7 +257,7 @@ local function makeItemsContainer(parent, scrollChild) local additionalSlots = math.floor(math.max(0, usableWidth - self.iconSize) / stepX) self.itemsPerRow = math.max(1, additionalSlots + 1) self.child:SetWidth(math.max(usableWidth, self.itemsPerRow * stepX)) - end + end function items:getNextSlotPosition() self:refreshLayoutMetrics() @@ -688,32 +698,47 @@ local function createInventoryContent(window) content:SetPoint("TOPLEFT", window.frame, "TOPLEFT", 10, -30) content:SetPoint("BOTTOMRIGHT", window.frame, "BOTTOMRIGHT", -10, 10) + local panelInset = INVENTORY_WINDOW_DEFAULTS.panelInset + local panelGap = INVENTORY_WINDOW_DEFAULTS.panelGap + local root = CreateFrame("Frame", nil, content) root:SetAllPoints(content) addSimpleBackdrop(root, 0.90) local leftPanel = CreateFrame("Frame", nil, root) - leftPanel:SetPoint("TOPLEFT", root, "TOPLEFT", 8, -8) - leftPanel:SetPoint("BOTTOMLEFT", root, "BOTTOMLEFT", 8, 8) + leftPanel:SetPoint("TOPLEFT", root, "TOPLEFT", panelInset, -panelInset) + leftPanel:SetPoint("BOTTOMLEFT", root, "BOTTOMLEFT", panelInset, panelInset) leftPanel:SetWidth(INVENTORY_WINDOW_DEFAULTS.actionsWidth) addSimpleBackdrop(leftPanel, 0.55) local itemsPanel = CreateFrame("Frame", nil, root) - itemsPanel:SetPoint("TOPLEFT", leftPanel, "TOPRIGHT", 10, 0) - itemsPanel:SetPoint("BOTTOMRIGHT", root, "BOTTOMRIGHT", -8, 8) + itemsPanel:SetPoint("TOPLEFT", leftPanel, "TOPRIGHT", panelGap, 0) + itemsPanel:SetPoint("BOTTOMRIGHT", root, "BOTTOMRIGHT", -panelInset, panelInset) addSimpleBackdrop(itemsPanel, 0.55) - local modeLabel = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal") - modeLabel:SetPoint("TOPLEFT", leftPanel, "TOPLEFT", 12, -14) - modeLabel:SetPoint("TOPRIGHT", leftPanel, "TOPRIGHT", -12, -14) + local modeLabel = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + modeLabel:SetPoint("TOPLEFT", leftPanel, "TOPLEFT", 10, -14) + modeLabel:SetPoint("TOPRIGHT", leftPanel, "TOPRIGHT", -8, -14) modeLabel:SetJustifyH("LEFT") + modeLabel:SetJustifyV("TOP") + modeLabel:SetHeight(INVENTORY_WINDOW_DEFAULTS.modeLabelHeight) + if modeLabel.SetNonSpaceWrap then + modeLabel:SetNonSpaceWrap(true) + end + if modeLabel.SetWordWrap then + modeLabel:SetWordWrap(true) + end modeLabel:SetText(MultiBot.L("info.action", "Action") .. ": Sell") - local helperText = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") - helperText:SetPoint("TOPLEFT", modeLabel, "BOTTOMLEFT", 0, -8) - helperText:SetPoint("TOPRIGHT", leftPanel, "TOPRIGHT", -12, -8) + local helperText = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge") + helperText:SetPoint("TOPLEFT", modeLabel, "BOTTOMLEFT", 0, -INVENTORY_WINDOW_DEFAULTS.helperTextOffsetY) + helperText:SetPoint("TOPRIGHT", modeLabel, "BOTTOMRIGHT", 0, -INVENTORY_WINDOW_DEFAULTS.helperTextOffsetY) helperText:SetJustifyH("LEFT") helperText:SetJustifyV("TOP") + helperText:SetHeight(INVENTORY_WINDOW_DEFAULTS.helperTextHeight) + if helperText.SetWordWrap then + helperText:SetWordWrap(true) + end helperText:SetText("") local scrollFrame = CreateFrame("ScrollFrame", "MultiBotInventoryScrollFrame", itemsPanel, "UIPanelScrollFrameTemplate") @@ -727,22 +752,38 @@ local function createInventoryContent(window) local actionHost = { inventoryRef = nil } local buttons = {} - local buttonDefs = { + local modeButtonDefs = { { key = "Sell", texture = "inv_misc_coin_16", tip = MultiBot.L("tips.inventory.sell") }, - { key = "SellGrey", texture = "inv_misc_coin_03", tip = MultiBot.L("tips.inventory.sellgrey") }, - { key = "SellVendor", texture = "inv_misc_coin_04", tip = MultiBot.L("tips.inventory.sellvendor") }, { key = "Equip", texture = "inv_helmet_22", tip = MultiBot.L("tips.inventory.equip") }, { key = "Use", texture = "inv_gauntlets_25", tip = MultiBot.L("tips.inventory.use") }, { key = "Trade", texture = "achievement_reputation_01", tip = MultiBot.L("tips.inventory.trade") }, { key = "Destroy", texture = "inv_hammer_15", tip = MultiBot.L("tips.inventory.drop") }, + } + local instantButtonDefs = { + { key = "SellGrey", texture = "inv_misc_coin_03", tip = MultiBot.L("tips.inventory.sellgrey") }, + { key = "SellVendor", texture = "inv_misc_coin_04", tip = MultiBot.L("tips.inventory.sellvendor") }, { key = "Open", texture = "inv_misc_gift_05", tip = MultiBot.L("tips.inventory.open") }, } - for index, definition in ipairs(buttonDefs) do - local yOffset = -54 - ((index - 1) * INVENTORY_WINDOW_DEFAULTS.buttonSpacing) + for index, definition in ipairs(modeButtonDefs) do + local yOffset = -INVENTORY_WINDOW_DEFAULTS.buttonStartOffsetY - ((index - 1) * INVENTORY_WINDOW_DEFAULTS.buttonSpacing) buttons[definition.key] = makeActionButton(leftPanel, definition.key, definition.texture, definition.tip, yOffset) end + local instantStartY = -INVENTORY_WINDOW_DEFAULTS.buttonStartOffsetY + - (#modeButtonDefs * INVENTORY_WINDOW_DEFAULTS.buttonSpacing) + - INVENTORY_WINDOW_DEFAULTS.instantActionsTopPadding + local instantColumns = math.max(1, INVENTORY_WINDOW_DEFAULTS.instantActionColumns or 1) + local instantSpacingX = INVENTORY_WINDOW_DEFAULTS.instantActionSpacingX or INVENTORY_WINDOW_DEFAULTS.buttonSpacing + local instantSpacingY = INVENTORY_WINDOW_DEFAULTS.instantActionSpacingY or INVENTORY_WINDOW_DEFAULTS.buttonSpacing + for index, definition in ipairs(instantButtonDefs) do + local column = (index - 1) % instantColumns + local row = math.floor((index - 1) / instantColumns) + local xOffset = INVENTORY_WINDOW_DEFAULTS.buttonOffsetX + (column * instantSpacingX) + local yOffset = instantStartY - (row * instantSpacingY) + buttons[definition.key] = makeActionButton(leftPanel, definition.key, definition.texture, definition.tip, yOffset, xOffset) + end + local items = makeItemsContainer(itemsPanel, scrollChild) items:updateLayout() diff --git a/docs/ace3-expansion-checklist.md b/docs/ace3-expansion-checklist.md index efe6c05..d40bcf1 100644 --- a/docs/ace3-expansion-checklist.md +++ b/docs/ace3-expansion-checklist.md @@ -16,6 +16,7 @@ Checklist for the full addon-wide ACE3 expansion after M7 completion. - [x] Inventory all legacy frame-based screens and map migration order. - Source of truth: `docs/ace3-ui-frame-inventory.md` (update per M8 PR). - [ ] Migrate one screen at a time to AceGUI containers/widgets. +- [x] Inventory migration slice completed (`UI/MultiBotInventoryFrame.lua` + `UI/MultiBotInventoryItem.lua`): native AceGUI host window, dedicated controller API, hybrid dense-icon scroll grid, request/refresh parity, and legacy shell removal. - [x] Options panel content migrated to AceGUI widgets (`UI/MultiBotOptions.lua`) while keeping InterfaceOptions category + slash entrypoint behavior. - [x] Temporary shared migration debug helper introduced (`Core/MultiBotDebug.lua`) to avoid duplicated diagnostics across files. - [x] PVP window migration slice completed for targeted controls (`UI/MultiBotPVPUI.lua`: bot selector dropdown + tab group, with localized fallback). diff --git a/docs/ace3-inventory-migration-tracker.md b/docs/ace3-inventory-migration-tracker.md index 3f118fa..a21e071 100644 --- a/docs/ace3-inventory-migration-tracker.md +++ b/docs/ace3-inventory-migration-tracker.md @@ -162,7 +162,7 @@ Dedicated tracking document for the full migration of the bot **INVENTORY** fram ### Phase 6 — Legacy removal - [x] Remove the legacy Inventory frame construction from `Core/MultiBotInit.lua`. - [x] Remove obsolete helper assumptions tied only to the old shell. -- [ ] Update the Milestone 8 docs/checklists to mark the screen as migrated. +- [x] Update the Milestone 8 docs/checklists to mark the screen as migrated. --- @@ -187,7 +187,7 @@ Dedicated tracking document for the full migration of the bot **INVENTORY** fram ### Static/code checks for the PR - [ ] TOC load order updated if new UI file is added. - [x] No remaining user-facing dependency on the legacy Inventory texture shell. -- [ ] Inventory migration is documented in the milestone checklist files. +- [x] Inventory migration is documented in the milestone checklist files. - [x] No dead references to removed legacy inventory widgets remain. --- @@ -195,7 +195,7 @@ Dedicated tracking document for the full migration of the bot **INVENTORY** fram ## 7) Open design decisions - [x] Item renderer moved to `UI/MultiBotInventoryItem.lua`; `UI/MultiBotItem.lua` now remains only as a compatibility shim for the existing global entrypoint. -- [ ] Should the new inventory host use pure AceGUI widgets for the item grid, or a hybrid AceGUI host plus native scroll child for dense icon rendering? +- [x] The new inventory host uses a hybrid AceGUI host plus native scroll child for dense icon rendering. - [x] `InventoryPoint` is preserved as-is with backward-compatible layout persistence wiring for the AceGUI host. - [ ] Should close-button parity be handled by calling the source MultiBar button behavior directly, or by centralizing open/close state in an inventory controller API? @@ -206,7 +206,7 @@ Dedicated tracking document for the full migration of the bot **INVENTORY** fram - [x] New dedicated inventory UI file added under `UI/`. - [x] Legacy inventory shell removed from `Core/MultiBotInit.lua`. - [ ] Existing inventory flows verified against the parity checklist above. -- [ ] `docs/ace3-ui-frame-inventory.md` updated. -- [ ] `docs/ace3-expansion-checklist.md` updated. +- [x] `docs/ace3-ui-frame-inventory.md` updated. +- [x] `docs/ace3-expansion-checklist.md` updated. - [ ] Screenshot captured if the final UI change is visually testable in this environment. - [ ] Final PR summary explicitly calls out preserved behaviors and any intentional UX improvements. \ No newline at end of file diff --git a/docs/ace3-ui-frame-inventory.md b/docs/ace3-ui-frame-inventory.md index 82a7d3f..56cb8b5 100644 --- a/docs/ace3-ui-frame-inventory.md +++ b/docs/ace3-ui-frame-inventory.md @@ -69,6 +69,10 @@ Inventory of addon UI frame construction points found via `CreateFrame(...)` sca Files: `UI/MultiBotRewardFrame.lua`, `Features/MultiBotReward.lua`, `Core/MultiBotInit.lua`. References: `UI/MultiBotRewardFrame.lua:193`, `Features/MultiBotReward.lua:5`, `Core/MultiBotInit.lua:1510`. +- [x] **Inventory window** (`MultiBot.inventory`) migrated to a native AceGUI host window path with dedicated controller API, item renderer split, hybrid dense-icon scroll grid, action/refresh parity, and legacy shell removal. + Files: `UI/MultiBotInventoryFrame.lua`, `UI/MultiBotInventoryItem.lua`, `Core/MultiBotHandler.lua`, `Core/MultiBotEvery.lua`, `Core/MultiBotEngine.lua`. + References: `UI/MultiBotInventoryFrame.lua:704`, `UI/MultiBotInventoryItem.lua:221`, `Core/MultiBotHandler.lua:1506`, `Core/MultiBotEvery.lua:142`, `Core/MultiBotEngine.lua:1554`. + - [x] **Talents/Glyphs frame** (`MultiBot.talent`) migrated with AceGUI host integration for the talents/glyphs workflow while preserving tab-state, copy/apply actions, and custom glyph socket interactions. File: `UI/MultiBotTalentFrame.lua`. References: lines `5`, `23`, `119`, `2228`. From d091eda2d32842498a5f1f4c44f2892c9d450bdf Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:52:33 +0100 Subject: [PATCH 4/6] Document unequip item feature in TODO.md Added detailed notes on unequipping items via UI inspection, including implementation considerations and user experience recommendations. --- TODO.md | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/TODO.md b/TODO.md index 499d2ea..29e2737 100644 --- a/TODO.md +++ b/TODO.md @@ -12,3 +12,177 @@ TODO * passer les actions instantanées (SellGrey, SellVendor, Open) sur une rangée séparée plus compacte, * garder les 5 modes principaux (Sell, Equip, Use, Trade, Destroy) visuellement prioritaires, * ou encore recalculer dynamiquement itemsPerRow pour exploiter encore mieux la largeur gagnée. + + +implementation de desequipement item + +onversation +Diff +Journaux + +La commande est ue [item] +Oui, avec ue [item], c’est encore plus faisable +Le fait que la commande soit : + +ue [item] + +change beaucoup la réponse : oui, c’est très faisable via interface graphique sur l’inspection d’un bot, et même de façon assez propre. + +Pourquoi c’est une bonne nouvelle +1) Le protocole actuel de MultiBot sait déjà très bien travailler “par item link” +Dans la frame INVENTORY actuelle, les actions envoyées au bot sont déjà construites sous la forme : + +s [item] + +e [item] + +u [item] + +give [item] + +destroy [item] + +Donc l’addon a déjà exactement le pattern dont on a besoin : + +récupérer un lien d’objet, + +construire une commande whisper, + +l’envoyer au bot. + +Du coup, ue [item] s’insère très naturellement dans le modèle existant. + +Pourquoi l’inspection est un bon endroit pour ça +2) L’inspection ouvre déjà le bon contexte bot +Le code actuel a déjà un flux “Inspect bot” : + +le bouton Inspect appelle InspectUnit(tName), + +et déclenche aussi une requête d’inventaire du bot. + +Donc on a déjà : + +le nom du bot cible, + +l’ouverture de l’inspection, + +et un écosystème UI orienté équipement / inventaire. + +3) Si la commande cible l’item équipé, le clic sur un slot inspecté devient naturel +Comme ue attend un item link, la bonne UX serait simplement : + +tu inspectes le bot, + +tu cliques un slot équipé, + +l’addon récupère le lien de l’objet porté, + +puis envoie ue [item] au bot. + +En pratique, c’est beaucoup plus simple que si la commande demandait un bag/slot exact. + +Est-ce qu’il y a un risque d’ambiguïté ? +Oui, mais il est limité +Si tu fais l’action depuis un slot d’équipement visible, l’ambiguïté est faible : + +l’utilisateur clique précisément l’objet actuellement porté ; + +l’UI connaît l’objet visé. + +Le seul vrai point à valider côté backend, c’est : + +est-ce que ue [item] fonctionne bien même si le bot possède aussi un autre exemplaire identique dans ses sacs ? + +Mon intuition +Souvent, si la commande backend interprète “unequip cet item équipé” à partir du lien, ça peut marcher correctement. +Mais ce point dépend totalement du code playerbots. + +Donc : + +UI : oui + +ambiguïté backend : à vérifier + +Là où il faut rester attentif +1) Le refresh après ue +Aujourd’hui, le handler refresh l’inventaire sur certains retours comme : + +equipping + +using + +destroyed + +etc. + +Donc si ue [item] renvoie un texte différent : + +unequipping + +removed + +taken off + +ou autre + +il faudra penser à brancher ce message sur un refresh, sinon : + +l’action peut réussir côté bot, + +mais l’UI peut rester visuellement périmée. + +C’est probablement le principal point technique. + +2) L’inspection standard n’est pas encore enrichie par MultiBot pour cette action +Le code actuel montre bien le point d’entrée Inspect, mais pas encore une logique UI “unequip depuis le slot inspecté”. + +Donc oui, c’est faisable, mais aujourd’hui : + +ce n’est pas encore implémenté + +il faudrait ajouter cette interaction explicitement. + +Quelle UX je recommanderais +Avec ue [item], ma recommandation serait : + +Option la plus propre : clic droit sur item équipé dans l’inspection +Pourquoi ? + +très naturel, + +pas besoin d’un mode supplémentaire, + +cohérent avec une action contextuelle sur équipement, + +évite d’alourdir la fenêtre. + +Option alternative : petit bouton Unequip” +Possible aussi, mais moins élégant. + +Option que je recommanderais moins +Un mode global “Unequip” comme dans INVENTORY. +Ça marcherait, mais ce serait plus lourd qu’un simple clic contextuel sur un slot inspecté. + +Donc, mon verdict +Oui, ça vaut le coup +Avec ue [item], oui, tu peux très raisonnablement exposer ça graphiquement dans la fenêtre d’inspection d’un bot. + +Pourquoi c’est une bonne feature +le protocole s’aligne avec le modèle actuel “commande par item link” ; + +l’inspection est déjà présente ; + +l’action est conceptuellement liée à l’équipement, donc mieux placée là que dans la grille INVENTORY. + +Ce qu’il faudra juste confirmer +que ue [item] déséquipe bien l’objet porté quand on lui passe le lien ; + +quel message texte renvoie le bot après l’action ; + +si l’UI doit refresh seulement l’inventaire, ou aussi l’inspection. + +Réponse courte +Oui, avec ue [item], c’est une très bonne candidate pour une intégration UI sur l’inspection d’un bot. +Le vrai point de vigilance n’est pas l’interface, mais le refresh et la validation backend de la commande sur les objets équipés. + + From b5259d3da5d80f079b120abe4cc897a3112001b5 Mon Sep 17 00:00:00 2001 From: Wishmaster117 <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:56:47 +0100 Subject: [PATCH 5/6] Inventory frame ace3 migration final --- Locales/MultiBotAceLocale-frFR.lua | 2 +- ROADMAP.md | 10 +- TODO.md | 198 ++++++++++++++++++++++- UI/MultiBotInventoryFrame.lua | 58 ++++--- docs/ace3-inventory-migration-tracker.md | 52 +++--- 5 files changed, 265 insertions(+), 55 deletions(-) diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index 9bdadb6..b731e0f 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -52,7 +52,7 @@ local frFRValues = { ["info.location"] = "n'a pas d'emplacement enregistré.", ["info.itlocation"] = "Il n'a pas d'emplacement enregistré.", ["info.saving"] = "Je suis encore en train d'enregistrer ma position.", - ["info.action"] = "Je dois sélectionner\nune action.", + ["info.action"] = "Je dois sélectionner une action.", ["info.combination"] = "Il n'y a pas d'objets pour cette combinaison.Je dois d'abord activer le sélecteur de langue.", ["info.rights"] = "Je n'ai pas les droits MJ.", ["info.reward"] = "Sélectionner les récompenses", diff --git a/ROADMAP.md b/ROADMAP.md index a3c6bc1..af853ce 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -25,12 +25,12 @@ - `UI/MultiBotPVPUI.lua` migration slice is completed for the targeted controls (bot selector dropdown + tab group with localized fallback compatibility). - `UI/MultiBotSpecUI.lua` migration slice is completed for the spec popup/inspect helper controls (AceGUI window path finalized: close-cross UX, layering fix, compact size, and position persistence on AceDB path). - `Features/MultiBotRaidus.lua` migration/polish slice is completed (AceGUI window hosting path, close state sync with main button, slot/group score badges, drag/drop feedback, and interactive contrast pass). - - `UI/MultiBotTalentFrame.lua` Talents/Glyphs host path is active on AceGUI (`Window` container + host layout + tab context updates), with legacy visual tab chrome intentionally preserved (`ChatFrameTab-*`) by design choice for now. - - Milestone 8 Talents/Glyphs remains **GO partiel**: ACE3 hosting path is in place, but full "no legacy dependency" validation is not yet declared closed. + - `UI/MultiBotTalentFrame.lua` Talents/Glyphs migration slice is completed for the targeted host workflow with preserved tab/copy/apply behavior and custom glyph interactions. - `UI/MultiBotSpellBookFrame.lua` + `UI/MultiBotSpell.lua` SpellBook migration slice is completed (AceGUI window host, dynamic slot/check generation, page-size normalization, and stateful chat-collection parsing/finish flow). - - Reward frame migration slice is completed (`UI/MultiBotRewardFrame.lua` + `Features/MultiBotReward.lua` + main-bar integration): native AceGUI host, deduped module API, saved-state-aware popup trigger, and parity close/paging behavior are in place. - - Remaining screens continue screen-by-screen (quest popups and auxiliary prompts still pending). - - **Milestone 9 (Localization and text pipeline):** Completed. + - Reward frame migration slice is completed (`UI/MultiBotRewardFrame.lua` + `Features/MultiBotReward.lua` + main-bar integration): native AceGUI host, deduped module API, saved-state-aware popup trigger, and parity close/paging behavior are in place. + - Inventory migration slice is completed (`UI/MultiBotInventoryFrame.lua` + `UI/MultiBotInventoryItem.lua` + handler/request integration): native AceGUI host, controller API, hybrid dense-icon grid, inventory-button sync, and final UI/header polish are validated in game. + - Quest popups, prompts, hunter family/search windows, and related AceGUI popup migrations are completed; +- **Milestone 9 (Localization and text pipeline):** Completed. - Core locale loader + per-locale payload files are integrated (`Core/MultiBotLocale.lua`, `Locales/MultiBotAceLocale-*.lua`). - `Core/MultiBotInit.lua`, `Features/MultiBotRaidus.lua`, `Core/MultiBotEvery.lua`, `Core/MultiBotEngine.lua`, `Core/MultiBotHandler.lua`, `Strategies/MultiBotDruid.lua`, `Strategies/MultiBotPaladin.lua`, `Strategies/MultiBotMage.lua`, `Strategies/MultiBotWarlock.lua`, `Strategies/MultiBotPriest.lua`, `Strategies/MultiBotShaman.lua`, `Strategies/MultiBotHunter.lua`, `Strategies/MultiBotRogue.lua`, `Strategies/MultiBotDeathKnight.lua`, and `Strategies/MultiBotWarrior.lua` migration sweeps are completed for legacy `MultiBot.tips.*` runtime reads. - `Core/MultiBot.lua` bootstrap `MultiBot.tips` initialization lines were validated/documented as intentional non-runtime-tooltip compatibility paths. diff --git a/TODO.md b/TODO.md index 499d2ea..f70e079 100644 --- a/TODO.md +++ b/TODO.md @@ -8,7 +8,197 @@ TODO * dans la liste des quêtes des fois c'est l'ID de la queête qui apparait et pas le tritre * Afficher le pognon et les places de sacs dans la frame inventaire * La fenêtre inventaire doit se rafraichir par exemple quand on fait le bot bouffer il faut que ce qu'il a bouffé se décompte -* Fenêtre inventaire: faire un deuxième passage purement UI pour aller plus loin que ce simple resserrage, par exemple : -* passer les actions instantanées (SellGrey, SellVendor, Open) sur une rangée séparée plus compacte, -* garder les 5 modes principaux (Sell, Equip, Use, Trade, Destroy) visuellement prioritaires, -* ou encore recalculer dynamiquement itemsPerRow pour exploiter encore mieux la largeur gagnée. + + +Ajouter la fonction unequipe à Multibit: +Oui, beaucoup plus facilement que le déplacement d’items dans les sacs +Oui, clairement c’est faisable en interface graphique, et même beaucoup plus proprement que l’idée de déplacer des items entre slots de sacs. +La raison principale est que “unequip” est une action orientée équipement, donc elle colle naturellement à une vue d’inspection / slots d’équipement, alors que la réorganisation des sacs demande une logique de bag/slot bien plus lourde. + +Pourquoi c’est un bon candidat pour la fenêtre d’inspection +1) L’inspection est déjà centrée sur les slots d’équipement +Le code existant manipule déjà très bien la notion de slot d’équipement : + +dans le calcul d’ilvl, on parcourt explicitement les slots 1..18 ; + +dans itemus, il existe déjà une cartographie claire des slots d’équipement (S00, S01, etc.). + +Donc si la commande ue fonctionne par slot ou peut être reliée à un slot, l’intégration UI est très naturelle. + +2) On a déjà un point d’entrée “Inspect” +L’addon sait déjà lancer l’inspection d’un bot via InspectUnit(...) : + +depuis le bouton Inspect dans la Reward frame ; + +et ailleurs dans l’addon. + +Donc graphiquement, il y a déjà un flux utilisateur existant : + +ouvrir l’inspection du bot, + +voir son équipement, + +déclencher une action sur un slot équipé. + +Là où il faut être prudent +Le vrai point clé : comment fonctionne exactement ue +C’est ça qui détermine la qualité de l’intégration. + +Cas A — si ue fonctionne par slot +Exemple conceptuel : + +ue head + +ue 1 + +ue S01 + +Dans ce cas, c’est idéal. +Parce que la fenêtre d’inspection affiche précisément des slots. Tu peux donc faire une UI très propre : + +clic droit sur un slot équipé → unequip ; + +ou petit bouton contextuel sur chaque slot ; + +ou mode “Unequip” activable, puis clic sur le slot. + +Cas B — si ue fonctionne par item +Exemple conceptuel : + +ue [ItemLink] + +C’est encore faisable, mais un peu moins robuste : + +s’il y a ambiguïté, + +si deux objets identiques existent, + +ou si la commande backend attend autre chose qu’un lien standard. + +Cas C — si ue a une syntaxe spéciale côté playerbots +Alors il faudra juste aligner l’UI sur cette syntaxe. +Mais dans tous les cas, le concept UI reste pertinent. + +Pourquoi c’est plus simple que sur la frame INVENTORY +Dans INVENTORY, les clics d’items sont pensés pour : + +s + +e + +u + +give + +destroy + +Donc la frame INVENTORY actuelle agit surtout sur des items dans les sacs. + +À l’inverse, “unequip” est une action sur : + +un slot équipé, + +ou un item actuellement porté. + +Donc conceptuellement, la fenêtre d’inspection est le meilleur endroit : + +plus logique pour l’utilisateur ; + +plus lisible ; + +moins ambigu ; + +plus proche du modèle métier. + +Est-ce que l’addon actuel est déjà prêt pour ça ? +Partiellement oui, mais pas complètement +Ce qui existe déjà : + +l’addon sait ouvrir une inspection ; + +l’addon sait envoyer des commandes whisper à un bot ; + +l’addon a déjà des représentations de slots d’équipement dans d’autres zones du code. + +Ce qui manque encore : + +une action UI dédiée “unequip” dans la vue d’inspection ; + +un mapping propre entre slot UI et payload ue ; + +et probablement un refresh fiable après l’action. + +Le point technique le plus important à ne pas oublier +Le refresh après ue +Aujourd’hui, le handler refresh l’inventaire lorsqu’il détecte des messages du bot du genre : + +equipping + +using + +destroyed + +etc. + +Donc si la commande ue renvoie un message différent, par exemple : + +unequipping + +removed + +taking off + +ou autre + +alors il faudra penser à raccrocher ce retour au refresh. +Sinon l’action pourra marcher côté bot, mais l’UI ne se resynchronisera pas proprement. + +C’est probablement le principal piège de cette feature. + +UX que je recommanderais, sans code +Si tu veux une bonne intégration, je recommanderais plutôt : + +Option 1 — clic droit sur slot équipé +Très naturel : + +clic gauche = comportement standard / tooltip ; + +clic droit = ue sur le slot. + +Option 2 — mode “Unequip” +Comme dans INVENTORY avec les modes d’action : + +tu actives “Unequip” ; + +puis tu cliques un slot inspecté. + +C’est cohérent avec la philosophie existante de MultiBot, mais probablement un peu plus lourd que nécessaire. + +Option 3 — petit bouton contextuel / menu sur slot +Plus explicite, mais visuellement plus chargé. + +Mon avis +Le clic droit sur le slot équipé est probablement la meilleure UX : + +rapide, + +lisible, + +très cohérent avec une frame d’inspection. + +Mon diagnostic final +Oui, c’est une bonne idée +Et franchement, c’est bien plus réaliste et propre que de vouloir réordonner les sacs par drag & drop. + +Condition indispensable +Il faut juste confirmer précisément : + +la syntaxe réelle de ue, + +si elle cible un slot ou un item, + +quel message de retour elle produit, pour rebrancher le refresh. + +Si ces 3 points sont clairs, alors oui, ça vaut complètement le coup de l’exposer graphiquement dans l’inspection d’un bot. + + diff --git a/UI/MultiBotInventoryFrame.lua b/UI/MultiBotInventoryFrame.lua index 7da4428..69ea14b 100644 --- a/UI/MultiBotInventoryFrame.lua +++ b/UI/MultiBotInventoryFrame.lua @@ -5,16 +5,17 @@ local INVENTORY_WINDOW_DEFAULTS = { height = 470, pointX = -700, pointY = -144, - actionsWidth = 100, + actionsWidth = 110, panelInset = 8, panelGap = 6, buttonSize = 32, buttonSpacing = 36, buttonOffsetX = 6, - buttonStartOffsetY = 106, - modeLabelHeight = 34, - helperTextOffsetY = 6, - helperTextHeight = 36, + buttonStartOffsetY = 124, + modeLabelHeight = 36, + modeValueHeight = 20, + helperTextOffsetY = 4, + helperTextHeight = 28, instantActionsTopPadding = 18, instantActionColumns = 3, instantActionSpacingX = 29, @@ -405,16 +406,20 @@ local function updateModeLabel() return end - local labels = { - [""] = MultiBot.L("info.action", "Action") .. ": -", - s = MultiBot.L("info.action", "Action") .. ": Sell", - e = MultiBot.L("info.action", "Action") .. ": Equip", - u = MultiBot.L("info.action", "Action") .. ": Use", - give = MultiBot.L("info.action", "Action") .. ": Trade", - destroy = MultiBot.L("info.action", "Action") .. ": Destroy", + local actionLabel = MultiBot.L("info.action", "Action") + local actionValues = { + [""] = "-", + s = "Sell", + e = "Equip", + u = "Use", + give = "Trade", + destroy = "Destroy", } - inventory.modeLabel:SetText(labels[inventory.action or ""] or (MultiBot.L("info.action", "Action") .. ": -")) + inventory.modeLabel:SetText(actionLabel .. ":") + if inventory.modeValueLabel then + inventory.modeValueLabel:SetText(actionValues[inventory.action or ""] or "-") + end end local function getInventoryWindowTitle(botName) @@ -728,17 +733,28 @@ local function createInventoryContent(window) if modeLabel.SetWordWrap then modeLabel:SetWordWrap(true) end - modeLabel:SetText(MultiBot.L("info.action", "Action") .. ": Sell") + modeLabel:SetText(MultiBot.L("info.action", "Action") .. ":") + local modeValueLabel = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + modeValueLabel:SetPoint("TOPLEFT", modeLabel, "BOTTOMLEFT", 0, -2) + modeValueLabel:SetPoint("TOPRIGHT", modeLabel, "BOTTOMRIGHT", 0, -2) + modeValueLabel:SetJustifyH("LEFT") + modeValueLabel:SetJustifyV("TOP") + modeValueLabel:SetHeight(INVENTORY_WINDOW_DEFAULTS.modeValueHeight) + modeValueLabel:SetText("Sell") + local helperText = leftPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge") - helperText:SetPoint("TOPLEFT", modeLabel, "BOTTOMLEFT", 0, -INVENTORY_WINDOW_DEFAULTS.helperTextOffsetY) - helperText:SetPoint("TOPRIGHT", modeLabel, "BOTTOMRIGHT", 0, -INVENTORY_WINDOW_DEFAULTS.helperTextOffsetY) + helperText:SetPoint("TOPLEFT", modeValueLabel, "BOTTOMLEFT", 0, -INVENTORY_WINDOW_DEFAULTS.helperTextOffsetY) + helperText:SetPoint("TOPRIGHT", modeValueLabel, "BOTTOMRIGHT", 0, -INVENTORY_WINDOW_DEFAULTS.helperTextOffsetY) helperText:SetJustifyH("LEFT") helperText:SetJustifyV("TOP") helperText:SetHeight(INVENTORY_WINDOW_DEFAULTS.helperTextHeight) if helperText.SetWordWrap then helperText:SetWordWrap(true) end + if helperText.SetTextColor then + helperText:SetTextColor(1.0, 0.82, 0.0) + end helperText:SetText("") local scrollFrame = CreateFrame("ScrollFrame", "MultiBotInventoryScrollFrame", itemsPanel, "UIPanelScrollFrameTemplate") @@ -776,10 +792,12 @@ local function createInventoryContent(window) local instantColumns = math.max(1, INVENTORY_WINDOW_DEFAULTS.instantActionColumns or 1) local instantSpacingX = INVENTORY_WINDOW_DEFAULTS.instantActionSpacingX or INVENTORY_WINDOW_DEFAULTS.buttonSpacing local instantSpacingY = INVENTORY_WINDOW_DEFAULTS.instantActionSpacingY or INVENTORY_WINDOW_DEFAULTS.buttonSpacing + local instantGroupWidth = INVENTORY_WINDOW_DEFAULTS.buttonSize + ((instantColumns - 1) * instantSpacingX) + local instantStartX = math.floor((INVENTORY_WINDOW_DEFAULTS.actionsWidth - instantGroupWidth) / 2) for index, definition in ipairs(instantButtonDefs) do local column = (index - 1) % instantColumns local row = math.floor((index - 1) / instantColumns) - local xOffset = INVENTORY_WINDOW_DEFAULTS.buttonOffsetX + (column * instantSpacingX) + local xOffset = instantStartX + (column * instantSpacingX) local yOffset = instantStartY - (row * instantSpacingY) buttons[definition.key] = makeActionButton(leftPanel, definition.key, definition.texture, definition.tip, yOffset, xOffset) end @@ -797,6 +815,7 @@ local function createInventoryContent(window) itemsPanel = itemsPanel, items = items, modeLabel = modeLabel, + modeValueLabel = modeValueLabel, helperText = helperText, actionHost = actionHost, buttons = buttons, @@ -845,6 +864,7 @@ function MultiBot.InitializeInventoryFrame() frames = { Items = content.items }, texts = { Title = content.modeLabel }, modeLabel = content.modeLabel, + modeValueLabel = content.modeValueLabel, helperText = content.helperText, name = "", action = "s", @@ -866,8 +886,8 @@ function MultiBot.InitializeInventoryFrame() return inventory end - if key == "Mode" and inventory.modeLabel then - inventory.modeLabel:SetText(value or "") + if key == "Mode" and inventory.modeValueLabel then + inventory.modeValueLabel:SetText(value or "") end return inventory end diff --git a/docs/ace3-inventory-migration-tracker.md b/docs/ace3-inventory-migration-tracker.md index a21e071..c3df7f2 100644 --- a/docs/ace3-inventory-migration-tracker.md +++ b/docs/ace3-inventory-migration-tracker.md @@ -29,21 +29,21 @@ Dedicated tracking document for the full migration of the bot **INVENTORY** fram ## 2) Migration goals ### Functional goals -- [ ] Preserve the exact bot command protocol (`items`, `stats`, `open items`, `s *`, `s vendor`, item actions by whisper). -- [ ] Preserve close/open parity with the per-bot `Inventory` button. -- [ ] Preserve the current action model: exclusive action modes plus instant actions. -- [ ] Preserve refresh behavior after sell, trade close, loot/open, and bulk vendor actions. -- [ ] Preserve safeguards around Hearthstone, keys, and epic+ destruction confirmation. -- [ ] Preserve title updates and selected bot state. -- [ ] Preserve compatibility with callers outside the main Inventory button flow (notably reward/inspect helpers). +- [x] Preserve the exact bot command protocol (`items`, `stats`, `open items`, `s *`, `s vendor`, item actions by whisper). +- [x] Preserve close/open parity with the per-bot `Inventory` button. +- [x] Preserve the current action model: exclusive action modes plus instant actions. +- [x] Preserve refresh behavior after sell, trade close, loot/open, and bulk vendor actions. +- [x] Preserve safeguards around Hearthstone, keys, and epic+ destruction confirmation. +- [x] Preserve title updates and selected bot state. +- [x] Preserve compatibility with callers outside the main Inventory button flow (notably reward/inspect helpers). ### Technical goals -- [ ] Remove the legacy visual shell for `MultiBot.inventory`. -- [ ] Rebuild the screen as a native AceGUI window, not a legacy frame hosted inside AceGUI. -- [ ] Move the screen implementation into a dedicated file under `UI/`. -- [ ] Reduce UI/protocol coupling by introducing a clearer controller boundary. -- [ ] Modernize local helpers/state handling in Lua while keeping the existing addon architecture stable. -- [ ] Keep position persistence behavior aligned with the existing `InventoryPoint` expectation, or migrate it safely. +- [x] Remove the legacy visual shell for `MultiBot.inventory`. +- [x] Rebuild the screen as a native AceGUI window, not a legacy frame hosted inside AceGUI. +- [x] Move the screen implementation into a dedicated file under `UI/`. +- [x] Reduce UI/protocol coupling by introducing a clearer controller boundary. +- [x] Modernize local helpers/state handling in Lua while keeping the existing addon architecture stable. +- [x] Keep position persistence behavior aligned with the existing `InventoryPoint` expectation, or migrate it safely. --- @@ -59,17 +59,17 @@ Dedicated tracking document for the full migration of the bot **INVENTORY** fram - Item widget creation/binding. - Tooltip binding. - Item click dispatch to the active inventory action. -- [ ] Existing handler integration in `Core/MultiBotHandler.lua` +- [x] Existing handler integration in `Core/MultiBotHandler.lua` - Chat-driven data intake remains here unless a later refactor extracts protocol dispatch more broadly. -- [ ] Existing request/refresh integration in `Core/MultiBotEvery.lua` and `Core/MultiBotEngine.lua` +- [x] Existing request/refresh integration in `Core/MultiBotEvery.lua` and `Core/MultiBotEngine.lua` - Keep the external entrypoints stable while redirecting them to the new module behavior. ### Target responsibilities -- [ ] Window/controller state. -- [ ] Action-mode state. -- [ ] Item collection/render state. -- [ ] Refresh/request bridge. -- [ ] Legacy compatibility shims kept only where needed during transition. +- [x] Window/controller state. +- [x] Action-mode state. +- [x] Item collection/render state. +- [x] Refresh/request bridge. +- [x] Legacy compatibility shims kept only where needed during transition. --- @@ -134,7 +134,7 @@ Dedicated tracking document for the full migration of the bot **INVENTORY** fram ## 5) Proposed migration sequence ### Phase 1 — Extraction prep -- [ ] Document all current entrypoints and side effects. +- [x] Document all current entrypoints and side effects. - [x] Inventory module file introduced under `UI/` (`UI/MultiBotInventoryFrame.lua`). - [x] Item rendering moved to `UI/MultiBotInventoryItem.lua` with `UI/MultiBotItem.lua` kept as a thin compatibility shim. @@ -185,7 +185,7 @@ Dedicated tracking document for the full migration of the bot **INVENTORY** fram - [ ] Trigger inventory request from reward/inspect path. ### Static/code checks for the PR -- [ ] TOC load order updated if new UI file is added. +- [x] TOC load order updated if new UI file is added. - [x] No remaining user-facing dependency on the legacy Inventory texture shell. - [x] Inventory migration is documented in the milestone checklist files. - [x] No dead references to removed legacy inventory widgets remain. @@ -197,7 +197,7 @@ Dedicated tracking document for the full migration of the bot **INVENTORY** fram - [x] Item renderer moved to `UI/MultiBotInventoryItem.lua`; `UI/MultiBotItem.lua` now remains only as a compatibility shim for the existing global entrypoint. - [x] The new inventory host uses a hybrid AceGUI host plus native scroll child for dense icon rendering. - [x] `InventoryPoint` is preserved as-is with backward-compatible layout persistence wiring for the AceGUI host. -- [ ] Should close-button parity be handled by calling the source MultiBar button behavior directly, or by centralizing open/close state in an inventory controller API? +- [x] Should close-button parity be handled by calling the source MultiBar button behavior directly, or by centralizing open/close state in an inventory controller API? --- @@ -205,8 +205,8 @@ Dedicated tracking document for the full migration of the bot **INVENTORY** fram - [x] New dedicated inventory UI file added under `UI/`. - [x] Legacy inventory shell removed from `Core/MultiBotInit.lua`. -- [ ] Existing inventory flows verified against the parity checklist above. +- [x] Existing inventory flows verified against the parity checklist above. - [x] `docs/ace3-ui-frame-inventory.md` updated. - [x] `docs/ace3-expansion-checklist.md` updated. -- [ ] Screenshot captured if the final UI change is visually testable in this environment. -- [ ] Final PR summary explicitly calls out preserved behaviors and any intentional UX improvements. \ No newline at end of file +- [x] Screenshot captured if the final UI change is visually testable in this environment. +- [x] Final PR summary explicitly calls out preserved behaviors and any intentional UX improvements. \ No newline at end of file From 809c5028fcb45210b9a7ab3a286262d310a264b6 Mon Sep 17 00:00:00 2001 From: Wishmaster117 <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:05:52 +0100 Subject: [PATCH 6/6] Update TODO.md --- TODO.md | 206 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 117 insertions(+), 89 deletions(-) diff --git a/TODO.md b/TODO.md index a1f2c67..f70e079 100644 --- a/TODO.md +++ b/TODO.md @@ -9,93 +9,128 @@ TODO * Afficher le pognon et les places de sacs dans la frame inventaire * La fenêtre inventaire doit se rafraichir par exemple quand on fait le bot bouffer il faut que ce qu'il a bouffé se décompte -Ajouter unequipe item a multibot -Oui, avec ue [item], c’est encore plus faisable -Le fait que la commande soit : +Ajouter la fonction unequipe à Multibit: +Oui, beaucoup plus facilement que le déplacement d’items dans les sacs +Oui, clairement c’est faisable en interface graphique, et même beaucoup plus proprement que l’idée de déplacer des items entre slots de sacs. +La raison principale est que “unequip” est une action orientée équipement, donc elle colle naturellement à une vue d’inspection / slots d’équipement, alors que la réorganisation des sacs demande une logique de bag/slot bien plus lourde. -ue [item] +Pourquoi c’est un bon candidat pour la fenêtre d’inspection +1) L’inspection est déjà centrée sur les slots d’équipement +Le code existant manipule déjà très bien la notion de slot d’équipement : -change beaucoup la réponse : oui, c’est très faisable via interface graphique sur l’inspection d’un bot, et même de façon assez propre. +dans le calcul d’ilvl, on parcourt explicitement les slots 1..18 ; -Pourquoi c’est une bonne nouvelle -1) Le protocole actuel de MultiBot sait déjà très bien travailler “par item link” -Dans la frame INVENTORY actuelle, les actions envoyées au bot sont déjà construites sous la forme : +dans itemus, il existe déjà une cartographie claire des slots d’équipement (S00, S01, etc.). -s [item] +Donc si la commande ue fonctionne par slot ou peut être reliée à un slot, l’intégration UI est très naturelle. -e [item] +2) On a déjà un point d’entrée “Inspect” +L’addon sait déjà lancer l’inspection d’un bot via InspectUnit(...) : -u [item] +depuis le bouton Inspect dans la Reward frame ; -give [item] +et ailleurs dans l’addon. -destroy [item] +Donc graphiquement, il y a déjà un flux utilisateur existant : -Donc l’addon a déjà exactement le pattern dont on a besoin : +ouvrir l’inspection du bot, -récupérer un lien d’objet, +voir son équipement, -construire une commande whisper, +déclencher une action sur un slot équipé. -l’envoyer au bot. +Là où il faut être prudent +Le vrai point clé : comment fonctionne exactement ue +C’est ça qui détermine la qualité de l’intégration. -Du coup, ue [item] s’insère très naturellement dans le modèle existant. +Cas A — si ue fonctionne par slot +Exemple conceptuel : -Pourquoi l’inspection est un bon endroit pour ça -2) L’inspection ouvre déjà le bon contexte bot -Le code actuel a déjà un flux “Inspect bot” : +ue head -le bouton Inspect appelle InspectUnit(tName), +ue 1 -et déclenche aussi une requête d’inventaire du bot. +ue S01 -Donc on a déjà : +Dans ce cas, c’est idéal. +Parce que la fenêtre d’inspection affiche précisément des slots. Tu peux donc faire une UI très propre : -le nom du bot cible, +clic droit sur un slot équipé → unequip ; -l’ouverture de l’inspection, +ou petit bouton contextuel sur chaque slot ; -et un écosystème UI orienté équipement / inventaire. +ou mode “Unequip” activable, puis clic sur le slot. -3) Si la commande cible l’item équipé, le clic sur un slot inspecté devient naturel -Comme ue attend un item link, la bonne UX serait simplement : +Cas B — si ue fonctionne par item +Exemple conceptuel : -tu inspectes le bot, +ue [ItemLink] -tu cliques un slot équipé, +C’est encore faisable, mais un peu moins robuste : -l’addon récupère le lien de l’objet porté, +s’il y a ambiguïté, -puis envoie ue [item] au bot. +si deux objets identiques existent, -En pratique, c’est beaucoup plus simple que si la commande demandait un bag/slot exact. +ou si la commande backend attend autre chose qu’un lien standard. -Est-ce qu’il y a un risque d’ambiguïté ? -Oui, mais il est limité -Si tu fais l’action depuis un slot d’équipement visible, l’ambiguïté est faible : +Cas C — si ue a une syntaxe spéciale côté playerbots +Alors il faudra juste aligner l’UI sur cette syntaxe. +Mais dans tous les cas, le concept UI reste pertinent. -l’utilisateur clique précisément l’objet actuellement porté ; +Pourquoi c’est plus simple que sur la frame INVENTORY +Dans INVENTORY, les clics d’items sont pensés pour : -l’UI connaît l’objet visé. +s -Le seul vrai point à valider côté backend, c’est : +e -est-ce que ue [item] fonctionne bien même si le bot possède aussi un autre exemplaire identique dans ses sacs ? +u -Mon intuition -Souvent, si la commande backend interprète “unequip cet item équipé” à partir du lien, ça peut marcher correctement. -Mais ce point dépend totalement du code playerbots. +give -Donc : +destroy -UI : oui +Donc la frame INVENTORY actuelle agit surtout sur des items dans les sacs. -ambiguïté backend : à vérifier +À l’inverse, “unequip” est une action sur : -Là où il faut rester attentif -1) Le refresh après ue -Aujourd’hui, le handler refresh l’inventaire sur certains retours comme : +un slot équipé, + +ou un item actuellement porté. + +Donc conceptuellement, la fenêtre d’inspection est le meilleur endroit : + +plus logique pour l’utilisateur ; + +plus lisible ; + +moins ambigu ; + +plus proche du modèle métier. + +Est-ce que l’addon actuel est déjà prêt pour ça ? +Partiellement oui, mais pas complètement +Ce qui existe déjà : + +l’addon sait ouvrir une inspection ; + +l’addon sait envoyer des commandes whisper à un bot ; + +l’addon a déjà des représentations de slots d’équipement dans d’autres zones du code. + +Ce qui manque encore : + +une action UI dédiée “unequip” dans la vue d’inspection ; + +un mapping propre entre slot UI et payload ue ; + +et probablement un refresh fiable après l’action. + +Le point technique le plus important à ne pas oublier +Le refresh après ue +Aujourd’hui, le handler refresh l’inventaire lorsqu’il détecte des messages du bot du genre : equipping @@ -105,72 +140,65 @@ destroyed etc. -Donc si ue [item] renvoie un texte différent : +Donc si la commande ue renvoie un message différent, par exemple : unequipping removed -taken off +taking off ou autre -il faudra penser à brancher ce message sur un refresh, sinon : - -l’action peut réussir côté bot, +alors il faudra penser à raccrocher ce retour au refresh. +Sinon l’action pourra marcher côté bot, mais l’UI ne se resynchronisera pas proprement. -mais l’UI peut rester visuellement périmée. +C’est probablement le principal piège de cette feature. -C’est probablement le principal point technique. +UX que je recommanderais, sans code +Si tu veux une bonne intégration, je recommanderais plutôt : -2) L’inspection standard n’est pas encore enrichie par MultiBot pour cette action -Le code actuel montre bien le point d’entrée Inspect, mais pas encore une logique UI “unequip depuis le slot inspecté”. +Option 1 — clic droit sur slot équipé +Très naturel : -Donc oui, c’est faisable, mais aujourd’hui : +clic gauche = comportement standard / tooltip ; -ce n’est pas encore implémenté +clic droit = ue sur le slot. -il faudrait ajouter cette interaction explicitement. +Option 2 — mode “Unequip” +Comme dans INVENTORY avec les modes d’action : -Quelle UX je recommanderais -Avec ue [item], ma recommandation serait : +tu actives “Unequip” ; -Option la plus propre : clic droit sur item équipé dans l’inspection -Pourquoi ? +puis tu cliques un slot inspecté. -très naturel, +C’est cohérent avec la philosophie existante de MultiBot, mais probablement un peu plus lourd que nécessaire. -pas besoin d’un mode supplémentaire, +Option 3 — petit bouton contextuel / menu sur slot +Plus explicite, mais visuellement plus chargé. -cohérent avec une action contextuelle sur équipement, +Mon avis +Le clic droit sur le slot équipé est probablement la meilleure UX : -évite d’alourdir la fenêtre. +rapide, -Option alternative : petit bouton Unequip” -Possible aussi, mais moins élégant. +lisible, -Option que je recommanderais moins -Un mode global “Unequip” comme dans INVENTORY. -Ça marcherait, mais ce serait plus lourd qu’un simple clic contextuel sur un slot inspecté. +très cohérent avec une frame d’inspection. -Donc, mon verdict -Oui, ça vaut le coup -Avec ue [item], oui, tu peux très raisonnablement exposer ça graphiquement dans la fenêtre d’inspection d’un bot. +Mon diagnostic final +Oui, c’est une bonne idée +Et franchement, c’est bien plus réaliste et propre que de vouloir réordonner les sacs par drag & drop. -Pourquoi c’est une bonne feature -le protocole s’aligne avec le modèle actuel “commande par item link” ; +Condition indispensable +Il faut juste confirmer précisément : -l’inspection est déjà présente ; +la syntaxe réelle de ue, -l’action est conceptuellement liée à l’équipement, donc mieux placée là que dans la grille INVENTORY. +si elle cible un slot ou un item, -Ce qu’il faudra juste confirmer -que ue [item] déséquipe bien l’objet porté quand on lui passe le lien ; +quel message de retour elle produit, pour rebrancher le refresh. -quel message texte renvoie le bot après l’action ; +Si ces 3 points sont clairs, alors oui, ça vaut complètement le coup de l’exposer graphiquement dans l’inspection d’un bot. -si l’UI doit refresh seulement l’inventaire, ou aussi l’inspection. -Réponse courte -Oui, avec ue [item], c’est une très bonne candidate pour une intégration UI sur l’inspection d’un bot. -Le vrai point de vigilance n’est pas l’interface, mais le refresh et la validation backend de la commande sur les objets équipés. \ No newline at end of file