From f5a873feefba0b3d8e991bf5f0983fae54e7a91d Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Sat, 21 Mar 2026 22:09:53 +0100 Subject: [PATCH 1/3] Stage 1 --- Core/MultiBotInit.lua | 1020 +---------------- MultiBot.toc | 2 + UI/MultiBotHunterQuickFrame.lua | 1124 +++++++++++++++++++ UI/MultiBotShamanQuickFrame.lua | 766 +++++++++++++ docs/ace3-expansion-checklist.md | 1 + docs/ace3-quick-frames-migration-tracker.md | 189 ++++ docs/ace3-ui-frame-inventory.md | 3 + 7 files changed, 2089 insertions(+), 1016 deletions(-) create mode 100644 UI/MultiBotHunterQuickFrame.lua create mode 100644 UI/MultiBotShamanQuickFrame.lua create mode 100644 docs/ace3-quick-frames-migration-tracker.md diff --git a/Core/MultiBotInit.lua b/Core/MultiBotInit.lua index 2497649..ddfc7b3 100644 --- a/Core/MultiBotInit.lua +++ b/Core/MultiBotInit.lua @@ -1913,6 +1913,8 @@ local function createAceQuestPopupHost(title, width, height, missingDepMessage, return host end +MultiBot.CreateAceQuestPopupHost = MultiBot.CreateAceQuestPopupHost or createAceQuestPopupHost + -- MAIN BUTTON -- local tButton = tRight.addButton("Quests Menu", 0, 0, "achievement_quests_completed_06", @@ -3432,1023 +3434,9 @@ do end end --- HUNTER PETS MENU -- -if not MultiBot.InitHunterQuick then - function MultiBot.InitHunterQuick() - local MBH = MultiBot.HunterQuick or {} - MultiBot.HunterQuick = MBH - - MBH.frame = MultiBot.addFrame("HunterQuick", -820, 300, 36, 36*8, 36*4) - MultiBot.PromoteFrame(MultiBot.HunterQuick.frame) - MBH.frame:SetMovable(true) - MBH.frame:EnableMouse(true) - MBH.frame:RegisterForDrag("RightButton") - MBH.frame:SetScript("OnDragStart", MBH.frame.StartMoving) - -- MBH.frame:SetScript("OnDragStop" , MBH.frame.StopMovingOrSizing) - MBH.frame:SetScript("OnDragStop", function(self) - self:StopMovingOrSizing() - local p, _, rp, x, y = self:GetPoint() - if MultiBot.SetQuickFramePosition then - MultiBot.SetQuickFramePosition("HunterQuick", p, rp, x, y) - end - end) - MBH.frame:Hide() - - function MBH:RestorePosition() - local st = MultiBot.GetQuickFramePosition and MultiBot.GetQuickFramePosition("HunterQuick") - if not st then return end - local f = self.frame - if not f then return end - - if f.ClearAllPoints and f.SetPoint then - f:ClearAllPoints() - f:SetPoint(st.point or "CENTER", UIParent, st.relPoint or "CENTER", st.x or 0, st.y or 0) - elseif f.setPoint then - -- fallback si votre wrapper n’expose que setPoint() - f:setPoint(st.point or "CENTER", st.relPoint or "CENTER", st.x or 0, st.y or 0) - end - end - - function MBH:ResolveUnitToken(name) - if GetNumRaidMembers and GetNumRaidMembers() > 0 then - for i = 1, GetNumRaidMembers() do - local u = "raid"..i - if UnitName(u) == name then return u, ("raidpet"..i) end - end - end - for i = 1, GetNumPartyMembers() do - local u = "party"..i - if UnitName(u) == name then return u, ("partypet"..i) end - end - if UnitName("player") == name then return "player", "pet" end - return nil, nil - end - - function MBH:UpdatePetPresence(row) - local unit, petUnit = self:ResolveUnitToken(row.owner) - row.unit, row.petUnit = unit, petUnit - local hasPet = petUnit and UnitExists(petUnit) and not UnitIsDead(petUnit) - if hasPet then - if row.modesBtn and row.modesBtn.setEnable then row.modesBtn.setEnable() end - else - if row.modesBtn and row.modesBtn.setDisable then row.modesBtn.setDisable() end - if row.modesStrip and row.modesStrip:IsShown() then row.modesStrip:Hide() end - end - end - - function MBH:UpdateAllPetPresence() - for _, r in pairs(self.entries or {}) do - if r.owner then self:UpdatePetPresence(r) end - end - end - - function MBH:GetSavedStance(name) - if MultiBot.GetHunterPetStance then - return MultiBot.GetHunterPetStance(name) - end - return nil - end - - function MBH:SetSavedStance(name, stance) - if MultiBot.SetHunterPetStance then - MultiBot.SetHunterPetStance(name, stance) - end - end - - function MBH:ApplyStanceVisual(row, stance) - row.stanceButtons = row.stanceButtons or {} - for _, btn in pairs(row.stanceButtons) do - if btn and btn.setDisable then btn.setDisable(true) end - end - if stance and row.stanceButtons[stance] and row.stanceButtons[stance].setEnable then - row.stanceButtons[stance].setEnable(true) - end - row.ActiveStance = stance - end - - MBH.entries, MBH.COL_GAP = {}, 40 - - local function SanitizeName(n) - return (tostring(n):gsub("[^%w_]", "_")) - end - - function MBH:BuildForHunter(hName) - local san = SanitizeName(hName) - local row = self.frame.addFrame("HunterQuickRow_"..san, -36*7, 0, 36, 36*8, 36*3) - row.owner = hName - - row.mainBtn = row.addButton("HunterQuickMain_"..san, 0, 0, - "Interface\\AddOns\\MultiBot\\Icons\\class_hunter.blp", - MultiBot.L("tips.hunter.ownbutton"):format(hName)) - row.mainBtn:SetFrameStrata("HIGH") - row.mainBtn:RegisterForDrag("RightButton") - row.mainBtn:SetScript("OnDragStart", function() self.frame:StartMoving() end) - row.mainBtn:SetScript("OnDragStop", function() - self.frame:StopMovingOrSizing() - local p, _, rp, x, y = self.frame:GetPoint() - if MultiBot.SetQuickFramePosition then - MultiBot.SetQuickFramePosition("HunterQuick", p, rp, x, y) - end - end) - - row.vmenu = row.addFrame("HunterQuickMenu_"..san, 0, 0, 36, 36, 36*3) - row.vmenu:Hide() - row.modesBtn = row.vmenu.addButton("HunterModesBtn_"..san, 0, 36, "ability_hunter_beasttaming", MultiBot.L("tips.hunter.pet.stances")) - row.utilsBtn = row.vmenu.addButton("HunterUtilsBtn_"..san, 0, 72, "trade_engineering", MultiBot.L("tips.hunter.pet.master")) - - row.modesStrip = row.addFrame("HunterQuickModesStrip_"..san, 0, 0, 36, 36*7, 36) - row.utilsStrip = row.addFrame("HunterQuickUtilsStrip_"..san, 0, 0, 36, 36*5, 36) - row.modesStrip:ClearAllPoints() - row.modesStrip:SetPoint("BOTTOMLEFT", row.modesBtn, "BOTTOMRIGHT", 0, 0) - row.modesStrip:SetWidth(36*7); row.modesStrip:SetHeight(36) - row.utilsStrip:ClearAllPoints() - row.utilsStrip:SetPoint("BOTTOMLEFT", row.utilsBtn, "BOTTOMRIGHT", 0, 0) - row.utilsStrip:SetWidth(36*5); row.utilsStrip:SetHeight(36) - row.modesStrip:EnableMouse(false); row.utilsStrip:EnableMouse(false) - row.modesStrip:Hide(); row.utilsStrip:Hide() - - MBH:UpdatePetPresence(row) - - row.mainBtn.doLeft = function() - MBH:CloseAllExcept(row) - if row.vmenu:IsShown() then - row.vmenu:Hide() - row.modesStrip:Hide() - row.utilsStrip:Hide() - else - row.vmenu:Show() - end - end - - local labels_and_tips = { - { key="aggressive", tip=MultiBot.L("tips.hunter.pet.aggressive") }, - { key="passive" , tip=MultiBot.L("tips.hunter.pet.passive") }, - { key="defensive" , tip=MultiBot.L("tips.hunter.pet.defensive") }, - { key="stance" , tip=MultiBot.L("tips.hunter.pet.curstance") }, - { key="attack" , tip=MultiBot.L("tips.hunter.pet.attack") }, - { key="follow" , tip=MultiBot.L("tips.hunter.pet.follow") }, - { key="stay" , tip=MultiBot.L("tips.hunter.pet.stay") }, - } - local PET_MODE_ICONS = { - aggressive = "ability_Racial_BloodRage", - passive = "Spell_Nature_Sleep", - defensive = "Ability_Defend", - stance = "Temp", - attack = "Ability_GhoulFrenzy", - follow = "ability_tracking", - stay = "Spell_Nature_TimeStop", - } - - row.stanceButtons = {} - for i, def in ipairs(labels_and_tips) do - local px = -36 * (7 - i) - local tex = PET_MODE_ICONS[def.key] or "inv_misc_questionmark" - local b = row.modesStrip.addButton("HunterQuickMode_"..san.."_"..i, px, 0, tex, def.tip) - if def.key == "aggressive" or def.key == "passive" or def.key == "defensive" then - row.stanceButtons[def.key] = b - if b.setDisable then b.setDisable(true) end - b.doLeft = function() - SendChatMessage("pet "..def.key, "WHISPER", nil, hName) - MBH:ApplyStanceVisual(row, def.key) - MBH:SetSavedStance(hName, def.key) - end - else - b.doLeft = function() - SendChatMessage("pet "..def.key, "WHISPER", nil, hName) - end - end - end - - MBH:ApplyStanceVisual(row, MBH:GetSavedStance(hName)) - - row.modesBtn.doLeft = function() - MBH:CloseAllExcept(row) - if row.modesStrip:IsShown() then - row.modesStrip:Hide() - else - row.modesStrip:Show() - row.utilsStrip:Hide() - MBH:ApplyStanceVisual(row, row.ActiveStance) - end - end - - local petCmdList = { - {"Name", "tame name %s", "inv_scroll_11", MultiBot.L("tips.hunter.pet.name")}, - {"Id", "tame id %s", "inv_scroll_14", MultiBot.L("tips.hunter.pet.id")}, - {"Family", "tame family %s", "inv_misc_enggizmos_03", MultiBot.L("tips.hunter.pet.family")}, - {"Rename", "tame rename %s", "inv_scroll_01", MultiBot.L("tips.hunter.pet.rename")}, - {"Abandon", "tame abandon", "spell_nature_spiritwolf", MultiBot.L("tips.hunter.pet.abandon")}, - } - for i, v in ipairs(petCmdList) do - local label, fmt, icon, tip = v[1], v[2], v[3], v[4] - local px = -36 * (5 - i) - local ub = row.utilsStrip.addButton("HunterQuickUtil_"..san.."_"..i, px, 0, icon, tip) - ub.doLeft = function() - if label == "Rename" then - MBH:ShowPrompt(fmt, hName, MultiBot.L("info.hunterpetnewname")) - row.utilsStrip:Hide() - elseif label == "Id" then - MBH:ShowPrompt(fmt, hName, MultiBot.L("info.hunterpetid")) - row.utilsStrip:Hide() - elseif label == "Family" then - MBH:ShowFamilyFrame(hName) - row.utilsStrip:Hide() - elseif label == "Abandon" then - SendChatMessage(fmt, "WHISPER", nil, hName) - row.utilsStrip:Hide() - else - MBH:EnsureSearchFrame() - local f = MBH.SEARCH_FRAME - f.TargetName = hName - f:Show() - f.EditBox:SetText("") - f.EditBox:SetFocus() - f:Refresh() - row.utilsStrip:Hide() - end - end - end - row.utilsBtn.doLeft = function() - MBH:CloseAllExcept(row) - if row.utilsStrip:IsShown() then - row.utilsStrip:Hide() - else - row.utilsStrip:Show() - row.modesStrip:Hide() - end - end - - self.entries[hName] = row - end - - function MBH:CollectHunterBots() - local out = {} - if GetNumRaidMembers and GetNumRaidMembers() > 0 then - for i=1, GetNumRaidMembers() do - local unit = "raid"..i - local name = UnitName(unit) - local _, cls = UnitClass(unit) - if name and cls == "HUNTER" and (not MultiBot.IsBot or MultiBot.IsBot(name)) then - table.insert(out, name) - end - end - else - if GetNumPartyMembers then - for i=1, GetNumPartyMembers() do - local unit = "party"..i - local name = UnitName(unit) - local _, cls = UnitClass(unit) - if name and cls == "HUNTER" and (not MultiBot.IsBot or MultiBot.IsBot(name)) then - table.insert(out, name) - end - end - end - end - table.sort(out) - return out - end - - function MBH:Rebuild() - local desired = self:CollectHunterBots() - - for name, row in pairs(self.entries) do - local found = false - for _, n in ipairs(desired) do if n==name then found=true; break end end - if not found then - row:Hide() - self.entries[name] = nil - end - end - - for _, name in ipairs(desired) do - if not self.entries[name] then - self:BuildForHunter(name) - end - end - - for idx, name in ipairs(desired) do - local row = self.entries[name] - if row then - row:ClearAllPoints() - row:SetPoint("BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", -36*7 + (idx-1)*self.COL_GAP, 0) - row:Show() - end - end - - if #desired > 0 then self.frame:Show() else self.frame:Hide() end - if self.RestorePosition then self:RestorePosition() end - end - - function MBH:CloseAllExcept(keepRow) - for _, r in pairs(self.entries) do - if r ~= keepRow then - if r.vmenu and r.vmenu:IsShown() then r.vmenu:Hide() end - if r.modesStrip and r.modesStrip:IsShown() then r.modesStrip:Hide() end - if r.utilsStrip and r.utilsStrip:IsShown() then r.utilsStrip:Hide() end - end - end - end - - function MBH:FindHunter() - if UnitExists("target") then - local _, cls = UnitClass("target") - if cls == "HUNTER" then - local tn = UnitName("target") - if tn and tn ~= "Unknown Entity" then return tn end - end - end - local i = MultiBot.index and MultiBot.index.classes - if i then - local p = i.players and i.players["Hunter"] - if p and #p > 0 then return p[1] end - local m = i.members and i.members["Hunter"] - if m and #m > 0 then return m[1] end - local f = i.friends and i.friends["Hunter"] - if f and #f > 0 then return f[1] end - end - return nil - end - - function MBH:ShowPrompt(fmt, targetName, title) - ShowPrompt(title or MultiBot.L("info.hunterpeteditentervalue"), function(text) - if text and text ~= "" and targetName then - local cmd = string.format(fmt, text) - SendChatMessage(cmd, "WHISPER", nil, targetName) - end - end, MultiBot.L("info.hunterpetentersomething")) - end - - function MBH:EnsureSearchFrame() - if self.SEARCH_FRAME then return end - local f = createAceQuestPopupHost(MultiBot.L("info.hunterpetcreaturelist"), 360, 360, "AceGUI-3.0 is required for MBHunterPetSearch", "hunter_pet_search") - assert(f, "AceGUI-3.0 is required for MBHunterPetSearch") - self.SEARCH_FRAME = f - - local e = CreateFrame("EditBox", nil, f, "InputBoxTemplate") - e:SetAutoFocus(true) - e:SetSize(200,20) - e:SetPoint("TOP", 0, -14) - f.EditBox = e - - local PREVIEW_WIDTH, PREVIEW_HEIGHT = 180, 260 - local PREVIEW_MODEL_SCALE = 0.6 - local PREVIEW_FACING = -math.pi/12 - local CURRENT_ENTRY = nil - - local function GetPreviewFrame() - if MBHunterPetPreview then return MBHunterPetPreview end - local p = CreateFrame("PlayerModel","MBHunterPetPreview",UIParent) - p:SetSize(PREVIEW_WIDTH, PREVIEW_HEIGHT) - p:SetBackdrop({ - bgFile="Interface\\Tooltips\\UI-Tooltip-Background", - edgeFile="Interface\\Tooltips\\UI-Tooltip-Border", - tile=true, tileSize=16, edgeSize=16, - insets={left=4,right=4,top=4,bottom=4}}) - p:SetBackdropColor(0,0,0,0.85) - p:SetFrameStrata("DIALOG") - p:SetMovable(true); p:EnableMouse(true) - p:RegisterForDrag("LeftButton") - p:SetScript("OnDragStart", p.StartMoving) - p:SetScript("OnDragStop" , p.StopMovingOrSizing) - CreateFrame("Button",nil,p,"UIPanelCloseButton"):SetPoint("TOPRIGHT",-5,-5) - -- Keep a stable default anchor; do not re-anchor on every preview click. - p:ClearAllPoints() - p:SetPoint("LEFT", UIParent, "CENTER", 180, 20) - return p - end - - local function HidePreviewFrame() - if MBHunterPetPreview and MBHunterPetPreview:IsShown() then - MBHunterPetPreview:Hide() - end - CURRENT_ENTRY = nil - end - - if f.window and f.window.frame and f.window.frame.HookScript then - f.window.frame:HookScript("OnHide", HidePreviewFrame) - end - - local function LoadCreatureToPreview(entryId, displayId) - local pv = GetPreviewFrame() - if pv:IsShown() and CURRENT_ENTRY==entryId then pv:Hide(); CURRENT_ENTRY=nil; return end - CURRENT_ENTRY = entryId - - pv:SetUnit("none") - pv:ClearModel() - pv:Show() - pv:SetScript("OnUpdate", function(self) - self:SetScript("OnUpdate",nil) - self:SetModelScale(PREVIEW_MODEL_SCALE) - self:SetFacing(PREVIEW_FACING) - - -- Prefer direct display ID to avoid cache-dependent creature preview resolution on 3.3.5 clients. - local displayNum = tonumber(displayId) - if displayNum and displayNum > 0 and type(self.SetDisplayInfo) == "function" then - self:SetDisplayInfo(displayNum) - else - self:SetCreature(entryId) - end - end) - end - - local ROW_H, VISIBLE_ROWS = 18, 17 - local OFFSET = 0 - local RESULTS = {} - - local sf = CreateFrame("ScrollFrame","MBHunterPetScroll",f,"UIPanelScrollFrameTemplate") - sf:SetPoint("TOPLEFT",10,-42) - sf:SetPoint("BOTTOMRIGHT",-30,10) - local content = CreateFrame("Frame",nil,sf) ; content:SetSize(1,1) - sf:SetScrollChild(content) - - f.Rows = {} - for i = 1, VISIBLE_ROWS do - local row = CreateFrame("Button", nil, content) - row:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") - row:SetHeight(ROW_H) - row:SetWidth(content:GetWidth()) - row:SetPoint("TOPLEFT", 0, -(i-1)*ROW_H) - - row.text = row:CreateFontString(nil,"ARTWORK","GameFontNormalSmall") - row.text:SetPoint("LEFT",2,0) - - local btn = CreateFrame("Button", nil, row) - btn:SetSize(16,16) - btn:SetPoint("RIGHT",-22,0) - btn:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-UP") - btn:SetPushedTexture("Interface\\Buttons\\UI-PlusButton-DOWN") - btn:SetHighlightTexture("Interface\\Buttons\\UI-PlusButton-Hilight") - row.previewBtn = btn - - f.Rows[i] = row - end - - local function localeField() - local l = GetLocale():lower() - if l=="frfr" then return "name_fr" - elseif l=="dede" then return "name_de" - elseif l=="eses" then return "name_es" - elseif l=="esmx" then return "name_esmx" - elseif l=="kokr" then return "name_ko" - elseif l=="zhtw" then return "name_zhtw" - elseif l=="zhcn" then return "name_zhcn" - elseif l=="ruru" then return "name_ru" - else return "name_en" end - end - - function f:RefreshRows() - for i = 1, VISIBLE_ROWS do - local idx = i + OFFSET - local data = RESULTS[idx] - local row = self.Rows[i] - local LIST_W = 320 - - row:ClearAllPoints() - row:SetPoint("TOPLEFT", 0, -((i-1 + OFFSET) * ROW_H)) - row:SetWidth(LIST_W) - - if data then - row.text:SetText( - string.format("|cffffd200%-24s|r |cff888888[%s]|r", - data.name, MultiBot.PET_FAMILY[data.family] or "?")) - - row:SetScript("OnClick", function() - if f.TargetName then - SendChatMessage(("tame id %d"):format(data.id), "WHISPER", nil, f.TargetName) - end - f:Hide() - end) - - row.previewBtn:SetScript("OnClick", function() - LoadCreatureToPreview(data.id, data.display) - end) +-- HUNTER QUICK FRAME MOVED TO UI\MultiBotHunterQuickFrame.lua -- - row:Show() - else - row:Hide() - end - end - end - - sf:SetScript("OnVerticalScroll", function(_,delta) - local newOffset = math.floor(sf:GetVerticalScroll()/ROW_H + 0.5) - if newOffset ~= OFFSET then OFFSET = newOffset; f:RefreshRows() end - end) - - function f:Refresh() - wipe(RESULTS) - local filter = (e:GetText() or ""):lower() - local field = localeField() - - for id,info in pairs(MultiBot.PET_DATA) do - local name = info[field] or info.name_en - if name:lower():find(filter,1,true) then - RESULTS[#RESULTS+1] = {id=id,name=name,family=info.family,display=info.display} - end - end - table.sort(RESULTS,function(a,b) return a.name 0 then + service.window:SetTitle(string.format("%s (%d)", WINDOW_TITLE, count)) + else + service.window:SetTitle(WINDOW_TITLE) + end +end + +local function persistWindowPosition(frame) + if not frame or not MultiBot.SetQuickFramePosition then + return + end + + local point, _, relPoint, x, y = frame:GetPoint() + MultiBot.SetQuickFramePosition(HUNTER_QUICK_FRAME_KEY, point, relPoint, x, y) +end + +local function bindWindowDrag(service) + if not service.window or not service.window.frame or service.__dragBound then + return + end + + service.__dragBound = true + + local frame = service.window.frame + frame:SetMovable(true) + frame:SetClampedToScreen(true) + + local title = service.window.title + if title then + title:HookScript("OnMouseDown", function(_, mouseButton) + if mouseButton ~= "RightButton" then + return + end + frame:StartMoving() + frame.__mbRightDragging = true + end) + + title:HookScript("OnMouseUp", function() + if not frame.__mbRightDragging then + return + end + frame.__mbRightDragging = nil + frame:StopMovingOrSizing() + persistWindowPosition(frame) + end) + end + + frame:HookScript("OnHide", function(current) + if current.__mbRightDragging then + current.__mbRightDragging = nil + current:StopMovingOrSizing() + end + end) + + frame:HookScript("OnMouseUp", function(current) + if not current.__mbRightDragging then + return + end + current.__mbRightDragging = nil + current:StopMovingOrSizing() + persistWindowPosition(current) + end) + + frame:HookScript("OnDragStop", function(current) + persistWindowPosition(current) + end) +end + +function HunterQuick:RestorePosition() + self:EnsureWindow() + + local frame = self.window and self.window.frame + if not frame then + return + end + + local state = MultiBot.GetQuickFramePosition and MultiBot.GetQuickFramePosition(HUNTER_QUICK_FRAME_KEY) or nil + state = state or WINDOW_DEFAULT_POINT + + frame:ClearAllPoints() + frame:SetPoint(state.point or "CENTER", UIParent, state.relPoint or "CENTER", state.x or 0, state.y or 0) +end + +function HunterQuick:ResolveUnitToken(name) + if GetNumRaidMembers and GetNumRaidMembers() > 0 then + for index = 1, GetNumRaidMembers() do + local unit = "raid" .. index + if UnitName(unit) == name then + return unit, "raidpet" .. index + end + end + end + + local partyCount = GetNumPartyMembers and GetNumPartyMembers() or 0 + for index = 1, partyCount do + local unit = "party" .. index + if UnitName(unit) == name then + return unit, "partypet" .. index + end + end + + if UnitName("player") == name then + return "player", "pet" + end + + return nil, nil +end + +function HunterQuick:CollectHunterBots() + local names = {} + + if GetNumRaidMembers and GetNumRaidMembers() > 0 then + for index = 1, GetNumRaidMembers() do + local unit = "raid" .. index + local name = UnitName(unit) + local _, classToken = UnitClass(unit) + if name and classToken == "HUNTER" and (not MultiBot.IsBot or MultiBot.IsBot(name)) then + table.insert(names, name) + end + end + else + local partyCount = GetNumPartyMembers and GetNumPartyMembers() or 0 + for index = 1, partyCount do + local unit = "party" .. index + local name = UnitName(unit) + local _, classToken = UnitClass(unit) + if name and classToken == "HUNTER" and (not MultiBot.IsBot or MultiBot.IsBot(name)) then + table.insert(names, name) + end + end + end + + table.sort(names) + return names +end + +function HunterQuick:GetSavedStance(name) + if MultiBot.GetHunterPetStance then + return MultiBot.GetHunterPetStance(name) + end + return nil +end + +function HunterQuick:SetSavedStance(name, stance) + if MultiBot.SetHunterPetStance then + MultiBot.SetHunterPetStance(name, stance) + end +end + +function HunterQuick:ApplyStanceVisual(row, stance) + row.stanceButtons = row.stanceButtons or {} + for _, button in pairs(row.stanceButtons) do + if button and button.SetButtonSelected then + button:SetButtonSelected(false) + end + end + + if stance and row.stanceButtons[stance] and row.stanceButtons[stance].SetButtonSelected then + row.stanceButtons[stance]:SetButtonSelected(true) + end + + row.activeStance = stance +end + +function HunterQuick:UpdatePetPresence(row) + if not row then + return + end + + local unit, petUnit = self:ResolveUnitToken(row.owner) + row.unit = unit + row.petUnit = petUnit + + local hasPet = petUnit and UnitExists(petUnit) and not UnitIsDead(petUnit) + row.hasPet = hasPet and true or false + + if row.modesButton and row.modesButton.SetButtonDisabled then + row.modesButton:SetButtonDisabled(not row.hasPet) + end + + if not row.hasPet and row.modesStrip and row.modesStrip:IsShown() then + row.modesStrip:Hide() + end +end + +function HunterQuick:UpdateAllPetPresence() + for _, row in pairs(self.entries or {}) do + self:UpdatePetPresence(row) + end +end + +function HunterQuick:CloseAllExcept(keepRow) + for _, row in pairs(self.entries or {}) do + if row ~= keepRow then + if row.menuFrame then row.menuFrame:Hide() end + if row.modesButton then row.modesButton:Hide() end + if row.utilsButton then row.utilsButton:Hide() end + if row.modesStrip then row.modesStrip:Hide() end + if row.utilsStrip then row.utilsStrip:Hide() end + row.expanded = false + end + end +end + +function HunterQuick:ToggleRow(row) + if not row then + return + end + + self:CloseAllExcept(row) + + row.expanded = not row.expanded + if row.expanded then + row.menuFrame:Show() + row.modesButton:Show() + row.utilsButton:Show() + else + row.menuFrame:Hide() + row.modesButton:Hide() + row.utilsButton:Hide() + row.modesStrip:Hide() + row.utilsStrip:Hide() + end +end + +function HunterQuick:ToggleStrip(row, stripKey) + if not row then + return + end + + self:CloseAllExcept(row) + row.expanded = true + row.menuFrame:Show() + row.modesButton:Show() + row.utilsButton:Show() + + local showModes = stripKey == "modes" + if row.modesStrip then + if showModes and row.hasPet then row.modesStrip:Show() else row.modesStrip:Hide() end + end + if row.utilsStrip then + if showModes then row.utilsStrip:Hide() else row.utilsStrip:Show() end + end + + if showModes then + self:ApplyStanceVisual(row, row.activeStance) + end +end + +function HunterQuick:ShowPrompt(formatString, targetName, title) + if type(ShowPrompt) ~= "function" then + return + end + + ShowPrompt(title or MultiBot.L("info.hunterpeteditentervalue"), function(text) + if text and text ~= "" and targetName then + SendChatMessage(string.format(formatString, text), "WHISPER", nil, targetName) + end + end, MultiBot.L("info.hunterpetentersomething")) +end + +function HunterQuick:EnsureSearchFrame() + if self.SEARCH_FRAME then + return self.SEARCH_FRAME + end + + local host = getPopupHost(MultiBot.L("info.hunterpetcreaturelist"), 360, 360, "AceGUI-3.0 is required for MBHunterPetSearch", "hunter_pet_search") + assert(host, "AceGUI-3.0 is required for MBHunterPetSearch") + + self.SEARCH_FRAME = host + + local searchBar = CreateFrame("Frame", nil, host) + searchBar:SetPoint("TOP", host, "TOP", 0, -18) + searchBar:SetSize(248, 26) + addInputBackdrop(searchBar) + host.SearchBar = searchBar + + local editBox = CreateFrame("EditBox", nil, searchBar) + editBox:SetAutoFocus(true) + editBox:SetPoint("TOPLEFT", searchBar, "TOPLEFT", 6, -5) + editBox:SetPoint("BOTTOMRIGHT", searchBar, "BOTTOMRIGHT", -6, 5) + editBox:SetFontObject(GameFontHighlightSmall) + editBox:SetTextInsets(4, 4, 0, 0) + editBox:SetScript("OnEscapePressed", function(self) + self:ClearFocus() + end) + host.EditBox = editBox + + local previewWidth, previewHeight = 180, 260 + local previewScale = 0.6 + local previewFacing = -math.pi / 12 + local currentEntry = nil + + local function getPreviewFrame() + if MBHunterPetPreview then + return MBHunterPetPreview + end + + local preview = CreateFrame("PlayerModel", "MBHunterPetPreview", UIParent) + preview:SetSize(previewWidth, previewHeight) + preview:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 }, + }) + preview:SetBackdropColor(0, 0, 0, 0.85) + preview:SetFrameStrata("DIALOG") + preview:SetMovable(true) + preview:EnableMouse(true) + preview:RegisterForDrag("LeftButton") + preview:SetScript("OnDragStart", preview.StartMoving) + preview:SetScript("OnDragStop", preview.StopMovingOrSizing) + CreateFrame("Button", nil, preview, "UIPanelCloseButton"):SetPoint("TOPRIGHT", -5, -5) + preview:ClearAllPoints() + preview:SetPoint("LEFT", UIParent, "CENTER", 180, 20) + return preview + end + + local function hidePreviewFrame() + if MBHunterPetPreview and MBHunterPetPreview:IsShown() then + MBHunterPetPreview:Hide() + end + currentEntry = nil + end + + if host.window and host.window.frame and host.window.frame.HookScript then + host.window.frame:HookScript("OnHide", hidePreviewFrame) + end + + local function loadCreaturePreview(entryId, displayId) + local preview = getPreviewFrame() + if preview:IsShown() and currentEntry == entryId then + preview:Hide() + currentEntry = nil + return + end + + currentEntry = entryId + preview:SetUnit("none") + preview:ClearModel() + preview:Show() + preview:SetScript("OnUpdate", function(self) + self:SetScript("OnUpdate", nil) + self:SetModelScale(previewScale) + self:SetFacing(previewFacing) + + local displayNumber = tonumber(displayId) + if displayNumber and displayNumber > 0 and type(self.SetDisplayInfo) == "function" then + self:SetDisplayInfo(displayNumber) + else + self:SetCreature(entryId) + end + end) + end + + local rowHeight = 18 + local visibleRows = 17 + local offset = 0 + local results = {} + local familyLocalization = MultiBot.PET_FAMILY_L10N and MultiBot.PET_FAMILY_L10N[GetLocale()] or nil + + local function getFamilyLabel(familyId) + if familyLocalization and familyLocalization[familyId] then + return familyLocalization[familyId] + end + + return MultiBot.PET_FAMILY[familyId] or "?" + end + + local scrollFrame = CreateFrame("ScrollFrame", "MBHunterPetScroll", host, "UIPanelScrollFrameTemplate") + scrollFrame:SetPoint("TOPLEFT", host, "TOPLEFT", 10, -58) + scrollFrame:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -30, 10) + + local content = CreateFrame("Frame", nil, scrollFrame) + content:SetSize(1, 1) + scrollFrame:SetScrollChild(content) + + host.Rows = {} + for index = 1, visibleRows do + local row = CreateFrame("Button", nil, content) + row:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") + row:SetHeight(rowHeight) + row:SetWidth(content:GetWidth()) + row:SetPoint("TOPLEFT", 0, -(index - 1) * rowHeight) + + row.text = row:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + row.text:SetPoint("LEFT", 2, 0) + + local previewButton = CreateFrame("Button", nil, row) + previewButton:SetSize(16, 16) + previewButton:SetPoint("RIGHT", -22, 0) + previewButton:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-UP") + previewButton:SetPushedTexture("Interface\\Buttons\\UI-PlusButton-DOWN") + previewButton:SetHighlightTexture("Interface\\Buttons\\UI-PlusButton-Hilight") + row.previewButton = previewButton + + host.Rows[index] = row + end + + local function localeField() + local locale = GetLocale():lower() + if locale == "frfr" then return "name_fr" end + if locale == "dede" then return "name_de" end + if locale == "eses" then return "name_es" end + if locale == "esmx" then return "name_esmx" end + if locale == "kokr" then return "name_ko" end + if locale == "zhtw" then return "name_zhtw" end + if locale == "zhcn" then return "name_zhcn" end + if locale == "ruru" then return "name_ru" end + return "name_en" + end + + function host:RefreshRows() + local listWidth = 320 + for index = 1, visibleRows do + local dataIndex = index + offset + local data = results[dataIndex] + local row = self.Rows[index] + + row:ClearAllPoints() + row:SetPoint("TOPLEFT", 0, -((index - 1 + offset) * rowHeight)) + row:SetWidth(listWidth) + + if data then + row.text:SetText(string.format("|cffffd200%-24s|r |cff888888[%s]|r", data.name, getFamilyLabel(data.family))) + row:SetScript("OnClick", function() + if host.TargetName then + SendChatMessage(("tame id %d"):format(data.id), "WHISPER", nil, host.TargetName) + end + host:Hide() + end) + row.previewButton:SetScript("OnClick", function() + loadCreaturePreview(data.id, data.display) + end) + row:Show() + else + row:Hide() + end + end + end + + scrollFrame:SetScript("OnVerticalScroll", function(_, delta) + local newOffset = math.floor(scrollFrame:GetVerticalScroll() / rowHeight + 0.5) + if newOffset ~= offset then + offset = newOffset + host:RefreshRows() + end + end) + + function host:Refresh() + wipe(results) + local filter = (editBox:GetText() or ""):lower() + local field = localeField() + + for creatureId, info in pairs(MultiBot.PET_DATA) do + local localizedName = info[field] or info.name_en + if localizedName:lower():find(filter, 1, true) then + results[#results + 1] = { id = creatureId, name = localizedName, family = info.family, display = info.display } + end + end + + table.sort(results, function(left, right) + return left.name < right.name + end) + + content:SetHeight(#results * rowHeight) + offset = 0 + scrollFrame:SetVerticalScroll(0) + host:RefreshRows() + end + + editBox:SetScript("OnTextChanged", function() + host:Refresh() + end) + + return host +end + +function HunterQuick:ShowFamilyFrame(targetName) + local frame = self.FAMILY_FRAME + if frame then + frame.TargetName = targetName + frame:Show() + return + end + + frame = getPopupHost(MultiBot.L("info.hunterpetrandomfamily"), 260, 340, "AceGUI-3.0 is required for MBHunterPetFamily", "hunter_pet_family") + assert(frame, "AceGUI-3.0 is required for MBHunterPetFamily") + self.FAMILY_FRAME = frame + frame.TargetName = targetName + + local scrollFrame = CreateFrame("ScrollFrame", "MBHunterFamilyScroll", frame, "UIPanelScrollFrameTemplate") + scrollFrame:SetPoint("TOPLEFT", 8, -10) + scrollFrame:SetPoint("BOTTOMRIGHT", -28, 8) + + local listWidth = 320 + local content = CreateFrame("Frame", nil, scrollFrame) + content:SetSize(listWidth, 1) + scrollFrame:SetScrollChild(content) + frame.Content = content + frame.Rows = {} + + local rowHeight = 18 + local locale = GetLocale() + local localization = MultiBot.PET_FAMILY_L10N and MultiBot.PET_FAMILY_L10N[locale] + local families = {} + + for familyId, englishName in pairs(MultiBot.PET_FAMILY) do + local localized = (localization and localization[familyId]) or englishName + table.insert(families, { id = familyId, eng = englishName, txt = localized }) + end + + table.sort(families, function(left, right) + return left.txt < right.txt + end) + + for index, data in ipairs(families) do + local row = CreateFrame("Button", nil, content) + row:EnableMouse(true) + row:SetHeight(rowHeight) + row:SetPoint("TOPLEFT", 0, -(index - 1) * rowHeight) + row:SetWidth(content:GetWidth()) + row:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") + + row.text = row:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + row.text:SetPoint("LEFT") + row.text:SetText("|cffffd200" .. data.txt .. "|r") + row:SetScript("OnClick", function() + local currentTargetName = frame.TargetName + if currentTargetName then + SendChatMessage(("tame family %s"):format(data.eng), "WHISPER", nil, currentTargetName) + end + frame:Hide() + end) + end + + frame:Show() +end + +function HunterQuick:BuildUtilityAction(row, definition, index) + local button = createIconButton(row.utilsStrip, string.format("MultiBotHunterQuickUtil_%s_%d", sanitizeName(row.owner), index), definition.icon, MultiBot.L(definition.tip), BUTTON_SIZE) + button:SetPoint("TOPLEFT", (index - 1) * (BUTTON_SIZE + BUTTON_GAP), 0) + + button:SetScript("OnClick", function() + if definition.action == "prompt_rename" then + self:ShowPrompt(definition.command, row.owner, MultiBot.L("info.hunterpetnewname")) + row.utilsStrip:Hide() + elseif definition.action == "prompt_id" then + self:ShowPrompt(definition.command, row.owner, MultiBot.L("info.hunterpetid")) + row.utilsStrip:Hide() + elseif definition.action == "family" then + self:ShowFamilyFrame(row.owner) + row.utilsStrip:Hide() + elseif definition.action == "direct" then + SendChatMessage(definition.command, "WHISPER", nil, row.owner) + row.utilsStrip:Hide() + else + local searchFrame = self:EnsureSearchFrame() + searchFrame.TargetName = row.owner + searchFrame:Show() + searchFrame.EditBox:SetText("") + searchFrame.EditBox:SetFocus() + searchFrame:Refresh() + row.utilsStrip:Hide() + end + end) + + return button +end + +function HunterQuick:BuildStanceAction(row, definition, index) + local button = createIconButton(row.modesStrip, string.format("MultiBotHunterQuickMode_%s_%d", sanitizeName(row.owner), index), definition.icon, MultiBot.L(definition.tip), BUTTON_SIZE) + button:SetPoint("TOPLEFT", (index - 1) * (BUTTON_SIZE + BUTTON_GAP), 0) + + if definition.persistent then + row.stanceButtons[definition.key] = button + end + + button:SetScript("OnClick", function() + if button.__mbDisabled then + return + end + + SendChatMessage("pet " .. definition.key, "WHISPER", nil, row.owner) + if definition.persistent then + self:ApplyStanceVisual(row, definition.key) + self:SetSavedStance(row.owner, definition.key) + end + end) + + return button +end + +function HunterQuick:BuildRow(ownerName) + self:EnsureWindow() + + local root = CreateFrame("Frame", string.format("MultiBotHunterQuickRow_%s", sanitizeName(ownerName)), self.canvas) + root:SetSize(ROW_WIDTH, ROW_HEIGHT) + root.owner = ownerName + root.expanded = false + root.stanceButtons = {} + + local tooltipText = (MultiBot.L("tips.hunter.ownbutton") or "Hunter: %s"):format(ownerName) + local mainButton = createIconButton(root, string.format("MultiBotHunterQuickMain_%s", sanitizeName(ownerName)), "Interface\\AddOns\\MultiBot\\Icons\\class_hunter.blp", tooltipText, BUTTON_SIZE) + mainButton:SetPoint("TOPLEFT", 0, 0) + mainButton:RegisterForDrag("RightButton") + mainButton:SetScript("OnDragStart", function() + if self.window and self.window.frame then + self.window.frame:StartMoving() + self.window.frame.__mbRightDragging = true + end + end) + mainButton:SetScript("OnDragStop", function() + local frame = self.window and self.window.frame + if not frame then + return + end + frame.__mbRightDragging = nil + frame:StopMovingOrSizing() + persistWindowPosition(frame) + end) + mainButton:SetScript("OnClick", function(_, mouseButton) + if mouseButton == "LeftButton" then + self:ToggleRow(root) + end + end) + root.mainButton = mainButton + + local menuFrame = CreateFrame("Frame", nil, root) + menuFrame:SetPoint("TOPLEFT", mainButton, "BOTTOMLEFT", 0, -BUTTON_GAP) + menuFrame:SetSize(BUTTON_SIZE, (BUTTON_SIZE * 2) + BUTTON_GAP) + menuFrame:Hide() + root.menuFrame = menuFrame + + local modesButton = createIconButton(menuFrame, string.format("MultiBotHunterQuickModes_%s", sanitizeName(ownerName)), "ability_hunter_beasttaming", MultiBot.L("tips.hunter.pet.stances"), BUTTON_SIZE) + modesButton:SetPoint("TOPLEFT", 0, 0) + modesButton:Hide() + modesButton:SetScript("OnClick", function() + if not root.hasPet then + return + end + self:ToggleStrip(root, "modes") + end) + root.modesButton = modesButton + + local utilsButton = createIconButton(menuFrame, string.format("MultiBotHunterQuickUtils_%s", sanitizeName(ownerName)), "trade_engineering", MultiBot.L("tips.hunter.pet.master"), BUTTON_SIZE) + utilsButton:SetPoint("TOPLEFT", 0, -(BUTTON_SIZE + BUTTON_GAP)) + utilsButton:Hide() + utilsButton:SetScript("OnClick", function() + self:ToggleStrip(root, "utils") + end) + root.utilsButton = utilsButton + + local modesStrip = CreateFrame("Frame", nil, root) + modesStrip:SetPoint("TOPLEFT", modesButton, "TOPRIGHT", BUTTON_GAP, 0) + modesStrip:SetSize((BUTTON_SIZE * #PET_STANCE_DEFINITIONS) + (BUTTON_GAP * (#PET_STANCE_DEFINITIONS - 1)), BUTTON_SIZE) + modesStrip:Hide() + root.modesStrip = modesStrip + + local utilsStrip = CreateFrame("Frame", nil, root) + utilsStrip:SetPoint("TOPLEFT", utilsButton, "TOPRIGHT", BUTTON_GAP, 0) + utilsStrip:SetSize((BUTTON_SIZE * #PET_UTILITY_DEFINITIONS) + (BUTTON_GAP * (#PET_UTILITY_DEFINITIONS - 1)), BUTTON_SIZE) + utilsStrip:Hide() + root.utilsStrip = utilsStrip + + for index, definition in ipairs(PET_STANCE_DEFINITIONS) do + self:BuildStanceAction(root, definition, index) + end + for index, definition in ipairs(PET_UTILITY_DEFINITIONS) do + self:BuildUtilityAction(root, definition, index) + end + + self:ApplyStanceVisual(root, self:GetSavedStance(ownerName)) + self:UpdatePetPresence(root) + + self.entries[ownerName] = root + return root +end + +function HunterQuick:UpdateWindowGeometry(count) + self:EnsureWindow() + + count = math.max(tonumber(count) or 0, 1) + local width = (WINDOW_PADDING_X * 2) + ROW_WIDTH + ((count - 1) * self:GetRowSpacing()) + + self.window:SetWidth(width) + self.window:SetHeight(WINDOW_HEIGHT) + self.canvas:SetWidth(width - (WINDOW_PADDING_X * 2)) + self.canvas:SetHeight(WINDOW_HEIGHT - (WINDOW_PADDING_Y * 2)) + updateWindowTitle(self, count) +end + +function HunterQuick:EnsureWindow() + if self.window and self.window.frame then + return self.window + end + + local aceGUI = getAceGUI() + if not aceGUI then + UIErrorsFrame:AddMessage("AceGUI-3.0 is required for Hunter Quick", 1, 0.2, 0.2, 1) + return nil + end + + local window = aceGUI:Create("Window") + window:SetTitle(WINDOW_TITLE) + window:SetLayout("Manual") + window:SetWidth((WINDOW_PADDING_X * 2) + ROW_WIDTH) + window:SetHeight(WINDOW_HEIGHT) + window.frame:SetFrameStrata("HIGH") + window.frame:SetClampedToScreen(true) + window.frame:SetPoint(WINDOW_DEFAULT_POINT.point, UIParent, WINDOW_DEFAULT_POINT.relPoint, WINDOW_DEFAULT_POINT.x, WINDOW_DEFAULT_POINT.y) + window:SetCallback("OnClose", function(widget) + widget:Hide() + end) + window:Hide() + + stripWindowChrome(window) + + if window.content then + window.content:ClearAllPoints() + window.content:SetPoint("TOPLEFT", window.frame, "TOPLEFT", 0, 0) + window.content:SetPoint("BOTTOMRIGHT", window.frame, "BOTTOMRIGHT", 0, 0) + end + + local canvas = CreateFrame("Frame", nil, window.content) + canvas:SetPoint("TOPLEFT", window.content, "TOPLEFT", WINDOW_PADDING_X, -WINDOW_PADDING_Y) + canvas:SetPoint("BOTTOMRIGHT", window.content, "BOTTOMRIGHT", -WINDOW_PADDING_X, WINDOW_PADDING_Y) + + self.window = window + self.frame = window.frame + self.canvas = canvas + self.__aceInitialized = true + + bindWindowDrag(self) + self:RestorePosition() + + return window +end + +function HunterQuick:Rebuild() + self:EnsureWindow() + + local desiredNames = self:CollectHunterBots() + local desiredLookup = {} + for _, name in ipairs(desiredNames) do + desiredLookup[name] = true + end + + for name, row in pairs(self.entries) do + if not desiredLookup[name] then + row:Hide() + row:SetParent(nil) + self.entries[name] = nil + end + end + + for _, name in ipairs(desiredNames) do + if not self.entries[name] then + self:BuildRow(name) + end + end + + for index, name in ipairs(desiredNames) do + local row = self.entries[name] + if row then + row:ClearAllPoints() + row:SetPoint("TOPLEFT", self.canvas, "TOPLEFT", (index - 1) * self:GetRowSpacing(), 0) + row:SetFrameLevel((self.window.frame:GetFrameLevel() or 0) + 2) + row:Show() + end + end + + if #desiredNames > 0 then + self:UpdateWindowGeometry(#desiredNames) + self.window:Show() + self:RestorePosition() + self:UpdateAllPetPresence() + elseif self.window then + updateWindowTitle(self, 0) + self.window:Hide() + end +end + +function MultiBot.InitHunterQuick() + if HunterQuick.__moduleReady then + return HunterQuick + end + + HunterQuick.__moduleReady = true + HunterQuick:EnsureWindow() + + if MultiBot.TimerAfter then + MultiBot.TimerAfter(0.5, function() + if MultiBot and MultiBot.HunterQuick and MultiBot.HunterQuick.Rebuild then + MultiBot.HunterQuick:Rebuild() + end + end) + end + + return HunterQuick +end + +MultiBot.InitHunterQuick() + +if MultiBot.HunterQuick and MultiBot.HunterQuick.RestorePosition then + MultiBot.HunterQuick:RestorePosition() +end \ No newline at end of file diff --git a/UI/MultiBotShamanQuickFrame.lua b/UI/MultiBotShamanQuickFrame.lua new file mode 100644 index 0000000..4b7f491 --- /dev/null +++ b/UI/MultiBotShamanQuickFrame.lua @@ -0,0 +1,766 @@ +if not MultiBot then return end + +local SHAMAN_QUICK_FRAME_KEY = "ShamanQuick" +local BUTTON_SIZE = 25 +local BUTTON_GAP = 4 +local ROW_WIDTH = 252 +local ROW_HEIGHT = (BUTTON_SIZE * 5) + (BUTTON_GAP * 4) +local SHAMAN_QUICK_ROW_SPACING_DEFAULT = 26 +local WINDOW_HEIGHT = ROW_HEIGHT +local WINDOW_PADDING_X = 0 +local WINDOW_PADDING_Y = 0 +local WINDOW_TITLE = "Quick Shaman" +local WINDOW_DEFAULT_POINT = { point = "CENTER", relPoint = "CENTER", x = -420, y = 240 } +local ICON_FALLBACK = "Interface\\Icons\\INV_Misc_QuestionMark" + +local ELEMENT_DEFINITIONS = { + { + key = "earth", + defaultIcon = "spell_nature_earthbindtotem", + tip = "tips.shaman.ctotem.earthtot", + spells = { + { key = "strength_of_earth", icon = "spell_nature_earthbindtotem", tip = "tips.shaman.ctotem.stoe", spell = "strength of earth" }, + { key = "stoneskin", icon = "spell_nature_stoneskintotem", tip = "tips.shaman.ctotem.stoskin", spell = "stoneskin" }, + { key = "tremor", icon = "spell_nature_tremortotem", tip = "tips.shaman.ctotem.tremor", spell = "tremor" }, + { key = "earthbind", icon = "spell_nature_strengthofearthtotem02", tip = "tips.shaman.ctotem.eabind", spell = "earthbind" }, + }, + }, + { + key = "fire", + defaultIcon = "spell_fire_searingtotem", + tip = "tips.shaman.ctotem.firetot", + spells = { + { key = "searing", icon = "spell_fire_searingtotem", tip = "tips.shaman.ctotem.searing", spell = "searing" }, + { key = "magma", icon = "spell_fire_moltenblood", tip = "tips.shaman.ctotem.magma", spell = "magma" }, + { key = "flametongue", icon = "spell_nature_guardianward", tip = "tips.shaman.ctotem.fltong", spell = "flametongue" }, + { key = "wrath", icon = "spell_fire_totemofwrath", tip = "tips.shaman.ctotem.towrath", spell = "wrath" }, + { key = "frost_resistance", icon = "spell_frost_frostward", tip = "tips.shaman.ctotem.frostres", spell = "frost resistance" }, + }, + }, + { + key = "water", + defaultIcon = "spell_nature_manaregentotem", + tip = "tips.shaman.ctotem.watertot", + spells = { + { key = "healing_stream", icon = "spell_nature_healingwavelesser", tip = "tips.shaman.ctotem.healstream", spell = "healing stream" }, + { key = "mana_spring", icon = "spell_nature_manaregentotem", tip = "tips.shaman.ctotem.manasprin", spell = "mana spring" }, + { key = "cleansing", icon = "spell_nature_nullifydisease", tip = "tips.shaman.ctotem.cleansing", spell = "cleansing" }, + { key = "fire_resistance", icon = "spell_fire_firearmor", tip = "tips.shaman.ctotem.fireres", spell = "fire resistance" }, + }, + }, + { + key = "air", + defaultIcon = "spell_nature_windfury", + tip = "tips.shaman.ctotem.airtot", + spells = { + { key = "wrath_of_air", icon = "spell_nature_slowingtotem", tip = "tips.shaman.ctotem.wrhatair", spell = "wrath of air" }, + { key = "windfury", icon = "spell_nature_windfury", tip = "tips.shaman.ctotem.windfury", spell = "windfury" }, + { key = "nature_resistance",icon = "spell_nature_natureresistancetotem", tip = "tips.shaman.ctotem.natres", spell = "nature resistance" }, + { key = "grounding", icon = "spell_nature_groundingtotem", tip = "tips.shaman.ctotem.grounding", spell = "grounding" }, + }, + }, +} + +local function getAceGUI() + if MultiBot.GetAceGUI then + local ace = MultiBot.GetAceGUI() + if type(ace) == "table" and type(ace.Create) == "function" then + return ace + end + end + + if type(LibStub) == "table" then + local ok, ace = pcall(LibStub.GetLibrary, LibStub, "AceGUI-3.0", true) + if ok and type(ace) == "table" and type(ace.Create) == "function" then + return ace + end + end + + return nil +end + +local function sanitizeName(name) + return tostring(name or ""):gsub("[^%w_]", "_") +end + +local function safeTexturePath(path) + if MultiBot.SafeTexturePath then + return MultiBot.SafeTexturePath(path or ICON_FALLBACK) + end + return path or ICON_FALLBACK +end + +local function setTooltip(owner, text) + if not owner or not GameTooltip or not text or text == "" then + return + end + + GameTooltip:SetOwner(owner, "ANCHOR_RIGHT") + GameTooltip:SetText(text, 1, 1, 1, true) + GameTooltip:Show() +end + +local function createIconButton(parent, name, iconPath, tooltipText, size) + local button = CreateFrame("Button", name, parent) + local actualSize = size or BUTTON_SIZE + + button:SetSize(actualSize, actualSize) + button:RegisterForClicks("LeftButtonUp", "RightButtonUp") + + local icon = button:CreateTexture(nil, "ARTWORK") + icon:SetAllPoints(button) + icon:SetTexture(safeTexturePath(iconPath)) + button.icon = icon + + local pushed = button:CreateTexture(nil, "OVERLAY") + pushed:SetAllPoints(icon) + pushed:SetTexture("Interface\\Buttons\\UI-Quickslot-Depress") + pushed:SetBlendMode("MOD") + button:SetPushedTexture(pushed) + + local highlight = button:CreateTexture(nil, "HIGHLIGHT") + highlight:SetAllPoints(icon) + highlight:SetTexture("Interface\\Buttons\\ButtonHilight-Square") + highlight:SetBlendMode("ADD") + button.highlight = highlight + + local selectedGlow = button:CreateTexture(nil, "OVERLAY") + selectedGlow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border") + selectedGlow:SetBlendMode("ADD") + selectedGlow:SetPoint("CENTER", button, "CENTER", 0, 0) + selectedGlow:SetSize(actualSize * 1.75, actualSize * 1.75) + selectedGlow:Hide() + button.selectedGlow = selectedGlow + + button.tooltipText = tooltipText + button.__mbDisabled = false + button.__mbSelected = false + + function button:SetIcon(path) + self.icon:SetTexture(safeTexturePath(path)) + self.__mbIcon = path + end + + function button:SetButtonDisabled(disabled) + self.__mbDisabled = disabled and true or false + self:EnableMouse(not self.__mbDisabled) + + if self.icon and self.icon.SetDesaturated then + self.icon:SetDesaturated(self.__mbDisabled) + end + + if self.icon and self.icon.SetVertexColor then + if self.__mbDisabled then + self.icon:SetVertexColor(0.45, 0.45, 0.45, 0.9) + else + self.icon:SetVertexColor(1, 1, 1, 1) + end + end + + if self.SetAlpha then + self:SetAlpha(self.__mbDisabled and 0.5 or 1) + end + end + + function button:SetButtonSelected(selected) + self.__mbSelected = selected and true or false + + if self.selectedGlow then + if self.__mbSelected then + self.selectedGlow:Show() + else + self.selectedGlow:Hide() + end + end + + if self.SetAlpha and not self.__mbDisabled then + self:SetAlpha(self.__mbSelected and 0.9 or 1) + end + end + + button:SetScript("OnEnter", function(self) + setTooltip(self, self.tooltipText) + end) + button:SetScript("OnLeave", function() + if GameTooltip and GameTooltip.Hide then + GameTooltip:Hide() + end + end) + + return button +end + +local ShamanQuick = MultiBot.ShamanQuick or {} +MultiBot.ShamanQuick = ShamanQuick +ShamanQuick.entries = ShamanQuick.entries or {} + +local function normalizeRowSpacing(value) + local spacing = tonumber(value) or SHAMAN_QUICK_ROW_SPACING_DEFAULT + if spacing < BUTTON_SIZE then + spacing = BUTTON_SIZE + end + return spacing +end + +function ShamanQuick:GetRowSpacing() + self.rowSpacing = normalizeRowSpacing(self.rowSpacing) + return self.rowSpacing +end + +function ShamanQuick:SetRowSpacing(value) + local spacing = normalizeRowSpacing(value) + if self.rowSpacing == spacing then + return spacing + end + + self.rowSpacing = spacing + if self.RefreshFromGroup then + self:RefreshFromGroup() + end + + return spacing +end + +MultiBot.GetShamanQuickSpacing = function() + return ShamanQuick:GetRowSpacing() +end + +MultiBot.SetShamanQuickSpacing = function(value) + return ShamanQuick:SetRowSpacing(value) +end + +local function stripWindowChrome(window) + if not window or not window.frame then + return + end + + if window.closebutton and window.closebutton.Hide then + window.closebutton:Hide() + end + if window.statusbg and window.statusbg.Hide then + window.statusbg:Hide() + end + if window.statustext and window.statustext.Hide then + window.statustext:Hide() + end + if window.title and window.title.Hide then + window.title:Hide() + end + if window.titletext and window.titletext.Hide then + window.titletext:Hide() + end + + window:EnableResize(false) + + local frame = window.frame + if frame and frame.EnableMouse then + frame:EnableMouse(false) + end + if frame.GetRegions then + local regions = { frame:GetRegions() } + for _, region in ipairs(regions) do + if region and region.Hide then + region:Hide() + end + end + end +end + +local function updateWindowTitle(service, count) + if not service.window or not service.window.SetTitle then + return + end + + if count and count > 0 then + service.window:SetTitle(string.format("%s (%d)", WINDOW_TITLE, count)) + else + service.window:SetTitle(WINDOW_TITLE) + end +end + +local function persistWindowPosition(frame) + if not frame or not MultiBot.SetQuickFramePosition then + return + end + + local point, _, relPoint, x, y = frame:GetPoint() + MultiBot.SetQuickFramePosition(SHAMAN_QUICK_FRAME_KEY, point, relPoint, x, y) +end + +local function bindWindowDrag(service) + if not service.window or not service.window.frame or service.__dragBound then + return + end + + service.__dragBound = true + + local frame = service.window.frame + frame:SetMovable(true) + frame:SetClampedToScreen(true) + + local title = service.window.title + if title then + title:HookScript("OnMouseDown", function(_, mouseButton) + if mouseButton ~= "RightButton" then + return + end + frame:StartMoving() + frame.__mbRightDragging = true + end) + + title:HookScript("OnMouseUp", function() + if not frame.__mbRightDragging then + return + end + frame.__mbRightDragging = nil + frame:StopMovingOrSizing() + persistWindowPosition(frame) + end) + end + + frame:HookScript("OnHide", function(current) + if current.__mbRightDragging then + current.__mbRightDragging = nil + current:StopMovingOrSizing() + end + end) + + frame:HookScript("OnMouseUp", function(current) + if not current.__mbRightDragging then + return + end + current.__mbRightDragging = nil + current:StopMovingOrSizing() + persistWindowPosition(current) + end) + + frame:HookScript("OnDragStop", function(current) + persistWindowPosition(current) + end) +end + +function ShamanQuick:RestorePosition() + self:EnsureWindow() + + local frame = self.window and self.window.frame + if not frame then + return + end + + local state = MultiBot.GetQuickFramePosition and MultiBot.GetQuickFramePosition(SHAMAN_QUICK_FRAME_KEY) or nil + state = state or WINDOW_DEFAULT_POINT + + frame:ClearAllPoints() + frame:SetPoint(state.point or "CENTER", UIParent, state.relPoint or "CENTER", state.x or 0, state.y or 0) +end + +function ShamanQuick:CollectShamanBots() + local names = {} + + local function considerUnit(unit) + if not UnitExists(unit) then + return + end + + local name = GetUnitName(unit, true) + local _, classToken = UnitClass(unit) + if classToken == "SHAMAN" and name and (not MultiBot.IsBot or MultiBot.IsBot(name)) then + table.insert(names, name) + end + end + + if IsInRaid and IsInRaid() then + local count = GetNumGroupMembers and GetNumGroupMembers() or 0 + for index = 1, count do + considerUnit("raid" .. index) + end + else + considerUnit("player") + local count = GetNumSubgroupMembers and GetNumSubgroupMembers() or 0 + for index = 1, count do + considerUnit("party" .. index) + end + end + + table.sort(names) + return names +end + +function ShamanQuick:CloseAllExcept(keepRow) + for _, row in pairs(self.entries or {}) do + if row ~= keepRow then + if row.menuFrame then row.menuFrame:Hide() end + for _, groupFrame in pairs(row.groupFrames or {}) do + groupFrame:Hide() + end + row.expanded = false + end + end +end + +function ShamanQuick:ToggleRow(row) + if not row then + return + end + + local shouldExpand = not row.expanded + self:CloseAllExcept(row) + + row.expanded = shouldExpand + if row.expanded then + row.menuFrame:Show() + else + row.menuFrame:Hide() + for _, groupFrame in pairs(row.groupFrames or {}) do + groupFrame:Hide() + end + end +end + +function ShamanQuick:ToggleElementGroup(row, elementKey) + if not row then + return + end + + local targetGroup = row.groupFrames and row.groupFrames[elementKey] + if not targetGroup then + return + end + + self:CloseAllExcept(row) + row.expanded = true + row.menuFrame:Show() + + local shouldShow = not targetGroup:IsShown() + for key, groupFrame in pairs(row.groupFrames or {}) do + if key ~= elementKey then + groupFrame:Hide() + end + end + + if shouldShow then + targetGroup:Show() + else + targetGroup:Hide() + end +end + +function ShamanQuick:SetElementIcon(row, elementKey, iconPath) + local elementButton = row.elementButtons and row.elementButtons[elementKey] + if elementButton and elementButton.SetIcon then + elementButton:SetIcon(iconPath) + end +end + +function ShamanQuick:SetSelectedTotemButton(row, elementKey, selectedButton) + row.selectedButtons = row.selectedButtons or {} + local previous = row.selectedButtons[elementKey] + if previous and previous ~= selectedButton and previous.SetButtonSelected then + previous:SetButtonSelected(false) + end + + row.selectedButtons[elementKey] = selectedButton + if selectedButton and selectedButton.SetButtonSelected then + selectedButton:SetButtonSelected(true) + end +end + +function ShamanQuick:ClearTotemSelection(row, elementKey, options) + options = options or {} + row.selectedIcons = row.selectedIcons or {} + row.selectedButtons = row.selectedButtons or {} + + local selectedButton = row.selectedButtons[elementKey] + if selectedButton and selectedButton.SetButtonSelected then + selectedButton:SetButtonSelected(false) + end + + row.selectedButtons[elementKey] = nil + row.selectedIcons[elementKey] = nil + self:SetElementIcon(row, elementKey, row.defaultIcons[elementKey]) + + if not options.skipPersist and MultiBot.ClearShamanTotemChoice then + MultiBot.ClearShamanTotemChoice(row.owner, elementKey) + end +end + +function ShamanQuick:ApplyPersistedChoice(row, elementKey, iconPath) + if not iconPath then + return + end + + row.selectedIcons = row.selectedIcons or {} + row.selectedIcons[elementKey] = iconPath + self:SetElementIcon(row, elementKey, iconPath) + + local buttons = row.gridButtons and row.gridButtons[elementKey] or nil + if not buttons then + return + end + + for _, button in ipairs(buttons) do + if button.__mbIcon == iconPath then + self:SetSelectedTotemButton(row, elementKey, button) + break + end + end +end + +function ShamanQuick:SelectTotem(row, elementKey, button, definition) + if not row or not elementKey or not button or not definition then + return + end + + row.selectedIcons = row.selectedIcons or {} + row.selectedButtons = row.selectedButtons or {} + + local currentButton = row.selectedButtons[elementKey] + local currentIcon = row.selectedIcons[elementKey] + local isSameSelection = currentButton == button or currentIcon == button.__mbIcon + + if isSameSelection then + MultiBot.ActionToTarget("co -" .. definition.spell .. ",?", row.owner) + self:ClearTotemSelection(row, elementKey) + return + end + + if currentButton and currentButton ~= button and currentButton.__mbSpell then + MultiBot.ActionToTarget("co -" .. currentButton.__mbSpell .. ",?", row.owner) + if currentButton.SetButtonSelected then + currentButton:SetButtonSelected(false) + end + end + + MultiBot.ActionToTarget("co +" .. definition.spell .. ",?", row.owner) + + row.selectedIcons[elementKey] = button.__mbIcon + self:SetSelectedTotemButton(row, elementKey, button) + self:SetElementIcon(row, elementKey, button.__mbIcon) + + if MultiBot.SetShamanTotemChoice then + MultiBot.SetShamanTotemChoice(row.owner, elementKey, button.__mbIcon) + end +end + +function ShamanQuick:CreateTotemButton(row, elementDefinition, groupFrame, spellDefinition, index) + local button = createIconButton(groupFrame, string.format("MultiBotShamanTotem_%s_%s_%d", sanitizeName(row.owner), elementDefinition.key, index), spellDefinition.icon, MultiBot.L(spellDefinition.tip), BUTTON_SIZE) + button:SetPoint("TOPLEFT", (index - 1) * (BUTTON_SIZE + BUTTON_GAP), 0) + button.__mbIcon = spellDefinition.icon + button.__mbSpell = spellDefinition.spell + button:SetScript("OnClick", function() + self:SelectTotem(row, elementDefinition.key, button, spellDefinition) + end) + return button +end + +function ShamanQuick:CreateElementButton(row, elementDefinition, index) + local button = createIconButton(row.menuFrame, string.format("MultiBotShamanElement_%s_%s", sanitizeName(row.owner), elementDefinition.key), elementDefinition.defaultIcon, MultiBot.L(elementDefinition.tip), BUTTON_SIZE) + button:SetPoint("TOPLEFT", 0, -((index - 1) * (BUTTON_SIZE + BUTTON_GAP))) + button:SetScript("OnClick", function() + self:ToggleElementGroup(row, elementDefinition.key) + end) + row.elementButtons[elementDefinition.key] = button + return button +end + +function ShamanQuick:BuildRow(ownerName) + self:EnsureWindow() + + local row = CreateFrame("Frame", string.format("MultiBotShamanQuickRow_%s", sanitizeName(ownerName)), self.canvas) + row:SetSize(ROW_WIDTH, ROW_HEIGHT) + row.owner = ownerName + row.expanded = false + row.elementButtons = {} + row.groupFrames = {} + row.gridButtons = {} + row.selectedButtons = {} + row.selectedIcons = {} + row.defaultIcons = {} + + local mainTooltip = (MultiBot.L("tips.shaman.ownbutton") or "Shaman: %s"):format(ownerName) + local mainButton = createIconButton(row, string.format("MultiBotShamanQuickMain_%s", sanitizeName(ownerName)), "Interface\\AddOns\\MultiBot\\Icons\\class_shaman.blp", mainTooltip, BUTTON_SIZE) + mainButton:SetPoint("TOPLEFT", 0, 0) + mainButton:RegisterForDrag("RightButton") + mainButton:SetScript("OnDragStart", function() + if self.window and self.window.frame then + self.window.frame:StartMoving() + self.window.frame.__mbRightDragging = true + end + end) + mainButton:SetScript("OnDragStop", function() + local frame = self.window and self.window.frame + if not frame then + return + end + frame.__mbRightDragging = nil + frame:StopMovingOrSizing() + persistWindowPosition(frame) + end) + mainButton:SetScript("OnClick", function(_, mouseButton) + if mouseButton == "LeftButton" then + self:ToggleRow(row) + end + end) + row.mainButton = mainButton + + local menuFrame = CreateFrame("Frame", nil, row) + menuFrame:SetPoint("TOPLEFT", mainButton, "BOTTOMLEFT", 0, -BUTTON_GAP) + menuFrame:SetSize(BUTTON_SIZE, (BUTTON_SIZE * #ELEMENT_DEFINITIONS) + (BUTTON_GAP * (#ELEMENT_DEFINITIONS - 1))) + menuFrame:Hide() + row.menuFrame = menuFrame + + for index, elementDefinition in ipairs(ELEMENT_DEFINITIONS) do + row.defaultIcons[elementDefinition.key] = elementDefinition.defaultIcon + self:CreateElementButton(row, elementDefinition, index) + + local groupFrame = CreateFrame("Frame", nil, row) + groupFrame:SetPoint("TOPLEFT", row.elementButtons[elementDefinition.key], "TOPRIGHT", BUTTON_GAP, 0) + groupFrame:SetSize((BUTTON_SIZE * #elementDefinition.spells) + (BUTTON_GAP * (#elementDefinition.spells - 1)), BUTTON_SIZE) + groupFrame:Hide() + row.groupFrames[elementDefinition.key] = groupFrame + row.gridButtons[elementDefinition.key] = {} + + for spellIndex, spellDefinition in ipairs(elementDefinition.spells) do + local button = self:CreateTotemButton(row, elementDefinition, groupFrame, spellDefinition, spellIndex) + table.insert(row.gridButtons[elementDefinition.key], button) + end + end + + local persistedChoices = MultiBot.GetShamanTotemsForBot and MultiBot.GetShamanTotemsForBot(ownerName) or nil + if persistedChoices then + for elementKey, iconPath in pairs(persistedChoices) do + self:ApplyPersistedChoice(row, elementKey, iconPath) + end + end + + self.entries[ownerName] = row + return row +end + +function ShamanQuick:UpdateWindowGeometry(count) + self:EnsureWindow() + + count = math.max(tonumber(count) or 0, 1) + local width = (WINDOW_PADDING_X * 2) + ROW_WIDTH + ((count - 1) * self:GetRowSpacing()) + + self.window:SetWidth(width) + self.window:SetHeight(WINDOW_HEIGHT) + self.canvas:SetWidth(width - (WINDOW_PADDING_X * 2)) + self.canvas:SetHeight(WINDOW_HEIGHT - (WINDOW_PADDING_Y * 2)) + updateWindowTitle(self, count) +end + +function ShamanQuick:EnsureWindow() + if self.window and self.window.frame then + return self.window + end + + local aceGUI = getAceGUI() + if not aceGUI then + UIErrorsFrame:AddMessage("AceGUI-3.0 is required for Shaman Quick", 1, 0.2, 0.2, 1) + return nil + end + + local window = aceGUI:Create("Window") + window:SetTitle(WINDOW_TITLE) + window:SetLayout("Manual") + window:SetWidth((WINDOW_PADDING_X * 2) + ROW_WIDTH) + window:SetHeight(WINDOW_HEIGHT) + window.frame:SetFrameStrata("HIGH") + window.frame:SetClampedToScreen(true) + window.frame:SetPoint(WINDOW_DEFAULT_POINT.point, UIParent, WINDOW_DEFAULT_POINT.relPoint, WINDOW_DEFAULT_POINT.x, WINDOW_DEFAULT_POINT.y) + window:SetCallback("OnClose", function(widget) + widget:Hide() + end) + window:Hide() + + stripWindowChrome(window) + + if window.content then + window.content:ClearAllPoints() + window.content:SetPoint("TOPLEFT", window.frame, "TOPLEFT", 0, 0) + window.content:SetPoint("BOTTOMRIGHT", window.frame, "BOTTOMRIGHT", 0, 0) + end + + local canvas = CreateFrame("Frame", nil, window.content) + canvas:SetPoint("TOPLEFT", window.content, "TOPLEFT", WINDOW_PADDING_X, -WINDOW_PADDING_Y) + canvas:SetPoint("BOTTOMRIGHT", window.content, "BOTTOMRIGHT", -WINDOW_PADDING_X, WINDOW_PADDING_Y) + + self.window = window + self.frame = window.frame + self.canvas = canvas + self.__aceInitialized = true + + bindWindowDrag(self) + self:RestorePosition() + + return window +end + +function ShamanQuick:RefreshFromGroup() + self:EnsureWindow() + + local desiredNames = self:CollectShamanBots() + local desiredLookup = {} + for _, name in ipairs(desiredNames) do + desiredLookup[name] = true + end + + for name, row in pairs(self.entries) do + if not desiredLookup[name] then + row:Hide() + row:SetParent(nil) + self.entries[name] = nil + end + end + + for _, name in ipairs(desiredNames) do + if not self.entries[name] then + self:BuildRow(name) + end + end + + for index, name in ipairs(desiredNames) do + local row = self.entries[name] + if row then + row:ClearAllPoints() + row:SetPoint("TOPLEFT", self.canvas, "TOPLEFT", (index - 1) * self:GetRowSpacing(), 0) + row:SetFrameLevel((self.window.frame:GetFrameLevel() or 0) + 2) + row:Show() + end + end + + if #desiredNames > 0 then + self:UpdateWindowGeometry(#desiredNames) + self.window:Show() + self:RestorePosition() + elseif self.window then + updateWindowTitle(self, 0) + self.window:Hide() + end +end + +function MultiBot.InitShamanQuick() + if ShamanQuick.__moduleReady then + return ShamanQuick + end + + ShamanQuick.__moduleReady = true + ShamanQuick:EnsureWindow() + + if MultiBot.TimerAfter then + MultiBot.TimerAfter(0.5, function() + if MultiBot and MultiBot.ShamanQuick and MultiBot.ShamanQuick.RefreshFromGroup then + MultiBot.ShamanQuick:RefreshFromGroup() + end + end) + end + + return ShamanQuick +end + +MultiBot.InitShamanQuick() + +if MultiBot.ShamanQuick and MultiBot.ShamanQuick.RestorePosition then + MultiBot.ShamanQuick:RestorePosition() +end \ No newline at end of file diff --git a/docs/ace3-expansion-checklist.md b/docs/ace3-expansion-checklist.md index 18fb9ea..761a920 100644 --- a/docs/ace3-expansion-checklist.md +++ b/docs/ace3-expansion-checklist.md @@ -28,6 +28,7 @@ Checklist for the full addon-wide ACE3 expansion after M7 completion. - [x] Talents/Glyphs frame migration slice completed (`UI/MultiBotTalentFrame.lua`): AceGUI host integration for the talents/glyphs workflow with preserved tab/copy/apply behavior and custom glyph interactions. - [x] ITEMUS migration slice completed (`UI/MultiBotItemusFrame.lua` + `Data/MultiBotItemus.lua` + `Core/MultiBotInit.lua`): native AceGUI host window, controller-owned paging/state helpers, lazy Masters launcher/reset initialization, dense icon grid parity, and verified legacy-combination coverage with localized in-window guidance reusing `tips.game.itemus`. - [x] ICONOS migration slice completed (`UI/MultiBotIconosFrame.lua` + `Core/MultiBotInit.lua` + `Data/MultiBotIconos.lua` + `Core/MultiBotHandler.lua`): native AceGUI host window, full legacy shell removal, dense 112-icon paging parity, right-drag position persistence via `IconosPoint`, ESC close support, copy-friendly in-window icon path display, and a first UX uplift pass with search (`ALL/PATH`) + original/A-Z ordering + selected-icon preview + jump-to-letter. +- [x] Quick Hunter / Quick Shaman migration slice completed (`UI/MultiBotHunterQuickFrame.lua` + `UI/MultiBotShamanQuickFrame.lua` + `Core/MultiBotInit.lua` + `Core/MultiBot.lua`): both quick bars now live in dedicated UI modules with native AceGUI-hosted windows, preserved position/state persistence, and full removal of the legacy quick-frame wrappers from `Core/MultiBotInit.lua`.. - [x] Hidden tooltip utility cleanup completed (`Core/MultiBotInit.lua`): `MB_LocalizeQuestTooltip` and `MBHiddenTip` now reuse a shared hidden-tooltip helper and remain native by design. - [x] AceGUI popup close-behavior parity tightened (`Core/MultiBotInit.lua`): migrated popup windows now hide on close (no release), preserving reopen behavior across the same session. - [x] AceGUI resolver deduplication completed (`Core/MultiBotInit.lua`): migrated popup paths now share a single resolver helper for dependency lookup + error reporting. diff --git a/docs/ace3-quick-frames-migration-tracker.md b/docs/ace3-quick-frames-migration-tracker.md new file mode 100644 index 0000000..7a885d5 --- /dev/null +++ b/docs/ace3-quick-frames-migration-tracker.md @@ -0,0 +1,189 @@ +# Ace3 Quick Frames Migration Tracker (Milestone 8) + +Document de suivi dédié à la migration complète des mini frames **Quick Hunter** et **Quick Shaman** vers de vraies interfaces Ace3/AceGUI. + +> Scope: documenter l’audit initial, suivre l’extraction des implémentations legacy anciennement imbriquées dans `Core/MultiBotInit.lua`, confirmer leur reconstruction dans des fichiers dédiés sous `UI/`, et tracer la suppression des shells legacy (`MultiBot.addFrame`, contours, groupes maison) tout en conservant la parité fonctionnelle. + +--- + +## 1) Source-of-truth actuel / historique de migration + +### Fichiers concernés par l’audit et la migration +- `Core/MultiBotInit.lua` +- `Core/MultiBot.lua` +- `UI/MultiBotHunterQuickFrame.lua` +- `UI/MultiBotShamanQuickFrame.lua` +- `Strategies/MultiBotHunter.lua` +- `Strategies/MultiBotShaman.lua` +- `docs/ace3-ui-frame-inventory.md` +- `docs/ace3-expansion-checklist.md` + +### Points d’entrée legacy audités +- `MultiBot.InitHunterQuick()` construisait toute la mini frame Hunter directement dans `Core/MultiBotInit.lua` avant son extraction vers `UI/MultiBotHunterQuickFrame.lua`. +- `MultiBot.InitShamanQuick()` construisait toute la mini frame Shaman directement dans `Core/MultiBotInit.lua` avant son extraction vers `UI/MultiBotShamanQuickFrame.lua`. +- Les deux quick bars restaurent leur position via `MultiBot.GetQuickFramePosition(...)` / `MultiBot.SetQuickFramePosition(...)`. +- La quick frame Hunter persiste en plus la stance du pet par bot via `MultiBot.GetHunterPetStance(...)` / `MultiBot.SetHunterPetStance(...)`. +- La quick frame Shaman persiste les totems choisis par bot/élément via `MultiBot.GetShamanTotemsForBot(...)`, `MultiBot.SetShamanTotemChoice(...)`, et `MultiBot.ClearShamanTotemChoice(...)`. + +--- + +## 2) Audit fonctionnel — Quick Hunter + +### Ce que la frame fait aujourd’hui +- [x] S’affiche uniquement lorsqu’au moins un **bot hunter** est détecté dans le groupe/raid. +- [x] Construit une colonne par hunter bot trié alphabétiquement. +- [x] Permet d’ouvrir un menu vertical par bot depuis un bouton principal `class_hunter`. +- [x] Expose un bloc **pet stances** avec 7 actions : `aggressive`, `passive`, `defensive`, `stance`, `attack`, `follow`, `stay`. +- [x] Sauvegarde/restaure visuellement la stance active (`aggressive/passive/defensive`) par hunter. +- [x] Désactive le bloc des stances quand le pet du hunter n’existe pas ou est mort. +- [x] Expose un bloc **pet utils** avec les actions : `Name`, `Id`, `Family`, `Rename`, `Abandon`. +- [x] Réutilise déjà des popups AceGUI pour le prompt, la recherche de créature, la preview modèle et le sélecteur de famille. +- [x] Sauvegarde/restaure la position globale de la quick frame. + +### Observations d’architecture +- [x] La logique de présence des bots, la construction de la vue et les handlers de chat sont mélangés dans une seule grosse fonction. +- [x] La frame racine et toutes les sous-zones reposent encore sur le wrapper legacy `MultiBot.addFrame(...)` / `row.addFrame(...)` / `addButton(...)`. +- [x] Les dimensions/offsets sont codés en dur sur une grille de `36px`. +- [x] Le code gère déjà un mini-contrôleur implicite (`entries`, `Rebuild`, `CollectHunterBots`, `UpdatePetPresence`) mais sans séparation formelle modèle/vue/contrôleur. + +### Risques spécifiques Hunter +- [ ] Régression sur l’apparition conditionnelle selon la composition du groupe. +- [ ] Régression sur la persistance des stances par nom de bot. +- [ ] Régression sur le drag & drop / restauration de position. +- [ ] Régression sur les popups déjà migrés (search/family/prompt) si on modifie mal les points d’appel. +- [ ] Régression sur la désactivation du menu des stances quand aucun pet n’est disponible. + +--- + +## 3) Audit fonctionnel — Quick Shaman + +### Ce que la frame fait aujourd’hui +- [x] S’affiche uniquement lorsqu’au moins un **bot shaman** est détecté dans le groupe/raid. +- [x] Construit une colonne par shaman bot. +- [x] Affiche un bouton principal `class_shaman` ouvrant un menu vertical par bot. +- [x] Expose 4 familles de totems : `earth`, `fire`, `water`, `air`. +- [x] Chaque famille ouvre une grille dédiée d’actions/totems sélectionnables. +- [x] Envoie les commandes chat `co +spell,?` et `co -spell,?` au bot ciblé. +- [x] Maintient une exclusivité visuelle par élément : un seul totem actif/retenti visuellement par famille. +- [x] Remplace l’icône du bouton d’élément par l’icône du totem choisi. +- [x] Sauvegarde/restaure le choix du totem par bot et par élément. +- [x] Sauvegarde/restaure la position globale de la quick frame. + +### Observations d’architecture +- [x] La frame est fortement couplée au wrapper legacy et à des helpers visuels ad hoc (`SetBtnIcon`, `SetGrey`, `AddTotemToggle`). +- [x] La logique d’état (`_chosen`, `_selectedBtn`, `_gridBtns`, `_defaults`) est portée directement par les rows/boutons. +- [x] La clé d’index des entrées n’est pas homogène avec Hunter (`sanitized name` côté Shaman, nom brut côté Hunter), ce qui mérite une normalisation lors de la migration. +- [x] `CloseAllExcept()` masque aussi les autres rows, ce qui crée une UX spécifique à préserver ou à faire évoluer explicitement. +- [x] Un appel legacy parasite subsiste dans `OnDragStop` (`_MB_GetOrCreateShamanPos()`), alors que la persistance AceDB est désormais prise en charge par `MultiBot.SetQuickFramePosition(...)`. + +### Risques spécifiques Shaman +- [ ] Régression sur l’exclusivité visuelle d’un totem par élément. +- [ ] Régression sur la restauration des icônes choisies après reload/relog. +- [ ] Régression sur les commandes `co +/-,?` si le mapping données/UI diverge. +- [ ] Régression sur le comportement d’expansion/fermeture des groupes Earth/Fire/Water/Air. +- [ ] Régression sur l’apparition conditionnelle selon la présence de shamans bots. + +--- + +## 4) Dette technique observée avant migration + +### Problèmes communs aux deux quick frames +- [x] Les deux implémentations vivent encore dans `Core/MultiBotInit.lua`, ce qui gonfle fortement le fichier et mélange bootstrap, runtime, popups, et écrans. +- [x] Les deux écrans utilisent encore la stack UI legacy (`MultiBot.addFrame`) au lieu d’un vrai host AceGUI. +- [x] Les responsabilités suivantes ne sont pas clairement séparées : découverte des bots, état UI, rendu, persistance, et dispatch des actions. +- [x] Les handlers d’input répètent beaucoup de logique de drag/persist/close-all. +- [x] Les structures de données sont majoritairement implicites et mutées dynamiquement sur les widgets. + +### Opportunités de modernisation Lua +- [x] Introduire des modules dédiés sous `UI/` (`UI/MultiBotHunterQuickFrame.lua`, `UI/MultiBotShamanQuickFrame.lua`). +- [x] Isoler des tables de configuration pures pour les actions/buttons plutôt que coder les boutons “à la main”. +- [x] Uniformiser le lifecycle avec une API du type `Ensure`, `RefreshFromGroup`, `ApplyLayout`, `PersistPosition`, `HideMenus`. +- [x] Réduire les globals implicites et privilégier `local` + helpers spécialisés. +- [x] Centraliser les primitives AceGUI/WoW natives nécessaires au drag, au close, et à la persistance. + +--- + +## 5) Objectif de migration + +### Contraintes fonctionnelles à respecter +- [ ] Ne pas encapsuler les anciennes frames legacy dans une fenêtre AceGUI. +- [ ] Refaire les quick frames comme de vraies fenêtres/conteneurs Ace3/AceGUI. +- [ ] Conserver les mêmes commandes envoyées aux bots. +- [ ] Conserver l’apparition conditionnelle selon la présence de hunters/shamans bots dans le groupe. +- [ ] Conserver la persistance de position existante (`HunterQuick`, `ShamanQuick`). +- [ ] Conserver la persistance des stances pet Hunter et des totems Shaman. +- [ ] Conserver les popups Hunter déjà migrés sans les régresser. + +### Cible de structure recommandée +- [x] `UI/MultiBotHunterQuickFrame.lua` + - host AceGUI / container racine + - découverte/refresh des hunters bots + - rendu des rows/actions Hunter + - intégration avec prompt/search/family existants +- [x] `UI/MultiBotShamanQuickFrame.lua` + - host AceGUI / container racine + - découverte/refresh des shamans bots + - rendu des rows/actions Totems + - restauration visuelle des totems choisis +- [x] `Core/MultiBotInit.lua` + - ne garder que les hooks d’initialisation minimum et les appels publics nécessaires + - supprimer les gros blocs inline une fois les modules UI branchés + +--- + +## 6) Plan de migration recommandé + +### Phase 1 — Préparation +- [x] Auditer les deux quick frames legacy. +- [x] Créer ce document de suivi. +- [x] Ajouter ces quick frames à l’inventaire M8 comme écrans non encore migrés. + +### Phase 2 — Extraction structurelle +- [x] Déplacer la logique Hunter vers `UI/MultiBotHunterQuickFrame.lua` sans changer encore le comportement. +- [x] Déplacer la logique Shaman vers `UI/MultiBotShamanQuickFrame.lua` sans changer encore le comportement. +- [x] Réduire `Core/MultiBotInit.lua` à des points d’entrée fins. + +### Phase 3 — Remplacement de l’UI legacy +- [x] Remplacer la racine `MultiBot.addFrame(...)` Hunter par un host AceGUI dédié. +- [x] Remplacer la racine `MultiBot.addFrame(...)` Shaman par un host AceGUI dédié. +- [x] Remplacer les groupes/boutons legacy par un layout AceGUI + frames natives WoW uniquement quand nécessaire. +- [x] Supprimer les dépendances aux bordures/contours legacy de ces deux quick frames. + +### Phase 4 — Parité fonctionnelle +- [ ] Valider la présence conditionnelle en groupe/raid. +- [ ] Valider la persistance de position des deux quick frames. +- [ ] Valider la persistance des stances Hunter. +- [ ] Valider la persistance des totems Shaman. +- [ ] Valider les flows Hunter prompt/search/family. +- [ ] Valider l’exclusivité visuelle et l’icône d’élément côté Shaman. + +--- + +## 7) Checklist de validation ciblée + +### Hunter Quick +- [ ] Un hunter bot seul fait apparaître la frame. +- [ ] Plusieurs hunters bots produisent plusieurs rows stables et triées. +- [ ] Le drag déplace bien la frame et la position revient après reload. +- [ ] Les stances `aggressive/passive/defensive` restent exclusives visuellement. +- [ ] L’absence de pet désactive correctement le sous-menu des stances. +- [ ] `Name`, `Id`, `Family`, `Rename`, `Abandon` continuent d’envoyer les bonnes commandes. +- [ ] Les popups search/family/prompt restent fonctionnels. + +### Shaman Quick +- [ ] Un shaman bot seul fait apparaître la frame. +- [ ] Plusieurs shamans bots produisent plusieurs rows stables. +- [ ] Le drag déplace bien la frame et la position revient après reload. +- [ ] Chaque élément affiche bien le totem choisi sur son bouton principal. +- [ ] La sélection d’un nouveau totem d’un même élément remplace bien l’ancien visuellement. +- [ ] Désélectionner un totem rétablit l’icône par défaut du bouton d’élément. +- [ ] Les choix persisted reviennent correctement après reload. + +--- + +## 8) Décisions validées avant implémentation + +- [x] **UX Shaman** : ne pas conserver le comportement legacy où une row ouverte masque complètement les autres. La cible retenue est une UX AceGUI où toutes les rows restent visibles, avec une seule row détaillée/dépliée à la fois par défaut. +- [x] **Architecture commune** : garder deux modules dédiés (`UI/MultiBotHunterQuickFrame.lua` et `UI/MultiBotShamanQuickFrame.lua`) et n’extraire que de petits utilitaires communs ciblés (position, drag, registre de rows, helpers de refresh), sans base abstraite prématurée. +- [x] **Granularité des boutons** : ne pas figer la migration sur la valeur legacy `36px`, mais conserver la même densité d’usage et la même vitesse de scan/clic, avec un léger ajustement possible si le layout AceGUI est plus lisible et plus confortable. +- [x] **Collecte des bots présents** : introduire un petit contrôleur partagé purement runtime/data pour la découverte des bots de groupe/raid par classe, afin d’éviter de dupliquer la logique de collecte Hunter/Shaman sans coupler ce contrôleur au rendu UI. \ No newline at end of file diff --git a/docs/ace3-ui-frame-inventory.md b/docs/ace3-ui-frame-inventory.md index 371fb5f..1d4c79b 100644 --- a/docs/ace3-ui-frame-inventory.md +++ b/docs/ace3-ui-frame-inventory.md @@ -85,6 +85,9 @@ Inventory of addon UI frame construction points found via `CreateFrame(...)` sca Files: `UI/MultiBotIconosFrame.lua`, `Core/MultiBotInit.lua`, `UI/MultiBotIconos.lua`, `Core/MultiBotHandler.lua`. References: `UI/MultiBotIconosFrame.lua:1`, `Core/MultiBotInit.lua:1675`, `Core/MultiBotInit.lua:3189`, `UI/MultiBotIconos.lua:1`, `Core/MultiBotHandler.lua:265`. +- [x] **Quick Hunter / Quick Shaman bars** (`MultiBot.HunterQuick`, `MultiBot.ShamanQuick`) migrated out of `Core/MultiBotInit.lua` into dedicated UI modules with native AceGUI-hosted quick frames, preserved position/state persistence, and legacy wrapper removal for both class-specific mini bars. + Files: `UI/MultiBotHunterQuickFrame.lua`, `UI/MultiBotShamanQuickFrame.lua`, `Core/MultiBotInit.lua`, `Core/MultiBot.lua`. + References: `UI/MultiBotHunterQuickFrame.lua:1`, `UI/MultiBotShamanQuickFrame.lua:1`, `Core/MultiBotInit.lua:1916`, `Core/MultiBotInit.lua:3437`, `Core/MultiBotInit.lua:3440`, `Core/MultiBot.lua:613`, `Core/MultiBot.lua:858` --- From 29dd8444a416e0fd5014948df484a4c40a3a62f9 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:37:31 +0100 Subject: [PATCH 2/3] Quick Shaman Hunter Bars ace3 Migration final --- Core/MultiBot.lua | 38 +++ Core/MultiBotInit.lua | 52 +++- TODO.md | 105 +------ UI/MultiBotHunterQuickFrame.lua | 320 +++++++++++++++++--- UI/MultiBotShamanQuickFrame.lua | 229 +++++++++++++- docs/ace3-expansion-checklist.md | 2 +- docs/ace3-quick-frames-migration-tracker.md | 84 ++--- docs/ace3-ui-frame-inventory.md | 6 +- 8 files changed, 651 insertions(+), 185 deletions(-) diff --git a/Core/MultiBot.lua b/Core/MultiBot.lua index 7456781..50b108d 100644 --- a/Core/MultiBot.lua +++ b/Core/MultiBot.lua @@ -672,6 +672,44 @@ function MultiBot.SetQuickFramePosition(frameKey, point, relPoint, x, y) return position end +function MultiBot.GetQuickFrameVisibleConfig(frameKey) + if type(frameKey) ~= "string" or frameKey == "" then + return true + end + + local profile = MultiBot.db and MultiBot.db.profile + if not profile then + return true + end + + profile.ui = profile.ui or {} + profile.ui.quickFrameVisibility = profile.ui.quickFrameVisibility or {} + + local value = profile.ui.quickFrameVisibility[frameKey] + if type(value) ~= "boolean" then + profile.ui.quickFrameVisibility[frameKey] = true + return true + end + + return value +end + +function MultiBot.SetQuickFrameVisibleConfig(frameKey, visible) + if type(frameKey) ~= "string" or frameKey == "" then + return true + end + + local value = not not visible + local profile = MultiBot.db and MultiBot.db.profile + if profile then + profile.ui = profile.ui or {} + profile.ui.quickFrameVisibility = profile.ui.quickFrameVisibility or {} + profile.ui.quickFrameVisibility[frameKey] = value + end + + return value +end + local function getLegacyHunterPetStanceStore(createIfMissing) local saved = getLegacyCharacterStateRoot(createIfMissing) local store = saved and saved.hunterPetStance diff --git a/Core/MultiBotInit.lua b/Core/MultiBotInit.lua index ddfc7b3..a8d8aa3 100644 --- a/Core/MultiBotInit.lua +++ b/Core/MultiBotInit.lua @@ -1537,7 +1537,7 @@ tMain.addButton("Actions", 0, 374, "inv_helmet_02", MultiBot.L("tips.main.action MultiBot.ActionToTargetOrGroup("reset") end --- [ADDED] Options button (opens/closes the sliders panel). +--[[ [ADDED] Options button (opens/closes the sliders panel). local tBtnOptions = tMain.addButton("Options", 0, 404, "inv_misc_gear_02", MultiBot.L("tips.main.options")) tBtnOptions._active = false @@ -1567,7 +1567,7 @@ tBtnOptions.doLeft = function(pButton) local tex = f:GetRegions() if tex and tex.SetDesaturated then tex:SetDesaturated(not opened) end end -end +end ]]-- -- GAMEMASTER REFORGED -- function MultiBot.BuildGmUI(tMultiBar) @@ -3003,6 +3003,52 @@ local PROMPT_WINDOW_WIDTH = 280 local PROMPT_WINDOW_HEIGHT = 108 local PROMPT_OK_BUTTON_WIDTH = 100 +local function stylePromptEditBox(widget) + if not widget or not widget.frame or not widget.editbox then + return + end + + local frame = widget.frame + local editbox = widget.editbox + + if frame.SetBackdrop then + 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, 0.92) + end + if frame.SetBackdropBorderColor then + frame:SetBackdropBorderColor(0.35, 0.35, 0.35, 0.95) + end + end + + if editbox.GetRegions then + local regions = { editbox:GetRegions() } + for _, region in ipairs(regions) do + if region and region.GetObjectType and region:GetObjectType() == "Texture" and region.SetAlpha then + region:SetAlpha(0) + end + end + end + + editbox:ClearAllPoints() + editbox:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -4) + editbox:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -8, 4) + editbox:SetFontObject(ChatFontNormal) + editbox:SetTextInsets(4, 4, 3, 3) + + widget:SetHeight(32) + if frame.SetHeight then + frame:SetHeight(32) + end +end + function ShowPrompt(title, onOk, defaultText) local aceGUI = resolveAceGUI("AceGUI-3.0 is required for MBUniversalPrompt") if not aceGUI then @@ -3028,6 +3074,8 @@ function ShowPrompt(title, onOk, defaultText) local edit = aceGUI:Create("EditBox") edit:SetLabel("") edit:SetFullWidth(true) + edit:DisableButton(true) + stylePromptEditBox(edit) window:AddChild(edit) local okButton = aceGUI:Create("Button") diff --git a/TODO.md b/TODO.md index 3ec492d..f13c307 100644 --- a/TODO.md +++ b/TODO.md @@ -1,48 +1,38 @@ TODO +* Ressortir les UI quetes dans leurs propre fichiers et uniformiser le tempklate de la frame comme celle de Itemus +* Uniformiser le templste de la frame reward comme celle de itemus * Essayer de faire disparaitre la barre multibots au bout d'un temps et la faire apparaitre quand on passe la souris dessus. * Faire en sorte que le réglage strata prenne en compte toutes les frames de multibots -* Mettre un bouton style décursive pour cacher la shaman quick et hunter quick * Quand on deplace ou fait quelque chose dans l'ui il faudrait que ça se sauvegarde tout de suite dans les variables dans deco reco * Raidus doit se rafraichir à l'ouverture et fermeture * dans la liste des quêtes des fois c'est l'ID de la queête qui apparait et pas le tritre * Afficher le pognon et les places de sacs dans la frame inventaire * La fenêtre inventaire doit se rafraichir par exemple quand on fait le bot bouffer il faut que ce qu'il a bouffé se décompte +* Iconos ne mémorise pas sa position +* Revoir le positionnement des fleches et pages de Iconos +* Mettre une option pour choisir la tailles des icones de la main barre et des quickhunter/shaman +* Voir si il y'a pas d'autres option que l'on peut ajouter à la frame options de multibot +* Faire en sorte que le strata soit appliqué a vraiment toutes les frames de multibot +* creer le multilangue pour le tooltip: setTooltip(self, "Show / Hide / Move Quick Shaman") des fichiers quickshaman et quickhunter -* Voir si on garde itemus (fixed grid vs pooled scroll container,) -en Fixed grid = “j’ai toujours la même grille visible, je change seulement son contenu”. -ou on bouge vers -Pooled scroll container = “j’ai un conteneur scrollable et je réutilise dynamiquement des boutons pour afficher les items”. - - -Ajouter la fonction unequipe à Multibit: -Oui, beaucoup plus facilement que le déplacement d’items dans les sacs -Oui, clairement c’est faisable en interface graphique, et même beaucoup plus proprement que l’idée de déplacer des items entre slots de sacs. -La raison principale est que “unequip” est une action orientée équipement, donc elle colle naturellement à une vue d’inspection / slots d’équipement, alors que la réorganisation des sacs demande une logique de bag/slot bien plus lourde. + Ajouter la fonction unequipe à Multibit: +“unequip” est une action orientée équipement, donc elle colle naturellement à une vue d’inspection / slots d’équipement, alors que la réorganisation des sacs demande une logique de bag/slot bien plus lourde. Pourquoi c’est un bon candidat pour la fenêtre d’inspection + 1) L’inspection est déjà centrée sur les slots d’équipement Le code existant manipule déjà très bien la notion de slot d’équipement : - dans le calcul d’ilvl, on parcourt explicitement les slots 1..18 ; - dans itemus, il existe déjà une cartographie claire des slots d’équipement (S00, S01, etc.). - Donc si la commande ue fonctionne par slot ou peut être reliée à un slot, l’intégration UI est très naturelle. 2) On a déjà un point d’entrée “Inspect” L’addon sait déjà lancer l’inspection d’un bot via InspectUnit(...) : - -depuis le bouton Inspect dans la Reward frame ; - -et ailleurs dans l’addon. - +depuis le bouton Inspect dans la Reward frame, et ailleurs dans l’addon. Donc graphiquement, il y a déjà un flux utilisateur existant : - ouvrir l’inspection du bot, - voir son équipement, - déclencher une action sur un slot équipé. Là où il faut être prudent @@ -51,159 +41,96 @@ C’est ça qui détermine la qualité de l’intégration. Cas A — si ue fonctionne par slot Exemple conceptuel : - ue head - ue 1 - ue S01 Dans ce cas, c’est idéal. Parce que la fenêtre d’inspection affiche précisément des slots. Tu peux donc faire une UI très propre : - clic droit sur un slot équipé → unequip ; - ou petit bouton contextuel sur chaque slot ; - ou mode “Unequip” activable, puis clic sur le slot. Cas B — si ue fonctionne par item Exemple conceptuel : - ue [ItemLink] - C’est encore faisable, mais un peu moins robuste : - s’il y a ambiguïté, - si deux objets identiques existent, - ou si la commande backend attend autre chose qu’un lien standard. Cas C — si ue a une syntaxe spéciale côté playerbots Alors il faudra juste aligner l’UI sur cette syntaxe. Mais dans tous les cas, le concept UI reste pertinent. -Pourquoi c’est plus simple que sur la frame INVENTORY -Dans INVENTORY, les clics d’items sont pensés pour : - -s - -e - -u - -give - -destroy - -Donc la frame INVENTORY actuelle agit surtout sur des items dans les sacs. - -À l’inverse, “unequip” est une action sur : - +“unequip” est une action sur : un slot équipé, - ou un item actuellement porté. Donc conceptuellement, la fenêtre d’inspection est le meilleur endroit : - plus logique pour l’utilisateur ; - plus lisible ; - moins ambigu ; - plus proche du modèle métier. Est-ce que l’addon actuel est déjà prêt pour ça ? Partiellement oui, mais pas complètement Ce qui existe déjà : - l’addon sait ouvrir une inspection ; - l’addon sait envoyer des commandes whisper à un bot ; - l’addon a déjà des représentations de slots d’équipement dans d’autres zones du code. Ce qui manque encore : - une action UI dédiée “unequip” dans la vue d’inspection ; - un mapping propre entre slot UI et payload ue ; - et probablement un refresh fiable après l’action. Le point technique le plus important à ne pas oublier Le refresh après ue Aujourd’hui, le handler refresh l’inventaire lorsqu’il détecte des messages du bot du genre : - equipping - using - destroyed - etc. Donc si la commande ue renvoie un message différent, par exemple : - unequipping - removed - taking off - ou autre - alors il faudra penser à raccrocher ce retour au refresh. Sinon l’action pourra marcher côté bot, mais l’UI ne se resynchronisera pas proprement. C’est probablement le principal piège de cette feature. -UX que je recommanderais, sans code -Si tu veux une bonne intégration, je recommanderais plutôt : +UX recommandé +Je verrais plutôt : Option 1 — clic droit sur slot équipé Très naturel : - clic gauche = comportement standard / tooltip ; - clic droit = ue sur le slot. Option 2 — mode “Unequip” Comme dans INVENTORY avec les modes d’action : - tu actives “Unequip” ; - puis tu cliques un slot inspecté. - C’est cohérent avec la philosophie existante de MultiBot, mais probablement un peu plus lourd que nécessaire. Option 3 — petit bouton contextuel / menu sur slot Plus explicite, mais visuellement plus chargé. - Mon avis Le clic droit sur le slot équipé est probablement la meilleure UX : - rapide, - lisible, - très cohérent avec une frame d’inspection. -Mon diagnostic final -Oui, c’est une bonne idée -Et franchement, c’est bien plus réaliste et propre que de vouloir réordonner les sacs par drag & drop. - +Au final Condition indispensable Il faut juste confirmer précisément : - la syntaxe réelle de ue, - si elle cible un slot ou un item, - quel message de retour elle produit, pour rebrancher le refresh. - Si ces 3 points sont clairs, alors oui, ça vaut complètement le coup de l’exposer graphiquement dans l’inspection d’un bot. diff --git a/UI/MultiBotHunterQuickFrame.lua b/UI/MultiBotHunterQuickFrame.lua index 9451384..de204fd 100644 --- a/UI/MultiBotHunterQuickFrame.lua +++ b/UI/MultiBotHunterQuickFrame.lua @@ -12,6 +12,11 @@ local WINDOW_PADDING_Y = 0 local WINDOW_TITLE = "Quick Hunter" local WINDOW_DEFAULT_POINT = { point = "CENTER", relPoint = "CENTER", x = -820, y = 300 } local ICON_FALLBACK = "Interface\\Icons\\INV_Misc_QuestionMark" +local HANDLE_WIDTH = 12 +local HANDLE_HEIGHT = 18 +local HANDLE_ALPHA = 0.45 +local HANDLE_HOVER_ALPHA = 0.85 +--local HANDLE_ICON = "Interface\\AddOns\\MultiBot\\Icons\\class_hunter.blp" local PET_STANCE_DEFINITIONS = { { key = "aggressive", icon = "ability_Racial_BloodRage", tip = "tips.hunter.pet.aggressive", persistent = true }, @@ -49,6 +54,29 @@ local function getAceGUI() return nil end +local function addPopupBackdrop(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 getPopupHost(title, width, height, missingDepMessage, persistenceKey) if type(MultiBot.CreateAceQuestPopupHost) == "function" then return MultiBot.CreateAceQuestPopupHost(title, width, height, missingDepMessage, persistenceKey) @@ -75,9 +103,16 @@ local function getPopupHost(title, width, height, missingDepMessage, persistence end) window:Hide() - local host = CreateFrame("Frame", nil, window.content) - host:SetAllPoints(window.content) + local root = CreateFrame("Frame", nil, window.content) + root:SetPoint("TOPLEFT", window.content, "TOPLEFT", 8, -8) + root:SetPoint("BOTTOMRIGHT", window.content, "BOTTOMRIGHT", -8, 8) + addPopupBackdrop(root, 0.92) + + local host = CreateFrame("Frame", nil, root) + host:SetPoint("TOPLEFT", root, "TOPLEFT", 8, -8) + host:SetPoint("BOTTOMRIGHT", root, "BOTTOMRIGHT", -8, 8) host.window = window + host.root = root host.Show = function(self) self.window:Show() end @@ -112,29 +147,6 @@ local function setTooltip(owner, text) GameTooltip:Show() end -local function addInputBackdrop(frame) - 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 = 12, - insets = { left = 3, right = 3, top = 3, bottom = 3 }, - }) - - if frame.SetBackdropColor then - frame:SetBackdropColor(0.05, 0.05, 0.06, 0.92) - end - - if frame.SetBackdropBorderColor then - frame:SetBackdropBorderColor(0.35, 0.35, 0.35, 0.95) - end -end - local function createIconButton(parent, name, iconPath, tooltipText, size) local button = CreateFrame("Button", name, parent) local actualSize = size or BUTTON_SIZE @@ -319,7 +331,81 @@ local function updateWindowTitle(service, count) end end -local function persistWindowPosition(frame) +local persistWindowPosition + +local function createCollapseHandle(service) + if not service.window or not service.window.frame or service.toggleHandle then + return service.toggleHandle + end + + local handle = CreateFrame("Button", nil, service.window.frame) + handle:SetFrameStrata("DIALOG") + handle:SetMovable(false) + handle:RegisterForClicks("LeftButtonUp", "RightButtonUp") + handle:RegisterForDrag("RightButton") + + handle:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 8, + edgeSize = 10, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + handle:SetBackdropColor(0.04, 0.04, 0.05, HANDLE_ALPHA) + handle:SetBackdropBorderColor(0.55, 0.55, 0.55, 0.85) + + local label = handle:CreateFontString(nil, "OVERLAY", "GameFontNormal") + label:SetPoint("CENTER", 0, 0) + label:SetText("×") + label:SetTextColor(0.92, 0.92, 0.92, 0.95) + handle.label = label + + handle:SetScript("OnEnter", function(self) + self:SetAlpha(HANDLE_HOVER_ALPHA) + if self.label and self.label.SetTextColor then + self.label:SetTextColor(1, 1, 1, 1) + end + setTooltip(self, "Left click : Show / Hide Right Click : Move Quick Hunter") + end) + handle:SetScript("OnLeave", function(self) + self:SetAlpha(HANDLE_ALPHA) + if self.label and self.label.SetTextColor then + self.label:SetTextColor(0.92, 0.92, 0.92, 0.95) + end + if GameTooltip and GameTooltip.Hide then + GameTooltip:Hide() + end + end) + handle:SetScript("OnClick", function(_, mouseButton) + if mouseButton == "LeftButton" and service.ToggleManualVisibility then + service:ToggleManualVisibility() + end + end) + handle:SetScript("OnDragStart", function() + local frame = service.window and service.window.frame + if not frame then + return + end + frame:StartMoving() + frame.__mbRightDragging = true + end) + handle:SetScript("OnDragStop", function() + local frame = service.window and service.window.frame + if not frame then + return + end + frame.__mbRightDragging = nil + frame:StopMovingOrSizing() + persistWindowPosition(frame) + end) + handle:SetAlpha(HANDLE_ALPHA) + + service.toggleHandle = handle + return handle +end + +persistWindowPosition = function(frame) if not frame or not MultiBot.SetQuickFramePosition then return end @@ -381,7 +467,9 @@ local function bindWindowDrag(service) end function HunterQuick:RestorePosition() - self:EnsureWindow() + if not self:EnsureWindow() then + return + end local frame = self.window and self.window.frame if not frame then @@ -461,6 +549,121 @@ function HunterQuick:SetSavedStance(name, stance) end end +function HunterQuick:IsManuallyVisible() + if self.manualVisible == nil then + if MultiBot.GetQuickFrameVisibleConfig then + self.manualVisible = MultiBot.GetQuickFrameVisibleConfig(HUNTER_QUICK_FRAME_KEY) + else + self.manualVisible = true + end + end + + return self.manualVisible ~= false +end + +function HunterQuick:SetManualVisibility(visible) + self.manualVisible = visible ~= false + if MultiBot.SetQuickFrameVisibleConfig then + MultiBot.SetQuickFrameVisibleConfig(HUNTER_QUICK_FRAME_KEY, self.manualVisible) + end +end + +function HunterQuick:ApplyCollapsedState() + if not self.window or not self.window.frame then + return + end + + if self.canvas then + self.canvas:Hide() + end + + for _, row in pairs(self.entries or {}) do + row:Hide() + end + + self.window:SetWidth(HANDLE_WIDTH) + self.window:SetHeight(HANDLE_HEIGHT) + self:UpdateToggleHandleLayout(true) + + self.window:Show() + self:RestorePosition() +end + +function HunterQuick:GetVisibleContentWidth() + local width = BUTTON_SIZE + local spacing = self:GetRowSpacing() + local index = 0 + + for _ in pairs(self.entries or {}) do + index = index + 1 + end + + if index == 0 then + return width + end + + local orderedNames = self:CollectHunterBots() + for orderedIndex, name in ipairs(orderedNames) do + local row = self.entries[name] + local rowWidth = BUTTON_SIZE + if row then + if row.modesStrip and row.modesStrip:IsShown() and row.modesStrip.GetWidth then + rowWidth = math.max(rowWidth, BUTTON_SIZE + BUTTON_GAP + row.modesStrip:GetWidth()) + end + if row.utilsStrip and row.utilsStrip:IsShown() and row.utilsStrip.GetWidth then + rowWidth = math.max(rowWidth, BUTTON_SIZE + BUTTON_GAP + row.utilsStrip:GetWidth()) + end + end + width = math.max(width, ((orderedIndex - 1) * spacing) + rowWidth) + end + + return width +end + +function HunterQuick:UpdateToggleHandleLayout(collapsed) + local handle = createCollapseHandle(self) + if not handle or not self.window or not self.window.frame then + return + end + + handle:ClearAllPoints() + if collapsed then + handle:SetPoint("TOPLEFT", self.window.frame, "TOPLEFT", 0, 0) + handle:SetPoint("BOTTOMRIGHT", self.window.frame, "BOTTOMRIGHT", 0, 0) + else + local visibleWidth = self:GetVisibleContentWidth() + handle:SetPoint("TOPLEFT", self.window.frame, "TOPLEFT", visibleWidth + BUTTON_GAP, 0) + handle:SetSize(HANDLE_WIDTH, HANDLE_HEIGHT) + end + + handle:Show() + handle:SetAlpha(HANDLE_ALPHA) +end + +function HunterQuick:ApplyExpandedState(count) + if not self.window or not self.window.frame then + return + end + + self:UpdateWindowGeometry(count) + + if self.canvas then + self.canvas:Show() + end + + self:UpdateToggleHandleLayout(false) + + self.window:Show() + self:RestorePosition() + self:UpdateAllPetPresence() +end + +function HunterQuick:ToggleManualVisibility() + local currentlyVisible = self:IsManuallyVisible() + self:SetManualVisibility(not currentlyVisible) + self:Rebuild() +end + function HunterQuick:ApplyStanceVisual(row, stance) row.stanceButtons = row.stanceButtons or {} for _, button in pairs(row.stanceButtons) do @@ -535,6 +738,10 @@ function HunterQuick:ToggleRow(row) row.modesStrip:Hide() row.utilsStrip:Hide() end + + if self:IsManuallyVisible() then + self:UpdateToggleHandleLayout(false) + end end function HunterQuick:ToggleStrip(row, stripKey) @@ -559,6 +766,10 @@ function HunterQuick:ToggleStrip(row, stripKey) if showModes then self:ApplyStanceVisual(row, row.activeStance) end + + if self:IsManuallyVisible() then + self:UpdateToggleHandleLayout(false) + end end function HunterQuick:ShowPrompt(formatString, targetName, title) @@ -586,7 +797,7 @@ function HunterQuick:EnsureSearchFrame() local searchBar = CreateFrame("Frame", nil, host) searchBar:SetPoint("TOP", host, "TOP", 0, -18) searchBar:SetSize(248, 26) - addInputBackdrop(searchBar) + addPopupBackdrop(searchBar, 0.92) host.SearchBar = searchBar local editBox = CreateFrame("EditBox", nil, searchBar) @@ -684,9 +895,14 @@ function HunterQuick:EnsureSearchFrame() return MultiBot.PET_FAMILY[familyId] or "?" end - local scrollFrame = CreateFrame("ScrollFrame", "MBHunterPetScroll", host, "UIPanelScrollFrameTemplate") - scrollFrame:SetPoint("TOPLEFT", host, "TOPLEFT", 10, -58) - scrollFrame:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", -30, 10) + local resultsPanel = CreateFrame("Frame", nil, host) + resultsPanel:SetPoint("TOPLEFT", host, "TOPLEFT", 0, -56) + resultsPanel:SetPoint("BOTTOMRIGHT", host, "BOTTOMRIGHT", 0, 0) + addPopupBackdrop(resultsPanel, 0.95) + + local scrollFrame = CreateFrame("ScrollFrame", "MBHunterPetScroll", resultsPanel, "UIPanelScrollFrameTemplate") + scrollFrame:SetPoint("TOPLEFT", resultsPanel, "TOPLEFT", 8, -8) + scrollFrame:SetPoint("BOTTOMRIGHT", resultsPanel, "BOTTOMRIGHT", -32, 8) local content = CreateFrame("Frame", nil, scrollFrame) content:SetSize(1, 1) @@ -705,7 +921,7 @@ function HunterQuick:EnsureSearchFrame() local previewButton = CreateFrame("Button", nil, row) previewButton:SetSize(16, 16) - previewButton:SetPoint("RIGHT", -22, 0) + previewButton:SetPoint("RIGHT", -32, 0) previewButton:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-UP") previewButton:SetPushedTexture("Interface\\Buttons\\UI-PlusButton-DOWN") previewButton:SetHighlightTexture("Interface\\Buttons\\UI-PlusButton-Hilight") @@ -806,9 +1022,14 @@ function HunterQuick:ShowFamilyFrame(targetName) self.FAMILY_FRAME = frame frame.TargetName = targetName - local scrollFrame = CreateFrame("ScrollFrame", "MBHunterFamilyScroll", frame, "UIPanelScrollFrameTemplate") - scrollFrame:SetPoint("TOPLEFT", 8, -10) - scrollFrame:SetPoint("BOTTOMRIGHT", -28, 8) + local listPanel = CreateFrame("Frame", nil, frame) + listPanel:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0) + listPanel:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0) + addPopupBackdrop(listPanel, 0.95) + + local scrollFrame = CreateFrame("ScrollFrame", "MBHunterFamilyScroll", listPanel, "UIPanelScrollFrameTemplate") + scrollFrame:SetPoint("TOPLEFT", listPanel, "TOPLEFT", 8, -8) + scrollFrame:SetPoint("BOTTOMRIGHT", listPanel, "BOTTOMRIGHT", -32, 8) local listWidth = 320 local content = CreateFrame("Frame", nil, scrollFrame) @@ -909,7 +1130,9 @@ function HunterQuick:BuildStanceAction(row, definition, index) end function HunterQuick:BuildRow(ownerName) - self:EnsureWindow() + if not self:EnsureWindow() then + return nil + end local root = CreateFrame("Frame", string.format("MultiBotHunterQuickRow_%s", sanitizeName(ownerName)), self.canvas) root:SetSize(ROW_WIDTH, ROW_HEIGHT) @@ -995,7 +1218,9 @@ function HunterQuick:BuildRow(ownerName) end function HunterQuick:UpdateWindowGeometry(count) - self:EnsureWindow() + if not self:EnsureWindow() then + return + end count = math.max(tonumber(count) or 0, 1) local width = (WINDOW_PADDING_X * 2) + ROW_WIDTH + ((count - 1) * self:GetRowSpacing()) @@ -1048,6 +1273,7 @@ function HunterQuick:EnsureWindow() self.canvas = canvas self.__aceInitialized = true + createCollapseHandle(self) bindWindowDrag(self) self:RestorePosition() @@ -1055,7 +1281,9 @@ function HunterQuick:EnsureWindow() end function HunterQuick:Rebuild() - self:EnsureWindow() + if not self:EnsureWindow() then + return + end local desiredNames = self:CollectHunterBots() local desiredLookup = {} @@ -1077,21 +1305,27 @@ function HunterQuick:Rebuild() end end + local manuallyVisible = self:IsManuallyVisible() for index, name in ipairs(desiredNames) do local row = self.entries[name] if row then row:ClearAllPoints() row:SetPoint("TOPLEFT", self.canvas, "TOPLEFT", (index - 1) * self:GetRowSpacing(), 0) row:SetFrameLevel((self.window.frame:GetFrameLevel() or 0) + 2) - row:Show() + if manuallyVisible then + row:Show() + else + row:Hide() + end end end if #desiredNames > 0 then - self:UpdateWindowGeometry(#desiredNames) - self.window:Show() - self:RestorePosition() - self:UpdateAllPetPresence() + if manuallyVisible then + self:ApplyExpandedState(#desiredNames) + else + self:ApplyCollapsedState() + end elseif self.window then updateWindowTitle(self, 0) self.window:Hide() diff --git a/UI/MultiBotShamanQuickFrame.lua b/UI/MultiBotShamanQuickFrame.lua index 4b7f491..db19b7a 100644 --- a/UI/MultiBotShamanQuickFrame.lua +++ b/UI/MultiBotShamanQuickFrame.lua @@ -12,6 +12,11 @@ local WINDOW_PADDING_Y = 0 local WINDOW_TITLE = "Quick Shaman" local WINDOW_DEFAULT_POINT = { point = "CENTER", relPoint = "CENTER", x = -420, y = 240 } local ICON_FALLBACK = "Interface\\Icons\\INV_Misc_QuestionMark" +local HANDLE_WIDTH = 12 +local HANDLE_HEIGHT = 18 +local HANDLE_ALPHA = 0.45 +local HANDLE_HOVER_ALPHA = 0.85 +--local HANDLE_ICON = "Interface\\AddOns\\MultiBot\\Icons\\class_shaman.blp" local ELEMENT_DEFINITIONS = { { @@ -278,7 +283,81 @@ local function updateWindowTitle(service, count) end end -local function persistWindowPosition(frame) +local persistWindowPosition + +local function createCollapseHandle(service) + if not service.window or not service.window.frame or service.toggleHandle then + return service.toggleHandle + end + + local handle = CreateFrame("Button", nil, service.window.frame) + handle:SetFrameStrata("DIALOG") + handle:SetMovable(false) + handle:RegisterForClicks("LeftButtonUp", "RightButtonUp") + handle:RegisterForDrag("RightButton") + + handle:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 8, + edgeSize = 10, + insets = { left = 2, right = 2, top = 2, bottom = 2 }, + }) + handle:SetBackdropColor(0.04, 0.04, 0.05, HANDLE_ALPHA) + handle:SetBackdropBorderColor(0.55, 0.55, 0.55, 0.85) + + local label = handle:CreateFontString(nil, "OVERLAY", "GameFontNormal") + label:SetPoint("CENTER", 0, 0) + label:SetText("×") + label:SetTextColor(0.92, 0.92, 0.92, 0.95) + handle.label = label + + handle:SetScript("OnEnter", function(self) + self:SetAlpha(HANDLE_HOVER_ALPHA) + if self.label and self.label.SetTextColor then + self.label:SetTextColor(1, 1, 1, 1) + end + setTooltip(self, "Left click : Show / Hide Right Click : Move Quick Shaman") + end) + handle:SetScript("OnLeave", function(self) + self:SetAlpha(HANDLE_ALPHA) + if self.label and self.label.SetTextColor then + self.label:SetTextColor(0.92, 0.92, 0.92, 0.95) + end + if GameTooltip and GameTooltip.Hide then + GameTooltip:Hide() + end + end) + handle:SetScript("OnClick", function(_, mouseButton) + if mouseButton == "LeftButton" and service.ToggleManualVisibility then + service:ToggleManualVisibility() + end + end) + handle:SetScript("OnDragStart", function() + local frame = service.window and service.window.frame + if not frame then + return + end + frame:StartMoving() + frame.__mbRightDragging = true + end) + handle:SetScript("OnDragStop", function() + local frame = service.window and service.window.frame + if not frame then + return + end + frame.__mbRightDragging = nil + frame:StopMovingOrSizing() + persistWindowPosition(frame) + end) + handle:SetAlpha(HANDLE_ALPHA) + + service.toggleHandle = handle + return handle +end + +persistWindowPosition = function(frame) if not frame or not MultiBot.SetQuickFramePosition then return end @@ -340,7 +419,9 @@ local function bindWindowDrag(service) end function ShamanQuick:RestorePosition() - self:EnsureWindow() + if not self:EnsureWindow() then + return + end local frame = self.window and self.window.frame if not frame then @@ -386,6 +467,114 @@ function ShamanQuick:CollectShamanBots() return names end +function ShamanQuick:IsManuallyVisible() + if self.manualVisible == nil then + if MultiBot.GetQuickFrameVisibleConfig then + self.manualVisible = MultiBot.GetQuickFrameVisibleConfig(SHAMAN_QUICK_FRAME_KEY) + else + self.manualVisible = true + end + end + + return self.manualVisible ~= false +end + +function ShamanQuick:SetManualVisibility(visible) + self.manualVisible = visible ~= false + if MultiBot.SetQuickFrameVisibleConfig then + MultiBot.SetQuickFrameVisibleConfig(SHAMAN_QUICK_FRAME_KEY, self.manualVisible) + end +end + +function ShamanQuick:ApplyCollapsedState() + if not self.window or not self.window.frame then + return + end + + if self.canvas then + self.canvas:Hide() + end + + for _, row in pairs(self.entries or {}) do + row:Hide() + end + + self.window:SetWidth(HANDLE_WIDTH) + self.window:SetHeight(HANDLE_HEIGHT) + self:UpdateToggleHandleLayout(true) + + self.window:Show() + self:RestorePosition() +end + +function ShamanQuick:GetVisibleContentWidth() + local width = BUTTON_SIZE + local spacing = self:GetRowSpacing() + local orderedNames = self:CollectShamanBots() + + if #orderedNames == 0 then + return width + end + + for orderedIndex, name in ipairs(orderedNames) do + local row = self.entries[name] + local rowWidth = BUTTON_SIZE + if row and row.groupFrames then + for _, groupFrame in pairs(row.groupFrames) do + if groupFrame and groupFrame.IsShown and groupFrame:IsShown() and groupFrame.GetWidth then + rowWidth = math.max(rowWidth, BUTTON_SIZE + BUTTON_GAP + groupFrame:GetWidth()) + end + end + end + width = math.max(width, ((orderedIndex - 1) * spacing) + rowWidth) + end + + return width +end + +function ShamanQuick:UpdateToggleHandleLayout(collapsed) + local handle = createCollapseHandle(self) + if not handle or not self.window or not self.window.frame then + return + end + + handle:ClearAllPoints() + if collapsed then + handle:SetPoint("TOPLEFT", self.window.frame, "TOPLEFT", 0, 0) + handle:SetPoint("BOTTOMRIGHT", self.window.frame, "BOTTOMRIGHT", 0, 0) + else + local visibleWidth = self:GetVisibleContentWidth() + handle:SetPoint("TOPLEFT", self.window.frame, "TOPLEFT", visibleWidth + BUTTON_GAP, 0) + handle:SetSize(HANDLE_WIDTH, HANDLE_HEIGHT) + end + + handle:Show() + handle:SetAlpha(HANDLE_ALPHA) +end + +function ShamanQuick:ApplyExpandedState(count) + if not self.window or not self.window.frame then + return + end + + self:UpdateWindowGeometry(count) + + if self.canvas then + self.canvas:Show() + end + + self:UpdateToggleHandleLayout(false) + + self.window:Show() + self:RestorePosition() +end + +function ShamanQuick:ToggleManualVisibility() + local currentlyVisible = self:IsManuallyVisible() + self:SetManualVisibility(not currentlyVisible) + self:RefreshFromGroup() +end + function ShamanQuick:CloseAllExcept(keepRow) for _, row in pairs(self.entries or {}) do if row ~= keepRow then @@ -415,6 +604,10 @@ function ShamanQuick:ToggleRow(row) groupFrame:Hide() end end + + if self:IsManuallyVisible() then + self:UpdateToggleHandleLayout(false) + end end function ShamanQuick:ToggleElementGroup(row, elementKey) @@ -443,6 +636,10 @@ function ShamanQuick:ToggleElementGroup(row, elementKey) else targetGroup:Hide() end + + if self:IsManuallyVisible() then + self:UpdateToggleHandleLayout(false) + end end function ShamanQuick:SetElementIcon(row, elementKey, iconPath) @@ -564,7 +761,9 @@ function ShamanQuick:CreateElementButton(row, elementDefinition, index) end function ShamanQuick:BuildRow(ownerName) - self:EnsureWindow() + if not self:EnsureWindow() then + return nil + end local row = CreateFrame("Frame", string.format("MultiBotShamanQuickRow_%s", sanitizeName(ownerName)), self.canvas) row:SetSize(ROW_WIDTH, ROW_HEIGHT) @@ -638,7 +837,9 @@ function ShamanQuick:BuildRow(ownerName) end function ShamanQuick:UpdateWindowGeometry(count) - self:EnsureWindow() + if not self:EnsureWindow() then + return + end count = math.max(tonumber(count) or 0, 1) local width = (WINDOW_PADDING_X * 2) + ROW_WIDTH + ((count - 1) * self:GetRowSpacing()) @@ -691,6 +892,7 @@ function ShamanQuick:EnsureWindow() self.canvas = canvas self.__aceInitialized = true + createCollapseHandle(self) bindWindowDrag(self) self:RestorePosition() @@ -698,7 +900,9 @@ function ShamanQuick:EnsureWindow() end function ShamanQuick:RefreshFromGroup() - self:EnsureWindow() + if not self:EnsureWindow() then + return + end local desiredNames = self:CollectShamanBots() local desiredLookup = {} @@ -720,20 +924,27 @@ function ShamanQuick:RefreshFromGroup() end end + local manuallyVisible = self:IsManuallyVisible() for index, name in ipairs(desiredNames) do local row = self.entries[name] if row then row:ClearAllPoints() row:SetPoint("TOPLEFT", self.canvas, "TOPLEFT", (index - 1) * self:GetRowSpacing(), 0) row:SetFrameLevel((self.window.frame:GetFrameLevel() or 0) + 2) - row:Show() + if manuallyVisible then + row:Show() + else + row:Hide() + end end end if #desiredNames > 0 then - self:UpdateWindowGeometry(#desiredNames) - self.window:Show() - self:RestorePosition() + if manuallyVisible then + self:ApplyExpandedState(#desiredNames) + else + self:ApplyCollapsedState() + end elseif self.window then updateWindowTitle(self, 0) self.window:Hide() diff --git a/docs/ace3-expansion-checklist.md b/docs/ace3-expansion-checklist.md index 761a920..9e8e165 100644 --- a/docs/ace3-expansion-checklist.md +++ b/docs/ace3-expansion-checklist.md @@ -28,7 +28,7 @@ Checklist for the full addon-wide ACE3 expansion after M7 completion. - [x] Talents/Glyphs frame migration slice completed (`UI/MultiBotTalentFrame.lua`): AceGUI host integration for the talents/glyphs workflow with preserved tab/copy/apply behavior and custom glyph interactions. - [x] ITEMUS migration slice completed (`UI/MultiBotItemusFrame.lua` + `Data/MultiBotItemus.lua` + `Core/MultiBotInit.lua`): native AceGUI host window, controller-owned paging/state helpers, lazy Masters launcher/reset initialization, dense icon grid parity, and verified legacy-combination coverage with localized in-window guidance reusing `tips.game.itemus`. - [x] ICONOS migration slice completed (`UI/MultiBotIconosFrame.lua` + `Core/MultiBotInit.lua` + `Data/MultiBotIconos.lua` + `Core/MultiBotHandler.lua`): native AceGUI host window, full legacy shell removal, dense 112-icon paging parity, right-drag position persistence via `IconosPoint`, ESC close support, copy-friendly in-window icon path display, and a first UX uplift pass with search (`ALL/PATH`) + original/A-Z ordering + selected-icon preview + jump-to-letter. -- [x] Quick Hunter / Quick Shaman migration slice completed (`UI/MultiBotHunterQuickFrame.lua` + `UI/MultiBotShamanQuickFrame.lua` + `Core/MultiBotInit.lua` + `Core/MultiBot.lua`): both quick bars now live in dedicated UI modules with native AceGUI-hosted windows, preserved position/state persistence, and full removal of the legacy quick-frame wrappers from `Core/MultiBotInit.lua`.. +- [x] Quick Hunter / Quick Shaman migration slice completed and finalized (`UI/MultiBotHunterQuickFrame.lua` + `UI/MultiBotShamanQuickFrame.lua` + `Core/MultiBotInit.lua` + `Core/MultiBot.lua`): both quick bars now live in dedicated UI modules with native AceGUI-hosted windows, preserved position/state persistence, validated Hunter/Shaman gameplay parity, and a lightweight persisted show/hide handle without legacy wrapper reintroduction. - [x] Hidden tooltip utility cleanup completed (`Core/MultiBotInit.lua`): `MB_LocalizeQuestTooltip` and `MBHiddenTip` now reuse a shared hidden-tooltip helper and remain native by design. - [x] AceGUI popup close-behavior parity tightened (`Core/MultiBotInit.lua`): migrated popup windows now hide on close (no release), preserving reopen behavior across the same session. - [x] AceGUI resolver deduplication completed (`Core/MultiBotInit.lua`): migrated popup paths now share a single resolver helper for dependency lookup + error reporting. diff --git a/docs/ace3-quick-frames-migration-tracker.md b/docs/ace3-quick-frames-migration-tracker.md index 7a885d5..2782946 100644 --- a/docs/ace3-quick-frames-migration-tracker.md +++ b/docs/ace3-quick-frames-migration-tracker.md @@ -47,11 +47,11 @@ Document de suivi dédié à la migration complète des mini frames **Quick Hunt - [x] Le code gère déjà un mini-contrôleur implicite (`entries`, `Rebuild`, `CollectHunterBots`, `UpdatePetPresence`) mais sans séparation formelle modèle/vue/contrôleur. ### Risques spécifiques Hunter -- [ ] Régression sur l’apparition conditionnelle selon la composition du groupe. -- [ ] Régression sur la persistance des stances par nom de bot. -- [ ] Régression sur le drag & drop / restauration de position. -- [ ] Régression sur les popups déjà migrés (search/family/prompt) si on modifie mal les points d’appel. -- [ ] Régression sur la désactivation du menu des stances quand aucun pet n’est disponible. +- [x] Régression sur l’apparition conditionnelle selon la composition du groupe. **Validation OK en jeu.** +- [x] Régression sur la persistance des stances par nom de bot. **Validation OK en jeu.** +- [x] Régression sur le drag & drop / restauration de position. **Validation OK en jeu.** +- [x] Régression sur les popups déjà migrés (search/family/prompt) si on modifie mal les points d’appel. **Validation OK en jeu.** +- [x] Régression sur la désactivation du menu des stances quand aucun pet n’est disponible. **Validation OK en jeu.** --- @@ -77,11 +77,11 @@ Document de suivi dédié à la migration complète des mini frames **Quick Hunt - [x] Un appel legacy parasite subsiste dans `OnDragStop` (`_MB_GetOrCreateShamanPos()`), alors que la persistance AceDB est désormais prise en charge par `MultiBot.SetQuickFramePosition(...)`. ### Risques spécifiques Shaman -- [ ] Régression sur l’exclusivité visuelle d’un totem par élément. -- [ ] Régression sur la restauration des icônes choisies après reload/relog. -- [ ] Régression sur les commandes `co +/-,?` si le mapping données/UI diverge. -- [ ] Régression sur le comportement d’expansion/fermeture des groupes Earth/Fire/Water/Air. -- [ ] Régression sur l’apparition conditionnelle selon la présence de shamans bots. +- [x] Régression sur l’exclusivité visuelle d’un totem par élément. **Validation OK en jeu.** +- [x] Régression sur la restauration des icônes choisies après reload/relog. **Validation OK en jeu.** +- [x] Régression sur les commandes `co +/-,?` si le mapping données/UI diverge. **Validation OK en jeu.** +- [x] Régression sur le comportement d’expansion/fermeture des groupes Earth/Fire/Water/Air. **Validation OK en jeu.** +- [x] Régression sur l’apparition conditionnelle selon la présence de shamans bots. **Validation OK en jeu.** --- @@ -106,13 +106,13 @@ Document de suivi dédié à la migration complète des mini frames **Quick Hunt ## 5) Objectif de migration ### Contraintes fonctionnelles à respecter -- [ ] Ne pas encapsuler les anciennes frames legacy dans une fenêtre AceGUI. -- [ ] Refaire les quick frames comme de vraies fenêtres/conteneurs Ace3/AceGUI. -- [ ] Conserver les mêmes commandes envoyées aux bots. -- [ ] Conserver l’apparition conditionnelle selon la présence de hunters/shamans bots dans le groupe. -- [ ] Conserver la persistance de position existante (`HunterQuick`, `ShamanQuick`). -- [ ] Conserver la persistance des stances pet Hunter et des totems Shaman. -- [ ] Conserver les popups Hunter déjà migrés sans les régresser. +- [x] Ne pas encapsuler les anciennes frames legacy dans une fenêtre AceGUI. +- [x] Refaire les quick frames comme de vraies fenêtres/conteneurs Ace3/AceGUI. +- [x] Conserver les mêmes commandes envoyées aux bots. +- [x] Conserver l’apparition conditionnelle selon la présence de hunters/shamans bots dans le groupe. +- [x] Conserver la persistance de position existante (`HunterQuick`, `ShamanQuick`). +- [x] Conserver la persistance des stances pet Hunter et des totems Shaman. +- [x] Conserver les popups Hunter déjà migrés sans les régresser. ### Cible de structure recommandée - [x] `UI/MultiBotHunterQuickFrame.lua` @@ -150,34 +150,34 @@ Document de suivi dédié à la migration complète des mini frames **Quick Hunt - [x] Supprimer les dépendances aux bordures/contours legacy de ces deux quick frames. ### Phase 4 — Parité fonctionnelle -- [ ] Valider la présence conditionnelle en groupe/raid. -- [ ] Valider la persistance de position des deux quick frames. -- [ ] Valider la persistance des stances Hunter. -- [ ] Valider la persistance des totems Shaman. -- [ ] Valider les flows Hunter prompt/search/family. -- [ ] Valider l’exclusivité visuelle et l’icône d’élément côté Shaman. +- [x] Valider la présence conditionnelle en groupe/raid. +- [x] Valider la persistance de position des deux quick frames. +- [x] Valider la persistance des stances Hunter. +- [x] Valider la persistance des totems Shaman. +- [x] Valider les flows Hunter prompt/search/family. +- [x] Valider l’exclusivité visuelle et l’icône d’élément côté Shaman. --- ## 7) Checklist de validation ciblée ### Hunter Quick -- [ ] Un hunter bot seul fait apparaître la frame. -- [ ] Plusieurs hunters bots produisent plusieurs rows stables et triées. -- [ ] Le drag déplace bien la frame et la position revient après reload. -- [ ] Les stances `aggressive/passive/defensive` restent exclusives visuellement. -- [ ] L’absence de pet désactive correctement le sous-menu des stances. -- [ ] `Name`, `Id`, `Family`, `Rename`, `Abandon` continuent d’envoyer les bonnes commandes. -- [ ] Les popups search/family/prompt restent fonctionnels. +- [x] Un hunter bot seul fait apparaître la frame. +- [x] Plusieurs hunters bots produisent plusieurs rows stables et triées. +- [x] Le drag déplace bien la frame et la position revient après reload. +- [x] Les stances `aggressive/passive/defensive` restent exclusives visuellement. +- [x] L’absence de pet désactive correctement le sous-menu des stances. +- [x] `Name`, `Id`, `Family`, `Rename`, `Abandon` continuent d’envoyer les bonnes commandes. +- [x] Les popups search/family/prompt restent fonctionnels. ### Shaman Quick -- [ ] Un shaman bot seul fait apparaître la frame. -- [ ] Plusieurs shamans bots produisent plusieurs rows stables. -- [ ] Le drag déplace bien la frame et la position revient après reload. -- [ ] Chaque élément affiche bien le totem choisi sur son bouton principal. -- [ ] La sélection d’un nouveau totem d’un même élément remplace bien l’ancien visuellement. -- [ ] Désélectionner un totem rétablit l’icône par défaut du bouton d’élément. -- [ ] Les choix persisted reviennent correctement après reload. +- [x] Un shaman bot seul fait apparaître la frame. +- [x] Plusieurs shamans bots produisent plusieurs rows stables. +- [x] Le drag déplace bien la frame et la position revient après reload. +- [x] Chaque élément affiche bien le totem choisi sur son bouton principal. +- [x] La sélection d’un nouveau totem d’un même élément remplace bien l’ancien visuellement. +- [x] Désélectionner un totem rétablit l’icône par défaut du bouton d’élément. +- [x] Les choix persisted reviennent correctement après reload. --- @@ -186,4 +186,12 @@ Document de suivi dédié à la migration complète des mini frames **Quick Hunt - [x] **UX Shaman** : ne pas conserver le comportement legacy où une row ouverte masque complètement les autres. La cible retenue est une UX AceGUI où toutes les rows restent visibles, avec une seule row détaillée/dépliée à la fois par défaut. - [x] **Architecture commune** : garder deux modules dédiés (`UI/MultiBotHunterQuickFrame.lua` et `UI/MultiBotShamanQuickFrame.lua`) et n’extraire que de petits utilitaires communs ciblés (position, drag, registre de rows, helpers de refresh), sans base abstraite prématurée. - [x] **Granularité des boutons** : ne pas figer la migration sur la valeur legacy `36px`, mais conserver la même densité d’usage et la même vitesse de scan/clic, avec un léger ajustement possible si le layout AceGUI est plus lisible et plus confortable. -- [x] **Collecte des bots présents** : introduire un petit contrôleur partagé purement runtime/data pour la découverte des bots de groupe/raid par classe, afin d’éviter de dupliquer la logique de collecte Hunter/Shaman sans coupler ce contrôleur au rendu UI. \ No newline at end of file +- [x] **Collecte des bots présents** : introduire un petit contrôleur partagé purement runtime/data pour la découverte des bots de groupe/raid par classe, afin d’éviter de dupliquer la logique de collecte Hunter/Shaman sans coupler ce contrôleur au rendu UI. + +--- + +## 9) Clôture du slice + +- [x] **Statut final** : la migration Quick Hunter / Quick Shaman est finalisée. +- [x] **Validation fonctionnelle** : tous les points de la checklist Hunter/Shaman ont été validés en jeu. +- [x] **UX additionnelle** : les quick frames disposent désormais d’une poignée de masquage/réaffichage semi-visible, persistée après reload, sans réintroduire de wrapper legacy. \ No newline at end of file diff --git a/docs/ace3-ui-frame-inventory.md b/docs/ace3-ui-frame-inventory.md index 1d4c79b..e960e7d 100644 --- a/docs/ace3-ui-frame-inventory.md +++ b/docs/ace3-ui-frame-inventory.md @@ -85,9 +85,9 @@ Inventory of addon UI frame construction points found via `CreateFrame(...)` sca Files: `UI/MultiBotIconosFrame.lua`, `Core/MultiBotInit.lua`, `UI/MultiBotIconos.lua`, `Core/MultiBotHandler.lua`. References: `UI/MultiBotIconosFrame.lua:1`, `Core/MultiBotInit.lua:1675`, `Core/MultiBotInit.lua:3189`, `UI/MultiBotIconos.lua:1`, `Core/MultiBotHandler.lua:265`. -- [x] **Quick Hunter / Quick Shaman bars** (`MultiBot.HunterQuick`, `MultiBot.ShamanQuick`) migrated out of `Core/MultiBotInit.lua` into dedicated UI modules with native AceGUI-hosted quick frames, preserved position/state persistence, and legacy wrapper removal for both class-specific mini bars. - Files: `UI/MultiBotHunterQuickFrame.lua`, `UI/MultiBotShamanQuickFrame.lua`, `Core/MultiBotInit.lua`, `Core/MultiBot.lua`. - References: `UI/MultiBotHunterQuickFrame.lua:1`, `UI/MultiBotShamanQuickFrame.lua:1`, `Core/MultiBotInit.lua:1916`, `Core/MultiBotInit.lua:3437`, `Core/MultiBotInit.lua:3440`, `Core/MultiBot.lua:613`, `Core/MultiBot.lua:858` +- [x] **Quick Hunter / Quick Shaman bars** (`MultiBot.HunterQuick`, `MultiBot.ShamanQuick`) migrated out of `Core/MultiBotInit.lua` into dedicated UI modules with native AceGUI-hosted quick frames, preserved position/state persistence, validated gameplay parity, and a compact persisted show/hide handle for both class-specific mini bars without reintroducing legacy shells. + Files: `UI/MultiBotHunterQuickFrame.lua`, `UI/MultiBotShamanQuickFrame.lua`, `Core/MultiBotInit.lua`, `Core/MultiBot.lua`. + References: `UI/MultiBotHunterQuickFrame.lua:1`, `UI/MultiBotShamanQuickFrame.lua:1`, `Core/MultiBotInit.lua:1916`, `Core/MultiBotInit.lua:3437`, `Core/MultiBotInit.lua:3440`, `Core/MultiBot.lua:613`, `Core/MultiBot.lua:858`. --- From 3c58b3860a76135f6bd5c961f525a0d28eb863d5 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:48:38 +0100 Subject: [PATCH 3/3] Lua lint fix --- UI/MultiBotHunterQuickFrame.lua | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/UI/MultiBotHunterQuickFrame.lua b/UI/MultiBotHunterQuickFrame.lua index de204fd..807e412 100644 --- a/UI/MultiBotHunterQuickFrame.lua +++ b/UI/MultiBotHunterQuickFrame.lua @@ -806,8 +806,8 @@ function HunterQuick:EnsureSearchFrame() editBox:SetPoint("BOTTOMRIGHT", searchBar, "BOTTOMRIGHT", -6, 5) editBox:SetFontObject(GameFontHighlightSmall) editBox:SetTextInsets(4, 4, 0, 0) - editBox:SetScript("OnEscapePressed", function(self) - self:ClearFocus() + editBox:SetScript("OnEscapePressed", function(editWidget) + editWidget:ClearFocus() end) host.EditBox = editBox @@ -867,16 +867,16 @@ function HunterQuick:EnsureSearchFrame() preview:SetUnit("none") preview:ClearModel() preview:Show() - preview:SetScript("OnUpdate", function(self) - self:SetScript("OnUpdate", nil) - self:SetModelScale(previewScale) - self:SetFacing(previewFacing) + preview:SetScript("OnUpdate", function(model) + model:SetScript("OnUpdate", nil) + model:SetModelScale(previewScale) + model:SetFacing(previewFacing) local displayNumber = tonumber(displayId) - if displayNumber and displayNumber > 0 and type(self.SetDisplayInfo) == "function" then - self:SetDisplayInfo(displayNumber) + if displayNumber and displayNumber > 0 and type(model.SetDisplayInfo) == "function" then + model:SetDisplayInfo(displayNumber) else - self:SetCreature(entryId) + model:SetCreature(entryId) end end) end @@ -943,12 +943,12 @@ function HunterQuick:EnsureSearchFrame() return "name_en" end - function host:RefreshRows() + function host.RefreshRows(hostFrame) local listWidth = 320 for index = 1, visibleRows do local dataIndex = index + offset local data = results[dataIndex] - local row = self.Rows[index] + local row = hostFrame.Rows[index] row:ClearAllPoints() row:SetPoint("TOPLEFT", 0, -((index - 1 + offset) * rowHeight)) @@ -980,7 +980,7 @@ function HunterQuick:EnsureSearchFrame() end end) - function host:Refresh() + function host.Refresh(hostFrame) wipe(results) local filter = (editBox:GetText() or ""):lower() local field = localeField() @@ -999,7 +999,7 @@ function HunterQuick:EnsureSearchFrame() content:SetHeight(#results * rowHeight) offset = 0 scrollFrame:SetVerticalScroll(0) - host:RefreshRows() + hostFrame:RefreshRows() end editBox:SetScript("OnTextChanged", function()