--!nonstrict --[[ Collect Cards Hub ----------------- A single-file helper for [Collect Cards] with five tools: 1. Tycoon - buys your shop upgrade pads for you (instant by default, walking if needed) 2. Shop - buys card packs and Building furniture, with pick-lists and a spend priority 3. Tower - plays the Battle Tower for you on the tower you choose 4. Packs - opens every pack in your inventory 5. Dupes - sells extra copies of cards you already own Press RightControl to hide or show the window. Every switch is off when the script starts, so nothing happens until you turn it on. Settings, theme and toggles are saved in the CollectCardsHub folder. ]] ---------------------------------------------------------------------- -- 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 for _, url in ipairs(UI_SOURCES) do local ok, lib = pcall(function() return loadstring(game:HttpGet(url))() end) if ok and type(lib) == "table" and type(lib.CreateWindow) == "function" then Airflow = lib break end end if not Airflow then error("[Collect Cards Hub] The UI library could not be loaded. Check your internet connection and try again.") end ---------------------------------------------------------------------- -- Game access ---------------------------------------------------------------------- local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local PathfindingService = game:GetService("PathfindingService") local LocalPlayer = Players.LocalPlayer local Modules = ReplicatedStorage:WaitForChild("Modules", 30) local Gameplay = Modules and Modules:WaitForChild("Gameplay", 30) local Game = Gameplay and require(Gameplay:WaitForChild("TGC_Client", 30)) if not Game then error("[Collect Cards Hub] This script only works in Collect Cards.") end ---------------------------------------------------------------------- -- Small helpers ---------------------------------------------------------------------- local function money(value) if type(value) ~= "number" then return "-" end local text = tostring(math.floor(value)) local grouped = text:reverse():gsub("(%d%d%d)", "%1 "):reverse() return (grouped:gsub("^%s+", "")) end local function gameState() return Game.State end local function catalog() return Game.Catalog end local function vendor() return Game.Vendor end local function cash() local s = gameState() return (s and tonumber(s.cash)) or 0 end local function busy() local s = gameState() return Game.Busy == true or (s ~= nil and s.pending == true) end local function waitIdle(timeout) local deadline = os.clock() + (timeout or 10) while busy() and os.clock() < deadline do task.wait(0.05) end return not busy() end --- Sends one purchase/menu action and waits for the game to answer it. --- Returns the answer and a short reason when it failed. local function runAction(action, payload, timeout) if busy() then waitIdle(timeout) end if busy() then return nil, "The game is busy" end local answer local connection = Game.CommandFinished.Event:Connect(function(finishedAction, finishedAnswer) if finishedAction == action then answer = finishedAnswer end end) local sent = Game.Command(action, payload) if not sent then connection:Disconnect() return nil, "Not ready yet" end local deadline = os.clock() + (timeout or 10) while not answer and os.clock() < deadline do task.wait(0.05) end connection:Disconnect() if not answer then return nil, "No answer from the game" end return answer, answer.code or answer.status end local function ask(action, payload) local ok, answer = pcall(function() return Game.Query(action, payload) end) if not ok or type(answer) ~= "table" then return nil end return answer end local function flatDistance(a, b) return (Vector3.new(a.X, 0, a.Z) - Vector3.new(b.X, 0, b.Z)).Magnitude end ---------------------------------------------------------------------- -- Moving to a pad ---------------------------------------------------------------------- --- Puts the character straight on the pad. Instant, no walking. local function jumpTo(position) local character = LocalPlayer.Character if not character then return false end local root = character:FindFirstChild("HumanoidRootPart") if not root then return false end local ok = pcall(function() character:PivotTo(CFrame.new(position + Vector3.new(0, 4, 0))) end) if not ok then return false end task.wait(0.15) return flatDistance(root.Position, position) < 5 end --- Walks the character to the pad along a path. Slower, but always allowed. local function walkTo(targetPosition, timeout) timeout = timeout or 20 local character = LocalPlayer.Character if not character then return false, -1 end local humanoid = character:FindFirstChildOfClass("Humanoid") local root = character:FindFirstChild("HumanoidRootPart") if not (humanoid and root) or humanoid.Health <= 0 then return false, -1 end local waypoints local ok = pcall(function() local path = PathfindingService:CreatePath({ AgentRadius = 2, AgentHeight = 5, AgentCanJump = true, WaypointSpacing = 5, }) path:ComputeAsync(root.Position, targetPosition) if path.Status == Enum.PathStatus.Success then waypoints = path:GetWaypoints() end end) if not ok or not waypoints then waypoints = { { Position = targetPosition, Action = Enum.PathWaypointAction.Walk } } end local deadline = os.clock() + timeout for index, waypoint in ipairs(waypoints) do if index > 1 then if waypoint.Action == Enum.PathWaypointAction.Jump then humanoid.Jump = true end humanoid:MoveTo(waypoint.Position) local stepDeadline = os.clock() + 3.5 while os.clock() < stepDeadline and os.clock() < deadline do if flatDistance(root.Position, waypoint.Position) < 2.5 then break end task.wait(0.08) end if os.clock() >= deadline then break end end end local finishDeadline = os.clock() + 4 while os.clock() < finishDeadline and os.clock() < deadline do if flatDistance(root.Position, targetPosition) < 1.6 then break end humanoid:MoveTo(targetPosition) task.wait(0.08) end humanoid:MoveTo(root.Position) local distance = flatDistance(root.Position, targetPosition) return distance < 3.5, distance end ---------------------------------------------------------------------- -- Window ---------------------------------------------------------------------- -- Colours taken from robscript.com: near-black surfaces and the pink from the logo. 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) local Window = Airflow:CreateWindow({ Name = "Collect Cards Hub", LoadingSubtitle = "tycoon, shop, tower", ToggleUIKeybind = "RightControl", Size = UDim2.fromOffset(780, 540), Backdrop = { Weather = "Fireflies", Tint = 0.35 }, Theme = THEME, ConfigurationSaving = { Enabled = true, FolderName = "CollectCardsHub" }, Home = { Title = "Collect Cards Hub", Welcome = "Welcome back,", Tier = "v1.1", Stats = { "Players", "Session", "FPS", "Ping", "Game", "Executor" }, }, }) local LogTab = Window:CreateTab({ Name = "Logs", Icon = "scroll-text" }) local LogBox = LogTab:CreateGroupbox({ Name = "What the script is doing", Side = "Left" }) local Log = LogBox:CreateLog({ Name = "Activity", MaxEntries = 300, MaxRows = 14, Timestamps = true, EmptyText = "Nothing yet", }) local function log(message, color) Log:Push(tostring(message), color) end local function notify(title, content, kind) pcall(function() Window:Notify({ Title = title, Content = content, Type = kind or "Info", Duration = 4 }) end) end ---------------------------------------------------------------------- -- Settings (the switches write here, the loops read from here) ---------------------------------------------------------------------- local Settings = { -- Tycoon tycoonAuto = false, tycoonMethod = "Teleport", -- "Teleport" | "Walk" | "Teleport then walk" tycoonOrder = "Cheapest", -- "Cheapest" | "Game order" tycoonReserve = 0, tycoonDelay = 0.5, -- Shop shopPriority = "Furniture", -- "Furniture" | "Packs" | "Balanced" shopReserve = 0, shopBuyFurniture = false, shopBuyPacks = false, shopPackFilter = {}, shopFurnitureFilter = {}, -- Tower battleAuto = false, battleTower = "EmberOrigins", battleDelay = 1.2, battleStopOnLoss = true, battleStopFloor = 0, battleAutoDeck = true, battleClaimPacks = true, -- Packs packsAuto = false, packsDelay = 0.8, packsFilter = {}, packsPerBatch = 3, -- Dupes dupesAuto = false, dupesKeep = 1, dupesMinValue = 0, dupesRarities = {}, dupesDryRun = true, dupesSkipDeck = true, } -- A switching element starts a loop, and stopping it (or running the script -- again) ends that loop cleanly. getgenv().__CollectCardsHubGen = (getgenv().__CollectCardsHubGen or 0) + 1 local SCRIPT_GENERATION = getgenv().__CollectCardsHubGen local function scriptAlive() return getgenv().__CollectCardsHubGen == SCRIPT_GENERATION end local runners = {} local function runner(name, step) runners[name] = runners[name] or { generation = 0 } local entry = runners[name] return function(enabled) entry.generation += 1 local myGeneration = entry.generation if not enabled then return end task.spawn(function() while entry.generation == myGeneration and scriptAlive() do local ok, err = pcall(step) if not ok then log("" .. name .. " stopped: " .. tostring(err) .. "") end if entry.generation ~= myGeneration or not scriptAlive() then break end task.wait(0.25) end end) end end local function runOnce(name, step) task.spawn(function() local ok, err = pcall(step) if not ok then log("" .. name .. ": " .. tostring(err) .. "") end end) end ---------------------------------------------------------------------- -- 1. TYCOON: buying upgrade pads ---------------------------------------------------------------------- local padCooldown = {} local function myPads() local result = {} local lobby = workspace:FindFirstChild("Lobby") local plots = lobby and lobby:FindFirstChild("Plots") if not plots then return result end for _, plot in ipairs(plots:GetChildren()) do local tycoon = plot:FindFirstChild("Tycoon") if tycoon then for _, model in ipairs(tycoon:GetChildren()) do local button = model:FindFirstChild("Button", true) if button and button:IsA("BasePart") and button:GetAttribute("TycoonOwner") == LocalPlayer.UserId and button:GetAttribute("TycoonAvailable") == true then table.insert(result, { name = model.Name, button = button, id = button:GetAttribute("TycoonUpgrade"), price = tonumber(button:GetAttribute("TycoonPrice")) or 0, floor = tonumber(button:GetAttribute("TycoonFloor")) or 1, }) end end end end return result end --- Standing on a pad is enough: the game buys it by itself. If it does not, --- we ask for the upgrade directly. Returns "Bought" or a short reason. local function usePad(pad) if busy() then waitIdle(8) end local deadline = os.clock() + 2 while os.clock() < deadline do local button = pad.button if not button or not button.Parent or button:GetAttribute("TycoonAvailable") ~= true then return "Bought" end task.wait(0.1) end local answer, reason = runAction("BuyTycoonUpgrade", { upgradeId = pad.id, floor = pad.floor, }, 12) if answer and answer.status == "Completed" then return "Bought" end return tostring(reason) end --- One attempt to buy the next upgrade we can afford. local function buyNextUpgrade() if not gameState() then return "Still loading your shop" end local pads = myPads() if #pads == 0 then return "No upgrades left to buy" end if Settings.tycoonOrder == "Cheapest" then table.sort(pads, function(a, b) return a.price < b.price end) end local affordable = 0 local cheapest for _, pad in ipairs(pads) do if pad.price <= cash() - Settings.tycoonReserve then affordable += 1 cheapest = cheapest or pad end end if affordable == 0 then table.sort(pads, function(a, b) return a.price < b.price end) return "Waiting for cash (next one costs " .. money(pads[1].price) .. ")" end for _, pad in ipairs(pads) do local blockedUntil = padCooldown[pad.id] if blockedUntil and os.clock() < blockedUntil then continue end if pad.price > cash() - Settings.tycoonReserve then continue end local reason if Settings.tycoonMethod ~= "Walk" then log("Going to " .. tostring(pad.name) .. " (" .. money(pad.price) .. ")") if jumpTo(pad.button.Position) then reason = usePad(pad) if reason == "Bought" then return "Bought " .. tostring(pad.name) end if Settings.tycoonMethod == "Teleport" then padCooldown[pad.id] = os.clock() + 10 return "Could not buy " .. tostring(pad.name) .. " (" .. reason .. ")" end log("" .. reason .. " - walking there instead") elseif Settings.tycoonMethod == "Teleport" then padCooldown[pad.id] = os.clock() + 10 return "Could not reach " .. tostring(pad.name) end else log("Walking to " .. tostring(pad.name) .. " (" .. money(pad.price) .. ")") end local reached, distance = walkTo(pad.button.Position, 22) if not reached then padCooldown[pad.id] = os.clock() + 20 return "Could not walk to " .. tostring(pad.name) .. " (" .. string.format("%.1f", distance) .. " studs away)" end reason = usePad(pad) if reason == "Bought" then return "Bought " .. tostring(pad.name) end padCooldown[pad.id] = os.clock() + 15 return "Could not buy " .. tostring(pad.name) .. " (" .. reason .. ")" end return "All affordable pads are cooling down" end local tycoonAutoStep tycoonAutoStep = runner("Tycoon", function() if not Settings.tycoonAuto then return end local outcome = buyNextUpgrade() if string.find(outcome, "Waiting for cash", 1, true) then task.wait(2) elseif string.find(outcome, "cooling down", 1, true) then task.wait(2) elseif string.find(outcome, "Could not", 1, true) then log("" .. outcome .. "") task.wait(1.5) elseif string.find(outcome, "No upgrades", 1, true) then log("" .. outcome .. "") Settings.tycoonAuto = false tycoonAutoStep(false) else log("" .. outcome .. "") task.wait(Settings.tycoonDelay) end end) ---------------------------------------------------------------------- -- 2. SHOP: packs and furniture ---------------------------------------------------------------------- local function packAllowed(id) if next(Settings.shopPackFilter) == nil then return true end return Settings.shopPackFilter[id] == true end local function furnitureAllowed(id) if next(Settings.shopFurnitureFilter) == nil then return true end return Settings.shopFurnitureFilter[id] == true end local function buyCheapestPack() local cat, ven, s = catalog(), vendor(), gameState() if not (cat and ven and s and cat.packs and ven.offers) then return false end if s.capacity and s.capacity.ownedPacks >= s.capacity.packs then log("Pack storage is full") return false end local best, bestPrice for id, offer in pairs(ven.offers) do local definition = cat.packs[id] if definition and packAllowed(id) and type(offer.price) == "number" and (offer.remaining or 0) >= 1 then if offer.price <= cash() - Settings.shopReserve then if not bestPrice or offer.price < bestPrice then best, bestPrice = id, offer.price end end end end if not best then return false end local answer, reason = runAction("BuyPack", { quantity = 1, skuId = best, offerId = ven.offers[best].offerId, }, 12) if answer and answer.status == "Completed" then local entry = cat.packs[best] local name = ((entry.definition or entry).name or best) log("Pack: " .. name .. " for " .. money(bestPrice) .. "") Game.Refresh() return true end log("Pack " .. tostring(best) .. ": " .. tostring(reason) .. "") return false end local function buyCheapestFurniture() local cat, ven = catalog(), vendor() if not (cat and ven and cat.furniture and ven.furniture) then return false end local placed = 0 if gameState() and gameState().furniture then for _ in pairs(gameState().furniture) do placed += 1 end end if placed >= (tonumber(cat.maxFurniture) or 400) then log("You own the maximum amount of furniture") return false end local best, bestPrice for id, offer in pairs(ven.furniture) do local definition = cat.furniture[id] if definition and furnitureAllowed(id) and type(offer.price) == "number" and (offer.remaining or 0) >= 1 then if offer.price <= cash() - Settings.shopReserve then if not bestPrice or offer.price < bestPrice then best, bestPrice = id, offer.price end end end end if not best then return false end local answer, reason = runAction("BuyFurniture", { definitionId = best, offerId = ven.furniture[best].offerId, }, 12) if answer and answer.status == "Completed" then local entry = cat.furniture[best] local name = ((entry.definition or entry).name or best) log("Furniture: " .. name .. " for " .. money(bestPrice) .. "") Game.Refresh() return true end log("Furniture " .. tostring(best) .. ": " .. tostring(reason) .. "") return false end local shopFlip = false local shopAutoStep shopAutoStep = runner("Shop", function() if not (Settings.shopBuyPacks or Settings.shopBuyFurniture) then return end local kinds = {} if Settings.shopBuyFurniture then table.insert(kinds, "Furniture") end if Settings.shopBuyPacks then table.insert(kinds, "Packs") end if #kinds == 2 then if Settings.shopPriority == "Packs" then kinds = { "Packs", "Furniture" } elseif Settings.shopPriority == "Balanced" then shopFlip = not shopFlip kinds = shopFlip and { "Furniture", "Packs" } or { "Packs", "Furniture" } end end for _, kind in ipairs(kinds) do if kind == "Furniture" then if buyCheapestFurniture() then task.wait(0.3) return end elseif kind == "Packs" then if buyCheapestPack() then task.wait(0.3) return end end end end) --- Keeps the shop loop running while at least one category is switched on. local function syncShop() shopAutoStep(Settings.shopBuyPacks or Settings.shopBuyFurniture) end ---------------------------------------------------------------------- -- 3. TOWER: auto battle ---------------------------------------------------------------------- local TOWERS = { { id = "EmberOrigins", name = "Ember Origins" }, { id = "HydraAwakening", name = "Hydra Awakening" }, { id = "PocketPandemonium", name = "Pocket Pandemonium" }, { id = "RealmCollision", name = "Realm Collision" }, { id = "PrismaticWonders", name = "Prismic Wonders" }, } local battleStats = { floor = 0, runs = 0, wins = 0, losses = 0, lastRunId = nil, stuckAttempts = 0, deckReady = false, deckAt = 0, } local function ensureDeck() local deck = ask("GetBattleDeck") if not deck or deck.status ~= "Completed" or not deck.battle then return false end if #(deck.battle.deck or {}) > 0 then return true end local cards = deck.cards or {} table.sort(cards, function(a, b) return (tonumber(a.score) or 0) > (tonumber(b.score) or 0) end) local ids = {} for index = 1, math.min(4, #cards) do ids[index] = cards[index].assetId end if #ids == 0 then return false end local answer = runAction("SaveBattleDeck", { assetIds = ids }, 10) if answer and answer.status == "Completed" then log("Deck filled with your best " .. #ids .. " cards") return true end return false end local function collectTowerRewards() local answer = runAction("ClaimBattlePacks", {}, 10) if answer and answer.status == "Completed" and answer.result and answer.result.packIds and #answer.result.packIds > 0 then log("Collected " .. #answer.result.packIds .. " tower reward pack(s)") end end local battleAutoStep battleAutoStep = runner("Tower", function() if not Settings.battleAuto then return end if Settings.battleStopFloor > 0 and battleStats.floor >= Settings.battleStopFloor then log("Reached floor " .. Settings.battleStopFloor .. ", battle stopped") Settings.battleAuto = false battleAutoStep(false) return end if busy() then waitIdle(10) end if Settings.battleAutoDeck and (not battleStats.deckReady or (os.clock() - battleStats.deckAt) > 60) then battleStats.deckReady = ensureDeck() battleStats.deckAt = os.clock() end local run = ask("StartBattle", { towerId = Settings.battleTower, continue = true }) if not run then log("No answer from the tower") return end if run.status ~= "Completed" or not run.runId then local code = tostring(run.code or run.status) if code == "BattleActive" then -- A battle is still open somewhere, usually left over from a reload. -- Cancel the one we know about; if that fails, respawn clears it. battleStats.stuckAttempts = (battleStats.stuckAttempts or 0) + 1 if battleStats.lastRunId then ask("CancelBattle", { runId = battleStats.lastRunId }) battleStats.lastRunId = nil log("Cancelling the old battle") elseif battleStats.stuckAttempts >= 3 then log("Resetting the stuck battle") local character = LocalPlayer.Character local humanoid = character and character:FindFirstChildOfClass("Humanoid") if humanoid then humanoid.Health = 0 end battleStats.stuckAttempts = 0 end task.wait(2) return end log("Tower said: " .. code .. "") Settings.battleAuto = false battleAutoStep(false) return end battleStats.stuckAttempts = 0 battleStats.lastRunId = run.runId battleStats.floor = tonumber(run.floor) or battleStats.floor local waitTime = math.max(tonumber(run.minSeconds) or 1.8, 1.4) + Settings.battleDelay task.wait(waitTime) local answer, reason = runAction("FinishBattle", { runId = run.runId }, 15) battleStats.lastRunId = nil if not answer then log("Battle did not finish: " .. tostring(reason) .. "") ask("CancelBattle", { runId = run.runId }) return end if answer.status ~= "Completed" then log("Battle: " .. tostring(answer.code or answer.status) .. "") return end local result = answer.result or {} battleStats.runs += 1 local earned = tonumber(result.cash) or 0 if result.winner == "Player" then battleStats.wins += 1 battleStats.floor = tonumber(result.nextFloor) or battleStats.floor log("Floor " .. tostring(result.floor) .. " cleared, +" .. money(earned) .. "") else battleStats.losses += 1 log("Floor " .. tostring(result.floor) .. " lost") if Settings.battleStopOnLoss then Settings.battleAuto = false battleAutoStep(false) notify("Auto battle", "Lost on floor " .. tostring(result.floor) .. ", stopped", "Warning") return end end if Settings.battleClaimPacks then if (result.packIds and #result.packIds > 0) or (result.winner == "Player" and battleStats.runs % 5 == 0) then collectTowerRewards() end end end) ---------------------------------------------------------------------- -- 4. PACKS: opening ---------------------------------------------------------------------- local packStats = { opened = 0, cards = 0, duplicates = 0 } local function packTypeAllowed(id) if next(Settings.packsFilter) == nil then return true end return Settings.packsFilter[id] == true end local packsAutoStep packsAutoStep = runner("Packs", function() if not Settings.packsAuto then return end local s = gameState() if not s or not s.capacity then return end local inventory = ask("GetInventory", { kind = "Pack", limit = 100, offset = 0 }) if not inventory or inventory.status ~= "Completed" or not inventory.result then return end local owned = {} for _, item in ipairs(inventory.result.items or {}) do if item.kind == "Pack" and packTypeAllowed(item.skuId) then table.insert(owned, item) end end if #owned == 0 then return end if s.capacity.ownedCards + 3 > s.capacity.cards then log("Card storage is full, packs stay closed") return end local batch = {} for index = 1, math.min(Settings.packsPerBatch, #owned) do batch[index] = owned[index].id end local answer, reason = runAction("OpenPack", (#batch == 1) and { assetId = batch[1] } or { assetIds = batch }, 20) if answer and answer.status == "Completed" then local cards = (answer.result and answer.result.cards) or {} packStats.opened += #batch packStats.cards += #cards local seen, duplicates = {}, 0 for _, card in ipairs(cards) do local id = card.definitionId or card.id if seen[id] then duplicates += 1 else seen[id] = true end end packStats.duplicates += duplicates log("Opened " .. #batch .. " pack(s): " .. #cards .. " cards" .. (duplicates > 0 and (", " .. duplicates .. " duplicates") or "") .. "") Game.Refresh() else log("Could not open a pack: " .. tostring(reason) .. "") end task.wait(Settings.packsDelay) end) ---------------------------------------------------------------------- -- 5. DUPES: selling spare copies ---------------------------------------------------------------------- local dupeStats = { sold = 0, earned = 0, batches = 0 } local function allCards() local all = {} local offset = 0 for _ = 1, 40 do local answer = ask("GetInventory", { kind = "Card", limit = 100, offset = offset }) if not answer or answer.status ~= "Completed" or not answer.result then break end for _, item in ipairs(answer.result.items or {}) do table.insert(all, item) end local nextOffset = answer.result.nextOffset if type(nextOffset) ~= "number" or nextOffset <= offset then break end offset = nextOffset end return all end local function rarityAllowed(rarity) if next(Settings.dupesRarities) == nil then return true end return Settings.dupesRarities[rarity] == true end local function spareCopies() local cat = catalog() if not cat or not cat.cards then return {} end local inDeck = {} if Settings.dupesSkipDeck and gameState() and gameState().battle and gameState().battle.deck then for _, assetId in ipairs(gameState().battle.deck) do inDeck[assetId] = true end end local groups = {} for _, item in ipairs(allCards()) do if item.kind == "Card" and item.state == "Available" and not item.favorite and item.grade == nil then groups[item.definitionId] = groups[item.definitionId] or {} table.insert(groups[item.definitionId], item) end end local candidates = {} for definitionId, items in pairs(groups) do local definition = cat.cards[definitionId] if #items > Settings.dupesKeep and definition and definition.dealerEnabled ~= false and type(definition.dealerValue) == "number" and definition.dealerValue >= Settings.dupesMinValue and rarityAllowed(definition.rarity) then table.sort(items, function(a, b) return tostring(a.id) < tostring(b.id) end) for index = Settings.dupesKeep + 1, #items do if not inDeck[items[index].id] then table.insert(candidates, items[index].id) end if #candidates >= 50 then break end end end if #candidates >= 50 then break end end return candidates end local function sellBatch() local ids = spareCopies() if #ids == 0 then return 0 end local batch = {} for index = 1, math.min(25, #ids) do batch[index] = ids[index] end if Settings.dupesDryRun then log("[dry run] Would sell " .. #batch .. " spare card(s)") task.wait(1) return 0 end local quote = ask("QuoteDealer", { assetIds = batch }) if not quote or quote.status ~= "Completed" or type(quote.quoteId) ~= "string" then log("The dealer is not buying right now") return 0 end local answer, reason = runAction("SellCards", { confirm = true, quoteId = quote.quoteId }, 15) if answer and answer.status == "Completed" then local received = tonumber(answer.result and answer.result.received) or 0 dupeStats.sold += #batch dupeStats.earned += received dupeStats.batches += 1 log("Sold " .. #batch .. " spare card(s) for " .. money(received) .. "") Game.Refresh() return #batch end log("Could not sell: " .. tostring(reason) .. "") return 0 end local dupesAutoStep dupesAutoStep = runner("Dupes", function() if not Settings.dupesAuto then return end sellBatch() task.wait(0.6) end) ---------------------------------------------------------------------- -- TABS ---------------------------------------------------------------------- ---------------------------------------------------------------------- -- TYCOON ---------------------------------------------------------------------- local TycoonTab = Window:CreateTab({ Name = "Tycoon", Icon = "building-2" }) local TycoonMain = TycoonTab:CreateGroupbox({ Name = "Auto buy", Side = "Left" }) local TycoonInfo = TycoonTab:CreateGroupbox({ Name = "Status", Side = "Right" }) TycoonMain:CreateToggle({ Name = "Auto buy upgrades", Desc = "Buys the next upgrade pad in your shop as soon as you can afford it", Flag = "TycoonAuto", CurrentValue = false, Callback = function(on) Settings.tycoonAuto = on tycoonAutoStep(on) if on then log("Auto buy is on (" .. Settings.tycoonMethod .. ")") end end, }) TycoonMain:CreateDropdown({ Name = "How to reach the pad", Desc = "Teleport is instant; walking is slower but never fails", Options = { "Teleport", "Walk", "Teleport then walk" }, CurrentOption = "Teleport", Flag = "TycoonMethod", Callback = function(value) Settings.tycoonMethod = type(value) == "table" and (value[1] or "Teleport") or value or "Teleport" end, }) TycoonMain:CreateDropdown({ Name = "Buy order", Options = { "Cheapest", "Game order" }, CurrentOption = "Cheapest", Flag = "TycoonOrder", Callback = function(value) Settings.tycoonOrder = type(value) == "table" and (value[1] or "Cheapest") or value or "Cheapest" end, }) TycoonMain:CreateSlider({ Name = "Keep this much cash", Desc = "Never spend below this balance", Range = { 0, 500000 }, Increment = 100, Suffix = " $", CurrentValue = 0, Flag = "TycoonReserve", Callback = function(value) Settings.tycoonReserve = value end, }) TycoonMain:CreateSlider({ Name = "Pause between buys", Range = { 0.2, 5 }, Increment = 0.1, Suffix = " s", CurrentValue = 0.5, Flag = "TycoonDelay", Callback = function(value) Settings.tycoonDelay = value end, }) TycoonMain:CreateButton({ Name = "Buy the next upgrade now", Icon = "hand-coins", Style = "Primary", Callback = function() runOnce("Tycoon once", function() log(buyNextUpgrade()) end) end, }) TycoonMain:CreateButton({ Name = "Forget skipped pads", Icon = "refresh-cw", Desc = "Try pads again that were skipped earlier", Callback = function() table.clear(padCooldown) log("Skipped pads will be tried again") end, }) TycoonInfo:CreateStatus({ Name = "Pads to buy", Style = "Row", Pin = true, Update = function() local pads = myPads() if #pads == 0 then return "none left" end table.sort(pads, function(a, b) return a.price < b.price end) return #pads .. " left, from " .. money(pads[1].price) end, }) TycoonInfo:CreateStatus({ Name = "Next upgrade", Style = "Row", Update = function() local pads = myPads() if #pads == 0 then return "-" end table.sort(pads, function(a, b) return a.price < b.price end) return tostring(pads[1].name) .. " - " .. money(pads[1].price) end, }) TycoonInfo:CreateStatus({ Name = "Shop floors", Style = "Row", Update = function() local s = gameState() local floors = s and s.tycoon and s.tycoon.floors return floors and tostring(floors) or "-" end, }) TycoonInfo:CreateParagraph({ Title = "How this works", Content = "The script looks at the buy pads standing in your shop, picks the next one you can afford and goes there. " .. "By default it jumps straight onto the pad, which is instant; the game then buys the upgrade. " .. "If the game will not accept the jump, pick \"Teleport then walk\" or \"Walk\" and the character will go there on foot. " .. "Pads that fail are skipped for a few seconds so the script does not get stuck on one of them.", }) ---------------------------------------------------------------------- -- SHOP ---------------------------------------------------------------------- local ShopTab = Window:CreateTab({ Name = "Shop", Icon = "shopping-cart" }) local ShopPacks = ShopTab:CreateGroupbox({ Name = "Card packs", Side = "Left" }) local ShopBuilding = ShopTab:CreateGroupbox({ Name = "Building (furniture)", Side = "Right" }) local ShopPriority = ShopTab:CreateGroupbox({ Name = "Where money goes first", Side = "Right" }) ShopPacks:CreateToggle({ Name = "Auto buy packs", Desc = "Buys the cheapest pack you allow below", Flag = "ShopAutoPacks", CurrentValue = false, Callback = function(on) Settings.shopBuyPacks = on syncShop() log(on and "Auto buy packs is on" or "Auto buy packs is off") end, }) local ShopPackFilter = ShopPacks:CreateDropdown({ Name = "Packs allowed", Desc = "Leave empty to allow every pack", Options = {}, MultipleOptions = true, Searchable = true, Flag = "ShopPackFilter", Callback = function(selected) local map = {} for _, label in ipairs(selected or {}) do local id = tostring(label):match("%[([^%]]+)%]$") if id then map[id] = true end end Settings.shopPackFilter = map end, }) ShopBuilding:CreateToggle({ Name = "Auto buy furniture", Desc = "Buys the cheapest item you allow below", Flag = "ShopAutoFurniture", CurrentValue = false, Callback = function(on) Settings.shopBuyFurniture = on syncShop() log(on and "Auto buy furniture is on" or "Auto buy furniture is off") end, }) local ShopFurnitureFilter = ShopBuilding:CreateDropdown({ Name = "Furniture allowed", Desc = "Leave empty to allow every item", Options = {}, MultipleOptions = true, Searchable = true, Flag = "ShopFurnitureFilter", Callback = function(selected) local map = {} for _, label in ipairs(selected or {}) do local id = tostring(label):match("%[([^%]]+)%]$") if id then map[id] = true end end Settings.shopFurnitureFilter = map end, }) ShopPriority:CreateDropdown({ Name = "Spend on first", Desc = "Only matters when both categories are on", Options = { "Furniture", "Packs", "Balanced" }, CurrentOption = "Furniture", Flag = "ShopPriority", Callback = function(value) Settings.shopPriority = type(value) == "table" and (value[1] or "Furniture") or value or "Furniture" end, }) ShopPriority:CreateSlider({ Name = "Keep this much cash", Range = { 0, 500000 }, Increment = 100, Suffix = " $", CurrentValue = 0, Flag = "ShopReserve", Callback = function(value) Settings.shopReserve = value end, }) ShopPriority:CreateStatus({ Name = "Buying", Style = "Row", Pin = true, Update = function() local parts = {} if Settings.shopBuyFurniture then table.insert(parts, "furniture") end if Settings.shopBuyPacks then table.insert(parts, "packs") end if #parts == 0 then return "nothing" end return "" .. table.concat(parts, " + ") .. "" end, }) ShopPriority:CreateStatus({ Name = "Cash", Style = "Row", Update = function() return "" .. money(cash()) .. "" end, }) ShopPriority:CreateStatus({ Name = "Storage", Style = "Row", Update = function() local s = gameState() if not s or not s.capacity then return "-" end return string.format("packs %d/%d, cards %d/%d", s.capacity.ownedPacks, s.capacity.packs, s.capacity.ownedCards, s.capacity.cards) end, }) -- Fill the pick-lists once the shop catalogue is loaded. task.spawn(function() local deadline = os.clock() + 60 while os.clock() < deadline do local cat = catalog() if cat and cat.packs and cat.furniture then local packs = {} for id, entry in pairs(cat.packs) do local definition = entry.definition or entry if type(definition.price) == "number" then table.insert(packs, { label = (definition.name or id) .. " [" .. id .. "]", price = definition.price }) end end table.sort(packs, function(a, b) return a.price < b.price end) local packLabels = {} for _, entry in ipairs(packs) do table.insert(packLabels, entry.label) end ShopPackFilter:SetOptions(packLabels, { KeepSelection = true }) local furniture = {} for id, definition in pairs(cat.furniture) do if type(definition.price) == "number" and id ~= "Cashier" then table.insert(furniture, { label = (definition.name or id) .. " [" .. id .. "]", price = definition.price }) end end table.sort(furniture, function(a, b) return a.price < b.price end) local furnitureLabels = {} for _, entry in ipairs(furniture) do table.insert(furnitureLabels, entry.label) end ShopFurnitureFilter:SetOptions(furnitureLabels, { KeepSelection = true }) return end task.wait(0.5) end end) ---------------------------------------------------------------------- -- TOWER ---------------------------------------------------------------------- local BattleTab = Window:CreateTab({ Name = "Tower", Icon = "swords" }) local BattleMain = BattleTab:CreateGroupbox({ Name = "Auto battle", Side = "Left" }) local BattleInfo = BattleTab:CreateGroupbox({ Name = "Progress", Side = "Right" }) local towerOptions = {} for _, tower in ipairs(TOWERS) do table.insert(towerOptions, tower.id) end BattleMain:CreateDropdown({ Name = "Tower", Options = towerOptions, CurrentOption = "EmberOrigins", Flag = "BattleTower", Callback = function(value) Settings.battleTower = type(value) == "table" and (value[1] or "EmberOrigins") or value or "EmberOrigins" end, }) BattleMain:CreateToggle({ Name = "Auto battle", Desc = "Fights floor after floor on the tower above", Flag = "BattleAuto", CurrentValue = false, Callback = function(on) Settings.battleAuto = on battleAutoStep(on) if on then log("Auto battle is on: " .. Settings.battleTower) end end, }) BattleMain:CreateToggle({ Name = "Use my best cards", Desc = "Fills an empty deck with your four strongest cards", Flag = "BattleAutoDeck", CurrentValue = true, Callback = function(on) Settings.battleAutoDeck = on end, }) BattleMain:CreateToggle({ Name = "Stop when I lose", Flag = "BattleStopOnLoss", CurrentValue = true, Callback = function(on) Settings.battleStopOnLoss = on end, }) BattleMain:CreateToggle({ Name = "Collect tower rewards", Flag = "BattleClaimPacks", CurrentValue = true, Callback = function(on) Settings.battleClaimPacks = on end, }) BattleMain:CreateSlider({ Name = "Extra wait per floor", Desc = "Raise it if floors finish too fast to count", Range = { 0, 6 }, Increment = 0.1, Suffix = " s", CurrentValue = 1.2, Flag = "BattleDelay", Callback = function(value) Settings.battleDelay = value end, }) BattleMain:CreateSlider({ Name = "Stop at floor", Desc = "0 means no limit", Range = { 0, 120 }, Increment = 1, CurrentValue = 0, Flag = "BattleStopFloor", Callback = function(value) Settings.battleStopFloor = value end, }) BattleMain:CreateButton({ Name = "Collect rewards now", Icon = "gift", Callback = function() runOnce("Rewards", collectTowerRewards) end, }) BattleInfo:CreateStatus({ Name = "Floor", Style = "Row", Pin = true, Update = function() local s = gameState() local best = s and s.battle and s.battle.floors and s.battle.floors[Settings.battleTower] return tostring(battleStats.floor) .. " (best " .. tostring(best or 0) .. ")" end, }) BattleInfo:CreateStatus({ Name = "Record", Style = "Row", Update = function() return string.format("%d won, %d lost, %d fought", battleStats.wins, battleStats.losses, battleStats.runs) end, }) BattleInfo:CreateStatusList({ MaxRows = 6, Update = function() local rows = {} local s = gameState() for _, tower in ipairs(TOWERS) do local best = s and s.battle and s.battle.floors and s.battle.floors[tower.id] or 0 local pending = s and s.battle and s.battle.pendingPacks and s.battle.pendingPacks[tower.id] or 0 table.insert(rows, { Text = tower.name, Value = "floor " .. tostring(best) .. (pending > 0 and (" - " .. pending .. " pack(s)") or ""), }) end return rows end, }) ---------------------------------------------------------------------- -- PACKS ---------------------------------------------------------------------- local PacksTab = Window:CreateTab({ Name = "Packs", Icon = "package-open" }) local PacksMain = PacksTab:CreateGroupbox({ Name = "Auto open", Side = "Left" }) local PacksInfo = PacksTab:CreateGroupbox({ Name = "Numbers", Side = "Right" }) PacksMain:CreateToggle({ Name = "Auto open packs", Desc = "Opens every pack in your inventory", Flag = "PacksAuto", CurrentValue = false, Callback = function(on) Settings.packsAuto = on packsAutoStep(on) if on then log("Auto open is on") end end, }) local PacksFilter = PacksMain:CreateDropdown({ Name = "Packs to open", Desc = "Leave empty to open every pack", Options = {}, MultipleOptions = true, Searchable = true, Flag = "PacksFilter", Callback = function(selected) local map = {} for _, label in ipairs(selected or {}) do local id = tostring(label):match("%[([^%]]+)%]$") if id then map[id] = true end end Settings.packsFilter = map end, }) PacksMain:CreateSlider({ Name = "Packs at a time", Range = { 1, 4 }, Increment = 1, CurrentValue = 3, Flag = "PacksPerBatch", Callback = function(value) Settings.packsPerBatch = value end, }) PacksMain:CreateSlider({ Name = "Pause between openings", Range = { 0.2, 5 }, Increment = 0.1, Suffix = " s", CurrentValue = 0.8, Flag = "PacksDelay", Callback = function(value) Settings.packsDelay = value end, }) PacksMain:CreateButton({ Name = "Open one pack now", Icon = "package", Callback = function() runOnce("Open one", function() local inventory = ask("GetInventory", { kind = "Pack", limit = 100, offset = 0 }) local item = inventory and inventory.result and inventory.result.items and inventory.result.items[1] if not item then log("You have no packs to open") return end local answer, reason = runAction("OpenPack", { assetId = item.id }, 20) if answer and answer.status == "Completed" then local cards = (answer.result and answer.result.cards) or {} packStats.opened += 1 packStats.cards += #cards log("Opened one pack: " .. #cards .. " cards") Game.Refresh() else log("Could not open the pack: " .. tostring(reason) .. "") end end) end, }) PacksInfo:CreateStatus({ Name = "Opened so far", Style = "Row", Pin = true, Update = function() return packStats.opened .. " pack(s), " .. packStats.cards .. " cards" end, }) PacksInfo:CreateStatus({ Name = "In inventory", Style = "Row", Update = function() local s = gameState() if not s or not s.capacity then return "-" end return string.format("%d pack(s), cards %d/%d", s.capacity.ownedPacks, s.capacity.ownedCards, s.capacity.cards) end, }) task.spawn(function() local deadline = os.clock() + 60 while os.clock() < deadline do local cat = catalog() if cat and cat.packs then local entries = {} for id, entry in pairs(cat.packs) do local definition = entry.definition or entry table.insert(entries, { label = (definition.name or id) .. " [" .. id .. "]", price = tonumber(definition.price) or 0, }) end table.sort(entries, function(a, b) return a.price < b.price end) local labels = {} for _, entry in ipairs(entries) do table.insert(labels, entry.label) end PacksFilter:SetOptions(labels, { KeepSelection = true }) return end task.wait(0.5) end end) ---------------------------------------------------------------------- -- DUPES ---------------------------------------------------------------------- local DupesTab = Window:CreateTab({ Name = "Dupes", Icon = "copy" }) local DupesMain = DupesTab:CreateGroupbox({ Name = "Sell spares", Side = "Left" }) local DupesInfo = DupesTab:CreateGroupbox({ Name = "Numbers", Side = "Right" }) DupesMain:CreateToggle({ Name = "Sell duplicate cards", Desc = "Sells copies you do not need for extra cash", Flag = "DupesAuto", CurrentValue = false, Callback = function(on) Settings.dupesAuto = on dupesAutoStep(on) if on then log("Selling duplicates is on" .. (Settings.dupesDryRun and " (dry run)" or "")) end end, }) DupesMain:CreateToggle({ Name = "Dry run only", Desc = "Shows what it would sell without selling", Flag = "DupesDryRun", CurrentValue = true, Callback = function(on) Settings.dupesDryRun = on end, }) DupesMain:CreateSlider({ Name = "Keep copies of each card", Range = { 1, 5 }, Increment = 1, CurrentValue = 1, Flag = "DupesKeep", Callback = function(value) Settings.dupesKeep = value end, }) DupesMain:CreateSlider({ Name = "Only sell cards worth more than", Range = { 0, 5000 }, Increment = 10, Suffix = " $", CurrentValue = 0, Flag = "DupesMinValue", Callback = function(value) Settings.dupesMinValue = value end, }) DupesMain:CreateToggle({ Name = "Never sell my battle deck", Flag = "DupesSkipDeck", CurrentValue = true, Callback = function(on) Settings.dupesSkipDeck = on end, }) local DupesRarityFilter = DupesMain:CreateDropdown({ Name = "Rarities to sell", Desc = "Leave empty to allow every rarity", Options = {}, MultipleOptions = true, Searchable = true, Flag = "DupesRarities", Callback = function(selected) local map = {} for _, rarity in ipairs(selected or {}) do map[tostring(rarity)] = true end Settings.dupesRarities = map end, }) DupesMain:CreateButton({ Name = "Sell one batch now", Icon = "coins", Style = "Primary", Callback = function() runOnce("Sell once", function() if sellBatch() == 0 and not Settings.dupesDryRun then log("Nothing to sell right now") end end) end, }) DupesInfo:CreateStatus({ Name = "Sold so far", Style = "Row", Pin = true, Update = function() return dupeStats.sold .. " card(s) for " .. money(dupeStats.earned) end, }) DupesInfo:CreateStatus({ Name = "Mode", Style = "Row", Update = function() return Settings.dupesDryRun and "dry run" or "selling" end, }) DupesInfo:CreateStatusList({ MaxRows = 8, UpdateRate = 5, Update = function() local cat = catalog() if not cat or not cat.cards then return {} end local counts, definitions = {}, {} for _, item in ipairs(allCards()) do if item.state == "Available" then local definition = cat.cards[item.definitionId] if definition then counts[item.definitionId] = (counts[item.definitionId] or 0) + 1 definitions[item.definitionId] = definition end end end local list = {} for definitionId, count in pairs(counts) do if count > 1 then table.insert(list, { id = definitionId, count = count, definition = definitions[definitionId] }) end end table.sort(list, function(a, b) return a.count > b.count end) local rows = {} for index = 1, math.min(8, #list) do local entry = list[index] table.insert(rows, { Text = (entry.definition.name or entry.id) .. " x" .. entry.count, Value = money((entry.definition.dealerValue or 0) * (entry.count - Settings.dupesKeep)), Tone = entry.count > 3 and "Warning" or "Muted", }) end return rows end, }) -- Rarity list comes from the card catalogue. task.spawn(function() local deadline = os.clock() + 60 while os.clock() < deadline do local cat = catalog() if cat and cat.cards then local seen = {} for _, definition in pairs(cat.cards) do if definition.rarity then seen[definition.rarity] = true end end local list = {} for rarity in pairs(seen) do table.insert(list, rarity) end table.sort(list) DupesRarityFilter:SetOptions(list, { KeepSelection = true }) return end task.wait(0.5) end end) ---------------------------------------------------------------------- -- SETTINGS ---------------------------------------------------------------------- local SettingsTab = Window:CreateTab({ Name = "Settings", Icon = "settings" }) SettingsTab:CreateConfigManager({ Name = "Configs", Side = "Left" }) SettingsTab:CreateThemeManager({ Name = "Theme", Side = "Right" }) local HelpBox = SettingsTab:CreateGroupbox({ Name = "Quick guide", Side = "Right" }) HelpBox:CreateParagraph({ Title = "Collect Cards Hub", Content = "Tycoon: turns your cash into shop upgrades automatically.\n" .. "Shop: buys packs and Building furniture that you allow.\n" .. "Tower: keeps fighting the tower you pick.\n" .. "Packs: opens everything in your pack inventory.\n" .. "Dupes: sells the extra copies of cards you already own.\n\n" .. "Every switch starts off. Use Configs to save your setup.\n" .. "RightControl hides the window.", }) ---------------------------------------------------------------------- -- Start ---------------------------------------------------------------------- task.spawn(function() pcall(function() Window:LoadAutoload() end) log("Loaded - you have " .. money(cash()) .. " cash") end) print("[Collect Cards Hub] loaded")