From 0f424974949b9ab5a12cfde382b8c9d0d06e525d Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Sun, 3 May 2026 21:04:44 +0200 Subject: [PATCH] Add bridge-based profession recipe crafting UI --- Core/MultiBotComm.lua | 78 ++++++++++++++++++ Locales/MultiBotAceLocale-deDE.lua | 29 ++++++- Locales/MultiBotAceLocale-enGB.lua | 27 ++++++ Locales/MultiBotAceLocale-enUS.lua | 28 +++++++ Locales/MultiBotAceLocale-esES.lua | 27 ++++++ Locales/MultiBotAceLocale-frFR.lua | 27 ++++++ Locales/MultiBotAceLocale-koKR.lua | 27 ++++++ Locales/MultiBotAceLocale-ruRU.lua | 27 ++++++ Locales/MultiBotAceLocale-zhCN.lua | 75 +++++++++++------ README.md | 3 +- TODO.md | 17 ++-- UI/MultiBotCharacterInfoFrame.lua | 127 ++++++++++++++++++++++++++++- 12 files changed, 459 insertions(+), 33 deletions(-) diff --git a/Core/MultiBotComm.lua b/Core/MultiBotComm.lua index a93d474..5868c20 100644 --- a/Core/MultiBotComm.lua +++ b/Core/MultiBotComm.lua @@ -121,6 +121,8 @@ local function ensureBridgeState() state.professionRecipes = state.professionRecipes or {} state.professionRecipeSeq = state.professionRecipeSeq or 0 state.professionRecipeActive = state.professionRecipeActive or nil + state.professionRecipeCraftSeq = state.professionRecipeCraftSeq or 0 + state.professionRecipeCrafts = state.professionRecipeCrafts or {} state.outfitSeq = state.outfitSeq or 0 state.outfitActive = state.outfitActive or nil state.outfitCommands = state.outfitCommands or {} @@ -622,6 +624,35 @@ function Comm.RequestProfessionRecipes(name, skillId) return true end +function Comm.RunProfessionRecipeCraft(name, skillId, spellId, itemId) + local state = ensureBridgeState() + name = trim(name) + skillId = tonumber(skillId or 0) or 0 + spellId = tonumber(spellId or 0) or 0 + itemId = tonumber(itemId or 0) or 0 + if name == "" or skillId <= 0 or spellId <= 0 or itemId < 0 or not state.connected then + return false + end + + state.professionRecipeCraftSeq = (tonumber(state.professionRecipeCraftSeq) or 0) + 1 + local token = tostring(math.floor(safeNow() * 1000)) .. "-craft-" .. tostring(state.professionRecipeCraftSeq) + state.professionRecipeCrafts[token] = { + botName = name, + botNameKey = string.lower(name), + skillId = skillId, + spellId = spellId, + itemId = itemId, + startedAt = safeNow(), + } + + if not Comm.Send("RUN", "CRAFT_RECIPE~" .. name .. "~" .. token .. "~" .. skillId .. "~" .. spellId .. "~" .. itemId) then + state.professionRecipeCrafts[token] = nil + return false + end + + return token +end + function Comm.MarkDisconnected(reason) local state = ensureBridgeState() state.connected = false @@ -632,6 +663,7 @@ function Comm.MarkDisconnected(reason) state.spellbookActive = nil state.botSkillActive = nil state.professionRecipeActive = nil + state.professionRecipeCrafts = {} state.outfitActive = nil state.outfitCommands = {} end @@ -1411,6 +1443,45 @@ function Comm.ApplyOutfitCommandPayload(payload) return true end +function Comm.ApplyProfessionRecipeCraftPayload(payload) + local botName, rest = splitOnce(payload or "", "~") + local token, rest2 = splitOnce(rest or "", "~") + local skillId, rest3 = splitOnce(rest2 or "", "~") + local spellId, rest4 = splitOnce(rest3 or "", "~") + local itemId, rest5 = splitOnce(rest4 or "", "~") + local result, reason = splitOnce(rest5 or "", "~") + + botName = trim(urlDecodeField(botName)) + token = trim(token) + skillId = tonumber(skillId or "0") or 0 + spellId = tonumber(spellId or "0") or 0 + itemId = tonumber(itemId or "0") or 0 + result = trim(result) + reason = trim(urlDecodeField(reason)) + + local state = ensureBridgeState() + local command = state.professionRecipeCrafts and state.professionRecipeCrafts[token] or nil + if not command then + return false + end + + command.botName = botName ~= "" and botName or command.botName + command.botNameKey = string.lower(command.botName or "") + command.skillId = skillId > 0 and skillId or command.skillId + command.spellId = spellId > 0 and spellId or command.spellId + command.itemId = itemId >= 0 and itemId or command.itemId + command.result = result + command.reason = reason + + if MultiBot.OnBridgeProfessionRecipeCraftResult then + MultiBot.OnBridgeProfessionRecipeCraftResult(command.botName, command.skillId, command.spellId, command.itemId, result, reason, command) + end + + state.professionRecipeCrafts[token] = nil + debugPrint("ADDON:RX", "PROFESSION_RECIPE_CRAFT", command.botName, command.skillId, command.spellId, result, reason) + return true +end + function Comm.ApplyGlyphsBeginPayload(payload) local botName, token = splitOnce(payload or "", "~") botName = trim(urlDecodeField(botName)) @@ -2160,6 +2231,12 @@ function Comm.HandleAddonMessage(prefix, message, distribution, sender) return true end + if opcode == "PROFESSION_RECIPE_CRAFT" then + state.connected = true + state.lastError = nil + return Comm.ApplyProfessionRecipeCraftPayload(payload) + end + if opcode == "RTI_ACK" then state.connected = true state.lastError = nil @@ -2289,6 +2366,7 @@ function Comm.OnPlayerEnteringWorld() state.botSkillActive = nil state.professionRecipes = {} state.professionRecipeActive = nil + state.professionRecipeCrafts = {} state.outfitActive = nil state.outfitCommands = {} Comm.MarkDisconnected(nil) diff --git a/Locales/MultiBotAceLocale-deDE.lua b/Locales/MultiBotAceLocale-deDE.lua index 3863b88..5b33ccb 100644 --- a/Locales/MultiBotAceLocale-deDE.lua +++ b/Locales/MultiBotAceLocale-deDE.lua @@ -46,6 +46,33 @@ local deDEValues = { ["character.info"] = "Charakterinformationen", ["character.skills"] = "Fertigkeiten", ["profession.recipes.loading"] = "Wird geladen...", + ["profession.recipes.craft"] = "Herstellen", + ["profession.recipes.craft.tooltip"] = "Fordert den Bot auf, dieses Rezept einmal herzustellen.", + ["profession.recipes.craft.pending"] = "Herstellung angefordert...", + ["profession.recipes.craft.ok"] = "Herstellung gestartet.", + ["profession.recipes.craft.failed"] = "Herstellungsanfrage fehlgeschlagen.", + ["profession.recipes.craft.err"] = "Herstellung fehlgeschlagen: %s", + ["profession.recipes.craft.reason.NO_BOT"] = "Bot nicht gefunden.", + ["profession.recipes.craft.reason.NO_AI"] = "Bot-KI nicht verfügbar.", + ["profession.recipes.craft.reason.BAD_REQUEST"] = "Ungültige Rezeptanfrage.", + ["profession.recipes.craft.reason.UNKNOWN_RECIPE"] = "Rezept ist diesem Bot nicht bekannt.", + ["profession.recipes.craft.reason.SKILL_MISMATCH"] = "Rezept passt nicht zu diesem Beruf.", + ["profession.recipes.craft.reason.BAD_RECIPE"] = "Ungültiges Rezept.", + ["profession.recipes.craft.reason.ITEM_MISMATCH"] = "Rezeptergebnis stimmt nicht überein.", + ["profession.recipes.craft.reason.NO_MATERIALS"] = "Materialien fehlen.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS"] = "Benötigte Handwerksstation nicht in der Nähe.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS.cooking"] = "Du musst in der Nähe eines Kochfeuers sein.", + ["profession.recipes.craft.reason.REQUIRES_AREA"] = "Das kann hier nicht hergestellt werden.", + ["profession.recipes.craft.reason.MISSING_TOOLS"] = "Benötigtes Werkzeug fehlt oder ist nicht angelegt.", + ["profession.recipes.craft.reason.MOVING"] = "Der Bot muss stehen bleiben.", + ["profession.recipes.craft.reason.NOT_STANDING"] = "Der Bot musste aufstehen. Erneut versuchen.", + ["profession.recipes.craft.reason.NOT_READY"] = "Das Rezept ist noch nicht bereit.", + ["profession.recipes.craft.reason.OUT_OF_RANGE"] = "Der Bot ist außer Reichweite.", + ["profession.recipes.craft.reason.TRY_AGAIN"] = "Gleich erneut versuchen.", + ["profession.recipes.craft.reason.IN_FLIGHT"] = "Der Bot kann im Flug nicht herstellen.", + ["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.skills.count"] = "Fertigkeit(en)", ["character.skills.loading"] = "Wird geladen...", ["character.skills.category.class"] = "Klassenfertigkeiten", @@ -396,7 +423,7 @@ local deDEValues = { ["tips.main.creator"] = "Creator-Switch\n|cffffffffDieser Schalter aktiviert oder deaktiviert die Creator-Control.|r\n\n|cffff0000Linksklicken um die Creator-Control ein- oder auszuschalten|r\n|cff999999(Ausführreihenfolge: System)|r", ["tips.main.beast"] = "Beastmaster-Switch\n|cffffffffDieser Schalter aktiviert oder deaktiviert die Beastmaster-Control.\nDie Beastmaster-Control ist für Mod-NPC-Beastmaster des Azerothcore.\nMod-NPC-Beastmaster erlaubt es jedem Character eine Bestie wie Jäger zu haben.\nDein Character kann die notwendigen Zauber bei 'White Fang' erlernen.\nDer GameMaster muss 'White Fang' in der Spielwelt platzieren.\n|cffff0000Linksklicken um die Beastmaster-Control ein- oder auszuschalten|r\n|cff999999(Ausführreihenfolge: System)|r", ["tips.main.disperse"] = "Disperse-Switch\n|cffffffffDieser Schalter zeigt oder versteckt die Disperse-Control.|r\n\n|cffff0000Linksklicken um die Disperse-Control ein- oder auszuschalten|r\n|cff999999(Ausführreihenfolge: System)|r", - ["tips.main.loot"] = "Beuteregeln-Switch\n|cffffffffDieser Schalter zeigt oder versteckt die Beuteregeln-Control.|r\n\n|cffff0000Linksklicken um die Beuteregeln-Control ein- oder auszuschalten|r\n|cff999999(Ausführreihenfolge: System)|r", + ["tips.main.loot"] = "Beuteregeln-Switch\n|cffffffffDieser Schalter zeigt oder versteckt die Beuteregeln-Control.|r\n\n|cffff0000Linksklicken um die Beuteregeln-Control ein- oder auszuschalten|r\n|cff999999(Ausführreihenfolge: System)|r", ["tips.main.expand"] = "Expand-Switch\n|cffffffffDiese Schaltfläche expandiert oder reduziert die Stay-Follow-Control.\n|cffff0000Linksklicken um die Stay-Follow-Control zu expandieren oder reduzieren|r\n|cff999999(Ausführreihenfolge: System)|r", ["tips.main.release"] = "Auto-Release\n|cffffffffDiese Erweiterung erkennt wenn ein Bot stirbt.\nTote Bots werden automatisch freigelassen und herbeigerufen.\nDadurch werden die Bots innerhalb von Sekunden wiederbelebt.|r\n\n|cffff0000Linksklicken um Auto-Release ein- oder auszuschalten|r\n|cff999999(Ausführreihenfolge: System)|r", ["tips.main.stats"] = "Auto-Stats\n|cffffffffDiese Erweiterung visualisiert die Werte vom Stats-Kommando.\nDie Stats-Werte aktualisieren sich alle 45 Sekunden.|r\n\n|cffff0000Linksklicken um Auto-Stats ein- oder auszuschalten|r\n|cff999999(Ausführreihenfolge: System)|r", diff --git a/Locales/MultiBotAceLocale-enGB.lua b/Locales/MultiBotAceLocale-enGB.lua index b71091b..ba28489 100644 --- a/Locales/MultiBotAceLocale-enGB.lua +++ b/Locales/MultiBotAceLocale-enGB.lua @@ -46,6 +46,33 @@ local enGBValues = { ["character.info"] = "Character Info", ["character.skills"] = "Skills", ["profession.recipes.loading"] = "Loading...", + ["profession.recipes.craft"] = "Craft", + ["profession.recipes.craft.tooltip"] = "Ask the bot to craft this recipe once.", + ["profession.recipes.craft.pending"] = "Craft requested...", + ["profession.recipes.craft.ok"] = "Craft started.", + ["profession.recipes.craft.failed"] = "Craft request failed.", + ["profession.recipes.craft.err"] = "Craft failed: %s", + ["profession.recipes.craft.reason.NO_BOT"] = "Bot not found.", + ["profession.recipes.craft.reason.NO_AI"] = "Bot AI unavailable.", + ["profession.recipes.craft.reason.BAD_REQUEST"] = "Invalid recipe request.", + ["profession.recipes.craft.reason.UNKNOWN_RECIPE"] = "Recipe not known by this bot.", + ["profession.recipes.craft.reason.SKILL_MISMATCH"] = "Recipe does not match this profession.", + ["profession.recipes.craft.reason.BAD_RECIPE"] = "Invalid recipe.", + ["profession.recipes.craft.reason.ITEM_MISMATCH"] = "Recipe result mismatch.", + ["profession.recipes.craft.reason.NO_MATERIALS"] = "Missing materials.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS"] = "Required crafting station not found nearby.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS.cooking"] = "You must be near a cooking fire.", + ["profession.recipes.craft.reason.REQUIRES_AREA"] = "You cannot craft that here.", + ["profession.recipes.craft.reason.MISSING_TOOLS"] = "Required tool missing or not equipped.", + ["profession.recipes.craft.reason.MOVING"] = "The bot must stop moving.", + ["profession.recipes.craft.reason.NOT_STANDING"] = "The bot had to stand up. Try again.", + ["profession.recipes.craft.reason.NOT_READY"] = "The recipe is not ready yet.", + ["profession.recipes.craft.reason.OUT_OF_RANGE"] = "The bot is out of range.", + ["profession.recipes.craft.reason.TRY_AGAIN"] = "Try again in a moment.", + ["profession.recipes.craft.reason.IN_FLIGHT"] = "The bot cannot craft while flying.", + ["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.skills.count"] = "skill(s)", ["character.skills.loading"] = "Loading...", ["character.skills.category.class"] = "Class Skills", diff --git a/Locales/MultiBotAceLocale-enUS.lua b/Locales/MultiBotAceLocale-enUS.lua index 84aa617..508661f 100644 --- a/Locales/MultiBotAceLocale-enUS.lua +++ b/Locales/MultiBotAceLocale-enUS.lua @@ -46,6 +46,34 @@ local enUSValues = { ["character.info"] = "Character Info", ["character.skills"] = "Skills", ["profession.recipes.loading"] = "Loading...", + ["profession.recipes.craft"] = "Craft", + ["profession.recipes.craft"] = "Craft", + ["profession.recipes.craft.tooltip"] = "Ask the bot to craft this recipe once.", + ["profession.recipes.craft.pending"] = "Craft requested...", + ["profession.recipes.craft.ok"] = "Craft started.", + ["profession.recipes.craft.failed"] = "Craft request failed.", + ["profession.recipes.craft.err"] = "Craft failed: %s", + ["profession.recipes.craft.reason.NO_BOT"] = "Bot not found.", + ["profession.recipes.craft.reason.NO_AI"] = "Bot AI unavailable.", + ["profession.recipes.craft.reason.BAD_REQUEST"] = "Invalid recipe request.", + ["profession.recipes.craft.reason.UNKNOWN_RECIPE"] = "Recipe not known by this bot.", + ["profession.recipes.craft.reason.SKILL_MISMATCH"] = "Recipe does not match this profession.", + ["profession.recipes.craft.reason.BAD_RECIPE"] = "Invalid recipe.", + ["profession.recipes.craft.reason.ITEM_MISMATCH"] = "Recipe result mismatch.", + ["profession.recipes.craft.reason.NO_MATERIALS"] = "Missing materials.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS"] = "Required crafting station not found nearby.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS.cooking"] = "You must be near a cooking fire.", + ["profession.recipes.craft.reason.REQUIRES_AREA"] = "You cannot craft that here.", + ["profession.recipes.craft.reason.MISSING_TOOLS"] = "Required tool missing or not equipped.", + ["profession.recipes.craft.reason.MOVING"] = "The bot must stop moving.", + ["profession.recipes.craft.reason.NOT_STANDING"] = "The bot had to stand up. Try again.", + ["profession.recipes.craft.reason.NOT_READY"] = "The recipe is not ready yet.", + ["profession.recipes.craft.reason.OUT_OF_RANGE"] = "The bot is out of range.", + ["profession.recipes.craft.reason.TRY_AGAIN"] = "Try again in a moment.", + ["profession.recipes.craft.reason.IN_FLIGHT"] = "The bot cannot craft while flying.", + ["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.skills.count"] = "skill(s)", ["character.skills.loading"] = "Loading...", ["character.skills.category.class"] = "Class Skills", diff --git a/Locales/MultiBotAceLocale-esES.lua b/Locales/MultiBotAceLocale-esES.lua index 49132c3..c077e27 100644 --- a/Locales/MultiBotAceLocale-esES.lua +++ b/Locales/MultiBotAceLocale-esES.lua @@ -46,6 +46,33 @@ local esESValues = { ["character.info"] = "Información del personaje", ["character.skills"] = "Habilidades", ["profession.recipes.loading"] = "Cargando...", + ["profession.recipes.craft"] = "Crear", + ["profession.recipes.craft.tooltip"] = "Pide al bot que cree esta receta una vez.", + ["profession.recipes.craft.pending"] = "Creación solicitada...", + ["profession.recipes.craft.ok"] = "Creación iniciada.", + ["profession.recipes.craft.failed"] = "La solicitud de creación falló.", + ["profession.recipes.craft.err"] = "Creación fallida: %s", + ["profession.recipes.craft.reason.NO_BOT"] = "Bot no encontrado.", + ["profession.recipes.craft.reason.NO_AI"] = "IA del bot no disponible.", + ["profession.recipes.craft.reason.BAD_REQUEST"] = "Solicitud de receta no válida.", + ["profession.recipes.craft.reason.UNKNOWN_RECIPE"] = "El bot no conoce esta receta.", + ["profession.recipes.craft.reason.SKILL_MISMATCH"] = "La receta no coincide con esta profesión.", + ["profession.recipes.craft.reason.BAD_RECIPE"] = "Receta no válida.", + ["profession.recipes.craft.reason.ITEM_MISMATCH"] = "El resultado de la receta no coincide.", + ["profession.recipes.craft.reason.NO_MATERIALS"] = "Faltan materiales.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS"] = "No hay una estación de creación requerida cerca.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS.cooking"] = "Debes estar cerca de un fuego de cocina.", + ["profession.recipes.craft.reason.REQUIRES_AREA"] = "No puedes crear eso aquí.", + ["profession.recipes.craft.reason.MISSING_TOOLS"] = "Falta una herramienta requerida o no está equipada.", + ["profession.recipes.craft.reason.MOVING"] = "El bot debe dejar de moverse.", + ["profession.recipes.craft.reason.NOT_STANDING"] = "El bot tuvo que levantarse. Inténtalo de nuevo.", + ["profession.recipes.craft.reason.NOT_READY"] = "La receta aún no está lista.", + ["profession.recipes.craft.reason.OUT_OF_RANGE"] = "El bot está fuera de alcance.", + ["profession.recipes.craft.reason.TRY_AGAIN"] = "Inténtalo de nuevo en un momento.", + ["profession.recipes.craft.reason.IN_FLIGHT"] = "El bot no puede crear mientras vuela.", + ["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.skills.count"] = "habilidad(es)", ["character.skills.loading"] = "Cargando...", ["character.skills.category.class"] = "Habilidades de clase", diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index f216735..7d540d7 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -46,6 +46,33 @@ local frFRValues = { ["character.info"] = "Infos personnage", ["character.skills"] = "Compétences", ["profession.recipes.loading"] = "Chargement...", + ["profession.recipes.craft"] = "Créer", + ["profession.recipes.craft.tooltip"] = "Demande au bot de créer cette recette une fois.", + ["profession.recipes.craft.pending"] = "Création demandée...", + ["profession.recipes.craft.ok"] = "Création lancée.", + ["profession.recipes.craft.failed"] = "La demande de création a échoué.", + ["profession.recipes.craft.err"] = "Création échouée : %s", + ["profession.recipes.craft.reason.NO_BOT"] = "Bot introuvable.", + ["profession.recipes.craft.reason.NO_AI"] = "IA du bot indisponible.", + ["profession.recipes.craft.reason.BAD_REQUEST"] = "Demande de recette invalide.", + ["profession.recipes.craft.reason.UNKNOWN_RECIPE"] = "Recette inconnue de ce bot.", + ["profession.recipes.craft.reason.SKILL_MISMATCH"] = "La recette ne correspond pas à ce métier.", + ["profession.recipes.craft.reason.BAD_RECIPE"] = "Recette invalide.", + ["profession.recipes.craft.reason.ITEM_MISMATCH"] = "Résultat de recette incohérent.", + ["profession.recipes.craft.reason.NO_MATERIALS"] = "Composants manquants.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS"] = "Poste de fabrication requis introuvable à proximité.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS.cooking"] = "Il faut être près d'un feu de cuisine.", + ["profession.recipes.craft.reason.REQUIRES_AREA"] = "Impossible de créer cette recette ici.", + ["profession.recipes.craft.reason.MISSING_TOOLS"] = "Outil requis manquant ou non équipé.", + ["profession.recipes.craft.reason.MOVING"] = "Le bot doit arrêter de bouger.", + ["profession.recipes.craft.reason.NOT_STANDING"] = "Le bot devait se lever. Réessaie.", + ["profession.recipes.craft.reason.NOT_READY"] = "La recette n'est pas encore prête.", + ["profession.recipes.craft.reason.OUT_OF_RANGE"] = "Le bot est hors de portée.", + ["profession.recipes.craft.reason.TRY_AGAIN"] = "Réessaie dans un instant.", + ["profession.recipes.craft.reason.IN_FLIGHT"] = "Le bot ne peut pas créer en vol.", + ["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.skills.count"] = "compétence(s)", ["character.skills.loading"] = "Chargement...", ["character.skills.category.class"] = "Compétences de classe", diff --git a/Locales/MultiBotAceLocale-koKR.lua b/Locales/MultiBotAceLocale-koKR.lua index 667e9a2..be63ff6 100644 --- a/Locales/MultiBotAceLocale-koKR.lua +++ b/Locales/MultiBotAceLocale-koKR.lua @@ -46,6 +46,33 @@ local koKRValues = { ["character.info"] = "캐릭터 정보", ["character.skills"] = "기술", ["profession.recipes.loading"] = "불러오는 중...", + ["profession.recipes.craft"] = "제작", + ["profession.recipes.craft.tooltip"] = "봇에게 이 제조법을 한 번 제작하도록 요청합니다.", + ["profession.recipes.craft.pending"] = "제작 요청됨...", + ["profession.recipes.craft.ok"] = "제작을 시작했습니다.", + ["profession.recipes.craft.failed"] = "제작 요청에 실패했습니다.", + ["profession.recipes.craft.err"] = "제작 실패: %s", + ["profession.recipes.craft.reason.NO_BOT"] = "봇을 찾을 수 없습니다.", + ["profession.recipes.craft.reason.NO_AI"] = "봇 AI를 사용할 수 없습니다.", + ["profession.recipes.craft.reason.BAD_REQUEST"] = "잘못된 제조법 요청입니다.", + ["profession.recipes.craft.reason.UNKNOWN_RECIPE"] = "이 봇이 모르는 제조법입니다.", + ["profession.recipes.craft.reason.SKILL_MISMATCH"] = "제조법이 이 전문 기술과 맞지 않습니다.", + ["profession.recipes.craft.reason.BAD_RECIPE"] = "잘못된 제조법입니다.", + ["profession.recipes.craft.reason.ITEM_MISMATCH"] = "제조법 결과가 일치하지 않습니다.", + ["profession.recipes.craft.reason.NO_MATERIALS"] = "재료가 부족합니다.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS"] = "필요한 제작 장소가 근처에 없습니다.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS.cooking"] = "요리용 불 근처에 있어야 합니다.", + ["profession.recipes.craft.reason.REQUIRES_AREA"] = "여기서는 제작할 수 없습니다.", + ["profession.recipes.craft.reason.MISSING_TOOLS"] = "필요한 도구가 없거나 장착되어 있지 않습니다.", + ["profession.recipes.craft.reason.MOVING"] = "봇이 움직임을 멈춰야 합니다.", + ["profession.recipes.craft.reason.NOT_STANDING"] = "봇이 일어서야 했습니다. 다시 시도하세요.", + ["profession.recipes.craft.reason.NOT_READY"] = "제조법을 아직 사용할 수 없습니다.", + ["profession.recipes.craft.reason.OUT_OF_RANGE"] = "봇이 사거리 밖에 있습니다.", + ["profession.recipes.craft.reason.TRY_AGAIN"] = "잠시 후 다시 시도하세요.", + ["profession.recipes.craft.reason.IN_FLIGHT"] = "봇은 비행 중 제작할 수 없습니다.", + ["profession.recipes.craft.reason.LOST_CONTROL"] = "봇이 지금 행동할 수 없습니다.", + ["profession.recipes.craft.reason.CHANNELING"] = "봇이 이미 주문을 시전 중입니다.", + ["profession.recipes.craft.reason.cast_code"] = "서버가 시전을 거부했습니다(코드 %s).", ["character.skills.count"] = "기술", ["character.skills.loading"] = "불러오는 중...", ["character.skills.category.class"] = "직업 기술", diff --git a/Locales/MultiBotAceLocale-ruRU.lua b/Locales/MultiBotAceLocale-ruRU.lua index 32d194e..2182fad 100644 --- a/Locales/MultiBotAceLocale-ruRU.lua +++ b/Locales/MultiBotAceLocale-ruRU.lua @@ -46,6 +46,33 @@ local ruRUValues = { ["character.info"] = "Информация о персонаже", ["character.skills"] = "Навыки", ["profession.recipes.loading"] = "Загрузка...", + ["profession.recipes.craft"] = "Создать", + ["profession.recipes.craft.tooltip"] = "Попросить бота создать этот рецепт один раз.", + ["profession.recipes.craft.pending"] = "Создание запрошено...", + ["profession.recipes.craft.ok"] = "Создание начато.", + ["profession.recipes.craft.failed"] = "Запрос создания не удался.", + ["profession.recipes.craft.err"] = "Создание не удалось: %s", + ["profession.recipes.craft.reason.NO_BOT"] = "Бот не найден.", + ["profession.recipes.craft.reason.NO_AI"] = "ИИ бота недоступен.", + ["profession.recipes.craft.reason.BAD_REQUEST"] = "Недопустимый запрос рецепта.", + ["profession.recipes.craft.reason.UNKNOWN_RECIPE"] = "Этот бот не знает рецепт.", + ["profession.recipes.craft.reason.SKILL_MISMATCH"] = "Рецепт не соответствует этой профессии.", + ["profession.recipes.craft.reason.BAD_RECIPE"] = "Недопустимый рецепт.", + ["profession.recipes.craft.reason.ITEM_MISMATCH"] = "Результат рецепта не совпадает.", + ["profession.recipes.craft.reason.NO_MATERIALS"] = "Не хватает материалов.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS"] = "Рядом нет требуемой ремесленной станции.", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS.cooking"] = "Нужно быть рядом с костром для готовки.", + ["profession.recipes.craft.reason.REQUIRES_AREA"] = "Здесь нельзя это создать.", + ["profession.recipes.craft.reason.MISSING_TOOLS"] = "Нет требуемого инструмента или он не экипирован.", + ["profession.recipes.craft.reason.MOVING"] = "Бот должен остановиться.", + ["profession.recipes.craft.reason.NOT_STANDING"] = "Бот должен был встать. Попробуй снова.", + ["profession.recipes.craft.reason.NOT_READY"] = "Рецепт еще не готов.", + ["profession.recipes.craft.reason.OUT_OF_RANGE"] = "Бот вне радиуса действия.", + ["profession.recipes.craft.reason.TRY_AGAIN"] = "Попробуй снова через мгновение.", + ["profession.recipes.craft.reason.IN_FLIGHT"] = "Бот не может создавать предметы в полете.", + ["profession.recipes.craft.reason.LOST_CONTROL"] = "Бот сейчас не может действовать.", + ["profession.recipes.craft.reason.CHANNELING"] = "Бот уже поддерживает заклинание.", + ["profession.recipes.craft.reason.cast_code"] = "Сервер отклонил применение (код %s).", ["character.skills.count"] = "навык(и)", ["character.skills.loading"] = "Загрузка...", ["character.skills.category.class"] = "Классовые навыки", diff --git a/Locales/MultiBotAceLocale-zhCN.lua b/Locales/MultiBotAceLocale-zhCN.lua index 30c5290..6eb8bf4 100644 --- a/Locales/MultiBotAceLocale-zhCN.lua +++ b/Locales/MultiBotAceLocale-zhCN.lua @@ -41,30 +41,57 @@ local zhCNValues = { ["lootmaster.profession.fishing"] = "钓鱼", ["lootmaster.profession.firstaid"] = "急救", ["lootmaster.gear_score"] = "GearScore:%d", - ["profession.recipes"] = "配方", - ["profession.recipes.count"] = "已知配方", - ["character.info"] = "角色信息", - ["character.skills"] = "技能", - ["profession.recipes.loading"] = "加载中...", - ["character.skills.count"] = "技能", - ["character.skills.loading"] = "加载中...", - ["character.skills.category.class"] = "职业技能", - ["character.skills.category.profession"] = "专业技能", - ["character.skills.category.secondary"] = "辅助专业", - ["character.skills.category.weapon"] = "武器技能", - ["character.skills.category.armor"] = "护甲", - ["lootmaster.profession.alchemy"] = "炼金术", - ["lootmaster.profession.blacksmithing"] = "锻造", - ["lootmaster.profession.enchanting"] = "附魔", - ["lootmaster.profession.herbalism"] = "草药学", - ["lootmaster.profession.inscription"] = "铭文", - ["lootmaster.profession.leatherworking"] = "制皮", - ["lootmaster.profession.mining"] = "采矿", - ["lootmaster.profession.skinning"] = "剥皮", - ["lootmaster.profession.tailoring"] = "裁缝", - ["character.skills.skill.arms"] = "武器", - ["character.skills.skill.fury"] = "狂怒", - ["character.skills.skill.protection"] = "防护", + ["profession.recipes"] = "配方", + ["profession.recipes.count"] = "已知配方", + ["character.info"] = "角色信息", + ["character.skills"] = "技能", + ["profession.recipes.loading"] = "加载中...", + ["profession.recipes.craft"] = "制作", + ["profession.recipes.craft.tooltip"] = "让机器人制作一次此配方。", + ["profession.recipes.craft.pending"] = "已请求制作...", + ["profession.recipes.craft.ok"] = "已开始制作。", + ["profession.recipes.craft.failed"] = "制作请求失败。", + ["profession.recipes.craft.err"] = "制作失败:%s", + ["profession.recipes.craft.reason.NO_BOT"] = "未找到机器人。", + ["profession.recipes.craft.reason.NO_AI"] = "机器人 AI 不可用。", + ["profession.recipes.craft.reason.BAD_REQUEST"] = "无效的配方请求。", + ["profession.recipes.craft.reason.UNKNOWN_RECIPE"] = "该机器人不会此配方。", + ["profession.recipes.craft.reason.SKILL_MISMATCH"] = "配方与此专业不匹配。", + ["profession.recipes.craft.reason.BAD_RECIPE"] = "无效的配方。", + ["profession.recipes.craft.reason.ITEM_MISMATCH"] = "配方产物不匹配。", + ["profession.recipes.craft.reason.NO_MATERIALS"] = "缺少材料。", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS"] = "附近没有所需的制作设施。", + ["profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS.cooking"] = "你必须靠近烹饪火。", + ["profession.recipes.craft.reason.REQUIRES_AREA"] = "你不能在这里制作。", + ["profession.recipes.craft.reason.MISSING_TOOLS"] = "缺少所需工具或未装备。", + ["profession.recipes.craft.reason.MOVING"] = "机器人必须停止移动。", + ["profession.recipes.craft.reason.NOT_STANDING"] = "机器人需要先站起来。请重试。", + ["profession.recipes.craft.reason.NOT_READY"] = "该配方尚未就绪。", + ["profession.recipes.craft.reason.OUT_OF_RANGE"] = "机器人距离太远。", + ["profession.recipes.craft.reason.TRY_AGAIN"] = "稍后再试。", + ["profession.recipes.craft.reason.IN_FLIGHT"] = "机器人飞行时无法制作。", + ["profession.recipes.craft.reason.LOST_CONTROL"] = "机器人现在无法行动。", + ["profession.recipes.craft.reason.CHANNELING"] = "机器人已经在引导法术。", + ["profession.recipes.craft.reason.cast_code"] = "服务器拒绝施法(代码 %s)。", + ["character.skills.count"] = "技能", + ["character.skills.loading"] = "加载中...", + ["character.skills.category.class"] = "职业技能", + ["character.skills.category.profession"] = "专业技能", + ["character.skills.category.secondary"] = "辅助专业", + ["character.skills.category.weapon"] = "武器技能", + ["character.skills.category.armor"] = "护甲", + ["lootmaster.profession.alchemy"] = "炼金术", + ["lootmaster.profession.blacksmithing"] = "锻造", + ["lootmaster.profession.enchanting"] = "附魔", + ["lootmaster.profession.herbalism"] = "草药学", + ["lootmaster.profession.inscription"] = "铭文", + ["lootmaster.profession.leatherworking"] = "制皮", + ["lootmaster.profession.mining"] = "采矿", + ["lootmaster.profession.skinning"] = "剥皮", + ["lootmaster.profession.tailoring"] = "裁缝", + ["character.skills.skill.arms"] = "武器", + ["character.skills.skill.fury"] = "狂怒", + ["character.skills.skill.protection"] = "防护", ["tips.units.rti"] = "RTI", ["tips.disperse.main"] = "分散", ["tips.disperse.set"] = "设置分散距离", diff --git a/README.md b/README.md index 46b467a..494affd 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ GET~GLYPHS GET~OUTFITS GET~QUESTS GET~GAMEOBJECTS +RUN~CRAFT_RECIPE RUN~OUTFIT RUN~RTI RUN~COMBAT @@ -170,7 +171,7 @@ The goal is to remove automatic UI-refresh spam. Profession recipe frame - Bridge-first recipe listing opened from Character Info profession and secondary skill rows + Bridge-first recipe listing and recipe crafting opened from Character Info profession and secondary skill rows Glyphs diff --git a/TODO.md b/TODO.md index 06ba72e..20a40b8 100644 --- a/TODO.md +++ b/TODO.md @@ -12,15 +12,12 @@ * Quick bars : ne pas faire apparaître les quick bars pour les joueurs humains. * Raidus : rafraîchir correctement à l'ouverture et à la fermeture. * Raidus : ajouter un bouton pour enlever les bots inconnus et les supprimer aussi des SavedVariables. -* Quêtes : dans la liste des quêtes, corriger les cas où l'ID de quête apparaît à la place du titre. +* Quêtes : dans la liste des quêtes, corriger les cas où l'ID de quête apparaît à la place du titre. quand on rouvre c'est ok * Talents / glyphes : revoir `UI/MultiBotTalent`, car il y a eu des modifications dans le fichier `.conf` de MultiBot. -* Menus déroulants de la main bar : fermer automatiquement les autres menus quand on en ouvre un nouveau. * J'ai l'impression que le disperse ne fait rien -* Quand on fait apparaitre les boutons creator ou/et Maitre des bêtes, les deux boutons Disperse et Règles de buttin ne bougent pas -ce qui fait que quand on ajoute Creator + maitre des betes le bouton attaque tank se retrouve caché par le bouton disperse -* Les tooltips du spellbook sont derrière la frale. * Refaire uns passe pour que toutes les frames respectent le strata de la config. -* Le SpellBook remonte encore quelques spells des métier cuisine et first aid +* 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. ## Informations bot ** Frame faite @@ -99,6 +96,14 @@ ce qui fait que quand on ajoute Creator + maitre des betes le bouton attaque tan * completed ; * all. * Outfits bridge-first. +* Character Info bridge-first. +* Profession recipes bridge-first. +* Craft de recettes métier via bridge `RUN~CRAFT_RECIPE`. +* Messages d'erreur détaillés pour le craft : + * feu de cuisine requis ; + * bot en mouvement ; + * outil / focus requis ; + * recette pas prête ou cast refusé. * RTI bridge-first. * Pull Control bridge-first. * Combat Strategies bridge-first. diff --git a/UI/MultiBotCharacterInfoFrame.lua b/UI/MultiBotCharacterInfoFrame.lua index 1b47fc6..380c9d0 100644 --- a/UI/MultiBotCharacterInfoFrame.lua +++ b/UI/MultiBotCharacterInfoFrame.lua @@ -37,7 +37,10 @@ local CHARACTER_SKILL_VALUE_WIDTH = 72 local RECIPE_FRAME_WIDTH = 340 local RECIPE_FRAME_X = 145 local RECIPE_ROW_WIDTH = 302 -local RECIPE_TEXT_WIDTH = 270 +local RECIPE_CRAFT_BUTTON_WIDTH = 54 +local RECIPE_TEXT_WIDTH = 206 +local RECIPE_REFRESH_DELAY = 3.0 +local COOKING_SKILL_ID = 185 local SKILL_DISPLAY_SPELL_IDS = { [171] = 2259, -- Alchemy @@ -292,6 +295,57 @@ local function buildMaterialsText(recipe) return table.concat(parts, ", ") end +local function getRecipePendingKey(botName, skillId, spellId) + return string.lower(tostring(botName or "")) .. ":" .. tostring(tonumber(skillId or 0) or 0) .. ":" .. tostring(tonumber(spellId or 0) or 0) +end + +local function getCraftReasonText(reason, skillId) + reason = tostring(reason or "") + if reason == "" or reason == "OK" then + return "" + end + + if reason == "REQUIRES_SPELL_FOCUS" and tonumber(skillId or 0) == COOKING_SKILL_ID then + return L("profession.recipes.craft.reason.REQUIRES_SPELL_FOCUS.cooking", "You must be near a cooking fire.") + end + + local castCode = string.match(reason, "^CAST_FAILED_(%d+)$") + if castCode then + return string.format(L("profession.recipes.craft.reason.cast_code", "The server refused the cast (code %s)."), castCode) + end + + return L("profession.recipes.craft.reason." .. reason, reason) +end + +local function scheduleRecipeRefresh(botName, skillId) + if not MultiBot.Comm or not MultiBot.Comm.RequestProfessionRecipes then + return + end + + local function refresh() + local frame = MultiBot.professionRecipeFrame + if frame and not frame:IsShown() then + return + end + + if frame and frame:IsShown() and frame.botName ~= botName then + return + end + + if frame and frame:IsShown() and frame.skill and tonumber(frame.skill.skillId or 0) ~= tonumber(skillId or 0) then + return + end + + MultiBot.Comm.RequestProfessionRecipes(botName, skillId) + end + + if type(MultiBot.TimerAfter) == "function" then + MultiBot.TimerAfter(RECIPE_REFRESH_DELAY, refresh) + else + refresh() + end +end + local function ensureRecipeFrame() if MultiBot.professionRecipeFrame then return MultiBot.professionRecipeFrame @@ -308,6 +362,7 @@ local function ensureRecipeFrame() frame.page = 1 frame.pageSize = 14 frame.recipes = {} + frame.pendingCrafts = {} for i = 1, frame.pageSize do local row = CreateFrame("Button", nil, content) @@ -321,6 +376,44 @@ local function ensureRecipeFrame() row.text = createText(row, "GameFontHighlightSmall", "LEFT", 26, 0) row.text:SetWidth(RECIPE_TEXT_WIDTH) row.text:SetHeight(20) + row.craftButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") + row.craftButton:SetPoint("RIGHT", row, "RIGHT", 0, 0) + row.craftButton:SetWidth(RECIPE_CRAFT_BUTTON_WIDTH) + row.craftButton:SetHeight(20) + row.craftButton:SetText(L("profession.recipes.craft", "Craft")) + row.craftButton:SetScript("OnClick", function() + local recipe = row.recipe + if not recipe or not frame.botName then return end + + local skillId = tonumber(recipe.skillId or (frame.skill and frame.skill.skillId) or 0) or 0 + local spellId = tonumber(recipe.spellId or 0) or 0 + local itemId = tonumber(recipe.itemId or 0) or 0 + local craftable = tonumber(recipe.craftable or 0) or 0 + if skillId <= 0 or spellId <= 0 or craftable <= 0 then + return + end + + if MultiBot.Comm and MultiBot.Comm.RunProfessionRecipeCraft then + local token = MultiBot.Comm.RunProfessionRecipeCraft(frame.botName, skillId, spellId, itemId) + if token then + frame.pendingCrafts[getRecipePendingKey(frame.botName, skillId, spellId)] = true + frame.status:SetText(L("profession.recipes.craft.pending", "Craft requested...")) + frame:render() + else + frame.status:SetText(L("profession.recipes.craft.failed", "Craft request failed.")) + end + end + end) + row.craftButton:SetScript("OnEnter", function(self) + if not GameTooltip then return end + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:AddLine(L("profession.recipes.craft", "Craft")) + GameTooltip:AddLine(L("profession.recipes.craft.tooltip", "Ask the bot to craft this recipe once."), 1, 1, 1, true) + GameTooltip:Show() + end) + row.craftButton:SetScript("OnLeave", function() + if GameTooltip then GameTooltip:Hide() end + end) row:SetScript("OnEnter", function(self) if not self.recipe or not GameTooltip then return end GameTooltip:SetOwner(self, "ANCHOR_RIGHT") @@ -372,10 +465,15 @@ local function ensureRecipeFrame() local name, icon = getSpellDisplay(recipe.spellId) local color = DIFFICULTY_COLORS[recipe.difficulty or ""] or "|cffffffff" local craftable = tonumber(recipe.craftable or 0) or 0 + local pending = self.pendingCrafts[getRecipePendingKey(self.botName, recipe.skillId, recipe.spellId)] row.icon:SetTexture(MultiBot.SafeTexturePath(icon)) row.text:SetText(color .. name .. "|r |cff999999x" .. craftable .. "|r") + row.craftButton:SetText(pending and "..." or L("profession.recipes.craft", "Craft")) + setButtonEnabled(row.craftButton, craftable > 0 and tonumber(recipe.spellId or 0) > 0 and not pending) + row.craftButton:Show() row:Show() else + row.craftButton:Hide() row:Hide() end end @@ -655,6 +753,33 @@ function MultiBot.OnBridgeProfessionRecipes(botName, skillId, recipes) ensureRecipeFrame():setRecipes(botName, skill or { name = "Skill " .. tostring(skillId), skillId = skillId }, recipes or {}) end +function MultiBot.OnBridgeProfessionRecipeCraftResult(botName, skillId, spellId, _itemId, result, reason) + local frame = ensureRecipeFrame() + local sameBot = string.lower(tostring(frame.botName or "")) == string.lower(tostring(botName or "")) + local sameSkill = frame.skill and tonumber(frame.skill.skillId or 0) == tonumber(skillId or 0) + + frame.pendingCrafts[getRecipePendingKey(botName, skillId, spellId)] = nil + + if result == "OK" then + if sameBot and sameSkill then + frame.status:SetText(L("profession.recipes.craft.ok", "Craft started.")) + frame:render() + end + scheduleRecipeRefresh(botName, skillId) + return + end + + if sameBot and sameSkill then + local reasonText = getCraftReasonText(reason, skillId) + if reasonText ~= "" then + frame.status:SetText(string.format(L("profession.recipes.craft.err", "Craft failed: %s"), reasonText)) + else + frame.status:SetText(L("profession.recipes.craft.failed", "Craft request failed.")) + end + frame:render() + end +end + function MultiBot.InitializeCharacterInfoFrame() ensureCharacterFrame() ensureRecipeFrame()