-- ============================================================================ -- MasterHub: pestana lateral de sistemas de MasterMu (borde izquierdo). -- Dibujo 100% por pixeles (nitido en cualquier resolucion); el estilo -- compartido vive en Scripts\MasterHub\Theme.lua. -- -- Cada sistema se registra agregando una entrada a la cola global -- MasterHubQueue (ver bloque "MASTERHUB REGISTRO" al final de cada script): -- -- MasterHubQueue = MasterHubQueue or {} -- table.insert(MasterHubQueue, { -- Id = "identificador", -- Label = "Nombre en el menu", -- Order = 10, -- orden en el menu (menor = arriba) -- IsVisible = function() ... end, -- opcional (default: visible) -- GetCount = function() ... end, -- opcional: alertas activas (numero) -- OnClick = function() ... end, -- abre el sistema; si retorna un -- -- string, se muestra como aviso -- }) -- ============================================================================ local CONFIG = require("Scripts\\MasterHub\\Config") local THEME = require("Scripts\\MasterHub\\Theme") MasterHubQueue = MasterHubQueue or {} MasterHub = { Systems = {}, MenuOpen = false, MenuArea = nil, Rows = {}, TabY = nil, Dragging = false, DragMoved = false, DragOffsetY = 0, Tick = 0, Toast = { Text = "", Ticks = 0 }, Seen = {}, -- notificaciones ya vistas por sistema (se limpian al hacer click) Announced = false, LastError = nil, } BridgeFunctionAttach("MainInterfaceProcThread", "MasterHub_Render") BridgeFunctionAttach("UpdateMouseEvent", "MasterHub_UpdateMouse") local function clamp(value, minValue, maxValue) value = tonumber(value) or minValue if value < minValue then return minValue end if value > maxValue then return maxValue end return value end local drawRect = THEME.DrawRect -- Pestana: lado izquierdo recto (pegado a la pared), lado derecho redondeado local function drawTabShape(x, y, w, h, color, alphaMul) drawRect(x, y, w - 3, h, color, alphaMul) drawRect(x, y + 2, w - 1, h - 4, color, alphaMul) drawRect(x, y + 4, w, h - 8, color, alphaMul) end local function mixColor(colorA, colorB, t) t = clamp(t, 0, 1) return { colorA[1] + ((colorB[1] - colorA[1]) * t), colorA[2] + ((colorB[2] - colorA[2]) * t), colorA[3] + ((colorB[3] - colorA[3]) * t), colorA[4] + ((colorB[4] - colorA[4]) * t), } end local function isMouseOver(x, y, w, h) local mouseX = MousePosX() local mouseY = MousePosY() return mouseX >= x and mouseX <= (x + w) and mouseY >= y and mouseY <= (y + h) end local function blockGameClick() if DisableCLick ~= nil then DisableCLick() elseif DisableClickClient ~= nil then DisableClickClient() end end local function resetMouseLeft() if ResetMouseL ~= nil then ResetMouseL() end end local function safeCall(fn, fallback) if type(fn) ~= "function" then return fallback end local ok, result = pcall(fn) if ok then return result end return fallback end function MasterHub.Register(entry) if type(entry) ~= "table" or entry.Label == nil then return end entry.Order = tonumber(entry.Order) or 100 if entry.Id ~= nil then local id = tostring(entry.Id) for i = #MasterHub.Systems, 1, -1 do if tostring(MasterHub.Systems[i].Id or "") == id then table.remove(MasterHub.Systems, i) end end end table.insert(MasterHub.Systems, entry) table.sort(MasterHub.Systems, function(a, b) return (tonumber(a.Order) or 100) < (tonumber(b.Order) or 100) end) end local function consumeQueue() if MasterHubQueue == nil or #MasterHubQueue == 0 then return end for i = 1, #MasterHubQueue do MasterHub.Register(MasterHubQueue[i]) end MasterHubQueue = {} end local function registerShortcuts() local shortcuts = CONFIG.Shortcuts or {} for i = 1, #shortcuts do local shortcut = shortcuts[i] if shortcut ~= nil and tonumber(shortcut.Enable) == 1 then local windowNumber = tonumber(shortcut.Window) or 0 MasterHub.Register({ Id = "shortcut" .. i, Label = tostring(shortcut.Label or "Ventana"), Order = tonumber(shortcut.Order) or (200 + i), OnClick = function() if windowNumber > 0 and OpenCustomWindow ~= nil then OpenCustomWindow(windowNumber) return nil end return "Ventana no disponible" end, }) end end end local function loadItemFusion() if ItemFusion ~= nil and type(ItemFusion.OpenWindow) == "function" and type(ItemFusion.CloseWindow) == "function" then return true end MasterHub.ItemFusionLoadError = nil if requirefolder ~= nil then local ok, err = pcall(requirefolder, "Scripts\\ItemFusion") if not ok then MasterHub.ItemFusionLoadError = tostring(err) end end if ItemFusion ~= nil and type(ItemFusion.OpenWindow) == "function" and type(ItemFusion.CloseWindow) == "function" then return true end local modules = { "Scripts\\ItemFusion\\ItemFusion", "Scripts/ItemFusion/ItemFusion", } for i = 1, #modules do local ok, err = pcall(require, modules[i]) if not ok then MasterHub.ItemFusionLoadError = tostring(err) elseif ItemFusion ~= nil and type(ItemFusion.OpenWindow) == "function" and type(ItemFusion.CloseWindow) == "function" then return true end end if dofile ~= nil then local files = { "Data\\Custom\\Script\\Scripts\\ItemFusion\\ItemFusion.lua", "Data//Custom//Script//Scripts//ItemFusion//ItemFusion.lua", "Scripts\\ItemFusion\\ItemFusion.lua", "Scripts//ItemFusion//ItemFusion.lua", } for i = 1, #files do local ok, err = pcall(dofile, files[i]) if not ok then MasterHub.ItemFusionLoadError = tostring(err) elseif ItemFusion ~= nil and type(ItemFusion.OpenWindow) == "function" and type(ItemFusion.CloseWindow) == "function" then return true end end end return false end local function ensureItemFusionEntry() if MasterHub.ItemFusionEntryReady then return end MasterHub.Register({ Id = "itemfusion", Label = "Fusion de Items", Order = 25, IsVisible = function() return true end, OnClick = function() if not loadItemFusion() then if MasterHub.ItemFusionLoadError ~= nil then pcall(Console, 1, "[MasterHub] ItemFusion no cargo: " .. MasterHub.ItemFusionLoadError) end return "Fusion no disponible" end if ItemFusion.Open then ItemFusion.CloseWindow() else ItemFusion.OpenWindow() end return nil end, }) MasterHub.ItemFusionEntryReady = true end local function getVisibleSystems() local visible = {} for i = 1, #MasterHub.Systems do local system = MasterHub.Systems[i] if system.IsVisible == nil or safeCall(system.IsVisible, false) == true then table.insert(visible, system) end end return visible end local function getSystemCount(system) return tonumber(safeCall(system.GetCount, 0)) or 0 end local function systemKey(system) return tostring(system.Id or system.Label or "?") end -- Cuenta solo las alertas NO vistas: al hacer click en el sistema se marcan -- como leidas (deja de parpadear y desaparece el numero), y vuelven a -- notificar cuando cambia la actividad local function getUnseenCount(system) local count = getSystemCount(system) local key = systemKey(system) if count <= 0 then MasterHub.Seen[key] = 0 return 0 end if (tonumber(MasterHub.Seen[key]) or 0) == count then return 0 end return count end local function getTotalUnseen(visible) local total = 0 for i = 1, #visible do total = total + getUnseenCount(visible[i]) end return total end function MasterHub.MarkSeen(system) if system ~= nil then MasterHub.Seen[systemKey(system)] = getSystemCount(system) end end local function getTabLayout() local tab = CONFIG.Tab or {} local width = tonumber(tab.Width) or 30 local height = tonumber(tab.Height) or 30 local margin = tonumber(tab.Margin) or 8 local marginB = tonumber(tab.MarginBottom) or 98 local screenWidth = GetWideX() local screenHeight = tonumber(CONFIG.ScreenHeight) or 480 local defaultX = tonumber(tab.PosX) or (screenWidth - width - margin) local defaultY = tonumber(tab.PosY) or (screenHeight - height - marginB) -- X fijo (anclado a la derecha), Y arrastrable en vertical MasterHub.TabY = clamp(tonumber(MasterHub.TabY) or defaultY, 0, math.max(0, screenHeight - height)) local x = clamp(defaultX, 0, math.max(0, screenWidth - width)) return x, MasterHub.TabY, width, height end local function getMenuLayout(visibleCount) local menu = CONFIG.Menu or {} local tabX, tabY, tabWidth, tabHeight = getTabLayout() local width = tonumber(menu.Width) or 182 local rowHeight = tonumber(menu.RowHeight) or 24 local headerHeight = tonumber(menu.HeaderHeight) or 20 local screenHeight = tonumber(CONFIG.ScreenHeight) or 480 local height = headerHeight + (visibleCount * rowHeight) + 8 -- El menu abre HACIA ARRIBA y alineado al borde derecho del boton local x = clamp(tabX + tabWidth - width, 0, math.max(0, GetWideX() - width)) local y = clamp(tabY - height - 4, 0, math.max(0, screenHeight - height)) return x, y, width, height, rowHeight, headerHeight end function MasterHub.ShowToast(text) MasterHub.Toast.Text = tostring(text or "") MasterHub.Toast.Ticks = tonumber(CONFIG.ToastTicks) or 110 end function MasterHub.DrawTab(totalCount) local colors = CONFIG.Colors or {} local theme = THEME.Colors local x, y, w, h = getTabLayout() local hovered = isMouseOver(x, y, w, h) or MasterHub.MenuOpen local pulse = 0.5 + (0.5 * math.sin(MasterHub.Tick * 0.09)) local plateGlow = colors.PlateGlow or { 0.30, 0.62, 0.74, 0.09 } local barLight = colors.MenuBar or { 0.62, 0.80, 0.88, 1.00 } local barDark = colors.MenuBarDark or { 0.24, 0.40, 0.48, 1.00 } local alert = colors.TabBorderAlert or { 0.39, 0.75, 0.88, 1.00 } local border = theme.GoldMid if hovered then border = theme.GoldLight elseif totalCount > 0 then border = mixColor(theme.GoldMid, alert, pulse) end -- Boton cuadrado de esquina: sombra + marco 1px + placa interior drawRect(x + 2, y + 2, w, h, theme.Shadow) drawRect(x, y, w, h, border) drawRect(x + 1, y + 1, w - 2, h - 2, theme.Plate) -- bisel: luz arriba, sombra abajo drawRect(x + 1, y + 1, w - 2, 1, theme.GoldLight) drawRect(x + 1, y + h - 2, w - 2, 1, theme.GoldDark) -- ambiente calido arriba drawRect(x + 2, y + 2, w - 4, 6, theme.PlateWarm) -- resplandor si hay alertas local glowBoost = 1 if totalCount > 0 then glowBoost = 0.8 + (pulse * 0.8) end drawRect(x + 3, y + math.floor(h / 2) - 8, w - 6, 16, plateGlow, glowBoost) -- icono de menu: 3 barras (hamburguesa) local cxm = x + math.floor(w / 2) local cym = y + math.floor(h / 2) local barWidth = w - 14 local barX = cxm - math.floor(barWidth / 2) local barY = cym - 6 local light = hovered and { 1.00, 0.93, 0.68, 1.00 } or barLight for i = 0, 2 do local rowY = barY + (i * 5) drawRect(barX + 1, rowY + 1, barWidth, 3, theme.Shadow) drawRect(barX, rowY, barWidth, 3, light) drawRect(barX, rowY + 2, barWidth, 1, barDark) end -- chip contador de alertas (esquina superior derecha del boton) if totalCount > 0 then local chipW = 15 local chipH = 12 local chipX = x + w - 10 local chipY = math.max(0, y - 5) local chipText = tostring(totalCount) if totalCount > 9 then chipText = "9+" end drawRect(chipX + 1, chipY + 1, chipW, chipH, theme.Shadow) drawRect(chipX, chipY, chipW, chipH, mixColor(colors.ChipBack or { 0.10, 0.20, 0.26, 0.95 }, colors.ChipBackAlert or { 0.20, 0.52, 0.64, 1.00 }, pulse)) drawRect(chipX, chipY, chipW, 1, colors.ChipShine or { 0.55, 0.85, 0.95, 0.55 }) SetFontType(0) SetTextBg(0, 0, 0, 0) SetTextColor(255, 244, 235, 255) RenderText(chipX, chipY + 2, chipText, chipW, ALIGN_CENTER) end end function MasterHub.DrawMenu(visible) local colors = CONFIG.Colors or {} local theme = THEME.Colors local x, y, width, height, rowHeight, headerHeight = getMenuLayout(#visible) local pulse = 0.5 + (0.5 * math.sin(MasterHub.Tick * 0.12)) MasterHub.Rows = {} MasterHub.MenuArea = { x = x, y = y, w = width, h = height } THEME.DrawFrame(x, y, width, height, headerHeight) SetFontType(1) SetTextBg(0, 0, 0, 0) THEME.SetTitleColor() RenderText(x, y + 5, CONFIG.MenuTitle or "SISTEMAS", width, ALIGN_CENTER) SetFontType(0) for i = 1, #visible do local system = visible[i] local count = getSystemCount(system) local unseen = getUnseenCount(system) local rowY = y + headerHeight + 4 + ((i - 1) * rowHeight) local hovered = isMouseOver(x + 5, rowY, width - 10, rowHeight - 2) MasterHub.Rows[i] = { x = x + 5, y = rowY, w = width - 10, h = rowHeight - 2, system = system } drawRect(x + 5, rowY, width - 10, rowHeight - 2, hovered and theme.RowHover or theme.Row) local dotColor = colors.DotIdle or { 0.42, 0.42, 0.42, 0.80 } local dotAlpha = 1 if count > 0 then dotColor = colors.DotActive or { 0.30, 0.95, 0.40, 1.00 } if unseen > 0 then dotAlpha = 0.55 + (pulse * 0.45) end end drawRect(x + 12, rowY + math.floor(rowHeight / 2) - 3, 4, 4, dotColor, dotAlpha) THEME.SetTextColor() RenderText(x + 24, rowY + 5, tostring(system.Label), width - 70, ALIGN_LEFT) if unseen > 0 then SetTextColor(120, 205, 240, 255) RenderText(x + width - 46, rowY + 5, tostring(unseen), 34, ALIGN_RIGHT) elseif count > 0 then THEME.SetTextDimColor() RenderText(x + width - 46, rowY + 5, tostring(count), 34, ALIGN_RIGHT) else SetTextColor(150, 150, 150, 190) RenderText(x + width - 46, rowY + 5, "-", 34, ALIGN_RIGHT) end end end function MasterHub.DrawToast() local toast = MasterHub.Toast if toast.Ticks <= 0 or toast.Text == "" then return end toast.Ticks = toast.Ticks - 1 local colors = CONFIG.Colors or {} local theme = THEME.Colors local tabX, tabY, tabWidth, tabHeight = getTabLayout() local width = math.max(150, (string.len(toast.Text) * 7) + 18) local toastX = tabWidth + 6 local toastY = tabY + tabHeight + 6 if MasterHub.MenuOpen and MasterHub.MenuArea ~= nil then toastY = MasterHub.MenuArea.y + MasterHub.MenuArea.h + 5 toastX = MasterHub.MenuArea.x end local screenHeight = tonumber(CONFIG.ScreenHeight) or 480 toastX = clamp(toastX, 0, math.max(0, GetWideX() - width)) toastY = clamp(toastY, 0, math.max(0, screenHeight - 18)) local alpha = 1 if toast.Ticks < 30 then alpha = toast.Ticks / 30 end drawRect(toastX, toastY, width, 18, colors.ToastBack or { 0.05, 0.03, 0.02, 0.88 }, alpha) drawRect(toastX, toastY, width, 1, theme.GoldMid, alpha) drawRect(toastX, toastY + 17, width, 1, theme.GoldDark, alpha) SetFontType(0) SetTextBg(0, 0, 0, 0) SetTextColor(255, 214, 170, math.floor(235 * alpha)) RenderText(toastX, toastY + 5, toast.Text, width, ALIGN_CENTER) end function MasterHub.MouseCore() if CONFIG.Enable == 0 then return end if GetMainScene() ~= 5 then MasterHub.MenuOpen = false MasterHub.Dragging = false return end local mouseY = MousePosY() local released = CheckReleasedKey(Keys.LButton) == 1 local pressed = CheckPressedKey(Keys.LButton) == 1 local x, y, w, h = getTabLayout() local overTab = isMouseOver(x, y, w, h) if MasterHub.Dragging then local screenHeight = tonumber(CONFIG.ScreenHeight) or 480 local nextY = clamp(mouseY - MasterHub.DragOffsetY, 0, math.max(0, screenHeight - h)) if math.abs(nextY - MasterHub.TabY) >= 2 then MasterHub.DragMoved = true end MasterHub.TabY = nextY blockGameClick() if released then if not MasterHub.DragMoved then MasterHub.MenuOpen = not MasterHub.MenuOpen end MasterHub.Dragging = false MasterHub.DragMoved = false resetMouseLeft() end return end if overTab then blockGameClick() if tonumber((CONFIG.Tab or {}).Locked) == 1 then -- Boton fijo: el click solo abre/cierra el menu (no se arrastra) if released then MasterHub.MenuOpen = not MasterHub.MenuOpen resetMouseLeft() return end elseif pressed then MasterHub.Dragging = true MasterHub.DragOffsetY = mouseY - y MasterHub.DragMoved = false resetMouseLeft() return end end if not MasterHub.MenuOpen then return end local area = MasterHub.MenuArea local overMenu = area ~= nil and isMouseOver(area.x, area.y, area.w, area.h) if overMenu then blockGameClick() end if not released then return end for i = 1, #MasterHub.Rows do local row = MasterHub.Rows[i] if isMouseOver(row.x, row.y, row.w, row.h) then local message = nil if row.system ~= nil and type(row.system.OnClick) == "function" then local ok, result = pcall(row.system.OnClick) if ok then message = result end -- Marcar como leido: se borra el contador y deja de parpadear MasterHub.MarkSeen(row.system) end if message ~= nil and message ~= "" then MasterHub.ShowToast(message) else MasterHub.MenuOpen = false end resetMouseLeft() return end end if not overTab and not overMenu then MasterHub.MenuOpen = false end end function MasterHub.RenderCore() if CONFIG.Enable == 0 then return end if GetMainScene() ~= 5 then return end consumeQueue() ensureItemFusionEntry() -- Aviso unico de carga (visible en pantalla, no requiere log). -- Se desactiva con Debug = 0 en el Config. if not MasterHub.Announced then MasterHub.Announced = true if tonumber(CONFIG.Debug or 0) == 1 then pcall(SendMessageClient, "[MasterHub] Script cargado y dibujando") end end MasterHub.Tick = MasterHub.Tick + 1 local visible = getVisibleSystems() local totalUnseen = getTotalUnseen(visible) MasterHub.DrawTab(totalUnseen) if MasterHub.MenuOpen then if #visible == 0 then MasterHub.MenuOpen = false else MasterHub.DrawMenu(visible) end end MasterHub.DrawToast() DisableAlphaBlend() end -- Envolturas protegidas: si algo falla, se reporta al log del cliente -- en lugar de dejar de dibujar la pestana en silencio local function reportError(where, err) err = tostring(err) if MasterHub.LastError ~= err then MasterHub.LastError = err pcall(Console, 1, "[MasterHub] Error en " .. where .. ": " .. err) end end function MasterHub_Render() local ok, err = pcall(MasterHub.RenderCore) if not ok then reportError("render", err) end end function MasterHub_UpdateMouse() local ok, err = pcall(MasterHub.MouseCore) if not ok then reportError("mouse", err) end end registerShortcuts() return MasterHub