--[[ ===================================================================== Kitten Game - Kitten Hub (Potassium / Airflow UI) v0.3 - farm, minigames, tower, shops, ESP ===================================================================== Farm * Auto coins - teleport onto every coin from Workspace.CoinSpawner * Auto daily - claim the daily pad when its timer is up Minigames * Auto join - accept invites (TeleportPlayer_Cinematic) * Simon Says bot - answers ShowCommand with the right SelectAction * Math Race bot - reads the question (answer included) and steps on the tile * Tower rewards - picks a card and verifies it the moment ShowCards arrives Shop * Auto buy kitten - strict guards: price cap, reserve, categories, dry run * Mini chests - opens only the chests you already own Visuals * ESP - coins, fish, crystals, chests, carried kittens Window: RightControl API: getgenv().KittenHub ===================================================================== --]] local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local Workspace = game:GetService("Workspace") local CollectionService = game:GetService("CollectionService") local VirtualUser = game:GetService("VirtualUser") local LocalPlayer = Players.LocalPlayer --------------------------------------------------------------------- -- UI library --------------------------------------------------------------------- local LIBRARY_URL = "https://raw.githubusercontent.com/bobloscript/Robly_Airflow/main/Source.luau" -- Unload a previous instance first, so its listeners die before ours appear. do local previous = getgenv().KittenHub if previous and type(previous.destroy) == "function" then pcall(previous.destroy) end end local Airflow do local ok, result = pcall(function() return loadstring(game:HttpGet(LIBRARY_URL))() end) if not ok or type(result) ~= "table" then error("[KittenHub] failed to load Airflow UI: " .. tostring(result)) end Airflow = result end --------------------------------------------------------------------- -- Remotes --------------------------------------------------------------------- local Remotes = ReplicatedStorage:WaitForChild("RemoteEvents", 30) if not Remotes then error("[KittenHub] RemoteEvents not found - is the client in Kitten Game?") end local function remote(folder, name) local parent = folder or Remotes local child = parent and parent:FindFirstChild(name, true) return child end local TeleportRemote = Remotes:WaitForChild("TeleportPlayer", 30) local TeleportHome = Remotes:WaitForChild("TeleportPlayer_Home", 30) local TeleportCinematic = Remotes:WaitForChild("TeleportPlayer_Cinematic", 30) local GameEvents = Remotes:WaitForChild("GameEvents", 20) local JoinPrompt = GameEvents and GameEvents:FindFirstChild("GameJoinPrompt") local JoinReset = GameEvents and GameEvents:FindFirstChild("GameJoinPrompt_Reset") local SimonEvents = GameEvents and GameEvents:FindFirstChild("SimonSaysEvents") local ShowCommand = SimonEvents and SimonEvents:FindFirstChild("ShowCommand") local SelectAction = SimonEvents and SimonEvents:FindFirstChild("SelectAction") local MathEvents = GameEvents and GameEvents:FindFirstChild("MathRace") local ShowQuestion = MathEvents and MathEvents:FindFirstChild("ShowQuestion") local TowerEvents = Remotes:FindFirstChild("TowerEvents") local ShowCards = TowerEvents and TowerEvents:FindFirstChild("TowerReward_ShowCards") local VerifyCard = TowerEvents and TowerEvents:FindFirstChild("TowerReward_Verify") local MorphEvents = Remotes:FindFirstChild("MorphEvents") local MorphRemote = MorphEvents and MorphEvents:FindFirstChild("Morph") local PurchasePrompt = MorphEvents and MorphEvents:FindFirstChild("PurchasePrompt") local PurchaseVerification = MorphEvents and MorphEvents:FindFirstChild("PurchaseVerification") local DailyShopEvents = Remotes:FindFirstChild("DailyShopEvents") local OpenMiniChests = DailyShopEvents and DailyShopEvents:FindFirstChild("OpenMiniChests") --------------------------------------------------------------------- -- Connection tracking (so a reload never leaves stale listeners) --------------------------------------------------------------------- local Connections = {} local function connect(signal, handler) if not signal then return nil end local connection = signal:Connect(handler) Connections[#Connections + 1] = connection return connection end local function disconnectAll() for _, connection in ipairs(Connections) do pcall(function() connection:Disconnect() end) end Connections = {} getgenv().__kittenHubConnections = Connections end do local stale = getgenv().__kittenHubConnections if type(stale) == "table" then for _, connection in ipairs(stale) do pcall(function() connection:Disconnect() end) end end getgenv().__kittenHubConnections = Connections end if not (TeleportRemote and TeleportHome and TeleportCinematic) then error("[KittenHub] Potassium bridge is attached to another game (remotes not found)") end --------------------------------------------------------------------- -- Player data --------------------------------------------------------------------- local function playerData() return LocalPlayer:FindFirstChild("PlayerData") end local function stat(name, default) local folder = playerData() local node = folder and folder:FindFirstChild(name) if node and node:IsA("ValueBase") then return node.Value end return default end local function gold() return tonumber(stat("Points", 0)) or 0 end local function serverNow() return Workspace:GetServerTimeNow() end local function root() local char = LocalPlayer.Character return char and char:FindFirstChild("HumanoidRootPart") end local function region() return LocalPlayer:GetAttribute("Region_Music") or "General" end local function logger(...) local parts = {} for index = 1, select("#", ...) do parts[#parts + 1] = tostring(select(index, ...)) end print("[KittenHub] " .. table.concat(parts, " ")) end local function formatNumber(value) local text = tostring(math.floor(tonumber(value) or 0)) return (text:reverse():gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")) end --------------------------------------------------------------------- -- State --------------------------------------------------------------------- local State = { options = { autoCoins = false, autoDaily = false, autoJoin = false, antiAfk = true, coinDelay = 0.35, anchorReturn = true, autoSimon = false, autoMath = false, autoTower = false, autoBuy = false, autoChests = false, dryRun = true, maxPrice = 50, reserve = 10, categories = { Free = true, Regular = true, Chunky = false, Baby = false, Munchkin = false, Group = false }, espCoins = false, espFish = false, espCrystals = false, espChests = false, espPlayers = false, espRange = 400, }, stats = { goldStart = gold(), goldLast = gold(), coins = 0, claims = 0, joins = 0, simon = 0, math = 0, tower = 0, bought = 0, chests = 0, spent = 0, startedAt = os.clock(), }, anchor = nil, lastJoinCfg = nil, joinCooldown = 0, running = true, esp = {}, } --------------------------------------------------------------------- -- Teleport helpers --------------------------------------------------------------------- local function teleportTo(position, regionName) if typeof(position) == "CFrame" then position = position.Position end if typeof(position) ~= "Vector3" then return false end TeleportRemote:FireServer(CFrame.new(position), regionName or "General") return true end local function saveAnchor() local hrp = root() if hrp then State.anchor = hrp.Position return true end return false end local function returnToAnchor() if State.anchor and State.options.anchorReturn then teleportTo(State.anchor) return true end return false end local function goHome() TeleportHome:FireServer() end --------------------------------------------------------------------- -- Coins --------------------------------------------------------------------- local function coinSpawner() return Workspace:FindFirstChild("CoinSpawner") end local function liveCoins() local list = {} local spawner = coinSpawner() if not spawner then return list end for _, child in ipairs(spawner:GetChildren()) do if child:IsA("BasePart") and child.Name == "Coin" then list[#list + 1] = child end end return list end local function nearestCoin() local hrp = root() if not hrp then return nil, math.huge end local best, bestDistance = nil, math.huge for _, coin in ipairs(liveCoins()) do local distance = (coin.Position - hrp.Position).Magnitude if distance < bestDistance then best, bestDistance = coin, distance end end return best, bestDistance end local function collectCoin(coin) if not coin or not coin.Parent then return false end local hrp = root() if not hrp then return false end if (coin.Position - hrp.Position).Magnitude <= 4 then return true end TeleportRemote:FireServer(CFrame.new(coin.Position), "General") return true end local coinThread = nil local function startCoinLoop() if coinThread then return end coinThread = task.spawn(function() while State.running and State.options.autoCoins do local ok, err = pcall(function() local coin, distance = nearestCoin() if coin and distance > 4 then collectCoin(coin) task.wait(State.options.coinDelay) else task.wait(0.2) end end) if not ok then logger("coin loop: " .. tostring(err)) task.wait(1) end end coinThread = nil end) end --------------------------------------------------------------------- -- Gold tracking (session stats) --------------------------------------------------------------------- task.spawn(function() while State.running do local current = gold() if current > State.stats.goldLast then State.stats.coins = State.stats.coins + (current - State.stats.goldLast) end State.stats.goldLast = current task.wait(0.5) end end) --------------------------------------------------------------------- -- Daily reward --------------------------------------------------------------------- local function dailyPad() local model = Workspace:FindFirstChild("DailyRewardModel") local pad = model and model:FindFirstChild("Pad") if pad and pad:IsA("BasePart") then return pad end return nil end local function dailyReadyIn() local ready = tonumber(stat("DailyReward_Time", 0)) or 0 return ready - serverNow() end local function claimDaily() local pad = dailyPad() if not pad then return false end teleportTo(pad.Position) task.wait(1.2) State.stats.claims = State.stats.claims + 1 task.wait(0.6) returnToAnchor() return true end task.spawn(function() while State.running do if State.options.autoDaily then local ok, err = pcall(function() if dailyReadyIn() <= 0 then claimDaily() end end) if not ok then logger("daily: " .. tostring(err)) end end task.wait(15) end end) --------------------------------------------------------------------- -- Minigame invites --------------------------------------------------------------------- local function configValid(cfg) if type(cfg) ~= "table" then return false end return cfg.gameName ~= nil and cfg.cameraPos ~= nil and cfg.lightingLock ~= nil and cfg.teleportTo ~= nil and cfg.regionName ~= nil end local function joinGame(cfg) if not configValid(cfg) then return false end TeleportCinematic:FireServer( cfg.gameName, cfg.cameraPos, cfg.lightingLock, cfg.clockTime, cfg.teleportTo, cfg.regionName, true ) State.stats.joins = State.stats.joins + 1 return true end local lastInvite = { name = nil, at = 0 } connect(JoinPrompt and JoinPrompt.OnClientEvent, function(_message, cfg) if configValid(cfg) then State.lastJoinCfg = cfg if cfg.gameName ~= lastInvite.name or os.clock() - lastInvite.at > 30 then lastInvite.name = cfg.gameName lastInvite.at = os.clock() logger("minigame invite: " .. tostring(cfg.gameName)) end end if not State.options.autoJoin then return end local now = os.clock() if now < State.joinCooldown then return end State.joinCooldown = now + 10 task.delay(1, function() pcall(function() joinGame(cfg) end) end) end) connect(JoinReset and JoinReset.OnClientEvent, function() State.joinCooldown = 0 end) --------------------------------------------------------------------- -- Simon Says --------------------------------------------------------------------- local SIMON_ACTIONS = { sit = true, stand = true, sploot = true, dance = true } local function cleanAction(text) return (tostring(text or ""):lower():gsub("%s+", "")) end local function simonAnswer(command, notFlag) local action = cleanAction(command) if not SIMON_ACTIONS[action] then return end if notFlag then logger("simon: trick command '" .. action .. "' - holding still") return end if not (State.options.autoSimon and SelectAction) then return end task.delay(0.9, function() if not State.options.autoSimon then return end pcall(function() SelectAction:FireServer(action) end) State.stats.simon = State.stats.simon + 1 end) end connect(ShowCommand and ShowCommand.OnClientEvent, function(command, notFlag) pcall(simonAnswer, command, notFlag) end) --------------------------------------------------------------------- -- Math Race --------------------------------------------------------------------- local function tileNumber(part) local attr = part:GetAttribute("Number") or part:GetAttribute("Value") or part:GetAttribute("Answer") if attr ~= nil then local n = tonumber(attr) if n then return n end end for _, descendant in ipairs(part:GetDescendants()) do if descendant:IsA("TextLabel") or descendant:IsA("TextButton") or descendant:IsA("SurfaceGui") then local text = descendant:IsA("SurfaceGui") and "" or (descendant.Text or "") local digits = text:gsub("[^%-%d%.]", "") local n = tonumber(digits) if n then return n end end end local digits = part.Name:gsub("[^%-%d%.]", "") return tonumber(digits) end local function findMathTile(answer) for _, part in ipairs(CollectionService:GetTagged("MathTile")) do if part:IsA("BasePart") then local n = tileNumber(part) if n and math.abs(n - answer) < 0.01 then return part end end end return nil end local function solveQuestion(question) if type(question) ~= "table" then return nil end local answer = tonumber(question.answer) if answer then return answer end local a = tonumber(question.number1) local b = tonumber(question.number2) local op = tostring(question.operator or "") if not (a and b) then return nil end if op == "+" then return a + b elseif op == "-" then return a - b elseif op == "*" or op == "x" or op == "X" then return a * b elseif op == "/" then return a / b end return nil end local function mathAnswer(question) if not State.options.autoMath then return end local answer = solveQuestion(question) if not answer then return end task.wait(0.4) local tile = findMathTile(answer) if not tile then logger("math: no tile for " .. tostring(answer)) return end teleportTo(tile.Position + Vector3.new(0, 3, 0), region()) State.stats.math = State.stats.math + 1 end connect(ShowQuestion and ShowQuestion.OnClientEvent, function(question) pcall(mathAnswer, question) end) --------------------------------------------------------------------- -- Tower rewards --------------------------------------------------------------------- connect(ShowCards and ShowCards.OnClientEvent, function(sessionId) if not (State.options.autoTower and VerifyCard) then return end task.delay(1.5, function() if not State.options.autoTower then return end pcall(function() VerifyCard:FireServer(1, sessionId) end) State.stats.tower = State.stats.tower + 1 logger("tower: card 1 verified") end) end) --------------------------------------------------------------------- -- Kitten catalog --------------------------------------------------------------------- local AssetData = ReplicatedStorage:FindFirstChild("AssetData") local KittenModule = AssetData and AssetData:FindFirstChild("KittenData") local Catalog = {} do local allowed = { Free = true, Regular = true, Chunky = true, Baby = true, Munchkin = true, Group = true } local ok, data = false, nil if KittenModule then ok, data = pcall(require, KittenModule) end if ok and type(data) == "table" and type(data.AllKittens) == "table" then for _, group in ipairs(data.AllKittens) do for id, entry in pairs(group) do if type(entry) == "table" and type(entry[2]) == "number" and entry[2] > 0 then local category = tostring(entry[6] or "Regular") if allowed[category] then Catalog[#Catalog + 1] = { id = tostring(id), name = tostring(entry[1] or id), cost = entry[2], category = category, } end end end end table.sort(Catalog, function(a, b) return a.cost < b.cost end) end end local function ownedKittens() local set = {} local folder = playerData() and playerData():FindFirstChild("OwnedCats") if folder then for _, child in ipairs(folder:GetChildren()) do set[child.Name] = true end end return set end local skipped = {} local function cheapestTarget() local owned = ownedKittens() local best = nil for _, kitten in ipairs(Catalog) do if not owned[kitten.id] and not skipped[kitten.id] and State.options.categories[kitten.category] then best = kitten break end end return best end -- The real shop flow: try to morph into a kitten -> the server answers with a -- PurchasePrompt(name, price) -> confirm only if the price fits the rules. -- The price shipped in KittenData is NOT the gold price, so we never guess it. local pendingBuy = { kit = nil, at = 0 } local function requestKitten(kitten) if not kitten or not MorphRemote then return false end pendingBuy.kit = kitten pendingBuy.at = os.clock() MorphRemote:FireServer(kitten.id, kitten.category) return true end connect(PurchasePrompt and PurchasePrompt.OnClientEvent, function(name, price) local priceNum = tonumber(price) or 0 local isPending = pendingBuy.kit ~= nil and tostring(name) == pendingBuy.kit.id if not (isPending or State.options.autoBuy) then return end if State.options.dryRun then logger(string.format("dry run: %s costs %d gold", tostring(name), priceNum)) skipped[tostring(name)] = true pendingBuy.kit = nil return end if priceNum > State.options.maxPrice or (gold() - priceNum) < State.options.reserve then logger(string.format("skip %s: %d gold (cap %d, reserve %d)", tostring(name), priceNum, State.options.maxPrice, State.options.reserve)) skipped[tostring(name)] = true pendingBuy.kit = nil return end if not PurchaseVerification then return end task.delay(0.4, function() pcall(function() PurchaseVerification:FireServer(name, price) end) State.stats.bought = State.stats.bought + 1 State.stats.spent = State.stats.spent + priceNum logger(string.format("bought %s for %d gold", tostring(name), priceNum)) pendingBuy.kit = nil end) end) -- No prompt within 5 s = this kitten is not sold through the morph flow -- (level/index unlock, event reward, ...). Skip it instead of looping. task.spawn(function() while State.running do if pendingBuy.kit and os.clock() - pendingBuy.at > 5 then skipped[pendingBuy.kit.id] = true pendingBuy.kit = nil end task.wait(1) end end) task.spawn(function() while State.running do if State.options.autoBuy and pendingBuy.kit == nil then local ok, err = pcall(function() local target = cheapestTarget() if target then requestKitten(target) task.wait(6) end end) if not ok then logger("buy: " .. tostring(err)) end end task.wait(5) end end) --------------------------------------------------------------------- -- Mini chests --------------------------------------------------------------------- task.spawn(function() while State.running do if State.options.autoChests and OpenMiniChests then local ok, err = pcall(function() local count = tonumber(stat("MiniChests", 0)) or 0 if count > 0 then local focus = "" local gui = LocalPlayer:FindFirstChild("PlayerGui") local scripts = gui and gui:FindFirstChild("PlayerUI") and gui.PlayerUI:FindFirstChild("Scripts") local client = scripts and scripts:FindFirstChild("MiniChestOpening_Client") if client then focus = client:GetAttribute("FocusSelection") or "" end OpenMiniChests:FireServer(focus) State.stats.chests = State.stats.chests + 1 task.wait(2.5) end end) if not ok then logger("chests: " .. tostring(err)) end end task.wait(8) end end) --------------------------------------------------------------------- -- ESP --------------------------------------------------------------------- local ESP_COLORS = { coins = Color3.fromRGB(255, 205, 60), fish = Color3.fromRGB(90, 200, 255), crystals = Color3.fromRGB(190, 120, 255), chests = Color3.fromRGB(255, 130, 90), players = Color3.fromRGB(255, 90, 140), } local espFolder = nil local function ensureEspFolder() if not espFolder or not espFolder.Parent then local gui = LocalPlayer:FindFirstChild("PlayerGui") espFolder = gui and gui:FindFirstChild("KittenHubESP") if not espFolder and gui then espFolder = Instance.new("Folder") espFolder.Name = "KittenHubESP" espFolder.Parent = gui end end return espFolder end local function addHighlight(key, part, color) local folder = ensureEspFolder() if not folder then return end local highlight = State.esp[key] if highlight and highlight.Parent == part then return end if highlight then pcall(function() highlight:Destroy() end) end highlight = Instance.new("Highlight") highlight.Name = "KittenHub" highlight.FillColor = color highlight.FillTransparency = 0.55 highlight.OutlineColor = color highlight.OutlineTransparency = 0 highlight.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop highlight.Parent = part State.esp[key] = highlight end local function clearEsp() for key, highlight in pairs(State.esp) do pcall(function() highlight:Destroy() end) State.esp[key] = nil end if espFolder then pcall(function() espFolder:Destroy() end) espFolder = nil end end local function espTargets() local targets = {} if State.options.espCoins then for index, coin in ipairs(liveCoins()) do targets[#targets + 1] = { key = coin, part = coin, color = ESP_COLORS.coins } end end if State.options.espFish then local fishAssets = Workspace:FindFirstChild("FishAssets") local spawned = fishAssets and fishAssets:FindFirstChild("SpawnedFish") if spawned then for _, part in ipairs(spawned:GetChildren()) do if part:IsA("BasePart") and part.Name ~= "SpawnedFish" then targets[#targets + 1] = { key = part, part = part, color = ESP_COLORS.fish } end end end end if State.options.espCrystals then local crystals = Workspace:FindFirstChild("Crystals") if crystals then for _, part in ipairs(crystals:GetChildren()) do if part:IsA("BasePart") then targets[#targets + 1] = { key = part, part = part, color = ESP_COLORS.crystals } end end end end if State.options.espChests then for _, descendant in ipairs(Workspace:GetDescendants()) do if descendant:IsA("BasePart") and string.find(string.lower(descendant.Name), "chest") then targets[#targets + 1] = { key = descendant, part = descendant, color = ESP_COLORS.chests } end end end if State.options.espPlayers then for _, player in ipairs(Players:GetPlayers()) do if player ~= LocalPlayer then local char = player.Character local hrp = char and char:FindFirstChild("HumanoidRootPart") local heldBy = player:GetAttribute("HeldBy") local pickedUp = player:GetAttribute("PickedUp") if hrp and ((type(heldBy) == "string" and heldBy ~= "") or (type(pickedUp) == "string" and pickedUp ~= "")) then targets[#targets + 1] = { key = hrp, part = hrp, color = ESP_COLORS.players } end end end end return targets end task.spawn(function() while State.running do local anyOn = State.options.espCoins or State.options.espFish or State.options.espCrystals or State.options.espChests or State.options.espPlayers if anyOn then local ok, err = pcall(function() local active = {} local hrp = root() for _, target in ipairs(espTargets()) do if target.part and target.part.Parent then local distance = hrp and (target.part.Position - hrp.Position).Magnitude or 0 if distance <= State.options.espRange then active[target.key] = true addHighlight(target.key, target.part, target.color) end end end for key, highlight in pairs(State.esp) do if not active[key] then pcall(function() highlight:Destroy() end) State.esp[key] = nil end end end) if not ok then logger("esp: " .. tostring(err)) end elseif next(State.esp) then clearEsp() end task.wait(1) end end) --------------------------------------------------------------------- -- Anti-AFK --------------------------------------------------------------------- connect(LocalPlayer.Idled, function() if not State.options.antiAfk then return end pcall(function() VirtualUser:CaptureController() VirtualUser:ClickButton2(Vector2.new()) end) end) --------------------------------------------------------------------- -- UI --------------------------------------------------------------------- State.running = true local Window = Airflow:CreateWindow({ Name = "Kitten Hub", LoadingSubtitle = "v0.3 farm + minigames + shop", ToggleUIKeybind = "RightControl", ConfigurationSaving = { Enabled = true, FolderName = "KittenHub" }, Home = false, }) local FarmTab = Window:CreateTab({ Name = "Farm", Icon = "coins" }) local GamesTab = Window:CreateTab({ Name = "Games", Icon = "zap" }) local ShopTab = Window:CreateTab({ Name = "Shop", Icon = "shopping-cart" }) local VisualTab = Window:CreateTab({ Name = "Visuals", Icon = "eye" }) local TravelTab = Window:CreateTab({ Name = "Travel", Icon = "map-pin" }) local InfoTab = Window:CreateTab({ Name = "Info", Icon = "info" }) --------------------------------------------------------------------- -- Farm tab --------------------------------------------------------------------- local farmLeft = FarmTab:CreateGroupbox({ Name = "Automation", Side = "Left" }) local farmRight = FarmTab:CreateGroupbox({ Name = "Session", Side = "Right" }) farmLeft:CreateToggle({ Name = "Auto coins", Desc = "Teleport onto every coin spawned in CoinSpawner", Flag = "AutoCoins", CurrentValue = false, Callback = function(value) State.options.autoCoins = value if value then saveAnchor() startCoinLoop() elseif State.options.anchorReturn then task.delay(0.5, returnToAnchor) end end, }) farmLeft:CreateSlider({ Name = "Coin delay", Desc = "Pause after each coin", Range = { 100, 1500 }, Increment = 50, CurrentValue = 350, Suffix = " ms", Flag = "CoinDelay", Callback = function(value) State.options.coinDelay = value / 1000 end, }) farmLeft:CreateToggle({ Name = "Return to saved spot", Desc = "After the farm stops, teleport back to where it started", Flag = "AnchorReturn", CurrentValue = true, Callback = function(value) State.options.anchorReturn = value end, }) farmLeft:CreateButton({ Name = "Save current spot", Style = "Primary", Callback = function() if saveAnchor() then Window:Notify({ Title = "Kitten Hub", Content = "Spot saved", Duration = 3 }) end end, }) farmLeft:CreateToggle({ Name = "Auto daily reward", Desc = "Touch the daily pad when its timer runs out", Flag = "AutoDaily", CurrentValue = false, Callback = function(value) State.options.autoDaily = value end, }) farmLeft:CreateToggle({ Name = "Auto join minigames", Desc = "Accept the invite and enter the minigame automatically", Flag = "AutoJoin", CurrentValue = false, Callback = function(value) State.options.autoJoin = value end, }) farmLeft:CreateToggle({ Name = "Anti-AFK", Desc = "Keeps the session alive while the script farms", Flag = "AntiAfk", CurrentValue = true, Callback = function(value) State.options.antiAfk = value end, }) farmRight:CreateStatus({ Name = "Gold", Style = "Row", Pin = true, Update = function() return "" .. formatNumber(gold()) .. "" end, }) farmRight:CreateStatus({ Name = "Collected", Style = "Row", Update = function() return formatNumber(State.stats.coins) end, }) farmRight:CreateStatus({ Name = "Coins/min", Style = "Row", Update = function() local elapsed = (os.clock() - State.stats.startedAt) / 60 if elapsed <= 0.05 then return "-" end return string.format("%.1f", State.stats.coins / elapsed) end, }) farmRight:CreateStatus({ Name = "Daily", Style = "Row", Update = function() local left = dailyReadyIn() if left <= 0 then return "ready" end return "" .. string.format("%.1f h", left / 3600) .. "" end, }) farmRight:CreateStatus({ Name = "Region", Style = "Row", Update = function() return tostring(region()) end, }) --------------------------------------------------------------------- -- Games tab --------------------------------------------------------------------- local gamesLeft = GamesTab:CreateGroupbox({ Name = "Bots", Side = "Left" }) local gamesRight = GamesTab:CreateGroupbox({ Name = "Results", Side = "Right" }) gamesLeft:CreateToggle({ Name = "Simon Says bot", Desc = "Answers ShowCommand with the right action (ignores 'do NOT' tricks)", Flag = "AutoSimon", CurrentValue = false, Callback = function(value) State.options.autoSimon = value end, }) gamesLeft:CreateToggle({ Name = "Math Race bot", Desc = "Reads the question and steps on the tile with the answer", Flag = "AutoMath", CurrentValue = false, Callback = function(value) State.options.autoMath = value end, }) gamesLeft:CreateToggle({ Name = "Auto tower reward", Desc = "Picks the first card and verifies it when the reward UI opens", Flag = "AutoTower", CurrentValue = false, Callback = function(value) State.options.autoTower = value end, }) gamesLeft:CreateButton({ Name = "Join last invited game", Style = "Primary", Callback = function() if State.lastJoinCfg then joinGame(State.lastJoinCfg) else Window:Notify({ Title = "Kitten Hub", Content = "No invite captured yet", Duration = 3 }) end end, }) gamesRight:CreateStatus({ Name = "Simon answers", Style = "Row", Update = function() return tostring(State.stats.simon) end, }) gamesRight:CreateStatus({ Name = "Math answers", Style = "Row", Update = function() return tostring(State.stats.math) end, }) gamesRight:CreateStatus({ Name = "Tower cards", Style = "Row", Update = function() return tostring(State.stats.tower) end, }) gamesRight:CreateStatus({ Name = "Last invite", Style = "Row", Update = function() local cfg = State.lastJoinCfg if not cfg then return "none" end return "" .. tostring(cfg.gameName) .. "" end, }) --------------------------------------------------------------------- -- Shop tab --------------------------------------------------------------------- local shopLeft = ShopTab:CreateGroupbox({ Name = "Kittens", Side = "Left" }) local shopRight = ShopTab:CreateGroupbox({ Name = "Rules", Side = "Right" }) shopLeft:CreateToggle({ Name = "Auto buy kittens", Desc = "Buys the cheapest unowned kitten that passes the rules", Flag = "AutoBuy", CurrentValue = false, Callback = function(value) State.options.autoBuy = value end, }) shopLeft:CreateToggle({ Name = "Dry run", Desc = "Only log what would be bought - nothing is spent", Flag = "DryRun", CurrentValue = true, Callback = function(value) State.options.dryRun = value end, }) shopLeft:CreateToggle({ Name = "Auto open mini chests", Desc = "Opens chests you already own (no gold spent)", Flag = "AutoChests", CurrentValue = false, Callback = function(value) State.options.autoChests = value end, }) shopLeft:CreateButton({ Name = "Buy cheapest now", Style = "Primary", Callback = function() local target = cheapestTarget() if not target then Window:Notify({ Title = "Kitten Hub", Content = "Nothing matches the rules", Duration = 3 }) return end if State.options.dryRun then Window:Notify({ Title = "Kitten Hub", Content = "Dry run: asking the server for " .. target.name, Duration = 4 }) requestKitten(target) return end if requestKitten(target) then Window:Notify({ Title = "Kitten Hub", Content = "Asked the server for " .. target.name, Duration = 4 }) end end, }) shopRight:CreateSlider({ Name = "Max price", Desc = "Never pay more than this for one kitten", Range = { 5, 2000 }, Increment = 5, CurrentValue = 50, Suffix = " gold", Flag = "MaxPrice", Callback = function(value) State.options.maxPrice = value end, }) shopRight:CreateSlider({ Name = "Reserve", Desc = "Always keep this much gold", Range = { 0, 1000 }, Increment = 5, CurrentValue = 10, Suffix = " gold", Flag = "Reserve", Callback = function(value) State.options.reserve = value end, }) shopRight:CreateToggle({ Name = "Free kittens", Flag = "CatFree", CurrentValue = true, Callback = function(value) State.options.categories.Free = value end, }) shopRight:CreateToggle({ Name = "Regular kittens", Flag = "CatRegular", CurrentValue = true, Callback = function(value) State.options.categories.Regular = value end, }) shopRight:CreateToggle({ Name = "Chunky kittens", Flag = "CatChunky", CurrentValue = false, Callback = function(value) State.options.categories.Chunky = value end, }) shopRight:CreateToggle({ Name = "Baby kittens", Flag = "CatBaby", CurrentValue = false, Callback = function(value) State.options.categories.Baby = value end, }) shopRight:CreateToggle({ Name = "Munchkin kittens", Flag = "CatMunchkin", CurrentValue = false, Callback = function(value) State.options.categories.Munchkin = value end, }) shopRight:CreateToggle({ Name = "Group kittens", Desc = "Needs a group membership", Flag = "CatGroup", CurrentValue = false, Callback = function(value) State.options.categories.Group = value end, }) shopRight:CreateStatus({ Name = "Catalog", Style = "Row", Update = function() return tostring(#Catalog) .. " kittens" end, }) shopRight:CreateStatus({ Name = "Next target", Style = "Row", Update = function() local target = cheapestTarget() if not target then return "none" end return target.name .. " (" .. target.category .. ")" end, }) shopRight:CreateStatus({ Name = "Bought", Style = "Row", Update = function() return tostring(State.stats.bought) .. " (" .. formatNumber(State.stats.spent) .. " gold)" end, }) --------------------------------------------------------------------- -- Visuals tab --------------------------------------------------------------------- local visLeft = VisualTab:CreateGroupbox({ Name = "ESP", Side = "Left" }) local visRight = VisualTab:CreateGroupbox({ Name = "Range", Side = "Right" }) visLeft:CreateToggle({ Name = "Coins", Flag = "EspCoins", CurrentValue = false, Callback = function(value) State.options.espCoins = value end, }) visLeft:CreateToggle({ Name = "Fish", Flag = "EspFish", CurrentValue = false, Callback = function(value) State.options.espFish = value end, }) visLeft:CreateToggle({ Name = "Crystals", Flag = "EspCrystals", CurrentValue = false, Callback = function(value) State.options.espCrystals = value end, }) visLeft:CreateToggle({ Name = "Chests", Flag = "EspChests", CurrentValue = false, Callback = function(value) State.options.espChests = value end, }) visLeft:CreateToggle({ Name = "Carried kittens", Desc = "Players that are being picked up right now", Flag = "EspPlayers", CurrentValue = false, Callback = function(value) State.options.espPlayers = value end, }) visRight:CreateSlider({ Name = "Max distance", Range = { 50, 2000 }, Increment = 25, CurrentValue = 400, Suffix = " studs", Flag = "EspRange", Callback = function(value) State.options.espRange = value end, }) visRight:CreateStatus({ Name = "Highlights", Style = "Row", Update = function() local count = 0 for _ in pairs(State.esp) do count = count + 1 end return tostring(count) end, }) visRight:CreateButton({ Name = "Clear ESP", Callback = function() clearEsp() end, }) --------------------------------------------------------------------- -- Travel tab --------------------------------------------------------------------- local travelLeft = TravelTab:CreateGroupbox({ Name = "Places", Side = "Left" }) local travelRight = TravelTab:CreateGroupbox({ Name = "Saved", Side = "Right" }) travelLeft:CreateButton({ Name = "Home", Style = "Primary", Callback = function() goHome() end, }) travelLeft:CreateButton({ Name = "Daily chest", Callback = function() local pad = dailyPad() if pad then teleportTo(pad.Position) end end, }) for _, name in ipairs({ "Emerald", "Sapphire", "Ruby", "Amethyst" }) do travelLeft:CreateButton({ Name = name .. " crystal", Callback = function() local folder = Workspace:FindFirstChild("Crystals") local part = folder and folder:FindFirstChild(name) if part then teleportTo(part.Position) else Window:Notify({ Title = "Kitten Hub", Content = name .. " is not in this server", Duration = 3 }) end end, }) end travelRight:CreateButton({ Name = "Return to saved spot", Style = "Primary", Callback = function() if not returnToAnchor() then Window:Notify({ Title = "Kitten Hub", Content = "No spot saved yet", Duration = 3 }) end end, }) travelRight:CreateStatus({ Name = "Saved spot", Style = "Row", Update = function() if not State.anchor then return "none" end return string.format("%.0f, %.0f, %.0f", State.anchor.X, State.anchor.Y, State.anchor.Z) end, }) --------------------------------------------------------------------- -- Info tab --------------------------------------------------------------------- local infoLeft = InfoTab:CreateGroupbox({ Name = "Progress", Side = "Left" }) local infoRight = InfoTab:CreateGroupbox({ Name = "About", Side = "Right" }) infoLeft:CreateStatus({ Name = "Level", Style = "Row", Pin = true, Update = function() return tostring(stat("Level", 0)) end, }) infoLeft:CreateStatus({ Name = "Exp", Style = "Row", Update = function() return formatNumber(stat("Exp", 0)) end, }) infoLeft:CreateStatus({ Name = "Mini chests", Style = "Row", Update = function() return tostring(stat("MiniChests", 0)) end, }) infoLeft:CreateStatus({ Name = "Fish", Style = "Row", Update = function() return tostring(stat("Fish", 0)) end, }) infoLeft:CreateStatus({ Name = "Catalog", Style = "Row", Update = function() return tostring(#Catalog) end, }) infoRight:CreateParagraph({ Name = "Build", Content = "v0.3 - coins, daily, minigame bots (Simon Says, Math Race), tower cards, kitten shop with guards, mini chests, ESP. RightControl toggles the window.", }) infoRight:CreateButton({ Name = "Destroy hub", Style = "Danger", Callback = function() if getgenv().KittenHub and type(getgenv().KittenHub.destroy) == "function" then getgenv().KittenHub.destroy() end end, }) --------------------------------------------------------------------- -- Public API --------------------------------------------------------------------- local api = {} function api.collectOnce() local coin = nearestCoin() return coin ~= nil and collectCoin(coin) end function api.claimDaily() return claimDaily() end function api.saveAnchor() return saveAnchor() end function api.anchor() return State.anchor end function api.joinLast() return joinGame(State.lastJoinCfg) end function api.option(name, value) if value == nil then return State.options[name] end State.options[name] = value if name == "autoCoins" and value then saveAnchor() startCoinLoop() end return value end function api.testSimon(command, notFlag) simonAnswer(command, notFlag) end function api.forceSimon(command) local action = cleanAction(command) if not (SIMON_ACTIONS[action] and SelectAction) then return false end SelectAction:FireServer(action) State.stats.simon = State.stats.simon + 1 return true end function api.testMath(question) mathAnswer(question) end function api.catalog() return Catalog end function api.cheapest() return cheapestTarget() end function api.buyKitten(id) for _, kitten in ipairs(Catalog) do if kitten.id == tostring(id) then return requestKitten(kitten) end end return false end function api.stats() return { gold = gold(), coins = State.stats.coins, claims = State.stats.claims, joins = State.stats.joins, simon = State.stats.simon, math = State.stats.math, tower = State.stats.tower, bought = State.stats.bought, chests = State.stats.chests, spent = State.stats.spent, esp = (function() local n = 0 for _ in pairs(State.esp) do n = n + 1 end return n end)(), anchor = State.anchor, lastGame = State.lastJoinCfg and State.lastJoinCfg.gameName or nil, } end function api.destroy() State.running = false State.options.autoCoins = false State.options.autoBuy = false clearEsp() disconnectAll() if getgenv().__kittenHubConnections == Connections then getgenv().__kittenHubConnections = nil end pcall(function() Window:Destroy() end) if getgenv().KittenHub and getgenv().KittenHub.window == Window then getgenv().KittenHub = nil end end api.window = Window getgenv().KittenHub = api logger("loaded v0.3: gold=" .. tostring(gold()) .. ", coins=" .. tostring(#liveCoins()) .. ", catalog=" .. tostring(#Catalog))