--[[
=====================================================================
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))