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 2497649..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) @@ -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", @@ -3001,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 @@ -3026,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") @@ -3432,1023 +3482,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) - - 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 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 + + 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() + if not self:EnsureWindow() then + return + end + + 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: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 + 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 + + if self:IsManuallyVisible() then + self:UpdateToggleHandleLayout(false) + 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 + + if self:IsManuallyVisible() then + self:UpdateToggleHandleLayout(false) + 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) + addPopupBackdrop(searchBar, 0.92) + 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(editWidget) + editWidget: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(model) + model:SetScript("OnUpdate", nil) + model:SetModelScale(previewScale) + model:SetFacing(previewFacing) + + local displayNumber = tonumber(displayId) + if displayNumber and displayNumber > 0 and type(model.SetDisplayInfo) == "function" then + model:SetDisplayInfo(displayNumber) + else + model: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 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) + 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", -32, 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(hostFrame) + local listWidth = 320 + for index = 1, visibleRows do + local dataIndex = index + offset + local data = results[dataIndex] + local row = hostFrame.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(hostFrame) + 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) + hostFrame: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 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) + 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) + 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) + 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) + 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()) + + 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 + + createCollapseHandle(self) + bindWindowDrag(self) + self:RestorePosition() + + return window +end + +function HunterQuick:Rebuild() + if not self:EnsureWindow() then + return + end + + 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 + + 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) + if manuallyVisible then + row:Show() + else + row:Hide() + end + end + end + + if #desiredNames > 0 then + if manuallyVisible then + self:ApplyExpandedState(#desiredNames) + else + self:ApplyCollapsedState() + end + 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..db19b7a --- /dev/null +++ b/UI/MultiBotShamanQuickFrame.lua @@ -0,0 +1,977 @@ +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 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 = { + { + 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 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 + + 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() + if not self:EnsureWindow() then + return + end + + 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: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 + 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 + + if self:IsManuallyVisible() then + self:UpdateToggleHandleLayout(false) + 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 + + if self:IsManuallyVisible() then + self:UpdateToggleHandleLayout(false) + 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) + 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) + 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) + 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()) + + 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 + + createCollapseHandle(self) + bindWindowDrag(self) + self:RestorePosition() + + return window +end + +function ShamanQuick:RefreshFromGroup() + if not self:EnsureWindow() then + return + end + + 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 + + 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) + if manuallyVisible then + row:Show() + else + row:Hide() + end + end + end + + if #desiredNames > 0 then + if manuallyVisible then + self:ApplyExpandedState(#desiredNames) + else + self:ApplyCollapsedState() + end + 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..9e8e165 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 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 new file mode 100644 index 0000000..2782946 --- /dev/null +++ b/docs/ace3-quick-frames-migration-tracker.md @@ -0,0 +1,197 @@ +# 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 +- [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.** + +--- + +## 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 +- [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.** + +--- + +## 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 +- [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` + - 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 +- [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 +- [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 +- [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. + +--- + +## 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. + +--- + +## 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 371fb5f..e960e7d 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, 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`. ---