diff --git a/.luacheckrc b/.luacheckrc index 5fd93fa..c747bfe 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", "GetItemQualityColor", "GiveMasterLoot", "GetLootThreshold" } 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-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-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index 0c4bb3f..93d810b 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -4,6 +4,43 @@ 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.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.", + ["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/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"] = "设置分散距离", 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/TODO.md b/TODO.md index 1a7f3be..096f3e3 100644 --- a/TODO.md +++ b/TODO.md @@ -17,6 +17,30 @@ * 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? +* Y afficher proffeions et niveau +* Monnaies (emblemes etc..) +* Réputations + +## 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. diff --git a/UI/MultiBotLootMasterUI.lua b/UI/MultiBotLootMasterUI.lua new file mode 100644 index 0000000..8356b04 --- /dev/null +++ b/UI/MultiBotLootMasterUI.lua @@ -0,0 +1,2573 @@ +-- MultiBotLootMasterUI.lua + +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 recentLootByCandidate = {} +local lootHistory = {} + +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 = 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 +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 + 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 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 + + 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) + return + end + + if C_Timer and type(C_Timer.After) == "function" then + C_Timer.After(delay, callback) + return + end + + 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 "" + 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 + +GuessSpecFromBuild = function(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 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 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, + 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 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) + + 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 + 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 + + 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, 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" }) + 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 hasTankPrimary = hasDefense or hasDodge or hasParry or hasBlock + local hasHealerHint = hasHealing or hasMp5 or hasSpirit + local primary + + if hasTankPrimary 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", + 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 + +local function BuildLootItemProfile(slot) + local itemLink = GetLootSlotLink(slot) + local _, itemName = GetLootSlotInfo(slot) + local query = itemLink or itemName + local _, _, _, _, _, itemType, itemSubType, _, equipLoc = GetItemInfo(query or "") + local tooltipText = GetLootTooltipText(itemLink) + + return { + link = itemLink, + itemType = itemType, + itemSubType = itemSubType, + equipLoc = equipLoc, + armorType = GetArmorType(itemSubType, equipLoc), + weaponType = GetWeaponType(itemSubType, equipLoc), + statFlags = GetItemStatFlags(itemLink, tooltipText), + requiredProfession = GetRequiredProfessionForLoot(itemName, itemType, itemSubType, tooltipText), + } +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.requiredProfession or "noprofession", + 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 == "physical" end + if role == "bear" then return flags.primary == "tank" 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 == "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 ~= "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 +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) + 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) + 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) + score = score + GetCandidateStatScore(candidate, flags) + end + + if CandidateHadRecentLoot(candidate) then + score = score - 15 + end + + candidate.lootScoreCap = GetCandidateStatScoreCap(candidate, flags) + candidate.lootScore = Clamp(score, 1, candidate.lootScoreCap or 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, candidate.lootScoreCap or 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 "" + 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 + + return GetClassColorCode(candidate.classFile) .. candidate.name .. "|r " .. GetCandidateSpecLabel(candidate) .. " " .. GetCandidateScoreColor(score) .. score .. "%|r" .. prefText .. professionText +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 + +GetClassColorCode = function(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 CLASS_ICON_TEXTURE +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 + +GetBridgeDetail = function(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 + +GetSavedBotSpec = function(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 + +GetSpecFromBridgeDetail = function(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 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 + 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) + + 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 + 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() + 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() + local itemProfile = BuildLootItemProfile(slot) + + 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) + + local candidate = { + index = index, + name = name, + className = className, + classToken = classToken, + 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 + +local function SafeItemText(slot) + local link = GetLootSlotLink(slot) + + if link then + return link + end + + local _, 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 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 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 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) + + 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 + + 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) + 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.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", "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) + 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", frame.history, "TOPRIGHT", 0, 8) + + frame.rows = {} + UpdateLootHistoryFrame(frame) + + 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 + 72) + dropdown:SetHeight((#candidates * 22) + 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:RegisterForClicks("LeftButtonUp", "RightButtonUp") + button:SetHeight(20) + 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 classIcon = GetClassIconTexture(candidate.classFile) + local classIconCoords = GetClassIconCoords(candidate.classFile) + + button.ownerRow = ownerRow + button.candidate = candidate + 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.selectedCandidate = selected + selectedRow.selectButton:SetText(BuildCandidateSelectionText(selected)) + 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:RegisterForClicks("LeftButtonUp", "RightButtonUp") + row.assignButton:SetText(MBLocal("lootmaster.assign", "Assign")) + 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 + AddSystemMessage(MBLocal("lootmaster.error.invalid_candidate", "Invalid loot candidate.")) + 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) + + 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(BuildCandidateSelectionText(candidate)) +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) + MarkCandidateRecentLoot(candidateName) + GiveMasterLoot(slot, candidateIndex) + AddLootHistory(itemText, candidateName) + + 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 + if IsLootSlotRelevant(slot) and not IsLootSlotAssignedLocally(slot) then + local texture, itemName, quantity, quality = 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() + if not IsLootMasterUIEnabled() then + self:Close() + return + end + + 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: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() + + 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 + + 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 + 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 not IsLootMasterUIEnabled() then + if event == "LOOT_CLOSED" then + LootMasterUI:Close() + end + return + end + + if event == "LOOT_OPENED" then + CancelQueuedLootClose() + assignedLootKeys = {} + 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 + QueueLootClose() + end +end) + +_G.MultiBotLootMasterUI = LootMasterUI 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)