From 2d866c277b927bbb08ec74c84a93ed2c76d76913 Mon Sep 17 00:00:00 2001
From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com>
Date: Tue, 12 May 2026 22:51:20 +0200
Subject: [PATCH] Add bridge-first character info tabs and advanced inventory
flows
---
Core/MultiBotComm.lua | 238 +++++++++++
Locales/MultiBotAceLocale-deDE.lua | 9 +
Locales/MultiBotAceLocale-enGB.lua | 10 +
Locales/MultiBotAceLocale-enUS.lua | 9 +
Locales/MultiBotAceLocale-esES.lua | 9 +
Locales/MultiBotAceLocale-frFR.lua | 9 +
Locales/MultiBotAceLocale-koKR.lua | 9 +
Locales/MultiBotAceLocale-ruRU.lua | 9 +
Locales/MultiBotAceLocale-zhCN.lua | 9 +
README.md | 21 +-
TODO.md | 18 +-
UI/MultiBotCharacterInfoFrame.lua | 491 +++++++++++++++++++++-
docs/multibot_bridge_chatless_roadmap.md | 47 ++-
docs/multibot_missing_commands_roadmap.md | 68 ++-
14 files changed, 921 insertions(+), 35 deletions(-)
diff --git a/Core/MultiBotComm.lua b/Core/MultiBotComm.lua
index 21f4410..946fa7a 100644
--- a/Core/MultiBotComm.lua
+++ b/Core/MultiBotComm.lua
@@ -126,6 +126,13 @@ local function ensureBridgeState()
state.botSkills = state.botSkills or {}
state.botSkillSeq = state.botSkillSeq or 0
state.botSkillActive = state.botSkillActive or nil
+ state.botReputations = state.botReputations or {}
+ state.botReputationSeq = state.botReputationSeq or 0
+ state.botReputationActive = state.botReputationActive or nil
+ state.botEmblems = state.botEmblems or {}
+ state.botEmblemMoney = state.botEmblemMoney or {}
+ state.botEmblemSeq = state.botEmblemSeq or 0
+ state.botEmblemActive = state.botEmblemActive or nil
state.professionRecipes = state.professionRecipes or {}
state.professionRecipeSeq = state.professionRecipeSeq or 0
state.professionRecipeActive = state.professionRecipeActive or nil
@@ -657,6 +664,57 @@ function Comm.RequestBotSkills(name)
return true
end
+function Comm.RequestBotReputations(name)
+ local state = ensureBridgeState()
+ name = trim(name)
+ if name == "" or not state.connected then
+ return false
+ end
+
+ state.botReputationSeq = (tonumber(state.botReputationSeq) or 0) + 1
+ local token = tostring(math.floor(safeNow() * 1000)) .. "-rep-" .. tostring(state.botReputationSeq)
+ state.botReputationActive = {
+ botName = name,
+ botNameKey = string.lower(name),
+ token = token,
+ startedAt = safeNow(),
+ items = {},
+ }
+
+ if not Comm.Send("GET", "BOT_REPUTATIONS~" .. name .. "~" .. token) then
+ state.botReputationActive = nil
+ return false
+ end
+
+ return true
+end
+
+function Comm.RequestBotEmblems(name)
+ local state = ensureBridgeState()
+ name = trim(name)
+ if name == "" or not state.connected then
+ return false
+ end
+
+ state.botEmblemSeq = (tonumber(state.botEmblemSeq) or 0) + 1
+ local token = tostring(math.floor(safeNow() * 1000)) .. "-emblem-" .. tostring(state.botEmblemSeq)
+ state.botEmblemActive = {
+ botName = name,
+ botNameKey = string.lower(name),
+ token = token,
+ startedAt = safeNow(),
+ items = {},
+ money = nil,
+ }
+
+ if not Comm.Send("GET", "BOT_EMBLEMS~" .. name .. "~" .. token) then
+ state.botEmblemActive = nil
+ return false
+ end
+
+ return true
+end
+
function Comm.RequestProfessionRecipes(name, skillId)
local state = ensureBridgeState()
name = trim(name)
@@ -754,6 +812,8 @@ function Comm.MarkDisconnected(reason)
state.inventoryItemActions = {}
state.spellbookActive = nil
state.botSkillActive = nil
+ state.botReputationActive = nil
+ state.botEmblemActive = nil
state.professionRecipeActive = nil
state.professionRecipeCrafts = {}
state.outfitActive = nil
@@ -1824,6 +1884,42 @@ local function getActiveBotSkillRequest(botName, token)
return active
end
+local function getActiveBotReputationRequest(botName, token)
+ local state = ensureBridgeState()
+ local active = state.botReputationActive
+ if type(active) ~= "table" then
+ return nil
+ end
+
+ if botName and botName ~= "" and string.lower(trim(botName)) ~= trim(active.botNameKey or "") then
+ return nil
+ end
+
+ if token and token ~= "" and tostring(token) ~= tostring(active.token or "") then
+ return nil
+ end
+
+ return active
+end
+
+local function getActiveBotEmblemRequest(botName, token)
+ local state = ensureBridgeState()
+ local active = state.botEmblemActive
+ if type(active) ~= "table" then
+ return nil
+ end
+
+ if botName and botName ~= "" and string.lower(trim(botName)) ~= trim(active.botNameKey or "") then
+ return nil
+ end
+
+ if token and token ~= "" and tostring(token) ~= tostring(active.token or "") then
+ return nil
+ end
+
+ return active
+end
+
local function getActiveProfessionRecipeRequest(botName, token, skillId)
local state = ensureBridgeState()
local active = state.professionRecipeActive
@@ -2478,6 +2574,143 @@ function Comm.HandleAddonMessage(prefix, message, distribution, sender)
return true
end
+ if opcode == "BOT_REPUTATIONS_BEGIN" then
+ local botName, token = splitOnce(payload or "", "~")
+ botName = trim(urlDecodeField(botName))
+ token = trim(token)
+ state.connected = true
+ state.lastError = nil
+
+ local active = getActiveBotReputationRequest(botName, token)
+ if active then
+ active.items = {}
+ end
+
+ return true
+ end
+
+ if opcode == "BOT_REPUTATION_ITEM" then
+ local botName, rest = splitOnce(payload or "", "~")
+ local token, rest2 = splitOnce(rest or "", "~")
+ local factionId, rest3 = splitOnce(rest2 or "", "~")
+ local factionName, rest4 = splitOnce(rest3 or "", "~")
+ local rank, rest5 = splitOnce(rest4 or "", "~")
+ local value, maxValue = splitOnce(rest5 or "", "~")
+
+ botName = trim(urlDecodeField(botName))
+ token = trim(token)
+ state.connected = true
+ state.lastError = nil
+
+ local active = getActiveBotReputationRequest(botName, token)
+ if active then
+ table.insert(active.items, {
+ factionId = tonumber(factionId or "0") or 0,
+ name = trim(urlDecodeField(factionName)),
+ rank = tonumber(rank or "0") or 0,
+ value = tonumber(value or "0") or 0,
+ max = tonumber(maxValue or "0") or 0,
+ })
+ end
+
+ return true
+ end
+
+ if opcode == "BOT_REPUTATIONS_END" then
+ local botName, token = splitOnce(payload or "", "~")
+ botName = trim(urlDecodeField(botName))
+ token = trim(token)
+ state.connected = true
+ state.lastError = nil
+
+ local active = getActiveBotReputationRequest(botName, token)
+ if active then
+ local key = string.lower(botName)
+ state.botReputations[key] = active.items or {}
+ if MultiBot.OnBridgeBotReputations then
+ MultiBot.OnBridgeBotReputations(botName, state.botReputations[key], token)
+ end
+ state.botReputationActive = nil
+ end
+
+ return true
+ end
+
+ if opcode == "BOT_EMBLEMS_BEGIN" then
+ local botName, token = splitOnce(payload or "", "~")
+ botName = trim(urlDecodeField(botName))
+ token = trim(token)
+ state.connected = true
+ state.lastError = nil
+
+ local active = getActiveBotEmblemRequest(botName, token)
+ if active then
+ active.items = {}
+ active.money = nil
+ end
+
+ return true
+ end
+
+ if opcode == "BOT_EMBLEM_ITEM" then
+ local botName, rest = splitOnce(payload or "", "~")
+ local token, rest2 = splitOnce(rest or "", "~")
+ local itemId, count = splitOnce(rest2 or "", "~")
+
+ botName = trim(urlDecodeField(botName))
+ token = trim(token)
+ state.connected = true
+ state.lastError = nil
+
+ local active = getActiveBotEmblemRequest(botName, token)
+ if active then
+ table.insert(active.items, {
+ itemId = tonumber(itemId or "0") or 0,
+ count = tonumber(count or "0") or 0,
+ })
+ end
+
+ return true
+ end
+
+ if opcode == "BOT_EMBLEMS_MONEY" then
+ local botName, rest = splitOnce(payload or "", "~")
+ local token, money = splitOnce(rest or "", "~")
+
+ botName = trim(urlDecodeField(botName))
+ token = trim(token)
+ state.connected = true
+ state.lastError = nil
+
+ local active = getActiveBotEmblemRequest(botName, token)
+ if active then
+ active.money = tonumber(money or "0") or 0
+ end
+
+ return true
+ end
+
+ if opcode == "BOT_EMBLEMS_END" then
+ local botName, token = splitOnce(payload or "", "~")
+ botName = trim(urlDecodeField(botName))
+ token = trim(token)
+ state.connected = true
+ state.lastError = nil
+
+ local active = getActiveBotEmblemRequest(botName, token)
+ if active then
+ local key = string.lower(botName)
+ state.botEmblems[key] = active.items or {}
+ state.botEmblemMoney[key] = active.money
+ if MultiBot.OnBridgeBotEmblems then
+ MultiBot.OnBridgeBotEmblems(botName, state.botEmblems[key], token, state.botEmblemMoney[key])
+ end
+ state.botEmblemActive = nil
+ end
+
+ return true
+ end
+
if opcode == "PROFESSION_RECIPES_BEGIN" then
local botName, rest = splitOnce(payload or "", "~")
local token, skillId = splitOnce(rest or "", "~")
@@ -2680,6 +2913,11 @@ function Comm.OnPlayerEnteringWorld()
state.spellbookActive = nil
state.botSkills = {}
state.botSkillActive = nil
+ state.botReputations = {}
+ state.botReputationActive = nil
+ state.botEmblems = {}
+ state.botEmblemMoney = {}
+ state.botEmblemActive = nil
state.professionRecipes = {}
state.professionRecipeActive = nil
state.professionRecipeCrafts = {}
diff --git a/Locales/MultiBotAceLocale-deDE.lua b/Locales/MultiBotAceLocale-deDE.lua
index 84e76af..fb805ec 100644
--- a/Locales/MultiBotAceLocale-deDE.lua
+++ b/Locales/MultiBotAceLocale-deDE.lua
@@ -77,8 +77,17 @@ local deDEValues = {
["profession.recipes.craft.reason.LOST_CONTROL"] = "Der Bot kann gerade nicht handeln.",
["profession.recipes.craft.reason.CHANNELING"] = "Der Bot kanalisiert bereits einen Zauber.",
["profession.recipes.craft.reason.cast_code"] = "Der Server hat den Zauber abgelehnt (Code %s).",
+ ["character.tab.skills"] = "Fertigkeiten",
+ ["character.tab.reputations"] = "Ruf",
+ ["character.tab.emblems"] = "Abzeichen",
["character.skills.count"] = "Fertigkeit(en)",
["character.skills.loading"] = "Wird geladen...",
+ ["character.reputations.count"] = "Rufstufe(n)",
+ ["character.reputations.loading"] = "Wird geladen...",
+ ["character.reputations.empty"] = "Kein Ruf verfügbar.",
+ ["character.emblems.count"] = "Abzeichen",
+ ["character.emblems.loading"] = "Wird geladen...",
+ ["character.emblems.empty"] = "Keine Abzeichen.",
["character.skills.category.class"] = "Klassenfertigkeiten",
["character.skills.category.profession"] = "Berufe",
["character.skills.category.secondary"] = "Sekundäre Berufe",
diff --git a/Locales/MultiBotAceLocale-enGB.lua b/Locales/MultiBotAceLocale-enGB.lua
index c685e38..01c81fb 100644
--- a/Locales/MultiBotAceLocale-enGB.lua
+++ b/Locales/MultiBotAceLocale-enGB.lua
@@ -77,8 +77,18 @@ local enGBValues = {
["profession.recipes.craft.reason.LOST_CONTROL"] = "The bot cannot act right now.",
["profession.recipes.craft.reason.CHANNELING"] = "The bot is already channeling a spell.",
["profession.recipes.craft.reason.cast_code"] = "The server refused the cast (code %s).",
+ ["character.tab.skills"] = "Skills",
+ ["character.tab.reputations"] = "Reputations",
+ ["character.tab.emblems"] = "Emblems",
["character.skills.count"] = "skill(s)",
["character.skills.loading"] = "Loading...",
+ ["character.reputations.count"] = "reputation(s)",
+ ["character.reputations.loading"] = "Loading...",
+ ["character.reputations.empty"] = "No reputations.",
+ ["character.emblems.count"] = "emblem(s)",
+ ["character.emblems.loading"] = "Loading...",
+ ["character.emblems.empty"] = "No emblems.",
+ ["character.skills.loading"] = "Loading...",
["character.skills.category.class"] = "Class Skills",
["character.skills.category.profession"] = "Professions",
["character.skills.category.secondary"] = "Secondary Professions",
diff --git a/Locales/MultiBotAceLocale-enUS.lua b/Locales/MultiBotAceLocale-enUS.lua
index 06d1283..543add3 100644
--- a/Locales/MultiBotAceLocale-enUS.lua
+++ b/Locales/MultiBotAceLocale-enUS.lua
@@ -77,8 +77,17 @@ local enUSValues = {
["profession.recipes.craft.reason.LOST_CONTROL"] = "The bot cannot act right now.",
["profession.recipes.craft.reason.CHANNELING"] = "The bot is already channeling a spell.",
["profession.recipes.craft.reason.cast_code"] = "The server refused the cast (code %s).",
+ ["character.tab.skills"] = "Skills",
+ ["character.tab.reputations"] = "Reputations",
+ ["character.tab.emblems"] = "Emblems",
["character.skills.count"] = "skill(s)",
["character.skills.loading"] = "Loading...",
+ ["character.reputations.count"] = "reputation(s)",
+ ["character.reputations.loading"] = "Loading...",
+ ["character.reputations.empty"] = "No reputations.",
+ ["character.emblems.count"] = "emblem(s)",
+ ["character.emblems.loading"] = "Loading...",
+ ["character.emblems.empty"] = "No emblems.",
["character.skills.category.class"] = "Class Skills",
["character.skills.category.profession"] = "Professions",
["character.skills.category.secondary"] = "Secondary Professions",
diff --git a/Locales/MultiBotAceLocale-esES.lua b/Locales/MultiBotAceLocale-esES.lua
index a27a6e2..a157bae 100644
--- a/Locales/MultiBotAceLocale-esES.lua
+++ b/Locales/MultiBotAceLocale-esES.lua
@@ -77,8 +77,17 @@ local esESValues = {
["profession.recipes.craft.reason.LOST_CONTROL"] = "El bot no puede actuar ahora.",
["profession.recipes.craft.reason.CHANNELING"] = "El bot ya está canalizando un hechizo.",
["profession.recipes.craft.reason.cast_code"] = "El servidor rechazó el lanzamiento (código %s).",
+ ["character.tab.skills"] = "Habilidades",
+ ["character.tab.reputations"] = "Reputaciones",
+ ["character.tab.emblems"] = "Emblemas",
["character.skills.count"] = "habilidad(es)",
["character.skills.loading"] = "Cargando...",
+ ["character.reputations.count"] = "reputación(es)",
+ ["character.reputations.loading"] = "Cargando...",
+ ["character.reputations.empty"] = "Sin reputaciones.",
+ ["character.emblems.count"] = "emblema(s)",
+ ["character.emblems.loading"] = "Cargando...",
+ ["character.emblems.empty"] = "Sin emblemas.",
["character.skills.category.class"] = "Habilidades de clase",
["character.skills.category.profession"] = "Profesiones",
["character.skills.category.secondary"] = "Profesiones secundarias",
diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua
index 624719e..d67e6ac 100644
--- a/Locales/MultiBotAceLocale-frFR.lua
+++ b/Locales/MultiBotAceLocale-frFR.lua
@@ -77,8 +77,17 @@ local frFRValues = {
["profession.recipes.craft.reason.LOST_CONTROL"] = "Le bot ne peut pas agir maintenant.",
["profession.recipes.craft.reason.CHANNELING"] = "Le bot canalise déjà un sort.",
["profession.recipes.craft.reason.cast_code"] = "Le serveur a refusé le cast (code %s).",
+ ["character.tab.skills"] = "Compétences",
+ ["character.tab.reputations"] = "Réputations",
+ ["character.tab.emblems"] = "Monaies",
["character.skills.count"] = "compétence(s)",
["character.skills.loading"] = "Chargement...",
+ ["character.reputations.count"] = "réputation(s)",
+ ["character.reputations.loading"] = "Chargement...",
+ ["character.reputations.empty"] = "Aucune réputation.",
+ ["character.emblems.count"] = "emblème(s)",
+ ["character.emblems.loading"] = "Chargement...",
+ ["character.emblems.empty"] = "Aucun emblème.",
["character.skills.category.class"] = "Compétences de classe",
["character.skills.category.profession"] = "Métiers",
["character.skills.category.secondary"] = "Métiers secondaires",
diff --git a/Locales/MultiBotAceLocale-koKR.lua b/Locales/MultiBotAceLocale-koKR.lua
index aa02cc1..757d1e8 100644
--- a/Locales/MultiBotAceLocale-koKR.lua
+++ b/Locales/MultiBotAceLocale-koKR.lua
@@ -77,8 +77,17 @@ local koKRValues = {
["profession.recipes.craft.reason.LOST_CONTROL"] = "봇이 지금 행동할 수 없습니다.",
["profession.recipes.craft.reason.CHANNELING"] = "봇이 이미 주문을 시전 중입니다.",
["profession.recipes.craft.reason.cast_code"] = "서버가 시전을 거부했습니다(코드 %s).",
+ ["character.tab.skills"] = "기술",
+ ["character.tab.reputations"] = "평판",
+ ["character.tab.emblems"] = "엠블렘",
["character.skills.count"] = "기술",
["character.skills.loading"] = "불러오는 중...",
+ ["character.reputations.count"] = "평판",
+ ["character.reputations.loading"] = "불러오는 중...",
+ ["character.reputations.empty"] = "평판이 없습니다.",
+ ["character.emblems.count"] = "엠블렘",
+ ["character.emblems.loading"] = "불러오는 중...",
+ ["character.emblems.empty"] = "엠블렘이 없습니다.",
["character.skills.category.class"] = "직업 기술",
["character.skills.category.profession"] = "전문 기술",
["character.skills.category.secondary"] = "보조 기술",
diff --git a/Locales/MultiBotAceLocale-ruRU.lua b/Locales/MultiBotAceLocale-ruRU.lua
index 0a85bb6..c3ff7ec 100644
--- a/Locales/MultiBotAceLocale-ruRU.lua
+++ b/Locales/MultiBotAceLocale-ruRU.lua
@@ -77,8 +77,17 @@ local ruRUValues = {
["profession.recipes.craft.reason.LOST_CONTROL"] = "Бот сейчас не может действовать.",
["profession.recipes.craft.reason.CHANNELING"] = "Бот уже поддерживает заклинание.",
["profession.recipes.craft.reason.cast_code"] = "Сервер отклонил применение (код %s).",
+ ["character.tab.skills"] = "Навыки",
+ ["character.tab.reputations"] = "Репутации",
+ ["character.tab.emblems"] = "Знаки",
["character.skills.count"] = "навык(и)",
["character.skills.loading"] = "Загрузка...",
+ ["character.reputations.count"] = "репутация(и)",
+ ["character.reputations.loading"] = "Загрузка...",
+ ["character.reputations.empty"] = "Нет репутаций.",
+ ["character.emblems.count"] = "знак(и)",
+ ["character.emblems.loading"] = "Загрузка...",
+ ["character.emblems.empty"] = "Нет знаков.",
["character.skills.category.class"] = "Классовые навыки",
["character.skills.category.profession"] = "Профессии",
["character.skills.category.secondary"] = "Вторичные профессии",
diff --git a/Locales/MultiBotAceLocale-zhCN.lua b/Locales/MultiBotAceLocale-zhCN.lua
index 54e9e6e..81b1487 100644
--- a/Locales/MultiBotAceLocale-zhCN.lua
+++ b/Locales/MultiBotAceLocale-zhCN.lua
@@ -77,8 +77,17 @@ local zhCNValues = {
["profession.recipes.craft.reason.LOST_CONTROL"] = "机器人现在无法行动。",
["profession.recipes.craft.reason.CHANNELING"] = "机器人已经在引导法术。",
["profession.recipes.craft.reason.cast_code"] = "服务器拒绝施法(代码 %s)。",
+ ["character.tab.skills"] = "技能",
+ ["character.tab.reputations"] = "声望",
+ ["character.tab.emblems"] = "徽章",
["character.skills.count"] = "技能",
["character.skills.loading"] = "加载中...",
+ ["character.reputations.count"] = "声望",
+ ["character.reputations.loading"] = "加载中...",
+ ["character.reputations.empty"] = "无声望。",
+ ["character.emblems.count"] = "徽章",
+ ["character.emblems.loading"] = "加载中...",
+ ["character.emblems.empty"] = "无徽章。",
["character.skills.category.class"] = "职业技能",
["character.skills.category.profession"] = "专业技能",
["character.skills.category.secondary"] = "辅助专业",
diff --git a/README.md b/README.md
index 494affd..53b610a 100644
--- a/README.md
+++ b/README.md
@@ -89,8 +89,12 @@ GET~STATS
GET~PVP_STATS
GET~TALENT_SPEC_LIST
GET~INVENTORY
+GET~BANK
+GET~GBANK
GET~SPELLBOOK
GET~BOT_SKILLS
+GET~BOT_REPUTATIONS
+GET~BOT_EMBLEMS
GET~PROFESSION_RECIPES
GET~GLYPHS
GET~OUTFITS
@@ -167,7 +171,11 @@ The goal is to remove automatic UI-refresh spam.
| Character Info frame |
- Bridge-first class skills, professions, secondary skills, weapon skills and armor skills |
+ Bridge-first Blizzard-style tabs for skills, reputations and currencies/emblems |
+
+
+ | Bot bank / guild bank / vendor buy |
+ Bridge-first bank snapshots, guild bank snapshots, bank deposit/withdraw, guild bank deposit and vendor buy actions |
| Profession recipe frame |
@@ -461,7 +469,8 @@ Implemented bridge-first / chatless areas:
- Talent spec list refresh.
- Inventory refresh with icons and item tooltips.
- Spellbook refresh, with profession/crafting spells separated from the combat spellbook path.
-- Character Info frame through the bridge with class, profession, secondary, weapon and armor skills.
+- Character Info frame through the bridge with Blizzard-style tabs for class, profession, secondary, weapon and armor skills, reputations and currencies/emblems.
+- Bot bank and guild bank snapshots through the bridge, plus bank deposit/withdraw, guild bank deposit and vendor buy item actions.
- Profession recipe frame through the bridge, opened from profession and secondary skill rows.
- Glyph refresh with icons and glyph tooltips.
- Outfits refresh and actions through the bridge.
@@ -491,11 +500,11 @@ Kept intentionally:
# Remaining Work
-The Outfits, RTI, Pull Control, Combat Strategy, Disperse, Loot Rules, Quest, Game Object, Character Info and Profession Recipe migrations are implemented. The Loot Master UI is also implemented as an optional client-side master-loot helper. The next step is final stabilization and cleanup.
+The Outfits, RTI, Pull Control, Combat Strategy, Disperse, Loot Rules, Quest, Game Object, Character Info, Profession Recipe, Reputations, Currencies and advanced inventory bank/vendor migrations are implemented. The Loot Master UI is also implemented as an optional client-side master-loot helper. The next step is final stabilization and cleanup.
Planned follow-up work:
-- Regression test login, `/reload`, large raid groups, Units, EveryBars, Stats, PvP Stats, Inventory, Spellbook, Character Info, Profession Recipes, Talents, Glyphs, Outfits, Quests, Game Objects, RTI, Pull Control, Combat Strategies, Disperse, Loot Rules and Loot Master.
+- Regression test login, `/reload`, large raid groups, Units, EveryBars, Stats, PvP Stats, Inventory, Bot Bank, Guild Bank, Vendor Buy, Spellbook, Character Info, Reputations, Currencies, Profession Recipes, Talents, Glyphs, Outfits, Quests, Game Objects, RTI, Pull Control, Combat Strategies, Disperse, Loot Rules and Loot Master.
- Verify that `MultiBot.allowLegacyChatFallback = false` prevents automatic legacy refresh spam on all migrated UI paths.
- Keep manual diagnostic commands documented and functional.
- Remove obsolete debug prints.
@@ -560,10 +569,14 @@ Check the server console for bridge requests such as:
```text
GET~INVENTORY
+GET~BANK
+GET~GBANK
GET~SPELLBOOK
GET~GLYPHS
GET~OUTFITS
GET~BOT_SKILLS
+GET~BOT_REPUTATIONS
+GET~BOT_EMBLEMS
GET~PROFESSION_RECIPES
GET~QUESTS
GET~GAMEOBJECTS
diff --git a/TODO.md b/TODO.md
index 02cf7b4..67cb5d8 100644
--- a/TODO.md
+++ b/TODO.md
@@ -18,16 +18,9 @@
* Refaire uns passe pour que toutes les frames respectent le strata de la config.
* Retravailler la frame pvp stats qu'elle ressemble aux autres, et ajuster l'alignement du texte
* Faire une UI pour enchanter les objets à moins qu'on arrive à faire le bot caster le spell sur la fenêtre de trade.
-* implementer les commandes rep all et emblems pour être affichées dans la frame d'info des bots
-* Ajouter un bouton dans la frame des quêtes du bot pour le faire abandonner la quête
-* Voi si besoin de quelque chose dans l'addon pour le : https://github.com/mod-playerbots/mod-playerbots/pull/2372
-
## Informations bot
* Dans les frames métier ajout d'un bouton pour faire le bot acheter les composants manquants pour crafter l'item.
-
-** TODO
-* Ajouter les Monnaies (emblemes etc..)
-* Ajouter les Réputations
+* Infos personnage : onglets style Blizzard pour compétences, réputations et monnaies.
## Inventaire Bot étendu
@@ -35,11 +28,9 @@
* Ajout d'une frame pour afficher le contenu de la banque du bot, avec un bouton pour retirer les objets de la banque.
* Ajout d'un bouton pour déposer des objets dans la banque de guilde.
* Ajout d'une frame pour afficher le contenu de la banque de guilde.
-*
** TODO
-* Voire si on tiends compte des droits de guilde pour les retraits et dépots, est ce que les rangs des bots évoluent dans playerbots.
-* Les banquier neutres comme par exemple Dalaran ne sont pas reconnus comme banquiers
+* Voire si on tient compte des droits de guilde pour les retraits et dépôts, et si les rangs des bots évoluent dans playerbots.
* Uniformiser le layout des frames de banque bot et BDG
* Ajouter un bouton retrait à la frame BDG
* Afficher les sous de la guilde dans la frame BDG
@@ -116,6 +107,11 @@
* all.
* Outfits bridge-first.
* Character Info bridge-first.
+* Réputations bridge-first dans la frame Infos personnage.
+* Monnaies / emblèmes bridge-first dans la frame Infos personnage, avec argent du bot.
+* Banque bot bridge-first avec consultation, dépôt et retrait.
+* Banque de guilde bot bridge-first avec consultation et dépôt.
+* Achat vendeur bridge-first depuis les composants manquants de recette métier.
* Profession recipes bridge-first.
* Craft de recettes métier via bridge `RUN~CRAFT_RECIPE`.
* Messages d'erreur détaillés pour le craft :
diff --git a/UI/MultiBotCharacterInfoFrame.lua b/UI/MultiBotCharacterInfoFrame.lua
index 32fcef3..8304c22 100644
--- a/UI/MultiBotCharacterInfoFrame.lua
+++ b/UI/MultiBotCharacterInfoFrame.lua
@@ -34,6 +34,20 @@ local CHARACTER_SKILL_HEADER_WIDTH = CHARACTER_SKILL_ROW_WIDTH - 20
local CHARACTER_SKILL_NAME_WIDTH = 132
local CHARACTER_SKILL_VALUE_X = 142
local CHARACTER_SKILL_VALUE_WIDTH = 72
+local CHARACTER_TABS = {
+ { id = "skills", key = "character.tab.skills", fallback = "Skills" },
+ { id = "reputations", key = "character.tab.reputations", fallback = "Reputations" },
+ { id = "emblems", key = "character.tab.emblems", fallback = "Emblems" },
+}
+local CHARACTER_TAB_WIDTH = 116
+local CHARACTER_TAB_HEIGHT = 28
+local CHARACTER_TAB_SPACING = 6
+local CHARACTER_TAB_OFFSET_Y = -4
+local CHARACTER_STATUS_Y = -10
+local CHARACTER_SCROLL_TOP_Y = -32
+local REPUTATION_ROW_HEIGHT = 24
+local EMBLEM_ROW_HEIGHT = 26
+local EMBLEM_ICON_SIZE = 16
local RECIPE_FRAME_WIDTH = 340
local RECIPE_FRAME_X = 145
local RECIPE_ROW_WIDTH = 286
@@ -42,6 +56,26 @@ local RECIPE_TEXT_WIDTH = 176
local RECIPE_REFRESH_DELAY = 3.0
local COOKING_SKILL_ID = 185
+local REPUTATION_BAR_COLORS = {
+ [0] = { 0.80, 0.12, 0.12 }, -- Hated
+ [1] = { 0.80, 0.25, 0.12 }, -- Hostile
+ [2] = { 0.75, 0.35, 0.12 }, -- Unfriendly
+ [3] = { 0.75, 0.75, 0.10 }, -- Neutral
+ [4] = { 0.10, 0.70, 0.10 }, -- Friendly
+ [5] = { 0.10, 0.65, 0.75 }, -- Honored
+ [6] = { 0.05, 0.45, 0.85 }, -- Revered
+ [7] = { 0.65, 0.30, 0.85 }, -- Exalted
+}
+
+local EMBLEM_NAME_KEYS = {
+ [29434] = "tips.every.BadgeofJustice",
+ [40752] = "tips.every.EmblemofHeroism",
+ [40753] = "tips.every.EmblemofValor",
+ [45624] = "tips.every.EmblemofConquest",
+ [47241] = "tips.every.EmblemofTriumph",
+ [49426] = "tips.every.EmblemofFrost",
+}
+
local SKILL_DISPLAY_SPELL_IDS = {
[171] = 2259, -- Alchemy
[164] = 2018, -- Blacksmithing
@@ -245,6 +279,106 @@ local function createText(parent, template, point, x, y)
return text
end
+local function setFrameShown(frame, shown)
+ if not frame then
+ return
+ end
+
+ if shown then
+ frame:Show()
+ else
+ frame:Hide()
+ end
+end
+
+local function getTabTitle(tab)
+ return L(tab.key, tab.fallback)
+end
+
+local function createCharacterTab(parent, name, index, text)
+ local template = (_G["CharacterFrameTabButtonTemplate"] and "CharacterFrameTabButtonTemplate") or "UIPanelButtonTemplate"
+ local button = CreateFrame("Button", name .. "Tab" .. tostring(index), parent, template)
+ button:SetID(index)
+ button:SetWidth(CHARACTER_TAB_WIDTH)
+ button:SetHeight(CHARACTER_TAB_HEIGHT)
+ button:SetText(text or "")
+ return button
+end
+
+local function getFactionDisplayName(reputation)
+ local factionId = tonumber(reputation and reputation.factionId or 0) or 0
+ if factionId > 0 and GetFactionInfoByID then
+ local name = GetFactionInfoByID(factionId)
+ if type(name) == "string" and name ~= "" then
+ return name
+ end
+ end
+
+ local name = tostring(reputation and reputation.name or "")
+ if name ~= "" then
+ return name
+ end
+
+ return "Faction " .. tostring(factionId)
+end
+
+local function getReputationRankLabel(rank)
+ local index = (tonumber(rank or 0) or 0) + 1
+ local label = _G["FACTION_STANDING_LABEL" .. tostring(index)]
+ if type(label) == "string" and label ~= "" then
+ return label
+ end
+
+ return tostring(rank or "")
+end
+
+local function getReputationColor(rank)
+ local color = REPUTATION_BAR_COLORS[tonumber(rank or 0) or 0] or REPUTATION_BAR_COLORS[3]
+ return color[1], color[2], color[3]
+end
+
+local function getEmblemInfo(itemId)
+ itemId = tonumber(itemId or 0) or 0
+ local name, _, _, _, _, _, _, _, _, icon
+ if itemId > 0 and GetItemInfo then
+ name, _, _, _, _, _, _, _, _, icon = GetItemInfo(itemId)
+ end
+
+ if itemId > 0 and not icon and GetItemIcon then
+ icon = GetItemIcon(itemId)
+ end
+
+ if type(name) ~= "string" or name == "" then
+ local key = EMBLEM_NAME_KEYS[itemId]
+ name = key and L(key, "item:" .. tostring(itemId)) or ("item:" .. tostring(itemId))
+ end
+
+ if type(icon) ~= "string" or icon == "" then
+ icon = "Interface\\Icons\\INV_Misc_QuestionMark"
+ end
+
+ return name, icon
+end
+
+local function formatMoneyText(money)
+ money = math.max(0, tonumber(money or 0) or 0)
+ local gold = math.floor(money / 10000)
+ local silver = math.floor((money % 10000) / 100)
+ local copper = money % 100
+ local parts = {}
+
+ if gold > 0 then
+ table.insert(parts, gold .. " |TInterface\\MoneyFrame\\UI-GoldIcon:12:12:2:0|t")
+ end
+
+ if silver > 0 or gold > 0 then
+ table.insert(parts, silver .. " |TInterface\\MoneyFrame\\UI-SilverIcon:12:12:2:0|t")
+ end
+
+ table.insert(parts, copper .. " |TInterface\\MoneyFrame\\UI-CopperIcon:12:12:2:0|t")
+ return table.concat(parts, " ")
+end
+
local function getSkillBarText(skill)
local value = tonumber(skill and skill.value or 0) or 0
local max = tonumber(skill and skill.max or 0) or 0
@@ -583,13 +717,42 @@ local function ensureCharacterFrame()
local content = getFrameContent(frame)
- frame.status = createText(content, "GameFontHighlightSmall", "TOPLEFT", 18, -10)
+ frame.tabs = {}
+ frame.activeTab = "skills"
+ frame.status = createText(content, "GameFontHighlightSmall", "TOPLEFT", 18, CHARACTER_STATUS_Y)
+ frame.reputationStatus = createText(content, "GameFontHighlightSmall", "TOPLEFT", 18, CHARACTER_STATUS_Y)
+ frame.emblemStatus = createText(content, "GameFontHighlightSmall", "TOPLEFT", 18, CHARACTER_STATUS_Y)
frame.rows = {}
+ frame.reputationRows = {}
+ frame.emblemRows = {}
frame.skills = {}
+ frame.reputations = {}
+ frame.emblems = {}
+ frame.emblemMoney = nil
frame.collapsedCategories = frame.collapsedCategories or {}
+ local tabTotalWidth = (#CHARACTER_TABS * CHARACTER_TAB_WIDTH) + ((#CHARACTER_TABS - 1) * CHARACTER_TAB_SPACING)
+ local tabStartX = -math.floor(tabTotalWidth / 2)
+
+ for index, tab in ipairs(CHARACTER_TABS) do
+ local button = createCharacterTab(frame, "MultiBotCharacterInfoFrame", index, getTabTitle(tab))
+ button:SetPoint("TOPLEFT", content, "BOTTOM", tabStartX + ((index - 1) * (CHARACTER_TAB_WIDTH + CHARACTER_TAB_SPACING)), CHARACTER_TAB_OFFSET_Y)
+ button:SetScript("OnClick", function()
+ frame:setActiveTab(tab.id)
+ end)
+ button.tabId = tab.id
+ button.tabIndex = index
+ frame.tabs[tab.id] = button
+ end
+
+ frame.numTabs = #CHARACTER_TABS
+ frame.usePanelTemplates = _G["CharacterFrameTabButtonTemplate"] and PanelTemplates_SetTab
+ if frame.usePanelTemplates and PanelTemplates_SetNumTabs then
+ PanelTemplates_SetNumTabs(frame, frame.numTabs)
+ end
+
frame.scrollFrame = CreateFrame("ScrollFrame", "MultiBotCharacterInfoFrameSkillScrollFrame", content, "UIPanelScrollFrameTemplate")
- frame.scrollFrame:SetPoint("TOPLEFT", content, "TOPLEFT", 18, -32)
+ frame.scrollFrame:SetPoint("TOPLEFT", content, "TOPLEFT", 18, CHARACTER_SCROLL_TOP_Y)
frame.scrollFrame:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", -28, 36)
frame.scrollChild = CreateFrame("Frame", "MultiBotCharacterInfoFrameSkillScrollChild", frame.scrollFrame)
@@ -597,6 +760,37 @@ local function ensureCharacterFrame()
frame.scrollChild:SetHeight(1)
frame.scrollFrame:SetScrollChild(frame.scrollChild)
+ frame.reputationScrollFrame = CreateFrame("ScrollFrame", "MultiBotCharacterInfoFrameReputationScrollFrame", content, "UIPanelScrollFrameTemplate")
+ frame.reputationScrollFrame:SetPoint("TOPLEFT", content, "TOPLEFT", 18, CHARACTER_SCROLL_TOP_Y)
+ frame.reputationScrollFrame:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", -28, 36)
+
+ frame.reputationScrollChild = CreateFrame("Frame", "MultiBotCharacterInfoFrameReputationScrollChild", frame.reputationScrollFrame)
+ frame.reputationScrollChild:SetWidth(CHARACTER_SKILL_ROW_WIDTH)
+ frame.reputationScrollChild:SetHeight(1)
+ frame.reputationScrollFrame:SetScrollChild(frame.reputationScrollChild)
+
+ frame.emblemScrollFrame = CreateFrame("ScrollFrame", "MultiBotCharacterInfoFrameEmblemScrollFrame", content, "UIPanelScrollFrameTemplate")
+ frame.emblemScrollFrame:SetPoint("TOPLEFT", content, "TOPLEFT", 18, CHARACTER_SCROLL_TOP_Y)
+ frame.emblemScrollFrame:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", -28, 58)
+
+ frame.emblemScrollChild = CreateFrame("Frame", "MultiBotCharacterInfoFrameEmblemScrollChild", frame.emblemScrollFrame)
+ frame.emblemScrollChild:SetWidth(CHARACTER_SKILL_ROW_WIDTH)
+ frame.emblemScrollChild:SetHeight(1)
+ frame.emblemScrollFrame:SetScrollChild(frame.emblemScrollChild)
+
+ frame.emblemMoneyText = createText(content, "GameFontHighlightSmall", "BOTTOMRIGHT", -18, 14)
+ frame.emblemMoneyText:SetWidth(170)
+ frame.emblemMoneyText:SetHeight(18)
+ frame.emblemMoneyText:SetJustifyH("RIGHT")
+
+ frame.itemInfoEventFrame = CreateFrame("Frame")
+ pcall(frame.itemInfoEventFrame.RegisterEvent, frame.itemInfoEventFrame, "GET_ITEM_INFO_RECEIVED")
+ frame.itemInfoEventFrame:SetScript("OnEvent", function()
+ if frame.activeTab == "emblems" then
+ frame:renderEmblems()
+ end
+ end)
+
local function ensureSkillRow(index)
if frame.rows[index] then
return frame.rows[index]
@@ -669,6 +863,102 @@ local function ensureCharacterFrame()
return row
end
+ local function ensureReputationRow(index)
+ if frame.reputationRows[index] then
+ return frame.reputationRows[index]
+ end
+
+ local row = CreateFrame("Frame", nil, frame.reputationScrollChild)
+ row:SetPoint("TOPLEFT", frame.reputationScrollChild, "TOPLEFT", 0, 0)
+ row:SetWidth(CHARACTER_SKILL_ROW_WIDTH)
+ row:SetHeight(REPUTATION_ROW_HEIGHT)
+ row:EnableMouse(true)
+
+ row.bar = CreateFrame("StatusBar", nil, row)
+ row.bar:SetPoint("LEFT", row, "LEFT", 0, 0)
+ row.bar:SetPoint("RIGHT", row, "RIGHT", 0, 0)
+ row.bar:SetHeight(18)
+ row.bar:SetStatusBarTexture(SKILL_BAR_TEXTURE)
+ row.bar:SetMinMaxValues(0, 1)
+ row.bar:SetValue(0)
+ row.bar:SetBackdrop({
+ bgFile = SKILL_BAR_BACKGROUND,
+ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
+ tile = false,
+ tileSize = 0,
+ edgeSize = 8,
+ insets = { left = 2, right = 2, top = 2, bottom = 2 },
+ })
+ row.bar:SetBackdropColor(0.02, 0.02, 0.10, 0.75)
+ row.bar:SetBackdropBorderColor(0.35, 0.35, 0.35, 1)
+
+ row.nameText = createText(row.bar, "GameFontNormalSmall", "LEFT", 8, 0)
+ row.nameText:SetWidth(128)
+ row.nameText:SetHeight(18)
+
+ row.valueText = createText(row.bar, "GameFontHighlightSmall", "RIGHT", -8, 0)
+ row.valueText:SetWidth(98)
+ row.valueText:SetHeight(18)
+ row.valueText:SetJustifyH("RIGHT")
+
+ row:SetScript("OnEnter", function(self)
+ if not self.reputation then
+ return
+ end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:AddLine(getFactionDisplayName(self.reputation), 1, 0.82, 0)
+ GameTooltip:AddLine(getReputationRankLabel(self.reputation.rank) .. " " .. tostring(self.reputation.value or 0) .. "/" .. tostring(self.reputation.max or 0), 1, 1, 1)
+ GameTooltip:Show()
+ end)
+ row:SetScript("OnLeave", function()
+ GameTooltip:Hide()
+ end)
+
+ frame.reputationRows[index] = row
+ return row
+ end
+
+ local function ensureEmblemRow(index)
+ if frame.emblemRows[index] then
+ return frame.emblemRows[index]
+ end
+
+ local row = CreateFrame("Button", nil, frame.emblemScrollChild)
+ row:SetPoint("TOPLEFT", frame.emblemScrollChild, "TOPLEFT", 0, 0)
+ row:SetWidth(CHARACTER_SKILL_ROW_WIDTH)
+ row:SetHeight(EMBLEM_ROW_HEIGHT)
+
+ row.icon = row:CreateTexture(nil, "ARTWORK")
+ row.icon:SetPoint("RIGHT", row, "RIGHT", -34, 0)
+ row.icon:SetWidth(EMBLEM_ICON_SIZE)
+ row.icon:SetHeight(EMBLEM_ICON_SIZE)
+
+ row.nameText = createText(row, "GameFontHighlightSmall", "LEFT", 0, 0)
+ row.nameText:SetWidth(160)
+ row.nameText:SetHeight(20)
+
+ row.countText = createText(row, "GameFontNormalSmall", "RIGHT", -56, 0)
+ row.countText:SetWidth(48)
+ row.countText:SetHeight(20)
+ row.countText:SetJustifyH("RIGHT")
+
+ row:SetScript("OnEnter", function(self)
+ local itemId = tonumber(self.itemId or 0) or 0
+ if itemId <= 0 then
+ return
+ end
+ GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
+ GameTooltip:SetHyperlink("item:" .. tostring(itemId))
+ GameTooltip:Show()
+ end)
+ row:SetScript("OnLeave", function()
+ GameTooltip:Hide()
+ end)
+
+ frame.emblemRows[index] = row
+ return row
+ end
+
local function showHeaderRow(row, item, y)
row:SetPoint("TOPLEFT", frame.scrollChild, "TOPLEFT", 0, -y)
row:SetHeight(HEADER_ROW_HEIGHT)
@@ -716,6 +1006,44 @@ local function ensureCharacterFrame()
return SKILL_ROW_HEIGHT
end
+ local function setVisibleRows(rows, visible, hasContent)
+ for _, row in ipairs(rows or {}) do
+ if visible and hasContent(row) then
+ row:Show()
+ else
+ row:Hide()
+ end
+ end
+ end
+
+ frame.syncTabVisibility = function(self)
+ local showSkills = self.activeTab == "skills"
+ local showReputations = self.activeTab == "reputations"
+ local showEmblems = self.activeTab == "emblems"
+
+ setFrameShown(self.status, showSkills)
+ setFrameShown(self.scrollFrame, showSkills)
+ setFrameShown(self.scrollChild, showSkills)
+ setVisibleRows(self.rows, showSkills, function(row)
+ return row.skill ~= nil or row.categoryHeader ~= nil
+ end)
+
+ setFrameShown(self.reputationStatus, showReputations)
+ setFrameShown(self.reputationScrollFrame, showReputations)
+ setFrameShown(self.reputationScrollChild, showReputations)
+ setVisibleRows(self.reputationRows, showReputations, function(row)
+ return row.reputation ~= nil
+ end)
+
+ setFrameShown(self.emblemStatus, showEmblems)
+ setFrameShown(self.emblemScrollFrame, showEmblems)
+ setFrameShown(self.emblemScrollChild, showEmblems)
+ setFrameShown(self.emblemMoneyText, showEmblems)
+ setVisibleRows(self.emblemRows, showEmblems, function(row)
+ return row.itemId ~= nil
+ end)
+ end
+
frame.renderSkills = function(self)
local ordered = {}
@@ -766,6 +1094,118 @@ local function ensureCharacterFrame()
self.scrollChild:SetHeight(math.max(1, contentHeight))
self.scrollFrame:SetVerticalScroll(0)
+ self:syncTabVisibility()
+ end
+
+ frame.renderReputations = function(self)
+ local reputations = {}
+ for _, reputation in ipairs(self.reputations or {}) do
+ table.insert(reputations, reputation)
+ end
+
+ table.sort(reputations, function(a, b)
+ return getFactionDisplayName(a) < getFactionDisplayName(b)
+ end)
+
+ self.reputationStatus:SetText(#reputations .. " " .. L("character.reputations.count", "reputation(s)"))
+ if #reputations == 0 then
+ self.reputationStatus:SetText(L("character.reputations.empty", "No reputation data."))
+ end
+
+ local contentHeight = 0
+ for i = 1, #reputations do
+ local reputation = reputations[i]
+ local row = ensureReputationRow(i)
+ local value = tonumber(reputation.value or 0) or 0
+ local max = tonumber(reputation.max or 0) or 0
+ if max <= 0 then
+ max = 1
+ end
+
+ row:SetPoint("TOPLEFT", frame.reputationScrollChild, "TOPLEFT", 0, -contentHeight)
+ row.reputation = reputation
+ row.bar:SetMinMaxValues(0, max)
+ row.bar:SetValue(math.min(value, max))
+ row.bar:SetStatusBarColor(getReputationColor(reputation.rank))
+ row.nameText:SetText("|cffffcc00" .. getFactionDisplayName(reputation) .. "|r")
+ row.valueText:SetText("|cffffffff" .. getReputationRankLabel(reputation.rank) .. " " .. value .. "/" .. max .. "|r")
+ row:Show()
+ contentHeight = contentHeight + REPUTATION_ROW_HEIGHT
+ end
+
+ for i = #reputations + 1, #self.reputationRows do
+ local row = self.reputationRows[i]
+ row.reputation = nil
+ row:Hide()
+ end
+
+ self.reputationScrollChild:SetHeight(math.max(1, contentHeight))
+ self.reputationScrollFrame:SetVerticalScroll(0)
+ self:syncTabVisibility()
+ end
+
+ frame.renderEmblems = function(self)
+ local emblems = {}
+ for _, emblem in ipairs(self.emblems or {}) do
+ table.insert(emblems, emblem)
+ end
+
+ table.sort(emblems, function(a, b)
+ return (tonumber(a.itemId or 0) or 0) < (tonumber(b.itemId or 0) or 0)
+ end)
+
+ self.emblemStatus:SetText(#emblems .. " " .. L("character.emblems.count", "emblem(s)"))
+ if #emblems == 0 then
+ self.emblemStatus:SetText(L("character.emblems.empty", "No emblems."))
+ end
+
+ local contentHeight = 0
+ for i = 1, #emblems do
+ local emblem = emblems[i]
+ local row = ensureEmblemRow(i)
+ local itemId = tonumber(emblem.itemId or 0) or 0
+ local count = tonumber(emblem.count or 0) or 0
+ local name, icon = getEmblemInfo(itemId)
+
+ row:SetPoint("TOPLEFT", frame.emblemScrollChild, "TOPLEFT", 0, -contentHeight)
+ row.itemId = itemId
+ row.icon:SetTexture(MultiBot.SafeTexturePath(icon))
+ row.nameText:SetText((count > 0 and "|cffffffff" or "|cff808080") .. name .. "|r")
+ row.countText:SetText((count > 0 and "|cffffcc00" or "|cff808080") .. tostring(count) .. "|r")
+ row:Show()
+ contentHeight = contentHeight + EMBLEM_ROW_HEIGHT
+ end
+
+ for i = #emblems + 1, #self.emblemRows do
+ local row = self.emblemRows[i]
+ row.itemId = nil
+ row:Hide()
+ end
+
+ self.emblemScrollChild:SetHeight(math.max(1, contentHeight))
+ self.emblemMoneyText:SetText(self.emblemMoney and formatMoneyText(self.emblemMoney) or "")
+ self:syncTabVisibility()
+ end
+
+ frame.setActiveTab = function(self, tabId)
+ tabId = tabId or "skills"
+ self.activeTab = tabId
+ local selectedIndex
+
+ for id, button in pairs(self.tabs or {}) do
+ if id == tabId then
+ selectedIndex = button.tabIndex
+ button:LockHighlight()
+ else
+ button:UnlockHighlight()
+ end
+ end
+
+ if selectedIndex and self.usePanelTemplates then
+ PanelTemplates_SetTab(self, selectedIndex)
+ end
+
+ self:syncTabVisibility()
end
frame.setSkills = function(self, botName, skills)
@@ -776,6 +1216,24 @@ local function ensureCharacterFrame()
self:Show()
end
+ frame.setReputations = function(self, botName, reputations)
+ self.botName = botName
+ self.reputations = reputations or {}
+ setWindowTitle(self, L("character.info", "Character info") .. " - " .. (botName or ""))
+ self:renderReputations()
+ self:Show()
+ end
+
+ frame.setEmblems = function(self, botName, emblems, money)
+ self.botName = botName
+ self.emblems = emblems or {}
+ self.emblemMoney = money
+ setWindowTitle(self, L("character.info", "Character info") .. " - " .. (botName or ""))
+ self:renderEmblems()
+ self:Show()
+ end
+
+ frame:setActiveTab("skills")
frame:Hide()
MultiBot.characterInfoFrame = frame
return frame
@@ -791,21 +1249,46 @@ function MultiBot.OpenCharacterInfo(botName)
frame.botName = botName
setWindowTitle(frame, L("character.info", "Character info") .. " - " .. botName)
frame.skills = {}
+ frame.reputations = {}
+ frame.emblems = {}
+ frame.emblemMoney = nil
frame:renderSkills()
+ frame:renderReputations()
+ frame:renderEmblems()
frame.status:SetText(L("character.skills.loading", "Loading..."))
+ frame.reputationStatus:SetText(L("character.reputations.loading", "Loading..."))
+ frame.emblemStatus:SetText(L("character.emblems.loading", "Loading..."))
+ frame:setActiveTab("skills")
frame:Show()
+ local requested = false
if MultiBot.Comm and MultiBot.Comm.RequestBotSkills then
- return MultiBot.Comm.RequestBotSkills(botName)
+ requested = MultiBot.Comm.RequestBotSkills(botName) or requested
end
- return false
+ if MultiBot.Comm and MultiBot.Comm.RequestBotReputations then
+ requested = MultiBot.Comm.RequestBotReputations(botName) or requested
+ end
+
+ if MultiBot.Comm and MultiBot.Comm.RequestBotEmblems then
+ requested = MultiBot.Comm.RequestBotEmblems(botName) or requested
+ end
+
+ return requested
end
function MultiBot.OnBridgeBotSkills(botName, skills)
ensureCharacterFrame():setSkills(botName, skills or {})
end
+function MultiBot.OnBridgeBotReputations(botName, reputations)
+ ensureCharacterFrame():setReputations(botName, reputations or {})
+end
+
+function MultiBot.OnBridgeBotEmblems(botName, emblems, _token, money)
+ ensureCharacterFrame():setEmblems(botName, emblems or {}, money)
+end
+
function MultiBot.OnBridgeProfessionRecipes(botName, skillId, recipes)
local skill
local frame = ensureCharacterFrame()
diff --git a/docs/multibot_bridge_chatless_roadmap.md b/docs/multibot_bridge_chatless_roadmap.md
index e31cc7b..ab38120 100644
--- a/docs/multibot_bridge_chatless_roadmap.md
+++ b/docs/multibot_bridge_chatless_roadmap.md
@@ -1,6 +1,6 @@
# MultiBot / Bridge — roadmap chatless
-Dernière mise à jour : 2026-05-03
+Dernière mise à jour : 2026-05-12
## Objectif exact
@@ -109,12 +109,19 @@ Le but n’est pas de supprimer ces commandes. Le but est de ne plus les lancer
- [x] Filtrage des recettes métiers, métiers secondaires et skills hors spellbook combat
- [x] Fallback whisper `spells` réactivable uniquement via `MultiBot.allowLegacyChatFallback = true`
-### Character Info / Skills
+### Character Info / Skills / Reputations / Monnaies
- [x] Endpoint côté bridge : `GET~BOT_SKILLS~~`
- [x] Réponses en paquets courts : `BOT_SKILLS_BEGIN`, `BOT_SKILLS_ITEM`, `BOT_SKILLS_END`
+- [x] Endpoint côté bridge : `GET~BOT_REPUTATIONS~~`
+- [x] Réponses en paquets courts : `BOT_REPUTATIONS_BEGIN`, `BOT_REPUTATION_ITEM`, `BOT_REPUTATIONS_END`
+- [x] Endpoint côté bridge : `GET~BOT_EMBLEMS~~`
+- [x] Réponses en paquets courts : `BOT_EMBLEMS_BEGIN`, `BOT_EMBLEM_ITEM`, `BOT_EMBLEMS_MONEY`, `BOT_EMBLEMS_END`
- [x] Nouvelle frame `MultiBotCharacterInfoFrame`
+- [x] Onglets style Blizzard : compétences, réputations et monnaies
- [x] Affichage séparé des compétences de classe, métiers, métiers secondaires, compétences d'arme et armures
+- [x] Affichage des réputations visibles du bot sans lancer `rep all` en chat
+- [x] Affichage des emblèmes et de l'argent du bot sans lancer `emblems` en chat
- [x] Noms de compétences localisés via les données client quand possible
- [x] Clic sur un métier ou métier secondaire pour ouvrir la frame recettes
- [x] Fonctionne avec AddClass bots, altbots et randombots groupés visibles par la bridge
@@ -134,6 +141,19 @@ Le but n’est pas de supprimer ces commandes. Le but est de ne plus les lancer
- [x] Refresh différé des recettes après craft lancé
- [x] Aucun changement requis dans `mod-playerbots` : le bridge lance le sort métier connu par le bot
+### Bot bank / Guild bank / Vendor buy
+
+- [x] Endpoint côté bridge : `GET~BANK~~`
+- [x] Réponses en paquets courts : `BANK_BEGIN`, `BANK_ITEM`, `BANK_ERROR`, `BANK_END`
+- [x] Endpoint côté bridge : `GET~GBANK~~`
+- [x] Réponses en paquets courts : `GBANK_BEGIN`, `GBANK_ITEM`, `GBANK_ERROR`, `GBANK_END`
+- [x] Endpoint côté bridge : `RUN~ITEM_ACTION~~~~~`
+- [x] Actions validées : dépôt banque bot, retrait banque bot, dépôt banque de guilde et achat vendeur
+- [x] Consultation BDG basée sur la guilde du bot, sans exiger que le joueur soit dans la même guilde
+- [x] Détection des banquiers neutres avec flags compatibles Dalaran
+- [x] Messages d'erreur structurés pour banquier introuvable, droits BDG, vendeur introuvable, objet non vendu et monnaie spéciale
+- [x] Bouton `Acheter` dans la frame recettes pour acheter les composants manquants disponibles chez un vendeur proche
+
### PVP Stats
- [x] Endpoint côté bridge : `GET~PVP_STATS~`
@@ -259,7 +279,11 @@ Avec beaucoup de bots, éviter les réponses globales trop grosses.
- `GET~GLYPHS` répond en paquets `GLYPHS_BEGIN` / `GLYPHS_ITEM` / `GLYPHS_END` ;
- `GET~OUTFITS` répond en paquets `OUTFITS_BEGIN` / `OUTFITS_ITEM` / `OUTFITS_END` ;
- `GET~BOT_SKILLS` répond en paquets `BOT_SKILLS_BEGIN` / `BOT_SKILLS_ITEM` / `BOT_SKILLS_END` ;
+- `GET~BOT_REPUTATIONS` répond en paquets `BOT_REPUTATIONS_BEGIN` / `BOT_REPUTATION_ITEM` / `BOT_REPUTATIONS_END` ;
+- `GET~BOT_EMBLEMS` répond en paquets `BOT_EMBLEMS_BEGIN` / `BOT_EMBLEM_ITEM` / `BOT_EMBLEMS_MONEY` / `BOT_EMBLEMS_END` ;
- `GET~PROFESSION_RECIPES` répond en paquets `PROFESSION_RECIPES_BEGIN` / `PROFESSION_RECIPES_ITEM` / `PROFESSION_RECIPES_END` ;
+- `GET~BANK` répond en paquets `BANK_BEGIN` / `BANK_ITEM` / `BANK_ERROR` / `BANK_END` ;
+- `GET~GBANK` répond en paquets `GBANK_BEGIN` / `GBANK_ITEM` / `GBANK_ERROR` / `GBANK_END` ;
- un paquet global vide peut seulement servir de réponse vide si aucun bot n’est disponible.
---
@@ -298,8 +322,16 @@ Les Outfits sont maintenant migrés en bridge-first. La liste des tenues ne dép
Les compétences et recettes métiers sont maintenant migrées en bridge-first. La frame Character Info récupère les skills par `GET~BOT_SKILLS`, et la frame recettes récupère les recettes par `GET~PROFESSION_RECIPES`.
+La frame Character Info expose aussi les réputations par `GET~BOT_REPUTATIONS` et les monnaies par `GET~BOT_EMBLEMS`. Les onglets sont maintenant alignés sur un comportement style Blizzard, avec compétences, réputations et monnaies séparées.
+
Le craft de recette passe par `RUN~CRAFT_RECIPE`. Les recettes à résultat aléatoire ou indirect sont pilotées par `spellId`, ce qui évite de bloquer les crafts comme les cartes de calligraphie. Les erreurs de cast remontent une raison structurée au lieu d'un simple échec générique.
+### Bot bank / Guild bank / Vendor buy
+
+Les extensions inventaire banque, banque de guilde et achat vendeur sont maintenant bridge-first pour les chemins validés. La banque du bot et la banque de guilde du bot peuvent être consultées via `GET~BANK` et `GET~GBANK`.
+
+Les actions dépôt banque, retrait banque, dépôt banque de guilde et achat vendeur passent par `RUN~ITEM_ACTION`. Le retrait banque de guilde reste à traiter séparément si l'UI doit l'exposer plus tard, car il demande des garde-fous supplémentaires côté droits et logs.
+
### Audit chatless final
Les refresh automatiques UI les plus sensibles ne retombent plus en chat legacy par défaut. Le switch `MultiBot.allowLegacyChatFallback` permet de réactiver temporairement les anciens fallbacks pour diagnostic, mais la configuration propre reste `false`.
@@ -358,10 +390,15 @@ Conclusion : ne pas migrer par réflexe. À traiter seulement si une fenêtre UI
| Spellbook bridge | Fait |
| Spellbook sans recettes métiers | Fait |
| Character Info bridge | Fait |
+| Character Info réputations bridge | Fait |
+| Character Info monnaies / emblèmes bridge | Fait |
| Profession recipes bridge | Fait |
| Craft recettes métier bridge | Fait |
| Craft recettes à résultat aléatoire / indirect | Fait |
| Erreurs détaillées de craft | Fait |
+| Banque bot bridge | Fait |
+| Banque de guilde bot bridge | Fait |
+| Achat vendeur bridge | Fait |
| PVP Stats bridge | Fait |
| Stats simples bridge | Fait |
| Quêtes bridge | Fait |
@@ -387,18 +424,18 @@ Conclusion : ne pas migrer par réflexe. À traiter seulement si une fenêtre UI
## Prochain pas logique recommandé
-Le prochain pas logique est maintenant : **audit résiduel + commandes items avancées**.
+Le prochain pas logique est maintenant : **audit résiduel + Roll / ventes / open items / enchantements**.
À valider après les derniers lots :
1. refaire un cycle complet login, `/reload`, groupe, raid, AddClass, randombot groupé et altbot ;
-2. ouvrir Inventory, Spellbook, Character Info, Profession recipes, Glyphs, Outfits, Quests, Stats, PVP Stats, Loot Master ;
+2. ouvrir Inventory, Bot Bank, Guild Bank, Spellbook, Character Info, Reputations, Currencies, Profession recipes, Glyphs, Outfits, Quests, Stats, PVP Stats, Loot Master ;
3. tester un craft métier classique avec item de résultat direct ;
4. tester un craft métier à résultat indirect ou aléatoire, par exemple carte de calligraphie ;
5. tester un craft cuisine sans feu puis avec feu, et vérifier l'erreur localisée ;
6. vérifier en console qu’il n’y a plus de refresh automatique legacy `stats`, `items`, `spells`, `glyphs`, `talents spec list`, `outfit ?` avec `MultiBot.allowLegacyChatFallback = false` ;
7. vérifier que les commandes manuelles volontaires restent fonctionnelles : `who`, `co ?`, `nc ?`, `ss ?`, `.playerbot bot add`, `.playerbot bot remove`, actions inventory, `glyph equip`, sélection de spec et actions Outfits ;
-8. poursuivre ensuite avec Roll, ventes existantes et enchantements d'objets si besoin.
+8. poursuivre ensuite avec Roll, ventes existantes, open items bridge-first, retrait BDG et enchantements d'objets si besoin.
Après cette stabilisation, il restera surtout un audit de nettoyage : supprimer les debug temporaires, garder les fallbacks legacy uniquement quand ils sont utiles au diagnostic, et retirer les parsers historiques devenus morts.
diff --git a/docs/multibot_missing_commands_roadmap.md b/docs/multibot_missing_commands_roadmap.md
index 9b2eefc..2c1c488 100644
--- a/docs/multibot_missing_commands_roadmap.md
+++ b/docs/multibot_missing_commands_roadmap.md
@@ -9,7 +9,7 @@ Ce document suit les commandes `mod-playerbots` encore intéressantes à intégr
- les commandes serveur/admin à ne pas intégrer dans l'addon ;
- les priorités d'intégration bridge-first/chatless.
-Le principe reste le même que pour Inventory, Spellbook, Glyphs, Talents, Stats, Quests, Outfits, RTI, Pull Control, Combat Strategies, Disperse, Loot Rules, Character Info, Profession Recipes, Craft et Loot Master :
+Le principe reste le même que pour Inventory, Spellbook, Glyphs, Talents, Stats, Quests, Outfits, RTI, Pull Control, Combat Strategies, Disperse, Loot Rules, Character Info, Reputations, Monnaies, Profession Recipes, Craft, Loot Master, Bot Bank, Guild Bank et Vendor Buy :
**éviter le spam chat automatique**, utiliser le bridge quand c'est possible, et conserver les commandes manuelles utiles comme `who`, `co ?`, `nc ?`, `ss ?`.
---
@@ -30,8 +30,11 @@ Le principe reste le même que pour Inventory, Spellbook, Glyphs, Talents, Stats
| Quests | Présent | UI quêtes existante, pas à mélanger avec les commandes manuelles de diagnostic. |
| Outfits | Fait | Endpoint bridge + commandes outfits intégrées. |
| Character Info / Skills | Fait | Nouvelle frame infos personnage alimentée par `GET~BOT_SKILLS`, catégories skills/professions/armes, noms localisés via le client quand possible. |
+| Character Info / Reputations | Fait | Onglet réputations alimenté par `GET~BOT_REPUTATIONS`, sans spam chat `rep all`. |
+| Character Info / Monnaies | Fait | Onglet monnaies alimenté par `GET~BOT_EMBLEMS`, avec emblèmes et argent du bot. |
| Profession recipes | Fait | Nouvelle frame recettes par métier via `GET~PROFESSION_RECIPES`, composants, compte craftable, recettes à résultat direct ou indirect. |
| Profession recipe craft | Fait | Bouton `Créer` via `RUN~CRAFT_RECIPE`, support recettes classiques et résultats aléatoires/indirects, erreurs détaillées. |
+| Bot bank / Guild bank / Vendor buy | Fait/Partiel | `GET~BANK`, `GET~GBANK` et `RUN~ITEM_ACTION` pour banque bot, banque de guilde bot, achat vendeur et actions inventaire avancées validées. |
| RTI / Target Icons | Fait | UI complète + bridge `RUN~RTI`, scopes `ALL`, `GROUP`, `BOT`. |
| Pull Control | Fait | Mini-frame MainBar + bridge `RUN~COMBAT`, séquences de commandes, scopes et presets. |
| Combat Strategies | Fait | Toggles individuels dans les EveryBars + mini-frame Party/Raid, via `RUN~COMBAT`. |
@@ -42,7 +45,7 @@ Le principe reste le même que pour Inventory, Spellbook, Glyphs, Talents, Stats
---
-## Derniers lots terminés : Pull Control + Combat Strategies + Disperse + Loot Rules + Character Info + Craft + Loot Master
+## Derniers lots terminés : Pull Control + Combat Strategies + Disperse + Loot Rules + Character Info + Craft + Loot Master + Reputations + Monnaies + Items avancés
### Pull Control
@@ -175,6 +178,21 @@ Fonctionnalités terminées :
- compatibilité AddClass bots, altbots et randombots groupés ;
- clic sur un métier ou une compétence secondaire pour ouvrir la frame recettes quand des recettes existent.
+### Character Info / Reputations / Monnaies
+
+La frame `Infos personnage` expose maintenant les réputations et monnaies dans des onglets séparés, sans lancer automatiquement `rep all` ou `emblems` en chat.
+
+Fonctionnalités terminées :
+
+- endpoint bridge `GET~BOT_REPUTATIONS~~` ;
+- payloads structurés `BOT_REPUTATIONS_BEGIN`, `BOT_REPUTATION_ITEM`, `BOT_REPUTATIONS_END` ;
+- endpoint bridge `GET~BOT_EMBLEMS~~` ;
+- payloads structurés `BOT_EMBLEMS_BEGIN`, `BOT_EMBLEM_ITEM`, `BOT_EMBLEMS_MONEY`, `BOT_EMBLEMS_END` ;
+- affichage des réputations visibles du bot ;
+- affichage des emblèmes dans l'ordre WotLK attendu ;
+- affichage de l'argent du bot en bas de l'onglet monnaies ;
+- onglets style Blizzard en bas de la frame.
+
### Profession Recipes / Craft
La frame recettes métier permet maintenant de consulter et lancer les crafts connus par un bot via bridge.
@@ -221,6 +239,30 @@ Fonctionnalités terminées :
- persistance SavedVariables de l'état visible/caché ;
- relayout des boutons visibles pour que `Flee` et `Stay/Follow` restent directement après `Formation` quand les boutons optionnels sont cachés.
+### Items avancés banque / guild bank / buy
+
+Le bloc inventaire avancé couvre maintenant les usages banque, banque de guilde et achat vendeur les plus utiles sans parsing chat automatique.
+
+Fonctionnalités terminées :
+
+- endpoint bridge `GET~BANK~~` ;
+- payloads structurés `BANK_BEGIN`, `BANK_ITEM`, `BANK_ERROR`, `BANK_END` ;
+- endpoint bridge `GET~GBANK~~` ;
+- payloads structurés `GBANK_BEGIN`, `GBANK_ITEM`, `GBANK_ERROR`, `GBANK_END` ;
+- endpoint bridge `RUN~ITEM_ACTION~~~~~` ;
+- dépôt dans la banque du bot ;
+- retrait depuis la banque du bot ;
+- dépôt en banque de guilde du bot ;
+- achat de composants chez un vendeur proche ;
+- messages d'échec plus précis, notamment vendeur sans monnaie compatible ou banquier introuvable ;
+- consultation de la banque de guilde du bot sans exiger que le joueur soit dans la même guilde.
+
+À garder pour plus tard :
+
+- retrait banque de guilde, si l'UI doit l'exposer ;
+- affichage de l'argent de guilde ;
+- harmonisation visuelle finale des frames banque et BDG.
+
---
## Priorité 1 - RTI / Target Icons
@@ -545,7 +587,7 @@ Pas besoin de parsing automatique de réponses longues, sauf si une future UI ve
### Pourquoi
-L'inventaire bridge-first est déjà en place, et le craft de recettes métier passe maintenant par le bridge. Les commandes restantes sont des améliorations secondaires autour des items ponctuels, de la vente, de l'ouverture d'objets et des enchantements ciblant un item.
+L'inventaire bridge-first est déjà en place, le craft de recettes métier passe par le bridge, et les actions banque/guild bank/buy principales sont maintenant couvertes. Les commandes restantes sont des améliorations secondaires autour des items ponctuels, de la vente, de l'ouverture d'objets, du retrait BDG et des enchantements ciblant un item.
### Commandes à couvrir
@@ -559,11 +601,11 @@ L'inventaire bridge-first est déjà en place, et le craft de recettes métier p
| Craft recette métier | Fait | Moyenne | Frame recettes + bouton `Créer` via `RUN~CRAFT_RECIPE` |
| Craft recette à résultat aléatoire / indirect | Fait | Moyenne | Support `spellId` sans `itemId`, par exemple cartes de calligraphie |
| Enchanter un item | Manquant / à étudier | Moyenne | UI dédiée probable, avec cible item/sort et garde-fous |
-| `bank [item]` | Manquant | Basse | Clic droit item |
-| `bank -[item]` | Manquant | Basse | UI banque |
-| `gb [item]` | Manquant | Basse | Clic droit item |
-| `gb -[item]` | Manquant | Basse | UI guild bank |
-| `b [item]` | Manquant | Basse | UI vendor |
+| `bank [item]` | Fait via bridge | Basse | Dépôt banque bot via action item |
+| `bank -[item]` | Fait via bridge | Basse | Retrait depuis la frame banque bot |
+| `gb [item]` | Fait via bridge | Basse | Dépôt banque de guilde du bot |
+| `gb -[item]` | Manquant / à étudier | Basse | Retrait BDG à exposer seulement avec garde-fous |
+| `b [item]` | Fait via bridge | Basse | Achat vendeur proche depuis composants manquants |
---
@@ -654,7 +696,7 @@ Ces commandes sont plutôt serveur/admin/debug ou trop dangereuses pour une UI u
| 10 | Ventes / Open items bridge-first | Migration de commandes déjà exposées côté addon | Moyenne | À faire |
| 11 | Enchantements d'items | UI dédiée et garde-fous | Moyenne | À étudier |
| 12 | Trainer / Maintenance extras | UI maintenance | Moyenne/Basse | À faire |
-| 13 | Items avancés banque/guild bank/buy | Extensions inventaire | Basse/Moyenne | À faire |
+| 13 | Items avancés banque/guild bank/buy | Extensions inventaire | Basse/Moyenne | Fait/partiel |
---
@@ -676,9 +718,9 @@ Ces commandes sont plutôt serveur/admin/debug ou trop dangereuses pour une UI u
## Point logique suivant
-Le prochain bloc logique conseillé est **Roll + migration optionnelle des ventes/open items vers bridge + étude des enchantements d'items**.
+Le prochain bloc logique conseillé est **Roll + migration optionnelle des ventes/open items vers bridge + retrait BDG + étude des enchantements d'items**.
-Raison : les blocs RTI, Pull Control, Combat Strategies, Disperse, Loot Rules, Character Info, Profession Recipes, Craft et Loot Master couvrent maintenant le ciblage, le pull, les stratégies combat, le positionnement collectif, les profils de loot, les compétences, les recettes métier, le craft et la préparation du master loot. Après vérification du code actuel, la vente grise, la vente vendor et `open items` existent déjà côté addon, mais restent en legacy whisper et ne sont pas encore bridge-first.
+Raison : les blocs RTI, Pull Control, Combat Strategies, Disperse, Loot Rules, Character Info, Reputations, Monnaies, Profession Recipes, Craft, Loot Master et Items avancés banque/guild bank/buy couvrent maintenant le ciblage, le pull, les stratégies combat, le positionnement collectif, les profils de loot, les compétences, les réputations, les monnaies, les recettes métier, le craft, la préparation du master loot et les actions banque/achat principales. Après vérification du code actuel, la vente grise, la vente vendor et `open items` existent déjà côté addon, mais restent en legacy whisper et ne sont pas encore bridge-first.
État précis des commandes items/loot ponctuelles :
@@ -687,6 +729,9 @@ Raison : les blocs RTI, Pull Control, Combat Strategies, Disperse, Loot Rules, C
- `s *` : Déjà présent côté addon, legacy whisper, pas encore bridge-first ;
- `s vendor` : Déjà présent côté addon inventaire, legacy whisper item par item, pas encore bridge-first ;
- `open items` : Déjà présent côté addon, legacy whisper ;
+- banque bot : Consultation, dépôt et retrait via bridge ;
+- banque de guilde bot : Consultation et dépôt via bridge, retrait à étudier ;
+- achat vendeur : Disponible via bridge depuis les composants manquants de recette ;
- enchantement d'un item précis : manquant / à étudier avec une UI dédiée ;
- `ll [item]` / `ll -[item]` : manquant, amélioration possible depuis l'inventaire.
@@ -696,6 +741,7 @@ Proposition de prochaine itération, plus tard :
- ne pas recréer de boutons `s *`, `s vendor` ou `open items`, car ils existent déjà côté addon ;
- décider si les ventes existantes doivent passer par un endpoint dédié ou par un endpoint existant whitelisté ;
- éviter toute vente automatique dangereuse sans action explicite de l'utilisateur ;
+- décider si le retrait BDG doit être exposé, et avec quels garde-fous ;
- étudier une UI d'enchantement d'item séparée du craft métier classique, car elle doit cibler un item et prévoir plus de garde-fous ;
- garder les commandes informatives manuelles inchangées ;
- noter que `Quest` et `Skill` devront être remplacés plus tard par `Disenchant`, ou devenir de vrais profils ajoutés côté `mod-playerbots` avant exposition définitive.