--!nonstrict --[[ Robscript team - NFT Battle Case opening and inventory automation for the NFT Battle case opener (place "Ugc"). UI framework: Robly Airflow (https://github.com/bobloscript/Robly_Airflow) Game surface this script drives (verified against the live client): * ReplicatedStorage.Events.OpenCase:InvokeServer(caseName, amount, {}) -> true opened -> false refused -> "Buy" the case needs a Robux product, the client should open a purchase prompt * ReplicatedStorage.Events.Inventory:FireServer("Sell", itemName) only honoured for the inventory cards the game itself rendered, so selling goes through each card's own Sell signal instead of a hand-made FireServer call. * ReplicatedStorage.Info.Cases / .PaidCases / .Gifts * PlayerGui.MainGui.Topbar.Right.Stars * PlayerGui.MainGui.Pages.Profile.MainFrame.Scroll.Default Pacing: the server accepts roughly one open request every 5.7 seconds (measured: an attempt 5.7s after the previous success goes through, anything below 4s is refused, and amount 1 and amount 10 share the same window). Refused attempts are cheap but useless, so the loop waits out the cooldown instead of hammering the remote. Strict typing is off on purpose: the executor injects globals (getconnections, firesignal, loadstring, HttpGet) that a strict analyser cannot see. ]] ----- Identity ----- local HUB_NAME = "Robscript team - NFT Battle" local HUB_VERSION = "1.0.0" local HUB_FOLDER = "RobScriptNFT" ----- UI library ----- local UI_SOURCES = { "https://raw.githubusercontent.com/bobloscript/Robly_Airflow/main/Source.luau", "https://raw.githubusercontent.com/PookiePepelsss/Airflow-UI/refs/heads/main/Source.luau", } local Airflow = nil for _, url in ipairs(UI_SOURCES) do local loaded, library = pcall(function() return loadstring(game:HttpGet(url))() end) if loaded and type(library) == "table" and type(library.CreateWindow) == "function" then Airflow = library break end end if not Airflow then error("[" .. HUB_NAME .. "] could not load the Airflow UI library") end ----- Theme ----- local THEME = { Background = Color3.fromHex("#0A0A0C"), Surface = Color3.fromHex("#141418"), Surface2 = Color3.fromHex("#1A1A20"), Surface3 = Color3.fromHex("#2A2A33"), Stroke = Color3.fromHex("#26262E"), StrokeHover = Color3.fromHex("#3C3C49"), Accent = Color3.fromHex("#FF3B7F"), AccentDark = Color3.fromHex("#1A0710"), Text = Color3.fromHex("#F5F5F7"), Muted = Color3.fromHex("#8D8D99"), } Airflow.ThemePresets.Robscript = THEME Airflow:SetDefaultTheme(THEME) pcall(function() Airflow:LoadFont({ Name = "ValleySans" }) end) pcall(function() Airflow:PreloadIcons() end) ----- Services ----- local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local LocalPlayer = Players.LocalPlayer ----- Constants ----- local MAX_OPEN_AMOUNT = 10 local DAILY_CASE_NAME = "Daily" -- One accepted open per server window. Anything faster is refused, so this is a floor. local SERVER_COOLDOWN = 5.8 local SELL_STEP_DELAY = 0.12 local RARE_DROP_PRICE = 5000 local MAX_CONSECUTIVE_FAILURES = 30 ----- Runtime state ----- local state = { selectedCase = nil, autoOpenRequested = false, autoSellRequested = false, sellBusy = false, opens = 0, spent = 0, sold = 0, earned = 0, bestDrop = nil, lastDrop = nil, lastOpenAt = 0, } local ui = {} ----- Small helpers ----- local function formatNumber(value) local number = math.floor(tonumber(value) or 0) local text = tostring(number) local sign = "" if string.sub(text, 1, 1) == "-" then sign = "-" text = string.sub(text, 2) end local reversed = string.reverse(text) reversed = string.gsub(reversed, "(%d%d%d)", "%1,") local formatted = string.reverse(reversed) if string.sub(formatted, 1, 1) == "," then formatted = string.sub(formatted, 2) end formatted = string.gsub(formatted, ",$", "") return sign .. formatted end local function escapeRichText(text) local value = tostring(text) value = string.gsub(value, "&", "&") value = string.gsub(value, "<", "<") value = string.gsub(value, ">", ">") return value end local function waitForChild(parent, childName, timeout) if not parent then return nil end local existing = parent:FindFirstChild(childName) if existing then return existing end local deadline = os.clock() + (timeout or 10) while os.clock() < deadline do local found = parent:FindFirstChild(childName) if found then return found end task.wait(0.2) end return nil end ----- Game data ----- local eventsFolder = waitForChild(ReplicatedStorage, "Events", 15) local openCaseRemote = waitForChild(eventsFolder, "OpenCase", 10) local infoFolder = waitForChild(ReplicatedStorage, "Info", 10) local casePrices = {} local caseData = {} local caseNames = {} local paidCaseNames = {} local giftPrices = {} do local casesModule = infoFolder and infoFolder:FindFirstChild("Cases") if casesModule then local loaded, data = pcall(require, casesModule) if loaded and type(data) == "table" then for name, entry in pairs(data) do if type(name) == "string" and type(entry) == "table" then casePrices[name] = tonumber(entry.Price) or 0 caseData[name] = entry table.insert(caseNames, name) end end end end local paidModule = infoFolder and infoFolder:FindFirstChild("PaidCases") if paidModule then local loaded, data = pcall(require, paidModule) if loaded and type(data) == "table" then for name in pairs(data) do if type(name) == "string" then paidCaseNames[name] = true end end end end -- Authoritative item prices. The card labels animate their number, so a scan that read -- them mid-animation would see values like 16 for a 100 star item. local giftsModule = infoFolder and infoFolder:FindFirstChild("Gifts") if giftsModule then local loaded, data = pcall(require, giftsModule) if loaded and type(data) == "table" then for name, entry in pairs(data) do if type(name) == "string" and type(entry) == "table" then local price = tonumber(entry.Price) if price then giftPrices[name] = price end end end end end -- Fallback: read the case buttons the cases page rendered, in case the modules change. if #caseNames == 0 then local playerGui = LocalPlayer:FindFirstChild("PlayerGui") local mainGui = playerGui and playerGui:FindFirstChild("MainGui") local pages = mainGui and mainGui:FindFirstChild("Pages") local casesPage = pages and pages:FindFirstChild("Cases") local mainFrame = casesPage and casesPage:FindFirstChild("MainFrame") if mainFrame then for _, group in ipairs(mainFrame:GetChildren()) do if string.sub(group.Name, 1, 2) == "G_" then for _, button in ipairs(group:GetChildren()) do if button:IsA("GuiButton") then casePrices[button.Name] = casePrices[button.Name] or 0 table.insert(caseNames, button.Name) end end end end end end end table.sort(caseNames, function(left, right) local leftPrice = casePrices[left] or 0 local rightPrice = casePrices[right] or 0 if leftPrice == rightPrice then return left < right end return leftPrice < rightPrice end) -- Short labels: a free case, a star price, or a Robux case. local function caseLabel(caseName) if paidCaseNames[caseName] then return caseName .. " - robux" end local price = casePrices[caseName] or 0 if price <= 0 then return caseName .. " - free" end return caseName .. " - " .. formatNumber(price) .. " stars" end local caseLabels = {} local labelToCase = {} for _, caseName in ipairs(caseNames) do local label = caseLabel(caseName) table.insert(caseLabels, label) labelToCase[label] = caseName end -- Accepts a current label, a bare case name, or a label saved by an older build -- ("Trash · free"), so a loaded config never leaves the hub without a case. local function resolveCase(option) if type(option) ~= "string" or option == "" then return nil end if labelToCase[option] then return labelToCase[option] end if casePrices[option] ~= nil then return option end local name = string.match(option, "^(.-)%s*[·–%-]") if name and casePrices[name] ~= nil then return name end return nil end local defaultCase = labelToCase[caseLabel("Trash")] and "Trash" or caseNames[1] state.selectedCase = defaultCase local function describeDrops(caseName) local entry = caseData[caseName] local drops = entry and entry.Drop if type(drops) ~= "table" then return "unknown" end local parts = {} for _, drop in ipairs(drops) do if type(drop) == "table" and drop[1] ~= nil then table.insert(parts, escapeRichText(drop[1]) .. " " .. tostring(drop[2]) .. "%") end end if #parts == 0 then return "unknown" end return table.concat(parts, " · ") end ----- Game access ----- local function getMainGui() local playerGui = LocalPlayer:FindFirstChild("PlayerGui") return playerGui and playerGui:FindFirstChild("MainGui") end local function getInventoryGrid() local mainGui = getMainGui() local pages = mainGui and mainGui:FindFirstChild("Pages") local profile = pages and pages:FindFirstChild("Profile") local mainFrame = profile and profile:FindFirstChild("MainFrame") local scroll = mainFrame and mainFrame:FindFirstChild("Scroll") return scroll and scroll:FindFirstChild("Default") end local function getStars() local mainGui = getMainGui() local topbar = mainGui and mainGui:FindFirstChild("Topbar") local right = topbar and topbar:FindFirstChild("Right") local stars = right and right:FindFirstChild("Stars") if not stars then return nil end for _, child in ipairs(stars:GetDescendants()) do if child:IsA("IntValue") then return child.Value end end return nil end local function parsePriceLabel(text) if type(text) ~= "string" then return 0 end local digits = string.gsub(text, "[^%d]", "") return tonumber(digits) or 0 end local function itemPrice(itemName, card) local known = giftPrices[itemName] if known then return known end local priceLabel = card and card:FindFirstChild("Price") return priceLabel and parsePriceLabel(priceLabel.Text) or 0 end local function findItemPrice(itemName) local grid = getInventoryGrid() return itemPrice(itemName, grid and grid:FindFirstChild(itemName)) end local function countItems() local grid = getInventoryGrid() local counts = {} if not grid then return counts end for _, card in ipairs(grid:GetChildren()) do if card:IsA("Frame") then counts[card.Name] = (counts[card.Name] or 0) + 1 end end return counts end local function totalCount(counts) local total = 0 for _, quantity in pairs(counts) do total = total + quantity end return total end -- The inventory grid is redrawn a frame or two after the server replies, so the drop -- detection has to wait for the cards instead of sampling straight away. local function awaitInventoryGrowth(beforeCounts, expected, timeout) local target = totalCount(beforeCounts) + math.max(expected, 1) local deadline = os.clock() + (timeout or 1.5) local counts = countItems() while os.clock() < deadline do if totalCount(counts) >= target then return counts end task.wait(0.1) counts = countItems() end return counts end local function scanInventory() local grid = getInventoryGrid() if not grid then return nil, nil end local items = {} local byName = {} for _, card in ipairs(grid:GetChildren()) do if card:IsA("Frame") then local entry = byName[card.Name] if not entry then entry = { name = card.Name, price = itemPrice(card.Name, card), quantity = 0, } byName[card.Name] = entry table.insert(items, entry) end entry.quantity = entry.quantity + 1 end end table.sort(items, function(left, right) if left.price == right.price then return left.name < right.name end return left.price < right.price end) return items, byName end local function fireButton(button) if not button or not button:IsA("GuiButton") then return false end local fired = false if type(getconnections) == "function" then local ok, connections = pcall(getconnections, button.Activated) if ok and type(connections) == "table" then for _, connection in ipairs(connections) do if connection.Enabled then fired = true task.spawn(function() pcall(connection.Function) end) end end end end if not fired and type(firesignal) == "function" then pcall(firesignal, button.Activated) fired = true end return fired end ----- Logging ----- local function pushActivity(text, kind) if ui.activityLog then ui.activityLog:Push(text, kind) end end local function pushDrop(text, kind) if ui.dropLog then ui.dropLog:Push(text, kind) end end ----- Pacing ----- local function configuredCooldown() local fromSlider = ui.openDelay and ui.openDelay.Value or SERVER_COOLDOWN return math.max(fromSlider, SERVER_COOLDOWN) end local function cooldownRemaining() return math.max(configuredCooldown() - (os.clock() - state.lastOpenAt), 0) end ----- Case opening ----- -- Returns status, message. Status is one of: "opened", "skipped", "failed", "purchase", "error". local function openOnce(ignoreCooldown) local caseName = state.selectedCase or defaultCase if type(caseName) ~= "string" or caseName == "" then return "error", "Pick a case first" end if not openCaseRemote then return "error", "Game remote is missing" end if not ignoreCooldown and cooldownRemaining() > 0 then return "skipped", string.format("Wait %.0fs, the server allows one open at a time", cooldownRemaining()) end if ui.skipDaily and ui.skipDaily.Value and caseName == DAILY_CASE_NAME then return "skipped", "Daily case is skipped" end if ui.skipPaid and ui.skipPaid.Value and paidCaseNames[caseName] then return "skipped", caseName .. " costs Robux" end local amount = 1 if ui.openAmount then amount = math.clamp(math.floor(ui.openAmount.Value + 0.5), 1, MAX_OPEN_AMOUNT) end local unitPrice = casePrices[caseName] or 0 local totalCost = unitPrice * amount local reserve = ui.minStars and math.floor(ui.minStars.Value) or 0 if totalCost > 0 then local stars = getStars() if stars and (stars - totalCost) < reserve then return "skipped", "Not enough stars" end end local before = countItems() local starsBefore = getStars() local invoked, result = pcall(function() return openCaseRemote:InvokeServer(caseName, amount, {}) end) -- Count the cooldown from the answer, not from the request, so the next attempt never -- lands inside the server window. state.lastOpenAt = os.clock() if not invoked then return "error", tostring(result) end if result == "Buy" then return "purchase", caseName .. " costs Robux" end if result ~= true then return "failed", "Server said no, trying again" end state.opens = state.opens + amount local starsAfter = getStars() if starsBefore and starsAfter and starsAfter < starsBefore then state.spent = state.spent + (starsBefore - starsAfter) end local after = awaitInventoryGrowth(before, amount, 1.5) local gained = {} for itemName, quantity in pairs(after) do local previous = before[itemName] or 0 if quantity > previous then table.insert(gained, { name = itemName, quantity = quantity - previous, price = findItemPrice(itemName), }) end end table.sort(gained, function(left, right) return left.price > right.price end) local totalValue = 0 for _, drop in ipairs(gained) do totalValue = totalValue + drop.price * drop.quantity if not state.bestDrop or drop.price > state.bestDrop.price then state.bestDrop = { name = drop.name, price = drop.price } end local kind = drop.price >= RARE_DROP_PRICE and "Success" or nil pushDrop(string.format( "%s x%d · %s⭐", escapeRichText(drop.name), drop.quantity, formatNumber(drop.price) ), kind) end if gained[1] then state.lastDrop = { name = gained[1].name, price = gained[1].price } end if #gained == 0 then pushActivity(string.format("Opened %s x%d", escapeRichText(caseName), amount)) else pushActivity(string.format( "Opened %s x%d · got %s⭐", escapeRichText(caseName), amount, formatNumber(totalValue) )) end return "opened", caseName end ----- Selling ----- -- The inventory server drops part of the requests sent back to back, so a sale is only -- counted once the card has actually left the grid. local function sellCard(card, timeout) if not card or not card:IsA("Frame") then return false end if not fireButton(card:FindFirstChild("Sell")) then return false end local deadline = os.clock() + (timeout or 0.7) while os.clock() < deadline do if card.Parent == nil then return true end task.wait(0.05) end return false end local function runSellPass(batchLimit) if state.sellBusy then return 0, 0 end local items = scanInventory() if not items then pushActivity("Inventory is still loading", "Warning") return 0, 0 end local threshold = ui.sellThreshold and ui.sellThreshold.Value or 0 local limit = batchLimit or (ui.sellBatch and math.floor(ui.sellBatch.Value) or 100) local starsBefore = getStars() or 0 local soldCount = 0 local misses = 0 state.sellBusy = true for _, entry in ipairs(items) do if soldCount >= limit or misses >= 3 then break end if entry.price > 0 and entry.price <= threshold then while soldCount < limit and misses < 3 do local grid = getInventoryGrid() local card = grid and grid:FindFirstChild(entry.name) if not card then break end if sellCard(card) then soldCount = soldCount + 1 misses = 0 task.wait(SELL_STEP_DELAY) else misses = misses + 1 task.wait(SELL_STEP_DELAY * 2) end end end end state.sellBusy = false local starsAfter = getStars() or starsBefore local earned = math.max(0, starsAfter - starsBefore) if soldCount > 0 then state.sold = state.sold + soldCount state.earned = state.earned + earned pushActivity(string.format( "Sold %d · +%s⭐", soldCount, formatNumber(earned) )) end if misses >= 3 then pushActivity("Slow down a bit, the server is refusing sells") end return soldCount, earned end local function printInventoryScan() local items = scanInventory() if not items then pushActivity("Inventory is still loading", "Warning") return end local quantity = 0 local value = 0 local rows = {} for _, entry in ipairs(items) do quantity = quantity + entry.quantity value = value + entry.price * entry.quantity table.insert(rows, { entry.name, entry.price, entry.quantity }) end print(string.format( "[NFT Battle] %d kinds, %d items, %s stars total", #items, quantity, formatNumber(value) )) for _, entry in ipairs(items) do print(string.format( " %-36s price %9s qty %d", entry.name, formatNumber(entry.price), entry.quantity )) end if ui.scanTable then ui.scanTable:SetRows(rows) end pushActivity(string.format( "Scanned %d kinds · %d items · %s⭐", #items, quantity, formatNumber(value) )) end ----- Loops ----- local autoOpenThread = nil local autoSellThread = nil local function stopAutoOpen() if autoOpenThread then task.cancel(autoOpenThread) autoOpenThread = nil end end -- Stops the loop, flips the switch back and tells the player why. Used for reasons the -- player has to fix (Robux case, daily case, not enough stars), so it never spins silently. local function haltAutoOpen(message, kind) state.autoOpenRequested = false if ui.autoOpenToggle then ui.autoOpenToggle:Set(false, true) end pushActivity("<" .. kind .. ">Stopped: " .. escapeRichText(message), kind) if ui.window then ui.window:Notify({ Title = "Auto open stopped", Content = message, Type = kind }) end end local function startAutoOpen() stopAutoOpen() autoOpenThread = task.spawn(function() local failures = 0 while state.autoOpenRequested do if state.sellBusy then task.wait(0.2) else local wait = cooldownRemaining() if wait > 0 then task.wait(math.min(wait, 0.4)) else local status, message = openOnce(true) if status == "opened" then failures = 0 elseif status == "failed" then failures = failures + 1 if failures == 1 or failures % 8 == 0 then pushActivity("" .. escapeRichText(message) .. "") end if failures >= MAX_CONSECUTIVE_FAILURES then haltAutoOpen("the server keeps refusing, check the case and your stars", "Error") break end task.wait(1) else -- skipped, purchase or error: the player has to change something haltAutoOpen(message, status == "error" and "Error" or "Warning") break end end end end end) end local function stopAutoSell() if autoSellThread then task.cancel(autoSellThread) autoSellThread = nil end end local function startAutoSell() stopAutoSell() autoSellThread = task.spawn(function() while state.autoSellRequested do runSellPass() task.wait(ui.sellScanDelay and ui.sellScanDelay.Value or 3) end end) end ----- Window ----- -- Re-running the script stops the previous instance instead of leaving a second set of -- loops selling and opening in the background. The old key is still checked so an upgrade -- from a previous build cannot leave a zombie behind. local shared = (type(getgenv) == "function" and getgenv()) or _G for _, key in ipairs({ "RobscriptNFTBattleShutdown", "UgcCaseHubShutdown" }) do if type(shared[key]) == "function" then pcall(shared[key]) shared[key] = nil end end -- Same idea for stray windows left by an older title. local holder = (type(gethui) == "function" and gethui()) or nil if holder then for _, child in ipairs(holder:GetChildren()) do if child:IsA("ScreenGui") and (child.Name == HUB_NAME or child.Name == "Ugc Case Hub") then pcall(function() child:Destroy() end) end end end local Window = Airflow:CreateWindow({ Name = HUB_NAME, LoadingSubtitle = "auto open · auto sell", ToggleUIKeybind = "RightControl", Size = UDim2.fromOffset(760, 520), Backdrop = { Weather = "Fireflies", Tint = 0.35 }, Theme = THEME, ConfigurationSaving = { Enabled = true, FolderName = HUB_FOLDER }, Home = { Title = HUB_NAME, Welcome = "Welcome back,", Tier = "v" .. HUB_VERSION, Stats = { "Players", "Session", "FPS", "Ping", "Game", "Executor" }, }, }) ui.window = Window if #caseLabels == 0 then caseLabels = { "No cases found" } end ----- Tab: Open ----- local OpenTab = Window:CreateTab({ Name = "Open", Icon = "package" }) local OpenBox = OpenTab:CreateGroupbox({ Name = "Auto Open", Icon = "zap", Side = "Left" }) local CaseBox = OpenTab:CreateGroupbox({ Name = "Selected Case", Icon = "gift", Side = "Right" }) local AdvancedBox = OpenTab:CreateGroupbox({ Name = "Advanced", Icon = "settings-2", Side = "Right", Collapsed = true, }) OpenBox:CreateParagraph({ Title = "How it works", Content = "Pick a case, switch AUTO OPEN on and leave it. The game server allows about one open every 6 seconds, so the timer on the right moves slowly - that is normal.", }) ui.autoOpenToggle = OpenBox:CreateToggle({ Name = "AUTO OPEN", Desc = "Keeps opening the selected case", Flag = "AutoOpen", CurrentValue = false, Callback = function(enabled) state.autoOpenRequested = enabled if enabled then startAutoOpen() else stopAutoOpen() end end, }) OpenBox:CreateDropdown({ Name = "Case", Desc = "Which case to open", Options = caseLabels, CurrentOption = defaultCase and caseLabel(defaultCase) or nil, Flag = "CaseName", Searchable = true, MaxRows = 8, Callback = function(option) state.selectedCase = resolveCase(option) or state.selectedCase or defaultCase end, }) ui.openAmount = OpenBox:CreateStepper({ Name = "Cases per open", Desc = "1 to 10 at a time, same waiting time", Range = { 1, MAX_OPEN_AMOUNT }, Increment = 1, CurrentValue = 1, Flag = "OpenAmount", }) OpenBox:CreateStatus({ Name = "Next open", Style = "Row", UpdateRate = 0.4, Pin = true, Update = function() if state.sellBusy then return "selling…" end if not state.autoOpenRequested then return "off" end local wait = cooldownRemaining() if wait > 0.1 then return string.format("%.0f s", math.ceil(wait)) end return "now" end, }) OpenBox:CreateStatus({ Name = "Opened", Style = "Stat", Update = function() return formatNumber(state.opens) end, }) OpenBox:CreateStatus({ Name = "Spent", Style = "Stat", Update = function() return formatNumber(state.spent) .. "⭐" end, }) CaseBox:CreateStatus({ Name = "Price", Style = "Row", Update = function() local caseName = state.selectedCase if not caseName then return "none" end if paidCaseNames[caseName] then return "Robux" end local price = casePrices[caseName] or 0 if price <= 0 then return "free" end return formatNumber(price) .. "⭐" end, }) CaseBox:CreateStatus({ Name = "Last drop", Style = "Row", Update = function() if not state.lastDrop then return "nothing yet" end return escapeRichText(state.lastDrop.name) .. " " .. formatNumber(state.lastDrop.price) .. "⭐" end, }) CaseBox:CreateStatus({ Name = "Best drop", Style = "Row", Update = function() if not state.bestDrop then return "nothing yet" end return escapeRichText(state.bestDrop.name) .. " " .. formatNumber(state.bestDrop.price) .. "⭐" end, }) CaseBox:CreateStatus({ Name = "Contains", Style = "Plain", UpdateRate = 2, Update = function() local caseName = state.selectedCase return caseName and describeDrops(caseName) or "none" end, }) CaseBox:CreateButton({ Name = "Open once", Desc = "One open, no loop", Icon = "play", Style = "Primary", Callback = function() task.spawn(function() local status, message = openOnce(false) if status == "opened" then return end local kind = (status == "error") and "Error" or "Warning" pushActivity("<" .. kind .. ">" .. escapeRichText(message) .. "", kind) end) end, }) ui.openDelay = AdvancedBox:CreateSlider({ Name = "Wait between opens", Desc = "Server limit is about 6 seconds", Range = { 6, 30 }, Increment = 1, Suffix = " s", CurrentValue = 6, Flag = "OpenDelay", }) ui.minStars = AdvancedBox:CreateSlider({ Name = "Keep this many stars", Desc = "Auto open stops before going below", Range = { 0, 10000000 }, Increment = 1000, CurrentValue = 0, Flag = "MinStars", }) ui.skipDaily = AdvancedBox:CreateToggle({ Name = "Skip Daily case", Desc = "It only works once a day", Flag = "SkipDaily", CurrentValue = true, }) ui.skipPaid = AdvancedBox:CreateToggle({ Name = "Skip Robux cases", Desc = "Never open a case that costs Robux", Flag = "SkipPaid", CurrentValue = true, }) ----- Tab: Sell ----- local SellTab = Window:CreateTab({ Name = "Sell", Icon = "coins" }) local SellBox = SellTab:CreateGroupbox({ Name = "Auto Sell", Icon = "tag", Side = "Left" }) local BagBox = SellTab:CreateGroupbox({ Name = "Inventory", Icon = "backpack", Side = "Right" }) ui.autoSellToggle = SellBox:CreateToggle({ Name = "AUTO SELL", Desc = "Sells everything cheaper than the price below", Flag = "AutoSell", CurrentValue = false, Callback = function(enabled) state.autoSellRequested = enabled if enabled then startAutoSell() else stopAutoSell() end end, }) ui.sellThreshold = SellBox:CreateSlider({ Name = "Sell items under", Desc = "Item price in stars", Range = { 0, 2000000 }, Increment = 25, Suffix = "⭐", CurrentValue = 0, Flag = "SellThreshold", }) SellBox:CreateStatus({ Name = "Will sell", Style = "Row", UpdateRate = 1, Update = function() local items = scanInventory() if not items then return "loading" end local threshold = ui.sellThreshold and ui.sellThreshold.Value or 0 local quantity = 0 local value = 0 for _, entry in ipairs(items) do if entry.price > 0 and entry.price <= threshold then quantity = quantity + entry.quantity value = value + entry.price * entry.quantity end end if quantity == 0 then return "nothing, raise the price" end return string.format("%d items · ~%s⭐", quantity, formatNumber(value)) end, }) SellBox:CreateButton({ Name = "Sell now", Desc = "One pass, no loop", Icon = "hand-coins", Style = "Primary", Callback = function() if state.sellBusy then pushActivity("Sell pass is already running", "Warning") return end task.spawn(function() local soldCount = runSellPass() if soldCount == 0 then pushActivity("Nothing matched the price") end end) end, }) ui.sellScanDelay = SellBox:CreateSlider({ Name = "Rescan every", Desc = "How often auto sell looks at the bag", Range = { 0.5, 30 }, Increment = 0.5, Suffix = " s", CurrentValue = 3, Flag = "SellScanDelay", }) ui.sellBatch = SellBox:CreateSlider({ Name = "Max per pass", Desc = "Stops after this many items", Range = { 5, 500 }, Increment = 5, CurrentValue = 100, Flag = "SellBatch", }) BagBox:CreateStatus({ Name = "Sold", Style = "Stat", Update = function() return formatNumber(state.sold) end, }) BagBox:CreateStatus({ Name = "Earned", Style = "Stat", Update = function() return formatNumber(state.earned) .. "⭐" end, }) BagBox:CreateStatus({ Name = "Stars", Style = "Row", Update = function() local stars = getStars() if not stars then return "?" end return formatNumber(stars) end, }) BagBox:CreateButton({ Name = "Print inventory", Desc = "Dump the bag to the console", Icon = "printer", Callback = function() task.spawn(printInventoryScan) end, }) ui.scanTable = BagBox:CreateTable({ Name = "Last scan", Columns = { { Name = "Item", Width = 0.52 }, { Name = "Price", Align = "Right", Width = 0.3 }, { Name = "Qty", Align = "Right", Width = 0.16 }, }, Rows = {}, MaxRows = 6, EmptyText = "Press Print inventory", Search = true, }) ----- Tab: Log ----- local LogTab = Window:CreateTab({ Name = "Log", Icon = "scroll-text" }) local ActivityBox = LogTab:CreateGroupbox({ Name = "Everything", Icon = "list", Side = "Left" }) local DropBox = LogTab:CreateGroupbox({ Name = "Drops", Icon = "gem", Side = "Right" }) ui.activityLog = ActivityBox:CreateLog({ Name = "Activity", MaxEntries = 400, MaxRows = 12, Timestamps = true, EmptyText = "Nothing yet", }) ui.dropLog = DropBox:CreateLog({ Name = "Drops", MaxEntries = 250, MaxRows = 12, Timestamps = true, EmptyText = "No drops yet", }) ----- Tab: Settings ----- local SettingsTab = Window:CreateTab({ Name = "Settings", Icon = "settings" }) SettingsTab:CreateConfigManager({ Name = "Configs", Side = "Left" }) SettingsTab:CreateThemeManager({ Name = "Theme", Side = "Right" }) SettingsTab:CreateButton({ Name = "Unload hub", Desc = "Closes the window and stops everything", Icon = "power", Confirm = "Click again to unload", Callback = function() if type(shared.RobscriptNFTBattleShutdown) == "function" then shared.RobscriptNFTBattleShutdown() end end, }) ----- Start ----- shared.RobscriptNFTBattleShutdown = function() state.autoOpenRequested = false state.autoSellRequested = false stopAutoOpen() stopAutoSell() pcall(function() Window:Destroy() end) shared.RobscriptNFTBattleShutdown = nil end Window:LoadAutoload() pushActivity(string.format( "Ready · %d cases · %s⭐", #caseNames, formatNumber(getStars() or 0) )) Window:Notify({ Title = HUB_NAME .. " v" .. HUB_VERSION, Content = "Pick a case, then switch AUTO OPEN on", Type = "Success", })