-- ============================================================================ -- Merge Shop -- Version 2.3.8 (Glass UI + optimized scheduler) -- ============================================================================ -- Design goal: keep the original clean glass/light panel style with sidebar, -- rounded rows, tabs, notifications, themes and font controls. -- Boot rule: UI is created first; game modules/remotes load after the panel. -- ============================================================================ local Services = { Players = game:GetService("Players"), TweenService = game:GetService("TweenService"), UserInputService = game:GetService("UserInputService"), RunService = game:GetService("RunService"), MarketplaceService = game:GetService("MarketplaceService"), TeleportService = game:GetService("TeleportService"), HttpService = game:GetService("HttpService"), LocalizationService = game:GetService("LocalizationService"), GuiService = game:GetService("GuiService"), CoreGui = game:GetService("CoreGui"), ReplicatedStorage = game:GetService("ReplicatedStorage"), Workspace = game:GetService("Workspace"), } local player = Services.Players.LocalPlayer local PlayerGui = player and player:FindFirstChildOfClass("PlayerGui") local unpackArgs = table.unpack or unpack local RUN_ID = tostring(os.clock()) .. ":" .. tostring(math.random(1, 1000000000)) pcall(function() if _G then for _, shutdownName in ipairs({ "MergeShopShutdown", "DevToolsPanelShutdown" }) do local oldShutdown = _G[shutdownName] if type(oldShutdown) == "function" then oldShutdown("restart") end end end end) local SCRIPT_NAME = "Merge Shop" local SCRIPT_VERSION = "2.3.8" local GUI_NAME = "MergeShop" local LEGACY_GUI_NAME = "DevToolsPanel" local CONFIG_FILE = "MergeShop_Settings.json" local LEGACY_CONFIG_FILE = "DevToolsPanel_Settings.json" local DEFAULT_FONT_NAME = "Cartoon" local DEFAULT_FONT_SIZE = 10 local CREATOR_NAME = "ep2a1" local WIN_W = 600 local WIN_H = 380 local TITLEBAR_H = 26 local MARGIN = 8 local TOP_MARGIN = 2 local GAP = 6 local SIDE_EXP = 120 local SIDE_COL = 32 local ITEM_H = 24 local MAX_DD_H = 132 local function rgb(r, g, b) return Color3.fromRGB(r, g, b) end local function clone(t) local c = {} for k, v in pairs(t) do c[k] = v end return c end local function indexOf(list, value) for i, v in ipairs(list) do if v == value then return i end end return 1 end local function getGuiInset() local ok = false local inset = nil ok, inset = pcall(function() return Services.GuiService:GetGuiInset() end) if ok and typeof(inset) == "Vector2" then return inset end return Vector2.new(0, 36) end local function adjustedScreenPoint(point) if not point then return nil end local inset = getGuiInset() return Vector2.new(point.X - inset.X, point.Y - inset.Y) end local function adjustedMousePosition() local mousePosition = Services.UserInputService:GetMouseLocation() return adjustedScreenPoint(mousePosition) end local function adjustedInputPosition(input) if not input then return nil end return adjustedScreenPoint(input.Position) end local FONT_LIST = { "GothamMedium", "GothamBold", "Gotham", "GothamBlack", "Arial", "ArialBold", "SourceSans", "SourceSansBold", "SourceSansLight", "SourceSansItalic", "SourceSansSemibold", "SourceSansPro", "SourceSansProBold", "BuilderSans", "BuilderSansMedium", "BuilderSansLight", "BuilderSansBold", "BuilderSansExtraBold", "Roboto", "RobotoCondensed", "RobotoMono", "Antique", "Arcade", "Bangers", "Bodoni", "BodoniBlack", "Cartoon", "Code", "Creepster", "DenkOne", "Fondamento", "FredokaOne", "Garamond", "GrenzeGotisch", "Highway", "IndieFlower", "JosefinSans", "JosefinSansBold", "Jura", "JuraBold", "Kalam", "KalamBold", "Lato", "LatoBold", "Legacy", "LuckiestGuy", "Merriweather", "MerriweatherBold", "Michroma", "Nunito", "NunitoBold", "Oswald", "OswaldBold", "PatrickHand", "PermanentMarker", "PressStart2P", "Raleway", "RalewayBold", "Sarpanch", "SciFi", "SpecialElite", "TitilliumWeb", "TitilliumWebBold", "Ubuntu", "UbuntuBold", "Zekton", "AmaticSC", } do local seen, out = {}, {} for _, f in ipairs(FONT_LIST) do if not seen[f] then seen[f] = true out[#out + 1] = f end end FONT_LIST = out end local function fontIndex(name) for i, f in ipairs(FONT_LIST) do if f == name then return i end end return 1 end local function resolveFont(name) local ok, enumFont = pcall(function() return Enum.Font[name] end) return (ok and enumFont) or Enum.Font.GothamMedium end local function theme(name, bg, panel, hover, inner, border, text, sub, accent, accentText) return { name = name, colors = { bg = bg, panel = panel, panelHover = hover, panelInner = inner, border = border, text = text, textSub = sub, accent = accent, accentText = accentText, knob = rgb(255, 255, 255), tlRed = rgb(255, 95, 86), tlYellow = rgb(255, 189, 46), tlGreen = rgb(40, 200, 64), }, } end local THEMES = { theme( "Dark", rgb(8, 10, 15), rgb(15, 18, 26), rgb(26, 31, 44), rgb(20, 24, 34), rgb(58, 67, 88), rgb(238, 242, 248), rgb(150, 160, 178), rgb(125, 155, 255), rgb(8, 10, 15) ), theme( "Light", rgb(242, 244, 248), rgb(255, 255, 255), rgb(232, 236, 244), rgb(248, 250, 253), rgb(198, 205, 218), rgb(32, 36, 45), rgb(102, 111, 128), rgb(62, 111, 240), rgb(255, 255, 255) ), theme( "Graphite", rgb(24, 27, 32), rgb(38, 43, 51), rgb(55, 62, 74), rgb(45, 50, 60), rgb(104, 116, 135), rgb(240, 244, 250), rgb(170, 181, 198), rgb(145, 175, 255), rgb(18, 22, 30) ), theme( "Noir", rgb(5, 5, 8), rgb(12, 12, 17), rgb(24, 24, 32), rgb(18, 18, 25), rgb(58, 58, 72), rgb(236, 236, 242), rgb(145, 145, 160), rgb(245, 245, 250), rgb(8, 8, 12) ), theme( "Midnight", rgb(5, 8, 24), rgb(12, 18, 45), rgb(25, 36, 78), rgb(18, 27, 60), rgb(62, 78, 145), rgb(220, 228, 255), rgb(140, 154, 205), rgb(105, 130, 255), rgb(5, 8, 24) ), theme( "Ocean", rgb(6, 22, 34), rgb(16, 42, 62), rgb(28, 68, 96), rgb(22, 54, 78), rgb(60, 112, 150), rgb(225, 242, 255), rgb(145, 190, 220), rgb(35, 185, 225), rgb(5, 18, 28) ), theme( "Violet", rgb(23, 17, 35), rgb(43, 32, 64), rgb(68, 52, 96), rgb(54, 41, 78), rgb(120, 95, 165), rgb(245, 238, 255), rgb(190, 174, 220), rgb(175, 125, 255), rgb(24, 18, 36) ), theme( "Amethyst", rgb(32, 22, 46), rgb(58, 42, 78), rgb(86, 64, 116), rgb(70, 52, 96), rgb(145, 112, 190), rgb(248, 240, 255), rgb(202, 182, 230), rgb(195, 135, 255), rgb(32, 22, 46) ), theme( "Arctic", rgb(210, 226, 240), rgb(238, 247, 255), rgb(205, 226, 244), rgb(228, 242, 255), rgb(126, 166, 200), rgb(35, 60, 82), rgb(80, 112, 140), rgb(40, 145, 220), rgb(255, 255, 255) ), theme( "Pearl", rgb(248, 249, 252), rgb(255, 255, 255), rgb(238, 240, 247), rgb(252, 253, 255), rgb(205, 210, 222), rgb(38, 42, 52), rgb(105, 112, 130), rgb(95, 125, 210), rgb(255, 255, 255) ), theme( "Emerald", rgb(8, 26, 18), rgb(18, 48, 34), rgb(32, 76, 52), rgb(24, 60, 42), rgb(70, 135, 98), rgb(226, 248, 236), rgb(150, 205, 175), rgb(62, 210, 135), rgb(8, 26, 18) ), theme( "Rose", rgb(38, 18, 28), rgb(66, 30, 48), rgb(98, 46, 70), rgb(80, 38, 58), rgb(160, 78, 110), rgb(255, 238, 246), rgb(220, 165, 190), rgb(255, 105, 155), rgb(38, 18, 28) ), } -- Deduplicate themes by name, keeping the first palette for each name. do local seen = {} local unique = {} for _, t in ipairs(THEMES) do local name = tostring(t and t.name or "") if name ~= "" and not seen[name] then seen[name] = true unique[#unique + 1] = t end end THEMES = unique end local TRANSPARENCY_MODES = { "None", "Sidebar", "Content", "Both" } local TRANSPARENCY_SET = { None = true, Sidebar = true, Content = true, Both = true } local Settings = { settingsVersion = SCRIPT_VERSION, themeIndex = 2, uiFontIndex = fontIndex(DEFAULT_FONT_NAME), uiFontSize = DEFAULT_FONT_SIZE, transparency = true, transparencyMode = "Both", customMaxPlayers = 6, infoSizeMode = "Medium", infoSizeScale = 100, recentServerIds = {}, statProfiles = {}, firstJoin = nil, antiAfk = false, processGrid = false, collectDrops = false, completeTasks = false, constructBuilding = false, smartProcessing = true, spinReward = false, dailyReward = false, claimInbox = false, handleObstacleCourse = false, obstacleDifficulty = "Hard", clearBubbles = false, vendorActive = false, vendorAutoFree = false, vendorSelectedItem = nil, vendorSelectedItemKey = nil, vendorQuantities = {}, walkSpeed = 50, mergeSpeed = 5, orderSpeed = 5, uiSize = 10, } local State = { running = true, theme = nil, uiFont = nil, uiFontSize = DEFAULT_FONT_SIZE, activePage = nil, sbCollapsed = false, sessionStart = tick(), firstJoin = os.time(), sessionMerges = 0, totalActions = 0, pickupCount = 0, ordersCompleted = 0, rewardsClaimed = 0, sessionCoinsSpent = 0, sessionEnergySpent = 0, sessionGemsSpent = 0, lastCoins = nil, lastEnergy = nil, lastGems = nil, claimedFreeOffers = {}, currentStatus = "Ready", vendorActive = false, vendorBuyCounts = {}, vendorLocalConsumed = {}, vendorLocalConsumedAt = {}, vendorServerBought = {}, vendorRefreshAfterDrag = false, nearbyPickups = {}, vendorOfferKeys = {}, recentServers = {}, openDropdown = nil, lastPickupScan = 0, lastOrderRemote = 0, lastOrderUiScan = 0, lastOrderCycle = 0, lastOrderRemoteBatch = 0, lastOrderFallbackScan = 0, lastOrderHash = nil, lastOrderDemandBuild = 0, cachedOrderDemand = nil, lastOrderKeysBuild = 0, cachedOrderKeys = nil, orderServeCursor = 1, orderServeStage = 0, lastOrderRemoteAttempt = 0, lastOrderRemoteSuccess = 0, lastOrderUiFallbackScan = 0, lastBubbleGridScan = 0, lastGridCycleStarted = 0, lastGridCycleDuration = 0, lastHeavyAutomationYield = 0, lastStatsUpdate = 0, lastRuntimeSave = 0, runtimeCheckpoint = tick(), lastObby = 0, lastDailySweep = 0, dailySweepCount = 0, dailyCursor = 1, obbyBusy = false, currentBuild = nil, lastAfk = 0, afkConn = nil, afkToken = nil, connections = {}, dragStop = nil, dragActive = false, lastUiInteraction = 0, closed = false, } local UI = { screen = nil, main = nil, clip = nil, sidebar = nil, content = nil, pages = {}, tabs = {}, themed = {}, fonts = {}, stats = {}, feed = {}, notifications = {}, vendorDropdown = nil, vendorStatusDot = nil, infoOverlay = nil, } local Data = { LocalData = nil, Items = {}, InventoryItems = {}, CurrencyInfo = {}, GridSize = 60, BuildConfig = nil, MerchantInfo = nil, ReplicaSignal = nil, ReplicaID = nil, ReplicaIDs = {}, ReplicaIDsSeenAt = {}, Events = {}, buttons = {}, labels = {}, cacheTime = 0, } local Limiter = { nextAt = {} } local Daily = {} local Stats = {} local REMOTE_INTERVALS = { ["MergeGrid.Collect"] = 0.06, ["MergeGrid.Tap"] = 0.08, ["MergeGrid.TapAutoGen"] = 0.08, ["MergeGrid.Merge"] = 0.018, ["MergeGrid.InboxRetrieve"] = 0.25, ["MergeGrid.BreakBubble"] = 0.08, ["Orders.Serve"] = 0.08, ["Orders.Complete"] = 0.08, ["Order.Serve"] = 0.08, } local LOOP = { grid = 0.45, bubbles = 1.40, pickup = 1.80, build = 1.25, spin = 1.25, daily = 1.0, obby = 3.5, vendor = 3.5, walkSpeed = 2.0, } local function speedInterval(speed, slow, fast) speed = math.clamp(tonumber(speed) or 5, 1, 10) local alpha = (speed - 1) / 9 return slow - ((slow - fast) * alpha) end local Perf = { busyUntil = 0, lastHeavyWork = 0, lastMergeFrame = 0, lastOrdersFrame = 0, } local function markHeavyWork(duration) duration = tonumber(duration) or 0 if duration > 0.10 then Perf.busyUntil = math.max(Perf.busyUntil, tick() + 0.12) elseif duration > 0.075 then Perf.busyUntil = math.max(Perf.busyUntil, tick() + 0.06) end end local function performanceBusy() return tick() < (Perf.busyUntil or 0) end local REFRESH_CACHE_INTERVAL = 8.0 local REFRESH_CACHE_MAX_OBJECTS = 1500 local REFRESH_CACHE_MAX_BUTTONS = 100 local REFRESH_CACHE_MAX_LABELS = 150 local refreshVendorDropdown local scheduleVendorDropdownRefresh function Stats.loadSettings() local loadedHasVersion = false local loadedVersion = nil pcall(function() local fileName = nil if isfile and isfile(CONFIG_FILE) then fileName = CONFIG_FILE elseif isfile and isfile(LEGACY_CONFIG_FILE) then fileName = LEGACY_CONFIG_FILE end if fileName then local ok, decoded = pcall(Services.HttpService.JSONDecode, Services.HttpService, readfile(fileName)) if ok and type(decoded) == "table" then loadedHasVersion = decoded.settingsVersion ~= nil loadedVersion = decoded.settingsVersion for k, v in pairs(decoded) do Settings[k] = v end end end end) if (not loadedHasVersion) or loadedVersion ~= SCRIPT_VERSION then Settings.themeIndex = 2 Settings.uiFontIndex = fontIndex(DEFAULT_FONT_NAME) Settings.uiFontSize = DEFAULT_FONT_SIZE Settings.transparencyMode = "Both" Settings.transparency = true Settings.walkSpeed = 50 Settings.obstacleDifficulty = "Hard" end Settings.settingsVersion = SCRIPT_VERSION if not Settings.firstJoin then Settings.firstJoin = os.time() end Settings.uiFontIndex = tonumber(Settings.uiFontIndex) or fontIndex(DEFAULT_FONT_NAME) Settings.uiFontSize = math.clamp(tonumber(Settings.uiFontSize) or DEFAULT_FONT_SIZE, 1, 100) if Settings.uiFontIndex < 1 or Settings.uiFontIndex > #FONT_LIST then Settings.uiFontIndex = fontIndex(DEFAULT_FONT_NAME) end if Settings.themeIndex < 1 or Settings.themeIndex > #THEMES then Settings.themeIndex = 2 end if Settings.infoSizeMode == "Compact" then Settings.infoSizeMode = "Small" end if Settings.infoSizeMode == "Normal" or Settings.infoSizeMode == "Wide" then Settings.infoSizeMode = "Medium" end if Settings.infoSizeMode == "Full" then Settings.infoSizeMode = "Extra Large" end if not ({ Small = true, Medium = true, Large = true, ["Extra Large"] = true })[Settings.infoSizeMode] then Settings.infoSizeMode = "Medium" end Settings.infoSizeScale = math.clamp(tonumber(Settings.infoSizeScale) or 100, 85, 120) Settings.playerFilter = nil Settings.customMaxPlayers = math.clamp(tonumber(Settings.customMaxPlayers) or 6, 1, 6) Settings.mergeSpeed = math.clamp(tonumber(Settings.mergeSpeed) or 5, 1, 10) Settings.orderSpeed = math.clamp(tonumber(Settings.orderSpeed) or 5, 1, 10) Settings.uiSize = math.clamp(tonumber(Settings.uiSize) or 10, 1, 20) if type(Settings.recentServerIds) ~= "table" then Settings.recentServerIds = {} end if type(Settings.statProfiles) ~= "table" then Settings.statProfiles = {} end if type(Settings.vendorQuantities) ~= "table" then Settings.vendorQuantities = {} end State.recentServers = Settings.recentServerIds if game.JobId and game.JobId ~= "" then State.recentServers[game.JobId] = os.time() end if not TRANSPARENCY_SET[Settings.transparencyMode] then Settings.transparencyMode = Settings.transparency and "Both" or "None" end Settings.transparency = Settings.transparencyMode ~= "None" State.theme = clone(THEMES[Settings.themeIndex].colors) State.uiFont = resolveFont(FONT_LIST[Settings.uiFontIndex]) State.uiFontSize = Settings.uiFontSize State.firstJoin = Settings.firstJoin State.vendorActive = Settings.vendorActive and true or false end function Stats.saveSettings() pcall(function() if writefile then writefile(CONFIG_FILE, Services.HttpService:JSONEncode(Settings)) end end) end function Stats.queueSave(delaySeconds) if State.settingsSaveQueued then return end State.settingsSaveQueued = true task.delay(delaySeconds or 2.0, function() State.settingsSaveQueued = false Stats.saveSettings() end) end function Stats.markDirty() State.statsDirty = true local now = tick() if now - (State.lastStatSave or 0) >= 5.0 then State.lastStatSave = now State.statsDirty = false Stats.saveSettings() else Stats.queueSave(2.0) end end function Stats.userProfile() local key = player and tostring(player.UserId) or "local" Settings.statProfiles = type(Settings.statProfiles) == "table" and Settings.statProfiles or {} local profile = Settings.statProfiles[key] if type(profile) ~= "table" then profile = { runtimeSeconds = 0, coinsSpent = 0, energySpent = 0, gemsSpent = 0 } Settings.statProfiles[key] = profile end profile.runtimeSeconds = tonumber(profile.runtimeSeconds) or 0 profile.coinsSpent = tonumber(profile.coinsSpent) or 0 profile.energySpent = tonumber(profile.energySpent) or 0 profile.gemsSpent = tonumber(profile.gemsSpent) or 0 return profile end function Stats.commitPendingSpent(force) local profile = Stats.userProfile() local pending = State.pendingSpent if type(pending) ~= "table" then return false end local changed = false for key, item in pairs(pending) do if type(item) == "table" and item.spentKey and item.amount and (force or item.confirmed) then local amount = math.max(0, tonumber(item.amount) or 0) if amount > 0 then profile[item.spentKey] = (tonumber(profile[item.spentKey]) or 0) + amount changed = true end if item.lastKey then State[item.lastKey] = tonumber(item.to) or State[item.lastKey] end pending[key] = nil end end if changed then Stats.markDirty() end return changed end function Stats.schedulePendingSpentCommit(spentKey, item) task.delay(1.25, function() if State.closed then return end local pending = State.pendingSpent if type(pending) ~= "table" or pending[spentKey] ~= item then return end item.confirmed = true Stats.commitPendingSpent(false) end) end function Stats.saveRuntimeProgress(force) if force then Stats.commitPendingSpent(true) end local profile = Stats.userProfile() local now = tick() local elapsed = math.max(0, now - (State.runtimeCheckpoint or now)) if elapsed > 0 then profile.runtimeSeconds = profile.runtimeSeconds + elapsed State.runtimeCheckpoint = now State.statsDirty = true end if force or now - (State.lastRuntimeSave or 0) >= 10 or (State.statsDirty and now - (State.lastStatSave or 0) >= 5) then State.lastRuntimeSave = now State.lastStatSave = now State.statsDirty = false Stats.saveSettings() elseif State.statsDirty then Stats.queueSave(2.0) end end function Stats.formatTimeSpan(seconds) local n = math.max(0, math.floor(tonumber(seconds) or 0)) if n >= 86400 then local days = math.floor(n / 86400) local hours = math.floor((n % 86400) / 3600) return tostring(days) .. "d " .. tostring(hours) .. "h" elseif n >= 3600 then local hours = math.floor(n / 3600) local minutes = math.floor((n % 3600) / 60) return tostring(hours) .. "h " .. tostring(minutes) .. "m" end return tostring(math.floor(n / 60)) .. "m" end local function currentServerAgeSeconds() local ok, value = pcall(function() return Services.Workspace.DistributedGameTime end) value = ok and tonumber(value) or nil if value and value > 0 then return value end return tick() - State.sessionStart end UI.activeMainTweens = {} UI.activeSidebarTweens = {} function UI.removeActiveMainTween(tweenObject) for index = #UI.activeMainTweens, 1, -1 do if UI.activeMainTweens[index] == tweenObject then table.remove(UI.activeMainTweens, index) return end end end function UI.cancelMainTweens() for index = #UI.activeMainTweens, 1, -1 do local tweenObject = UI.activeMainTweens[index] table.remove(UI.activeMainTweens, index) pcall(function() tweenObject:Cancel() end) end end function UI.removeActiveSidebarTween(tweenObject) for index = #UI.activeSidebarTweens, 1, -1 do if UI.activeSidebarTweens[index] == tweenObject then table.remove(UI.activeSidebarTweens, index) return end end end function UI.trackSidebarTween(tweenObject) if not tweenObject then return nil end UI.activeSidebarTweens[#UI.activeSidebarTweens + 1] = tweenObject tweenObject.Completed:Connect(function() UI.removeActiveSidebarTween(tweenObject) end) return tweenObject end function UI.cancelSidebarTweens() for index = #UI.activeSidebarTweens, 1, -1 do local tweenObject = UI.activeSidebarTweens[index] table.remove(UI.activeSidebarTweens, index) pcall(function() tweenObject:Cancel() end) end end function UI.cancelAllLayoutTweens() UI.cancelMainTweens() UI.cancelSidebarTweens() end UI.pendingVisualRefreshAfterDrag = false UI.activeTweensByObject = setmetatable({}, { __mode = "k" }) function UI.tween(obj, time, props, style, dir) if not obj then return nil end -- OPTIMIZATION: Skip all TweenService tweens during drag if State.dragActive then if obj == UI.main and props and props.Position then obj.Position = props.Position else UI.pendingVisualRefreshAfterDrag = true end return nil end -- OPTIMIZATION: Cancel previous tween on same object for smoother animations local prevTween = UI.activeTweensByObject[obj] if prevTween then pcall(function() prevTween:Cancel() end) UI.activeTweensByObject[obj] = nil end local tweenInfo = TweenInfo.new( time or 0.15, style or Enum.EasingStyle.Quad, dir or Enum.EasingDirection.Out ) local tweenObject = Services.TweenService:Create(obj, tweenInfo, props) if UI and obj == UI.main then UI.activeMainTweens[#UI.activeMainTweens + 1] = tweenObject tweenObject.Completed:Connect(function() UI.removeActiveMainTween(tweenObject) UI.activeTweensByObject[obj] = nil end) else tweenObject.Completed:Connect(function() UI.activeTweensByObject[obj] = nil end) end UI.activeTweensByObject[obj] = tweenObject tweenObject:Play() return tweenObject end function UI.addCorner(parent, radius) local c = Instance.new("UICorner") c.CornerRadius = UDim.new(0, radius or 8) c.Parent = parent return c end function UI.addPadding(parent, top, right, bottom, left) local p = Instance.new("UIPadding") p.PaddingTop = UDim.new(0, top or 0) p.PaddingRight = UDim.new(0, right or 0) p.PaddingBottom = UDim.new(0, bottom or 0) p.PaddingLeft = UDim.new(0, left or 0) p.Parent = parent return p end function UI.regTheme(inst, role, area, stateFn) UI.themed[#UI.themed + 1] = { inst = inst, role = role, area = area, stateFn = stateFn } end function UI.regFont(inst, sizeOverride) if inst and (inst:IsA("TextLabel") or inst:IsA("TextButton") or inst:IsA("TextBox")) then UI.fonts[#UI.fonts + 1] = { inst = inst, size = sizeOverride } end end function UI.addStroke(parent, transparency) local s = Instance.new("UIStroke") s.Color = State.theme.border s.Thickness = 1 s.Transparency = transparency or 0.55 s.Parent = parent UI.regTheme(s, "border") return s end function UI.trackConnection(conn) if conn then State.connections[#State.connections + 1] = conn end return conn end function UI.removeTrackedConnection(conn) for index = #State.connections, 1, -1 do if State.connections[index] == conn then table.remove(State.connections, index) return end end end function UI.disconnectAll() for _, conn in ipairs(State.connections) do pcall(function() if conn and conn.Disconnect then conn:Disconnect() end end) end State.connections = {} if State.openDropdown and State.openDropdown.close then pcall(function() State.openDropdown.close() end) State.openDropdown = nil end if State.afkConn then pcall(function() State.afkConn:Disconnect() end) State.afkConn = nil end State.afkToken = nil if State.dragStop then pcall(State.dragStop) end end function UI.shutdown(reason, destroyUi) if State.closed then return end State.closed = true State.running = false -- FIX: reset player settings when UI closes if player and player.Character then local hum = player.Character:FindFirstChild("Humanoid") if hum and Settings.walkSpeed ~= 16 then pcall(function() hum.WalkSpeed = 16 end) end end -- FIX: stop all active vendor processes State.vendorActive = false Settings.vendorActive = false pcall(function() Stats.saveRuntimeProgress(true) end) UI.disconnectAll() State.nearbyPickups = {} if UI.screen and UI.screen.Parent and destroyUi ~= false then pcall(function() UI.screen:Destroy() end) end pcall(function() if _G and _G.MergeShopRunId == RUN_ID then _G.MergeShopShutdown = nil _G.MergeShopRunId = nil _G.DevToolsPanelShutdown = nil _G.DevToolsPanelRunId = nil end end) end pcall(function() if _G then _G.MergeShopRunId = RUN_ID _G.MergeShopShutdown = function(reason) UI.shutdown(reason, true) end _G.DevToolsPanelRunId = RUN_ID _G.DevToolsPanelShutdown = _G.MergeShopShutdown end end) function UI.areaTransparent(area) local mode = Settings.transparencyMode or "None" return mode == "Both" or mode == area end function UI.transparencyFor(role, area) if role == "main" then return Settings.transparencyMode ~= "None" and 0.26 or 0 end if role == "panel" then return UI.areaTransparent(area) and 0.38 or 0 end if role == "row" then return UI.areaTransparent(area) and 0.22 or 0 end if role == "tabActive" then return UI.areaTransparent("Sidebar") and 0.30 or 0 end if role == "tab" then return 1 end if role == "button" then return UI.areaTransparent(area) and 0.16 or 0 end return nil end function UI.applyTheme() local c = State.theme for _, item in ipairs(UI.themed) do local inst, role, area, stateFn = item.inst, item.role, item.area, item.stateFn if inst and inst.Parent then if role == "main" then inst.BackgroundColor3 = c.bg elseif role == "panel" then inst.BackgroundColor3 = c.panel elseif role == "row" then inst.BackgroundColor3 = c.panelInner elseif role == "button" then inst.BackgroundColor3 = c.panelInner elseif role == "hover" then inst.BackgroundColor3 = c.panelHover elseif role == "solidRow" then inst.BackgroundColor3 = c.panelInner elseif role == "solidHover" then inst.BackgroundColor3 = c.panelHover elseif role == "border" then inst.Color = c.border elseif role == "line" then inst.BackgroundColor3 = c.border elseif role == "accent" then inst.BackgroundColor3 = c.accent elseif role == "text" and inst:IsA("GuiObject") then inst.TextColor3 = c.text elseif role == "sub" and inst:IsA("GuiObject") then inst.TextColor3 = c.textSub elseif role == "tlRed" then inst.BackgroundColor3 = c.tlRed elseif role == "tlYellow" then inst.BackgroundColor3 = c.tlYellow elseif role == "tlGreen" then inst.BackgroundColor3 = c.tlGreen elseif role == "togglePill" then inst.BackgroundColor3 = stateFn() and c.accent or c.panelHover elseif role == "toggleKnob" then inst.BackgroundColor3 = stateFn() and c.accentText or c.knob end local t = UI.transparencyFor(role, area) if t and inst:IsA("GuiObject") then inst.BackgroundTransparency = t end end end for name, tab in pairs(UI.tabs) do local active = name == State.activePage if tab.button then tab.button.BackgroundTransparency = active and UI.transparencyFor("tabActive", "Sidebar") or 1 tab.button.BackgroundColor3 = State.theme.panelHover end if tab.indicator then tab.indicator.BackgroundTransparency = active and 0 or 1 end if tab.label then tab.label.TextColor3 = active and c.text or c.textSub end if tab.icon then if tab.textIcon then tab.icon.TextColor3 = active and c.accent or c.textSub else tab.icon.ImageColor3 = active and c.accent or c.textSub end end end end function UI.setSidebarCollapsed(value, animate) if not UI.sidebar or not UI.content then return end State.sbCollapsed = value and true or false local sidebarWidth = State.sbCollapsed and SIDE_COL or SIDE_EXP local time = animate and 0.22 or 0 local sidebarTween = UI.tween(UI.sidebar, time, { Size = UDim2.new(0, sidebarWidth, 1, -(TITLEBAR_H + TOP_MARGIN + MARGIN)), }) local contentTween = UI.tween(UI.content, time, { Size = UDim2.new(1, -(sidebarWidth + MARGIN + GAP + MARGIN), 1, -(TITLEBAR_H + TOP_MARGIN + MARGIN)), Position = UDim2.new(0, MARGIN + sidebarWidth + GAP, 0, TITLEBAR_H + TOP_MARGIN), }) UI.trackSidebarTween(sidebarTween) UI.trackSidebarTween(contentTween) for _, tab in pairs(UI.tabs) do if tab.label then UI.tween(tab.label, time, { TextTransparency = State.sbCollapsed and 1 or 0 }) end if tab.icon then UI.tween(tab.icon, time, { Position = State.sbCollapsed and UDim2.new(0.5, -7, 0.5, -7) or UDim2.new(0, 8, 0.5, -7), }) end end end function UI.applyFont() for _, item in ipairs(UI.fonts) do if item.inst and item.inst.Parent then pcall(function() item.inst.Font = State.uiFont if not item.size then item.inst.TextSize = State.uiFontSize end end) end end end function UI.parentScreen(screen) local ok = false if gethui then ok = pcall(function() screen.Parent = gethui() end) end if not ok and PlayerGui then ok = pcall(function() screen.Parent = PlayerGui end) end if not ok then pcall(function() screen.Parent = Services.CoreGui end) end if not screen.Parent and player then task.spawn(function() local pg = player:WaitForChild("PlayerGui", 3) if pg and not screen.Parent then PlayerGui = pg pcall(function() screen.Parent = pg end) end end) end end function UI.showNotification(title, sub, dur) if not UI.screen or not UI.screen.Parent then return end local w, h = 260, 54 local note = Instance.new("Frame") note.Size = UDim2.new(0, w, 0, h) note.Position = UDim2.new(1, 20, 1, -h - 14) note.BackgroundColor3 = State.theme.panel note.BackgroundTransparency = Settings.transparencyMode ~= "None" and 0.18 or 0 note.BorderSizePixel = 0 note.ZIndex = 500 note.Parent = UI.screen UI.addCorner(note, 10) UI.addStroke(note, 0.45) UI.regTheme(note, "panel", "Content") local bar = Instance.new("Frame") bar.Size = UDim2.new(0, 3, 1, -12) bar.Position = UDim2.new(0, 6, 0, 6) bar.BackgroundColor3 = State.theme.accent bar.BorderSizePixel = 0 bar.ZIndex = 501 bar.Parent = note UI.addCorner(bar, 2) UI.regTheme(bar, "accent") local tl = Instance.new("TextLabel") tl.Size = UDim2.new(1, -22, 0, 18) tl.Position = UDim2.new(0, 18, 0, 8) tl.BackgroundTransparency = 1 tl.Text = title tl.Font = Enum.Font.GothamBold tl.TextSize = 12 tl.TextColor3 = State.theme.text tl.TextXAlignment = Enum.TextXAlignment.Left tl.TextTruncate = Enum.TextTruncate.AtEnd tl.ZIndex = 501 tl.Parent = note UI.regTheme(tl, "text") local sl = Instance.new("TextLabel") sl.Size = UDim2.new(1, -22, 0, 14) sl.Position = UDim2.new(0, 18, 0, 28) sl.BackgroundTransparency = 1 sl.Text = sub or "" sl.Font = Enum.Font.GothamMedium sl.TextSize = 10 sl.TextColor3 = State.theme.textSub sl.TextXAlignment = Enum.TextXAlignment.Left sl.TextTruncate = Enum.TextTruncate.AtEnd sl.ZIndex = 501 sl.Parent = note UI.regTheme(sl, "sub") table.insert(UI.notifications, 1, note) for i, n in ipairs(UI.notifications) do UI.tween(n, 0.2, { Position = UDim2.new(1, -(w + 14), 1, -(h + 8) * i) }) end task.delay(dur or 3, function() for i, n in ipairs(UI.notifications) do if n == note then table.remove(UI.notifications, i) break end end if note and note.Parent then UI.tween(note, 0.22, { Position = UDim2.new(1, 20, 1, note.Position.Y.Offset) }) end task.delay(0.25, function() if note and note.Parent then note:Destroy() end end) for i, n in ipairs(UI.notifications) do UI.tween(n, 0.2, { Position = UDim2.new(1, -(w + 14), 1, -(h + 8) * i) }) end end) end function UI.sectionTitle(parent, text) local label = Instance.new("TextLabel") label.Size = UDim2.new(1, 0, 0, 18) label.BackgroundTransparency = 1 label.Text = text label.Font = Enum.Font.GothamBold label.TextSize = 10 label.TextColor3 = State.theme.textSub label.TextXAlignment = Enum.TextXAlignment.Left label.Parent = parent UI.regTheme(label, "sub") return label end function UI.separator(parent) local line = Instance.new("Frame") line.Size = UDim2.new(1, -20, 0, 1) line.Position = UDim2.new(0, 10, 0, 0) line.BackgroundColor3 = State.theme.border line.BackgroundTransparency = 0.65 line.BorderSizePixel = 0 line.Parent = parent UI.regTheme(line, "line") end function UI.rowBase(parent, area, height) local row = Instance.new("Frame") row.Size = UDim2.new(1, 0, 0, height or 36) row.BackgroundColor3 = State.theme.panelInner row.BorderSizePixel = 0 row.Parent = parent UI.addCorner(row, 8) UI.addStroke(row, 0.72) UI.regTheme(row, "row", area) return row end function UI.createButton(parent, text, callback) local btn = Instance.new("TextButton") btn.Size = UDim2.new(1, 0, 0, 28) btn.BackgroundColor3 = State.theme.panelInner btn.BorderSizePixel = 0 btn.Text = text btn.Font = Enum.Font.GothamBold btn.TextSize = 11 btn.TextColor3 = State.theme.text btn.AutoButtonColor = false btn.Parent = parent UI.addCorner(btn, 6) UI.addStroke(btn, 0.72) UI.regTheme(btn, "button", "Content") UI.regTheme(btn, "text") btn.MouseEnter:Connect(function() UI.tween(btn, 0.1, { BackgroundColor3 = State.theme.panelHover }) end) btn.MouseLeave:Connect(function() UI.tween(btn, 0.1, { BackgroundColor3 = State.theme.panelInner }) end) btn.MouseButton1Click:Connect(function() if callback then callback() end end) return btn end function UI.createToggle(parent, text, default, callback) local state = default and true or false local row = UI.rowBase(parent, "Content", 28) local label = Instance.new("TextLabel") label.Size = UDim2.new(1, -46, 1, 0) label.Position = UDim2.new(0, 8, 0, 0) label.BackgroundTransparency = 1 label.Text = text label.Font = State.uiFont label.TextSize = State.uiFontSize label.TextColor3 = State.theme.text label.TextXAlignment = Enum.TextXAlignment.Left label.TextTruncate = Enum.TextTruncate.AtEnd label.Parent = row UI.regTheme(label, "text") UI.regFont(label) local pill = Instance.new("Frame") pill.Size = UDim2.new(0, 28, 0, 15) pill.Position = UDim2.new(1, -36, 0.5, -7.5) pill.BorderSizePixel = 0 pill.Parent = row UI.addCorner(pill, 8) local knob = Instance.new("Frame") knob.Size = UDim2.new(0, 11, 0, 11) knob.BorderSizePixel = 0 knob.Parent = pill UI.addCorner(knob, 6) local hit = Instance.new("TextButton") hit.Size = UDim2.new(1, 0, 1, 0) hit.BackgroundTransparency = 1 hit.Text = "" hit.Parent = pill local function draw(animated) local pos = state and UDim2.new(1, -13, 0.5, -5.5) or UDim2.new(0, 2, 0.5, -5.5) local pc = state and State.theme.accent or State.theme.panelHover local kc = state and State.theme.accentText or State.theme.knob if animated then UI.tween(pill, 0.16, { BackgroundColor3 = pc }) UI.tween(knob, 0.16, { Position = pos, BackgroundColor3 = kc }) else pill.BackgroundColor3 = pc knob.BackgroundColor3 = kc knob.Position = pos end end local function set(v, quiet) state = v and true or false draw(true) if callback and not quiet then callback(state) end end hit.MouseButton1Click:Connect(function() set(not state) end) UI.regTheme(pill, "togglePill", nil, function() return state end) UI.regTheme(knob, "toggleKnob", nil, function() return state end) draw(false) return row, function() return state end, set end function UI.createDropdown(parent, title, options, defaultIndex, callback) local current = math.clamp(defaultIndex or 1, 1, math.max(#options, 1)) local open = false local pointerHover = Services.UserInputService.MouseEnabled and not Services.UserInputService.TouchEnabled local row = UI.rowBase(parent, "Content", 28) row.ClipsDescendants = true row.ZIndex = 40 local label = Instance.new("TextLabel") label.Size = UDim2.new(0.48, -8, 0, 28) label.Position = UDim2.new(0, 8, 0, 0) label.BackgroundTransparency = 1 label.Text = title label.Font = State.uiFont label.TextSize = State.uiFontSize label.TextColor3 = State.theme.text label.TextXAlignment = Enum.TextXAlignment.Left label.TextTruncate = Enum.TextTruncate.AtEnd label.ZIndex = 61 label.Parent = row UI.regTheme(label, "text") UI.regFont(label) local trigger = Instance.new("TextButton") trigger.Size = UDim2.new(0, 125, 0, 20) trigger.Position = UDim2.new(1, -133, 0, 4) trigger.BackgroundColor3 = State.theme.panelHover trigger.BorderSizePixel = 0 trigger.Text = "" trigger.AutoButtonColor = false trigger.ZIndex = 62 trigger.Parent = row UI.addCorner(trigger, 5) UI.addStroke(trigger, 0.78) UI.regTheme(trigger, "hover", "Content") local value = Instance.new("TextLabel") value.Size = UDim2.new(1, -24, 1, 0) value.Position = UDim2.new(0, 6, 0, 0) value.BackgroundTransparency = 1 value.Text = options[current] or "" value.Font = State.uiFont value.TextSize = State.uiFontSize value.TextColor3 = State.theme.text value.TextXAlignment = Enum.TextXAlignment.Left value.TextTruncate = Enum.TextTruncate.AtEnd value.ZIndex = 63 value.Parent = trigger UI.regTheme(value, "text") UI.regFont(value) local arrow = Instance.new("TextLabel") arrow.Size = UDim2.new(0, 16, 1, 0) arrow.Position = UDim2.new(1, -17, 0, 0) arrow.BackgroundTransparency = 1 arrow.Text = "v" arrow.Font = Enum.Font.GothamBold arrow.TextSize = 9 arrow.TextColor3 = State.theme.textSub arrow.ZIndex = 63 arrow.Parent = trigger UI.regTheme(arrow, "sub") local menuScroll = Instance.new("ScrollingFrame") menuScroll.Size = UDim2.new(1, -12, 0, 0) menuScroll.Position = UDim2.new(0, 6, 0, 30) menuScroll.BackgroundColor3 = State.theme.panelInner menuScroll.BackgroundTransparency = 0 menuScroll.BorderSizePixel = 0 menuScroll.ScrollBarThickness = 0 menuScroll.ScrollBarImageTransparency = 1 menuScroll.ScrollingDirection = Enum.ScrollingDirection.Y menuScroll.ClipsDescendants = true menuScroll.ZIndex = 62 menuScroll.Parent = row UI.addCorner(menuScroll, 6) UI.regTheme(menuScroll, "solidRow") local menuStroke = Instance.new("UIStroke") menuStroke.Color = State.theme.border menuStroke.Thickness = 1 menuStroke.Transparency = 0.5 menuStroke.Parent = menuScroll UI.regTheme(menuStroke, "border") UI.addPadding(menuScroll, 4, 5, 4, 5) local api = { quantityInputs = false, optionMeta = nil, } local function markUiInteraction() State.lastUiInteraction = tick() end local function currentMouseAbsolutePosition() return adjustedMousePosition() end local function pointInsideGuiObject(guiObj, point) if not guiObj then return false end if not guiObj.Parent then return false end local position = guiObj.AbsolutePosition local size = guiObj.AbsoluteSize local left = position.X local right = position.X + size.X local top = position.Y local bottom = position.Y + size.Y if point.X < left then return false end if point.X > right then return false end if point.Y < top then return false end if point.Y > bottom then return false end return true end local function buttonContainsPoint(button, point) if not button then return false end if not button.Parent then return false end local position = button.AbsolutePosition local size = button.AbsoluteSize local left = position.X local right = position.X + size.X local top = position.Y local bottom = position.Y + size.Y if point.X < left then return false end if point.X > right then return false end if point.Y < top then return false end if point.Y > bottom then return false end return true end local function disconnectPointerMoveConnection() local connection = api.pointerMoveConnection if not connection then return end api.pointerMoveConnection = nil UI.removeTrackedConnection(connection) pcall(function() connection:Disconnect() end) end local function connectPointerMoveConnection() if not pointerHover then return end if api.pointerMoveConnection then return end api.pointerMoveConnection = UI.trackConnection(Services.UserInputService.InputChanged:Connect(function(input) if input.UserInputType ~= Enum.UserInputType.MouseMovement then return end markUiInteraction() if api.recheckPointerHover then api.recheckPointerHover() end end)) end local menuLayout = Instance.new("UIListLayout") menuLayout.SortOrder = Enum.SortOrder.LayoutOrder menuLayout.Padding = UDim.new(0, 3) menuLayout.Parent = menuScroll local menuSizeScheduled = false menuLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function() if menuSizeScheduled then return end menuSizeScheduled = true task.defer(function() menuSizeScheduled = false menuScroll.CanvasSize = UDim2.new(0, 0, 0, menuLayout.AbsoluteContentSize.Y + 8) end) end) UI.trackConnection(menuScroll:GetPropertyChangedSignal("CanvasPosition"):Connect(function() if not pointerHover then return end if not open then return end markUiInteraction() if api.recheckPointerHover then api.recheckPointerHover() end end)) local ddTweens = {} local function ddTween(obj, time, props, dir) if State.dragActive then for k, v in pairs(props or {}) do pcall(function() obj[k] = v end) end return nil end if ddTweens[obj] then pcall(function() ddTweens[obj]:Cancel() end) end if not (obj and obj.Parent) then return nil end local tweenObj = Services.TweenService:Create(obj, TweenInfo.new(time, Enum.EasingStyle.Sine, dir or Enum.EasingDirection.Out), props) ddTweens[obj] = tweenObj tweenObj:Play() return tweenObj end local function popupHeight() if type(options) ~= "table" then return 0 end return math.min(#options * (ITEM_H + 2) + 10, MAX_DD_H) end local function buildItems() if type(options) ~= "table" then options = {} end for _, child in ipairs(menuScroll:GetChildren()) do if child:IsA("TextButton") then child:Destroy() end end local refs = {} local currentRef = nil local function paint(ref, active) if not ref then return end if not ref.button then return end local bg local tx if active then bg = State.theme.accent tx = State.theme.accentText else bg = State.theme.panelInner tx = State.theme.text end if ref.button.BackgroundColor3 ~= bg then ref.button.BackgroundColor3 = bg end if ref.label and ref.label.TextColor3 ~= tx then ref.label.TextColor3 = tx end if ref.button.Text ~= "" and ref.button.TextColor3 ~= tx then ref.button.TextColor3 = tx end end local function paintStaticSelection() local staticRef = nil if not pointerHover then staticRef = currentRef end api.hoveredRef = nil for _, ref in ipairs(refs) do local isActive = ref == staticRef paint(ref, isActive) end end local function paintHoveredRef(targetRef) if not pointerHover then return end if api.hoveredRef == targetRef then return end markUiInteraction() api.hoveredRef = targetRef for _, ref in ipairs(refs) do local isActive = ref == targetRef paint(ref, isActive) end end local function hoveredRefFromMouse() if not pointerHover then return nil end local mousePosition = currentMouseAbsolutePosition() if not pointInsideGuiObject(menuScroll, mousePosition) then return nil end for _, ref in ipairs(refs) do if ref.button and ref.button.Parent then local buttonCenter = ref.button.AbsolutePosition + (ref.button.AbsoluteSize * 0.5) if pointInsideGuiObject(menuScroll, buttonCenter) and buttonContainsPoint(ref.button, mousePosition) then return ref end end end return nil end local function recheckPointerHover() if not pointerHover then return end if not open then return end if not menuScroll or not menuScroll.Parent then return end local mousePosition = currentMouseAbsolutePosition() if not pointInsideGuiObject(menuScroll, mousePosition) then paintStaticSelection() return end local hoveredRef = hoveredRefFromMouse() if hoveredRef then paintHoveredRef(hoveredRef) return end paintStaticSelection() end for i, opt in ipairs(options) do local meta = api.optionMeta and api.optionMeta[opt] or nil local richRow = api.quantityInputs and meta ~= nil local withQuantity = richRow and meta.quantityInput ~= false local selected = (not pointerHover) and i == current local btn = Instance.new("TextButton") btn.Size = UDim2.new(1, 0, 0, 22) btn.BackgroundColor3 = selected and State.theme.accent or State.theme.panelInner btn.BackgroundTransparency = 0 btn.BorderSizePixel = 0 btn.Text = richRow and "" or opt btn.Font = State.uiFont btn.TextSize = State.uiFontSize btn.TextColor3 = selected and State.theme.accentText or State.theme.text btn.TextTransparency = 0 btn.TextXAlignment = Enum.TextXAlignment.Left btn.AutoButtonColor = false btn.ZIndex = 63 btn.Parent = menuScroll UI.addCorner(btn, 4) UI.addPadding(btn, 0, 5, 0, 5) local ref = { button = btn, meta = meta, } if i == current then currentRef = ref end local qtyBox if richRow then local leftInset = meta.icon and 28 or 5 if meta.icon then local icon = Instance.new("ImageLabel") icon.Size = UDim2.new(0, 18, 0, 18) icon.Position = UDim2.new(0, 4, 0.5, -9) icon.BackgroundTransparency = 1 icon.Image = meta.icon icon.ScaleType = Enum.ScaleType.Fit icon.ZIndex = 64 icon.Parent = btn end local optLabel = Instance.new("TextLabel") optLabel.Size = UDim2.new(1, withQuantity and -(leftInset + 58) or -leftInset, 1, 0) optLabel.Position = UDim2.new(0, leftInset, 0, 0) optLabel.BackgroundTransparency = 1 optLabel.Text = opt optLabel.Font = State.uiFont optLabel.TextSize = State.uiFontSize optLabel.TextColor3 = selected and State.theme.accentText or State.theme.text optLabel.TextXAlignment = Enum.TextXAlignment.Left optLabel.TextTruncate = Enum.TextTruncate.AtEnd optLabel.ZIndex = 64 optLabel.Parent = btn ref.label = optLabel if withQuantity then qtyBox = Instance.new("TextBox") qtyBox.Size = UDim2.new(0, 44, 0, 17) qtyBox.Position = UDim2.new(1, -47, 0.5, -8.5) qtyBox.BackgroundColor3 = State.theme.panelHover qtyBox.BorderSizePixel = 0 qtyBox.ClearTextOnFocus = false qtyBox.PlaceholderText = "" qtyBox.Text = meta.key and Settings.vendorQuantities and Settings.vendorQuantities[meta.key] and tostring(Settings.vendorQuantities[meta.key]) or "" qtyBox.Font = Enum.Font.GothamBold qtyBox.TextSize = 10 qtyBox.TextColor3 = State.theme.text qtyBox.PlaceholderColor3 = State.theme.textSub qtyBox.TextXAlignment = Enum.TextXAlignment.Center qtyBox.ZIndex = 65 qtyBox.Parent = btn ref.qtyBox = qtyBox UI.addCorner(qtyBox, 4) UI.addStroke(qtyBox, 0.82) UI.regTheme(qtyBox, "hover", "Content") UI.regTheme(qtyBox, "text") qtyBox.MouseEnter:Connect(function() paintHoveredRef(ref) end) qtyBox.FocusLost:Connect(function() local raw = tonumber(qtyBox.Text) if not raw or raw <= 0 then qtyBox.Text = "" if meta.key then Settings.vendorQuantities[meta.key] = nil end else local desired = math.max(1, math.floor(raw)) local maxQty = tonumber(meta.max) if maxQty and desired > maxQty then desired = maxQty UI.showNotification("Merchant", "Only " .. tostring(maxQty) .. " available", 2.6) end qtyBox.Text = tostring(desired) if meta.key then Settings.vendorQuantities[meta.key] = desired end end current = i currentRef = ref value.Text = opt api.lastSelectedMeta = meta if not pointerHover then paintStaticSelection() end if callback then callback(i, opt) end end) end end refs[#refs + 1] = ref btn.MouseEnter:Connect(function() paintHoveredRef(ref) end) btn.MouseLeave:Connect(function() if api.hoveredRef == ref then recheckPointerHover() end end) local function selectOption() current = i value.Text = opt api.close() api.lastSelectedMeta = meta if callback then callback(i, opt) end end if withQuantity then local selectHit = Instance.new("TextButton") selectHit.Size = UDim2.new(1, -54, 1, 0) selectHit.BackgroundTransparency = 1 selectHit.Text = "" selectHit.AutoButtonColor = false selectHit.ZIndex = 66 selectHit.Parent = btn selectHit.MouseEnter:Connect(function() paintHoveredRef(ref) end) selectHit.MouseButton1Click:Connect(selectOption) else btn.MouseButton1Click:Connect(selectOption) end end api.hoverRefs = refs api.hoveredRef = nil api.paintStaticSelection = paintStaticSelection api.findHoveredRef = hoveredRefFromMouse api.recheckPointerHover = recheckPointerHover paintStaticSelection() menuScroll.CanvasSize = UDim2.new(0, 0, 0, (#options * 25) + 8) end function api.commitQuantityInputs() if not api.hoverRefs then return end for _, ref in ipairs(api.hoverRefs) do if ref.qtyBox and ref.qtyBox.Parent and ref.meta and ref.meta.key then local raw = tonumber(ref.qtyBox.Text) if raw and raw > 0 then local desired = math.max(1, math.floor(raw)) local maxQty = tonumber(ref.meta.max) if maxQty and desired > maxQty then desired = maxQty end Settings.vendorQuantities[ref.meta.key] = desired ref.qtyBox.Text = tostring(desired) else Settings.vendorQuantities[ref.meta.key] = nil ref.qtyBox.Text = "" end end end end function api.commitSelectedQuantity() if not api.hoverRefs or not Settings.vendorSelectedItemKey then return end for _, ref in ipairs(api.hoverRefs) do if ref.meta and ref.meta.key == Settings.vendorSelectedItemKey and ref.qtyBox and ref.qtyBox.Parent then local raw = tonumber(ref.qtyBox.Text) if raw and raw > 0 then local desired = math.max(1, math.floor(raw)) local maxQty = tonumber(ref.meta.max) if maxQty and desired > maxQty then desired = maxQty end Settings.vendorQuantities[ref.meta.key] = desired ref.qtyBox.Text = tostring(desired) else Settings.vendorQuantities[ref.meta.key] = nil ref.qtyBox.Text = "" end break end end end function api.open() -- OPTIMIZATION: Do not open dropdown during drag if State.dragActive then return end if open then return end if State.openDropdown and State.openDropdown ~= api and State.openDropdown.close then State.openDropdown.close() end State.openDropdown = api open = true row.ZIndex = 60 row.BackgroundColor3 = State.theme.panelHover buildItems() connectPointerMoveConnection() local h = popupHeight() ddTween(row, 0.16, { Size = UDim2.new(1, 0, 0, 32 + h) }) ddTween(menuScroll, 0.16, { Size = UDim2.new(1, -12, 0, h) }) ddTween(arrow, 0.14, { Rotation = 180 }) row.BackgroundTransparency = 0 if api.recheckPointerHover then api.recheckPointerHover() end end function api.close() if not open then return end open = false disconnectPointerMoveConnection() api.hoveredRef = nil if State.openDropdown == api then State.openDropdown = nil end row.ZIndex = 40 row.BackgroundColor3 = State.theme.panelInner ddTween(row, 0.14, { Size = UDim2.new(1, 0, 0, 28) }, Enum.EasingDirection.InOut) ddTween(menuScroll, 0.14, { Size = UDim2.new(1, -12, 0, 0) }, Enum.EasingDirection.InOut) ddTween(arrow, 0.12, { Rotation = 0 }, Enum.EasingDirection.InOut) row.BackgroundTransparency = UI.transparencyFor("row", "Content") or 0 end function api.update(newOptions, newIndex) newOptions = newOptions or {} local newCurrent = math.clamp(newIndex or 1, 1, math.max(#newOptions, 1)) local changed = #newOptions ~= #options or newCurrent ~= current if not changed then for i, opt in ipairs(newOptions) do if options[i] ~= opt then changed = true break end end end options = newOptions current = newCurrent if value and value.Parent then value.Text = options[current] or "" end if open and changed then buildItems() local h = popupHeight() ddTween(row, 0.12, { Size = UDim2.new(1, 0, 0, 32 + h) }) ddTween(menuScroll, 0.12, { Size = UDim2.new(1, -12, 0, h) }) end end trigger.MouseButton1Click:Connect(function() if open then api.close() else api.open() end end) return row, api end function UI.createNumber(parent, title, default, minVal, maxVal, callback) local row = UI.rowBase(parent, "Content", 28) local label = Instance.new("TextLabel") label.Size = UDim2.new(1, -82, 1, 0) label.Position = UDim2.new(0, 8, 0, 0) label.BackgroundTransparency = 1 label.Text = title label.Font = State.uiFont label.TextSize = State.uiFontSize label.TextColor3 = State.theme.text label.TextXAlignment = Enum.TextXAlignment.Left label.Parent = row UI.regTheme(label, "text") UI.regFont(label) local box = Instance.new("TextBox") box.Size = UDim2.new(0, 62, 0, 20) box.Position = UDim2.new(1, -70, 0.5, -10) box.BackgroundColor3 = State.theme.panelHover box.BorderSizePixel = 0 box.Text = tostring(default) box.PlaceholderText = tostring(minVal) .. "-" .. tostring(maxVal) box.Font = Enum.Font.GothamBold box.TextSize = 11 box.TextColor3 = State.theme.text box.Parent = row UI.addCorner(box, 5) UI.addStroke(box, 0.72) UI.regTheme(box, "hover", "Content") UI.regTheme(box, "text") local lastValid = default box.FocusLost:Connect(function() local n = tonumber(box.Text) if not n then box.Text = tostring(lastValid) return end n = math.clamp(math.floor(n), minVal, maxVal) lastValid = n box.Text = tostring(n) if callback then callback(n) end end) return row end function UI.createSlider(parent, title, minVal, maxVal, default, callback) local value = math.clamp(tonumber(default) or minVal, minVal, maxVal) local row = UI.rowBase(parent, "Content", 40) local label = Instance.new("TextLabel") label.Size = UDim2.new(0.5, 0, 0, 17) label.Position = UDim2.new(0, 8, 0, 3) label.BackgroundTransparency = 1 label.Text = title label.Font = State.uiFont label.TextSize = State.uiFontSize label.TextColor3 = State.theme.text label.TextXAlignment = Enum.TextXAlignment.Left label.Parent = row UI.regTheme(label, "text") UI.regFont(label) local valueLabel = Instance.new("TextLabel") valueLabel.Size = UDim2.new(0.5, -8, 0, 17) valueLabel.Position = UDim2.new(0.5, 0, 0, 3) valueLabel.BackgroundTransparency = 1 valueLabel.Text = tostring(value) valueLabel.Font = Enum.Font.GothamBold valueLabel.TextSize = 10 valueLabel.TextColor3 = State.theme.textSub valueLabel.TextXAlignment = Enum.TextXAlignment.Right valueLabel.Parent = row UI.regTheme(valueLabel, "sub") local track = Instance.new("TextButton") track.Size = UDim2.new(1, -18, 0, 18) track.Position = UDim2.new(0, 9, 1, -20) track.BackgroundTransparency = 1 track.BorderSizePixel = 0 track.Text = "" track.AutoButtonColor = false track.Parent = row local rail = Instance.new("Frame") rail.Size = UDim2.new(1, 0, 0, 7) rail.Position = UDim2.new(0, 0, 0.5, -3.5) rail.BackgroundColor3 = State.theme.panelHover rail.BorderSizePixel = 0 rail.Parent = track UI.addCorner(rail, 4) UI.regTheme(rail, "hover", "Content") local fill = Instance.new("Frame") fill.Size = UDim2.new((value - minVal) / math.max(maxVal - minVal, 1), 0, 1, 0) fill.BackgroundColor3 = State.theme.accent fill.BorderSizePixel = 0 fill.Parent = rail UI.addCorner(fill, 4) UI.regTheme(fill, "accent") local knob = Instance.new("Frame") knob.Size = UDim2.new(0, 14, 0, 14) knob.BackgroundColor3 = State.theme.accent knob.BorderSizePixel = 0 knob.Parent = track UI.addCorner(knob, 7) UI.addStroke(knob, 0.55) UI.regTheme(knob, "accent") local dragging = false local function percentFromValue(sliderValue) local range = math.max(maxVal - minVal, 1) return math.clamp((sliderValue - minVal) / range, 0, 1) end local function percentFromInput(input) local trackX = track.AbsolutePosition.X local trackWidth = math.max(track.AbsoluteSize.X, 1) local pointerX = input.Position.X return math.clamp((pointerX - trackX) / trackWidth, 0, 1) end local function applySliderVisuals(percent) local clampedPercent = math.clamp(percent, 0, 1) fill.Size = UDim2.new(clampedPercent, 0, 1, 0) knob.Position = UDim2.new(clampedPercent, -7, 0.5, -7) end local function setFromInput(input, commit) local percent = percentFromInput(input) value = math.floor(minVal + (maxVal - minVal) * percent + 0.5) valueLabel.Text = tostring(value) applySliderVisuals(percent) if callback then callback(value, commit == true) end end local function beginSliderDrag(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then dragging = true setFromInput(input, false) end end applySliderVisuals(percentFromValue(value)) UI.trackConnection(track.InputBegan:Connect(beginSliderDrag)) UI.trackConnection(rail.InputBegan:Connect(beginSliderDrag)) UI.trackConnection(fill.InputBegan:Connect(beginSliderDrag)) UI.trackConnection(knob.InputBegan:Connect(beginSliderDrag)) UI.trackConnection(Services.UserInputService.InputChanged:Connect(function(input) if dragging and (input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch) then setFromInput(input, false) end end)) UI.trackConnection(Services.UserInputService.InputEnded:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then if dragging and callback then callback(value, true) end dragging = false end end)) return row end function UI.addFeed(text) if not UI.feedCard then return end table.insert(UI.feed, 1, text) while #UI.feed > 4 do table.remove(UI.feed) end for _, child in ipairs(UI.feedCard:GetChildren()) do if child:IsA("Frame") then child:Destroy() end end for _, line in ipairs(UI.feed) do local row = Instance.new("Frame") row.Size = UDim2.new(1, 0, 0, 13) row.BackgroundTransparency = 1 row.Parent = UI.feedCard local dot = Instance.new("Frame") dot.Size = UDim2.new(0, 3, 0, 3) dot.Position = UDim2.new(0, 0, 0.5, -1.5) dot.BackgroundColor3 = State.theme.accent dot.BorderSizePixel = 0 dot.Parent = row UI.addCorner(dot, 2) UI.regTheme(dot, "accent") local label = Instance.new("TextLabel") label.Size = UDim2.new(1, -8, 1, 0) label.Position = UDim2.new(0, 8, 0, 0) label.BackgroundTransparency = 1 label.Text = line label.Font = State.uiFont label.TextSize = 9 label.TextColor3 = State.theme.textSub label.TextXAlignment = Enum.TextXAlignment.Left label.TextTruncate = Enum.TextTruncate.AtEnd label.Parent = row UI.regTheme(label, "sub") UI.regFont(label, 9) end end function UI.createPage(name) local page = Instance.new("ScrollingFrame") page.Name = name page.Size = UDim2.new(1, 0, 1, 0) page.BackgroundTransparency = 1 page.BorderSizePixel = 0 page.ScrollBarThickness = 0 page.ScrollBarImageTransparency = 1 page.ScrollingDirection = Enum.ScrollingDirection.Y page.ClipsDescendants = true page.Visible = false page.Parent = UI.content UI.addPadding(page, 8, 10, 8, 10) local layout = Instance.new("UIListLayout") layout.SortOrder = Enum.SortOrder.LayoutOrder layout.Padding = UDim.new(0, 5) layout.Parent = page local pageSizeScheduled = false layout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function() if pageSizeScheduled then return end pageSizeScheduled = true task.defer(function() pageSizeScheduled = false page.CanvasSize = UDim2.new(0, 0, 0, layout.AbsoluteContentSize.Y + 16) end) end) UI.pages[name] = page return page end function UI.switchPage(name) if State.openDropdown and State.openDropdown.close then State.openDropdown.close() end if State.activePage == name then UI.setSidebarCollapsed(not State.sbCollapsed, true) return end State.activePage = name for pageName, page in pairs(UI.pages) do page.Visible = pageName == name if page.Visible then page.CanvasPosition = Vector2.new(0, 0) end end UI.applyTheme() end function UI.createTab(name, iconId, textIcon) local btn = Instance.new("TextButton") btn.Size = UDim2.new(1, 0, 0, 26) btn.Position = UDim2.new(0, 0, 0, 0) btn.BackgroundColor3 = State.theme.panelHover btn.BackgroundTransparency = 1 btn.BorderSizePixel = 0 btn.Text = "" btn.AutoButtonColor = false btn.Parent = UI.sidebar UI.addCorner(btn, 6) local indicator = Instance.new("Frame") indicator.Size = UDim2.new(0, 3, 0, 12) indicator.Position = UDim2.new(0, 0, 0.5, -6) indicator.BackgroundColor3 = State.theme.accent indicator.BackgroundTransparency = 1 indicator.BorderSizePixel = 0 indicator.Parent = btn UI.addCorner(indicator, 2) UI.regTheme(indicator, "accent") local icon if textIcon then icon = Instance.new("TextLabel") icon.Text = iconId icon.Font = Enum.Font.GothamBold icon.TextSize = 13 icon.TextColor3 = State.theme.textSub icon.BackgroundTransparency = 1 icon.Size = UDim2.new(0, 14, 0, 14) icon.Position = UDim2.new(0, 8, 0.5, -7) icon.Parent = btn else icon = Instance.new("ImageLabel") icon.Image = iconId icon.ImageColor3 = State.theme.textSub icon.BackgroundTransparency = 1 icon.Size = UDim2.new(0, 14, 0, 14) icon.Position = UDim2.new(0, 8, 0.5, -7) icon.Parent = btn end local label = Instance.new("TextLabel") label.Size = UDim2.new(1, -30, 1, 0) label.Position = UDim2.new(0, 28, 0, 0) label.BackgroundTransparency = 1 label.Text = name label.Font = State.uiFont label.TextSize = 11 label.TextColor3 = State.theme.textSub label.TextXAlignment = Enum.TextXAlignment.Left label.TextTruncate = Enum.TextTruncate.AtEnd label.Parent = btn UI.regFont(label, 11) UI.tabs[name] = { button = btn, icon = icon, label = label, indicator = indicator, textIcon = textIcon } btn.MouseEnter:Connect(function() if State.activePage ~= name then UI.tween(btn, 0.12, { BackgroundTransparency = 0.6 }) end end) btn.MouseLeave:Connect(function() if State.activePage ~= name then UI.tween(btn, 0.12, { BackgroundTransparency = 1 }) end end) btn.MouseButton1Click:Connect(function() UI.switchPage(name) end) end function UI.bootError(err) local screen = Instance.new("ScreenGui") screen.Name = GUI_NAME screen.ResetOnSpawn = false screen.IgnoreGuiInset = true screen.DisplayOrder = 999 UI.parentScreen(screen) local frame = Instance.new("Frame") frame.Size = UDim2.new(0, 460, 0, 130) frame.Position = UDim2.new(0.5, -230, 0.5, -65) frame.BackgroundColor3 = rgb(245, 246, 250) frame.BorderSizePixel = 0 frame.Parent = screen UI.addCorner(frame, 12) local label = Instance.new("TextLabel") label.Size = UDim2.new(1, -24, 1, -24) label.Position = UDim2.new(0, 12, 0, 12) label.BackgroundTransparency = 1 label.Text = SCRIPT_NAME .. " boot error:\n" .. tostring(err):sub(1, 260) label.Font = Enum.Font.GothamMedium label.TextSize = 12 label.TextWrapped = true label.TextColor3 = rgb(35, 35, 42) label.TextXAlignment = Enum.TextXAlignment.Left label.TextYAlignment = Enum.TextYAlignment.Top label.Parent = frame end local function getConfiguredWindowSize() local scale = math.clamp(tonumber(Settings.uiSize) or 10, 1, 20) / 10 return math.floor(WIN_W * scale), math.floor(WIN_H * scale) end function UI.createShell() pcall(function() local holder = gethui and gethui() or Services.CoreGui for _, guiName in ipairs({ GUI_NAME, LEGACY_GUI_NAME }) do local old = holder:FindFirstChild(guiName) if old then old:Destroy() end end end) pcall(function() if PlayerGui then for _, guiName in ipairs({ GUI_NAME, LEGACY_GUI_NAME }) do local old = PlayerGui:FindFirstChild(guiName) if old then old:Destroy() end end end end) UI.screen = Instance.new("ScreenGui") UI.screen.Name = GUI_NAME UI.screen.ResetOnSpawn = false UI.screen.IgnoreGuiInset = true UI.screen.DisplayOrder = 999 UI.screen.ZIndexBehavior = Enum.ZIndexBehavior.Global UI.parentScreen(UI.screen) UI.main = Instance.new("Frame") UI.main.Name = "Main" local _iW, _iH = getConfiguredWindowSize() UI.main.Size = UDim2.new(0, _iW, 0, _iH) UI.main.Position = UDim2.new(0.5, -_iW / 2, 0.5, -_iH / 2) UI.main.BackgroundColor3 = State.theme.bg UI.main.BorderSizePixel = 0 UI.main.Active = true UI.main.Draggable = true UI.main.Parent = UI.screen UI.addCorner(UI.main, 12) UI.addStroke(UI.main, 0.48) UI.regTheme(UI.main, "main") UI.clip = Instance.new("Frame") UI.clip.Size = UDim2.new(1, 0, 1, 0) UI.clip.BackgroundTransparency = 1 UI.clip.ClipsDescendants = true UI.clip.Parent = UI.main UI.addCorner(UI.clip, 12) local titlebar = Instance.new("Frame") titlebar.Size = UDim2.new(1, 0, 0, TITLEBAR_H) titlebar.BackgroundTransparency = 1 titlebar.Parent = UI.clip local function dot(x, role) local b = Instance.new("TextButton") b.Size = UDim2.new(0, 11, 0, 11) b.Position = UDim2.new(0, x, 0.5, -5.5) b.BackgroundColor3 = State.theme[role] b.BorderSizePixel = 0 b.Text = "" b.AutoButtonColor = false b.Parent = titlebar UI.addCorner(b, 6) UI.regTheme(b, role) return b end local closeBtn = dot(12, "tlRed") local minBtn = dot(24, "tlYellow") local maxBtn = dot(36, "tlGreen") local ver = Instance.new("TextLabel") ver.Size = UDim2.new(0, 70, 0, 16) ver.Position = UDim2.new(1, -78, 0.5, -8) ver.BackgroundTransparency = 1 ver.Text = SCRIPT_VERSION ver.Font = Enum.Font.GothamMedium ver.TextSize = 10 ver.TextColor3 = State.theme.textSub ver.TextXAlignment = Enum.TextXAlignment.Right ver.Parent = titlebar UI.regTheme(ver, "sub") UI.sidebar = Instance.new("Frame") UI.sidebar.Size = UDim2.new(0, SIDE_EXP, 1, -(TITLEBAR_H + TOP_MARGIN + MARGIN)) UI.sidebar.Position = UDim2.new(0, MARGIN, 0, TITLEBAR_H + TOP_MARGIN) UI.sidebar.BackgroundColor3 = State.theme.panel UI.sidebar.BorderSizePixel = 0 UI.sidebar.ClipsDescendants = true UI.sidebar.Parent = UI.clip UI.addCorner(UI.sidebar, 10) UI.addStroke(UI.sidebar, 0.58) UI.addPadding(UI.sidebar, 6, 4, 6, 4) UI.regTheme(UI.sidebar, "panel", "Sidebar") local sideLayout = Instance.new("UIListLayout") sideLayout.Padding = UDim.new(0, 3) sideLayout.SortOrder = Enum.SortOrder.LayoutOrder sideLayout.Parent = UI.sidebar UI.content = Instance.new("Frame") UI.content.Size = UDim2.new(1, -(SIDE_EXP + MARGIN + GAP + MARGIN), 1, -(TITLEBAR_H + TOP_MARGIN + MARGIN)) UI.content.Position = UDim2.new(0, MARGIN + SIDE_EXP + GAP, 0, TITLEBAR_H + TOP_MARGIN) UI.content.BackgroundColor3 = State.theme.panel UI.content.BorderSizePixel = 0 UI.content.ClipsDescendants = true UI.content.Parent = UI.clip UI.addCorner(UI.content, 10) UI.addStroke(UI.content, 0.58) UI.regTheme(UI.content, "panel", "Content") UI.trackConnection(UI.main.InputChanged:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then State.lastUiInteraction = tick() end end)) closeBtn.MouseButton1Click:Connect(function() UI.shutdown("close", false) UI.tween(UI.main, 0.18, { Size = UDim2.new(0, 0, 0, 0), Position = UDim2.new(0.5, 0, 0.5, 0), BackgroundTransparency = 1 }) task.delay(0.22, function() if UI.screen and UI.screen.Parent then UI.screen:Destroy() end end) end) minBtn.MouseButton1Click:Connect(function() if UI.minimizing then return end UI.minimizing = true if State.openDropdown and State.openDropdown.close then State.openDropdown.close() end local savedSize, savedPos = UI.main.Size, UI.main.Position local savedTransparency = UI.main.BackgroundTransparency local pillPos = UDim2.new(0.5, -17, 0, 12) UI.tween(UI.main, 0.26, { Size = UDim2.new(0, 34, 0, 34), Position = pillPos, BackgroundTransparency = 0.88, }, Enum.EasingStyle.Quint, Enum.EasingDirection.InOut) task.wait(0.26) UI.main.Visible = false UI.main.Size = savedSize UI.main.Position = savedPos local pill = Instance.new("TextButton") pill.Size = UDim2.new(0, 24, 0, 24) pill.Position = UDim2.new(0.5, -12, 0, 18) pill.BackgroundColor3 = State.theme.panel pill.BackgroundTransparency = 0.2 pill.BorderSizePixel = 0 pill.Text = "" pill.Parent = UI.screen UI.addCorner(pill, 17) UI.addStroke(pill, 0.5) local pillIcon = Instance.new("ImageLabel") pillIcon.Size = UDim2.new(0, 16, 0, 16) pillIcon.Position = UDim2.new(0.5, -8, 0.5, -8) pillIcon.BackgroundTransparency = 1 pillIcon.Image = "rbxassetid://7733960981" pillIcon.ImageColor3 = State.theme.accent pillIcon.Parent = pill UI.tween( pill, 0.24, { Size = UDim2.new(0, 34, 0, 34), Position = pillPos, BackgroundTransparency = UI.transparencyFor("panel", "Sidebar") or 0 }, Enum.EasingStyle.Back ) pill.MouseButton1Click:Connect(function() UI.tween(pill, 0.14, { Size = UDim2.new(0, 24, 0, 24), Position = UDim2.new(0.5, -12, 0, 18), BackgroundTransparency = 1 }) task.delay(0.12, function() if pill.Parent then pill:Destroy() end UI.main.Size = UDim2.new(0, 34, 0, 34) UI.main.Position = pillPos UI.main.BackgroundTransparency = 0.88 UI.main.Visible = true UI.tween(UI.main, 0.28, { Size = savedSize, Position = savedPos, BackgroundTransparency = savedTransparency, }, Enum.EasingStyle.Quint, Enum.EasingDirection.Out) task.delay(0.3, function() UI.minimizing = false end) end) end) end) State.windowFullscreen = false maxBtn.MouseButton1Click:Connect(function() State.windowFullscreen = not State.windowFullscreen if State.windowFullscreen then UI.tween(UI.main, 0.24, { Size = UDim2.new(0.85, 0, 0.85, 0), Position = UDim2.new(0.075, 0, 0.075, 0) }) else local rw, rh = getConfiguredWindowSize() UI.tween(UI.main, 0.24, { Size = UDim2.new(0, rw, 0, rh), Position = UDim2.new(0.5, -rw / 2, 0.5, -rh / 2) }) end end) UI.createTab("Home", "rbxassetid://7733960981") UI.createTab("Auto", "rbxassetid://7733917120") UI.createTab("Misc", "rbxassetid://6031302931") UI.createTab("Stats", "rbxassetid://6034687957") UI.createTab("More", "!", true) UI.createTab("Settings", "rbxassetid://6031280882") UI.createPage("Home") UI.createPage("Auto") UI.createPage("Misc") UI.createPage("Stats") UI.createPage("More") UI.createPage("Settings") UI.switchPage("Home") UI.setSidebarCollapsed(false, false) end function UI.infoRow(parent, key, value) local row = Instance.new("Frame") row.Size = UDim2.new(1, 0, 0, 16) row.BackgroundTransparency = 1 row.Parent = parent local label = Instance.new("TextLabel") label.Size = UDim2.new(0, 78, 1, 0) label.BackgroundTransparency = 1 label.Text = key label.Font = State.uiFont label.TextSize = 10 label.TextColor3 = State.theme.textSub label.TextXAlignment = Enum.TextXAlignment.Left label.Parent = row UI.regTheme(label, "sub") UI.regFont(label, 10) local val = Instance.new("TextLabel") val.Size = UDim2.new(1, -78, 1, 0) val.Position = UDim2.new(0, 78, 0, 0) val.BackgroundTransparency = 1 val.Text = tostring(value) val.Font = Enum.Font.GothamBold val.TextSize = 10 val.TextColor3 = State.theme.text val.TextXAlignment = Enum.TextXAlignment.Left val.TextTruncate = Enum.TextTruncate.AtEnd val.Parent = row UI.regTheme(val, "text") return val end function UI.statCard(parent, key, title) local card = UI.rowBase(parent, "Content", 34) local label = Instance.new("TextLabel") label.Size = UDim2.new(0.55, 0, 1, 0) label.Position = UDim2.new(0, 12, 0, 0) label.BackgroundTransparency = 1 label.Text = title label.Font = State.uiFont label.TextSize = 10 label.TextColor3 = State.theme.textSub label.TextXAlignment = Enum.TextXAlignment.Left label.Parent = card UI.regTheme(label, "sub") UI.regFont(label, 10) local val = Instance.new("TextLabel") val.Size = UDim2.new(0.45, -24, 1, 0) val.Position = UDim2.new(0.55, 12, 0, 0) val.BackgroundTransparency = 1 val.Text = "-" val.Font = Enum.Font.GothamBold val.TextSize = 12 val.TextColor3 = State.theme.text val.TextXAlignment = Enum.TextXAlignment.Right val.TextTruncate = Enum.TextTruncate.AtEnd val.Parent = card UI.regTheme(val, "text") UI.stats[key] = val end function UI.buildHome() local page = UI.pages.Home UI.sectionTitle(page, "PROFILE") local card = UI.rowBase(page, "Content", 96) local avatar = Instance.new("ImageLabel") avatar.Size = UDim2.new(0, 58, 0, 58) avatar.Position = UDim2.new(0, 12, 0, 12) avatar.BackgroundColor3 = State.theme.panelHover avatar.BorderSizePixel = 0 avatar.ScaleType = Enum.ScaleType.Crop avatar.Parent = card UI.addCorner(avatar, 29) UI.regTheme(avatar, "hover", "Content") local info = Instance.new("Frame") info.Size = UDim2.new(1, -88, 1, -20) info.Position = UDim2.new(0, 82, 0, 10) info.BackgroundTransparency = 1 info.Parent = card local il = Instance.new("UIListLayout") il.Padding = UDim.new(0, 3) il.Parent = info local exec = "Unknown" pcall(function() if identifyexecutor then exec = identifyexecutor() elseif getexecutorname then exec = getexecutorname() end end) UI.infoRow(info, "User", player and ("@" .. player.Name) or "Unknown") UI.infoRow(info, "ID", player and tostring(player.UserId) or "-") UI.infoRow(info, "Age", player and (player.AccountAge .. " days") or "-") UI.infoRow(info, "Exec", exec) if player then task.spawn(function() local ok, thumb = pcall(function() return Services.Players:GetUserThumbnailAsync(player.UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size150x150) end) if ok and avatar.Parent then avatar.Image = thumb end end) end UI.separator(page) UI.sectionTitle(page, "GAME") local g = UI.rowBase(page, "Content", 72) local gthumb = Instance.new("ImageLabel") gthumb.Size = UDim2.new(0, 50, 0, 50) gthumb.Position = UDim2.new(0, 12, 0.5, -25) gthumb.BackgroundColor3 = State.theme.panelHover gthumb.BorderSizePixel = 0 gthumb.ScaleType = Enum.ScaleType.Crop gthumb.Parent = g UI.addCorner(gthumb, 7) UI.regTheme(gthumb, "hover", "Content") local gi = Instance.new("Frame") gi.Size = UDim2.new(1, -78, 1, -16) gi.Position = UDim2.new(0, 72, 0, 8) gi.BackgroundTransparency = 1 gi.Parent = g local gil = Instance.new("UIListLayout") gil.Padding = UDim.new(0, 2) gil.Parent = gi local gName = UI.infoRow(gi, "Name", "Loading...") local gBy = UI.infoRow(gi, "By", "...") UI.infoRow(gi, "Place", tostring(game.PlaceId)) task.spawn(function() local ok, product = pcall(function() return Services.MarketplaceService:GetProductInfo(game.PlaceId) end) if ok and product then if gName.Parent then gName.Text = product.Name or "Unknown" end if gBy.Parent then gBy.Text = product.Creator and product.Creator.Name or "Unknown" end if product.IconImageAssetId and gthumb.Parent then gthumb.Image = "rbxassetid://" .. tostring(product.IconImageAssetId) end end end) UI.separator(page) UI.sectionTitle(page, "SERVER") local s = UI.rowBase(page, "Content", 80) UI.addPadding(s, 6, 12, 6, 12) local sl = Instance.new("UIListLayout") sl.Padding = UDim.new(0, 1) sl.Parent = s UI.serverPlayers = UI.infoRow(s, "Players", #Services.Players:GetPlayers() .. " / " .. Services.Players.MaxPlayers) UI.serverPrivacy = UI.infoRow(s, "Privacy", "Local Only") UI.serverUptime = UI.infoRow(s, "Server Time", "0m") UI.serverPing = UI.infoRow(s, "Ping", "...") UI.separator(page) UI.sectionTitle(page, "ACTIVITY") UI.feedCard = UI.rowBase(page, "Content", 74) UI.addPadding(UI.feedCard, 8, 12, 8, 12) local fl = Instance.new("UIListLayout") fl.Padding = UDim.new(0, 2) fl.Parent = UI.feedCard UI.addFeed("UI loaded - " .. SCRIPT_VERSION) end local updateAntiAfk local processObby local processBuild local safeCall function UI.openInfoWindow() if UI.infoOverlay and UI.infoOverlay.Parent then UI.infoOverlay:Destroy() end local overlay = Instance.new("TextButton") overlay.Name = "InfoOverlay" overlay.Size = UDim2.new(1, 0, 1, 0) overlay.BackgroundColor3 = Color3.new(0, 0, 0) overlay.BackgroundTransparency = 0.62 overlay.BorderSizePixel = 0 overlay.ClipsDescendants = true overlay.Text = "" overlay.AutoButtonColor = false overlay.Active = true overlay.Modal = true overlay.ZIndex = 700 overlay.Parent = UI.main UI.addCorner(overlay, 12) UI.infoOverlay = overlay overlay.MouseButton1Click:Connect(function() end) local sizeModes = { { name = "Small", w = 450, h = 292 }, { name = "Medium", w = 500, h = 330 }, { name = "Large", w = 540, h = 356 }, { name = "Extra Large", w = 566, h = 360 }, } local function currentModeIndex() for i, mode in ipairs(sizeModes) do if mode.name == Settings.infoSizeMode then return i end end return 2 end local modeIndex = currentModeIndex() local infoPanelTransparency = Settings.transparencyMode ~= "None" and 0.22 or 0 local infoRowTransparency = Settings.transparencyMode ~= "None" and 0.16 or 0 local function solid(obj) pcall(function() obj.Active = true end) return obj end local box = Instance.new("Frame") box.AnchorPoint = Vector2.new(0.5, 0.5) box.Position = UDim2.new(0.5, 0, 0.5, 0) box.Size = UDim2.new(0, 430, 0, 0) box.BackgroundColor3 = State.theme.panel box.BackgroundTransparency = infoPanelTransparency box.BorderSizePixel = 0 box.ClipsDescendants = true box.Active = true box.ZIndex = 701 box.Parent = overlay UI.addCorner(box, 10) UI.addStroke(box, 0.38) solid(box) local boxShield = Instance.new("TextButton") boxShield.Size = UDim2.new(1, 0, 1, 0) boxShield.BackgroundTransparency = 1 boxShield.BorderSizePixel = 0 boxShield.Text = "" boxShield.AutoButtonColor = false boxShield.Active = true boxShield.ZIndex = 701 boxShield.Parent = box boxShield.MouseButton1Click:Connect(function() end) local title = Instance.new("TextLabel") title.Size = UDim2.new(1, -46, 0, 30) title.Position = UDim2.new(0, 14, 0, 0) title.BackgroundTransparency = 1 title.Text = "Info" title.Font = Enum.Font.GothamBold title.TextSize = 12 title.TextColor3 = State.theme.text title.TextXAlignment = Enum.TextXAlignment.Left title.ZIndex = 702 title.Parent = box solid(title) local close = Instance.new("TextButton") close.Size = UDim2.new(0, 24, 0, 24) close.Position = UDim2.new(1, -32, 0, 4) close.BackgroundColor3 = State.theme.panelHover close.BorderSizePixel = 0 close.Text = "x" close.Font = Enum.Font.GothamBold close.TextSize = 14 close.TextColor3 = State.theme.textSub close.AutoButtonColor = false close.ZIndex = 703 close.Parent = box solid(close) UI.addCorner(close, 6) local sizeBar = Instance.new("Frame") sizeBar.Size = UDim2.new(0, 154, 0, 24) sizeBar.Position = UDim2.new(1, -192, 0, 4) sizeBar.BackgroundColor3 = State.theme.panelInner sizeBar.BackgroundTransparency = 1 sizeBar.BorderSizePixel = 0 sizeBar.ClipsDescendants = false sizeBar.ZIndex = 704 sizeBar.Visible = false sizeBar.Active = false sizeBar.Parent = box solid(sizeBar) local sizeLabel = Instance.new("TextLabel") sizeLabel.Size = UDim2.new(0, 34, 1, 0) sizeLabel.Position = UDim2.new(0, 0, 0, 0) sizeLabel.BackgroundTransparency = 1 sizeLabel.Text = "Size" sizeLabel.Font = State.uiFont sizeLabel.TextSize = 10 sizeLabel.TextColor3 = State.theme.textSub sizeLabel.TextXAlignment = Enum.TextXAlignment.Right sizeLabel.TextTruncate = Enum.TextTruncate.AtEnd sizeLabel.ZIndex = 705 sizeLabel.Parent = sizeBar solid(sizeLabel) local sizeTrigger = Instance.new("TextButton") sizeTrigger.Size = UDim2.new(0, 108, 0, 22) sizeTrigger.Position = UDim2.new(1, -108, 0, 1) sizeTrigger.BackgroundColor3 = State.theme.panelHover sizeTrigger.BorderSizePixel = 0 sizeTrigger.Text = "" sizeTrigger.AutoButtonColor = false sizeTrigger.ZIndex = 706 sizeTrigger.Parent = sizeBar solid(sizeTrigger) UI.addCorner(sizeTrigger, 5) local sizeValue = Instance.new("TextLabel") sizeValue.Size = UDim2.new(1, -24, 1, 0) sizeValue.Position = UDim2.new(0, 7, 0, 0) sizeValue.BackgroundTransparency = 1 sizeValue.Text = sizeModes[modeIndex].name sizeValue.Font = State.uiFont sizeValue.TextSize = State.uiFontSize sizeValue.TextColor3 = State.theme.text sizeValue.TextXAlignment = Enum.TextXAlignment.Left sizeValue.TextTruncate = Enum.TextTruncate.AtEnd sizeValue.ZIndex = 707 sizeValue.Parent = sizeTrigger solid(sizeValue) local sizeArrow = Instance.new("TextLabel") sizeArrow.Size = UDim2.new(0, 16, 1, 0) sizeArrow.Position = UDim2.new(1, -17, 0, 0) sizeArrow.BackgroundTransparency = 1 sizeArrow.Text = "v" sizeArrow.Font = Enum.Font.GothamBold sizeArrow.TextSize = 9 sizeArrow.TextColor3 = State.theme.textSub sizeArrow.ZIndex = 707 sizeArrow.Parent = sizeTrigger solid(sizeArrow) local sizeMenu = Instance.new("Frame") sizeMenu.Size = UDim2.new(0, 108, 0, 0) sizeMenu.Position = UDim2.new(1, -108, 0, 26) sizeMenu.BackgroundColor3 = State.theme.panelInner sizeMenu.BackgroundTransparency = infoRowTransparency sizeMenu.BorderSizePixel = 0 sizeMenu.ClipsDescendants = true sizeMenu.ZIndex = 730 sizeMenu.Visible = false sizeMenu.Active = false sizeMenu.Parent = sizeBar UI.addCorner(sizeMenu, 6) local sizeMenuStroke = UI.addStroke(sizeMenu, 1) local sizeMenuLayout = Instance.new("UIListLayout") sizeMenuLayout.SortOrder = Enum.SortOrder.LayoutOrder sizeMenuLayout.Padding = UDim.new(0, 2) sizeMenuLayout.Parent = sizeMenu local applyInfoSize local sizeOpen = false local function setSizeMenuOpen(value) sizeOpen = value if value then sizeMenu.Visible = true sizeMenu.Active = true sizeMenuStroke.Transparency = 0.5 else sizeMenu.Active = false sizeMenuStroke.Transparency = 1 end UI.tween(sizeArrow, 0.12, { Rotation = value and 180 or 0 }) UI.tween(sizeMenu, 0.14, { Size = UDim2.new(0, 108, 0, value and 122 or 0) }) if not value then task.delay(0.16, function() if not sizeOpen and sizeMenu and sizeMenu.Parent then sizeMenu.Visible = false end end) end end for i, mode in ipairs(sizeModes) do local option = Instance.new("TextButton") option.Size = UDim2.new(1, -8, 0, 22) option.BackgroundColor3 = i == modeIndex and State.theme.accent or State.theme.panelInner option.BorderSizePixel = 0 option.Text = mode.name option.Font = State.uiFont option.TextSize = State.uiFontSize option.TextColor3 = i == modeIndex and State.theme.accentText or State.theme.text option.TextXAlignment = Enum.TextXAlignment.Left option.AutoButtonColor = false option.ZIndex = 731 option.Parent = sizeMenu solid(option) UI.addCorner(option, 4) UI.addPadding(option, 0, 6, 0, 6) option.MouseButton1Click:Connect(function() modeIndex = i Settings.infoSizeMode = mode.name Stats.saveSettings() sizeValue.Text = mode.name for _, child in ipairs(sizeMenu:GetChildren()) do if child:IsA("TextButton") then local active = child == option child.BackgroundColor3 = active and State.theme.accent or State.theme.panelInner child.TextColor3 = active and State.theme.accentText or State.theme.text end end setSizeMenuOpen(false) if applyInfoSize then applyInfoSize(true) end end) end sizeTrigger.MouseButton1Click:Connect(function() setSizeMenuOpen(not sizeOpen) end) local creatorCard = Instance.new("Frame") creatorCard.Size = UDim2.new(1, -16, 0, 58) creatorCard.Position = UDim2.new(0, 8, 0, 32) creatorCard.BackgroundColor3 = State.theme.panelInner creatorCard.BackgroundTransparency = infoRowTransparency creatorCard.BorderSizePixel = 0 creatorCard.ZIndex = 702 creatorCard.Parent = box solid(creatorCard) UI.addCorner(creatorCard, 7) UI.addStroke(creatorCard, 0.72) local creatorAvatar = Instance.new("ImageLabel") creatorAvatar.Size = UDim2.new(0, 38, 0, 38) creatorAvatar.Position = UDim2.new(0, 9, 0.5, -19) creatorAvatar.BackgroundColor3 = State.theme.panelHover creatorAvatar.BorderSizePixel = 0 creatorAvatar.ScaleType = Enum.ScaleType.Crop creatorAvatar.ZIndex = 703 creatorAvatar.Parent = creatorCard solid(creatorAvatar) UI.addCorner(creatorAvatar, 19) local creatorInfo = Instance.new("Frame") creatorInfo.Size = UDim2.new(1, -60, 1, -8) creatorInfo.Position = UDim2.new(0, 56, 0, 4) creatorInfo.BackgroundTransparency = 1 creatorInfo.ZIndex = 703 creatorInfo.Parent = creatorCard solid(creatorInfo) local creatorLayout = Instance.new("UIListLayout") creatorLayout.Padding = UDim.new(0, 2) creatorLayout.SortOrder = Enum.SortOrder.LayoutOrder creatorLayout.Parent = creatorInfo local creatorName = UI.infoRow(creatorInfo, "Creator", "Loading...") local creatorId = UI.infoRow(creatorInfo, "Creator ID", "Loading...") UI.infoRow(creatorInfo, "Game ID", tostring(game.PlaceId)) for _, obj in ipairs(creatorInfo:GetDescendants()) do if obj:IsA("TextLabel") then obj.ZIndex = 704 end end task.spawn(function() local ok, product = pcall(function() return Services.MarketplaceService:GetProductInfo(game.PlaceId) end) if not (ok and product) then if creatorName.Parent then creatorName.Text = "Unavailable" end if creatorId.Parent then creatorId.Text = "N/A" end return end local creator = product.Creator or {} local name = creator.Name or "Unknown" local creatorType = tostring(creator.CreatorType or creator.Type or "") local id = creatorType:find("User") and tonumber(creator.Id) or nil if creatorName.Parent then creatorName.Text = name end if creatorId.Parent then creatorId.Text = tostring(creator.Id or id or "N/A") end if not id and name ~= "Unknown" then local okId, resolved = pcall(function() return Services.Players:GetUserIdFromNameAsync(name) end) if okId then id = resolved end end if id then local okThumb, thumb = pcall(function() return Services.Players:GetUserThumbnailAsync(id, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size150x150) end) if okThumb and creatorAvatar.Parent then creatorAvatar.Image = thumb end elseif product.IconImageAssetId and creatorAvatar.Parent then creatorAvatar.Image = "rbxassetid://" .. tostring(product.IconImageAssetId) end end) local playersTitle = Instance.new("TextLabel") playersTitle.Size = UDim2.new(1, -16, 0, 15) playersTitle.Position = UDim2.new(0, 8, 0, 95) playersTitle.BackgroundTransparency = 1 playersTitle.Text = "PLAYERS IN SERVER" playersTitle.Font = Enum.Font.GothamBold playersTitle.TextSize = 10 playersTitle.TextColor3 = State.theme.textSub playersTitle.TextXAlignment = Enum.TextXAlignment.Left playersTitle.ZIndex = 702 playersTitle.Parent = box solid(playersTitle) local list = Instance.new("ScrollingFrame") list.Size = UDim2.new(1, -16, 0, 164) list.Position = UDim2.new(0, 8, 0, 110) list.BackgroundTransparency = 1 list.BorderSizePixel = 0 list.ScrollBarThickness = 0 list.ScrollBarImageTransparency = 1 list.ScrollingDirection = Enum.ScrollingDirection.Y list.ZIndex = 702 list.Parent = box solid(list) local listLayout = Instance.new("UIListLayout") listLayout.Padding = UDim.new(0, 5) listLayout.SortOrder = Enum.SortOrder.LayoutOrder listLayout.Parent = list listLayout:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function() list.CanvasSize = UDim2.new(0, 0, 0, listLayout.AbsoluteContentSize.Y + 8) end) local function playerRow(plr) local card = Instance.new("Frame") card.Size = UDim2.new(1, 0, 0, 44) card.BackgroundColor3 = State.theme.panelInner card.BackgroundTransparency = infoRowTransparency card.BorderSizePixel = 0 card.ZIndex = 703 card.Parent = list solid(card) UI.addCorner(card, 7) UI.addStroke(card, 0.74) local avatar = Instance.new("ImageLabel") avatar.Size = UDim2.new(0, 32, 0, 32) avatar.Position = UDim2.new(0, 8, 0.5, -16) avatar.BackgroundColor3 = State.theme.panelHover avatar.BorderSizePixel = 0 avatar.ScaleType = Enum.ScaleType.Crop avatar.ZIndex = 704 avatar.Parent = card solid(avatar) UI.addCorner(avatar, 16) task.spawn(function() local ok, thumb = pcall(function() return Services.Players:GetUserThumbnailAsync(plr.UserId, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size100x100) end) if ok and avatar.Parent then avatar.Image = thumb end end) local name = Instance.new("TextLabel") name.Size = UDim2.new(1, -56, 0, 17) name.Position = UDim2.new(0, 50, 0, 5) name.BackgroundTransparency = 1 name.Text = "@" .. plr.Name name.Font = Enum.Font.GothamBold name.TextSize = 11 name.TextColor3 = State.theme.text name.TextXAlignment = Enum.TextXAlignment.Left name.TextTruncate = Enum.TextTruncate.AtEnd name.ZIndex = 704 name.Parent = card solid(name) local details = Instance.new("TextLabel") details.Size = UDim2.new(1, -56, 0, 15) details.Position = UDim2.new(0, 50, 0, 23) details.BackgroundTransparency = 1 details.Text = "ID: " .. tostring(plr.UserId) .. " | Account age: " .. tostring(plr.AccountAge) .. " days" details.Font = State.uiFont details.TextSize = 9 details.TextColor3 = State.theme.textSub details.TextXAlignment = Enum.TextXAlignment.Left details.TextTruncate = Enum.TextTruncate.AtEnd details.ZIndex = 704 details.Parent = card solid(details) end for _, plr in ipairs(Services.Players:GetPlayers()) do playerRow(plr) end applyInfoSize = function(animated) local mode = sizeModes[modeIndex] or sizeModes[2] local scale = math.clamp(tonumber(Settings.infoSizeScale) or 100, 85, 120) / 100 local mainSize = UI.main and UI.main.AbsoluteSize or Vector2.new(WIN_W, WIN_H) local w = math.min(math.floor(mode.w * scale), math.max(360, mainSize.X - 28)) local h = math.min(math.floor(mode.h * scale), math.max(286, mainSize.Y - 28)) local listH = math.max(150, h - 118) list.Size = UDim2.new(1, -16, 0, listH) list.CanvasSize = UDim2.new(0, 0, 0, listLayout.AbsoluteContentSize.Y + 8) if animated then UI.tween(box, 0.22, { Size = UDim2.new(0, w, 0, h) }, Enum.EasingStyle.Quad, Enum.EasingDirection.Out) else box.Size = UDim2.new(0, w, 0, h) end end for _, obj in ipairs(box:GetDescendants()) do solid(obj) end sizeMenu.Active = sizeOpen sizeMenu.Visible = sizeOpen sizeMenuStroke.Transparency = sizeOpen and 0.5 or 1 for _, obj in ipairs(list:GetDescendants()) do if obj:IsA("TextLabel") or obj:IsA("ImageLabel") then pcall(function() obj.Active = false end) end end list.Active = true local function closeModal() UI.tween(box, 0.16, { Size = UDim2.new(0, 430, 0, 0), BackgroundTransparency = 1 }, Enum.EasingStyle.Quad, Enum.EasingDirection.In) task.delay(0.18, function() if overlay and overlay.Parent then overlay:Destroy() end if UI.infoOverlay == overlay then UI.infoOverlay = nil end end) end close.MouseButton1Click:Connect(closeModal) applyInfoSize(true) end local buyFreeVendorOffers local function buildPages() local auto = UI.pages.Auto UI.sectionTitle(auto, "AUTOMATION") UI.createToggle(auto, "Auto Merge", Settings.processGrid, function(v) Settings.processGrid = v Stats.saveSettings() UI.addFeed("Auto Merge " .. (v and "ON" or "OFF")) end) UI.createToggle(auto, "Auto Pickup", Settings.collectDrops, function(v) Settings.collectDrops = v Stats.saveSettings() UI.addFeed("Auto Pickup " .. (v and "ON" or "OFF")) end) UI.createToggle(auto, "Auto Orders", Settings.completeTasks, function(v) Settings.completeTasks = v Stats.saveSettings() UI.addFeed("Auto Orders " .. (v and "ON" or "OFF")) end) UI.createToggle(auto, "Auto Build", Settings.constructBuilding, function(v) Settings.constructBuilding = v Stats.saveSettings() if v then State.warnedBuildHoldMissing = nil end task.defer(function() if processBuild then safeCall("BuildImmediate", processBuild) end end) UI.addFeed("Auto Build " .. (v and "ON" or "OFF")) end) UI.separator(auto) UI.sectionTitle(auto, "MERGE MODE") UI.createDropdown(auto, "Mode", { "Smart", "Normal" }, Settings.smartProcessing and 1 or 2, function(_, value) Settings.smartProcessing = value == "Smart" Stats.saveSettings() UI.addFeed("Merge Mode: " .. value) end) UI.createNumber(auto, "Merge Speed", Settings.mergeSpeed or 5, 1, 10, function(v) Settings.mergeSpeed = v Stats.saveSettings() end) UI.createNumber(auto, "Order Speed", Settings.orderSpeed or 5, 1, 10, function(v) Settings.orderSpeed = v Stats.saveSettings() end) local misc = UI.pages.Misc UI.sectionTitle(misc, "TOOLS") UI.createToggle(misc, "Anti AFK", Settings.antiAfk, function(v) Settings.antiAfk = v Stats.saveSettings() updateAntiAfk() end) UI.createToggle(misc, "Auto Inbox", Settings.claimInbox, function(v) Settings.claimInbox = v Stats.saveSettings() end) UI.createToggle(misc, "Auto Break Bubbles", Settings.clearBubbles, function(v) Settings.clearBubbles = v Stats.saveSettings() end) UI.separator(misc) UI.sectionTitle(misc, "PLAYER SPEED") UI.createNumber(misc, "WalkSpeed", Settings.walkSpeed or 16, 1, 100, function(v) Settings.walkSpeed = v Stats.saveSettings() end) UI.separator(misc) UI.sectionTitle(misc, "REWARDS") UI.createToggle(misc, "Auto Spin", Settings.spinReward, function(v) Settings.spinReward = v Limiter.nextAt["Reward:Spin"] = nil Limiter.nextAt["Reward:ButtonScan:spin"] = nil if v then task.defer(function() if pulseRewards then pulseRewards("spin toggle") elseif claimSpinReward then pcall(claimSpinReward) end end) end Stats.saveSettings() end) UI.createToggle(misc, "Auto Daily Reward", Settings.dailyReward, function(v) Settings.dailyReward = v State.lastDailySweep = 0 State.dailySweepCount = 0 State.dailyCursor = 1 State.dailyPendingConfirm = nil State.lastDailyStateSig = nil State.dailyConfirmScheduled = nil State.dailyFirstUnconfirmedRemoteAt = nil State.lastDailyConfirmedAt = nil State.lastDailyRemoteAttempt = nil State.dailyBurstActive = false State.dailyInitialBurstDone = false State.dailyForceBurst = v and true or false for key in pairs(Limiter.nextAt) do if tostring(key):find("^Reward:Daily:") then Limiter.nextAt[key] = nil end end Limiter.nextAt["Reward:Daily:Sweep"] = nil Limiter.nextAt["Reward:Daily:Buttons"] = nil Limiter.nextAt["Reward:ButtonScan"] = nil Limiter.nextAt["Reward:ButtonScan:daily"] = nil Limiter.nextAt["Reward:Daily:FallbackDebug"] = nil if v then task.defer(function() if pulseRewards then pulseRewards("daily toggle") elseif Daily.claimSweep then pcall(Daily.claimSweep) end end) end Stats.saveSettings() end) UI.createToggle(misc, "Auto Obby", Settings.handleObstacleCourse, function(v) Settings.handleObstacleCourse = v State.obbyBusy = false State.lastObby = 0 Stats.saveSettings() if v and processObby then task.spawn(function() pcall(processObby) if pulseRewards then pulseRewards("obby toggle") end end) end end) UI.createDropdown( misc, "Obby Difficulty", { "Easy", "Medium", "Hard" }, ({ Easy = 1, Medium = 2, Hard = 3 })[Settings.obstacleDifficulty] or 3, function(_, v) Settings.obstacleDifficulty = v State.obbyBusy = false State.lastObby = 0 Limiter.nextAt["Obby:Start:" .. tostring(v)] = nil Stats.saveSettings() if Settings.handleObstacleCourse and processObby then task.spawn(function() pcall(processObby) if pulseRewards then pulseRewards("obby difficulty") end end) end end ) UI.separator(misc) UI.sectionTitle(misc, "MERCHANT") local merchantStatusCard = UI.rowBase(misc, "Content", 26) UI.vendorStatusDot = Instance.new("Frame") UI.vendorStatusDot.Size = UDim2.new(0, 7, 0, 7) UI.vendorStatusDot.Position = UDim2.new(0, 8, 0.5, -3.5) UI.vendorStatusDot.BackgroundColor3 = State.vendorActive and State.theme.accent or State.theme.textSub UI.vendorStatusDot.BorderSizePixel = 0 UI.vendorStatusDot.Parent = merchantStatusCard UI.addCorner(UI.vendorStatusDot, 4) UI.vendorStatus = Instance.new("TextLabel") UI.vendorStatus.Size = UDim2.new(1, -28, 1, 0) UI.vendorStatus.Position = UDim2.new(0, 22, 0, 0) UI.vendorStatus.BackgroundTransparency = 1 UI.vendorStatus.Text = "Status: " .. (State.vendorActive and "Running" or "Idle") UI.vendorStatus.Font = State.uiFont UI.vendorStatus.TextSize = State.uiFontSize UI.vendorStatus.TextColor3 = State.theme.text UI.vendorStatus.TextXAlignment = Enum.TextXAlignment.Left UI.vendorStatus.TextTruncate = Enum.TextTruncate.AtEnd UI.vendorStatus.Parent = merchantStatusCard UI.regTheme(UI.vendorStatus, "text") UI.regFont(UI.vendorStatus) UI.createToggle(misc, "Auto-buy Free Items", Settings.vendorAutoFree, function(v) Settings.vendorAutoFree = v Stats.saveSettings() end) local vendorDD local _, returnedDD = UI.createDropdown(misc, "Item to buy", { "No items" }, 1, function(_, value) Settings.vendorSelectedItem = value local dd = vendorDD or UI.vendorDropdown if dd and dd.lastSelectedMeta and dd.lastSelectedMeta.key then Settings.vendorSelectedItemKey = dd.lastSelectedMeta.key elseif dd and dd.optionMeta and dd.optionMeta[value] and dd.optionMeta[value].key then Settings.vendorSelectedItemKey = dd.optionMeta[value].key else Settings.vendorSelectedItemKey = nil end Stats.saveSettings() end) vendorDD = returnedDD if vendorDD then vendorDD.quantityInputs = true end UI.vendorDropdown = vendorDD UI.vendorButton = UI.createButton(misc, State.vendorActive and "Stop Merchant" or "Start Merchant", function() -- Commit selected item's quantity before toggling if UI.vendorDropdown and UI.vendorDropdown.commitSelectedQuantity then UI.vendorDropdown.commitSelectedQuantity() end State.vendorActive = not State.vendorActive Settings.vendorActive = State.vendorActive if State.vendorActive then State.vendorBuyCounts = {} if buyFreeVendorOffers then task.defer(function() pcall(buyFreeVendorOffers) if pulseRewards then pulseRewards("merchant toggle") end end) end end Stats.saveSettings() UI.vendorButton.Text = State.vendorActive and "Stop Merchant" or "Start Merchant" if UI.vendorStatus then UI.vendorStatus.Text = "Status: " .. (State.vendorActive and "Running" or "Idle") end if UI.vendorStatusDot then UI.vendorStatusDot.BackgroundColor3 = State.vendorActive and State.theme.accent or State.theme.textSub end UI.addFeed("Merchant " .. (State.vendorActive and "ON" or "OFF")) end) local stats = UI.pages.Stats UI.sectionTitle(stats, "STATS") UI.statCard(stats, "energy", "Energy") UI.statCard(stats, "coins", "Coins") UI.statCard(stats, "gems", "Gems") UI.statCard(stats, "runtime", "Game Session") UI.separator(stats) UI.sectionTitle(stats, "SPENT") UI.statCard(stats, "energySpent", "Energy Spent") UI.statCard(stats, "coinsSpent", "Coins Spent") UI.statCard(stats, "gemsSpent", "Gems Spent") local more = UI.pages.More UI.sectionTitle(more, "CREATOR") local creator = UI.rowBase(more, "Content", 104) UI.addPadding(creator, 12, 12, 12, 12) local avatarWrap = Instance.new("Frame") avatarWrap.Size = UDim2.new(0, 64, 0, 64) avatarWrap.Position = UDim2.new(0, 14, 0.5, -32) avatarWrap.BackgroundColor3 = State.theme.panelHover avatarWrap.BorderSizePixel = 0 avatarWrap.Parent = creator UI.addCorner(avatarWrap, 32) UI.regTheme(avatarWrap, "hover", "Content") local avatarImg = Instance.new("ImageLabel") avatarImg.Size = UDim2.new(1, 0, 1, 0) avatarImg.BackgroundTransparency = 1 avatarImg.ScaleType = Enum.ScaleType.Crop avatarImg.Parent = avatarWrap UI.addCorner(avatarImg, 32) local creatorInfo = Instance.new("Frame") creatorInfo.Size = UDim2.new(1, -98, 1, -8) creatorInfo.Position = UDim2.new(0, 96, 0, 4) creatorInfo.BackgroundTransparency = 1 creatorInfo.Parent = creator local cl = Instance.new("UIListLayout") cl.SortOrder = Enum.SortOrder.LayoutOrder cl.Padding = UDim.new(0, 4) cl.Parent = creatorInfo UI.infoRow(creatorInfo, "Creator", CREATOR_NAME) local creatorId = UI.infoRow(creatorInfo, "Roblox ID", "Loading...") UI.infoRow(creatorInfo, "Project", SCRIPT_NAME) UI.infoRow(creatorInfo, "Version", SCRIPT_VERSION) task.spawn(function() local okId, uid = pcall(function() return Services.Players:GetUserIdFromNameAsync(CREATOR_NAME) end) if okId and uid then if creatorId and creatorId.Parent then creatorId.Text = tostring(uid) end local okThumb, thumb = pcall(function() return Services.Players:GetUserThumbnailAsync(uid, Enum.ThumbnailType.HeadShot, Enum.ThumbnailSize.Size150x150) end) if okThumb and thumb and avatarImg.Parent then avatarImg.Image = thumb end elseif creatorId and creatorId.Parent then creatorId.Text = "Unavailable" end end) UI.separator(more) UI.sectionTitle(more, "ABOUT THIS SCRIPT") local aboutCard = UI.rowBase(more, "Content", 70) UI.addPadding(aboutCard, 10, 12, 10, 12) local aboutText = Instance.new("TextLabel") aboutText.Size = UDim2.new(1, 0, 1, 0) aboutText.BackgroundTransparency = 1 aboutText.Text = "Thank you for using my script.\n\nBy ep2a1" aboutText.Font = State.uiFont aboutText.TextSize = State.uiFontSize aboutText.TextColor3 = State.theme.text aboutText.TextXAlignment = Enum.TextXAlignment.Center aboutText.TextYAlignment = Enum.TextYAlignment.Center aboutText.TextWrapped = true aboutText.Parent = aboutCard UI.regTheme(aboutText, "text") UI.regFont(aboutText) local settingsPage = UI.pages.Settings UI.sectionTitle(settingsPage, "APPEARANCE") local themeNames = {} for i, t in ipairs(THEMES) do themeNames[i] = t.name end UI.createDropdown(settingsPage, "Theme", themeNames, Settings.themeIndex, function(i) Settings.themeIndex = i State.theme = clone(THEMES[i].colors) Stats.saveSettings() UI.applyTheme() UI.addFeed("Theme: " .. themeNames[i]) end) local _, _, setTransToggle = UI.createToggle(settingsPage, "Transparency", Settings.transparencyMode ~= "None", function(v) Settings.transparencyMode = v and (Settings.transparencyMode ~= "None" and Settings.transparencyMode or "Both") or "None" Settings.transparency = Settings.transparencyMode ~= "None" Stats.saveSettings() UI.applyTheme() end) UI.setTransparencyToggle = setTransToggle UI.separator(settingsPage) UI.sectionTitle(settingsPage, "FONT") UI.createDropdown(settingsPage, "UI Font", FONT_LIST, Settings.uiFontIndex, function(i) Settings.uiFontIndex = i State.uiFont = resolveFont(FONT_LIST[i]) Stats.saveSettings() UI.applyFont() UI.addFeed("Font: " .. FONT_LIST[i]) end) UI.createNumber(settingsPage, "Font Size", Settings.uiFontSize, 1, 100, function(v) Settings.uiFontSize = v State.uiFontSize = v Stats.saveSettings() UI.applyFont() end) UI.separator(settingsPage) UI.sectionTitle(settingsPage, "SERVER") UI.createNumber(settingsPage, "Server Size", Settings.customMaxPlayers or 6, 1, 6, function(v) Settings.customMaxPlayers = math.clamp(v, 1, 6) Stats.saveSettings() end) UI.createButton(settingsPage, "Hop Server", function() task.spawn(function() local function setServerStatus(text) if UI.status then UI.status.Text = "" end end local function currentPing() local ok, ping = pcall(function() return math.floor((player and player:GetNetworkPing() or 0) * 1000) end) return ok and ping or nil end local function playerTarget() local wanted = math.clamp(tonumber(Settings.customMaxPlayers) or 6, 1, 6) return wanted, wanted end local function rememberServer(id) if not id or id == "" then return end Settings.recentServerIds = Settings.recentServerIds or {} Settings.recentServerIds[id] = os.time() for serverId, seenAt in pairs(Settings.recentServerIds) do if os.time() - (tonumber(seenAt) or 0) > 3600 then Settings.recentServerIds[serverId] = nil end end State.recentServers = Settings.recentServerIds end local function parseServerStamp(value) if type(value) == "number" then if value > 1000000000 then return value end if value > 0 then return os.time() - value end elseif type(value) == "string" and value ~= "" then local ok, dt = pcall(function() return DateTime.fromIsoDate(value) end) if ok and dt then return dt.UnixTimestamp end local n = tonumber(value) if n then return parseServerStamp(n) end end return nil end local function serverCreatedAt(server) for _, key in ipairs({ "created", "createdAt", "started", "startedAt", "startTime", "serverStartTime", "ServerStartTime" }) do local stamp = parseServerStamp(server[key]) if stamp then return stamp end end return nil end setServerStatus("Fetching servers...") rememberServer(game.JobId) local servers, cursor = {}, "" for _ = 1, 6 do local url = "https://games.roblox.com/v1/games/" .. game.PlaceId .. "/servers/Public?sortOrder=Asc&limit=100" .. (cursor ~= "" and ("&cursor=" .. cursor) or "") local ok, resp = pcall(function() return game:HttpGet(url) end) if not ok then break end local decoded pcall(function() decoded = Services.HttpService:JSONDecode(resp) end) if not decoded or not decoded.data then break end for _, sv in ipairs(decoded.data) do servers[#servers + 1] = sv end cursor = decoded.nextPageCursor if not cursor or cursor == "" then break end end local filtered = {} local targetPlayers, maxPlayersWanted = playerTarget() local now = os.time() for _, sv in ipairs(servers) do local recentAt = sv.id and Settings.recentServerIds and Settings.recentServerIds[sv.id] local recentlyLeft = recentAt and now - recentAt < 600 if sv.id and sv.id ~= game.JobId and not recentlyLeft and sv.playing and sv.maxPlayers and sv.maxPlayers - sv.playing >= 1 and sv.playing <= math.min(maxPlayersWanted, sv.maxPlayers - 1) then filtered[#filtered + 1] = sv end end if #filtered == 0 and maxPlayersWanted < 6 then for _, sv in ipairs(servers) do if sv.id and sv.id ~= game.JobId and sv.playing and sv.maxPlayers and sv.maxPlayers - sv.playing >= 1 then filtered[#filtered + 1] = sv end end end local pingNow = currentPing() table.sort(filtered, function(a, b) local ap, bp = tonumber(a.ping), tonumber(b.ping) local ad = math.abs((a.playing or 0) - targetPlayers) local bd = math.abs((b.playing or 0) - targetPlayers) if ap and bp and ap ~= bp then return ap < bp end if ap and not bp then return true end if bp and not ap then return false end if ad ~= bd then return ad < bd end return (a.playing or 0) < (b.playing or 0) end) local target = filtered[1] if not target then setServerStatus("No server found") UI.showNotification("Server Hop", "No server found", 3) return end rememberServer(target.id) Stats.saveRuntimeProgress(true) Stats.saveSettings() local pingText = tostring(math.floor(tonumber(target.ping) or pingNow or 0)) .. " ms" local playersText = tostring(target.playing or "?") .. " / " .. tostring(target.maxPlayers or "?") setServerStatus("Teleporting | Players: " .. playersText .. " | Ping: " .. pingText) UI.showNotification("Server Hop", "Players: " .. playersText .. " | Ping: " .. pingText, 3) task.wait(2) pcall(function() Services.TeleportService:TeleportToPlaceInstance(game.PlaceId, target.id, player) end) end) end) UI.createButton(settingsPage, "Rejoin Server", function() UI.showNotification("Rejoining...", "", 2) Stats.saveRuntimeProgress(true) task.wait(0.5) pcall(function() Services.TeleportService:TeleportToPlaceInstance(game.PlaceId, game.JobId, player) end) end) UI.separator(settingsPage) UI.sectionTitle(settingsPage, "INFO") UI.createDropdown( settingsPage, "Size", { "Small", "Medium", "Large", "Extra Large" }, indexOf({ "Small", "Medium", "Large", "Extra Large" }, Settings.infoSizeMode), function(_, v) Settings.infoSizeMode = v Settings.infoSizeScale = 100 Stats.saveSettings() end ) UI.createButton(settingsPage, "Info", UI.openInfoWindow) UI.separator(settingsPage) UI.sectionTitle(settingsPage, "WINDOW") UI.createNumber(settingsPage, "UI Size", Settings.uiSize or 10, 1, 20, function(v) Settings.uiSize = v Stats.saveSettings() if not State.windowFullscreen then local rw, rh = getConfiguredWindowSize() UI.tween(UI.main, 0.24, { Size = UDim2.new(0, rw, 0, rh), Position = UDim2.new(0.5, -rw / 2, 0.5, -rh / 2), }) end end) UI.createButton(settingsPage, "Reset Window", function() local rw, rh = getConfiguredWindowSize() UI.main.Size = UDim2.new(0, rw, 0, rh) UI.main.Position = UDim2.new(0.5, -rw / 2, 0.5, -rh / 2) State.windowFullscreen = false UI.setSidebarCollapsed(false, true) UI.showNotification("Window Reset", "", 2) end) UI.createButton(settingsPage, "Copy Roblox ID", function() pcall(function() setclipboard(tostring(player.UserId)) end) UI.showNotification("Copied", tostring(player.UserId), 2) end) end local function findPath(root, ...) local node = root for _, name in ipairs({ ... }) do if not node then return nil end node = node:FindFirstChild(name) end return node end local function safeRequire(...) local module = findPath(Services.ReplicatedStorage, ...) if not module then return nil end local ok, result = pcall(require, module) return ok and result or nil end local vendorDropdownRefreshQueued = false local offerPrice local isPaidOffer local isFreeOffer Daily.REMOTE_NAMES = { "ClaimDailyReward", } function Daily.isRemote(obj) if not obj then return false end local ok, result = pcall(function() return obj:IsA("RemoteEvent") or obj:IsA("RemoteFunction") end) return ok and result or false end function Daily.findRemoteInFolder(folder) if not folder then return nil end local remote = folder:FindFirstChild("ClaimDailyReward") if Daily.isRemote(remote) then return remote end return nil end scheduleVendorDropdownRefresh = function(delaySeconds, force) if vendorDropdownRefreshQueued and not force then return end if not force then vendorDropdownRefreshQueued = true end task.delay(math.max(tonumber(delaySeconds) or 0.05, 0.03), function() if not force then vendorDropdownRefreshQueued = false end if refreshVendorDropdown then pcall(refreshVendorDropdown) end end) end local function initBackend() if State.closed or not State.running then return end Data.LocalData = safeRequire("ClientModules", "LocalDataModule") local mc = safeRequire("Shared", "MergeItemConfig") if mc then Data.Items = mc.Items or {} Data.GridSize = mc.GridSize or 60 end local inv = safeRequire("Shared", "InventoryItemConfig") if inv then Data.InventoryItems = inv.Items or {} end local cur = safeRequire("Shared", "CurrencyInfo") if cur then Data.CurrencyInfo = cur end Data.BuildConfig = safeRequire("Shared", "BuildableConfig") Data.MerchantInfo = safeRequire("Shared", "MerchantInfo") local events = Services.ReplicatedStorage:FindFirstChild("Events") if events then Data.Events.ClaimPickup = events:FindFirstChild("ClaimPickup") Data.Events.SpawnPickup = events:FindFirstChild("SpawnPickup") Data.Events.BuildHold = events:FindFirstChild("BuildHold") Data.Events.ClaimDaily = Daily.findRemoteInFolder(events) or Data.Events.ClaimDaily Data.Events.ClaimSpin = events:FindFirstChild("ClaimSpin") or events:FindFirstChild("DailySpin") or events:FindFirstChild("SpinWheel") or events:FindFirstChild("SpinReward") or events:FindFirstChild("ClaimDailySpin") or events:FindFirstChild("WheelSpin") or events:FindFirstChild("ClaimSpinReward") or events:FindFirstChild("SpinDaily") Data.Events.StartObby = events:FindFirstChild("StartObby") Data.Events.EndObby = events:FindFirstChild("EndObby") or events:FindFirstChild("FinishObby") or events:FindFirstChild("ExitObby") Data.Events.BuyVendorOffer = events:FindFirstChild("BuyMerchantOffer") or events:FindFirstChild("BuyVendorOffer") or Data.Events.BuyVendorOffer Data.Events.BreakBubble = events:FindFirstChild("BreakBubble") end local remoteEvents = Services.ReplicatedStorage:FindFirstChild("RemoteEvents") Data.ReplicaSignal = remoteEvents and remoteEvents:FindFirstChild("ReplicaSignal") if not (Data.LocalData and Data.LocalData.DataReplica) and Data.ReplicaSignal then local conn conn = UI.trackConnection(Data.ReplicaSignal.OnClientEvent:Connect(function(id, mod) if type(id) == "number" and type(mod) == "string" then Data.ReplicaIDs[mod] = id if not Data.ReplicaID and mod == "MergeGrid" then Data.ReplicaID = id conn:Disconnect() end end end)) task.delay(2, function() if not Data.ReplicaID then Data.ReplicaID = 149 end end) end if Data.Events.SpawnPickup then UI.trackConnection(Data.Events.SpawnPickup.OnClientEvent:Connect(function(payload) if type(payload) == "table" and payload.id then State.nearbyPickups[payload.id] = payload end end)) end UI.addFeed("Backend linked") scheduleVendorDropdownRefresh(0.05) end local function gameData() return Data.LocalData and Data.LocalData.DataReplica and Data.LocalData.DataReplica.Data or nil end local function gridData() local d = gameData() return d and d.MergeGrid or nil end local function getOrders() local d = gameData() return d and d.Orders or {} end local function itemName(itemId) local def = Data.Items[itemId] or Data.InventoryItems[itemId] or Data.CurrencyInfo[itemId] return (def and (def.Name or def.DisplayName)) or tostring(itemId) end local function normalizeImage(value) if value == nil or value == "" then return nil end if type(value) == "number" then return "rbxassetid://" .. tostring(value) end value = tostring(value) if value:match("^%d+$") then return "rbxassetid://" .. value end return value end local function itemIcon(itemId) local def = Data.Items[itemId] or Data.InventoryItems[itemId] or Data.CurrencyInfo[itemId] if type(def) ~= "table" then return nil end return normalizeImage(def.DecalId or def.Icon or def.Image or def.ImageId or def.Thumbnail or def.Texture) end local refreshCache local function offerQuantity(offer) if type(offer) ~= "table" then return nil end local direct = tonumber(offer.Quantity or offer.quantity) if direct and direct > 0 then return math.floor(direct) end local candidates = {} local weakDefaultOne = false local function push(value, weak) local n if type(value) == "table" then n = tonumber( value.Quantity or value.quantity or value.Qty or value.qty or value.Count or value.count or value.Amount or value.amount ) else n = tonumber(value) end if not n or n <= 0 then return end if weak and n == 1 then weakDefaultOne = true return end candidates[#candidates + 1] = n end for _, key in ipairs({ "Qty", "qty", "Count", "count", "ItemQuantity", "itemQuantity", "RewardQuantity", "rewardQuantity", "ProductQuantity", "productQuantity", "Stack", "stack", }) do push(offer[key], false) end push(offer.Quantity, true) push(offer.quantity, true) for _, bucketName in ipairs({ "Reward", "reward", "Item", "item", "Product", "product" }) do local bucket = offer[bucketName] if type(bucket) == "table" then for _, key in ipairs({ "Quantity", "quantity", "Qty", "qty", "Count", "count", "Amount", "amount" }) do push(bucket[key], false) end end end for _, bucketName in ipairs({ "Rewards", "rewards", "Items", "items", "Products", "products" }) do local bucket = offer[bucketName] if type(bucket) == "table" then for _, entry in pairs(bucket) do if type(entry) == "table" then push(offerQuantity(entry), false) elseif type(entry) == "number" then push(entry, false) end end end end local best for _, n in ipairs(candidates) do if not best or n > best then best = n end end if best then return best end if #candidates > 0 then return candidates[1] end if weakDefaultOne then return nil end return nil end local function textQuantity(text) text = tostring(text or "") local n = text:match("[xX]%s*(%d+)") or text:match("(%d+)%s*[xX]") return tonumber(n) end local function plainQuantity(text) text = tostring(text or "") local lower = text:lower() if lower:find("coin") or lower:find("gem") or lower:find("cash") or lower:find("%$") or lower:find("r$") then return nil end local n = tonumber(text:match("^%s*(%d+)%s*$") or text:match("[Ss]tock%s*:?%s*(%d+)") or text:match("[Qq]ty%s*:?%s*(%d+)")) if n and n >= 2 and n <= 999 then return n end return nil end local function guiQuantityForItem(displayName) if not refreshCache then return nil end refreshCache() local target = tostring(displayName or ""):lower():gsub("%s+", "") if target == "" then return nil end local function scanScope(scope) local fallback = nil for _, obj in ipairs(scope:GetDescendants()) do if obj:IsA("TextLabel") or obj:IsA("TextButton") then local n = textQuantity(obj.Text) if n then return n end local objName = tostring(obj.Name or ""):lower() if objName:find("qty") or objName:find("quantity") or objName:find("count") or objName:find("stock") or objName:find("amount") then n = plainQuantity(obj.Text) if n then return n end end fallback = fallback or plainQuantity(obj.Text) end end return fallback end for _, label in ipairs(Data.labels or {}) do local text = label.Text or "" if text:lower():gsub("%s+", ""):find(target, 1, true) then local n = textQuantity(text) if n then return n end local scope = label.Parent for _ = 1, 4 do if not scope then break end n = scanScope(scope) if n then return n end if #scope:GetDescendants() > 80 then break end scope = scope.Parent end end end return nil end local VENDOR_PENDING_CONSUMED_TTL = 4 local DEBUG_VENDOR_DROPDOWN = false local DEBUG_DIAGNOSTICS = false local _DEBUG_DAILY = false function Daily.debug(...) if not _DEBUG_DAILY then return end local args = table.pack(...) task.defer(function() print("[MergeShop][Daily]", unpackArgs(args, 1, args.n)) end) end function Daily.debugLimited(key, interval, ...) if not _DEBUG_DAILY then return end local limiterKey = "Debug:Daily:" .. tostring(key or "log") local now = os.clock() if now < (Limiter.nextAt[limiterKey] or 0) then return end Limiter.nextAt[limiterKey] = now + (tonumber(interval) or 2.0) Daily.debug(...) end safeCall = function(tag, fn) local ok, err = pcall(fn) if not ok and DEBUG_DIAGNOSTICS then warn("[MergeShop][" .. tostring(tag) .. "]", err) end return ok end local function diag(tag, ...) if not DEBUG_DIAGNOSTICS then return end local args = table.pack(...) task.defer(function() print("[MergeShop][" .. tostring(tag) .. "]", table.unpack(args, 1, args.n)) end) end local function vendorPurchasedCount(section, merchant, mi, idx) if not (section and merchant and mi and idx) then return 0 end local info = mi.Sections and mi.Sections[section] local purchased = (info and info.PurchasedKey and merchant[info.PurchasedKey]) or {} return tonumber(purchased[tostring(idx)]) or 0 end local function setVendorLocalConsumed(key, value) if not key then return 0 end State.vendorLocalConsumed = State.vendorLocalConsumed or {} State.vendorLocalConsumedAt = State.vendorLocalConsumedAt or {} local consumed = math.max(0, tonumber(value) or 0) if consumed > 0 then State.vendorLocalConsumed[key] = consumed State.vendorLocalConsumedAt[key] = tick() else State.vendorLocalConsumed[key] = nil State.vendorLocalConsumedAt[key] = nil end return consumed end local function addVendorLocalConsumed(key, amount) local current = key and tonumber(State.vendorLocalConsumed and State.vendorLocalConsumed[key]) or 0 return setVendorLocalConsumed(key, current + math.max(1, tonumber(amount) or 1)) end local function reconcileVendorLocalConsumed(key, serverBought) if not key then return 0 end State.vendorServerBought = State.vendorServerBought or {} local bought = tonumber(serverBought) or 0 local consumed = tonumber(State.vendorLocalConsumed and State.vendorLocalConsumed[key]) or 0 local lastBought = tonumber(State.vendorServerBought[key]) if lastBought == nil then State.vendorServerBought[key] = bought if consumed > 0 and bought > 0 then consumed = setVendorLocalConsumed(key, 0) end elseif bought > lastBought then consumed = math.max(0, consumed - (bought - lastBought)) State.vendorServerBought[key] = bought setVendorLocalConsumed(key, consumed) elseif bought < lastBought then State.vendorServerBought[key] = bought consumed = setVendorLocalConsumed(key, 0) end local consumedAt = State.vendorLocalConsumedAt and State.vendorLocalConsumedAt[key] if consumed > 0 and consumedAt and tick() - consumedAt > VENDOR_PENDING_CONSUMED_TTL then consumed = setVendorLocalConsumed(key, 0) end return consumed end refreshVendorDropdown = function() if State.closed or not UI.screen or not UI.screen.Parent or not UI.vendorDropdown then return end if State.dragActive then State.vendorRefreshAfterDrag = true return end State.vendorLocalConsumed = State.vendorLocalConsumed or {} local options = {} local optionMeta = {} State.vendorOfferKeys = {} State.vendorOfferKeysByKey = {} local seenOfferKeys = {} local seenLabels = {} local validOfferKeys = {} local data = gameData() local merchant = data and data.Merchant local mi = Data.MerchantInfo if mi and merchant and mi.BuildOffers then local function windowKey(section) return tostring(merchant[section .. "Start"] or merchant[section .. "start"] or merchant.Start or merchant.start or "unknown") end local function remainingFor(section, offer) local idx = offer.Index or offer.index if not idx then return 0, 0 end local bought = vendorPurchasedCount(section, merchant, mi, idx) if mi.GetRemainingStock then local ok, rem = pcall(function() return mi.GetRemainingStock(offer, bought) end) if ok and tonumber(rem) then return tonumber(rem), bought end end return math.max(0, tonumber(offer.Stock or offer.stock or 1) - bought), bought end local totalOffersSeen = 0 local totalOffersAdded = 0 for _, section in ipairs({ "Daily", "Flash" }) do local ok, offers = pcall(function() return mi.BuildOffers(section, merchant[section .. "Start"]) end) if ok and type(offers) == "table" then for _, offer in ipairs(offers) do local idx = offer.Index or offer.index local key = idx and (tostring(section) .. ":" .. tostring(idx) .. ":" .. windowKey(section)) or nil -- FIX: merchant local stock cache – subtract locally consumed local rawRemaining, serverBought = 0, 0 if idx then rawRemaining, serverBought = remainingFor(section, offer) end local localConsumed = key and reconcileVendorLocalConsumed(key, serverBought) or 0 local remaining = math.max(0, rawRemaining - localConsumed) totalOffersSeen = totalOffersSeen + 1 -- Determine if offer is paid or free local offerIsPaid = isPaidOffer(offer) local offerIsFree = isFreeOffer(section, offer) local addedToDropdown = false -- Build dropdown from every merchant offer that has remaining stock -- Only hide an offer if it is clearly Robux/GamePass/DeveloperProduct paid if idx and key and remaining > 0 and not seenOfferKeys[key] then seenOfferKeys[key] = true -- Daily offers must always be visible -- Flash offers must be visible if they have remaining stock -- Only hide if clearly paid (Robux/GamePass/DevProduct with Robux price) if offerIsPaid then validOfferKeys[key] = true if Settings.vendorQuantities and Settings.vendorQuantities[key] then Settings.vendorQuantities[key] = nil end addedToDropdown = false else validOfferKeys[key] = true local offerId = offer.Id or offer.id local name = itemName(offerId) local qty = offerQuantity(offer) local bundleQty = math.max(1, tonumber(qty) or 1) local totalQty = remaining * bundleQty local baseLabel = name .. " x" .. tostring(totalQty) local label = baseLabel if seenLabels[label] then label = baseLabel .. " [" .. tostring(section) .. ":" .. tostring(idx) .. "]" end seenLabels[label] = true local maxUnits = remaining * bundleQty options[#options + 1] = label totalOffersAdded = totalOffersAdded + 1 State.vendorOfferKeys[label] = { section = section, idx = idx, key = key, quantity = qty, bundleQuantity = bundleQty, remaining = remaining, maxUnits = maxUnits, } State.vendorOfferKeysByKey[key] = State.vendorOfferKeys[label] optionMeta[label] = { key = key, max = maxUnits, remaining = remaining, icon = itemIcon(offerId), quantityInput = section ~= "Daily", } addedToDropdown = true end end if DEBUG_VENDOR_DROPDOWN then task.defer(function() local offerIdentifier = tostring(offer.Id or offer.id or offer.Name or offer.name or idx or "?") print(string.format( "[MERCHANT-DROPDOWN] %s idx:%s key:%s id:%s remaining:%d paid:%s free:%s added:%s", section, tostring(idx), key or "nil", offerIdentifier, remaining, tostring(offerIsPaid), tostring(offerIsFree), tostring(addedToDropdown) )) end) end end end end if DEBUG_VENDOR_DROPDOWN then task.defer(function() print(string.format( "[MERCHANT-DROPDOWN] Total offers seen: %d, Added to dropdown: %d", totalOffersSeen, totalOffersAdded )) end) end end if #options == 0 then options = { "No merchant offers found" } end if type(Settings.vendorQuantities) == "table" then for key in pairs(Settings.vendorQuantities) do if not validOfferKeys[key] then Settings.vendorQuantities[key] = nil if Settings.vendorSelectedItemKey == key then Settings.vendorSelectedItemKey = nil Settings.vendorSelectedItem = nil end end end end UI.vendorDropdown.quantityInputs = true UI.vendorDropdown.optionMeta = optionMeta local selectedIndex = indexOf(options, tostring(Settings.vendorSelectedItem or "")) local foundSelectedItem = false if selectedIndex == 1 and options[1] ~= tostring(Settings.vendorSelectedItem or "") and Settings.vendorSelectedItemKey then for i, opt in ipairs(options) do local meta = optionMeta[opt] if meta and meta.key == Settings.vendorSelectedItemKey then selectedIndex = i foundSelectedItem = true break end end else foundSelectedItem = options[selectedIndex] == tostring(Settings.vendorSelectedItem or "") end -- FIX: reset selection if item no longer in dropdown if Settings.vendorSelectedItemKey and not foundSelectedItem then Settings.vendorSelectedItemKey = nil Settings.vendorSelectedItem = nil selectedIndex = 1 end if UI.vendorDropdown and type(UI.vendorDropdown.update) == "function" then UI.vendorDropdown.update(options, selectedIndex) end end local function normNumber(v) if type(v) == "number" then return v end if type(v) == "string" then return tonumber((v:gsub("[,%s]", ""))) end if typeof and typeof(v) == "Instance" then local ok, value = pcall(function() return v.Value end) if ok then return normNumber(value) end end return nil end local function caseValue(tbl, key) if type(tbl) ~= "table" then return nil end local wanted = tostring(key):lower() for k, v in pairs(tbl) do if tostring(k):lower() == wanted then return v end end return nil end local nestedNumber local function getCurrency(name) local d = gameData() if not d then return 0 end for _, bucket in ipairs({ d.Currencies, d.Currency, d.Wallet, d.Resources, d.Stats }) do local n = normNumber(caseValue(bucket, name)) if n ~= nil then return n end end return 0 end local function leaderStat(...) if not player then return nil end local ls = player:FindFirstChild("leaderstats") if not ls then return nil end for _, name in ipairs({ ... }) do local direct = ls:FindFirstChild(name) local n = direct and normNumber(direct) if n ~= nil then return n end local wanted = tostring(name):lower() for _, child in ipairs(ls:GetChildren()) do if child.Name:lower() == wanted then n = normNumber(child) if n ~= nil then return n end end end end return nil end function nestedNumber(root, names, depth, seen) if type(root) ~= "table" or (depth or 0) > 5 then return nil end seen = seen or {} if seen[root] then return nil end seen[root] = true for _, name in ipairs(names) do local direct = normNumber(caseValue(root, name)) if direct ~= nil then return direct end end for _, value in pairs(root) do if type(value) == "table" then local found = nestedNumber(value, names, (depth or 0) + 1, seen) if found ~= nil then return found end end end return nil end local function dataNumber(...) local d = gameData() if not d then return nil end return nestedNumber(d, { ... }, 0) end local function canFire(key, interval) local now = os.clock() if now < (Limiter.nextAt[key] or 0) then return false end Limiter.nextAt[key] = now + (interval or 0.15) return true end local DEBUG_ORDERS = false local function ordersDebug(...) if not DEBUG_ORDERS then return end local args = table.pack(...) task.defer(function() print(table.unpack(args, 1, args.n)) end) end local function isOrdersModule(module) module = tostring(module or "") return module == "Orders" or module == "Order" end local function rememberReplicaId(id, moduleName) local numericId = tonumber(id) moduleName = tostring(moduleName or "") if not numericId or moduleName == "" then return end Data.ReplicaIDs = Data.ReplicaIDs or {} Data.ReplicaIDsSeenAt = Data.ReplicaIDsSeenAt or {} Data.ReplicaIDs[moduleName] = numericId Data.ReplicaIDsSeenAt[moduleName] = tick() if moduleName == "MergeGrid" then Data.ReplicaID = numericId end ordersDebug("[ReplicaID]", moduleName, numericId) end local function getReplicaIdForModule(module) module = tostring(module or "") Data.ReplicaIDs = Data.ReplicaIDs or {} if isOrdersModule(module) then local ordersId = tonumber(Data.ReplicaIDs.Orders) if ordersId then return ordersId end local orderId = tonumber(Data.ReplicaIDs.Order) if orderId then return orderId end return nil end local exact = tonumber(Data.ReplicaIDs[module]) if exact then return exact end if module == "MergeGrid" then return tonumber(Data.ReplicaID) end return tonumber(Data.ReplicaID) end local function replicaIdForModule(module) return getReplicaIdForModule(module) end local function remoteCall(module, action, arg1, arg2, suffix) local interval = REMOTE_INTERVALS[module .. "." .. action] or 0.15 local key = module .. "." .. action .. ":" .. tostring(suffix or arg1 or "") if isOrdersModule(module) then local replicaId = getReplicaIdForModule(module) if replicaId and Data.ReplicaSignal then if not canFire(key, interval) then return false end local args = { replicaId, module, action } if arg1 ~= nil then args[#args + 1] = tonumber(arg1) or arg1 end if arg2 ~= nil then args[#args + 1] = tonumber(arg2) or arg2 end ordersDebug("[Orders Fire]", module, action, "replica:", replicaId, "arg1:", arg1) return pcall(function() Data.ReplicaSignal:FireServer(unpackArgs(args)) end) end ordersDebug("[Orders Skip]", module, action, "no dynamic replica id") if Data.LocalData and Data.LocalData.DataReplica then if not canFire(key .. ":DataReplica", interval) then return false end return pcall(function() Data.LocalData.DataReplica:FireServer(module, action, arg1, arg2) end) end return false end if not canFire(key, interval) then return false end if Data.LocalData and Data.LocalData.DataReplica then return pcall(function() Data.LocalData.DataReplica:FireServer(module, action, arg1, arg2) end) end local replicaId = getReplicaIdForModule(module) if Data.ReplicaSignal and replicaId then local args = { replicaId, module, action } if arg1 ~= nil then args[#args + 1] = tostring(arg1) end if arg2 ~= nil then args[#args + 1] = tostring(arg2) end return pcall(function() Data.ReplicaSignal:FireServer(unpackArgs(args)) end) end return false end local function resolveReplicaID(module) if isOrdersModule(module) then return getReplicaIdForModule(module) end local moduleReplicaId = getReplicaIdForModule(module) if moduleReplicaId then return moduleReplicaId end local replica = Data.LocalData and Data.LocalData.DataReplica if replica then for _, key in ipairs({ "ReplicaID", "ReplicaId", "Id", "ID", "_id" }) do local ok, value = pcall(function() return replica[key] end) if ok and tonumber(value) then Data.ReplicaID = tonumber(value) return Data.ReplicaID end end end if isOrdersModule(module) then return nil end return 149 end local function fireReplica(module, action, arg1, arg2, key, interval) if not Data.ReplicaSignal then return false end local replicaId = resolveReplicaID(module) if isOrdersModule(module) and not replicaId then ordersDebug("[Orders Replica Skip]", module, action, "no dynamic replica id") return false end if not replicaId then return false end if not canFire(key or (module .. "." .. action .. ":" .. tostring(arg1 or "")), interval or 0.15) then return false end local args = { replicaId, module, action } if arg1 ~= nil then if isOrdersModule(module) then args[#args + 1] = tonumber(arg1) or arg1 else args[#args + 1] = tostring(arg1) end end if arg2 ~= nil then if isOrdersModule(module) then args[#args + 1] = tonumber(arg2) or arg2 else args[#args + 1] = tostring(arg2) end end return pcall(function() Data.ReplicaSignal:FireServer(unpackArgs(args)) end) end local function fireEvent(event, key, interval, ...) if not event then return false end if key and not canFire(key, interval or 0.15) then return false end local args, count = { ... }, select("#", ...) return pcall(function() event:FireServer(unpackArgs(args, 1, count)) end) end local function getRoot() local char = player and player.Character return char and char:FindFirstChild("HumanoidRootPart") end local function touchPart(part) local root = getRoot() if not (root and part and part:IsA("BasePart")) then return false end pcall(function() firetouchinterest(root, part, 0) task.wait() firetouchinterest(root, part, 1) end) return true end local function pickupIdFromObject(obj) if not obj then return nil end for _, key in ipairs({ "id", "Id", "ID", "pickupId", "PickupId", "PickupID" }) do local ok, value = pcall(function() return obj:GetAttribute(key) end) if ok and value ~= nil then return tostring(value) end end if obj.Name:match("^%d+$") then return obj.Name end if obj.Parent and obj.Parent.Name:match("^%d+$") then return obj.Parent.Name end return nil end local PICKUP_FOLDERS = { "Pickups", "EnergyPickups", "CurrencyPickups", "DropPickups", "Drops" } local cachedPickupFolders = {} local function eachPickupObject(callback) for _, folderName in ipairs(PICKUP_FOLDERS) do local folder = cachedPickupFolders[folderName] if not folder or not folder.Parent then folder = Services.Workspace:FindFirstChild(folderName) or Services.Workspace:FindFirstChild(folderName, true) if folder then cachedPickupFolders[folderName] = folder end end if folder then local count = 0 for _, obj in ipairs(folder:GetDescendants()) do count = count + 1 if count > 200 then break end if callback(obj, folderName) == false then return end end end end end local function findPickupPartById(id) if not id then return nil end local wanted = tostring(id) local found = nil eachPickupObject(function(obj) if obj.Name == wanted or pickupIdFromObject(obj) == wanted then if obj:IsA("BasePart") then found = obj return false end if obj:IsA("Model") then found = obj.PrimaryPart or obj:FindFirstChildWhichIsA("BasePart") return false end end return end) return found end local function pickupCFrameFromPayload(payload) if type(payload) ~= "table" then return nil end local cf = payload.CFrame or payload.cframe local pos = payload.Position or payload.position or payload.Pos or payload.pos if typeof and typeof(cf) == "CFrame" then return cf end if typeof and typeof(pos) == "Vector3" then return CFrame.new(pos) end if type(pos) == "table" and pos.X and pos.Y and pos.Z then return CFrame.new(pos.X, pos.Y, pos.Z) end return nil end local function claimPickup(id, part, payload) local root = getRoot() if not root then return false end part = part or findPickupPartById(id) local targetCFrame = part and part:IsA("BasePart") and part.CFrame or pickupCFrameFromPayload(payload) local original = root.CFrame local claimed = false if id ~= nil then claimed = fireEvent(Data.Events.ClaimPickup, "Pickup:" .. tostring(id), 0.06, tostring(id)) or claimed if claimed and not targetCFrame then return true end end if targetCFrame then pcall(function() root.AssemblyLinearVelocity = Vector3.zero root.CFrame = targetCFrame if part then touchPart(part) end root.CFrame = original root.AssemblyLinearVelocity = Vector3.zero end) end if id ~= nil then claimed = fireEvent(Data.Events.ClaimPickup, "PickupRetry:" .. tostring(id), 0.06, tostring(id)) or claimed end return claimed end local function collectPhysicalPickups() if not Settings.collectDrops then return end local root = getRoot() if not root then return end if State.lastPickupScan and tick() - State.lastPickupScan < 0.5 then return end State.lastPickupScan = tick() local originPos = root.Position local picked = 0 eachPickupObject(function(obj) if picked >= 5 then return false end local name = obj.Name:lower() local isBad = name == "collect" or name:find("box") or name:find("generator") or name:find("tank") or name:find("button") local isPickupName = not isBad and ( name:find("pickup") or name:find("drop") or name:find("energy") or name:find("coin") or name:find("gem") or obj:IsA("BasePart") ) local part = nil if obj:IsA("BasePart") then part = obj elseif obj:IsA("Model") then part = obj.PrimaryPart or obj:FindFirstChildWhichIsA("BasePart") end if isPickupName and part and part:IsDescendantOf(Services.Workspace) and (part.Position - originPos).Magnitude <= 700 then claimPickup(pickupIdFromObject(obj) or pickupIdFromObject(part), part) picked = picked + 1 end end) if picked > 0 then State.pickupCount = State.pickupCount + picked end end refreshCache = function() if State.dragActive then return end if State.closed then return end if not player or not player.PlayerGui then return end if tick() - Data.cacheTime < REFRESH_CACHE_INTERVAL then return end Data.buttons, Data.labels = {}, {} if not PlayerGui then return end local scanned = 0 for _, sg in ipairs(PlayerGui:GetChildren()) do if scanned >= REFRESH_CACHE_MAX_OBJECTS then break end if sg:IsA("ScreenGui") and sg.Enabled and sg.Name ~= GUI_NAME and sg.Name ~= LEGACY_GUI_NAME then for _, obj in ipairs(sg:GetDescendants()) do scanned = scanned + 1 if scanned >= REFRESH_CACHE_MAX_OBJECTS then break end if (obj:IsA("TextButton") or obj:IsA("ImageButton")) and obj.Visible then if #Data.buttons < REFRESH_CACHE_MAX_BUTTONS then Data.buttons[#Data.buttons + 1] = obj end elseif obj:IsA("TextLabel") and obj.Visible then if #Data.labels < REFRESH_CACHE_MAX_LABELS then Data.labels[#Data.labels + 1] = obj end end if #Data.buttons >= REFRESH_CACHE_MAX_BUTTONS and #Data.labels >= REFRESH_CACHE_MAX_LABELS then break end -- FIX: Prevent freezing the client during heavy UI scans if scanned % 150 == 0 then task.wait() end end if #Data.buttons >= REFRESH_CACHE_MAX_BUTTONS and #Data.labels >= REFRESH_CACHE_MAX_LABELS then break end end end Data.cacheTime = tick() end local function clickButton(btn) local getCons = getconnections or get_signal_cons if not (btn and btn.Parent and getCons) then return false end local fired = false for _, signalName in ipairs({ "MouseButton1Click", "Activated", "MouseButton1Down" }) do local ok, signal = pcall(function() return btn[signalName] end) if ok and signal then for _, cn in ipairs(getCons(signal)) do pcall(function() cn:Fire() end) fired = true end end end return fired end local function compactButtonText(btn) local text = "" if not btn then return text end if btn:IsA("TextButton") then text = btn.Text or "" end if text == "" or #text <= 8 then for _, child in ipairs(btn:GetDescendants()) do if child:IsA("TextLabel") or child:IsA("TextButton") then text = text .. " " .. (child.Text or "") end end end return text:upper():gsub("%s", "") end local function orderValue(order) local value = 0 for name, amount in pairs(order.Rewards or order.rewards or {}) do if type(amount) == "number" then local key = tostring(name):lower() if key == "gems" or key == "gem" then value = value + amount * 500 elseif key == "energy" then value = value + amount * 25 elseif key == "coins" or key == "coin" or key == "gold" then value = value + amount else value = value + amount end end end return value end local function orderDemand() local demand = {} local orders = {} for _, order in pairs(getOrders()) do if type(order) == "table" then orders[#orders + 1] = order end end table.sort(orders, function(a, b) return orderValue(a) > orderValue(b) end) local function addReq(req) if type(req) ~= "table" then return end local id = req.Id or req.id or req.ItemId or req.itemId or req.Item or req.item or req.Name or req.name if id ~= nil then demand[tostring(id)] = (demand[tostring(id)] or 0) + tonumber(req.Count or req.count or req.Quantity or req.quantity or req.Amount or req.amount or 1) end end for _, order in ipairs(orders) do for _, field in ipairs({ "Items", "items", "Requirements", "requirements", "RequiredItems", "requiredItems", "OrderItems", "orderItems", "Needs", "needs", "Requests", "requests", }) do local bucket = order[field] if type(bucket) == "table" then for key, req in pairs(bucket) do if type(req) == "table" then addReq(req) elseif type(key) == "string" then demand[tostring(key)] = (demand[tostring(key)] or 0) + (tonumber(req) or 1) end end end end end return demand end local function cachedOrderDemand() local now = tick() if State.cachedOrderDemand and now - (State.lastOrderDemandBuild or 0) < 2.5 then return State.cachedOrderDemand end State.lastOrderDemandBuild = now State.cachedOrderDemand = orderDemand() return State.cachedOrderDemand end local function itemFamily(id) id = tostring(id or "") id = id:gsub("Generator%d+$", "") id = id:gsub("%d+$", "") return id end local function demandHasFamily(demand, family) if not family or family == "" then return false end for id in pairs(demand) do if itemFamily(id) == family then return true end end return false end local function poolCanHelpDemand(pool, demand) if type(pool) ~= "table" then return false end for _, entry in ipairs(pool) do local id = entry.Id or entry.id or entry[1] if id and (demand[tostring(id)] or demandHasFamily(demand, itemFamily(id))) then return true end end return false end local function _generatorCanHelpDemand(itemId, def, demand) if next(demand) == nil then return false end if demandHasFamily(demand, itemFamily(itemId)) then return true end if def and def.OnTap and poolCanHelpDemand(def.OnTap.Pool, demand) then return true end if def and def.AutoGen and poolCanHelpDemand(def.AutoGen.Pool, demand) then return true end return false end local function shouldTapContainer(itemId, def) if not (def and def.OnTap) then return false end local id = tostring(itemId or "") return id:find("Bag") ~= nil or id:find("Chest") ~= nil or id:find("Box") ~= nil or id:find("Crate") ~= nil or id:find("Case") ~= nil end local automationPausedForUi local function orderIndexList(orders) local indices = {} local seen = {} if type(orders) ~= "table" then return indices end for key, order in pairs(orders) do local idx = nil if type(key) == "number" or tonumber(key) then idx = tonumber(key) elseif type(order) == "table" then idx = tonumber(order.Index or order.index or order.Id or order.id) end if idx and not seen[idx] then seen[idx] = true indices[#indices + 1] = idx end end table.sort(indices) return indices end local function orderKeyList(orders) local keys = {} local seen = {} if type(orders) ~= "table" then return keys end for key, order in pairs(orders) do local keyStr = tostring(key) if not seen[keyStr] then local isValid = type(order) == "table" or type(key) == "number" or tonumber(key) ~= nil if isValid then seen[keyStr] = true keys[#keys + 1] = key end end end table.sort(keys, function(a, b) local na, nb = tonumber(a), tonumber(b) if na and nb then return na < nb end return tostring(a) < tostring(b) end) return keys end local function cachedOrderKeyList(orders) local now = tick() local speed = math.clamp(tonumber(Settings.orderSpeed) or 5, 1, 10) local ttl = speed >= 9 and 0.35 or (speed >= 7 and 0.60 or 1.2) if State.cachedOrderKeys and now - (State.lastOrderKeysBuild or 0) < ttl then return State.cachedOrderKeys end State.lastOrderKeysBuild = now State.cachedOrderKeys = orderKeyList(orders) return State.cachedOrderKeys end local function serveOrderIndex(index) index = tonumber(index) if not index then return false end local served = remoteCall("Orders", "Serve", index, nil, "serve:" .. tostring(index)) or fireReplica("Orders", "Serve", index, nil, "Orders.Serve:" .. tostring(index), 0.08) local completed = remoteCall("Orders", "Complete", index, nil, "complete:" .. tostring(index)) or remoteCall("Order", "Serve", index, nil, "order:" .. tostring(index)) return served or completed end local function completeOrders() if not Settings.completeTasks then return end if automationPausedForUi and automationPausedForUi() then return end local now = tick() local function clickOrderButtons(maxClicks) refreshCache() local clicked = 0 for _, btn in ipairs(Data.buttons) do if clicked >= maxClicks then break end local compact = compactButtonText(btn) local isOrderBtn = compact == "GO" or compact == "CLAIM" or compact == "ACCEPT" or compact == "COMPLETE" or compact == "DELIVER" or compact == "DONE" or compact == "FINISH" or compact == "FULFILL" or compact == "COLLECT" or compact == "SUBMIT" or compact:find("^GO") ~= nil or compact:find("SERVE") ~= nil or compact:find("ORDER") ~= nil or compact:find("TASK") ~= nil if isOrderBtn and clickButton(btn) then clicked = clicked + 1 end end return clicked end local keys = cachedOrderKeyList(getOrders()) local keyCount = #keys if keyCount == 0 then State.orderServeCursor = 1 if now - (State.lastOrderUiFallbackScan or 0) >= 5.0 then State.lastOrderUiFallbackScan = now clickOrderButtons(2) end return end local orderSpeed = math.clamp(tonumber(Settings.orderSpeed) or 5, 1, 10) local batchDelay = orderSpeed >= 9 and 0.08 or (orderSpeed >= 7 and 0.12 or 0.25) local maxRemoteSteps = orderSpeed >= 10 and 4 or (orderSpeed >= 8 and 3 or (orderSpeed >= 6 and 2 or 1)) -- Remote: at high order speed, process a few Serve/Complete steps per cycle. if now - (State.lastOrderRemoteBatch or 0) >= batchDelay then State.lastOrderRemoteBatch = now local firedAny = false for _ = 1, maxRemoteSteps do if (State.orderServeCursor or 1) > keyCount then State.orderServeCursor = 1 end local cursor = State.orderServeCursor or 1 local key = keys[cursor] local numKey = tonumber(key) local keyStr = tostring(key) local fired = false if (State.orderServeStage or 0) == 0 then fired = remoteCall("Orders", "Serve", numKey or keyStr, nil, "serve:" .. keyStr) or fireReplica("Orders", "Serve", numKey or keyStr, nil, "Orders.Serve:" .. keyStr, 0.08) or (numKey ~= nil and remoteCall("Order", "Serve", numKey, nil, "order:" .. keyStr)) State.orderServeStage = 1 else fired = remoteCall("Orders", "Complete", numKey or keyStr, nil, "complete:" .. keyStr) or remoteCall("Order", "Complete", numKey or keyStr, nil, "ordercomplete:" .. keyStr) State.orderServeStage = 0 State.orderServeCursor = (cursor % keyCount) + 1 end firedAny = firedAny or fired if not fired and orderSpeed < 8 then break end end if firedAny then State.lastOrderRemoteSuccess = now return end end -- UI fallback: only if no remote success in 4s, throttled at 5s if now - (State.lastOrderRemoteSuccess or 0) < 4.0 then return end if now - (State.lastOrderUiFallbackScan or 0) < 5.0 then return end State.lastOrderUiFallbackScan = now clickOrderButtons(2) end local function clickRewardButtons(mode) local cacheKey = "Reward:ButtonScan:" .. (mode or "generic") if not canFire(cacheKey, 15.0) then return false end refreshCache() local clicked = 0 local function normalized(value) return tostring(value or ""):upper():gsub("%W", "") end local function rewardButtonContext(btn) local parts = { btn.Name or "" } local parent = btn.Parent for _ = 1, 5 do if not parent then break end parts[#parts + 1] = parent.Name or "" parent = parent.Parent end return normalized(table.concat(parts, " ")) end local function hasAnyToken(text, tokens) for _, token in ipairs(tokens) do if text:find(token, 1, true) then return true end end return false end local actionTokens = { "CLAIM", "COLLECT", "GET", "TAKE", "RECEIVE", "OK", "FREE", "OPEN", "UNLOCK" } local rewardTokens if mode == "daily" then rewardTokens = { "DAILY", "PRIZE", "LOGIN", "GIFT", "REWARD", "REWARDS", "GIFTS", "BONUS", "DAILYPRIZE", "DAILYLOGIN", "LOGINREWARD", "LOGINPRIZE", "CLAIMPRIZE", "DAILYBONUS", "FREEPRIZE" } elseif mode == "spin" then rewardTokens = { "SPIN", "WHEEL", "DAILYSPIN", "SPINREWARD" } else rewardTokens = { "REWARD", "REWARDS", "GIFT", "GIFTS", "DAILY", "SPIN", "PRIZE", "BONUS", "LOGIN", "DAILYPRIZE", "DAILYLOGIN", "LOGINREWARD", "LOGINPRIZE", "CLAIMPRIZE", "DAILYBONUS", "FREEPRIZE" } end local blockedContextTokens = { "MERCHANT", "VENDOR", "SHOP", "STORE", "SPECIAL" } local function isExcluded(combinedKey) if hasAnyToken(combinedKey, blockedContextTokens) then return true end -- In daily mode, skip buttons whose only reward context is a spin wheel if mode == "daily" and combinedKey:find("SPIN", 1, true) and not combinedKey:find("DAILY", 1, true) and not combinedKey:find("LOGIN", 1, true) and not combinedKey:find("PRIZE", 1, true) then return true end -- In spin mode, skip buttons whose only context is daily/prize without spin if mode == "spin" and not combinedKey:find("SPIN", 1, true) and not combinedKey:find("WHEEL", 1, true) then return true end return false end for _, btn in ipairs(Data.buttons) do if clicked >= 5 then break end local compact = compactButtonText(btn) local textKey = normalized(compact) local contextKey = rewardButtonContext(btn) local combinedKey = textKey .. contextKey local directRewardAction = textKey == "CLAIM" or textKey == "COLLECT" or textKey == "GET" or textKey == "TAKE" or textKey == "OK" or textKey == "FREE" or textKey == "OPEN" local contextualRewardAction = hasAnyToken(combinedKey, actionTokens) and hasAnyToken(combinedKey, rewardTokens) if not isExcluded(combinedKey) and (directRewardAction or contextualRewardAction) then if clickButton(btn) then clicked = clicked + 1 end end end return clicked > 0 end local function isBubbleSlot(item) return item and not item.Hidden and (item.Id == "Bubble" or item.Bubble ~= nil) end local function breakBubbleSlot(slot) local slotStr = tostring(slot) -- Primer: Attempt to wake the bubble up first fireReplica("MergeGrid", "Tap", slotStr, nil, "ReplicaTapBubble:" .. slotStr, 0.08) local ok = fireReplica("MergeGrid", "BreakBubble", slotStr, nil, "ReplicaBreakBubble:" .. slotStr, 0.08) if not ok then ok = fireEvent(Data.Events.BreakBubble, "BreakBubble:" .. slotStr, 0.12, slotStr) end if not ok then ok = remoteCall("MergeGrid", "BreakBubble", slotStr) end return ok end local function breakExistingBubbles(slots, limit, includeLockedFallback) local found, sent = 0, 0 if not slots then local grid = gridData() slots = grid and grid.Slots or {} end for slot, item in pairs(slots) do if isBubbleSlot(item) or (includeLockedFallback and item and not item.Hidden and item.Locked) then found = found + 1 if sent < (limit or 6) and breakBubbleSlot(slot) then sent = sent + 1 end end end return found, sent end local function countEmpty(slots) local n = 0 for i = 1, Data.GridSize do if not slots[tostring(i)] then n = n + 1 end end return n end automationPausedForUi = function() if State.dragActive then return true end local focusedBox = nil pcall(function() focusedBox = Services.UserInputService:GetFocusedTextBox() end) return focusedBox ~= nil end local function processGrid() local gridCycleStart = os.clock() local mergeSpeedValue = math.clamp(tonumber(Settings.mergeSpeed) or 5, 1, 10) local gridTimeBudget = mergeSpeedValue >= 9 and 0.028 or (mergeSpeedValue >= 7 and 0.040 or 0.060) State.lastGridCycleStarted = tick() if automationPausedForUi() then return end local function finishCycle() -- Handled by separate scheduler end local grid = gridData() if not grid then finishCycle() return end local slots = grid.Slots or {} local queues = grid.Queues or {} local cooldowns = grid.Cooldowns or {} local uses = grid.Uses or {} local smart = Settings.smartProcessing and true or false local demand = {} if smart then demand = cachedOrderDemand() end local empty = countEmpty(slots) local occupied = 0 local groups = {} local itemIdToSlots = {} -- slot index keyed by tostring(itemId) for reservedSlots local wildcards = {} -- wildcard item slots local wildcardCandidates = {} -- potential wildcard-merge targets (filtered by reservedSlots later) for slot, item in pairs(slots) do if item then occupied = occupied + 1 if not item.Hidden then local def = Data.Items[item.Id] local idStr = tostring(item.Id) if def then if def.MergesInto then local g = groups[item.Id] if not g then g = { free = {}, locked = {} } groups[item.Id] = g end if item.Locked then g.locked[#g.locked + 1] = slot else g.free[#g.free + 1] = slot end end if not item.Locked then local lst = itemIdToSlots[idStr] if not lst then lst = {} itemIdToSlots[idStr] = lst end lst[#lst + 1] = slot if def.Wildcard then wildcards[#wildcards + 1] = slot elseif def.MergesInto and not def.OnTap and not def.AutoGen and not idStr:find("Generator", 1, true) then wildcardCandidates[#wildcardCandidates + 1] = slot end end end end end end -- Pre-built demand-family set for O(1) lookups instead of per-item O(demand) scans local demandFamilies = {} if smart and next(demand) ~= nil then for id in pairs(demand) do demandFamilies[itemFamily(id)] = true end end -- Fast demand check: uses pre-built demandFamilies instead of scanning demand keys per call local function fastCanHelpDemand(itemId, def) if next(demand) == nil then return false end local idStr = tostring(itemId) if demand[idStr] then return true end if demandFamilies[itemFamily(idStr)] then return true end if def and def.OnTap and poolCanHelpDemand(def.OnTap.Pool, demand) then return true end if def and def.AutoGen and poolCanHelpDemand(def.AutoGen.Pool, demand) then return true end return false end local heavyGrid = occupied >= 42 or empty >= 24 local budget if occupied >= 50 then budget = 10 elseif heavyGrid then if smart then budget = 16 else budget = 18 end elseif smart then budget = 16 else budget = 32 end if mergeSpeedValue >= 9 then budget = math.min(budget, 8) elseif mergeSpeedValue >= 7 then budget = math.min(budget, 12) end if performanceBusy() then budget = math.max(1, math.floor(budget * 0.65)) end local now = os.time() local energy = getCurrency("Energy") local sentThisCycle = 0 local function send(action, a, b, suffix, cost) if automationPausedForUi() then budget = 0 return false end if budget <= 0 then return false end if remoteCall("MergeGrid", action, a, b, suffix) then budget = budget - 1 sentThisCycle = sentThisCycle + 1 if sentThisCycle % 8 == 0 then task.wait() end if os.clock() - gridCycleStart > gridTimeBudget then budget = 0 return true end if cost and cost > 0 then energy = math.max(0, energy - cost) end return true end return false end local function retrieveInbox() if Settings.claimInbox and empty > 0 and grid.Inbox and #grid.Inbox > 0 then local sentAny = false for i = 1, math.min(#grid.Inbox, 3) do if send("InboxRetrieve", nil, nil, "inbox:" .. tostring(i)) then sentAny = true end if i < math.min(#grid.Inbox, 3) then task.wait(0.03) end end return sentAny end return false end local reservedSlots = {} if smart and next(demand) ~= nil then for wantedId, needed in pairs(demand) do local candidates = itemIdToSlots[tostring(wantedId)] if candidates and #candidates > 0 then if #candidates > 1 then table.sort(candidates, function(a, b) return (tonumber(a) or 0) < (tonumber(b) or 0) end) end local cap = math.min(tonumber(needed) or 0, #candidates) for i = 1, cap do reservedSlots[candidates[i]] = true end end end end -- Filter wildcard targets: exclude slots reserved for order fulfillment local wildcardTargets = {} for _, slot in ipairs(wildcardCandidates) do if not reservedSlots[slot] then wildcardTargets[#wildcardTargets + 1] = slot end end if not Settings.processGrid then if retrieveInbox() then finishCycle() return end finishCycle() return end if Settings.processGrid then if Settings.clearBubbles then if tick() - (State.lastBubbleGridScan or 0) >= 1.0 then State.lastBubbleGridScan = tick() local bubbleCount = breakExistingBubbles(slots, 4) if bubbleCount > 0 then finishCycle() return end end end if retrieveInbox() then finishCycle() return end local mergeOrder = {} local seen = {} if smart then local demandIdSet = {} for id in pairs(demand) do demandIdSet[tostring(id)] = true end for groupId in pairs(groups) do if demandIdSet[tostring(groupId)] and not seen[groupId] then seen[groupId] = true mergeOrder[#mergeOrder + 1] = groupId end end for groupId in pairs(groups) do if not seen[groupId] and demandFamilies[itemFamily(groupId)] then seen[groupId] = true mergeOrder[#mergeOrder + 1] = groupId end end for groupId in pairs(groups) do if not seen[groupId] then seen[groupId] = true mergeOrder[#mergeOrder + 1] = groupId end end end if not smart then for id in pairs(groups) do if not seen[id] then mergeOrder[#mergeOrder + 1] = id end end end local merged = false for _, id in ipairs(mergeOrder) do local group = groups[id] local list = {} if group and group.free then for _, slot in ipairs(group.free) do if not reservedSlots[slot] then list[#list + 1] = slot end end end local i = 1 while list and i < #list and budget > 0 do if send("Merge", list[i], list[i + 1], list[i] .. ":" .. list[i + 1]) then State.sessionMerges = State.sessionMerges + 1 merged = true end i = i + 2 end if list and i == #list and group and #group.locked > 0 and budget > 0 then if send("Merge", list[i], group.locked[1], list[i] .. ":" .. group.locked[1]) then State.sessionMerges = State.sessionMerges + 1 merged = true end end end if not merged then local wi = 1 local ti = 1 while wi <= #wildcards and ti <= #wildcardTargets and budget > 0 do if send("Merge", wildcards[wi], wildcardTargets[ti], wildcards[wi] .. ":" .. wildcardTargets[ti]) then State.sessionMerges = State.sessionMerges + 1 merged = true end wi = wi + 1 ti = ti + 1 end end if merged then finishCycle() return end local tapped = false for slot, item in pairs(slots) do if budget <= 0 then finishCycle() return end if item and not item.Hidden and not item.Locked then local def = Data.Items[item.Id] if def and def.OnTap then local ok = true local energyCost = tonumber(def.OnTap.EnergyCost) or 0 local containerTap = shouldTapContainer(item.Id, def) if smart and (reservedSlots[slot] or (not containerTap and not fastCanHelpDemand(item.Id, def))) then ok = false end if energyCost > 0 and energy < energyCost then ok = false end if cooldowns[slot] and now < cooldowns[slot] then ok = false end if def.OnTap.MaxUses and uses[slot] ~= nil and (uses[slot] or 1) <= 0 then ok = false end if def.OnTap.Type == "spawn" and ((not queues[slot] or #queues[slot] == 0) or empty <= 1) then ok = false end if ok and send("Tap", slot, nil, nil, energyCost) then tapped = true end end if def and def.AutoGen and (not smart or fastCanHelpDemand(item.Id, def)) and queues[slot] and #queues[slot] > 0 and empty > 1 and not (cooldowns[slot] and now < cooldowns[slot]) then local autoGenCost = tonumber(def.OnTap and def.OnTap.EnergyCost) or 0 if send("TapAutoGen", slot, nil, nil, autoGenCost) then tapped = true end end end end if tapped then finishCycle() return end for slot, item in pairs(slots) do if budget <= 0 then finishCycle() return end if item and not item.Hidden and not item.Locked then local def = Data.Items[item.Id] if def and def.OnCollect and not reservedSlots[slot] then send("Collect", slot) end end end end finishCycle() State.lastGridCycleDuration = os.clock() - gridCycleStart markHeavyWork(State.lastGridCycleDuration) end local function offerAmount(value) if type(value) == "number" then return value end if type(value) == "string" then return tonumber(value) end if type(value) == "table" then return tonumber(value.Amount or value.amount) end return nil end offerPrice = function(offer) return offerAmount(offer.Price) or offerAmount(offer.price) or offerAmount(offer.Cost) or offerAmount(offer.cost) end local function offerHasRobuxMarker(value, seen, depth) local valueType = type(value) if valueType == "string" then local text = value:lower() return text:find("robux", 1, true) ~= nil or text:find("r$", 1, true) ~= nil end if valueType ~= "table" then return false end if (depth or 0) > 4 then return false end seen = seen or {} if seen[value] then return false end seen[value] = true for k, v in pairs(value) do if offerHasRobuxMarker(tostring(k), seen, (depth or 0) + 1) or offerHasRobuxMarker(v, seen, (depth or 0) + 1) then return true end end return false end local function offerHasPositiveAmount(offer, fields) for _, field in ipairs(fields) do local amount = offerAmount(offer[field]) if amount and amount > 0 then return true end end return false end isPaidOffer = function(offer) if type(offer) ~= "table" then return false end -- Only classify as paid if there is clear evidence of Robux payment -- ProductId/Id/ItemId alone must NOT mean paid -- Check for explicit Robux price if offer.RobuxPrice ~= nil and tonumber(offer.RobuxPrice) > 0 then return true end -- Check for currency field indicating Robux local currency = offer.Currency or offer.currency if currency and (currency == "Robux" or tostring(currency):upper() == "ROBUX") then return true end -- GamePass always requires Robux if offer.GamePassId ~= nil or offer.GamepassId ~= nil then return true end -- DeveloperProduct only if it has Robux price/currency evidence if offer.DeveloperProductId ~= nil or offer.DevProductId ~= nil then -- Only classify as paid if there's also Robux price/currency if offer.RobuxPrice ~= nil or (currency and (currency == "Robux" or tostring(currency):upper() == "ROBUX")) then return true end end -- Check for any explicit field saying Robux if offer.RobuxProductId ~= nil then return true end return false end isFreeOffer = function(section, offer) if type(offer) ~= "table" then return false end -- Do NOT call isPaidOffer first in a way that blocks Daily offers incorrectly -- Check for explicit Free flag if offer.Free == true or offer.free == true or offer.IsFree == true or offer.isFree == true then return true end -- Check price - 0 should count as free local price = offerPrice(offer) if price ~= nil then return price <= 0 end -- Daily section should count as free unless clearly Robux-paid if section == "Daily" then return not isPaidOffer(offer) end return false end local DEBUG_MERCHANT = false local function merchantDebugPrint(...) if not DEBUG_MERCHANT then return end local args = table.pack(...) task.defer(function() print("[Merchant]", table.unpack(args, 1, args.n)) end) end local function offerRemaining(section, offer, merchant) local idx = offer.Index or offer.index if not idx then return 0 end local mi = Data.MerchantInfo if not mi then mi = safeRequire("Shared", "MerchantInfo") Data.MerchantInfo = mi end local bought = vendorPurchasedCount(section, merchant, mi, idx) if mi and mi.GetRemainingStock then local ok, rem = pcall(function() return mi.GetRemainingStock(offer, bought) end) if ok and tonumber(rem) then return tonumber(rem) end end return math.max(0, tonumber(offer.Stock or offer.stock or 1) - bought) end local function merchantWindowKey(section, merchant) if type(merchant) ~= "table" then return "unknown" end return tostring(merchant[section .. "Start"] or merchant[section .. "start"] or merchant.Start or merchant.start or "unknown") end local function merchantOfferKey(section, idx, merchant) return tostring(section) .. ":" .. tostring(idx) .. ":" .. merchantWindowKey(section, merchant) end buyFreeVendorOffers = function() local data = gameData() local mi = Data.MerchantInfo if not mi or not mi.BuildOffers then mi = safeRequire("Shared", "MerchantInfo") Data.MerchantInfo = mi end local ev = Data.Events.BuyVendorOffer if not ev then local events = Services.ReplicatedStorage:FindFirstChild("Events") ev = events and ( events:FindFirstChild("BuyMerchantOffer") or events:FindFirstChild("BuyVendorOffer") or events:FindFirstChild("PurchaseOffer") ) if ev then Data.Events.BuyVendorOffer = ev end end local merchant = data and data.Merchant if not ev then if not State.warnedMerchantRemoteMissing then State.warnedMerchantRemoteMissing = true UI.addFeed("Merchant remote missing") end return end if not (merchant and mi and mi.BuildOffers) then if UI.vendorStatus then UI.vendorStatus.Text = "Status: Merchant data unavailable" end return end -- FIX: merchant local stock cache – init once State.vendorLocalConsumed = State.vendorLocalConsumed or {} -- OPTIMIZATION: batch refresh and cycle cap to prevent lag local needsVendorRefresh = false local cyclePurchaseCount = 0 local maxPurchasesPerCycle = 5 local freeOfferFound = false local finalStatus = nil local bought = false local boughtSelected = false Settings.vendorQuantities = type(Settings.vendorQuantities) == "table" and Settings.vendorQuantities or {} State.vendorBuyCounts = type(State.vendorBuyCounts) == "table" and State.vendorBuyCounts or {} -- FIX: Check if selected item has valid quantity for manual purchase local selectedItemKey = Settings.vendorSelectedItemKey local selectedItemQty = selectedItemKey and tonumber(Settings.vendorQuantities[selectedItemKey]) or nil local selectedItemValidQty = selectedItemQty and selectedItemQty > 0 and math.floor(selectedItemQty) or nil local selectedItemIsNoBox = false if selectedItemKey then if tostring(selectedItemKey):match("^Daily:") then selectedItemIsNoBox = true elseif UI.vendorDropdown and type(UI.vendorDropdown.optionMeta) == "table" then for _, meta in pairs(UI.vendorDropdown.optionMeta) do if meta.key == selectedItemKey and meta.quantityInput == false then selectedItemIsNoBox = true break end end end end if selectedItemIsNoBox and State.vendorActive and selectedItemKey then selectedItemValidQty = 1 end -- FIX: If Start is ON but selected item has no valid quantity, show status and do not purchase if State.vendorActive and selectedItemKey and not selectedItemIsNoBox and not selectedItemValidQty then if UI and UI.vendorStatus then UI.vendorStatus.Text = "Status: No selected quantity" end return end -- Collect verification tasks to batch them local verificationTasks = {} for _, section in ipairs({ "Daily", "Flash" }) do local sectionStart = merchant[section .. "Start"] or merchant[section .. "start"] or merchant.Start or merchant.start local ok, offers = pcall(function() return mi.BuildOffers(section, sectionStart) end) if ok and type(offers) == "table" then for _, offer in ipairs(offers) do if type(offer) == "table" then local idx = offer.Index or offer.index local key = idx and merchantOfferKey(section, idx, merchant) -- FIX: merchant local stock cache – effective remaining local serverBought = idx and vendorPurchasedCount(section, merchant, mi, idx) or 0 local rawRemaining = idx and offerRemaining(section, offer, merchant) or 0 local localConsumed = key and reconcileVendorLocalConsumed(key, serverBought) or 0 local remaining = math.max(0, rawRemaining - localConsumed) local isPaid = isPaidOffer(offer) local isFree = isFreeOffer(section, offer) -- FIX: paid offers – clear stale quantity if isPaid and key and Settings.vendorQuantities[key] then Settings.vendorQuantities[key] = nil end -- RULE: Auto-buy Free Items only for no-box free offers local wantsFree = idx and key and Settings.vendorAutoFree and isFree and not isPaid -- RULE: Boxed offers require valid positive quantity AND Start is ON local manualQty = State.vendorActive and key and tonumber(Settings.vendorQuantities[key]) or nil local validManualQty = manualQty and manualQty > 0 and math.floor(manualQty) or nil -- FIX: Fallback to 1 for no-box/Daily selected offers so they can be bought local isNoBox = section == "Daily" if not isNoBox and UI.vendorDropdown and type(UI.vendorDropdown.optionMeta) == "table" then for _, meta in pairs(UI.vendorDropdown.optionMeta) do if meta.key == key and meta.quantityInput == false then isNoBox = true break end end end if isNoBox and State.vendorActive and key == Settings.vendorSelectedItemKey and not isPaid then validManualQty = 1 end -- FIX: Cap manual quantity to remaining available stock if validManualQty and remaining and validManualQty > remaining then validManualQty = remaining if not isNoBox then Settings.vendorQuantities[key] = remaining end end -- RULE: Selected item alone does NOT trigger purchase for boxed offers -- ONLY the selected item with its own valid quantity can be bought when Start is ON local wantsSelected = idx and key and State.vendorActive and section ~= "Daily" and Settings.vendorSelectedItemKey == key and validManualQty ~= nil and validManualQty > 0 and not isPaid -- RULE: Daily selected offer buy once when selected and Start is ON (no quantity box) local isDailyManualSelect = idx and key and State.vendorActive and section == "Daily" and not isPaid and Settings.vendorSelectedItemKey == key merchantDebugPrint( "[DEBUG-BUY] offer:", section, idx, "key:", key, "isFree:", isFree, "isPaid:", isPaid, "remaining:", remaining, "wantsFree:", wantsFree, "wantsSelected:", wantsSelected, "isDailyManualSelect:", isDailyManualSelect, "validManualQty:", validManualQty, "cyclePurchaseCount:", cyclePurchaseCount ) if idx and key and remaining > 0 then local boughtCount = key and math.max(tonumber(State.vendorBuyCounts[key]) or 0, serverBought) or 0 -- FIX: when user changes quantity, reset all tracking for that item local currentLocalConsumed = key and reconcileVendorLocalConsumed(key, serverBought) or 0 local shouldReset = false if manualQty and boughtCount > 0 then shouldReset = true elseif currentLocalConsumed >= remaining then shouldReset = true end if shouldReset then State.vendorBuyCounts[key] = 0 setVendorLocalConsumed(key, 0) boughtCount = 0 end -- OPTIMIZATION: hard cap per cycle to prevent lag if cyclePurchaseCount >= maxPurchasesPerCycle then merchantDebugPrint("[DEBUG-BUY] Cycle purchase cap reached, stopping for this cycle") break end -- ── Auto-free offers: buy exactly once per key/window (no-box only) ── if wantsFree and boughtCount < 1 then local beforeBought = vendorPurchasedCount(section, merchant, mi, idx) local beforeRemaining = offerRemaining(section, offer, merchant) merchantDebugPrint("[DEBUG-BUY] Free offer BEFORE - bought:", beforeBought, "remaining:", beforeRemaining) local limiterKey = "MerchantFree:" .. key .. ":" .. tostring(boughtCount + 1) merchantDebugPrint("[DEBUG-BUY] Firing free event for", section, idx) local okFire = fireEvent(ev, limiterKey, 0.05, section, idx) merchantDebugPrint("[DEBUG-BUY] Free event result:", okFire) if okFire then cyclePurchaseCount = cyclePurchaseCount + 1 needsVendorRefresh = true bought = true -- Batch verification task table.insert(verificationTasks, { key = key, section = section, idx = idx, offer = offer, beforeBought = beforeBought, beforeRemaining = beforeRemaining, boughtCount = boughtCount, type = "free" }) end -- ── Daily manual-select: buy exactly once (no-box only) ── elseif isDailyManualSelect and boughtCount < 1 then local beforeBought = vendorPurchasedCount(section, merchant, mi, idx) local beforeRemaining = offerRemaining(section, offer, merchant) merchantDebugPrint("[DEBUG-BUY] Daily-select BEFORE - bought:", beforeBought, "remaining:", beforeRemaining) local limiterKey = "MerchantDaily:" .. key .. ":" .. tostring(boughtCount + 1) merchantDebugPrint("[DEBUG-BUY] Firing daily-select event for", section, idx) local okFire = fireEvent(ev, limiterKey, 0.05, section, idx) merchantDebugPrint("[DEBUG-BUY] Daily-select event result:", okFire) if okFire then cyclePurchaseCount = cyclePurchaseCount + 1 needsVendorRefresh = true bought = true boughtSelected = true -- Batch verification task table.insert(verificationTasks, { key = key, section = section, idx = idx, offer = offer, beforeBought = beforeBought, beforeRemaining = beforeRemaining, type = "daily" }) end -- ── Flash manual qty: buy exactly validManualQty times (boxed offers) ── elseif wantsSelected then local targetPurchases = math.min(validManualQty, maxPurchasesPerCycle - cyclePurchaseCount) local initialBought = vendorPurchasedCount(section, merchant, mi, idx) local initialRemaining = offerRemaining(section, offer, merchant) merchantDebugPrint("[DEBUG-BUY] Manual batch BEFORE - bought:", initialBought, "remaining:", initialRemaining, "target:", targetPurchases) local firedCount = 0 for purchaseNum = 1, targetPurchases do if rawRemaining - purchaseNum + 1 <= 0 then break end local limiterKey = "MerchantSelected:" .. key .. ":" .. tostring(purchaseNum) local okFire = fireEvent(ev, limiterKey, 0.05, section, idx) if okFire then firedCount = firedCount + 1 cyclePurchaseCount = cyclePurchaseCount + 1 else merchantDebugPrint("[DEBUG-BUY] Manual fire failed at attempt:", purchaseNum, "stopping") break end -- OPTIMIZATION: hard cap per cycle if cyclePurchaseCount >= maxPurchasesPerCycle then merchantDebugPrint("[DEBUG-BUY] Cycle purchase cap reached during manual batch") break end end if firedCount > 0 then needsVendorRefresh = true bought = true boughtSelected = true -- Batch verification task table.insert(verificationTasks, { key = key, section = section, idx = idx, offer = offer, initialBought = initialBought, initialRemaining = initialRemaining, firedCount = firedCount, targetPurchases = targetPurchases, type = "manual" }) else merchantDebugPrint("[DEBUG-BUY] No purchases fired - keeping original quantity") end end end end end end end -- OPTIMIZATION: Process all verification tasks in one batch if #verificationTasks > 0 then task.defer(function() task.wait(0.25) local freshData = gameData() local freshMerchant = freshData and freshData.Merchant for _, taskData in ipairs(verificationTasks) do if taskData.type == "free" then local afterBought = freshMerchant and vendorPurchasedCount(taskData.section, freshMerchant, mi, taskData.idx) or taskData.beforeBought local afterRemaining = freshMerchant and offerRemaining(taskData.section, taskData.offer, freshMerchant) or taskData.beforeRemaining merchantDebugPrint("[DEBUG-BUY] Free offer AFTER - bought:", afterBought, "remaining:", afterRemaining) local purchaseConfirmed = (afterBought > taskData.beforeBought) or (afterRemaining < taskData.beforeRemaining) if purchaseConfirmed then State.vendorBuyCounts[taskData.key] = taskData.boughtCount + 1 addVendorLocalConsumed(taskData.key, 1) State.claimedFreeOffers[taskData.key] = true else merchantDebugPrint("[DEBUG-BUY] Free offer NOT confirmed - no data change detected") end elseif taskData.type == "daily" then local afterBought = freshMerchant and vendorPurchasedCount(taskData.section, freshMerchant, mi, taskData.idx) or taskData.beforeBought local afterRemaining = freshMerchant and offerRemaining(taskData.section, taskData.offer, freshMerchant) or taskData.beforeRemaining merchantDebugPrint("[DEBUG-BUY] Daily-select AFTER - bought:", afterBought, "remaining:", afterRemaining) local purchaseConfirmed = (afterBought > taskData.beforeBought) or (afterRemaining < taskData.beforeRemaining) if purchaseConfirmed then Settings.vendorSelectedItemKey = nil else merchantDebugPrint("[DEBUG-BUY] Daily-select NOT confirmed - no data change detected") end elseif taskData.type == "manual" then local finalBought = freshMerchant and vendorPurchasedCount(taskData.section, freshMerchant, mi, taskData.idx) or taskData.initialBought local finalRemaining = freshMerchant and offerRemaining(taskData.section, taskData.offer, freshMerchant) or taskData.initialRemaining merchantDebugPrint("[DEBUG-BUY] Manual batch AFTER - bought:", finalBought, "remaining:", finalRemaining) local boughtDiff = finalBought - taskData.initialBought local remainingDiff = taskData.initialRemaining - finalRemaining local confirmedCount = math.max(boughtDiff, remainingDiff) confirmedCount = math.min(confirmedCount, taskData.firedCount) merchantDebugPrint("[DEBUG-BUY] Confirmed purchases:", confirmedCount, "of", taskData.firedCount, "fired") if confirmedCount > 0 then State.vendorBuyCounts[taskData.key] = taskData.initialBought + confirmedCount for _ = 1, confirmedCount do addVendorLocalConsumed(taskData.key, 1) end if confirmedCount >= taskData.targetPurchases and taskData.key then Settings.vendorQuantities[taskData.key] = nil merchantDebugPrint("[DEBUG-BUY] All target purchases confirmed, cleared quantity") elseif confirmedCount < taskData.targetPurchases and taskData.key then local remainingQty = taskData.targetPurchases - confirmedCount Settings.vendorQuantities[taskData.key] = remainingQty merchantDebugPrint("[DEBUG-BUY] Partial success - keeping remaining quantity:", remainingQty) end else merchantDebugPrint("[DEBUG-BUY] No purchases confirmed - keeping original quantity") end end end -- OPTIMIZATION: Single refresh after all verifications complete if needsVendorRefresh then scheduleVendorDropdownRefresh(0.25) end end) end -- OPTIMIZATION: Update status once at the end if UI.vendorStatus then if not bought then finalStatus = "No free offers" elseif boughtSelected then finalStatus = "Offer bought" else finalStatus = "Free offer claimed" end UI.vendorStatus.Text = "Status: " .. finalStatus end end function updateAntiAfk() if Settings.antiAfk and not State.afkConn then local function pulse() if tick() - State.lastAfk < 20 then return end State.lastAfk = tick() pcall(function() local vu = game:GetService("VirtualUser") vu:CaptureController() vu:Button2Down( Vector2.new(0, 0), Services.Workspace.CurrentCamera and Services.Workspace.CurrentCamera.CFrame or CFrame.new() ) task.wait(0.05) vu:Button2Up( Vector2.new(0, 0), Services.Workspace.CurrentCamera and Services.Workspace.CurrentCamera.CFrame or CFrame.new() ) end) pcall(function() local char = player.Character local hrp = char and char:FindFirstChild("HumanoidRootPart") if hrp then char:MoveTo(hrp.Position + Vector3.new(0, 0.08, 0)) end end) end if player and player.Idled then State.afkConn = player.Idled:Connect(pulse) else State.afkConn = { Disconnect = function() end } end local token = {} State.afkToken = token task.spawn(function() while State.running and Settings.antiAfk and State.afkToken == token do task.wait(55) if State.running and Settings.antiAfk and State.afkToken == token then pulse() end end end) elseif not Settings.antiAfk and State.afkConn then State.afkToken = nil State.afkConn:Disconnect() State.afkConn = nil end end local function resolveBuildHoldRemote() if Data.Events.BuildHold then return Data.Events.BuildHold end local ev = Services.ReplicatedStorage:FindFirstChild("Events") and Services.ReplicatedStorage.Events:FindFirstChild("BuildHold") if not ev then ev = Services.ReplicatedStorage:FindFirstChild("RemoteEvents") and Services.ReplicatedStorage.RemoteEvents:FindFirstChild("BuildHold") end if not ev then ev = Services.ReplicatedStorage:FindFirstChild("BuildHold", true) end if ev then Data.Events.BuildHold = ev end return ev end local function fireBuildHold(target, holding) local ev = resolveBuildHoldRemote() if not ev then return false end local key = "BuildHold:" .. tostring(target) .. ":" .. tostring(holding) if not canFire(key, 0.4) then return false end local ok = pcall(function() ev:FireServer(target, holding) end) return ok end processBuild = function() if not Settings.constructBuilding then if State.currentBuild then fireBuildHold(State.currentBuild, false) State.currentBuild = nil end return end local ev = resolveBuildHoldRemote() if not ev then if not State.warnedBuildHoldMissing then State.warnedBuildHoldMissing = true UI.addFeed("BuildHold remote missing") end return end local plots = Services.Workspace:FindFirstChild("PlayerPlots") local plot = plots and player and plots:FindFirstChild("Plot_" .. tostring(player.UserId)) if not plot then return end local target = nil local buildable = plot:FindFirstChild("Buildable") or plot:FindFirstChild("Buildables") or plot:FindFirstChild("Buildings") if buildable then for _, obj in ipairs(buildable:GetChildren()) do if obj:IsA("Model") or obj:IsA("BasePart") then local cfg = Data.BuildConfig and Data.BuildConfig.Pieces and Data.BuildConfig.Pieces[obj.Name] if cfg and not cfg.Disabled then local data = gameData() local progress = (data and data.Plot and data.Plot.Buildable and data.Plot.Buildable[obj.Name]) or 0 if progress < (cfg.Cost or 0) then target = obj.Name break end end end end end if target then if State.currentBuild and State.currentBuild ~= target then fireBuildHold(State.currentBuild, false) end State.currentBuild = target fireBuildHold(target, true) elseif State.currentBuild then fireBuildHold(State.currentBuild, false) State.currentBuild = nil end end function processObby() if not Settings.handleObstacleCourse then return end if not Data.Events.StartObby then local events = Services.ReplicatedStorage:FindFirstChild("Events") Data.Events.StartObby = events and events:FindFirstChild("StartObby") end if not Data.Events.StartObby then return false end if State.obbyBusy then return false end State.obbyBusy = true local root = getRoot() if not root then State.obbyBusy = false return false end local original = root.CFrame local difficulty = Settings.obstacleDifficulty or "Hard" local started = fireEvent(Data.Events.StartObby, "Obby:Start:" .. tostring(difficulty), 0.45, difficulty) if not started then State.obbyBusy = false return false end task.wait(0.5) local endpoint = Services.Workspace:FindFirstChild("ObbyEnd", true) or Services.Workspace:FindFirstChild("Reward", true) or Services.Workspace:FindFirstChild("Finish", true) or Services.Workspace:FindFirstChild("Prize", true) if endpoint and endpoint:IsA("BasePart") then pcall(function() root.CFrame = endpoint.CFrame + Vector3.new(0, 2, 0) end) task.wait(0.2) elseif endpoint and endpoint:IsA("Model") then local part = endpoint.PrimaryPart or endpoint:FindFirstChildWhichIsA("BasePart") if part then pcall(function() root.CFrame = part.CFrame + Vector3.new(0, 2, 0) end) task.wait(0.2) end end fireEvent(Data.Events.EndObby, "Obby:End", 60) task.wait(0.05) pcall(function() root.CFrame = original end) if started then State.lastObby = tick() end State.obbyBusy = false return started end function Stats.update() if State.closed then return end if tick() - State.lastStatsUpdate < 1.0 then return end State.lastStatsUpdate = tick() Stats.saveRuntimeProgress(false) local function setStat(key, value) if UI.stats[key] then local text = value ~= nil and tostring(value) or "N/A" if UI.stats[key].Text ~= text then UI.stats[key].Text = text end end end local function currencyStat(primary, ...) local ls = leaderStat(primary, ...) if ls ~= nil then return ls end local d = gameData() if not d then return nil end for _, bucket in ipairs({ d.Currencies, d.Currency, d.Wallet, d.Resources, d.Stats }) do local n = normNumber(caseValue(bucket, primary)) if n ~= nil then return n end for _, alias in ipairs({ ... }) do n = normNumber(caseValue(bucket, alias)) if n ~= nil then return n end end end return nil end local function trackSpent(current, lastKey, spentKey) local profile = Stats.userProfile() current = tonumber(current) if current ~= nil then local last = State[lastKey] State.pendingSpent = State.pendingSpent or {} local pending = State.pendingSpent[spentKey] if pending then if current >= pending.from then State.pendingSpent[spentKey] = nil State[lastKey] = current else pending.to = current pending.amount = math.max(0, pending.from - current) pending.samples = (tonumber(pending.samples) or 1) + 1 if pending.samples >= 2 or tick() - (pending.at or 0) >= 1.0 then pending.confirmed = true Stats.commitPendingSpent(false) end State[lastKey] = current end elseif last ~= nil and current < last then local item = { from = last, to = current, amount = last - current, spentKey = spentKey, lastKey = lastKey, samples = 1, at = tick(), } State.pendingSpent[spentKey] = item Stats.schedulePendingSpentCommit(spentKey, item) else State[lastKey] = current end end return profile[spentKey] end local energy = currencyStat("Energy", "energy") local coins = currencyStat("Coins", "coins", "Gold") local gems = currencyStat("Gems", "gems", "Diamond") local profile = Stats.userProfile() setStat("energy", energy) setStat("coins", coins) setStat("gems", gems) setStat("energySpent", trackSpent(energy, "lastEnergy", "energySpent")) setStat("coinsSpent", trackSpent(coins, "lastCoins", "coinsSpent")) setStat("gemsSpent", trackSpent(gems, "lastGems", "gemsSpent")) setStat("runtime", Stats.formatTimeSpan(profile.runtimeSeconds)) end function Daily.remotePath(remote) local ok, path = pcall(function() return remote:GetFullName() end) return ok and path or tostring(remote) end function Daily.rememberRemote(remote, source) if not Daily.isRemote(remote) then return nil end if Data.Events then Data.Events.ClaimDaily = remote end local path = Daily.remotePath(remote) if State.dailyRemoteDebugPath ~= path then State.dailyRemoteDebugPath = path Daily.debug("remote resolved", source or "unknown", path) end return remote end function Daily.stateKeyRelevant(key) key = tostring(key or ""):lower() if key:find("daily", 1, true) or key:find("login", 1, true) or key:find("prize", 1, true) or key:find("gift", 1, true) or key:find("streak", 1, true) then return true end if key:find("reward", 1, true) and (key:find("claim", 1, true) or key:find("daily", 1, true) or key:find("login", 1, true)) then return true end if key:find("claim", 1, true) and (key:find("daily", 1, true) or key:find("login", 1, true) or key:find("gift", 1, true)) then return true end return false end function Daily.stateValueTrackable(path) path = tostring(path or ""):lower() return not (path:find("countdown", 1, true) or path:find("cooldown", 1, true) or path:find("timeleft", 1, true) or path:find("timer", 1, true) or path:find("remaining", 1, true) or path:find("seconds", 1, true)) end function Daily.stateSignature() local data = gameData() if type(data) ~= "table" then return nil end local parts = {} local seen = {} local visited = 0 local maxVisited = 350 local maxPerTable = 120 local function addPart(path, value) local valueType = type(value) if valueType == "string" or valueType == "number" or valueType == "boolean" then parts[#parts + 1] = path .. "=" .. tostring(value) end end local function visit(value, path, depth, relevant) if visited >= maxVisited or depth > 5 then return end if type(value) ~= "table" then if relevant then addPart(path, value) end return end if seen[value] then return end seen[value] = true visited = visited + 1 local scanned = 0 for key, child in pairs(value) do scanned = scanned + 1 if scanned > maxPerTable or visited >= maxVisited then break end local keyText = tostring(key) local childPath = path == "" and keyText or (path .. "." .. keyText) local childRelevant = relevant or Daily.stateKeyRelevant(keyText) if type(child) == "table" then if childRelevant or depth < 3 then visit(child, childPath, depth + 1, childRelevant) end elseif childRelevant and Daily.stateValueTrackable(childPath) then addPart(childPath, child) end end end for _, key in ipairs({ "DailyReward", "DailyRewards", "DailyPrize", "DailyPrizes", "DailyLogin", "LoginReward", "DailyGift", "DailyGifts", "DailyStreak", "LoginStreak", "ClaimedDaily", "ClaimedDailyReward", "ClaimedDailyRewards", "LastDailyClaim", "DailyRewardClaimed", }) do local value = caseValue(data, key) if value ~= nil and Daily.stateValueTrackable(key) then visit(value, key, 0, true) end end visit(data, "", 0, false) if #parts == 0 then return nil end table.sort(parts) return table.concat(parts, "|") end function Daily.noteStateBeforeSweep() local signature = Daily.stateSignature() if signature and State.dailyPendingConfirm and State.lastDailyStateSig and signature ~= State.lastDailyStateSig then Daily.debug("success confirmed", State.dailyPendingConfirm, "replicated state changed") State.dailyPendingConfirm = nil State.dailyFirstUnconfirmedRemoteAt = nil State.lastDailyConfirmedAt = tick() end if signature then State.lastDailyStateSig = signature end return signature end function Daily.confirmStateLater(beforeSignature, strategy) if not beforeSignature then Daily.debug("success confirmation unavailable", strategy) return end State.dailyPendingConfirm = strategy if State.dailyConfirmScheduled then return end State.dailyConfirmScheduled = true task.delay(0.75, function() State.dailyConfirmScheduled = false if State.closed or not Settings.dailyReward then return end local afterSignature = Daily.stateSignature() if afterSignature and afterSignature ~= beforeSignature then State.lastDailyStateSig = afterSignature State.dailyPendingConfirm = nil State.dailyFirstUnconfirmedRemoteAt = nil State.lastDailyConfirmedAt = tick() Daily.debug("success confirmed", strategy, "replicated state changed") else Daily.debug("success not confirmed yet", strategy) end end) end function Daily.resolveRemote() local cached = Data.Events and Data.Events.ClaimDaily if Daily.isRemote(cached) and cached.Parent and cached.Name == "ClaimDailyReward" then return Daily.rememberRemote(cached, "cache") elseif Data.Events then Data.Events.ClaimDaily = nil end local rs = Services.ReplicatedStorage local events = rs:FindFirstChild("Events") local remote = events and events:FindFirstChild("ClaimDailyReward") if not Daily.isRemote(remote) then Daily.debug("remote not found", "ReplicatedStorage.Events.ClaimDailyReward") return nil end return Daily.rememberRemote(remote, "exact") end function Daily.claimSweep() local remote = Daily.resolveRemote() if not remote then Daily.debug("sweep skipped", "remote missing") return false end if State.dailyBurstActive then Daily.debug("burst skipped", "already active") return false end local fastBurst = State.dailyForceBurst or not State.dailyInitialBurstDone if not fastBurst and not canFire("Reward:Daily:MaintenanceSweep", 3.0) then return false end State.dailyBurstActive = true State.lastDailySweep = tick() task.spawn(function() local okBurst, burstErr = pcall(function() local fired = false local passes = fastBurst and 5 or 2 local indexDelay = 0.04 local passDelay = fastBurst and 0.25 or 0.18 State.dailyForceBurst = false State.dailyInitialBurstDone = true Daily.debug(fastBurst and "burst start" or "maintenance start", Daily.remotePath(remote), "passes:", passes) for pass = 1, passes do if State.closed or not Settings.dailyReward then break end Daily.debug("sweep start", "pass:", pass) for index = 1, 7 do if State.closed or not Settings.dailyReward then break end local ok, err if remote:IsA("RemoteFunction") then ok, err = pcall(function() return remote:InvokeServer(index) end) else ok, err = pcall(function() remote:FireServer(index) end) end if ok then fired = true State.lastDailyRemoteAttempt = tick() Daily.debug("fired index", index) else Daily.debug("fire failed", "index:", index, tostring(err)) end if index < 7 then task.wait(indexDelay) end end if pass < passes then task.wait(passDelay) end end Daily.debug(fastBurst and "burst end" or "maintenance end", "fired:", fired) end) if not okBurst then Daily.debug("burst error", tostring(burstErr)) end State.dailyBurstActive = false end) return true end function claimSpinReward() local fired = false if fireEvent(Data.Events.ClaimSpin, "Reward:Spin", 2.0) then fired = true end if canFire("Reward:Spin:Buttons", 5.0) then local ok, clicked = pcall(clickRewardButtons, "spin") fired = (ok and clicked) or fired end return fired end function pulseRewards(reason) if State.rewardPulseQueued then State.rewardPulseReason = reason or State.rewardPulseReason return end State.rewardPulseReason = reason or State.rewardPulseReason State.rewardPulseQueued = true local waitTime = math.max(0, (Limiter.nextAt["Reward:Pulse"] or 0) - os.clock()) task.delay(waitTime, function() State.rewardPulseQueued = false Limiter.nextAt["Reward:Pulse"] = os.clock() + 0.20 if State.closed or not State.running then return end if automationPausedForUi() then task.delay(0.35, function() pulseRewards(State.rewardPulseReason or reason or "ui retry") end) return end local pulseReason = State.rewardPulseReason or reason or "scheduler" State.rewardPulseReason = nil if Settings.spinReward then pcall(claimSpinReward) end if Settings.dailyReward then Daily.debug("reward pulse", pulseReason) pcall(Daily.claimSweep) end end) end function startLoops() if State.closed or not State.running then return end updateAntiAfk() diag("startLoops", "Grid loop starting") task.defer(function() task.wait(0.15) pulseRewards("startup") end) task.spawn(function() while State.running do if tick() - Perf.lastOrdersFrame < 0.015 then task.wait() end Perf.lastMergeFrame = tick() if not automationPausedForUi() then pcall(processGrid) end local waitTime = speedInterval(Settings.mergeSpeed, 0.65, 0.20) task.wait(waitTime) end end) task.spawn(function() while State.running do if Settings.clearBubbles and not Settings.processGrid and not automationPausedForUi() then pcall(function() breakExistingBubbles(nil, 6, true) end) end task.wait(Settings.processGrid and 1.4 or LOOP.bubbles) end end) task.spawn(function() while State.running do if Settings.collectDrops and not automationPausedForUi() then local claimedNearby = 0 for id, payload in pairs(State.nearbyPickups) do if claimedNearby >= 5 then break end if claimPickup(id, nil, payload) then State.pickupCount = State.pickupCount + 1 end State.nearbyPickups[id] = nil claimedNearby = claimedNearby + 1 end pcall(collectPhysicalPickups) end task.wait(LOOP.pickup) end end) task.spawn(function() while State.running do if not automationPausedForUi() then safeCall("BuildLoop", processBuild) end task.wait(State.currentBuild and 0.45 or LOOP.build) end end) task.spawn(function() task.wait(0.5) while State.running do if Settings.spinReward and not automationPausedForUi() then pcall(claimSpinReward) end task.wait(LOOP.spin) end end) task.spawn(function() task.wait(0.6) while State.running do if Settings.dailyReward and not automationPausedForUi() then pcall(Daily.claimSweep) end task.wait(LOOP.daily) end end) task.spawn(function() task.wait(1.5) while State.running do if (Settings.vendorAutoFree or State.vendorActive) and not automationPausedForUi() then safeCall("MerchantLoop", buyFreeVendorOffers) pulseRewards("merchant loop") end task.wait(LOOP.vendor) end end) -- Ensure Auto Orders runs independently of grid processing task.spawn(function() while State.running do if tick() - Perf.lastMergeFrame < 0.015 then task.wait() end Perf.lastOrdersFrame = tick() if Settings.completeTasks and not automationPausedForUi() then pcall(completeOrders) end local orderSpeed = math.clamp(tonumber(Settings.orderSpeed) or 5, 1, 10) local orderFloor = orderSpeed >= 9 and 0.08 or (orderSpeed >= 7 and 0.12 or 0.18) local waitTime = speedInterval(Settings.orderSpeed, 1.00, orderFloor) task.wait(waitTime) end end) task.spawn(function() while State.running do if Settings.handleObstacleCourse and not automationPausedForUi() and tick() - State.lastObby >= 60 then pcall(processObby) pulseRewards("obby loop") end task.wait(LOOP.obby) end end) task.spawn(function() while State.running do if Settings.walkSpeed and Settings.walkSpeed ~= 16 and player then pcall(function() local hum = player.Character and player.Character:FindFirstChild("Humanoid") if hum and hum.WalkSpeed ~= Settings.walkSpeed then hum.WalkSpeed = Settings.walkSpeed end end) end task.wait(LOOP.walkSpeed) end end) local frames, last = 0, tick() if UI.stats.fps then UI.trackConnection(Services.RunService.RenderStepped:Connect(function() if State.running then frames = frames + 1 end end)) end task.spawn(function() while State.running do task.wait(2) if UI.stats.fps then UI.stats.fps.Text = tostring(math.floor(frames / math.max(tick() - last, 0.001))) frames, last = 0, tick() end if player then local ok, ping = pcall(function() return math.floor(player:GetNetworkPing() * 1000) end) if ok then local pingText = tostring(ping) .. " ms" if UI.stats.ping then UI.stats.ping.Text = tostring(ping) end if UI.serverPing then UI.serverPing.Text = pingText end end end if UI.stats.players then UI.stats.players.Text = #Services.Players:GetPlayers() .. " / " .. Services.Players.MaxPlayers end if UI.serverPlayers then UI.serverPlayers.Text = #Services.Players:GetPlayers() .. " / " .. Services.Players.MaxPlayers end if UI.serverUptime then UI.serverUptime.Text = Stats.formatTimeSpan(currentServerAgeSeconds()) end if not automationPausedForUi() then Stats.update() end end end) end Stats.loadSettings() State.bootOk, State.bootErr = pcall(UI.createShell) if not State.bootOk then UI.bootError(State.bootErr) else State.bootOk, State.bootErr = pcall(UI.buildHome) if not State.bootOk then UI.showNotification("Home build error", tostring(State.bootErr):sub(1, 120), 8) end State.bootOk, State.bootErr = pcall(buildPages) if not State.bootOk then UI.showNotification("Pages build error", tostring(State.bootErr):sub(1, 120), 8) end UI.applyTheme() UI.applyFont() State.bootFinalSize = UI.main.Size State.bootFinalPosition = UI.main.Position State.bootFinalTransparency = UI.transparencyFor("main") or 0 State.bootOpeningSize = UDim2.new( State.bootFinalSize.X.Scale, State.bootFinalSize.X.Offset - 18, State.bootFinalSize.Y.Scale, State.bootFinalSize.Y.Offset - 14 ) UI.main.Size = State.bootOpeningSize UI.main.Position = UDim2.new(0.5, -State.bootOpeningSize.X.Offset / 2, 0.5, -State.bootOpeningSize.Y.Offset / 2) UI.main.BackgroundTransparency = 1 UI.tween( UI.main, 0.22, { Size = State.bootFinalSize, Position = State.bootFinalPosition, BackgroundTransparency = State.bootFinalTransparency, }, Enum.EasingStyle.Back ) UI.switchPage("Auto") UI.showNotification(SCRIPT_NAME, "Loaded " .. SCRIPT_VERSION, 4) task.defer(initBackend) task.defer(startLoops) end