From 5cf20fa40c825bd7307c7a8e6deb13a84507ba65 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:34:54 +0200 Subject: [PATCH 01/11] First upload --- Locales/MultiBotAceLocale-frFR.lua | 17 + MultiBot.toc | 1 + UI/MultiBotLootMasterUI.lua | 1384 ++++++++++++++++++++++++++++ 3 files changed, 1402 insertions(+) create mode 100644 UI/MultiBotLootMasterUI.lua diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index 0c4bb3f..9027072 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -4,6 +4,23 @@ if type(register) ~= "function" then end local frFRValues = { + ["lootmaster.title"] = "MultiBot Responsable du butin", + ["lootmaster.refresh"] = "Rafraîchir", + ["lootmaster.assign_to"] = "Attribuer à :", + ["lootmaster.unknown_item"] = "Objet inconnu", + ["lootmaster.unknown_candidate"] = "inconnu", + ["lootmaster.status.ready"] = "Sélectionne le destinataire de l'objet.", + ["lootmaster.status.not_master"] = "Le butin n'est pas en responsable du butin, ou tu n'es pas le responsable.", + ["lootmaster.status.no_loot"] = "Aucun butin disponible.", + ["lootmaster.status.no_candidates"] = "Aucun candidat de butin trouvé pour le loot actuel.", + ["lootmaster.error.not_master"] = "Tu n'es pas le responsable du butin.", + ["lootmaster.error.invalid_candidate"] = "Candidat de butin invalide.", + ["lootmaster.assigned"] = "%s attribué à %s.", + ["lootmaster.no_candidates_for_item"] = "Aucun candidat pour cet objet.", + ["lootmaster.assign"] = "Attribuer", + ["lootmaster.select_bot"] = "Choisir un bot", + ["lootmaster.no_candidates_for_item"] = "Aucun candidat pour cet objet.", + ["lootmaster.unknown_spec"] = "Spec inconnue.", ["tips.units.rti"] = "RTI", ["tips.disperse.main"] = "Disperse", ["tips.disperse.set"] = "Définir la distance de disperse", diff --git a/MultiBot.toc b/MultiBot.toc index e90e6fd..43b65f6 100644 --- a/MultiBot.toc +++ b/MultiBot.toc @@ -94,6 +94,7 @@ UI\MultiBotFleeUI.lua UI\MultiBotUnitsFilterUI.lua UI\MultiBotUnitsRosterUI.lua UI\MultiBotUnitsRootUI.lua +UI\MultiBotLootMasterUI.lua UI\MultiBotBeastUI.lua UI\MultiBotCreatorUI.lua UI\MultiBotMainUI.lua diff --git a/UI/MultiBotLootMasterUI.lua b/UI/MultiBotLootMasterUI.lua new file mode 100644 index 0000000..47a704b --- /dev/null +++ b/UI/MultiBotLootMasterUI.lua @@ -0,0 +1,1384 @@ +-- MultiBotLootMasterUI.lua + +local addonName = ... + +local AceLocale = LibStub and LibStub("AceLocale-3.0", true) +local AceGUI = LibStub and LibStub("AceGUI-3.0", true) +local L = AceLocale and AceLocale:GetLocale("MultiBot", true) + +if not L then + L = setmetatable({}, { + __index = function(_, key) + return key + end, + }) +end + +local LootMasterUI = {} +local assignedLootKeys = {} +local assignedLootSlots = {} + +local FRAME_WIDTH = 500 +local FRAME_HEIGHT = 420 +local ITEM_ROW_HEIGHT = 64 +local DROPDOWN_WIDTH = 190 +local MAX_MASTER_LOOT_CANDIDATES = 40 +local CLASS_ICON_TEXTURE = "Interface\\GLUES\\CHARACTERCREATE\\UI-CHARACTERCREATE-CLASSES" +local AssignLoot +local QueueRefresh +local CloseAllDropdowns + +local function AddDarkBackdrop(frame, bgAlpha) + if not frame or not frame.SetBackdrop then + return + end + + frame:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + + if frame.SetBackdropColor then + frame:SetBackdropColor(0.06, 0.06, 0.08, bgAlpha or 0.92) + end + + if frame.SetBackdropBorderColor then + frame:SetBackdropBorderColor(0.35, 0.35, 0.35, 0.95) + end +end + +local function MBLocal(key, fallback) + local value = L[key] + if value == key then + return fallback + end + + return value or fallback +end + +local function AddSystemMessage(message) + if DEFAULT_CHAT_FRAME then + DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99MultiBot|r: " .. tostring(message)) + end +end + +local function RunLater(delay, callback) + if MultiBot and type(MultiBot.TimerAfter) == "function" then + MultiBot.TimerAfter(delay, callback) + return + end + + if C_Timer and type(C_Timer.After) == "function" then + C_Timer.After(delay, callback) + return + end + + callback() +end + +local function ShortName(name) + if type(name) ~= "string" then + return "" + end + + if Ambiguate then + name = Ambiguate(name, "none") or name + end + + return (name:match("^[^-]+") or name) +end + +local function NormalizeName(name) + return string.lower(ShortName(name) or "") +end + +local function FindGroupUnitByName(name) + local wanted = NormalizeName(name) + + if wanted == "" then + return nil + end + + if NormalizeName(UnitName("player")) == wanted then + return "player" + end + + local raidCount = GetNumRaidMembers and (GetNumRaidMembers() or 0) or 0 + for index = 1, raidCount do + local unit = "raid" .. index + if NormalizeName(UnitName(unit)) == wanted then + return unit + end + end + + local partyCount = GetNumPartyMembers and (GetNumPartyMembers() or 0) or 0 + for index = 1, partyCount do + local unit = "party" .. index + if NormalizeName(UnitName(unit)) == wanted then + return unit + end + end + + return nil +end + +local function GetCandidateClassInfo(name) + local unit = FindGroupUnitByName(name) + if not unit then + return nil, nil + end + + local localizedClass, classToken = UnitClass(unit) + return localizedClass, classToken +end + +local SPEC_TREE_NAMES = { + WARRIOR = { "Arms", "Fury", "Protection" }, + PALADIN = { "Holy", "Protection", "Retribution" }, + HUNTER = { "Beast Mastery", "Marksmanship", "Survival" }, + ROGUE = { "Assassination", "Combat", "Subtlety" }, + PRIEST = { "Discipline", "Holy", "Shadow" }, + DEATHKNIGHT = { "Blood", "Frost", "Unholy" }, + SHAMAN = { "Elemental", "Enhancement", "Restoration" }, + MAGE = { "Arcane", "Fire", "Frost" }, + WARLOCK = { "Affliction", "Demonology", "Destruction" }, + DRUID = { "Balance", "Feral", "Restoration" }, +} + +local function GetKnownBuildForCandidate(name) + local spec = MultiBot and MultiBot.spec + local builds = spec and spec.currentBuild + + if type(builds) ~= "table" then + return nil + end + + local shortName = ShortName(name) + local normalized = NormalizeName(name) + + return builds[normalized] + or builds[string.lower(shortName or "")] + or builds[shortName] + or builds[name] +end + +local function GetCachedTalentSpecList(name) + local bridge = MultiBot and MultiBot.bridge + local specs = bridge and bridge.talentSpecs + + if type(specs) ~= "table" then + return nil + end + + return specs[NormalizeName(name)] or specs[string.lower(ShortName(name) or "")] or specs[name] +end + +local function FindKnownSpecNameFromBuild(name, build) + if type(build) ~= "string" or build == "" then + return nil + end + + local specs = GetCachedTalentSpecList(name) + if type(specs) ~= "table" then + return nil + end + + for _, entry in ipairs(specs) do + if type(entry) == "table" and entry.build == build and type(entry.name) == "string" and entry.name ~= "" then + return entry.name + end + end + + return nil +end + +local function GuessSpecFromBuild(classToken, build) + if type(build) ~= "string" then + return nil + end + + local a, b, c = build:match("^(%d+)%-(%d+)%-(%d+)$") + a, b, c = tonumber(a), tonumber(b), tonumber(c) + + if not a or not b or not c then + return nil + end + + local treeIndex = 1 + local best = a + + if b > best then + treeIndex = 2 + best = b + end + + if c > best then + treeIndex = 3 + end + + local treeNames = SPEC_TREE_NAMES[classToken or ""] + return treeNames and treeNames[treeIndex] or nil +end + +local function BuildCandidateSpecText(candidate) + if not candidate then + return MBLocal("lootmaster.unknown_spec", "Unknown spec") + end + + local build = candidate.build + local knownSpecName = FindKnownSpecNameFromBuild(candidate.name, build) + local guessed = GuessSpecFromBuild(candidate.classToken, build) + + if knownSpecName and build then + return knownSpecName .. " (" .. build .. ")" + end + + if guessed and build then + return guessed .. " (" .. build .. ")" + end + + if build then + return build + end + + return MBLocal("lootmaster.unknown_spec", "Unknown spec") +end + +local function GetCandidateDisplayName(candidate) + local name = candidate and candidate.name or "" + local classToken = candidate and candidate.classToken + local color = classToken and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classToken] + + if color and color.colorStr then + return "|c" .. color.colorStr .. name .. "|r" + end + + return name +end + +local function ApplyClassIcon(texture, classToken) + if not texture then + return + end + + if classToken and CLASS_ICON_TCOORDS and CLASS_ICON_TCOORDS[classToken] then + local coords = CLASS_ICON_TCOORDS[classToken] + texture:SetTexture(CLASS_ICON_TEXTURE) + texture:SetTexCoord(coords[1], coords[2], coords[3], coords[4]) + texture:Show() + return + end + + texture:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") + texture:SetTexCoord(0, 1, 0, 1) + texture:Show() +end + +local function SetClassIconTooltip(button, candidate) + if not button then + return + end + + button.candidate = candidate + button:SetScript("OnEnter", function(self) + local activeCandidate = self.candidate + if not activeCandidate then + return + end + + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:SetText(activeCandidate.name or MBLocal("lootmaster.unknown_candidate", "unknown"), 1, 1, 1) + GameTooltip:AddLine(activeCandidate.className or MBLocal("lootmaster.unknown_class", "Unknown class"), 0.8, 0.8, 0.8) + GameTooltip:AddLine(BuildCandidateSpecText(activeCandidate), 0.6, 0.8, 1) + GameTooltip:Show() + end) + button:SetScript("OnLeave", function() + GameTooltip:Hide() + end) +end + +local function IsPlayerMasterLooter() + local method, partyIndex, raidIndex = GetLootMethod() + + if method ~= "master" then + return false + end + + if partyIndex == 0 then + return true + end + + if raidIndex and raidIndex > 0 then + return UnitIsUnit("raid" .. raidIndex, "player") + end + + if partyIndex and partyIndex > 0 then + return UnitIsUnit("party" .. partyIndex, "player") + end + + return false +end + +local function GetCandidateName(index) + + if not GetMasterLootCandidate then + return nil + end + + local candidate = GetMasterLootCandidate(index) + + if type(candidate) == "string" and candidate ~= "" then + return candidate + end + + return nil +end + +local function NormalizeUnitName(name) + if not name or name == "" then + return nil + end + + return string.match(name, "^([^%-]+)") or name +end + +local function GetClassColorCode(classFile) + local color = classFile and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile] + + if not color then + return "|cffffffff" + end + + return string.format("|cff%02x%02x%02x", (color.r or 1) * 255, (color.g or 1) * 255, (color.b or 1) * 255) +end + +local function GetLocalizedClassName(classFile) + if not classFile then return nil end + + if LOCALIZED_CLASS_NAMES_MALE and LOCALIZED_CLASS_NAMES_MALE[classFile] then + return LOCALIZED_CLASS_NAMES_MALE[classFile] + end + + if LOCALIZED_CLASS_NAMES_FEMALE and LOCALIZED_CLASS_NAMES_FEMALE[classFile] then + return LOCALIZED_CLASS_NAMES_FEMALE[classFile] + end + + return classFile +end + +local function GetLootMasterUnit() + local method, partyIndex, raidIndex = GetLootMethod() + + if method ~= "master" then return nil end + if raidIndex and raidIndex > 0 then return "raid" .. raidIndex end + if partyIndex == 0 then return "player" end + if partyIndex and partyIndex > 0 then return "party" .. partyIndex end + + return nil +end + +local function BuildLootMasterTitle() + local title = MBLocal("lootmaster.title", "MultiBot Loot Master") + local unit = GetLootMasterUnit() + + if not unit then return title end + + local name = UnitName(unit) + if not name or name == "" then return title end + + local _, classFile = UnitClass(unit) + return title .. " - " .. GetClassColorCode(classFile) .. name .. "|r" +end + +local function UpdateLootMasterTitle(frame) + if not frame then return end + + local title = BuildLootMasterTitle() + + if frame.widget and type(frame.widget.SetTitle) == "function" then + frame.widget:SetTitle(title) + end + + if frame.title then + frame.title:SetText(title) + end +end + +local function GetClassIconTexture(classFile) + if not classFile or not CLASS_ICON_TCOORDS or not CLASS_ICON_TCOORDS[classFile] then + return nil + end + + return "Interface\\Glues\\CharacterCreate\\UI-CharacterCreate-Classes" +end + +local function GetClassIconCoords(classFile) + if not classFile or not CLASS_ICON_TCOORDS then + return nil + end + + return CLASS_ICON_TCOORDS[classFile] +end + + +local function SetLootIconTooltip(button, itemLink) + if not button then + return + end + + button.itemLink = itemLink + button:SetScript("OnEnter", function(self) + if self.itemLink then + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:SetHyperlink(self.itemLink) + GameTooltip:Show() + end + end) + button:SetScript("OnLeave", function() + GameTooltip:Hide() + end) +end + +local function GetBridgeDetail(name) + if not name or name == "" then + return nil + end + + local shortName = NormalizeUnitName(name) or name + + if MultiBot and type(MultiBot.GetCachedBridgeDetail) == "function" then + local detail = MultiBot.GetCachedBridgeDetail(shortName) or MultiBot.GetCachedBridgeDetail(name) + if type(detail) == "table" then + return detail + end + end + + local details = MultiBot and MultiBot.bridge and MultiBot.bridge.details + if type(details) == "table" then + return details[string.lower(shortName)] or details[string.lower(name)] or details[shortName] or details[name] + end + + return nil +end + +local function GetSavedBotSpec(name) + if not name or not _G.MultiBotGlobalSave then + return nil + end + + local shortName = NormalizeUnitName(name) or name + local value = _G.MultiBotGlobalSave[shortName] or _G.MultiBotGlobalSave[name] + if type(value) ~= "string" then + return nil + end + + local fields = {} + for field in string.gmatch(value, "([^,]+)") do + fields[#fields + 1] = field + end + + return fields[3], fields[4] +end + +local function GetSpecFromBridgeDetail(candidate) + local detail = candidate and GetBridgeDetail(candidate.name) + if type(detail) ~= "table" then + return nil + end + + local talent1 = tonumber(detail.talent1 or 0) or 0 + local talent2 = tonumber(detail.talent2 or 0) or 0 + local talent3 = tonumber(detail.talent3 or 0) or 0 + local tabIndex = 1 + + if talent3 > talent2 and talent3 > talent1 then + tabIndex = 3 + elseif talent2 > talent3 and talent2 > talent1 then + tabIndex = 2 + end + + local classCanon = candidate.classFile + if MultiBot and type(MultiBot.toClass) == "function" then + classCanon = MultiBot.toClass(detail.className or detail.class or candidate.classFile or "") or classCanon + end + + if MultiBot and type(MultiBot.L) == "function" and classCanon and classCanon ~= "" then + local localized = MultiBot.L("info.talent." .. classCanon .. tabIndex) + if localized and localized ~= "" then + return (MultiBot.CLEAR and MultiBot.CLEAR(localized, 1)) or localized, talent1 .. "/" .. talent2 .. "/" .. talent3 + end + end + + return nil, talent1 .. "/" .. talent2 .. "/" .. talent3 +end + +local function GetCandidateSpecText(candidate) + local spec, build = GetSpecFromBridgeDetail(candidate) + + if not spec then + spec, build = GetSavedBotSpec(candidate and candidate.name) + end + + if spec and build and build ~= "" then + return spec .. " (" .. build .. ")" + end + + if spec and spec ~= "" then + return spec + end + + return MBLocal("lootmaster.unknown_spec", "Unknown spec") +end + +local function SetClassIconTooltip(button, candidate) + if not button then + return + end + + button.candidate = candidate + button:SetScript("OnEnter", function(self) + local current = self.candidate + if not current then + return + end + + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:AddLine(current.name or MBLocal("lootmaster.unknown_candidate", "unknown"), 1, 1, 1) + + local className = current.className or GetLocalizedClassName(current.classFile) + if className then + GameTooltip:AddLine(className, 0.75, 0.75, 0.75) + end + + GameTooltip:AddLine(GetCandidateSpecText(current), 0.6, 0.8, 1) + GameTooltip:Show() + end) +end + +local function BuildUnitClassLookup() + local lookup = {} + local raidCount = 0 + local partyCount = 0 + + if GetNumRaidMembers then + raidCount = GetNumRaidMembers() or 0 + end + + if raidCount > 0 then + for index = 1, raidCount do + local unit = "raid" .. index + local name = NormalizeUnitName(UnitName(unit)) + local _, classFile = UnitClass(unit) + + if name and classFile then + lookup[name] = classFile + end + end + + return lookup + end + + local playerName = NormalizeUnitName(UnitName("player")) + local _, playerClass = UnitClass("player") + + if playerName and playerClass then + lookup[playerName] = playerClass + end + + if GetNumPartyMembers then + partyCount = GetNumPartyMembers() or 0 + end + + for index = 1, partyCount do + local unit = "party" .. index + local name = NormalizeUnitName(UnitName(unit)) + local _, classFile = UnitClass(unit) + + if name and classFile then + lookup[name] = classFile + end + end + + return lookup +end + +local function GetCandidateScanLimit() + local raidCount = 0 + local partyCount = 0 + + if GetNumRaidMembers then + raidCount = GetNumRaidMembers() or 0 + end + + if raidCount > 0 then + if raidCount > MAX_MASTER_LOOT_CANDIDATES then + return MAX_MASTER_LOOT_CANDIDATES + end + + return raidCount + end + + if GetNumPartyMembers then + partyCount = GetNumPartyMembers() or 0 + end + + if partyCount > 0 then + return partyCount + 1 + end + + return 1 +end + +local function BuildCandidateList(slot) + local candidates = {} + local seen = {} + local limit = GetCandidateScanLimit() + local unitClasses = BuildUnitClassLookup() + + for index = 1, limit do + local name = GetCandidateName(index) + + if not name then + break + end + + if not seen[name] then + seen[name] = true + + local className, classToken = GetCandidateClassInfo(name) + + candidates[#candidates + 1] = { + index = index, + name = name, + className = className, + classToken = classToken, + build = GetKnownBuildForCandidate(name), + classFile = unitClasses[NormalizeUnitName(name)] or classToken, + } + end + end + + return candidates +end + +local function SafeItemText(slot) + local link = GetLootSlotLink(slot) + + if link then + return link + end + + local texture, itemName, quantity = GetLootSlotInfo(slot) + + if itemName and itemName ~= "" then + if quantity and quantity > 1 then + return itemName .. " x" .. quantity + end + + return itemName + end + + return MBLocal("lootmaster.unknown_item", "Unknown item") +end + +local function BuildLootSlotKey(slot) + local link = GetLootSlotLink(slot) + if link and link ~= "" then + return link + end + + local _, itemName, quantity = GetLootSlotInfo(slot) + return tostring(slot) .. ":" .. tostring(itemName or "") .. ":" .. tostring(quantity or 1) +end + +local function MarkLootSlotAssigned(slot) + assignedLootKeys[BuildLootSlotKey(slot)] = GetTime and GetTime() or time() + assignedLootSlots[slot] = GetTime and GetTime() or time() +end + +local function IsLootSlotAssignedLocally(slot) + return assignedLootSlots[slot] ~= nil or assignedLootKeys[BuildLootSlotKey(slot)] ~= nil +end + +local function IsLootSlotMoney(slot) + return LootSlotIsCoin and LootSlotIsCoin(slot) +end + +local function IsLootSlotEmpty(slot) + if LootSlotIsItem and LootSlotIsItem(slot) then + return false + end + + local link = GetLootSlotLink(slot) + if link and link ~= "" then + return false + end + + local _, itemName = GetLootSlotInfo(slot) + return not itemName or itemName == "" +end + +local function GetLootSlotHideKey(slot) + local link = GetLootSlotLink(slot) + + if link and link ~= "" then + return link + end + + local _, itemName, quantity, quality = GetLootSlotInfo(slot) + + if itemName and itemName ~= "" then + return table.concat({ tostring(itemName), tostring(quantity or 1), tostring(quality or 0) }, "|") + end + + return nil +end + +local function PruneAssignedLootKeys() + local now = GetTime and GetTime() or time() or 0 + + for key, expiresAt in pairs(assignedLootKeys) do + if (tonumber(expiresAt) or 0) <= now then + assignedLootKeys[key] = nil + end + end +end + +local function MarkLootSlotAssigned(slot) + local key = GetLootSlotHideKey(slot) + + if key then + assignedLootKeys[key] = (GetTime and GetTime() or time() or 0) + 3 + end +end + +local function IsLootSlotAssignedLocally(slot) + local key = GetLootSlotHideKey(slot) + + return key and assignedLootKeys[key] ~= nil +end + +local function CreateBasicFrame() + local widget + local frame + local contentParent + + if AceGUI and type(AceGUI.Create) == "function" then + widget = AceGUI:Create("Window") + widget:SetTitle(BuildLootMasterTitle()) + widget:SetWidth(FRAME_WIDTH) + widget:SetHeight(FRAME_HEIGHT) + widget:EnableResize(false) + widget:SetCallback("OnClose", function(activeWidget) + activeWidget.frame:Hide() + end) + + frame = widget.frame + frame.widget = widget + frame:SetPoint("CENTER", UIParent, "CENTER", 0, 40) + frame:SetFrameStrata("DIALOG") + frame:SetToplevel(true) + + contentParent = widget.content or frame + AddDarkBackdrop(contentParent, 0.90) + else + frame = CreateFrame("Frame", "MultiBotLootMasterFrame", UIParent) + frame:SetWidth(FRAME_WIDTH) + frame:SetHeight(FRAME_HEIGHT) + frame:SetPoint("CENTER", UIParent, "CENTER", 0, 40) + frame:SetFrameStrata("DIALOG") + frame:SetToplevel(true) + frame:SetMovable(true) + frame:EnableMouse(true) + frame:RegisterForDrag("LeftButton") + frame:SetScript("OnDragStart", function(self) + self:StartMoving() + end) + frame:SetScript("OnDragStop", function(self) + self:StopMovingOrSizing() + end) + + AddDarkBackdrop(frame, 0.90) + + frame.title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") + frame.title:SetPoint("TOPLEFT", frame, "TOPLEFT", 18, -14) + frame.title:SetText(BuildLootMasterTitle()) + + frame.close = CreateFrame("Button", nil, frame, "UIPanelCloseButton") + frame.close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -6) + frame.close:SetScript("OnClick", function() + frame:Hide() + end) + + contentParent = frame + end + + frame.status = contentParent:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + frame.status:SetPoint("TOPLEFT", contentParent, "TOPLEFT", 8, -8) + frame.status:SetWidth(FRAME_WIDTH - 40) + frame.status:SetJustifyH("LEFT") + frame.status:SetText(MBLocal("lootmaster.status.ready", "Select an item recipient.")) + + frame.refresh = CreateFrame("Button", nil, contentParent, "UIPanelButtonTemplate") + frame.refresh:SetWidth(90) + frame.refresh:SetHeight(22) + frame.refresh:SetPoint("TOPRIGHT", contentParent, "TOPRIGHT", -8, -4) + frame.refresh:SetText(MBLocal("lootmaster.refresh", "Refresh")) + frame.refresh:SetScript("OnClick", function() + LootMasterUI:Refresh() + end) + + frame.content = CreateFrame("Frame", nil, contentParent) + frame.content:SetPoint("TOPLEFT", frame.status, "BOTTOMLEFT", 0, -16) + frame.content:SetPoint("BOTTOMRIGHT", contentParent, "BOTTOMRIGHT", -8, 8) + + frame.rows = {} + + frame:Hide() + + return frame +end + +local function ClearRows(frame) + if not frame or not frame.rows then + return + end + + for _, row in ipairs(frame.rows) do + row:Hide() + end +end + +local function AcquireRow(frame, index) + frame.rows[index] = frame.rows[index] or CreateFrame("Frame", nil, frame.content) + + local row = frame.rows[index] + row.index = index + + row:SetParent(frame.content) + row:SetWidth(FRAME_WIDTH - 52) + row:SetHeight(ITEM_ROW_HEIGHT) + row:ClearAllPoints() + + if index == 1 then + row:SetPoint("TOPLEFT", frame.content, "TOPLEFT", 0, 0) + else + row:SetPoint("TOPLEFT", frame.rows[index - 1], "BOTTOMLEFT", 0, -8) + end + + if not row.icon then + row.icon = row:CreateTexture(nil, "ARTWORK") + row.icon:SetWidth(28) + row.icon:SetHeight(28) + row.icon:SetPoint("TOPLEFT", row, "TOPLEFT", 10, -4) + end + + if not row.iconButton then + row.iconButton = CreateFrame("Button", nil, row) + row.iconButton:SetWidth(28) + row.iconButton:SetHeight(28) + row.iconButton:SetPoint("CENTER", row.icon, "CENTER", 0, 0) + end + + if not row.itemButton then + row.itemButton = CreateFrame("Button", nil, row) + row.itemButton:SetWidth(FRAME_WIDTH - 128) + row.itemButton:SetHeight(22) + row.itemButton:SetPoint("TOPLEFT", row.icon, "TOPRIGHT", 8, -1) + + row.itemButton.text = row.itemButton:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + row.itemButton.text:SetPoint("LEFT", row.itemButton, "LEFT", 0, 0) + row.itemButton.text:SetWidth(FRAME_WIDTH - 128) + row.itemButton.text:SetJustifyH("LEFT") + + end + + if not row.candidateLabel then + row.candidateLabel = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + row.candidateLabel:SetPoint("TOPLEFT", row.itemButton, "BOTTOMLEFT", 0, -8) + row.candidateLabel:SetText(MBLocal("lootmaster.assign_to", "Assign to:")) + end + + if not row.selectButton then + row.selectButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") + row.selectButton:SetWidth(DROPDOWN_WIDTH) + row.selectButton:SetHeight(22) + row.selectButton:SetPoint("LEFT", row.candidateLabel, "RIGHT", 8, 0) + row.selectButton.ownerRow = row + + row.selectButton:SetScript("OnClick", function(self) + local ownerRow = self.ownerRow + local candidates = ownerRow and ownerRow.candidates or nil + + if not candidates or #candidates == 0 then + return + end + + if ownerRow.dropdown and ownerRow.dropdown:IsShown() then + ownerRow.dropdown:Hide() + return + end + + if CloseAllDropdowns then + CloseAllDropdowns() + end + + ownerRow.dropdown = ownerRow.dropdown or CreateFrame("Frame", nil, UIParent) + local dropdown = ownerRow.dropdown + dropdown:SetFrameStrata("TOOLTIP") + dropdown:SetWidth(DROPDOWN_WIDTH + 32) + dropdown:SetHeight((#candidates * 20) + 12) + dropdown:ClearAllPoints() + dropdown:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -2) + dropdown:SetBackdrop({ + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + dropdown:SetBackdropColor(0, 0, 0, 0.95) + dropdown.buttons = dropdown.buttons or {} + + for i, candidate in ipairs(candidates) do + local button = dropdown.buttons[i] + + if not button then + button = CreateFrame("Button", nil, dropdown) + dropdown.buttons[i] = button + button:SetHeight(18) + button:SetPoint("LEFT", dropdown, "LEFT", 8, 0) + button:SetPoint("RIGHT", dropdown, "RIGHT", -8, 0) + + button.text = button:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + button.text:SetPoint("LEFT", button, "LEFT", 0, 0) + button.text:SetPoint("RIGHT", button, "RIGHT", -24, 0) + button.text:SetJustifyH("LEFT") + + button.classIconButton = CreateFrame("Button", nil, button) + button.classIconButton:SetWidth(16) + button.classIconButton:SetHeight(16) + button.classIconButton:SetPoint("RIGHT", button, "RIGHT", 0, 0) + button.classIcon = button.classIconButton:CreateTexture(nil, "ARTWORK") + button.classIcon:SetAllPoints(button.classIconButton) + end + + button:ClearAllPoints() + if i == 1 then + button:SetPoint("TOPLEFT", dropdown, "TOPLEFT", 8, -6) + button:SetPoint("TOPRIGHT", dropdown, "TOPRIGHT", -8, -6) + else + button:SetPoint("TOPLEFT", dropdown.buttons[i - 1], "BOTTOMLEFT", 0, -2) + button:SetPoint("TOPRIGHT", dropdown.buttons[i - 1], "BOTTOMRIGHT", 0, -2) + end + + local classColor = GetClassColorCode(candidate.classFile) + local classIcon = GetClassIconTexture(candidate.classFile) + local classIconCoords = GetClassIconCoords(candidate.classFile) + + button.ownerRow = ownerRow + button.candidate = candidate + button.text:SetText(classColor .. candidate.name .. "|r") + button:SetScript("OnClick", function(clicked) + local selected = clicked.candidate + local selectedRow = clicked.ownerRow + + selectedRow.selectedCandidateIndex = selected.index + selectedRow.selectedCandidateName = selected.name + selectedRow.selectButton:SetText(GetClassColorCode(selected.classFile) .. selected.name .. "|r") + dropdown:Hide() + end) + + if classIcon then + button.classIcon:SetTexture(classIcon) + if classIconCoords then + button.classIcon:SetTexCoord(classIconCoords[1], classIconCoords[2], classIconCoords[3], classIconCoords[4]) + else + button.classIcon:SetTexCoord(0, 1, 0, 1) + end + button.classIconButton:Show() + SetClassIconTooltip(button.classIconButton, candidate) + else + button.classIconButton:Hide() + SetClassIconTooltip(button.classIconButton, nil) + end + + button:Show() + end + + for i = #candidates + 1, #dropdown.buttons do + dropdown.buttons[i]:Hide() + end + + dropdown:Show() + end) + end + + if not row.assignButton then + row.assignButton = CreateFrame("Button", nil, row, "UIPanelButtonTemplate") + row.assignButton:SetWidth(90) + row.assignButton:SetHeight(22) + row.assignButton:SetPoint("LEFT", row.selectButton, "RIGHT", 8, 0) + row.assignButton:SetText(MBLocal("lootmaster.assign", "Assign")) + row.assignButton:SetScript("OnClick", function(self) + local ownerRow = self.ownerRow + + if not ownerRow or not ownerRow.slot or not ownerRow.selectedCandidateIndex then + AddSystemMessage(MBLocal("lootmaster.error.invalid_candidate", "Invalid loot candidate.")) + return + end + + AssignLoot(ownerRow.slot, ownerRow.selectedCandidateIndex, ownerRow.selectedCandidateName) + end) + + row.assignButton.ownerRow = row + end + + if not row.noCandidateText then + row.noCandidateText = row:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall") + row.noCandidateText:SetPoint("LEFT", row.candidateLabel, "RIGHT", 8, 0) + row.noCandidateText:SetJustifyH("LEFT") + row.noCandidateText:SetText(MBLocal("lootmaster.no_candidates_for_item", "No candidates for this item.")) + row.noCandidateText:Hide() + end + + if row.dropdown then + row.dropdown:Hide() + end + + row.candidates = nil + row.selectedCandidateIndex = nil + row.selectedCandidateName = nil + row.selectedCandidate = nil + row.slot = nil + SetLootIconTooltip(row.iconButton, nil) + + row:Show() + + return row +end + +local function ApplyCandidateSelection(row, candidate) + if not row then + return + end + + if not candidate then + row.selectedCandidateIndex = nil + row.selectedCandidateName = nil + row.selectedCandidate = nil + row.selectButton:SetText(MBLocal("lootmaster.select_bot", "Select a bot")) + return + end + + row.selectedCandidateIndex = candidate.index + row.selectedCandidateName = candidate.name + row.selectedCandidate = candidate + row.selectButton:SetText(GetClassColorCode(candidate.classFile) .. candidate.name .. "|r") +end + +local function PrepareRowSelection(row, candidates) + row.candidates = candidates or {} + ApplyCandidateSelection(row, row.candidates[1]) +end + +CloseAllDropdowns = function() + local frame = LootMasterUI.frame + if not frame or not frame.rows then + return + end + + for _, row in ipairs(frame.rows) do + if row.dropdown then + row.dropdown:Hide() + end + end +end + +local function UpdateRowCandidateControls(row, candidates) + PrepareRowSelection(row, candidates) + + if candidates and #candidates > 0 then + row.selectButton:Show() + row.assignButton:Show() + row.noCandidateText:Hide() + row.candidateLabel:SetText(MBLocal("lootmaster.assign_to", "Assign to:")) + else + row.selectButton:Hide() + row.assignButton:Hide() + row.noCandidateText:Show() + row.candidateLabel:SetText(MBLocal("lootmaster.assign_to", "Assign to:")) + end +end + +QueueRefresh = function(delay) + local frame = LootMasterUI.frame + + if not frame then + return + end + + frame.refreshDelay = delay or 0.15 + frame:SetScript("OnUpdate", function(self, elapsed) + self.refreshDelay = (self.refreshDelay or 0) - elapsed + + if self.refreshDelay > 0 then + return + end + + self:SetScript("OnUpdate", nil) + + if LootMasterUI.frame and LootMasterUI.frame:IsShown() then + LootMasterUI:Refresh() + end + end) +end + +AssignLoot = function(slot, candidateIndex, candidateName) + local itemText = SafeItemText(slot) + + if not IsPlayerMasterLooter() then + AddSystemMessage(MBLocal("lootmaster.error.not_master", "You are not the master looter.")) + return + end + + if not candidateIndex then + AddSystemMessage(MBLocal("lootmaster.error.invalid_candidate", "Invalid loot candidate.")) + return + end + + MarkLootSlotAssigned(slot) + GiveMasterLoot(slot, candidateIndex) + + if LootMasterUI.frame and LootMasterUI.frame:IsShown() then + LootMasterUI:Refresh() + end + + AddSystemMessage( + string.format( + MBLocal("lootmaster.assigned", "Assigned %s to %s."), + itemText, + candidateName or MBLocal("lootmaster.unknown_candidate", "unknown") + ) + ) + + RunLater(0.2, function() + if LootMasterUI.frame and LootMasterUI.frame:IsShown() then + QueueRefresh(0.05) + end + end) + + RunLater(0.7, function() + if LootMasterUI.frame and LootMasterUI.frame:IsShown() then + QueueRefresh(0.05) + end + end) + + RunLater(1.4, function() + if LootMasterUI.frame and LootMasterUI.frame:IsShown() then + LootMasterUI:Refresh() + end + end) +end + +function LootMasterUI:EnsureFrame() + if self.frame then + return self.frame + end + + self.frame = CreateBasicFrame() + + return self.frame +end + +function LootMasterUI:Refresh() + local frame = self:EnsureFrame() + local numSlots = GetNumLootItems() or 0 + local rowIndex = 1 + local totalCandidates = 0 + + PruneAssignedLootKeys() + ClearRows(frame) + UpdateLootMasterTitle(frame) + + if not IsPlayerMasterLooter() then + frame.status:SetText(MBLocal("lootmaster.status.not_master", "Loot method is not master loot, or you are not the master looter.")) + return + end + + if numSlots <= 0 then + frame.status:SetText(MBLocal("lootmaster.status.no_loot", "No loot available.")) + return + end + + frame.status:SetText(MBLocal("lootmaster.status.ready", "Select an item recipient.")) + + for slot = 1, numSlots do + local isMoney = IsLootSlotMoney(slot) + + if not isMoney and not IsLootSlotEmpty(slot) and not IsLootSlotAssignedLocally(slot) then + local texture, itemName, quantity, quality, locked = GetLootSlotInfo(slot) + local itemLink = GetLootSlotLink(slot) + local candidates = BuildCandidateList(slot) + + if itemName and itemName ~= "" then + local row = AcquireRow(frame, rowIndex) + local itemText = itemName + + if quantity and quantity > 1 then + itemText = itemText .. " x" .. quantity + end + + row.slot = slot + row.icon:SetTexture(texture or "Interface\\Icons\\INV_Misc_QuestionMark") + SetLootIconTooltip(row.iconButton, itemLink) + row.itemButton.text:SetText(itemText) + + if quality and GetItemQualityColor then + local r, g, b = GetItemQualityColor(quality) + row.itemButton.text:SetTextColor(r or 1, g or 1, b or 1) + else + row.itemButton.text:SetTextColor(1, 1, 1) + end + + UpdateRowCandidateControls(row, candidates) + + totalCandidates = totalCandidates + #candidates + rowIndex = rowIndex + 1 + end + end + end + + for i = rowIndex, #frame.rows do + if frame.rows[i] then + frame.rows[i]:Hide() + end + end + + if rowIndex == 1 then + frame.status:SetText(MBLocal("lootmaster.status.no_loot", "No loot available.")) + return + end + + if totalCandidates == 0 and rowIndex > 1 then + frame.status:SetText(MBLocal("lootmaster.status.no_candidates", "Loot found, but no master-loot candidates were returned by the client.")) + AddSystemMessage("Loot master debug: loot exists, but GetMasterLootCandidate returned no candidates.") + return + end +end + +function LootMasterUI:Open() + local frame = self:EnsureFrame() + + frame:Show() + + if frame.Raise then + frame:Raise() + end + + AddSystemMessage("Loot master UI opening frame.") + + self:Refresh() + + if frame.Raise then + frame:Raise() + end +end + +function LootMasterUI:Close() + if CloseAllDropdowns then + CloseAllDropdowns() + end + + if self.frame then + self.frame:Hide() + end +end + +function LootMasterUI:OnLootOpened(autoLoot) + local numSlots = GetNumLootItems() or 0 + local method, partyIndex, raidIndex = GetLootMethod() + + AddSystemMessage( + string.format( + "LOOT_OPENED debug: autoLoot=%s, method=%s, partyMaster=%s, raidMaster=%s, slots=%d", + tostring(autoLoot), + tostring(method), + tostring(partyIndex), + tostring(raidIndex), + numSlots + ) + ) + + if raidIndex and raidIndex > 0 then + AddSystemMessage( + string.format( + "Loot master debug: raid master unit=raid%d, name=%s, isPlayer=%s", + raidIndex, + tostring(UnitName("raid" .. raidIndex)), + tostring(UnitIsUnit("raid" .. raidIndex, "player")) + ) + ) + elseif partyIndex then + AddSystemMessage( + string.format("Loot master debug: partyMaster=%s, isPlayerMaster=%s", tostring(partyIndex), tostring(partyIndex == 0)) + ) + end + + if not IsPlayerMasterLooter() then + AddSystemMessage("Loot master UI skipped: you are not detected as master looter.") + return + end + + if numSlots <= 0 then + AddSystemMessage("Loot master UI skipped: no visible loot slots.") + return + end + + self:Open() + + if MultiBot and MultiBot.Comm and type(MultiBot.Comm.RequestBotDetails) == "function" then + MultiBot.Comm.RequestBotDetails() + QueueRefresh(0.35) + end +end + +local eventFrame = CreateFrame("Frame") +eventFrame:RegisterEvent("LOOT_OPENED") +eventFrame:RegisterEvent("LOOT_SLOT_CHANGED") +eventFrame:RegisterEvent("LOOT_CLOSED") +eventFrame:RegisterEvent("PARTY_LOOT_METHOD_CHANGED") + +eventFrame:SetScript("OnEvent", function(_, event, ...) + if event == "LOOT_OPENED" then + assignedLootKeys = {} + assignedLootSlots = {} + LootMasterUI:OnLootOpened(...) + return + end + + if event == "LOOT_SLOT_CHANGED" then + if LootMasterUI.frame and LootMasterUI.frame:IsShown() then + QueueRefresh(0.05) + end + return + end + + if event == "PARTY_LOOT_METHOD_CHANGED" then + if LootMasterUI.frame and LootMasterUI.frame:IsShown() then + LootMasterUI:Refresh() + end + return + end + + if event == "LOOT_CLOSED" then + LootMasterUI:Close() + end +end) + +_G.MultiBotLootMasterUI = LootMasterUI From b47366695f527a4ae8692fb5d5e93c623742071e Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:01:31 +0200 Subject: [PATCH 02/11] Update TODO.md --- TODO.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/TODO.md b/TODO.md index 1a7f3be..c361b89 100644 --- a/TODO.md +++ b/TODO.md @@ -17,6 +17,24 @@ * 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 +## Frame Loot + +* Tri intelligent des bots : afficher en haut les bots qui peuvent réellement utiliser l’objet selon classe, spé, type d’armure, arme, rôle. +* Filtre par rôle : boutons Tous, Tank, Heal, DPS, Caster, Mêlée, ou par classe. +* Suggestion automatique : préselectionner le bot le plus pertinent au lieu du premier candidat. +* Tooltip enrichi : survol du bot = classe, spé, rôle, niveau, équipement actuel comparable si l’addon connaît l’inventaire. +* Indicateur de pertinence : par exemple Excellent, Possible, Mauvais choix, avec couleur verte/orange/rouge. +* Avertissement avant erreur : confirmer si tu attribues une plaque à un mage, une arme inutilisable, ou un item épique à un bot non adapté. +* Bouton “Attribuer recommandé” : un seul clic pour donner l’objet au meilleur candidat. +* Historique des loots : petite liste “objet donné à X” pendant la session, utile en raid. +* Mémorisation des préférences : par exemple toujours donner tissu spell à tel bot, plaques tank à tel autre, etc. +* Masquer les bots non pertinents : option pour ne voir que ceux qui peuvent équiper/utiliser l’objet. +* Lien avec MultiBotInventoryFrame : clic droit sur un bot dans la dropdown = ouvrir son inventaire/équipement. +* Mode compact : pour les combats, n’afficher que icône item + nom + dropdown + bouton. +* Bouton refresh transformé en icône : garder la sécurité manuelle sans prendre autant de place. +* Attribution rapide par raccourcis : Alt+clic attribue au bot recommandé, Shift+clic ouvre détails. +* Debug discret : une option /mb lootdebug au lieu de spam chat permanent. + ## Améliorations UI encore ouvertes * Passer les gobject dans la bridge. From a8b392de4546578853e26fd3ea23c26b5d63b377 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:25:43 +0200 Subject: [PATCH 03/11] Update TODO.md --- TODO.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/TODO.md b/TODO.md index c361b89..b9c3476 100644 --- a/TODO.md +++ b/TODO.md @@ -35,6 +35,41 @@ * Attribution rapide par raccourcis : Alt+clic attribue au bot recommandé, Shift+clic ouvre détails. * Debug discret : une option /mb lootdebug au lieu de spam chat permanent. +-- Avancement: + +Fonction Statut Détail +Tri intelligent des bots **oui / oui** Score calculé selon armure, arme utilisable, profil principal des stats, rôle/spé estimé, et pénalité loot récent. !!! Je garderais juste une petite réserve mentale : ce n’est pas encore un simulateur BiS ou comparaison d’équipement, mais pour classer correctement les bots selon “qui peut vraiment utiliser l’objet”, la base est maintenant bonne. +Classe compatible **oui** Compatibilité évaluée via les tables de classe pour l’armure maximale, les boucliers et les armes utilisables. !!! La seule limite : ça vérifie la compatibilité “peut porter/utiliser”, pas encore la pertinence fine type “ce paladin peut porter cette masse caster mais ce n’est pas pour Vindicte”. Cette pertinence-là est couverte par le scoring stats/rôle. +Type d’armure compatible **Oui** cloth/leather/mail/plate/shield sont scorés. +Arme utilisable **Oui** Table d’armes par classe utilisée dans le score. +Stats utiles à la spé **oui** Le tooltip est scanné pour déterminer un profil principal d’objet : tank, physical, caster ou healer, puis comparé au rôle/spé estimé du bot. !!! Réserve : ça reste une lecture par grandes familles de stats. Ça ne compare pas encore les caps, le BiS, l’équipement actuel, ni les besoins exacts de chaque spé. +Rôle compatible **Oui** Le rôle du bot est estimé depuis sa classe et son arbre de talents/build, puis comparé au profil principal de l’objet dans le score. !!! Même réserve que les autres : c’est une compatibilité de rôle “logique” tank/heal/caster/physical, pas une évaluation ultra fine de chaque spécialisation. +Objet déjà attribué récemment **Oui** Pénalité temporaire via recentLootByCandidate, 120 sec. +Classe/spé non pertinente **Partiel** Malus de score, mais pas encore masquage ni avertissement clair. +Dropdown Nom Spé 92% **Oui** Présent dans BuildCandidateDropdownText. +Suggestion automatique **Oui** Les candidats sont triés par score, puis le premier est présélectionné. +Tooltip enrichi **Partiel** Nom, classe localisée, spé/build, score. Pas encore niveau ni équipement comparable. +Indicateur Excellent/Possible/Mauvais **Partiel** Il y a un % coloré, mais pas encore de libellé texte. +Avertissement avant mauvais choix **Non** Pas de confirmation si mage/plaque, arme inutilisable, etc. +Bouton “Attribuer recommandé” **Partiel** Le bouton actuel attribue le candidat présélectionné, donc le meilleur par défaut, mais pas de bouton dédié. +Historique des loots visible **Oui** Les derniers loots attribués apparaissent en bas de la frame pendant la session. +Mémorisation des préférences **Oui** Clic droit sur Attribuer mémorise le bot choisi pour les objets similaires et lui donne un bonus de score ensuite. +Masquer bots non pertinents **Non** Les mauvais choix restent visibles, seulement avec score bas. +Lien avec MultiBotInventoryFrame **Oui** clic droit pour ouvrir inventaire/équipement. +Mode compact **Non** Frame réduite/assombrie, mais pas de vrai mode compact. +Refresh en icône **Non** Le bouton texte Rafraîchir est encore là. +Raccourcis Alt/Shift clic **Non** Pas encore implémenté. +/mb lootdebug discret **Non** Il reste au moins un message debug chat direct quand aucun candidat n’est retourné. + +**Ajouter en variables: + 'lootmaster.history_title' + 'lootmaster.history_empty' + 'lootmaster.preference_hint' + 'lootmaster.preference_saved' + 'lootmaster.history_title' + 'lootmaster.preference_saved' + 'lootmaster.preference_cleared' + ## Améliorations UI encore ouvertes * Passer les gobject dans la bridge. From e59d9c4861efd54d2a2757f8c33ca57f5fbe9a5e Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:34:04 +0200 Subject: [PATCH 04/11] Update TODO.md --- TODO.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/TODO.md b/TODO.md index b9c3476..5553086 100644 --- a/TODO.md +++ b/TODO.md @@ -62,13 +62,6 @@ Raccourcis Alt/Shift clic **Non** Pas encore implémenté. /mb lootdebug discret **Non** Il reste au moins un message debug chat direct quand aucun candidat n’est retourné. **Ajouter en variables: - 'lootmaster.history_title' - 'lootmaster.history_empty' - 'lootmaster.preference_hint' - 'lootmaster.preference_saved' - 'lootmaster.history_title' - 'lootmaster.preference_saved' - 'lootmaster.preference_cleared' ## Améliorations UI encore ouvertes From 6bc2137f5bbd87692ffb14926384ae5d48ac8384 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:34:56 +0200 Subject: [PATCH 05/11] Add more functions --- Locales/MultiBotAceLocale-frFR.lua | 9 + TODO.md | 28 -- UI/MultiBotLootMasterUI.lua | 653 ++++++++++++++++++++++++++++- 3 files changed, 646 insertions(+), 44 deletions(-) diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index 9027072..f1f1f5f 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -21,6 +21,15 @@ local frFRValues = { ["lootmaster.select_bot"] = "Choisir un bot", ["lootmaster.no_candidates_for_item"] = "Aucun candidat pour cet objet.", ["lootmaster.unknown_spec"] = "Spec inconnue.", + ["lootmaster.inventory_unavailable"] = "Inventaire indisponible.", + ["lootmaster.priority_score"] = "Priorite:", + ["lootmaster.inventory_hint"] = "Click-droit: ouvre inventaire/équipment.", + ["lootmaster.history_title"] = "Loots Récents", + ["lootmaster.history_empty"] = "Pas de Loots Encore Attribués.", + ["lootmaster.preference_hint"] = "Click-Droit: sauvegarder. Shift-click-droit: Effacer.", + ["lootmaster.preference_saved"] = "Preference sauvegardée: items simmilaires -> %s.", + ["lootmaster.preference_cleared"] = "Preference effacée pour items similaires.", + ["lootmaster.preference_missing"] = "Pas de préférence sauvegardée pour cet item.", ["tips.units.rti"] = "RTI", ["tips.disperse.main"] = "Disperse", ["tips.disperse.set"] = "Définir la distance de disperse", diff --git a/TODO.md b/TODO.md index 5553086..c361b89 100644 --- a/TODO.md +++ b/TODO.md @@ -35,34 +35,6 @@ * Attribution rapide par raccourcis : Alt+clic attribue au bot recommandé, Shift+clic ouvre détails. * Debug discret : une option /mb lootdebug au lieu de spam chat permanent. --- Avancement: - -Fonction Statut Détail -Tri intelligent des bots **oui / oui** Score calculé selon armure, arme utilisable, profil principal des stats, rôle/spé estimé, et pénalité loot récent. !!! Je garderais juste une petite réserve mentale : ce n’est pas encore un simulateur BiS ou comparaison d’équipement, mais pour classer correctement les bots selon “qui peut vraiment utiliser l’objet”, la base est maintenant bonne. -Classe compatible **oui** Compatibilité évaluée via les tables de classe pour l’armure maximale, les boucliers et les armes utilisables. !!! La seule limite : ça vérifie la compatibilité “peut porter/utiliser”, pas encore la pertinence fine type “ce paladin peut porter cette masse caster mais ce n’est pas pour Vindicte”. Cette pertinence-là est couverte par le scoring stats/rôle. -Type d’armure compatible **Oui** cloth/leather/mail/plate/shield sont scorés. -Arme utilisable **Oui** Table d’armes par classe utilisée dans le score. -Stats utiles à la spé **oui** Le tooltip est scanné pour déterminer un profil principal d’objet : tank, physical, caster ou healer, puis comparé au rôle/spé estimé du bot. !!! Réserve : ça reste une lecture par grandes familles de stats. Ça ne compare pas encore les caps, le BiS, l’équipement actuel, ni les besoins exacts de chaque spé. -Rôle compatible **Oui** Le rôle du bot est estimé depuis sa classe et son arbre de talents/build, puis comparé au profil principal de l’objet dans le score. !!! Même réserve que les autres : c’est une compatibilité de rôle “logique” tank/heal/caster/physical, pas une évaluation ultra fine de chaque spécialisation. -Objet déjà attribué récemment **Oui** Pénalité temporaire via recentLootByCandidate, 120 sec. -Classe/spé non pertinente **Partiel** Malus de score, mais pas encore masquage ni avertissement clair. -Dropdown Nom Spé 92% **Oui** Présent dans BuildCandidateDropdownText. -Suggestion automatique **Oui** Les candidats sont triés par score, puis le premier est présélectionné. -Tooltip enrichi **Partiel** Nom, classe localisée, spé/build, score. Pas encore niveau ni équipement comparable. -Indicateur Excellent/Possible/Mauvais **Partiel** Il y a un % coloré, mais pas encore de libellé texte. -Avertissement avant mauvais choix **Non** Pas de confirmation si mage/plaque, arme inutilisable, etc. -Bouton “Attribuer recommandé” **Partiel** Le bouton actuel attribue le candidat présélectionné, donc le meilleur par défaut, mais pas de bouton dédié. -Historique des loots visible **Oui** Les derniers loots attribués apparaissent en bas de la frame pendant la session. -Mémorisation des préférences **Oui** Clic droit sur Attribuer mémorise le bot choisi pour les objets similaires et lui donne un bonus de score ensuite. -Masquer bots non pertinents **Non** Les mauvais choix restent visibles, seulement avec score bas. -Lien avec MultiBotInventoryFrame **Oui** clic droit pour ouvrir inventaire/équipement. -Mode compact **Non** Frame réduite/assombrie, mais pas de vrai mode compact. -Refresh en icône **Non** Le bouton texte Rafraîchir est encore là. -Raccourcis Alt/Shift clic **Non** Pas encore implémenté. -/mb lootdebug discret **Non** Il reste au moins un message debug chat direct quand aucun candidat n’est retourné. - -**Ajouter en variables: - ## Améliorations UI encore ouvertes * Passer les gobject dans la bridge. diff --git a/UI/MultiBotLootMasterUI.lua b/UI/MultiBotLootMasterUI.lua index 47a704b..d76f0e4 100644 --- a/UI/MultiBotLootMasterUI.lua +++ b/UI/MultiBotLootMasterUI.lua @@ -17,16 +17,27 @@ end local LootMasterUI = {} local assignedLootKeys = {} local assignedLootSlots = {} +local recentLootByCandidate = {} +local lootHistory = {} local FRAME_WIDTH = 500 local FRAME_HEIGHT = 420 local ITEM_ROW_HEIGHT = 64 -local DROPDOWN_WIDTH = 190 +local DROPDOWN_WIDTH = 220 local MAX_MASTER_LOOT_CANDIDATES = 40 +local RECENT_LOOT_PENALTY_SECONDS = 120 +local LOOT_HISTORY_LINE_HEIGHT = 14 +local LOOT_HISTORY_HEIGHT = 62 +local LOOT_PREFERENCE_BONUS = 18 local CLASS_ICON_TEXTURE = "Interface\\GLUES\\CHARACTERCREATE\\UI-CHARACTERCREATE-CLASSES" local AssignLoot local QueueRefresh local CloseAllDropdowns +local GetBridgeDetail +local GetSavedBotSpec +local GetSpecFromBridgeDetail +local GuessSpecFromBuild +local GetClassColorCode local function AddDarkBackdrop(frame, bgAlpha) if not frame or not frame.SetBackdrop then @@ -66,6 +77,31 @@ local function AddSystemMessage(message) end end +local function OpenCandidateInventory(candidate) + local botName = candidate and candidate.name + + if not botName or botName == "" then + return + end + + if MultiBot then + if type(MultiBot.RequestBotInventory) == "function" then + MultiBot.RequestBotInventory(botName) + return + end + + if type(MultiBot.InitializeInventoryFrame) == "function" then + local inventory = MultiBot.InitializeInventoryFrame() + if inventory and type(inventory.requestBotInventory) == "function" then + inventory:requestBotInventory(botName) + return + end + end + end + + AddSystemMessage(MBLocal("lootmaster.inventory_unavailable", "Inventory frame is not available.")) +end + local function RunLater(delay, callback) if MultiBot and type(MultiBot.TimerAfter) == "function" then MultiBot.TimerAfter(delay, callback) @@ -196,7 +232,7 @@ local function FindKnownSpecNameFromBuild(name, build) return nil end -local function GuessSpecFromBuild(classToken, build) +GuessSpecFromBuild = function(classToken, build) if type(build) ~= "string" then return nil end @@ -278,6 +314,415 @@ local function ApplyClassIcon(texture, classToken) texture:Show() end +local function GetCandidateSpecLabel(candidate) + if not candidate then return MBLocal("lootmaster.unknown_spec", "Unknown spec") end + + local spec, build = GetSpecFromBridgeDetail(candidate) + if not spec then + spec, build = GetSavedBotSpec(candidate.name) + end + if not spec then + spec = GuessSpecFromBuild(candidate.classFile or candidate.classToken, candidate.build or build) + end + + return spec or MBLocal("lootmaster.unknown_spec", "Unknown spec") +end + +local function Clamp(value, minValue, maxValue) + if value < minValue then return minValue end + if value > maxValue then return maxValue end + return value +end + +local function TextHasAny(text, patterns) + if type(text) ~= "string" then return false end + text = string.lower(text) + + for _, pattern in ipairs(patterns) do + if string.find(text, pattern) then + return true + end + end + + return false +end + +local ARMOR_RANK = { CLOTH = 1, LEATHER = 2, MAIL = 3, PLATE = 4 } +local CLASS_ARMOR_RANK = { + WARRIOR = 4, PALADIN = 4, DEATHKNIGHT = 4, + HUNTER = 3, SHAMAN = 3, + ROGUE = 2, DRUID = 2, + PRIEST = 1, MAGE = 1, WARLOCK = 1, +} + +local CLASS_WEAPONS = { + WARRIOR = { AXE_1H = true, AXE_2H = true, MACE_1H = true, MACE_2H = true, SWORD_1H = true, SWORD_2H = true, DAGGER = true, FIST = true, POLEARM = true, SHIELD = true, BOW = true, CROSSBOW = true, GUN = true, THROWN = true }, + PALADIN = { AXE_1H = true, AXE_2H = true, MACE_1H = true, MACE_2H = true, SWORD_1H = true, SWORD_2H = true, POLEARM = true, SHIELD = true }, + HUNTER = { AXE_1H = true, AXE_2H = true, SWORD_1H = true, SWORD_2H = true, DAGGER = true, FIST = true, POLEARM = true, STAFF = true, BOW = true, CROSSBOW = true, GUN = true, THROWN = true }, + ROGUE = { AXE_1H = true, MACE_1H = true, SWORD_1H = true, DAGGER = true, FIST = true, BOW = true, CROSSBOW = true, GUN = true, THROWN = true }, + PRIEST = { MACE_1H = true, DAGGER = true, STAFF = true, WAND = true, HOLDABLE = true }, + DEATHKNIGHT = { AXE_1H = true, AXE_2H = true, MACE_1H = true, MACE_2H = true, SWORD_1H = true, SWORD_2H = true, POLEARM = true }, + SHAMAN = { AXE_1H = true, MACE_1H = true, DAGGER = true, FIST = true, STAFF = true, SHIELD = true, HOLDABLE = true }, + MAGE = { DAGGER = true, SWORD_1H = true, STAFF = true, WAND = true, HOLDABLE = true }, + WARLOCK = { DAGGER = true, SWORD_1H = true, STAFF = true, WAND = true, HOLDABLE = true }, + DRUID = { MACE_1H = true, MACE_2H = true, DAGGER = true, FIST = true, STAFF = true, POLEARM = true, HOLDABLE = true }, +} + +local function GetCandidateTreeIndex(candidate) + local detail = candidate and GetBridgeDetail(candidate.name) + if type(detail) == "table" then + local a, b, c = tonumber(detail.talent1 or 0) or 0, tonumber(detail.talent2 or 0) or 0, tonumber(detail.talent3 or 0) or 0 + if c > a and c > b then return 3 end + if b > a and b > c then return 2 end + return 1 + end + + local build = candidate and candidate.build + if not build then + local _, savedBuild = GetSavedBotSpec(candidate and candidate.name) + build = savedBuild + end + + local a, b, c = tostring(build or ""):match("^(%d+)[%/%-](%d+)[%/%-](%d+)$") + a, b, c = tonumber(a), tonumber(b), tonumber(c) + if not a or not b or not c then return nil end + if c > a and c > b then return 3 end + if b > a and b > c then return 2 end + return 1 +end + +local function GetCandidateRole(candidate) + local classFile = candidate and (candidate.classFile or candidate.classToken) + local tree = GetCandidateTreeIndex(candidate) + + if classFile == "WARRIOR" then return tree == 3 and "tank" or "physical" end + if classFile == "PALADIN" then return tree == 1 and "healer" or (tree == 2 and "tank" or "physical") end + if classFile == "PRIEST" then return tree == 3 and "caster" or "healer" end + if classFile == "DEATHKNIGHT" then return tree == 1 and "tank" or "physical" end + if classFile == "SHAMAN" then return tree == 1 and "caster" or (tree == 2 and "physical" or "healer") end + if classFile == "DRUID" then return tree == 1 and "caster" or (tree == 2 and "feral" or "healer") end + if classFile == "MAGE" or classFile == "WARLOCK" then return "caster" end + if classFile == "HUNTER" or classFile == "ROGUE" then return "physical" end + + return "unknown" +end + +local function GetArmorType(itemSubType, equipLoc) + if equipLoc == "INVTYPE_SHIELD" then return "SHIELD" end + if TextHasAny(itemSubType, { "cloth", "tissu" }) then return "CLOTH" end + if TextHasAny(itemSubType, { "leather", "cuir" }) then return "LEATHER" end + if TextHasAny(itemSubType, { "mail", "maille" }) then return "MAIL" end + if TextHasAny(itemSubType, { "plate", "plaque" }) then return "PLATE" end + return nil +end + +local function GetWeaponType(itemSubType, equipLoc) + local twoHand = equipLoc == "INVTYPE_2HWEAPON" + if equipLoc == "INVTYPE_SHIELD" then return "SHIELD" end + if equipLoc == "INVTYPE_HOLDABLE" then return "HOLDABLE" end + if equipLoc ~= "INVTYPE_WEAPON" and equipLoc ~= "INVTYPE_2HWEAPON" and equipLoc ~= "INVTYPE_WEAPONMAINHAND" and equipLoc ~= "INVTYPE_WEAPONOFFHAND" and equipLoc ~= "INVTYPE_RANGED" and equipLoc ~= "INVTYPE_RANGEDRIGHT" and equipLoc ~= "INVTYPE_THROWN" then return nil end + + if TextHasAny(itemSubType, { "crossbow", "arbal" }) then return "CROSSBOW" end + if TextHasAny(itemSubType, { "bow", "arc" }) then return "BOW" end + if TextHasAny(itemSubType, { "gun", "fusil" }) then return "GUN" end + if TextHasAny(itemSubType, { "wand", "baguette" }) then return "WAND" end + if TextHasAny(itemSubType, { "thrown", "jet" }) then return "THROWN" end + if TextHasAny(itemSubType, { "dagger", "dague" }) then return "DAGGER" end + if TextHasAny(itemSubType, { "fist", "pugilat" }) then return "FIST" end + if TextHasAny(itemSubType, { "polearm", "hast" }) then return "POLEARM" end + if TextHasAny(itemSubType, { "staff", "b.ton" }) then return "STAFF" end + if TextHasAny(itemSubType, { "axe", "hache" }) then return twoHand and "AXE_2H" or "AXE_1H" end + if TextHasAny(itemSubType, { "mace", "masse" }) then return twoHand and "MACE_2H" or "MACE_1H" end + if TextHasAny(itemSubType, { "sword", "epee", "p..e" }) then return twoHand and "SWORD_2H" or "SWORD_1H" end + + return nil +end + +local lootScanTooltip +local function GetLootTooltipText(itemLink) + if not itemLink then return "" end + if not lootScanTooltip then + lootScanTooltip = CreateFrame("GameTooltip", "MultiBotLootMasterScanTooltip", nil, "GameTooltipTemplate") + end + + lootScanTooltip:SetOwner(UIParent, "ANCHOR_NONE") + lootScanTooltip:ClearLines() + lootScanTooltip:SetHyperlink(itemLink) + + local lines = {} + for i = 2, lootScanTooltip:NumLines() do + local left = _G["MultiBotLootMasterScanTooltipTextLeft" .. i] + local text = left and left:GetText() + if text and text ~= "" then + lines[#lines + 1] = text + end + end + + lootScanTooltip:Hide() + return table.concat(lines, " ") +end + +local function GetItemStatFlags(itemLink) + local text = GetLootTooltipText(itemLink) + local hasDefense = TextHasAny(text, { "defense", "d.fense", "dodge", "esquive", "parry", "parade", "block", "blocage" }) + local hasAgility = TextHasAny(text, { "agility", "agilit" }) + local hasStrength = TextHasAny(text, { "strength", "force" }) + local hasAttackPower = TextHasAny(text, { "attack power", "puissance d.attaque" }) + local hasExpertise = TextHasAny(text, { "expertise" }) + local hasArmorPen = TextHasAny(text, { "armor penetration", "p.n.tration d.armure" }) + local hasSpellPower = TextHasAny(text, { "spell power", "puissance des sorts", "spell damage", "d.g.ts des sorts" }) + local hasHealing = TextHasAny(text, { "healing", "soins" }) + local hasMp5 = TextHasAny(text, { "mana per", "mp5", "mana toutes les" }) + local hasSpirit = TextHasAny(text, { "spirit", "esprit" }) + local hasIntellect = TextHasAny(text, { "intellect", "intelligence" }) + local hasHit = TextHasAny(text, { "hit rating", "score de toucher" }) + local hasCrit = TextHasAny(text, { "critical strike", "coup critique", "critique" }) + local hasHaste = TextHasAny(text, { "haste", "h.te" }) + + local hasPhysicalPower = hasAgility or hasStrength or hasAttackPower or hasExpertise or hasArmorPen + local hasHealerHint = hasHealing or hasMp5 or hasSpirit + local primary + + if hasDefense then + primary = "tank" + elseif hasPhysicalPower and not hasSpellPower and not hasHealing then + primary = "physical" + elseif hasSpellPower or hasHealing then + if hasHit then + primary = "caster" + elseif hasHealerHint then + primary = "healer" + else + primary = "caster" + end + elseif hasPhysicalPower then + primary = "physical" + elseif hasIntellect then + primary = "caster" + end + + return { + primary = primary, + tank = primary == "tank", + healer = primary == "healer", + caster = primary == "caster", + physical = primary == "physical", + } +end + +local function BuildLootItemProfile(slot) + local itemLink = GetLootSlotLink(slot) + local _, itemName = GetLootSlotInfo(slot) + local query = itemLink or itemName + local _, _, _, _, _, itemType, itemSubType, _, equipLoc = GetItemInfo(query or "") + + return { + link = itemLink, + itemType = itemType, + itemSubType = itemSubType, + equipLoc = equipLoc, + armorType = GetArmorType(itemSubType, equipLoc), + weaponType = GetWeaponType(itemSubType, equipLoc), + statFlags = GetItemStatFlags(itemLink), + } +end + +local function BuildLootPreferenceKey(profile) + if type(profile) ~= "table" then + return nil + end + + local flags = profile.statFlags or {} + return table.concat({ + flags.primary or "any", + profile.armorType or "noarmor", + profile.weaponType or "noweapon", + profile.equipLoc or "noequip", + }, "|") +end + +local function GetLootPreferenceStore(createIfMissing) + local store + + if MultiBot and MultiBot.Store then + if createIfMissing and type(MultiBot.Store.EnsureUIChildStore) == "function" then + store = MultiBot.Store.EnsureUIChildStore("lootMaster") + elseif type(MultiBot.Store.GetUIChildStore) == "function" then + store = MultiBot.Store.GetUIChildStore("lootMaster") + end + end + + if type(store) ~= "table" then + if not createIfMissing then + store = _G.MultiBotSave and _G.MultiBotSave.lootMaster + if type(store) ~= "table" then + return nil + end + else + _G.MultiBotSave = _G.MultiBotSave or {} + _G.MultiBotSave.lootMaster = _G.MultiBotSave.lootMaster or {} + store = _G.MultiBotSave.lootMaster + end + end + + if createIfMissing then + store.preferences = store.preferences or {} + end + + return type(store.preferences) == "table" and store.preferences or nil +end + +local function GetPreferredLootCandidate(profile) + local key = BuildLootPreferenceKey(profile) + local preferences = key and GetLootPreferenceStore(false) + local value = preferences and preferences[key] + + if type(value) == "table" then + return value.name + end + + return value +end + +local function SaveLootPreference(slot, candidate) + if not slot or not candidate or not candidate.name then + return + end + + local profile = BuildLootItemProfile(slot) + local key = BuildLootPreferenceKey(profile) + local preferences = key and GetLootPreferenceStore(true) + + if not preferences then + return + end + + preferences[key] = { + name = candidate.name, + savedAt = time and time() or 0, + } + + AddSystemMessage(string.format(MBLocal("lootmaster.preference_saved", "Preference saved: similar items -> %s."), candidate.name)) +end + +local function ClearLootPreference(slot) + if not slot then + return false + end + + local profile = BuildLootItemProfile(slot) + local key = BuildLootPreferenceKey(profile) + local preferences = key and GetLootPreferenceStore(false) + + if not preferences or not preferences[key] then + AddSystemMessage(MBLocal("lootmaster.preference_missing", "No preference saved for this item type.")) + return false + end + + preferences[key] = nil + AddSystemMessage(MBLocal("lootmaster.preference_cleared", "Preference cleared for similar items.")) + return true +end + +local function RoleMatchesStats(role, flags) + if not flags then return false end + if flags.primary then + if role == "feral" then + return flags.primary == "tank" or flags.primary == "physical" + end + + return role == flags.primary + end + + return role and flags[role] == true +end + +local function GetRoleStatScore(role, flags) + if not flags or not flags.primary then return 0 end + if RoleMatchesStats(role, flags) then return 42 end + if flags.primary == "physical" and (role == "healer" or role == "caster") then return -45 end + if flags.primary == "caster" and (role == "physical" or role == "tank" or role == "feral") then return -45 end + if flags.primary == "healer" and role ~= "healer" then return -45 end + if flags.primary == "tank" and role ~= "tank" and role ~= "feral" then return -35 end + return -25 +end + +local function CandidateHadRecentLoot(candidate) + local key = NormalizeName(candidate and candidate.name) + return key and recentLootByCandidate[key] ~= nil +end + +local function ScoreCandidateForItem(candidate, profile) + local classFile = candidate.classFile or candidate.classToken + local score = 45 + local flags = profile.statFlags + local hasKnownStats = flags and (flags.primary or flags.tank or flags.healer or flags.caster or flags.physical) + candidate.lootRole = GetCandidateRole(candidate) + + if profile.armorType == "SHIELD" then + score = score + ((CLASS_WEAPONS[classFile] and CLASS_WEAPONS[classFile].SHIELD) and 18 or -35) + elseif profile.armorType then + local itemRank = ARMOR_RANK[profile.armorType] + local classRank = CLASS_ARMOR_RANK[classFile] + if itemRank and classRank and itemRank <= classRank then + if itemRank == classRank then + score = score + 18 + elseif flags and (flags.primary == "caster" or flags.primary == "healer") then + score = score + 2 + else + score = score - 18 + end + else + score = score - 35 + end + end + + if profile.weaponType then + score = score + ((CLASS_WEAPONS[classFile] and CLASS_WEAPONS[classFile][profile.weaponType]) and 18 or -35) + end + + if hasKnownStats then + score = score + GetRoleStatScore(candidate.lootRole, flags) + end + + if CandidateHadRecentLoot(candidate) then + score = score - 15 + end + + candidate.lootScore = Clamp(score, 1, 99) +end + +local function ApplyLootPreferenceScore(candidate, profile) + local preferredName = GetPreferredLootCandidate(profile) + + candidate.lootPreference = false + + if preferredName and NormalizeName(candidate.name) == NormalizeName(preferredName) then + candidate.lootPreference = true + candidate.lootScore = Clamp((candidate.lootScore or 0) + LOOT_PREFERENCE_BONUS, 1, 99) + end +end + +local function GetCandidateScoreColor(score) + if score >= 80 then return "|cff33ff66" end + if score >= 60 then return "|cffffff66" end + if score >= 35 then return "|cffff9933" end + return "|cffff4444" +end + +local function BuildCandidateSelectionText(candidate) + if not candidate then return MBLocal("lootmaster.select_bot", "Select a bot") end + local score = candidate.lootScore or 0 + local prefText = candidate.lootPreference and " |cffffcc00Pref|r" or "" + return GetClassColorCode(candidate.classFile) .. candidate.name .. "|r " .. GetCandidateScoreColor(score) .. score .. "%|r" .. prefText +end + +local function BuildCandidateDropdownText(candidate) + local score = candidate.lootScore or 0 + local prefText = candidate.lootPreference and " |cffffcc00Pref|r" or "" + return GetClassColorCode(candidate.classFile) .. candidate.name .. "|r " .. GetCandidateSpecLabel(candidate) .. " " .. GetCandidateScoreColor(score) .. score .. "%|r" .. prefText +end + local function SetClassIconTooltip(button, candidate) if not button then return @@ -346,7 +791,7 @@ local function NormalizeUnitName(name) return string.match(name, "^([^%-]+)") or name end -local function GetClassColorCode(classFile) +GetClassColorCode = function(classFile) local color = classFile and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classFile] if not color then @@ -443,7 +888,7 @@ local function SetLootIconTooltip(button, itemLink) end) end -local function GetBridgeDetail(name) +GetBridgeDetail = function(name) if not name or name == "" then return nil end @@ -465,7 +910,7 @@ local function GetBridgeDetail(name) return nil end -local function GetSavedBotSpec(name) +GetSavedBotSpec = function(name) if not name or not _G.MultiBotGlobalSave then return nil end @@ -484,7 +929,7 @@ local function GetSavedBotSpec(name) return fields[3], fields[4] end -local function GetSpecFromBridgeDetail(candidate) +GetSpecFromBridgeDetail = function(candidate) local detail = candidate and GetBridgeDetail(candidate.name) if type(detail) ~= "table" then return nil @@ -555,8 +1000,15 @@ local function SetClassIconTooltip(button, candidate) end GameTooltip:AddLine(GetCandidateSpecText(current), 0.6, 0.8, 1) + if current.lootScore then + GameTooltip:AddLine(MBLocal("lootmaster.priority_score", "Priorite:") .. " " .. GetCandidateScoreColor(current.lootScore) .. current.lootScore .. "%|r", 0.9, 0.9, 0.9) + end + GameTooltip:AddLine(MBLocal("lootmaster.inventory_hint", "Right-click: open inventory/equipment."), 0.6, 1, 0.6) GameTooltip:Show() end) + button:SetScript("OnLeave", function() + GameTooltip:Hide() + end) end local function BuildUnitClassLookup() @@ -638,6 +1090,7 @@ local function BuildCandidateList(slot) local seen = {} local limit = GetCandidateScanLimit() local unitClasses = BuildUnitClassLookup() + local itemProfile = BuildLootItemProfile(slot) for index = 1, limit do local name = GetCandidateName(index) @@ -651,7 +1104,7 @@ local function BuildCandidateList(slot) local className, classToken = GetCandidateClassInfo(name) - candidates[#candidates + 1] = { + local candidate = { index = index, name = name, className = className, @@ -659,9 +1112,20 @@ local function BuildCandidateList(slot) build = GetKnownBuildForCandidate(name), classFile = unitClasses[NormalizeUnitName(name)] or classToken, } + + ScoreCandidateForItem(candidate, itemProfile) + ApplyLootPreferenceScore(candidate, itemProfile) + candidates[#candidates + 1] = candidate end end + table.sort(candidates, function(left, right) + if (left.lootScore or 0) == (right.lootScore or 0) then + return tostring(left.name or "") < tostring(right.name or "") + end + return (left.lootScore or 0) > (right.lootScore or 0) + end) + return candidates end @@ -695,6 +1159,78 @@ local function BuildLootSlotKey(slot) return tostring(slot) .. ":" .. tostring(itemName or "") .. ":" .. tostring(quantity or 1) end +local function EnsureLootHistoryLine(frame, index) + if not frame or not frame.historyContent then + return nil + end + + frame.historyLines = frame.historyLines or {} + + if frame.historyLines[index] then + return frame.historyLines[index] + end + + local line = frame.historyContent:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + line:SetWidth(FRAME_WIDTH - 72) + line:SetJustifyH("LEFT") + + if index == 1 then + line:SetPoint("TOPLEFT", frame.historyContent, "TOPLEFT", 0, -2) + else + line:SetPoint("TOPLEFT", frame.historyLines[index - 1], "BOTTOMLEFT", 0, -1) + end + + frame.historyLines[index] = line + return line +end + +local function UpdateLootHistoryFrame(frame) + frame = frame or LootMasterUI.frame + + if not frame or not frame.historyLines then + return + end + + if #lootHistory == 0 then + local line = EnsureLootHistoryLine(frame, 1) + line:SetText("|cff888888" .. MBLocal("lootmaster.history_empty", "No loot assigned yet.") .. "|r") + line:Show() + + for i = 2, #frame.historyLines do + frame.historyLines[i]:Hide() + end + + frame.historyContent:SetHeight(LOOT_HISTORY_HEIGHT) + return + end + + for i = 1, #lootHistory do + local entry = lootHistory[i] + local line = EnsureLootHistoryLine(frame, i) + + if entry then + line:SetText(string.format("|cffaaaaaa%s|r %s |cffaaaaaa->|r %s", entry.timeText or "", entry.itemText or "?", entry.candidateName or "?")) + line:Show() + end + end + + for i = #lootHistory + 1, #frame.historyLines do + frame.historyLines[i]:Hide() + end + + frame.historyContent:SetHeight(math.max(LOOT_HISTORY_HEIGHT, (#lootHistory * LOOT_HISTORY_LINE_HEIGHT) + 8)) +end + +local function AddLootHistory(itemText, candidateName) + table.insert(lootHistory, 1, { + itemText = itemText or MBLocal("lootmaster.unknown_item", "Unknown item"), + candidateName = candidateName or MBLocal("lootmaster.unknown_candidate", "unknown"), + timeText = date and date("%H:%M") or "", + }) + + UpdateLootHistoryFrame() +end + local function MarkLootSlotAssigned(slot) assignedLootKeys[BuildLootSlotKey(slot)] = GetTime and GetTime() or time() assignedLootSlots[slot] = GetTime and GetTime() or time() @@ -746,6 +1282,19 @@ local function PruneAssignedLootKeys() assignedLootKeys[key] = nil end end + + for name, expiresAt in pairs(recentLootByCandidate) do + if (tonumber(expiresAt) or 0) <= now then + recentLootByCandidate[name] = nil + end + end +end + +local function MarkCandidateRecentLoot(candidateName) + local key = NormalizeName(candidateName) + if key and key ~= "" then + recentLootByCandidate[key] = (GetTime and GetTime() or time() or 0) + RECENT_LOOT_PENALTY_SECONDS + end end local function MarkLootSlotAssigned(slot) @@ -832,11 +1381,43 @@ local function CreateBasicFrame() LootMasterUI:Refresh() end) + frame.history = CreateFrame("Frame", nil, contentParent) + frame.history:SetPoint("BOTTOMLEFT", contentParent, "BOTTOMLEFT", 8, 8) + frame.history:SetPoint("BOTTOMRIGHT", contentParent, "BOTTOMRIGHT", -8, 8) + frame.history:SetHeight(LOOT_HISTORY_HEIGHT) + + frame.historyTitle = frame.history:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + frame.historyTitle:SetPoint("TOPLEFT", frame.history, "TOPLEFT", 0, 0) + frame.historyTitle:SetText(MBLocal("lootmaster.history_title", "Recent loot")) + + frame.historyScroll = CreateFrame("ScrollFrame", nil, frame.history, "UIPanelScrollFrameTemplate") + frame.historyScroll:SetPoint("TOPLEFT", frame.historyTitle, "BOTTOMLEFT", 0, -2) + frame.historyScroll:SetPoint("BOTTOMRIGHT", frame.history, "BOTTOMRIGHT", -24, 0) + frame.historyScroll:EnableMouseWheel(true) + frame.historyScroll:SetScript("OnMouseWheel", function(self, delta) + local current = self:GetVerticalScroll() or 0 + local maxScroll = self:GetVerticalScrollRange() or 0 + + if delta < 0 then + self:SetVerticalScroll(math.min(current + 20, maxScroll)) + else + self:SetVerticalScroll(math.max(current - 20, 0)) + end + end) + + frame.historyContent = CreateFrame("Frame", nil, frame.historyScroll) + frame.historyContent:SetWidth(FRAME_WIDTH - 72) + frame.historyContent:SetHeight(LOOT_HISTORY_HEIGHT) + frame.historyScroll:SetScrollChild(frame.historyContent) + + frame.historyLines = {} + frame.content = CreateFrame("Frame", nil, contentParent) frame.content:SetPoint("TOPLEFT", frame.status, "BOTTOMLEFT", 0, -16) - frame.content:SetPoint("BOTTOMRIGHT", contentParent, "BOTTOMRIGHT", -8, 8) + frame.content:SetPoint("BOTTOMRIGHT", frame.history, "TOPRIGHT", 0, 8) frame.rows = {} + UpdateLootHistoryFrame(frame) frame:Hide() @@ -930,8 +1511,8 @@ local function AcquireRow(frame, index) ownerRow.dropdown = ownerRow.dropdown or CreateFrame("Frame", nil, UIParent) local dropdown = ownerRow.dropdown dropdown:SetFrameStrata("TOOLTIP") - dropdown:SetWidth(DROPDOWN_WIDTH + 32) - dropdown:SetHeight((#candidates * 20) + 12) + dropdown:SetWidth(DROPDOWN_WIDTH + 72) + dropdown:SetHeight((#candidates * 22) + 12) dropdown:ClearAllPoints() dropdown:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 0, -2) dropdown:SetBackdrop({ @@ -951,7 +1532,8 @@ local function AcquireRow(frame, index) if not button then button = CreateFrame("Button", nil, dropdown) dropdown.buttons[i] = button - button:SetHeight(18) + button:RegisterForClicks("LeftButtonUp", "RightButtonUp") + button:SetHeight(20) button:SetPoint("LEFT", dropdown, "LEFT", 8, 0) button:SetPoint("RIGHT", dropdown, "RIGHT", -8, 0) @@ -983,14 +1565,27 @@ local function AcquireRow(frame, index) button.ownerRow = ownerRow button.candidate = candidate - button.text:SetText(classColor .. candidate.name .. "|r") - button:SetScript("OnClick", function(clicked) + button.text:SetText(BuildCandidateDropdownText(candidate)) + SetClassIconTooltip(button, candidate) + + button:SetScript("OnClick", function(clicked, mouseButton) local selected = clicked.candidate local selectedRow = clicked.ownerRow + if mouseButton == "RightButton" then + OpenCandidateInventory(selected) + dropdown:Hide() + return + end + + if not selected or not selectedRow then + return + end + selectedRow.selectedCandidateIndex = selected.index selectedRow.selectedCandidateName = selected.name - selectedRow.selectButton:SetText(GetClassColorCode(selected.classFile) .. selected.name .. "|r") + selectedRow.selectedCandidate = selected + selectedRow.selectButton:SetText(BuildCandidateSelectionText(selected)) dropdown:Hide() end) @@ -1024,8 +1619,18 @@ local function AcquireRow(frame, index) row.assignButton:SetWidth(90) row.assignButton:SetHeight(22) row.assignButton:SetPoint("LEFT", row.selectButton, "RIGHT", 8, 0) + row.assignButton:RegisterForClicks("LeftButtonUp", "RightButtonUp") row.assignButton:SetText(MBLocal("lootmaster.assign", "Assign")) - row.assignButton:SetScript("OnClick", function(self) + row.assignButton:SetScript("OnEnter", function(self) + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:AddLine(MBLocal("lootmaster.assign", "Assign"), 1, 1, 1) + GameTooltip:AddLine(MBLocal("lootmaster.preference_hint", "Right-click: remember. Shift-right-click: forget."), 0.6, 1, 0.6) + GameTooltip:Show() + end) + row.assignButton:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + row.assignButton:SetScript("OnClick", function(self, mouseButton) local ownerRow = self.ownerRow if not ownerRow or not ownerRow.slot or not ownerRow.selectedCandidateIndex then @@ -1033,6 +1638,20 @@ local function AcquireRow(frame, index) return end + if mouseButton == "RightButton" then + if IsShiftKeyDown and IsShiftKeyDown() then + ClearLootPreference(ownerRow.slot) + else + SaveLootPreference(ownerRow.slot, ownerRow.selectedCandidate) + end + + if LootMasterUI.frame and LootMasterUI.frame:IsShown() then + LootMasterUI:Refresh() + end + + return + end + AssignLoot(ownerRow.slot, ownerRow.selectedCandidateIndex, ownerRow.selectedCandidateName) end) @@ -1079,7 +1698,7 @@ local function ApplyCandidateSelection(row, candidate) row.selectedCandidateIndex = candidate.index row.selectedCandidateName = candidate.name row.selectedCandidate = candidate - row.selectButton:SetText(GetClassColorCode(candidate.classFile) .. candidate.name .. "|r") + row.selectButton:SetText(BuildCandidateSelectionText(candidate)) end local function PrepareRowSelection(row, candidates) @@ -1153,7 +1772,9 @@ AssignLoot = function(slot, candidateIndex, candidateName) end MarkLootSlotAssigned(slot) + MarkCandidateRecentLoot(candidateName) GiveMasterLoot(slot, candidateIndex) + AddLootHistory(itemText, candidateName) if LootMasterUI.frame and LootMasterUI.frame:IsShown() then LootMasterUI:Refresh() From 59f6f245210cbc8718b1b1c03e14408142cefffa Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:45:19 +0200 Subject: [PATCH 06/11] Lua Lint fix --- .luacheckrc | 3 ++- UI/MultiBotLootMasterUI.lua | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 5fd93fa..dd5054f 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -31,7 +31,8 @@ globals = { "IsShiftKeyDown", "RAID_CLASS_COLORS", "ITEM", "UISpecialFrames", "IsControlKeyDown", "IsShiftKeyDown", "INV_SLOT_MAINHAND", "LoadAddOn", "ShowUIPanel", "HandleModifiedItemClick", "ChatFontNormal", "UnitIsDead", "ShowPrompt", "GameFontHighlightSmall", "GameFontNormalSmall", "SEARCH", "UnitIsUnit", "MB_INVENTORY_LABEL", "INVENTORY_TOOLTIP", "BAGSLOT", "GetItemInfoInstant", "sendInventoryItemCommand", "LE_ITEM_CLASS_QUESTITEM", "ITEMS", "LOADING", "QUEST_LOG", "INSPECT", "SPELLBOOK", "MB_TAB_TITLE_DEFAULT", "ensureHiddenTooltip", "IsInGuild", "GetGuildInfo", "GetGuildRosterShowOffline", - "SetGuildRosterShowOffline", "PLAYER","Ambiguate", "ChatFrame_AddMessageEventFilter", "ChatTypeInfo", "x", "y" + "SetGuildRosterShowOffline", "PLAYER","Ambiguate", "ChatFrame_AddMessageEventFilter", "ChatTypeInfo", "x", "y", "CLASS_ICON_TCOORDS", "GetLootSlotLink", "GetLootSlotInfo", "GetLootMethod", "GetMasterLootCandidate", + "LOCALIZED_CLASS_NAMES_MALE", "LOCALIZED_CLASS_NAMES_FEMALE", "date", "LootSlotIsCoin", "LootSlotIsItem", "classColor", "GetNumLootItems" } diff --git a/UI/MultiBotLootMasterUI.lua b/UI/MultiBotLootMasterUI.lua index d76f0e4..a90b0d4 100644 --- a/UI/MultiBotLootMasterUI.lua +++ b/UI/MultiBotLootMasterUI.lua @@ -990,15 +990,15 @@ local function SetClassIconTooltip(button, candidate) if not current then return end - + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") GameTooltip:AddLine(current.name or MBLocal("lootmaster.unknown_candidate", "unknown"), 1, 1, 1) - + local className = current.className or GetLocalizedClassName(current.classFile) if className then GameTooltip:AddLine(className, 0.75, 0.75, 0.75) end - + GameTooltip:AddLine(GetCandidateSpecText(current), 0.6, 0.8, 1) if current.lootScore then GameTooltip:AddLine(MBLocal("lootmaster.priority_score", "Priorite:") .. " " .. GetCandidateScoreColor(current.lootScore) .. current.lootScore .. "%|r", 0.9, 0.9, 0.9) @@ -1114,7 +1114,7 @@ local function BuildCandidateList(slot) } ScoreCandidateForItem(candidate, itemProfile) - ApplyLootPreferenceScore(candidate, itemProfile) + ApplyLootPreferenceScore(candidate, itemProfile) candidates[#candidates + 1] = candidate end end @@ -1218,7 +1218,7 @@ local function UpdateLootHistoryFrame(frame) frame.historyLines[i]:Hide() end - frame.historyContent:SetHeight(math.max(LOOT_HISTORY_HEIGHT, (#lootHistory * LOOT_HISTORY_LINE_HEIGHT) + 8)) + frame.historyContent:SetHeight(math.max(LOOT_HISTORY_HEIGHT, (#lootHistory * LOOT_HISTORY_LINE_HEIGHT) + 8)) end local function AddLootHistory(itemText, candidateName) @@ -1417,7 +1417,7 @@ local function CreateBasicFrame() frame.content:SetPoint("BOTTOMRIGHT", frame.history, "TOPRIGHT", 0, 8) frame.rows = {} - UpdateLootHistoryFrame(frame) + UpdateLootHistoryFrame(frame) frame:Hide() @@ -1619,7 +1619,7 @@ local function AcquireRow(frame, index) row.assignButton:SetWidth(90) row.assignButton:SetHeight(22) row.assignButton:SetPoint("LEFT", row.selectButton, "RIGHT", 8, 0) - row.assignButton:RegisterForClicks("LeftButtonUp", "RightButtonUp") + row.assignButton:RegisterForClicks("LeftButtonUp", "RightButtonUp") row.assignButton:SetText(MBLocal("lootmaster.assign", "Assign")) row.assignButton:SetScript("OnEnter", function(self) GameTooltip:SetOwner(self, "ANCHOR_RIGHT") @@ -1774,7 +1774,7 @@ AssignLoot = function(slot, candidateIndex, candidateName) MarkLootSlotAssigned(slot) MarkCandidateRecentLoot(candidateName) GiveMasterLoot(slot, candidateIndex) - AddLootHistory(itemText, candidateName) + AddLootHistory(itemText, candidateName) if LootMasterUI.frame and LootMasterUI.frame:IsShown() then LootMasterUI:Refresh() From 13b00d693e814d71d78f7cdcf5edde80b09adfd1 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 1 May 2026 15:49:14 +0200 Subject: [PATCH 07/11] Frame improvments --- Core/MultiBot.lua | 29 + Core/MultiBotComm.lua | 95 +++- Locales/MultiBotAceLocale-frFR.lua | 16 +- TODO.md | 3 + UI/MultiBotLootMasterUI.lua | 827 +++++++++++++++++++++++++---- UI/MultiBotOptions.lua | 47 +- 6 files changed, 911 insertions(+), 106 deletions(-) diff --git a/Core/MultiBot.lua b/Core/MultiBot.lua index db2a907..211e1a5 100644 --- a/Core/MultiBot.lua +++ b/Core/MultiBot.lua @@ -517,6 +517,35 @@ function MultiBot.SetMainUIVisibleConfig(value) return visible end +local LOOT_MASTER_UI_ENABLED_DEFAULT = true + +function MultiBot.GetLootMasterUIEnabled() + local value = MultiBot.Store and MultiBot.Store.GetUIValue and MultiBot.Store.GetUIValue("lootMasterUIEnabled") + if type(value) == "boolean" then + return value + end + + local save = ensureSavedVariables() + if type(save.LootMasterUIEnabled) == "boolean" then + return save.LootMasterUIEnabled + end + + return LOOT_MASTER_UI_ENABLED_DEFAULT +end + +function MultiBot.SetLootMasterUIEnabled(value) + local enabled = not not value + + if MultiBot.Store and MultiBot.Store.SetUIValue then + MultiBot.Store.SetUIValue("lootMasterUIEnabled", enabled) + else + local save = ensureSavedVariables() + save.LootMasterUIEnabled = enabled + end + + return enabled +end + local function getLegacyCharacterStateRoot(createIfMissing) local saved = _G.MultiBotSaved if type(saved) ~= "table" then diff --git a/Core/MultiBotComm.lua b/Core/MultiBotComm.lua index 0540a09..081aafe 100644 --- a/Core/MultiBotComm.lua +++ b/Core/MultiBotComm.lua @@ -97,6 +97,7 @@ local function ensureBridgeState() state.roster = state.roster or {} state.states = state.states or {} state.details = state.details or {} + state.professions = state.professions or {} state.pvpStats = state.pvpStats or {} state.stats = state.stats or {} state.quests = state.quests or {} @@ -577,6 +578,33 @@ local function parseBridgeDetailPayload(payload) } end +local function parseBridgeProfessionPayload(payload) + local name, professionPayload = splitOnce(payload or "", "~") + + name = trim(urlDecodeField(name)) + if name == "" then + return nil + end + + local professions = {} + for token in string.gmatch(professionPayload or "", "([^;]+)") do + token = trim(urlDecodeField(token)) + + local profession, value = splitOnce(token, ":") + profession = string.lower(trim(profession or "")) + + if profession ~= "" then + professions[profession] = value ~= "" and value or true + end + end + + return { + name = name, + professions = professions, + lastUpdateAt = safeNow(), + } +end + local function parseRosterEntry(entry) local fields = {} for value in string.gmatch(entry or "", "([^,]+)") do @@ -667,7 +695,19 @@ function Comm.ApplyBotDetailPayload(payload) return nil end - state.details[string.lower(detail.name)] = detail + local key = string.lower(detail.name) + local existing = state.details[key] + local professionEntry = state.professions[key] + + if type(existing) == "table" and type(existing.professions) == "table" then + detail.professions = existing.professions + end + + if type(professionEntry) == "table" and type(professionEntry.professions) == "table" then + detail.professions = professionEntry.professions + end + + state.details[key] = detail if MultiBot.ApplyBridgeBotDetail then MultiBot.ApplyBridgeBotDetail(detail) @@ -677,6 +717,45 @@ function Comm.ApplyBotDetailPayload(payload) return detail end +function Comm.ApplyBotProfessionPayload(payload) + local state = ensureBridgeState() + local entry = parseBridgeProfessionPayload(payload) + if not entry then + return nil + end + + local key = string.lower(entry.name) + state.professions[key] = entry + + local detail = state.details[key] + if type(detail) == "table" then + detail.professions = entry.professions + detail.lastProfessionUpdateAt = entry.lastUpdateAt + end + + if MultiBot.ApplyBridgeBotProfession then + MultiBot.ApplyBridgeBotProfession(entry.name, entry.professions) + end + + debugPrint("ADDON:RX", "PROFESSION", entry.name) + return entry +end + +function Comm.ApplyBotProfessionsPayload(payload) + local applied = 0 + + if type(payload) == "string" and payload ~= "" then + for entryPayload in string.gmatch(payload, "([^|]+)") do + if Comm.ApplyBotProfessionPayload(entryPayload) then + applied = applied + 1 + end + end + end + + debugPrint("ADDON:RX", "PROFESSIONS", tostring(applied)) + return applied +end + function Comm.ApplyBotDetailsPayload(payload) local applied = 0 @@ -1453,6 +1532,20 @@ function Comm.HandleAddonMessage(prefix, message, distribution, sender) return true end + if opcode == "PROFESSION" then + state.connected = true + state.lastError = nil + Comm.ApplyBotProfessionPayload(payload) + return true + end + + if opcode == "PROFESSIONS" then + state.connected = true + state.lastError = nil + Comm.ApplyBotProfessionsPayload(payload) + return true + end + if opcode == "TALENT_SPEC_BEGIN" then state.connected = true state.lastError = nil diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index f1f1f5f..f0165b8 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -19,10 +19,9 @@ local frFRValues = { ["lootmaster.no_candidates_for_item"] = "Aucun candidat pour cet objet.", ["lootmaster.assign"] = "Attribuer", ["lootmaster.select_bot"] = "Choisir un bot", - ["lootmaster.no_candidates_for_item"] = "Aucun candidat pour cet objet.", ["lootmaster.unknown_spec"] = "Spec inconnue.", ["lootmaster.inventory_unavailable"] = "Inventaire indisponible.", - ["lootmaster.priority_score"] = "Priorite:", + ["lootmaster.priority_score"] = "Priorite:", ["lootmaster.inventory_hint"] = "Click-droit: ouvre inventaire/équipment.", ["lootmaster.history_title"] = "Loots Récents", ["lootmaster.history_empty"] = "Pas de Loots Encore Attribués.", @@ -30,6 +29,19 @@ local frFRValues = { ["lootmaster.preference_saved"] = "Preference sauvegardée: items simmilaires -> %s.", ["lootmaster.preference_cleared"] = "Preference effacée pour items similaires.", ["lootmaster.preference_missing"] = "Pas de préférence sauvegardée pour cet item.", + ["options.lootmaster.enable"] = "Activer la fenêtre de loot", + ["options.lootmaster.enable_desc"] = "Affiche automatiquement la fenêtre MultiBot de responsable du butin pendant les loots.", + ["lootmaster.profession.jewelcrafting"] = "Joaillerie", + ["lootmaster.profession_known"] = "Metier : %s", + ["lootmaster.profession_missing"] = "Metier requis : %s (non connu)", + ["lootmaster.profession_unknown"] = "Metier requis : %s (inconnu)", + ["lootmaster.professions"] = "Metiers : %s", + ["lootmaster.professions_unknown"] = "Metiers : inconnus", + ["lootmaster.profession.engineering"] = "Ingénierie", + ["lootmaster.profession.cooking"] = "Cuisine", + ["lootmaster.profession.fishing"] = "Pêche", + ["lootmaster.profession.firstaid"] = "Secourisme", + ["lootmaster.gear_score"] = "GearScore : %d", ["tips.units.rti"] = "RTI", ["tips.disperse.main"] = "Disperse", ["tips.disperse.set"] = "Définir la distance de disperse", diff --git a/TODO.md b/TODO.md index c361b89..a7d1722 100644 --- a/TODO.md +++ b/TODO.md @@ -17,6 +17,9 @@ * 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 +## Informations bot +** Faire une frame qui affiche les infos du bot comme la profession ou peut être ajouter la profession à la frame inventaire? + ## Frame Loot * Tri intelligent des bots : afficher en haut les bots qui peuvent réellement utiliser l’objet selon classe, spé, type d’armure, arme, rôle. diff --git a/UI/MultiBotLootMasterUI.lua b/UI/MultiBotLootMasterUI.lua index a90b0d4..750e593 100644 --- a/UI/MultiBotLootMasterUI.lua +++ b/UI/MultiBotLootMasterUI.lua @@ -1,7 +1,5 @@ -- MultiBotLootMasterUI.lua -local addonName = ... - local AceLocale = LibStub and LibStub("AceLocale-3.0", true) local AceGUI = LibStub and LibStub("AceGUI-3.0", true) local L = AceLocale and AceLocale:GetLocale("MultiBot", true) @@ -16,19 +14,20 @@ end local LootMasterUI = {} local assignedLootKeys = {} -local assignedLootSlots = {} local recentLootByCandidate = {} local lootHistory = {} -local FRAME_WIDTH = 500 -local FRAME_HEIGHT = 420 +local FRAME_WIDTH = 480 +local FRAME_HEIGHT = 460 local ITEM_ROW_HEIGHT = 64 local DROPDOWN_WIDTH = 220 local MAX_MASTER_LOOT_CANDIDATES = 40 local RECENT_LOOT_PENALTY_SECONDS = 120 local LOOT_HISTORY_LINE_HEIGHT = 14 -local LOOT_HISTORY_HEIGHT = 62 +local LOOT_HISTORY_HEIGHT = 96 local LOOT_PREFERENCE_BONUS = 18 +local LOOT_PROFESSION_BONUS = 55 +local LOOT_PROFESSION_MISMATCH_PENALTY = 35 local CLASS_ICON_TEXTURE = "Interface\\GLUES\\CHARACTERCREATE\\UI-CHARACTERCREATE-CLASSES" local AssignLoot local QueueRefresh @@ -77,6 +76,14 @@ local function AddSystemMessage(message) end end +local function IsLootMasterUIEnabled() + if MultiBot and type(MultiBot.GetLootMasterUIEnabled) == "function" then + return MultiBot.GetLootMasterUIEnabled() + end + + return true +end + local function OpenCandidateInventory(candidate) local botName = candidate and candidate.name @@ -116,6 +123,40 @@ local function RunLater(delay, callback) callback() end +local pendingLootCloseToken = 0 + +local function CancelQueuedLootClose() + pendingLootCloseToken = pendingLootCloseToken + 1 +end + +local function IsNativeLootFrameStillOpen() + if _G.LootFrame and _G.LootFrame:IsShown() then + return true + end + + return GetNumLootItems and (GetNumLootItems() or 0) > 0 +end + +local function QueueLootClose() + CancelQueuedLootClose() + local token = pendingLootCloseToken + + RunLater(0.20, function() + if token ~= pendingLootCloseToken then + return + end + + if IsNativeLootFrameStillOpen() then + if LootMasterUI.frame and LootMasterUI.frame:IsShown() and QueueRefresh then + QueueRefresh(0.05) + end + return + end + + LootMasterUI:Close() + end) +end + local function ShortName(name) if type(name) ~= "string" then return "" @@ -284,36 +325,6 @@ local function BuildCandidateSpecText(candidate) return MBLocal("lootmaster.unknown_spec", "Unknown spec") end -local function GetCandidateDisplayName(candidate) - local name = candidate and candidate.name or "" - local classToken = candidate and candidate.classToken - local color = classToken and RAID_CLASS_COLORS and RAID_CLASS_COLORS[classToken] - - if color and color.colorStr then - return "|c" .. color.colorStr .. name .. "|r" - end - - return name -end - -local function ApplyClassIcon(texture, classToken) - if not texture then - return - end - - if classToken and CLASS_ICON_TCOORDS and CLASS_ICON_TCOORDS[classToken] then - local coords = CLASS_ICON_TCOORDS[classToken] - texture:SetTexture(CLASS_ICON_TEXTURE) - texture:SetTexCoord(coords[1], coords[2], coords[3], coords[4]) - texture:Show() - return - end - - texture:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") - texture:SetTexCoord(0, 1, 0, 1) - texture:Show() -end - local function GetCandidateSpecLabel(candidate) if not candidate then return MBLocal("lootmaster.unknown_spec", "Unknown spec") end @@ -347,6 +358,173 @@ local function TextHasAny(text, patterns) return false end +local PROFESSION_DISPLAY_KEYS = { + alchemy = { key = "lootmaster.profession.alchemy", fallback = "Alchemy" }, + blacksmithing = { key = "lootmaster.profession.blacksmithing", fallback = "Blacksmithing" }, + enchanting = { key = "lootmaster.profession.enchanting", fallback = "Enchanting" }, + engineering = { key = "lootmaster.profession.engineering", fallback = "Engineering" }, + herbalism = { key = "lootmaster.profession.herbalism", fallback = "Herbalism" }, + inscription = { key = "lootmaster.profession.inscription", fallback = "Inscription" }, + jewelcrafting = { key = "lootmaster.profession.jewelcrafting", fallback = "Jewelcrafting" }, + leatherworking = { key = "lootmaster.profession.leatherworking", fallback = "Leatherworking" }, + mining = { key = "lootmaster.profession.mining", fallback = "Mining" }, + skinning = { key = "lootmaster.profession.skinning", fallback = "Skinning" }, + tailoring = { key = "lootmaster.profession.tailoring", fallback = "Tailoring" }, + cooking = { key = "lootmaster.profession.cooking", fallback = "Cooking" }, + firstaid = { key = "lootmaster.profession.firstaid", fallback = "First Aid" }, + fishing = { key = "lootmaster.profession.fishing", fallback = "Fishing" }, +} + +local PROFESSION_SPELL_IDS = { + alchemy = 2259, + blacksmithing = 2018, + enchanting = 7411, + engineering = 4036, + herbalism = 2366, + inscription = 45357, + jewelcrafting = 25229, + leatherworking = 2108, + mining = 2575, + skinning = 8613, + tailoring = 3908, + cooking = 2550, + firstaid = 3273, + fishing = 7620, +} + +local PROFESSION_MATCH_PATTERNS = { + alchemy = { "alchemy", "alchimie" }, + blacksmithing = { "blacksmithing", "blacksmith", "forge" }, + enchanting = { "enchanting", "enchantement", "enchant" }, + engineering = { "engineering", "engineer", "ingenier", "ing.nier" }, + inscription = { "inscription", "calligraphie", "scribe" }, + jewelcrafting = { "jewelcrafting", "jewelcraft", "joaillerie" }, + leatherworking = { "leatherworking", "leatherwork", "travail du cuir" }, + tailoring = { "tailoring", "couture" }, + cooking = { "cooking", "cuisine" }, + firstaid = { "first aid", "firstaid", "secourisme", "premiers soins" }, + fishing = { "fishing", "peche", "p.che" }, +} + +local function GetProfessionKeyFromText(text) + if type(text) ~= "string" or text == "" then + return nil + end + + text = string.lower(text) + for profession, patterns in pairs(PROFESSION_MATCH_PATTERNS) do + if TextHasAny(text, patterns) then + return profession + end + end + + return nil +end + +local function GetProfessionDisplayName(profession) + local spellId = PROFESSION_SPELL_IDS[profession or ""] + if spellId and GetSpellInfo then + local spellName = GetSpellInfo(spellId) + if spellName and spellName ~= "" then + return spellName + end + end + + local entry = PROFESSION_DISPLAY_KEYS[profession or ""] + if entry then + return MBLocal(entry.key, entry.fallback) + end + + return profession +end + +local function GetRequiredProfessionForLoot(itemName, itemType, itemSubType, tooltipText) + local text = table.concat({ + itemName or "", + itemType or "", + itemSubType or "", + tooltipText or "", + }, " ") + + if not TextHasAny(text, { + "recipe", "recette", "pattern", "patron", "design", "dessin", + "formula", "formule", "plans", "plan", "schematic", "sch.ma", + "manual", "manuel", "technique", + }) then + return nil + end + + return GetProfessionKeyFromText(text) +end + +local function AddProfessionKeysFromValue(professions, value) + if type(value) == "string" then + local key = GetProfessionKeyFromText(value) + if key then + professions[key] = true + end + return + end + + if type(value) ~= "table" then + return + end + + for key, entry in pairs(value) do + if entry == true then + AddProfessionKeysFromValue(professions, tostring(key)) + else + AddProfessionKeysFromValue(professions, entry) + AddProfessionKeysFromValue(professions, tostring(key)) + end + end +end + +local function GetCandidateProfessionKeys(candidate) + local professions = {} + local detail = candidate and GetBridgeDetail(candidate.name) + + if type(detail) == "table" then + AddProfessionKeysFromValue(professions, detail.professions) + AddProfessionKeysFromValue(professions, detail.profession) + AddProfessionKeysFromValue(professions, detail.profession1) + AddProfessionKeysFromValue(professions, detail.profession2) + AddProfessionKeysFromValue(professions, detail.primaryProfession) + AddProfessionKeysFromValue(professions, detail.secondaryProfession) + end + + local cache = _G.MultiBotLootMasterProfessionCache + if type(cache) == "table" and candidate and candidate.name then + AddProfessionKeysFromValue(professions, cache[NormalizeName(candidate.name)]) + AddProfessionKeysFromValue(professions, cache[ShortName(candidate.name)]) + AddProfessionKeysFromValue(professions, cache[candidate.name]) + end + + return professions +end + +local function CandidateHasProfession(candidate, profession) + if not profession then + return nil + end + + local professions = GetCandidateProfessionKeys(candidate) + local hasKnownProfession = false + + for key in pairs(professions) do + hasKnownProfession = true + if key == profession then + return true + end + end + + if hasKnownProfession then + return false + end + + return nil +end + local ARMOR_RANK = { CLOTH = 1, LEATHER = 2, MAIL = 3, PLATE = 4 } local CLASS_ARMOR_RANK = { WARRIOR = 4, PALADIN = 4, DEATHKNIGHT = 4, @@ -391,6 +569,28 @@ local function GetCandidateTreeIndex(candidate) return 1 end +local function GetCandidateSpecRoleHint(candidate) + local parts = {} + local spec = GetSpecFromBridgeDetail(candidate) + local savedSpec = GetSavedBotSpec(candidate and candidate.name) + + if spec and spec ~= "" then parts[#parts + 1] = spec end + if savedSpec and savedSpec ~= "" then parts[#parts + 1] = savedSpec end + + local label = table.concat(parts, " "):lower() + if label == "" then return nil end + + if label:find("bear") or label:find("ours") or label:find("tank") then + return "bear" + end + + if label:find("cat") or label:find("chat") or label:find("dps") or label:find("feral") or label:find("farouche") then + return "feral" + end + + return nil +end + local function GetCandidateRole(candidate) local classFile = candidate and (candidate.classFile or candidate.classToken) local tree = GetCandidateTreeIndex(candidate) @@ -399,8 +599,17 @@ local function GetCandidateRole(candidate) if classFile == "PALADIN" then return tree == 1 and "healer" or (tree == 2 and "tank" or "physical") end if classFile == "PRIEST" then return tree == 3 and "caster" or "healer" end if classFile == "DEATHKNIGHT" then return tree == 1 and "tank" or "physical" end - if classFile == "SHAMAN" then return tree == 1 and "caster" or (tree == 2 and "physical" or "healer") end - if classFile == "DRUID" then return tree == 1 and "caster" or (tree == 2 and "feral" or "healer") end + if classFile == "SHAMAN" then + if tree == 1 then return "caster" end + if tree == 2 then return "physical" end + if tree == 3 then return "healer" end + return "unknown" + end + if classFile == "DRUID" then + if tree == 1 then return "caster" end + if tree == 2 then return GetCandidateSpecRoleHint(candidate) or "feral" end + return "healer" + end if classFile == "MAGE" or classFile == "WARLOCK" then return "caster" end if classFile == "HUNTER" or classFile == "ROGUE" then return "physical" end @@ -462,9 +671,13 @@ local function GetLootTooltipText(itemLink) return table.concat(lines, " ") end -local function GetItemStatFlags(itemLink) - local text = GetLootTooltipText(itemLink) - local hasDefense = TextHasAny(text, { "defense", "d.fense", "dodge", "esquive", "parry", "parade", "block", "blocage" }) +local function GetItemStatFlags(itemLink, tooltipText) + local text = tooltipText or GetLootTooltipText(itemLink) + local hasDefense = TextHasAny(text, { "defense", "d.fense" }) + local hasDodge = TextHasAny(text, { "dodge", "esquive" }) + local hasParry = TextHasAny(text, { "parry", "parade" }) + local hasBlock = TextHasAny(text, { "block", "blocage" }) + local hasStamina = TextHasAny(text, { "stamina", "endurance" }) local hasAgility = TextHasAny(text, { "agility", "agilit" }) local hasStrength = TextHasAny(text, { "strength", "force" }) local hasAttackPower = TextHasAny(text, { "attack power", "puissance d.attaque" }) @@ -477,13 +690,14 @@ local function GetItemStatFlags(itemLink) local hasIntellect = TextHasAny(text, { "intellect", "intelligence" }) local hasHit = TextHasAny(text, { "hit rating", "score de toucher" }) local hasCrit = TextHasAny(text, { "critical strike", "coup critique", "critique" }) - local hasHaste = TextHasAny(text, { "haste", "h.te" }) + local hasHaste = TextHasAny(text, { "haste", "h.te" }) local hasPhysicalPower = hasAgility or hasStrength or hasAttackPower or hasExpertise or hasArmorPen + local hasTankPrimary = hasDefense or hasDodge or hasParry or hasBlock local hasHealerHint = hasHealing or hasMp5 or hasSpirit local primary - if hasDefense then + if hasTankPrimary then primary = "tank" elseif hasPhysicalPower and not hasSpellPower and not hasHealing then primary = "physical" @@ -507,6 +721,24 @@ local function GetItemStatFlags(itemLink) healer = primary == "healer", caster = primary == "caster", physical = primary == "physical", + defense = hasDefense, + dodge = hasDodge, + parry = hasParry, + block = hasBlock, + stamina = hasStamina, + agility = hasAgility, + strength = hasStrength, + attackPower = hasAttackPower, + expertise = hasExpertise, + armorPen = hasArmorPen, + spellPower = hasSpellPower, + healing = hasHealing, + mp5 = hasMp5, + spirit = hasSpirit, + intellect = hasIntellect, + hit = hasHit, + crit = hasCrit, + haste = hasHaste, } end @@ -515,6 +747,7 @@ local function BuildLootItemProfile(slot) local _, itemName = GetLootSlotInfo(slot) local query = itemLink or itemName local _, _, _, _, _, itemType, itemSubType, _, equipLoc = GetItemInfo(query or "") + local tooltipText = GetLootTooltipText(itemLink) return { link = itemLink, @@ -523,7 +756,8 @@ local function BuildLootItemProfile(slot) equipLoc = equipLoc, armorType = GetArmorType(itemSubType, equipLoc), weaponType = GetWeaponType(itemSubType, equipLoc), - statFlags = GetItemStatFlags(itemLink), + statFlags = GetItemStatFlags(itemLink, tooltipText), + requiredProfession = GetRequiredProfessionForLoot(itemName, itemType, itemSubType, tooltipText), } end @@ -535,6 +769,7 @@ local function BuildLootPreferenceKey(profile) local flags = profile.statFlags or {} return table.concat({ flags.primary or "any", + profile.requiredProfession or "noprofession", profile.armorType or "noarmor", profile.weaponType or "noweapon", profile.equipLoc or "noequip", @@ -627,9 +862,8 @@ end local function RoleMatchesStats(role, flags) if not flags then return false end if flags.primary then - if role == "feral" then - return flags.primary == "tank" or flags.primary == "physical" - end + if role == "feral" then return flags.primary == "physical" end + if role == "bear" then return flags.primary == "tank" end return role == flags.primary end @@ -641,12 +875,275 @@ local function GetRoleStatScore(role, flags) if not flags or not flags.primary then return 0 end if RoleMatchesStats(role, flags) then return 42 end if flags.primary == "physical" and (role == "healer" or role == "caster") then return -45 end - if flags.primary == "caster" and (role == "physical" or role == "tank" or role == "feral") then return -45 end + if flags.primary == "physical" and role == "bear" then return -45 end + if flags.primary == "caster" and (role == "physical" or role == "tank" or role == "feral" or role == "bear") then return -45 end if flags.primary == "healer" and role ~= "healer" then return -45 end - if flags.primary == "tank" and role ~= "tank" and role ~= "feral" then return -35 end + if flags.primary == "tank" and role ~= "tank" and role ~= "bear" then return -35 end return -25 end +local STAT_SCORE_KEYS = { + "defense", "dodge", "parry", "block", "stamina", + "agility", "strength", "attackPower", "expertise", "armorPen", + "spellPower", "healing", "mp5", "spirit", "intellect", + "hit", "crit", "haste", +} + +local ROLE_STAT_WEIGHTS = { + physical = { + agility = 10, strength = 10, attackPower = 12, expertise = 10, armorPen = 10, + hit = 8, crit = 8, haste = 5, stamina = 1, + spellPower = -25, healing = -30, mp5 = -15, spirit = -15, intellect = -4, + defense = -18, dodge = -12, parry = -12, block = -12, + }, + caster = { + spellPower = 16, intellect = 8, hit = 10, crit = 8, haste = 8, spirit = 4, + mp5 = -6, healing = -4, + agility = -22, strength = -28, attackPower = -28, expertise = -35, armorPen = -28, + defense = -20, dodge = -15, parry = -15, block = -15, + }, + healer = { + healing = 18, spellPower = 12, intellect = 10, mp5 = 12, spirit = 10, crit = 6, haste = 8, + hit = -20, + agility = -22, strength = -28, attackPower = -28, expertise = -35, armorPen = -28, + defense = -12, dodge = -8, parry = -8, block = -8, + }, + tank = { + defense = 18, dodge = 14, parry = 14, block = 12, stamina = 10, + strength = 6, expertise = 8, hit = 6, agility = 3, + spellPower = -25, healing = -25, mp5 = -10, spirit = -10, armorPen = -5, + }, + feral = { + agility = 16, attackPower = 12, armorPen = 12, expertise = 10, + hit = 9, crit = 10, haste = 4, stamina = 2, strength = 4, + defense = -25, dodge = -10, parry = -30, block = -30, + spellPower = -25, healing = -25, mp5 = -10, spirit = -8, + }, + bear = { + stamina = 14, dodge = 16, defense = 12, agility = 10, + expertise = 8, hit = 6, strength = 4, attackPower = 4, + armorPen = 2, crit = 4, + parry = -30, block = -30, + spellPower = -25, healing = -25, mp5 = -10, spirit = -8, + }, +} + +local CLASS_ROLE_STAT_WEIGHTS = { + SHAMAN = { + caster = { + spellPower = 18, hit = 10, haste = 9, crit = 8, intellect = 8, mp5 = 2, + agility = -25, strength = -25, attackPower = -30, expertise = -30, + armorPen = -30, healing = -18, spirit = -8, + }, + physical = { + agility = 12, attackPower = 12, hit = 10, expertise = 10, crit = 9, + haste = 8, intellect = 4, strength = 4, armorPen = 2, + spellPower = -18, healing = -25, mp5 = -8, spirit = -12, + defense = -25, dodge = -18, parry = -18, block = -18, + }, + healer = { + healing = 18, spellPower = 14, intellect = 12, haste = 10, + crit = 8, mp5 = 8, + hit = -20, agility = -25, strength = -25, attackPower = -30, + expertise = -30, armorPen = -30, spirit = -8, + }, + }, + + DRUID = { + caster = { + spellPower = 16, intellect = 8, hit = 10, crit = 8, haste = 8, spirit = 6, + agility = -25, strength = -25, attackPower = -30, expertise = -35, armorPen = -30, + }, + healer = { + healing = 18, spellPower = 12, intellect = 10, spirit = 12, haste = 8, crit = 6, mp5 = 6, + hit = -20, agility = -25, strength = -25, attackPower = -30, expertise = -35, armorPen = -30, + }, + feral = { + agility = 16, attackPower = 12, armorPen = 12, expertise = 10, hit = 9, crit = 10, + stamina = 2, defense = -25, dodge = -10, block = -30, parry = -30, + spellPower = -25, healing = -25, + }, + bear = { + stamina = 14, dodge = 16, defense = 12, agility = 10, expertise = 8, hit = 6, + attackPower = 4, armorPen = 2, crit = 4, + block = -30, parry = -30, spellPower = -25, healing = -25, + }, + }, + + HUNTER = { + physical = { + agility = 16, attackPower = 14, armorPen = 16, hit = 12, crit = 10, haste = 4, intellect = 2, + strength = -35, expertise = -45, + spellPower = -35, healing = -35, mp5 = -20, spirit = -20, + }, + }, + ROGUE = { + physical = { agility = 16, attackPower = 12, expertise = 14, armorPen = 12, hit = 12, crit = 10, haste = 6, strength = -15 }, + }, + PALADIN = { + physical = { strength = 16, expertise = 12, hit = 10, crit = 10, haste = 8, attackPower = 8, agility = -8, spellPower = -25 }, + healer = { healing = 18, spellPower = 14, intellect = 12, mp5 = 10, crit = 8, haste = 8, spirit = -8 }, + tank = { defense = 18, dodge = 12, parry = 12, block = 14, stamina = 10, strength = 8, expertise = 8, hit = 6 }, + }, + WARRIOR = { + physical = { strength = 16, expertise = 14, armorPen = 12, hit = 10, crit = 10, haste = 6, agility = -5, spellPower = -35 }, + tank = { defense = 18, dodge = 12, parry = 12, block = 12, stamina = 10, strength = 8, expertise = 8, hit = 6 }, + }, + DEATHKNIGHT = { + physical = { strength = 16, expertise = 14, armorPen = 10, hit = 10, crit = 10, haste = 6, agility = -8, spellPower = -35 }, + tank = { defense = 18, dodge = 12, parry = 12, stamina = 10, strength = 8, expertise = 8, hit = 6, block = -20 }, + }, +} + +local CLASS_ROLE_STAT_RULES = { + SHAMAN = { + caster = { + requiredAny = { "spellPower", "hit", "crit", "haste", "intellect" }, + badPrimary = { + agility = true, strength = true, attackPower = true, + expertise = true, armorPen = true, healing = true, + }, + badPrimaryCap = 49, + }, + physical = { + requiredAny = { "agility", "attackPower", "expertise", "hit", "crit", "haste" }, + badPrimary = { + spellPower = true, healing = true, mp5 = true, spirit = true, + defense = true, dodge = true, parry = true, block = true, + }, + badPrimaryCap = 49, + }, + healer = { + requiredAny = { "healing", "spellPower", "intellect", "haste", "crit", "mp5" }, + badPrimary = { + hit = true, agility = true, strength = true, attackPower = true, + expertise = true, armorPen = true, + }, + badPrimaryCap = 49, + }, + }, + + DRUID = { + feral = { + requiredAny = { "agility", "attackPower", "armorPen", "expertise", "hit", "crit" }, + badPrimary = { + defense = true, dodge = true, parry = true, block = true, + spellPower = true, healing = true, intellect = true, spirit = true, mp5 = true, + }, + badPrimaryCap = 49, + }, + bear = { + requiredAny = { "stamina", "dodge", "defense", "agility" }, + badPrimary = { + spellPower = true, healing = true, intellect = true, spirit = true, mp5 = true, + block = true, parry = true, + }, + badPrimaryCap = 49, + }, + }, + + HUNTER = { + physical = { + requiredAny = { "agility", "attackPower", "armorPen" }, + badPrimary = { strength = true, expertise = true }, + badPrimaryCap = 49, + }, + }, + WARRIOR = { + physical = { + requiredAny = { "strength", "attackPower", "armorPen" }, + badPrimary = { spellPower = true, healing = true, intellect = true, spirit = true, mp5 = true }, + badPrimaryCap = 45, + }, + }, + PALADIN = { + physical = { + requiredAny = { "strength", "attackPower" }, + badPrimary = { spellPower = true, healing = true, spirit = true, mp5 = true }, + badPrimaryCap = 45, + }, + healer = { + requiredAny = { "spellPower", "healing", "intellect", "mp5", "crit", "haste" }, + badPrimary = { strength = true, agility = true, attackPower = true, armorPen = true, expertise = true }, + badPrimaryCap = 45, + }, + }, + DEATHKNIGHT = { + physical = { + requiredAny = { "strength", "attackPower", "armorPen" }, + badPrimary = { spellPower = true, healing = true, intellect = true, spirit = true, mp5 = true, block = true }, + badPrimaryCap = 45, + }, + }, +} + +local function HasAnyStat(flags, statList) + if type(flags) ~= "table" or type(statList) ~= "table" then return false end + + for _, statKey in ipairs(statList) do + if flags[statKey] then + return true + end + end + + return false +end + +local function GetCandidateStatRule(candidate) + local classFile = candidate and (candidate.classFile or candidate.classToken) + local role = candidate and candidate.lootRole + local classRules = classFile and CLASS_ROLE_STAT_RULES[classFile] + return classRules and role and classRules[role] or nil +end + +local function GetCandidateStatWeight(candidate, statKey) + local classFile = candidate and (candidate.classFile or candidate.classToken) + local role = candidate and candidate.lootRole + local classWeights = classFile and CLASS_ROLE_STAT_WEIGHTS[classFile] + local classRoleWeights = classWeights and role and classWeights[role] + + if classRoleWeights and classRoleWeights[statKey] ~= nil then + return classRoleWeights[statKey] + end + + local roleWeights = role and ROLE_STAT_WEIGHTS[role] + return roleWeights and roleWeights[statKey] or 0 +end + +local function GetCandidateStatScore(candidate, flags) + local score = 0 + if type(flags) ~= "table" then return 0 end + + for _, statKey in ipairs(STAT_SCORE_KEYS) do + if flags[statKey] then + score = score + GetCandidateStatWeight(candidate, statKey) + end + end + + return Clamp(score, -55, 35) +end + +local function GetCandidateStatScoreCap(candidate, flags) + local rule = GetCandidateStatRule(candidate) + + if not rule or type(flags) ~= "table" or type(rule.badPrimary) ~= "table" then + return nil + end + + local hasBadPrimary = false + for statKey in pairs(rule.badPrimary) do + if flags[statKey] then + hasBadPrimary = true + break + end + end + + if not hasBadPrimary then return nil end + if HasAnyStat(flags, rule.requiredAny) then return nil end + + return rule.badPrimaryCap or 49 +end + local function CandidateHadRecentLoot(candidate) local key = NormalizeName(candidate and candidate.name) return key and recentLootByCandidate[key] ~= nil @@ -658,6 +1155,17 @@ local function ScoreCandidateForItem(candidate, profile) local flags = profile.statFlags local hasKnownStats = flags and (flags.primary or flags.tank or flags.healer or flags.caster or flags.physical) candidate.lootRole = GetCandidateRole(candidate) + candidate.lootProfession = profile.requiredProfession + candidate.lootProfessionMatch = nil + + if profile.requiredProfession then + candidate.lootProfessionMatch = CandidateHasProfession(candidate, profile.requiredProfession) + if candidate.lootProfessionMatch == true then + score = score + LOOT_PROFESSION_BONUS + elseif candidate.lootProfessionMatch == false then + score = score - LOOT_PROFESSION_MISMATCH_PENALTY + end + end if profile.armorType == "SHIELD" then score = score + ((CLASS_WEAPONS[classFile] and CLASS_WEAPONS[classFile].SHIELD) and 18 or -35) @@ -683,13 +1191,15 @@ local function ScoreCandidateForItem(candidate, profile) if hasKnownStats then score = score + GetRoleStatScore(candidate.lootRole, flags) + score = score + GetCandidateStatScore(candidate, flags) end if CandidateHadRecentLoot(candidate) then score = score - 15 end - candidate.lootScore = Clamp(score, 1, 99) + candidate.lootScoreCap = GetCandidateStatScoreCap(candidate, flags) + candidate.lootScore = Clamp(score, 1, candidate.lootScoreCap or 99) end local function ApplyLootPreferenceScore(candidate, profile) @@ -699,7 +1209,7 @@ local function ApplyLootPreferenceScore(candidate, profile) if preferredName and NormalizeName(candidate.name) == NormalizeName(preferredName) then candidate.lootPreference = true - candidate.lootScore = Clamp((candidate.lootScore or 0) + LOOT_PREFERENCE_BONUS, 1, 99) + candidate.lootScore = Clamp((candidate.lootScore or 0) + LOOT_PREFERENCE_BONUS, 1, candidate.lootScoreCap or 99) end end @@ -720,30 +1230,20 @@ end local function BuildCandidateDropdownText(candidate) local score = candidate.lootScore or 0 local prefText = candidate.lootPreference and " |cffffcc00Pref|r" or "" - return GetClassColorCode(candidate.classFile) .. candidate.name .. "|r " .. GetCandidateSpecLabel(candidate) .. " " .. GetCandidateScoreColor(score) .. score .. "%|r" .. prefText -end - -local function SetClassIconTooltip(button, candidate) - if not button then - return - end - - button.candidate = candidate - button:SetScript("OnEnter", function(self) - local activeCandidate = self.candidate - if not activeCandidate then - return + local professionText = "" + + if candidate.lootProfession then + local professionName = GetProfessionDisplayName(candidate.lootProfession) + if candidate.lootProfessionMatch == true then + professionText = " |cff66ccff" .. professionName .. "|r" + elseif candidate.lootProfessionMatch == false then + professionText = " |cff777777-" .. professionName .. "|r" + else + professionText = " |cffaaaaaa" .. professionName .. "?|r" end + end - GameTooltip:SetOwner(self, "ANCHOR_RIGHT") - GameTooltip:SetText(activeCandidate.name or MBLocal("lootmaster.unknown_candidate", "unknown"), 1, 1, 1) - GameTooltip:AddLine(activeCandidate.className or MBLocal("lootmaster.unknown_class", "Unknown class"), 0.8, 0.8, 0.8) - GameTooltip:AddLine(BuildCandidateSpecText(activeCandidate), 0.6, 0.8, 1) - GameTooltip:Show() - end) - button:SetScript("OnLeave", function() - GameTooltip:Hide() - end) + return GetClassColorCode(candidate.classFile) .. candidate.name .. "|r " .. GetCandidateSpecLabel(candidate) .. " " .. GetCandidateScoreColor(score) .. score .. "%|r" .. prefText .. professionText end local function IsPlayerMasterLooter() @@ -858,7 +1358,7 @@ local function GetClassIconTexture(classFile) return nil end - return "Interface\\Glues\\CharacterCreate\\UI-CharacterCreate-Classes" + return CLASS_ICON_TEXTURE end local function GetClassIconCoords(classFile) @@ -979,6 +1479,43 @@ local function GetCandidateSpecText(candidate) return MBLocal("lootmaster.unknown_spec", "Unknown spec") end +local function GetCandidateGearScore(candidate) + if type(candidate) ~= "table" then + return nil + end + + local score = tonumber(candidate.gearScore or candidate.itemLevelScore or candidate.score) + if score and score > 0 then + return score + end + + local detail = GetBridgeDetail and GetBridgeDetail(candidate.name) + if type(detail) == "table" then + score = tonumber(detail.gearScore or detail.itemLevelScore or detail.score) + if score and score > 0 then + return score + end + end + + if candidate.name and _G.MultiBotGlobalSave then + local shortName = NormalizeUnitName(candidate.name) or candidate.name + local value = _G.MultiBotGlobalSave[shortName] or _G.MultiBotGlobalSave[candidate.name] + if type(value) == "string" then + local fields = {} + for field in string.gmatch(value, "([^,]+)") do + fields[#fields + 1] = field + end + + score = tonumber(fields[7]) + if score and score > 0 then + return score + end + end + end + + return nil +end + local function SetClassIconTooltip(button, candidate) if not button then return @@ -1000,6 +1537,44 @@ local function SetClassIconTooltip(button, candidate) end GameTooltip:AddLine(GetCandidateSpecText(current), 0.6, 0.8, 1) + + local gearScore = GetCandidateGearScore(current) + if gearScore then + GameTooltip:AddLine(string.format(MBLocal("lootmaster.gear_score", "GearScore: %d"), gearScore), 0.95, 0.82, 0.35) + end + + local professions = GetCandidateProfessionKeys and GetCandidateProfessionKeys(current) + if type(professions) == "table" then + local professionNames = {} + for profession in pairs(professions) do + professionNames[#professionNames + 1] = GetProfessionDisplayName(profession) + end + + table.sort(professionNames) + + if #professionNames > 0 then + GameTooltip:AddLine( + string.format( + MBLocal("lootmaster.professions", "Professions: %s"), + table.concat(professionNames, ", ") + ), + 0.4, 1, 0.7 + ) + else + GameTooltip:AddLine(MBLocal("lootmaster.professions_unknown", "Professions: unknown"), 0.65, 0.65, 0.65) + end + end + + if current.lootProfession then + local professionName = GetProfessionDisplayName(current.lootProfession) + if current.lootProfessionMatch == true then + GameTooltip:AddLine(string.format(MBLocal("lootmaster.profession_known", "Profession: %s"), professionName), 0.4, 1, 0.7) + elseif current.lootProfessionMatch == false then + GameTooltip:AddLine(string.format(MBLocal("lootmaster.profession_missing", "Required profession: %s (not known)"), professionName), 1, 0.45, 0.35) + else + GameTooltip:AddLine(string.format(MBLocal("lootmaster.profession_unknown", "Required profession: %s (unknown)"), professionName), 0.9, 0.9, 0.5) + end + end if current.lootScore then GameTooltip:AddLine(MBLocal("lootmaster.priority_score", "Priorite:") .. " " .. GetCandidateScoreColor(current.lootScore) .. current.lootScore .. "%|r", 0.9, 0.9, 0.9) end @@ -1114,7 +1689,7 @@ local function BuildCandidateList(slot) } ScoreCandidateForItem(candidate, itemProfile) - ApplyLootPreferenceScore(candidate, itemProfile) + ApplyLootPreferenceScore(candidate, itemProfile) candidates[#candidates + 1] = candidate end end @@ -1136,7 +1711,7 @@ local function SafeItemText(slot) return link end - local texture, itemName, quantity = GetLootSlotInfo(slot) + local _, itemName, quantity = GetLootSlotInfo(slot) if itemName and itemName ~= "" then if quantity and quantity > 1 then @@ -1149,16 +1724,6 @@ local function SafeItemText(slot) return MBLocal("lootmaster.unknown_item", "Unknown item") end -local function BuildLootSlotKey(slot) - local link = GetLootSlotLink(slot) - if link and link ~= "" then - return link - end - - local _, itemName, quantity = GetLootSlotInfo(slot) - return tostring(slot) .. ":" .. tostring(itemName or "") .. ":" .. tostring(quantity or 1) -end - local function EnsureLootHistoryLine(frame, index) if not frame or not frame.historyContent then return nil @@ -1231,15 +1796,6 @@ local function AddLootHistory(itemText, candidateName) UpdateLootHistoryFrame() end -local function MarkLootSlotAssigned(slot) - assignedLootKeys[BuildLootSlotKey(slot)] = GetTime and GetTime() or time() - assignedLootSlots[slot] = GetTime and GetTime() or time() -end - -local function IsLootSlotAssignedLocally(slot) - return assignedLootSlots[slot] ~= nil or assignedLootKeys[BuildLootSlotKey(slot)] ~= nil -end - local function IsLootSlotMoney(slot) return LootSlotIsCoin and LootSlotIsCoin(slot) end @@ -1258,6 +1814,30 @@ local function IsLootSlotEmpty(slot) return not itemName or itemName == "" end +local function IsLootSlotIgnoredQuality(slot) + local _, itemName, _, quality = GetLootSlotInfo(slot) + + if not itemName or itemName == "" then + return false + end + + local threshold = GetLootThreshold and (GetLootThreshold() or 2) or 2 + + if threshold < 2 then + threshold = 2 + end + + return not quality or quality < threshold +end + +local function IsLootSlotRelevant(slot) + if IsLootSlotMoney(slot) then return false end + if IsLootSlotEmpty(slot) then return false end + if IsLootSlotIgnoredQuality(slot) then return false end + + return true +end + local function GetLootSlotHideKey(slot) local link = GetLootSlotLink(slot) @@ -1390,7 +1970,7 @@ local function CreateBasicFrame() frame.historyTitle:SetPoint("TOPLEFT", frame.history, "TOPLEFT", 0, 0) frame.historyTitle:SetText(MBLocal("lootmaster.history_title", "Recent loot")) - frame.historyScroll = CreateFrame("ScrollFrame", nil, frame.history, "UIPanelScrollFrameTemplate") + frame.historyScroll = CreateFrame("ScrollFrame", "MultiBotLootMasterHistoryScrollFrame", frame.history, "UIPanelScrollFrameTemplate") frame.historyScroll:SetPoint("TOPLEFT", frame.historyTitle, "BOTTOMLEFT", 0, -2) frame.historyScroll:SetPoint("BOTTOMRIGHT", frame.history, "BOTTOMRIGHT", -24, 0) frame.historyScroll:EnableMouseWheel(true) @@ -1559,7 +2139,6 @@ local function AcquireRow(frame, index) button:SetPoint("TOPRIGHT", dropdown.buttons[i - 1], "BOTTOMRIGHT", 0, -2) end - local classColor = GetClassColorCode(candidate.classFile) local classIcon = GetClassIconTexture(candidate.classFile) local classIconCoords = GetClassIconCoords(candidate.classFile) @@ -1840,10 +2419,8 @@ function LootMasterUI:Refresh() frame.status:SetText(MBLocal("lootmaster.status.ready", "Select an item recipient.")) for slot = 1, numSlots do - local isMoney = IsLootSlotMoney(slot) - - if not isMoney and not IsLootSlotEmpty(slot) and not IsLootSlotAssignedLocally(slot) then - local texture, itemName, quantity, quality, locked = GetLootSlotInfo(slot) + if IsLootSlotRelevant(slot) and not IsLootSlotAssignedLocally(slot) then + local texture, itemName, quantity, quality = GetLootSlotInfo(slot) local itemLink = GetLootSlotLink(slot) local candidates = BuildCandidateList(slot) @@ -1894,6 +2471,11 @@ function LootMasterUI:Refresh() end function LootMasterUI:Open() + if not IsLootMasterUIEnabled() then + self:Close() + return + end + local frame = self:EnsureFrame() frame:Show() @@ -1921,7 +2503,27 @@ function LootMasterUI:Close() end end +function LootMasterUI:RegisterProfessionCandidate(botName, profession) + local professionKey = GetProfessionKeyFromText(profession) + if not botName or botName == "" or not professionKey then + return false + end + + _G.MultiBotLootMasterProfessionCache = _G.MultiBotLootMasterProfessionCache or {} + + local nameKey = NormalizeName(botName) + _G.MultiBotLootMasterProfessionCache[nameKey] = _G.MultiBotLootMasterProfessionCache[nameKey] or {} + _G.MultiBotLootMasterProfessionCache[nameKey][professionKey] = true + + return true +end + function LootMasterUI:OnLootOpened(autoLoot) + if not IsLootMasterUIEnabled() then + self:Close() + return + end + local numSlots = GetNumLootItems() or 0 local method, partyIndex, raidIndex = GetLootMethod() @@ -1961,6 +2563,19 @@ function LootMasterUI:OnLootOpened(autoLoot) return end + local hasRelevantLoot = false + for slot = 1, numSlots do + if IsLootSlotRelevant(slot) then + hasRelevantLoot = true + break + end + end + + if not hasRelevantLoot then + self:Close() + return + end + self:Open() if MultiBot and MultiBot.Comm and type(MultiBot.Comm.RequestBotDetails) == "function" then @@ -1976,7 +2591,15 @@ eventFrame:RegisterEvent("LOOT_CLOSED") eventFrame:RegisterEvent("PARTY_LOOT_METHOD_CHANGED") eventFrame:SetScript("OnEvent", function(_, event, ...) + if not IsLootMasterUIEnabled() then + if event == "LOOT_CLOSED" then + LootMasterUI:Close() + end + return + end + if event == "LOOT_OPENED" then + CancelQueuedLootClose() assignedLootKeys = {} assignedLootSlots = {} LootMasterUI:OnLootOpened(...) @@ -1998,7 +2621,7 @@ eventFrame:SetScript("OnEvent", function(_, event, ...) end if event == "LOOT_CLOSED" then - LootMasterUI:Close() + QueueLootClose() end end) diff --git a/UI/MultiBotOptions.lua b/UI/MultiBotOptions.lua index fcc3630..cb18cd7 100644 --- a/UI/MultiBotOptions.lua +++ b/UI/MultiBotOptions.lua @@ -9,6 +9,14 @@ local function optL(key) return MultiBot.L(key) end +local function optLF(key, fallback) + local value = optL(key) + if not value or value == "" or value == key then + return fallback + end + return value +end + local function secondsLabel(value) local suffix = MultiBot.L("options.seconds_suffix") return string.format("%.1f %s", value, suffix) @@ -160,6 +168,7 @@ local function buildLegacyOptionsContent(panel) local disableAutoCollapse = MultiBot.GetDisableAutoCollapse and MultiBot.GetDisableAutoCollapse() or false local mainBarAutoHideEnabled = MultiBot.GetMainBarAutoHideEnabled and MultiBot.GetMainBarAutoHideEnabled() or false local mainBarAutoHideDelay = MultiBot.GetMainBarAutoHideDelay and MultiBot.GetMainBarAutoHideDelay() or 60 + local lootMasterUIEnabled = MultiBot.GetLootMasterUIEnabled and MultiBot.GetLootMasterUIEnabled() or true local strataDropDown = CreateFrame("Frame", "MultiBotStrataDropDown", scrollChild, "UIDropDownMenuTemplate") @@ -205,8 +214,23 @@ local function buildLegacyOptionsContent(panel) end end) + local chkLootMasterUI = CreateFrame("CheckButton", "MultiBot_LootMasterUICheck", scrollChild, "InterfaceOptionsCheckButtonTemplate") + chkLootMasterUI:SetPoint("TOPLEFT", chkDisableAutoCollapse, "BOTTOMLEFT", 0, -8) + _G[chkLootMasterUI:GetName() .. "Text"]:SetText(optLF("options.lootmaster.enable", "Activer la fenêtre de loot")) + chkLootMasterUI.tooltipText = optLF("options.lootmaster.enable_desc", "Affiche automatiquement la fenêtre MultiBot de responsable du butin pendant les loots.") + chkLootMasterUI:SetChecked(lootMasterUIEnabled and true or false) + chkLootMasterUI:SetScript("OnClick", function(btn) + local enabled = btn:GetChecked() and true or false + if MultiBot.SetLootMasterUIEnabled then + MultiBot.SetLootMasterUIEnabled(enabled) + end + if not enabled and _G.MultiBotLootMasterUI and _G.MultiBotLootMasterUI.Close then + _G.MultiBotLootMasterUI:Close() + end + end) + local chkMainBarAutoHide = CreateFrame("CheckButton", "MultiBot_MainBarAutoHideCheck", scrollChild, "InterfaceOptionsCheckButtonTemplate") - chkMainBarAutoHide:SetPoint("TOPLEFT", chkDisableAutoCollapse, "BOTTOMLEFT", 0, -8) + chkMainBarAutoHide:SetPoint("TOPLEFT", chkLootMasterUI, "BOTTOMLEFT", 0, -8) _G[chkMainBarAutoHide:GetName() .. "Text"]:SetText(optL("options.layout.mainbar_autohide")) chkMainBarAutoHide.tooltipText = optL("options.layout.mainbar_autohide_desc") chkMainBarAutoHide:SetChecked(mainBarAutoHideEnabled and true or false) @@ -262,6 +286,7 @@ local function buildLegacyOptionsContent(panel) panel.chkMinimapHide = chkMinimapHide panel.chkMainBarMoveLocked = chkMainBarMoveLocked panel.chkDisableAutoCollapse = chkDisableAutoCollapse + panel.chkLootMasterUI = chkLootMasterUI panel.chkMainBarAutoHide = chkMainBarAutoHide panel.mainBarAutoHideDelaySlider = mainBarAutoHideDelaySlider @@ -594,6 +619,7 @@ function MultiBot.BuildOptionsPanel() local disableAutoCollapse = MultiBot.GetDisableAutoCollapse and MultiBot.GetDisableAutoCollapse() or false local mainBarAutoHideEnabled = MultiBot.GetMainBarAutoHideEnabled and MultiBot.GetMainBarAutoHideEnabled() or false local mainBarAutoHideDelay = MultiBot.GetMainBarAutoHideDelay and MultiBot.GetMainBarAutoHideDelay() or 60 + local lootMasterUIEnabled = MultiBot.GetLootMasterUIEnabled and MultiBot.GetLootMasterUIEnabled() or true local chkMainBarMoveLocked = AceGUI:Create("CheckBox") chkMainBarMoveLocked:SetLabel(mainBarMoveLockLabel) @@ -625,6 +651,25 @@ function MultiBot.BuildOptionsPanel() scroll:AddChild(chkDisableAutoCollapse) panel.chkDisableAutoCollapse = chkDisableAutoCollapse + local chkLootMasterUI = AceGUI:Create("CheckBox") + chkLootMasterUI:SetLabel(optLF("options.lootmaster.enable", "Enable Advanced loot window")) + if chkLootMasterUI.SetDescription then + chkLootMasterUI:SetDescription(optLF("options.lootmaster.enable_desc", "Automatically shows the MultiBot loot master window during looting.")) + end + chkLootMasterUI:SetValue(lootMasterUIEnabled and true or false) + chkLootMasterUI:SetFullWidth(true) + chkLootMasterUI:SetCallback("OnValueChanged", function(_, _, value) + local enabled = value and true or false + if MultiBot.SetLootMasterUIEnabled then + MultiBot.SetLootMasterUIEnabled(enabled) + end + if not enabled and _G.MultiBotLootMasterUI and _G.MultiBotLootMasterUI.Close then + _G.MultiBotLootMasterUI:Close() + end + end) + scroll:AddChild(chkLootMasterUI) + panel.chkLootMasterUI = chkLootMasterUI + local autoHideDelaySlider = AceGUI:Create("Slider") autoHideDelaySlider:SetLabel(optL("options.layout.mainbar_autohide_delay")) autoHideDelaySlider:SetSliderValues(5, 600, 1) From a630fb19ad8e8fd30ce31d2de723cfd547a90896 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 1 May 2026 16:01:22 +0200 Subject: [PATCH 08/11] Remove unused stuffs --- .luacheckrc | 2 +- Locales/MultiBotAceLocale-frFR.lua | 1 - TODO.md | 3 +++ UI/MultiBotLootMasterUI.lua | 31 +++--------------------------- 4 files changed, 7 insertions(+), 30 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index dd5054f..c747bfe 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -32,7 +32,7 @@ globals = { "ChatFontNormal", "UnitIsDead", "ShowPrompt", "GameFontHighlightSmall", "GameFontNormalSmall", "SEARCH", "UnitIsUnit", "MB_INVENTORY_LABEL", "INVENTORY_TOOLTIP", "BAGSLOT", "GetItemInfoInstant", "sendInventoryItemCommand", "LE_ITEM_CLASS_QUESTITEM", "ITEMS", "LOADING", "QUEST_LOG", "INSPECT", "SPELLBOOK", "MB_TAB_TITLE_DEFAULT", "ensureHiddenTooltip", "IsInGuild", "GetGuildInfo", "GetGuildRosterShowOffline", "SetGuildRosterShowOffline", "PLAYER","Ambiguate", "ChatFrame_AddMessageEventFilter", "ChatTypeInfo", "x", "y", "CLASS_ICON_TCOORDS", "GetLootSlotLink", "GetLootSlotInfo", "GetLootMethod", "GetMasterLootCandidate", - "LOCALIZED_CLASS_NAMES_MALE", "LOCALIZED_CLASS_NAMES_FEMALE", "date", "LootSlotIsCoin", "LootSlotIsItem", "classColor", "GetNumLootItems" + "LOCALIZED_CLASS_NAMES_MALE", "LOCALIZED_CLASS_NAMES_FEMALE", "date", "LootSlotIsCoin", "LootSlotIsItem", "classColor", "GetNumLootItems", "GetItemQualityColor", "GiveMasterLoot", "GetLootThreshold" } diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index f0165b8..93d810b 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -19,7 +19,6 @@ local frFRValues = { ["lootmaster.no_candidates_for_item"] = "Aucun candidat pour cet objet.", ["lootmaster.assign"] = "Attribuer", ["lootmaster.select_bot"] = "Choisir un bot", - ["lootmaster.unknown_spec"] = "Spec inconnue.", ["lootmaster.inventory_unavailable"] = "Inventaire indisponible.", ["lootmaster.priority_score"] = "Priorite:", ["lootmaster.inventory_hint"] = "Click-droit: ouvre inventaire/équipment.", diff --git a/TODO.md b/TODO.md index a7d1722..096f3e3 100644 --- a/TODO.md +++ b/TODO.md @@ -19,6 +19,9 @@ ## Informations bot ** Faire une frame qui affiche les infos du bot comme la profession ou peut être ajouter la profession à la frame inventaire? +* Y afficher proffeions et niveau +* Monnaies (emblemes etc..) +* Réputations ## Frame Loot diff --git a/UI/MultiBotLootMasterUI.lua b/UI/MultiBotLootMasterUI.lua index 750e593..a68950f 100644 --- a/UI/MultiBotLootMasterUI.lua +++ b/UI/MultiBotLootMasterUI.lua @@ -301,30 +301,6 @@ GuessSpecFromBuild = function(classToken, build) return treeNames and treeNames[treeIndex] or nil end -local function BuildCandidateSpecText(candidate) - if not candidate then - return MBLocal("lootmaster.unknown_spec", "Unknown spec") - end - - local build = candidate.build - local knownSpecName = FindKnownSpecNameFromBuild(candidate.name, build) - local guessed = GuessSpecFromBuild(candidate.classToken, build) - - if knownSpecName and build then - return knownSpecName .. " (" .. build .. ")" - end - - if guessed and build then - return guessed .. " (" .. build .. ")" - end - - if build then - return build - end - - return MBLocal("lootmaster.unknown_spec", "Unknown spec") -end - local function GetCandidateSpecLabel(candidate) if not candidate then return MBLocal("lootmaster.unknown_spec", "Unknown spec") end @@ -690,10 +666,10 @@ local function GetItemStatFlags(itemLink, tooltipText) local hasIntellect = TextHasAny(text, { "intellect", "intelligence" }) local hasHit = TextHasAny(text, { "hit rating", "score de toucher" }) local hasCrit = TextHasAny(text, { "critical strike", "coup critique", "critique" }) - local hasHaste = TextHasAny(text, { "haste", "h.te" }) + local hasHaste = TextHasAny(text, { "haste", "h.te" }) local hasPhysicalPower = hasAgility or hasStrength or hasAttackPower or hasExpertise or hasArmorPen - local hasTankPrimary = hasDefense or hasDodge or hasParry or hasBlock + local hasTankPrimary = hasDefense or hasDodge or hasParry or hasBlock local hasHealerHint = hasHealing or hasMp5 or hasSpirit local primary @@ -747,7 +723,7 @@ local function BuildLootItemProfile(slot) local _, itemName = GetLootSlotInfo(slot) local query = itemLink or itemName local _, _, _, _, _, itemType, itemSubType, _, equipLoc = GetItemInfo(query or "") - local tooltipText = GetLootTooltipText(itemLink) + local tooltipText = GetLootTooltipText(itemLink) return { link = itemLink, @@ -2601,7 +2577,6 @@ eventFrame:SetScript("OnEvent", function(_, event, ...) if event == "LOOT_OPENED" then CancelQueuedLootClose() assignedLootKeys = {} - assignedLootSlots = {} LootMasterUI:OnLootOpened(...) return end From f61c1fb24f1e9b018f5c9f5c90958cb2d77051e5 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 1 May 2026 16:03:06 +0200 Subject: [PATCH 09/11] Update MultiBotLootMasterUI.lua --- UI/MultiBotLootMasterUI.lua | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/UI/MultiBotLootMasterUI.lua b/UI/MultiBotLootMasterUI.lua index a68950f..92c8cc0 100644 --- a/UI/MultiBotLootMasterUI.lua +++ b/UI/MultiBotLootMasterUI.lua @@ -254,25 +254,6 @@ local function GetCachedTalentSpecList(name) return specs[NormalizeName(name)] or specs[string.lower(ShortName(name) or "")] or specs[name] end -local function FindKnownSpecNameFromBuild(name, build) - if type(build) ~= "string" or build == "" then - return nil - end - - local specs = GetCachedTalentSpecList(name) - if type(specs) ~= "table" then - return nil - end - - for _, entry in ipairs(specs) do - if type(entry) == "table" and entry.build == build and type(entry.name) == "string" and entry.name ~= "" then - return entry.name - end - end - - return nil -end - GuessSpecFromBuild = function(classToken, build) if type(build) ~= "string" then return nil From dafae8f88c3931e5983fbec8b7cbe9dc391758a7 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 1 May 2026 16:12:35 +0200 Subject: [PATCH 10/11] Update MultiBotLootMasterUI.lua --- UI/MultiBotLootMasterUI.lua | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/UI/MultiBotLootMasterUI.lua b/UI/MultiBotLootMasterUI.lua index 92c8cc0..8356b04 100644 --- a/UI/MultiBotLootMasterUI.lua +++ b/UI/MultiBotLootMasterUI.lua @@ -243,17 +243,6 @@ local function GetKnownBuildForCandidate(name) or builds[name] end -local function GetCachedTalentSpecList(name) - local bridge = MultiBot and MultiBot.bridge - local specs = bridge and bridge.talentSpecs - - if type(specs) ~= "table" then - return nil - end - - return specs[NormalizeName(name)] or specs[string.lower(ShortName(name) or "")] or specs[name] -end - GuessSpecFromBuild = function(classToken, build) if type(build) ~= "string" then return nil From b0a663af53728aaf47bc68a0814d3413657feb99 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Fri, 1 May 2026 16:20:07 +0200 Subject: [PATCH 11/11] Add missing translations --- Locales/MultiBotAceLocale-deDE.lua | 37 ++++++++++++++++++++++++++++++ Locales/MultiBotAceLocale-enGB.lua | 37 ++++++++++++++++++++++++++++++ Locales/MultiBotAceLocale-enUS.lua | 37 ++++++++++++++++++++++++++++++ Locales/MultiBotAceLocale-esES.lua | 37 ++++++++++++++++++++++++++++++ Locales/MultiBotAceLocale-koKR.lua | 37 ++++++++++++++++++++++++++++++ Locales/MultiBotAceLocale-ruRU.lua | 37 ++++++++++++++++++++++++++++++ Locales/MultiBotAceLocale-zhCN.lua | 37 ++++++++++++++++++++++++++++++ 7 files changed, 259 insertions(+) diff --git a/Locales/MultiBotAceLocale-deDE.lua b/Locales/MultiBotAceLocale-deDE.lua index 2ca6976..fe3e836 100644 --- a/Locales/MultiBotAceLocale-deDE.lua +++ b/Locales/MultiBotAceLocale-deDE.lua @@ -4,6 +4,43 @@ if type(register) ~= "function" then end local deDEValues = { + ["lootmaster.title"] = "MultiBot Plündermeister", + ["lootmaster.refresh"] = "Aktualisieren", + ["lootmaster.assign_to"] = "Zuweisen an:", + ["lootmaster.unknown_item"] = "Unbekannter Gegenstand", + ["lootmaster.unknown_candidate"] = "unbekannt", + ["lootmaster.status.ready"] = "Wähle den Empfänger des Gegenstands aus.", + ["lootmaster.status.not_master"] = "Die Beute ist nicht auf Plündermeister eingestellt, oder du bist nicht der Plündermeister.", + ["lootmaster.status.no_loot"] = "Keine Beute verfügbar.", + ["lootmaster.status.no_candidates"] = "Keine Beutekandidaten für die aktuelle Beute gefunden.", + ["lootmaster.error.not_master"] = "Du bist nicht der Plündermeister.", + ["lootmaster.error.invalid_candidate"] = "Ungültiger Beutekandidat.", + ["lootmaster.assigned"] = "%s wurde %s zugewiesen.", + ["lootmaster.no_candidates_for_item"] = "Keine Kandidaten für diesen Gegenstand.", + ["lootmaster.assign"] = "Zuweisen", + ["lootmaster.select_bot"] = "Bot auswählen", + ["lootmaster.inventory_unavailable"] = "Inventar nicht verfügbar.", + ["lootmaster.priority_score"] = "Priorität:", + ["lootmaster.inventory_hint"] = "Rechtsklick: Inventar/Ausrüstung öffnen.", + ["lootmaster.history_title"] = "Kürzliche Beute", + ["lootmaster.history_empty"] = "Noch keine Beute zugewiesen.", + ["lootmaster.preference_hint"] = "Rechtsklick: speichern. Umschalt+Rechtsklick: löschen.", + ["lootmaster.preference_saved"] = "Präferenz gespeichert: ähnliche Gegenstände -> %s.", + ["lootmaster.preference_cleared"] = "Präferenz für ähnliche Gegenstände gelöscht.", + ["lootmaster.preference_missing"] = "Keine gespeicherte Präferenz für diesen Gegenstand.", + ["options.lootmaster.enable"] = "Beutefenster aktivieren", + ["options.lootmaster.enable_desc"] = "Zeigt das MultiBot-Plündermeisterfenster beim Plündern automatisch an.", + ["lootmaster.profession.jewelcrafting"] = "Juwelenschleifen", + ["lootmaster.profession_known"] = "Beruf: %s", + ["lootmaster.profession_missing"] = "Erforderlicher Beruf: %s (nicht bekannt)", + ["lootmaster.profession_unknown"] = "Erforderlicher Beruf: %s (unbekannt)", + ["lootmaster.professions"] = "Berufe: %s", + ["lootmaster.professions_unknown"] = "Berufe: unbekannt", + ["lootmaster.profession.engineering"] = "Ingenieurskunst", + ["lootmaster.profession.cooking"] = "Kochkunst", + ["lootmaster.profession.fishing"] = "Angeln", + ["lootmaster.profession.firstaid"] = "Erste Hilfe", + ["lootmaster.gear_score"] = "GearScore: %d", ["tips.units.rti"] = "RTI", ["tips.disperse.main"] = "Disperse", ["tips.disperse.set"] = "Disperse-Distanz setzen", diff --git a/Locales/MultiBotAceLocale-enGB.lua b/Locales/MultiBotAceLocale-enGB.lua index abb65fa..ac7f39a 100644 --- a/Locales/MultiBotAceLocale-enGB.lua +++ b/Locales/MultiBotAceLocale-enGB.lua @@ -4,6 +4,43 @@ if type(register) ~= "function" then end local enGBValues = { + ["lootmaster.title"] = "MultiBot Loot Master", + ["lootmaster.refresh"] = "Refresh", + ["lootmaster.assign_to"] = "Assign to:", + ["lootmaster.unknown_item"] = "Unknown item", + ["lootmaster.unknown_candidate"] = "unknown", + ["lootmaster.status.ready"] = "Select the item recipient.", + ["lootmaster.status.not_master"] = "Loot is not set to Master Looter, or you are not the loot master.", + ["lootmaster.status.no_loot"] = "No loot available.", + ["lootmaster.status.no_candidates"] = "No loot candidates found for the current loot.", + ["lootmaster.error.not_master"] = "You are not the loot master.", + ["lootmaster.error.invalid_candidate"] = "Invalid loot candidate.", + ["lootmaster.assigned"] = "%s assigned to %s.", + ["lootmaster.no_candidates_for_item"] = "No candidates for this item.", + ["lootmaster.assign"] = "Assign", + ["lootmaster.select_bot"] = "Select a bot", + ["lootmaster.inventory_unavailable"] = "Inventory unavailable.", + ["lootmaster.priority_score"] = "Priority:", + ["lootmaster.inventory_hint"] = "Right-click: open inventory/equipment.", + ["lootmaster.history_title"] = "Recent Loot", + ["lootmaster.history_empty"] = "No loot assigned yet.", + ["lootmaster.preference_hint"] = "Right-click: save. Shift-right-click: clear.", + ["lootmaster.preference_saved"] = "Preference saved: similar items -> %s.", + ["lootmaster.preference_cleared"] = "Preference cleared for similar items.", + ["lootmaster.preference_missing"] = "No saved preference for this item.", + ["options.lootmaster.enable"] = "Enable loot window", + ["options.lootmaster.enable_desc"] = "Automatically shows the MultiBot loot master window during looting.", + ["lootmaster.profession.jewelcrafting"] = "Jewelcrafting", + ["lootmaster.profession_known"] = "Profession: %s", + ["lootmaster.profession_missing"] = "Required profession: %s (not known)", + ["lootmaster.profession_unknown"] = "Required profession: %s (unknown)", + ["lootmaster.professions"] = "Professions: %s", + ["lootmaster.professions_unknown"] = "Professions: unknown", + ["lootmaster.profession.engineering"] = "Engineering", + ["lootmaster.profession.cooking"] = "Cooking", + ["lootmaster.profession.fishing"] = "Fishing", + ["lootmaster.profession.firstaid"] = "First Aid", + ["lootmaster.gear_score"] = "GearScore: %d", ["tips.units.rti"] = "RTI", ["tips.disperse.main"] = "Disperse", ["tips.disperse.set"] = "Set disperse distance", diff --git a/Locales/MultiBotAceLocale-enUS.lua b/Locales/MultiBotAceLocale-enUS.lua index 7431df4..7a36364 100644 --- a/Locales/MultiBotAceLocale-enUS.lua +++ b/Locales/MultiBotAceLocale-enUS.lua @@ -4,6 +4,43 @@ if type(register) ~= "function" then end local enUSValues = { + ["lootmaster.title"] = "MultiBot Loot Master", + ["lootmaster.refresh"] = "Refresh", + ["lootmaster.assign_to"] = "Assign to:", + ["lootmaster.unknown_item"] = "Unknown item", + ["lootmaster.unknown_candidate"] = "unknown", + ["lootmaster.status.ready"] = "Select the item recipient.", + ["lootmaster.status.not_master"] = "Loot is not set to Master Looter, or you are not the loot master.", + ["lootmaster.status.no_loot"] = "No loot available.", + ["lootmaster.status.no_candidates"] = "No loot candidates found for the current loot.", + ["lootmaster.error.not_master"] = "You are not the loot master.", + ["lootmaster.error.invalid_candidate"] = "Invalid loot candidate.", + ["lootmaster.assigned"] = "%s assigned to %s.", + ["lootmaster.no_candidates_for_item"] = "No candidates for this item.", + ["lootmaster.assign"] = "Assign", + ["lootmaster.select_bot"] = "Select a bot", + ["lootmaster.inventory_unavailable"] = "Inventory unavailable.", + ["lootmaster.priority_score"] = "Priority:", + ["lootmaster.inventory_hint"] = "Right-click: open inventory/equipment.", + ["lootmaster.history_title"] = "Recent Loot", + ["lootmaster.history_empty"] = "No loot assigned yet.", + ["lootmaster.preference_hint"] = "Right-click: save. Shift-right-click: clear.", + ["lootmaster.preference_saved"] = "Preference saved: similar items -> %s.", + ["lootmaster.preference_cleared"] = "Preference cleared for similar items.", + ["lootmaster.preference_missing"] = "No saved preference for this item.", + ["options.lootmaster.enable"] = "Enable loot window", + ["options.lootmaster.enable_desc"] = "Automatically shows the MultiBot loot master window during looting.", + ["lootmaster.profession.jewelcrafting"] = "Jewelcrafting", + ["lootmaster.profession_known"] = "Profession: %s", + ["lootmaster.profession_missing"] = "Required profession: %s (not known)", + ["lootmaster.profession_unknown"] = "Required profession: %s (unknown)", + ["lootmaster.professions"] = "Professions: %s", + ["lootmaster.professions_unknown"] = "Professions: unknown", + ["lootmaster.profession.engineering"] = "Engineering", + ["lootmaster.profession.cooking"] = "Cooking", + ["lootmaster.profession.fishing"] = "Fishing", + ["lootmaster.profession.firstaid"] = "First Aid", + ["lootmaster.gear_score"] = "GearScore: %d", ["tips.units.rti"] = "RTI", ["tips.disperse.main"] = "Disperse", ["tips.disperse.set"] = "Set disperse distance", diff --git a/Locales/MultiBotAceLocale-esES.lua b/Locales/MultiBotAceLocale-esES.lua index 00ff256..d88d8a6 100644 --- a/Locales/MultiBotAceLocale-esES.lua +++ b/Locales/MultiBotAceLocale-esES.lua @@ -4,6 +4,43 @@ if type(register) ~= "function" then end local esESValues = { + ["lootmaster.title"] = "MultiBot Maestro despojador", + ["lootmaster.refresh"] = "Actualizar", + ["lootmaster.assign_to"] = "Asignar a:", + ["lootmaster.unknown_item"] = "Objeto desconocido", + ["lootmaster.unknown_candidate"] = "desconocido", + ["lootmaster.status.ready"] = "Selecciona el destinatario del objeto.", + ["lootmaster.status.not_master"] = "El botín no está configurado como Maestro despojador, o no eres el maestro despojador.", + ["lootmaster.status.no_loot"] = "No hay botín disponible.", + ["lootmaster.status.no_candidates"] = "No se encontraron candidatos de botín para el botín actual.", + ["lootmaster.error.not_master"] = "No eres el maestro despojador.", + ["lootmaster.error.invalid_candidate"] = "Candidato de botín no válido.", + ["lootmaster.assigned"] = "%s asignado a %s.", + ["lootmaster.no_candidates_for_item"] = "No hay candidatos para este objeto.", + ["lootmaster.assign"] = "Asignar", + ["lootmaster.select_bot"] = "Seleccionar un bot", + ["lootmaster.inventory_unavailable"] = "Inventario no disponible.", + ["lootmaster.priority_score"] = "Prioridad:", + ["lootmaster.inventory_hint"] = "Clic derecho: abrir inventario/equipo.", + ["lootmaster.history_title"] = "Botín reciente", + ["lootmaster.history_empty"] = "Aún no se ha asignado botín.", + ["lootmaster.preference_hint"] = "Clic derecho: guardar. Mayús+clic derecho: borrar.", + ["lootmaster.preference_saved"] = "Preferencia guardada: objetos similares -> %s.", + ["lootmaster.preference_cleared"] = "Preferencia borrada para objetos similares.", + ["lootmaster.preference_missing"] = "No hay ninguna preferencia guardada para este objeto.", + ["options.lootmaster.enable"] = "Activar ventana de botín", + ["options.lootmaster.enable_desc"] = "Muestra automáticamente la ventana de maestro despojador de MultiBot durante el despojo.", + ["lootmaster.profession.jewelcrafting"] = "Joyería", + ["lootmaster.profession_known"] = "Profesión: %s", + ["lootmaster.profession_missing"] = "Profesión requerida: %s (no conocida)", + ["lootmaster.profession_unknown"] = "Profesión requerida: %s (desconocida)", + ["lootmaster.professions"] = "Profesiones: %s", + ["lootmaster.professions_unknown"] = "Profesiones: desconocidas", + ["lootmaster.profession.engineering"] = "Ingeniería", + ["lootmaster.profession.cooking"] = "Cocina", + ["lootmaster.profession.fishing"] = "Pesca", + ["lootmaster.profession.firstaid"] = "Primeros auxilios", + ["lootmaster.gear_score"] = "GearScore: %d", ["tips.units.rti"] = "RTI", ["tips.disperse.main"] = "Dispersar", ["tips.disperse.set"] = "Definir distancia de dispersión", diff --git a/Locales/MultiBotAceLocale-koKR.lua b/Locales/MultiBotAceLocale-koKR.lua index 9aeeaf7..90881e7 100644 --- a/Locales/MultiBotAceLocale-koKR.lua +++ b/Locales/MultiBotAceLocale-koKR.lua @@ -4,6 +4,43 @@ if type(register) ~= "function" then end local koKRValues = { + ["lootmaster.title"] = "MultiBot 전리품 담당자", + ["lootmaster.refresh"] = "새로 고침", + ["lootmaster.assign_to"] = "할당 대상:", + ["lootmaster.unknown_item"] = "알 수 없는 아이템", + ["lootmaster.unknown_candidate"] = "알 수 없음", + ["lootmaster.status.ready"] = "아이템을 받을 대상을 선택하세요.", + ["lootmaster.status.not_master"] = "전리품 획득 방식이 담당자 분배로 설정되어 있지 않거나, 당신이 전리품 담당자가 아닙니다.", + ["lootmaster.status.no_loot"] = "사용 가능한 전리품이 없습니다.", + ["lootmaster.status.no_candidates"] = "현재 전리품에 대한 후보를 찾을 수 없습니다.", + ["lootmaster.error.not_master"] = "당신은 전리품 담당자가 아닙니다.", + ["lootmaster.error.invalid_candidate"] = "잘못된 전리품 후보입니다.", + ["lootmaster.assigned"] = "%s|1을;를; %s에게 할당했습니다.", + ["lootmaster.no_candidates_for_item"] = "이 아이템에 대한 후보가 없습니다.", + ["lootmaster.assign"] = "할당", + ["lootmaster.select_bot"] = "봇 선택", + ["lootmaster.inventory_unavailable"] = "인벤토리를 사용할 수 없습니다.", + ["lootmaster.priority_score"] = "우선순위:", + ["lootmaster.inventory_hint"] = "우클릭: 인벤토리/장비 열기.", + ["lootmaster.history_title"] = "최근 전리품", + ["lootmaster.history_empty"] = "아직 할당된 전리품이 없습니다.", + ["lootmaster.preference_hint"] = "우클릭: 저장. Shift+우클릭: 지우기.", + ["lootmaster.preference_saved"] = "선호도 저장됨: 유사 아이템 -> %s.", + ["lootmaster.preference_cleared"] = "유사 아이템에 대한 선호도가 지워졌습니다.", + ["lootmaster.preference_missing"] = "이 아이템에 저장된 선호도가 없습니다.", + ["options.lootmaster.enable"] = "전리품 창 활성화", + ["options.lootmaster.enable_desc"] = "전리품 획득 중 MultiBot 전리품 담당자 창을 자동으로 표시합니다.", + ["lootmaster.profession.jewelcrafting"] = "보석세공", + ["lootmaster.profession_known"] = "전문 기술: %s", + ["lootmaster.profession_missing"] = "필요 전문 기술: %s (알 수 없음)", + ["lootmaster.profession_unknown"] = "필요 전문 기술: %s (알 수 없음)", + ["lootmaster.professions"] = "전문 기술: %s", + ["lootmaster.professions_unknown"] = "전문 기술: 알 수 없음", + ["lootmaster.profession.engineering"] = "기계공학", + ["lootmaster.profession.cooking"] = "요리", + ["lootmaster.profession.fishing"] = "낚시", + ["lootmaster.profession.firstaid"] = "응급치료", + ["lootmaster.gear_score"] = "GearScore: %d", ["tips.units.rti"] = "RTI", ["tips.disperse.main"] = "분산", ["tips.disperse.set"] = "분산 거리 설정", diff --git a/Locales/MultiBotAceLocale-ruRU.lua b/Locales/MultiBotAceLocale-ruRU.lua index 9db2f8a..d02deb0 100644 --- a/Locales/MultiBotAceLocale-ruRU.lua +++ b/Locales/MultiBotAceLocale-ruRU.lua @@ -4,6 +4,43 @@ if type(register) ~= "function" then end local ruRUValues = { + ["lootmaster.title"] = "MultiBot ответственный за добычу", + ["lootmaster.refresh"] = "Обновить", + ["lootmaster.assign_to"] = "Назначить:", + ["lootmaster.unknown_item"] = "Неизвестный предмет", + ["lootmaster.unknown_candidate"] = "неизвестно", + ["lootmaster.status.ready"] = "Выберите получателя предмета.", + ["lootmaster.status.not_master"] = "Режим добычи не установлен на распределение ответственным, или вы не являетесь ответственным за добычу.", + ["lootmaster.status.no_loot"] = "Нет доступной добычи.", + ["lootmaster.status.no_candidates"] = "Не найдено кандидатов для текущей добычи.", + ["lootmaster.error.not_master"] = "Вы не являетесь ответственным за добычу.", + ["lootmaster.error.invalid_candidate"] = "Недопустимый кандидат на добычу.", + ["lootmaster.assigned"] = "%s назначено %s.", + ["lootmaster.no_candidates_for_item"] = "Нет кандидатов на этот предмет.", + ["lootmaster.assign"] = "Назначить", + ["lootmaster.select_bot"] = "Выбрать бота", + ["lootmaster.inventory_unavailable"] = "Инвентарь недоступен.", + ["lootmaster.priority_score"] = "Приоритет:", + ["lootmaster.inventory_hint"] = "ПКМ: открыть инвентарь/экипировку.", + ["lootmaster.history_title"] = "Недавняя добыча", + ["lootmaster.history_empty"] = "Добыча ещё не назначалась.", + ["lootmaster.preference_hint"] = "ПКМ: сохранить. Shift+ПКМ: очистить.", + ["lootmaster.preference_saved"] = "Предпочтение сохранено: похожие предметы -> %s.", + ["lootmaster.preference_cleared"] = "Предпочтение для похожих предметов очищено.", + ["lootmaster.preference_missing"] = "Для этого предмета нет сохранённого предпочтения.", + ["options.lootmaster.enable"] = "Включить окно добычи", + ["options.lootmaster.enable_desc"] = "Автоматически показывает окно MultiBot ответственного за добычу во время сбора добычи.", + ["lootmaster.profession.jewelcrafting"] = "Ювелирное дело", + ["lootmaster.profession_known"] = "Профессия: %s", + ["lootmaster.profession_missing"] = "Требуемая профессия: %s (не изучена)", + ["lootmaster.profession_unknown"] = "Требуемая профессия: %s (неизвестна)", + ["lootmaster.professions"] = "Профессии: %s", + ["lootmaster.professions_unknown"] = "Профессии: неизвестны", + ["lootmaster.profession.engineering"] = "Инженерное дело", + ["lootmaster.profession.cooking"] = "Кулинария", + ["lootmaster.profession.fishing"] = "Рыбная ловля", + ["lootmaster.profession.firstaid"] = "Первая помощь", + ["lootmaster.gear_score"] = "GearScore: %d", ["tips.units.rti"] = "RTI", ["tips.disperse.main"] = "Рассредоточение", ["tips.disperse.set"] = "Задать дистанцию рассредоточения", diff --git a/Locales/MultiBotAceLocale-zhCN.lua b/Locales/MultiBotAceLocale-zhCN.lua index e80114b..bc49f2e 100644 --- a/Locales/MultiBotAceLocale-zhCN.lua +++ b/Locales/MultiBotAceLocale-zhCN.lua @@ -4,6 +4,43 @@ if type(register) ~= "function" then end local zhCNValues = { + ["lootmaster.title"] = "MultiBot 战利品分配者", + ["lootmaster.refresh"] = "刷新", + ["lootmaster.assign_to"] = "分配给:", + ["lootmaster.unknown_item"] = "未知物品", + ["lootmaster.unknown_candidate"] = "未知", + ["lootmaster.status.ready"] = "选择该物品的接收者。", + ["lootmaster.status.not_master"] = "拾取方式不是队长分配,或者你不是战利品分配者。", + ["lootmaster.status.no_loot"] = "没有可用战利品。", + ["lootmaster.status.no_candidates"] = "未找到当前战利品的候选者。", + ["lootmaster.error.not_master"] = "你不是战利品分配者。", + ["lootmaster.error.invalid_candidate"] = "无效的战利品候选者。", + ["lootmaster.assigned"] = "%s 已分配给 %s。", + ["lootmaster.no_candidates_for_item"] = "此物品没有候选者。", + ["lootmaster.assign"] = "分配", + ["lootmaster.select_bot"] = "选择机器人", + ["lootmaster.inventory_unavailable"] = "背包不可用。", + ["lootmaster.priority_score"] = "优先级:", + ["lootmaster.inventory_hint"] = "右键:打开背包/装备。", + ["lootmaster.history_title"] = "最近战利品", + ["lootmaster.history_empty"] = "尚未分配任何战利品。", + ["lootmaster.preference_hint"] = "右键:保存。Shift+右键:清除。", + ["lootmaster.preference_saved"] = "偏好已保存:类似物品 -> %s。", + ["lootmaster.preference_cleared"] = "已清除类似物品的偏好。", + ["lootmaster.preference_missing"] = "此物品没有已保存的偏好。", + ["options.lootmaster.enable"] = "启用战利品窗口", + ["options.lootmaster.enable_desc"] = "拾取时自动显示 MultiBot 战利品分配者窗口。", + ["lootmaster.profession.jewelcrafting"] = "珠宝加工", + ["lootmaster.profession_known"] = "专业:%s", + ["lootmaster.profession_missing"] = "所需专业:%s(未知)", + ["lootmaster.profession_unknown"] = "所需专业:%s(未知)", + ["lootmaster.professions"] = "专业:%s", + ["lootmaster.professions_unknown"] = "专业:未知", + ["lootmaster.profession.engineering"] = "工程学", + ["lootmaster.profession.cooking"] = "烹饪", + ["lootmaster.profession.fishing"] = "钓鱼", + ["lootmaster.profession.firstaid"] = "急救", + ["lootmaster.gear_score"] = "GearScore:%d", ["tips.units.rti"] = "RTI", ["tips.disperse.main"] = "分散", ["tips.disperse.set"] = "设置分散距离",