Tutorial · Game systems

How to make a game like Steal a Brainrot in Roblox Studio

Steal a Brainrot made a simple loop famous: characters ride past on a conveyor, you buy them, they earn money in your base, and other players sneak in to take them while you race to lock your door. This guide builds that whole loop in two server scripts, with your own unit names and plain blocks you can swap for your own models. It is not the real game's code or art, and it uses nothing from it.

Updated 25 October 20269 min readIntermediateLuau

In short

A "Steal a" game is five systems on the server: a conveyor that spawns units with a ProximityPrompt to buy them, a base per player with slots, an income loop that pays each owner every second, a steal prompt with a hold time on every unit in someone else's base, and a lock that makes the base entrance solid for everyone but its owner. Save cash and the unit on each slot in a data store.

  • Do every check on the server: who owns the base, whether it is locked, whether the buyer has the cash. Prompts only ask.
  • A collision group per base lets its owner walk through their own barrier while it blocks everyone else.
  • A stolen unit floats over the thief until they reach their own Home pad. If they die first, it goes back to its owner.

Get the script

Brainrot game script: conveyor, bases, buying and saving

One server Script runs the whole loop: units on a conveyor to buy, cash per second, other players taking unlocked units, base locks and saving.

Script in ServerScriptService named StealGame
-- Script in ServerScriptService named StealGame.
-- The whole loop on the server: conveyor, buying, income, stealing, locks and saving.
local DataStoreService = game:GetService("DataStoreService")
local PhysicsService = game:GetService("PhysicsService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

type UnitInfo = { name: string, price: number, income: number, weight: number, color: Color3 }

-- Your units: price to buy, cash per second once owned, and how often they show up
local UNITS: { UnitInfo } = {
	{ name = "Pebble Pal", price = 25, income = 1, weight = 50, color = Color3.fromRGB(160, 160, 170) },
	{ name = "Toast Buddy", price = 150, income = 5, weight = 30, color = Color3.fromRGB(230, 170, 90) },
	{ name = "Cactus Chef", price = 900, income = 25, weight = 14, color = Color3.fromRGB(90, 190, 100) },
	{ name = "Disco Moon", price = 6000, income = 120, weight = 5, color = Color3.fromRGB(150, 110, 255) },
	{ name = "Golden Noodle", price = 40000, income = 700, weight = 1, color = Color3.fromRGB(255, 210, 60) },
}
local START_CASH = 100
local SPAWN_EVERY = 2.5 -- seconds between units on the conveyor
local BELT_SPEED = 8 -- studs per second
local STEAL_HOLD = 1.5 -- seconds a thief holds the prompt
local LOCK_SECONDS = 30
local STORE = DataStoreService:GetDataStore("StealGame_v1")

local bases = workspace:WaitForChild("Bases") :: Folder
local conveyor = workspace:WaitForChild("Conveyor") :: Folder
local beltStart = conveyor:WaitForChild("Start") :: BasePart
local beltEnd = conveyor:WaitForChild("End") :: BasePart
local templates = ReplicatedStorage:FindFirstChild("Units") -- optional models named like the units

type PlayerState = {
	base: Model,
	slots: { [number]: string }, -- slot number -> unit name
	lockedUntil: number,
	carrying: Model?, -- a unit this player stole and has not brought home yet
	carriedFrom: Player?,
	carriedName: string?,
}
local states: { [Player]: PlayerState } = {}

local function findUnit(name: string): UnitInfo?
	for _, info in UNITS do
		if info.name == name then
			return info
		end
	end
	return nil
end

local function slotPart(base: Model, index: number): BasePart?
	local slots = base:FindFirstChild("Slots")
	local slot = slots and slots:FindFirstChild("Slot" .. index)
	return if slot and slot:IsA("BasePart") then slot else nil
end

local function slotCount(base: Model): number
	local slots = base:FindFirstChild("Slots")
	return if slots then #slots:GetChildren() else 0
end

local function freeSlot(state: PlayerState): number?
	for i = 1, slotCount(state.base) do
		if state.slots[i] == nil then
			return i
		end
	end
	return nil
end

local function cashOf(player: Player): IntValue?
	local stats = player:FindFirstChild("leaderstats")
	local cash = stats and stats:FindFirstChild("Cash")
	return if cash and cash:IsA("IntValue") then cash else nil
end

-- A unit's model: your template from ReplicatedStorage.Units, or a coloured block
local function makeModel(info: UnitInfo): Model
	local template = templates and templates:FindFirstChild(info.name)
	if template and template:IsA("Model") then
		local copy = template:Clone()
		for _, d in copy:GetDescendants() do
			if d:IsA("BasePart") then
				d.Anchored = true
				d.CanCollide = false
			end
		end
		return copy
	end
	local model = Instance.new("Model")
	model.Name = info.name
	local body = Instance.new("Part")
	body.Name = "Body"
	body.Size = Vector3.new(3, 3, 3)
	body.Color = info.color
	body.Anchored = true
	body.CanCollide = false
	body.Parent = model
	model.PrimaryPart = body
	return model
end

local function addPrompt(model: Model, action: string, object: string, hold: number): ProximityPrompt
	local prompt = Instance.new("ProximityPrompt")
	prompt.ActionText = action
	prompt.ObjectText = object
	prompt.HoldDuration = hold
	prompt.MaxActivationDistance = 10
	prompt.RequiresLineOfSight = false
	prompt.Parent = model.PrimaryPart or model:FindFirstChildWhichIsA("BasePart")
	return prompt
end

local function isLocked(state: PlayerState): boolean
	return os.clock() < state.lockedUntil
end

local placeUnit: (owner: Player, index: number, name: string) -> ()

-- A thief picks up a unit from someone else's base
local function startSteal(thief: Player, owner: Player, index: number, display: Model)
	local thiefState, ownerState = states[thief], states[owner]
	if not thiefState or not ownerState or thief == owner then
		return
	end
	local name = ownerState.slots[index]
	if not name or isLocked(ownerState) or thiefState.carrying then
		return
	end
	ownerState.slots[index] = nil
	display:Destroy()
	local info = findUnit(name)
	if not info then
		return
	end
	local carried = makeModel(info)
	carried.Parent = workspace
	thiefState.carrying = carried
	thiefState.carriedFrom = owner
	thiefState.carriedName = name
end

-- Puts a unit on a base's slot, with a Steal prompt for everyone but the owner
placeUnit = function(owner: Player, index: number, name: string)
	local state = states[owner]
	local info = findUnit(name)
	local slot = state and slotPart(state.base, index)
	if not state or not info or not slot then
		return
	end
	state.slots[index] = name
	local model = makeModel(info)
	model:SetAttribute("Slot", index)
	model:PivotTo(slot.CFrame * CFrame.new(0, 2.5, 0))
	model.Parent = slot
	local prompt = addPrompt(model, "Steal", name, STEAL_HOLD)
	prompt.Enabled = not isLocked(state)
	prompt.Triggered:Connect(function(thief: Player)
		startSteal(thief, owner, index, model)
	end)
end

-- The thief is gone or died: the unit goes back to its owner if there is room
local function dropCarried(thief: Player)
	local state = states[thief]
	if not state or not state.carrying then
		return
	end
	state.carrying:Destroy()
	local owner, name = state.carriedFrom, state.carriedName
	state.carrying, state.carriedFrom, state.carriedName = nil, nil, nil
	local ownerState = owner and states[owner]
	if owner and ownerState and name then
		local index = freeSlot(ownerState)
		if index then
			placeUnit(owner, index, name)
		end
	end
end

-- The thief reached their own Home pad with the unit
local function deliver(thief: Player)
	local state = states[thief]
	if not state or not state.carrying or not state.carriedName then
		return
	end
	local index = freeSlot(state)
	if not index then
		return -- base full: they keep carrying it until a slot frees up
	end
	local name = state.carriedName
	state.carrying:Destroy()
	state.carrying, state.carriedFrom, state.carriedName = nil, nil, nil
	placeUnit(thief, index, name)
end

local function setLock(state: PlayerState, locked: boolean)
	local barrier = state.base:FindFirstChild("Barrier")
	if barrier and barrier:IsA("BasePart") then
		barrier.CanCollide = locked
		barrier.Transparency = if locked then 0.4 else 1
	end
	-- Steal prompts are off while the base is locked (the server checks again anyway)
	for _, d in state.base:GetDescendants() do
		if d:IsA("ProximityPrompt") and d.ActionText == "Steal" then
			d.Enabled = not locked
		end
	end
end

-- Each base gets two collision groups: its barrier never blocks its owner
local function ownerGroup(base: Model): string
	return base.Name .. "_Owner"
end

-- Wire up each base's Home pad, barrier and lock button once
for _, base in bases:GetChildren() do
	if not base:IsA("Model") then
		continue
	end
	local barrierGroup = base.Name .. "_Barrier"
	PhysicsService:RegisterCollisionGroup(barrierGroup)
	PhysicsService:RegisterCollisionGroup(ownerGroup(base))
	PhysicsService:CollisionGroupSetCollidable(barrierGroup, ownerGroup(base), false)
	local barrierPart = base:FindFirstChild("Barrier")
	if barrierPart and barrierPart:IsA("BasePart") then
		barrierPart.CollisionGroup = barrierGroup
	end
	local home = base:FindFirstChild("Home")
	if home and home:IsA("BasePart") then
		home.Touched:Connect(function(hit: BasePart)
			local character = hit.Parent
			local player = if character and character:IsA("Model") then Players:GetPlayerFromCharacter(character) else nil
			local state = player and states[player]
			if player and state and state.base == base then
				deliver(player)
			end
		end)
	end
	local button = base:FindFirstChild("LockButton")
	if button and button:IsA("BasePart") then
		local prompt = Instance.new("ProximityPrompt")
		prompt.ActionText = "Lock base"
		prompt.ObjectText = LOCK_SECONDS .. " seconds"
		prompt.RequiresLineOfSight = false
		prompt.Parent = button
		prompt.Triggered:Connect(function(player: Player)
			local state = states[player]
			if not state or state.base ~= base or isLocked(state) then
				return -- only the owner, and not while already locked
			end
			state.lockedUntil = os.clock() + LOCK_SECONDS
			setLock(state, true)
			task.delay(LOCK_SECONDS, function()
				if states[player] == state then
					setLock(state, false)
				end
			end)
		end)
	end
end

-- Saving: cash and the unit on each slot
local function save(player: Player)
	local state, cash = states[player], cashOf(player)
	if not state or not cash then
		return
	end
	local units: { string } = {}
	for i = 1, slotCount(state.base) do
		units[i] = state.slots[i] or ""
	end
	local ok, err = pcall(function()
		STORE:SetAsync("player_" .. player.UserId, { cash = cash.Value, units = units })
	end)
	if not ok then
		warn("StealGame save failed for", player.Name, err)
	end
end

local function onPlayerAdded(player: Player)
	local base: Model? = nil
	for _, b in bases:GetChildren() do
		if b:IsA("Model") and b:GetAttribute("OwnerId") == nil then
			base = b
			break
		end
	end
	if not base then
		player:Kick("This server is full. Please join another server.")
		return
	end
	base:SetAttribute("OwnerId", player.UserId)
	base:SetAttribute("OwnerName", player.DisplayName)

	local stats = Instance.new("Folder")
	stats.Name = "leaderstats"
	local cash = Instance.new("IntValue")
	cash.Name = "Cash"
	cash.Value = START_CASH
	cash.Parent = stats
	stats.Parent = player

	local state: PlayerState = { base = base, slots = {}, lockedUntil = 0 }
	states[player] = state

	local ok, data = pcall(function()
		return STORE:GetAsync("player_" .. player.UserId)
	end)
	if ok and type(data) == "table" then
		cash.Value = tonumber(data.cash) or START_CASH
		if type(data.units) == "table" then
			for i, name in data.units do
				if type(i) == "number" and type(name) == "string" and findUnit(name) then
					placeUnit(player, i, name)
				end
			end
		end
	end

	local function onCharacter(character: Model)
		-- the owner walks through their own barrier
		local function joinGroup(d: Instance)
			if d:IsA("BasePart") then
				d.CollisionGroup = ownerGroup(base)
			end
		end
		for _, d in character:GetDescendants() do
			joinGroup(d)
		end
		character.DescendantAdded:Connect(joinGroup)
		local home = base:FindFirstChild("Home")
		if home and home:IsA("BasePart") then
			task.defer(function()
				character:PivotTo(home.CFrame * CFrame.new(0, 4, 0))
			end)
		end
		local humanoid = character:WaitForChild("Humanoid") :: Humanoid
		humanoid.Died:Connect(function()
			dropCarried(player)
		end)
	end
	player.CharacterAdded:Connect(onCharacter)
	if player.Character then
		onCharacter(player.Character)
	end
end

Players.PlayerAdded:Connect(onPlayerAdded)
for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end

Players.PlayerRemoving:Connect(function(player: Player)
	dropCarried(player)
	save(player)
	local state = states[player]
	if state then
		for _, d in state.base:GetDescendants() do
			if d:IsA("Model") and d:GetAttribute("Slot") then
				d:Destroy()
			end
		end
		setLock(state, false)
		state.base:SetAttribute("OwnerId", nil)
		state.base:SetAttribute("OwnerName", nil)
	end
	-- anyone carrying this player's units keeps them
	states[player] = nil
end)

game:BindToClose(function()
	for _, player in Players:GetPlayers() do
		save(player)
	end
end)

-- Income: every owned unit pays its owner once a second
task.spawn(function()
	while true do
		task.wait(1)
		for player, state in states do
			local cash = cashOf(player)
			if cash then
				local total = 0
				for _, name in state.slots do
					local info = findUnit(name)
					total += if info then info.income else 0
				end
				cash.Value += total
			end
		end
	end
end)

-- The conveyor: a random unit every SPAWN_EVERY seconds, rarer ones less often
local totalWeight = 0
for _, info in UNITS do
	totalWeight += info.weight
end

local function pickUnit(): UnitInfo
	local roll = math.random() * totalWeight
	for _, info in UNITS do
		roll -= info.weight
		if roll <= 0 then
			return info
		end
	end
	return UNITS[1]
end

type Rider = { model: Model, info: UnitInfo, distance: number }
local riders: { Rider } = {}
local beltLength = (beltEnd.Position - beltStart.Position).Magnitude
local beltDirection = (beltEnd.Position - beltStart.Position).Unit

local function spawnRider()
	local info = pickUnit()
	local model = makeModel(info)
	model:PivotTo(CFrame.new(beltStart.Position + Vector3.new(0, 2.5, 0)))
	model.Parent = conveyor
	local rider: Rider = { model = model, info = info, distance = 0 }
	table.insert(riders, rider)
	local prompt = addPrompt(model, "Buy", info.name .. " ($" .. info.price .. ")", 0)
	prompt.Triggered:Connect(function(player: Player)
		local state, cash = states[player], cashOf(player)
		if not state or not cash or model.Parent ~= conveyor then
			return -- someone else bought it first
		end
		local index = freeSlot(state)
		if not index or cash.Value < info.price then
			return
		end
		cash.Value -= info.price
		model:Destroy()
		placeUnit(player, index, info.name)
	end)
end

task.spawn(function()
	while true do
		spawnRider()
		task.wait(SPAWN_EVERY)
	end
end)

RunService.Heartbeat:Connect(function(dt: number)
	-- move the conveyor units, and remove the ones that reached the end or were bought
	for i = #riders, 1, -1 do
		local rider = riders[i]
		rider.distance += BELT_SPEED * dt
		if rider.model.Parent ~= conveyor or rider.distance >= beltLength then
			rider.model:Destroy()
			table.remove(riders, i)
		else
			rider.model:PivotTo(CFrame.new(beltStart.Position + beltDirection * rider.distance + Vector3.new(0, 2.5, 0)))
		end
	end
	-- carried units float over the thief's head
	for player, state in states do
		local carried = state.carrying
		local character = player.Character
		local head = character and character:FindFirstChild("Head")
		if carried and head and head:IsA("BasePart") then
			carried:PivotTo(head.CFrame * CFrame.new(0, 3.5, 0))
		end
	end
end)

RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts

Where to put it

Explorer: the map names the script looks for
Workspace
  Conveyor Folder
    Start Part
    End Part
  Bases Folder
    Base1 Model
      Slots Folder
      Home Part
      Barrier Part
      LockButton Part
ServerScriptService
  BuildStealMap Script new
  StealGame Script new

Settings you can change

START_CASHIn the script
Cash a new player starts with: enough for a few cheap units. Default: 100.
LOCK_SECONDSIn the script
How long a base stays locked after its owner presses the lock button. Default: 30.
STEAL_HOLDIn the script
Seconds a thief must hold the prompt. Longer gives owners time to react. Default: 1.5.

No map yet? Add the BuildStealMap script from Step 1 too: it makes a conveyor and six bases with the right names. Turn on Enable Studio Access to API Services (File, then Experience Settings, then Security) to test saving in Studio.

What a cheater can and cannot doBuying, income, stealing and locks all run on the server, so a cheater cannot give themselves cash or take a unit from a locked base. They can still walk fast: see Step 4 for defending.

Type-checked with luau-lsp strict against Roblox's API definitions on 25 October 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.

Prompt for RoCode

Make a Steal a Brainrot style game in my place with my own unit names: a conveyor that spawns units with Buy prompts, a base with 8 slots for each player, cash per second from owned units, a hold prompt to take units from unlocked bases, a lock button that makes the base entrance solid for everyone but the owner for 30 seconds, and saving.

On this page
  1. Get the script
  2. How it works
  3. 1. The map
  4. 2. The game script
  5. 3. Buying and income
  6. 4. Stealing and locks
  7. 5. Saving
  8. 6. Make it yours
  9. Common errors
  10. With RoCode
  11. Questions
  12. Sources

How the loop works

Every "Steal a" game runs the same loop, and each part feeds the next. Buying gives you income, income buys rarer units, rarer units make you a target, and the lock is your few seconds of safety.

The pieces and what each one does
PieceWhat players seeWhat the server does
ConveyorUnits slide past with a priceSpawns a random unit every few seconds, rarer ones less often, and removes it at the end
BuyingPress E on a unitChecks cash and a free slot, takes the price, puts the unit in the buyer's base
IncomeCash going upAdds every owned unit's income to its owner once a second
StealingHold E on someone else's unit, run homeTakes the unit off the owner's slot, floats it over the thief, gives it to them at their Home pad
LockA glowing wall across the doorMakes the barrier solid for everyone but the owner and turns steal prompts off
SavingYour base is back next timeStores cash and the unit on each slot in a data store

Step 1: Build the map

The game script looks for a few names in Workspace. Build them yourself, or let the script below build a simple test map: a conveyor down the middle and six walled bases on both sides, each with eight slots, a yellow Home pad, a red lock button and an invisible barrier across its entrance.

The names the game script needs
Workspace
  Conveyor Folder
    Belt Part
    Start Part        units appear here
    End Part          and disappear here
  Bases Folder
    Base1 Model       one per player in a server
      Floor Part
      Home Part       spawn point, and where stolen units are delivered
      Barrier Part    the entrance, solid only while locked
      LockButton Part
      Slots Folder
        Slot1 Part ... Slot8 Part
  1. Insert a Script in ServerScriptService, name it BuildStealMap and paste the code below.
  2. Press Play once to check the map appears, then stop. It builds at the start of every server, so your own map can replace it later: keep the same names and delete this script.
Script in ServerScriptService named BuildStealMap
-- Script in ServerScriptService named BuildStealMap.
-- Makes a simple test map once: a conveyor down the middle and bases on both sides.
-- Delete this script when you build your own map with the same names.
local BASE_COUNT = 6 -- bases, half on each side of the conveyor
local SLOTS_PER_BASE = 8

if workspace:FindFirstChild("Bases") then
	return -- the map is already there
end

local function part(name: string, size: Vector3, cframe: CFrame, color: Color3, parent: Instance): Part
	local p = Instance.new("Part")
	p.Name = name
	p.Size = size
	p.CFrame = cframe
	p.Color = color
	p.Anchored = true
	p.TopSurface = Enum.SurfaceType.Smooth
	p.Parent = parent
	return p
end

-- The conveyor: units ride from Start to End
local conveyor = Instance.new("Folder")
conveyor.Name = "Conveyor"
conveyor.Parent = workspace
local length = 40 * (BASE_COUNT / 2) + 40
part("Belt", Vector3.new(10, 1, length), CFrame.new(0, 0.5, 0), Color3.fromRGB(45, 45, 55), conveyor)
local startPart = part("Start", Vector3.new(10, 1, 2), CFrame.new(0, 1.5, -length / 2), Color3.fromRGB(80, 200, 120), conveyor)
local endPart = part("End", Vector3.new(10, 1, 2), CFrame.new(0, 1.5, length / 2), Color3.fromRGB(220, 80, 80), conveyor)
startPart.CanCollide = false
endPart.CanCollide = false

local bases = Instance.new("Folder")
bases.Name = "Bases"
bases.Parent = workspace

for i = 1, BASE_COUNT do
	local side = if i % 2 == 1 then -1 else 1
	local row = math.ceil(i / 2) - 1
	local z = -length / 2 + 40 + row * 40
	local origin = CFrame.new(side * 40, 0, z)

	local base = Instance.new("Model")
	base.Name = "Base" .. i
	base.Parent = bases
	local floor = part("Floor", Vector3.new(36, 1, 32), origin * CFrame.new(0, 0.5, 0), Color3.fromRGB(70, 90, 140), base)
	base.PrimaryPart = floor

	-- Home: where the owner spawns and where stolen units are delivered
	local home = part("Home", Vector3.new(8, 1, 8), origin * CFrame.new(side * 12, 1.1, 0), Color3.fromRGB(240, 200, 60), base)
	home.CanCollide = false

	-- Barrier: the entrance on the conveyor side, solid only while the base is locked
	local barrier = part("Barrier", Vector3.new(1, 10, 32), origin * CFrame.new(-side * 18, 5.5, 0), Color3.fromRGB(120, 180, 255), base)
	barrier.Transparency = 1
	barrier.CanCollide = false

	-- Walls on the other three sides, so the entrance is the only way in
	local wallColor = Color3.fromRGB(55, 70, 110)
	part("BackWall", Vector3.new(1, 10, 32), origin * CFrame.new(side * 18, 5.5, 0), wallColor, base)
	part("SideWallA", Vector3.new(36, 10, 1), origin * CFrame.new(0, 5.5, -16), wallColor, base)
	part("SideWallB", Vector3.new(36, 10, 1), origin * CFrame.new(0, 5.5, 16), wallColor, base)

	local lockButton = part("LockButton", Vector3.new(4, 1, 4), origin * CFrame.new(side * 12, 1.1, 10), Color3.fromRGB(255, 90, 90), base)
	lockButton.CanCollide = false

	local slots = Instance.new("Folder")
	slots.Name = "Slots"
	slots.Parent = base
	for s = 1, SLOTS_PER_BASE do
		local col = (s - 1) % 4
		local rowS = math.floor((s - 1) / 4)
		local slot = part("Slot" .. s, Vector3.new(5, 1, 5), origin * CFrame.new(-side * (2 + rowS * 7), 1.1, -12 + col * 8), Color3.fromRGB(200, 200, 210), slots)
		slot.CanCollide = false
	end
end

A server holds as many players as you have bases. Six bases suits a server of six: set Max Players to match in the experience settings when you publish.

Step 2: Add the game script

  1. Insert a second Script in ServerScriptService named StealGame and paste the code below.
  2. Edit the UNITS table: each unit has a name, a price, its income per second, a weight (how often it rides the conveyor) and a colour for its placeholder block.
  3. Press Play. You spawn on your Home pad with 100 Cash. Walk to the conveyor and press E on a unit you can afford.
Script in ServerScriptService named StealGame
-- Script in ServerScriptService named StealGame.
-- The whole loop on the server: conveyor, buying, income, stealing, locks and saving.
local DataStoreService = game:GetService("DataStoreService")
local PhysicsService = game:GetService("PhysicsService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")

type UnitInfo = { name: string, price: number, income: number, weight: number, color: Color3 }

-- Your units: price to buy, cash per second once owned, and how often they show up
local UNITS: { UnitInfo } = {
	{ name = "Pebble Pal", price = 25, income = 1, weight = 50, color = Color3.fromRGB(160, 160, 170) },
	{ name = "Toast Buddy", price = 150, income = 5, weight = 30, color = Color3.fromRGB(230, 170, 90) },
	{ name = "Cactus Chef", price = 900, income = 25, weight = 14, color = Color3.fromRGB(90, 190, 100) },
	{ name = "Disco Moon", price = 6000, income = 120, weight = 5, color = Color3.fromRGB(150, 110, 255) },
	{ name = "Golden Noodle", price = 40000, income = 700, weight = 1, color = Color3.fromRGB(255, 210, 60) },
}
local START_CASH = 100
local SPAWN_EVERY = 2.5 -- seconds between units on the conveyor
local BELT_SPEED = 8 -- studs per second
local STEAL_HOLD = 1.5 -- seconds a thief holds the prompt
local LOCK_SECONDS = 30
local STORE = DataStoreService:GetDataStore("StealGame_v1")

local bases = workspace:WaitForChild("Bases") :: Folder
local conveyor = workspace:WaitForChild("Conveyor") :: Folder
local beltStart = conveyor:WaitForChild("Start") :: BasePart
local beltEnd = conveyor:WaitForChild("End") :: BasePart
local templates = ReplicatedStorage:FindFirstChild("Units") -- optional models named like the units

type PlayerState = {
	base: Model,
	slots: { [number]: string }, -- slot number -> unit name
	lockedUntil: number,
	carrying: Model?, -- a unit this player stole and has not brought home yet
	carriedFrom: Player?,
	carriedName: string?,
}
local states: { [Player]: PlayerState } = {}

local function findUnit(name: string): UnitInfo?
	for _, info in UNITS do
		if info.name == name then
			return info
		end
	end
	return nil
end

local function slotPart(base: Model, index: number): BasePart?
	local slots = base:FindFirstChild("Slots")
	local slot = slots and slots:FindFirstChild("Slot" .. index)
	return if slot and slot:IsA("BasePart") then slot else nil
end

local function slotCount(base: Model): number
	local slots = base:FindFirstChild("Slots")
	return if slots then #slots:GetChildren() else 0
end

local function freeSlot(state: PlayerState): number?
	for i = 1, slotCount(state.base) do
		if state.slots[i] == nil then
			return i
		end
	end
	return nil
end

local function cashOf(player: Player): IntValue?
	local stats = player:FindFirstChild("leaderstats")
	local cash = stats and stats:FindFirstChild("Cash")
	return if cash and cash:IsA("IntValue") then cash else nil
end

-- A unit's model: your template from ReplicatedStorage.Units, or a coloured block
local function makeModel(info: UnitInfo): Model
	local template = templates and templates:FindFirstChild(info.name)
	if template and template:IsA("Model") then
		local copy = template:Clone()
		for _, d in copy:GetDescendants() do
			if d:IsA("BasePart") then
				d.Anchored = true
				d.CanCollide = false
			end
		end
		return copy
	end
	local model = Instance.new("Model")
	model.Name = info.name
	local body = Instance.new("Part")
	body.Name = "Body"
	body.Size = Vector3.new(3, 3, 3)
	body.Color = info.color
	body.Anchored = true
	body.CanCollide = false
	body.Parent = model
	model.PrimaryPart = body
	return model
end

local function addPrompt(model: Model, action: string, object: string, hold: number): ProximityPrompt
	local prompt = Instance.new("ProximityPrompt")
	prompt.ActionText = action
	prompt.ObjectText = object
	prompt.HoldDuration = hold
	prompt.MaxActivationDistance = 10
	prompt.RequiresLineOfSight = false
	prompt.Parent = model.PrimaryPart or model:FindFirstChildWhichIsA("BasePart")
	return prompt
end

local function isLocked(state: PlayerState): boolean
	return os.clock() < state.lockedUntil
end

local placeUnit: (owner: Player, index: number, name: string) -> ()

-- A thief picks up a unit from someone else's base
local function startSteal(thief: Player, owner: Player, index: number, display: Model)
	local thiefState, ownerState = states[thief], states[owner]
	if not thiefState or not ownerState or thief == owner then
		return
	end
	local name = ownerState.slots[index]
	if not name or isLocked(ownerState) or thiefState.carrying then
		return
	end
	ownerState.slots[index] = nil
	display:Destroy()
	local info = findUnit(name)
	if not info then
		return
	end
	local carried = makeModel(info)
	carried.Parent = workspace
	thiefState.carrying = carried
	thiefState.carriedFrom = owner
	thiefState.carriedName = name
end

-- Puts a unit on a base's slot, with a Steal prompt for everyone but the owner
placeUnit = function(owner: Player, index: number, name: string)
	local state = states[owner]
	local info = findUnit(name)
	local slot = state and slotPart(state.base, index)
	if not state or not info or not slot then
		return
	end
	state.slots[index] = name
	local model = makeModel(info)
	model:SetAttribute("Slot", index)
	model:PivotTo(slot.CFrame * CFrame.new(0, 2.5, 0))
	model.Parent = slot
	local prompt = addPrompt(model, "Steal", name, STEAL_HOLD)
	prompt.Enabled = not isLocked(state)
	prompt.Triggered:Connect(function(thief: Player)
		startSteal(thief, owner, index, model)
	end)
end

-- The thief is gone or died: the unit goes back to its owner if there is room
local function dropCarried(thief: Player)
	local state = states[thief]
	if not state or not state.carrying then
		return
	end
	state.carrying:Destroy()
	local owner, name = state.carriedFrom, state.carriedName
	state.carrying, state.carriedFrom, state.carriedName = nil, nil, nil
	local ownerState = owner and states[owner]
	if owner and ownerState and name then
		local index = freeSlot(ownerState)
		if index then
			placeUnit(owner, index, name)
		end
	end
end

-- The thief reached their own Home pad with the unit
local function deliver(thief: Player)
	local state = states[thief]
	if not state or not state.carrying or not state.carriedName then
		return
	end
	local index = freeSlot(state)
	if not index then
		return -- base full: they keep carrying it until a slot frees up
	end
	local name = state.carriedName
	state.carrying:Destroy()
	state.carrying, state.carriedFrom, state.carriedName = nil, nil, nil
	placeUnit(thief, index, name)
end

local function setLock(state: PlayerState, locked: boolean)
	local barrier = state.base:FindFirstChild("Barrier")
	if barrier and barrier:IsA("BasePart") then
		barrier.CanCollide = locked
		barrier.Transparency = if locked then 0.4 else 1
	end
	-- Steal prompts are off while the base is locked (the server checks again anyway)
	for _, d in state.base:GetDescendants() do
		if d:IsA("ProximityPrompt") and d.ActionText == "Steal" then
			d.Enabled = not locked
		end
	end
end

-- Each base gets two collision groups: its barrier never blocks its owner
local function ownerGroup(base: Model): string
	return base.Name .. "_Owner"
end

-- Wire up each base's Home pad, barrier and lock button once
for _, base in bases:GetChildren() do
	if not base:IsA("Model") then
		continue
	end
	local barrierGroup = base.Name .. "_Barrier"
	PhysicsService:RegisterCollisionGroup(barrierGroup)
	PhysicsService:RegisterCollisionGroup(ownerGroup(base))
	PhysicsService:CollisionGroupSetCollidable(barrierGroup, ownerGroup(base), false)
	local barrierPart = base:FindFirstChild("Barrier")
	if barrierPart and barrierPart:IsA("BasePart") then
		barrierPart.CollisionGroup = barrierGroup
	end
	local home = base:FindFirstChild("Home")
	if home and home:IsA("BasePart") then
		home.Touched:Connect(function(hit: BasePart)
			local character = hit.Parent
			local player = if character and character:IsA("Model") then Players:GetPlayerFromCharacter(character) else nil
			local state = player and states[player]
			if player and state and state.base == base then
				deliver(player)
			end
		end)
	end
	local button = base:FindFirstChild("LockButton")
	if button and button:IsA("BasePart") then
		local prompt = Instance.new("ProximityPrompt")
		prompt.ActionText = "Lock base"
		prompt.ObjectText = LOCK_SECONDS .. " seconds"
		prompt.RequiresLineOfSight = false
		prompt.Parent = button
		prompt.Triggered:Connect(function(player: Player)
			local state = states[player]
			if not state or state.base ~= base or isLocked(state) then
				return -- only the owner, and not while already locked
			end
			state.lockedUntil = os.clock() + LOCK_SECONDS
			setLock(state, true)
			task.delay(LOCK_SECONDS, function()
				if states[player] == state then
					setLock(state, false)
				end
			end)
		end)
	end
end

-- Saving: cash and the unit on each slot
local function save(player: Player)
	local state, cash = states[player], cashOf(player)
	if not state or not cash then
		return
	end
	local units: { string } = {}
	for i = 1, slotCount(state.base) do
		units[i] = state.slots[i] or ""
	end
	local ok, err = pcall(function()
		STORE:SetAsync("player_" .. player.UserId, { cash = cash.Value, units = units })
	end)
	if not ok then
		warn("StealGame save failed for", player.Name, err)
	end
end

local function onPlayerAdded(player: Player)
	local base: Model? = nil
	for _, b in bases:GetChildren() do
		if b:IsA("Model") and b:GetAttribute("OwnerId") == nil then
			base = b
			break
		end
	end
	if not base then
		player:Kick("This server is full. Please join another server.")
		return
	end
	base:SetAttribute("OwnerId", player.UserId)
	base:SetAttribute("OwnerName", player.DisplayName)

	local stats = Instance.new("Folder")
	stats.Name = "leaderstats"
	local cash = Instance.new("IntValue")
	cash.Name = "Cash"
	cash.Value = START_CASH
	cash.Parent = stats
	stats.Parent = player

	local state: PlayerState = { base = base, slots = {}, lockedUntil = 0 }
	states[player] = state

	local ok, data = pcall(function()
		return STORE:GetAsync("player_" .. player.UserId)
	end)
	if ok and type(data) == "table" then
		cash.Value = tonumber(data.cash) or START_CASH
		if type(data.units) == "table" then
			for i, name in data.units do
				if type(i) == "number" and type(name) == "string" and findUnit(name) then
					placeUnit(player, i, name)
				end
			end
		end
	end

	local function onCharacter(character: Model)
		-- the owner walks through their own barrier
		local function joinGroup(d: Instance)
			if d:IsA("BasePart") then
				d.CollisionGroup = ownerGroup(base)
			end
		end
		for _, d in character:GetDescendants() do
			joinGroup(d)
		end
		character.DescendantAdded:Connect(joinGroup)
		local home = base:FindFirstChild("Home")
		if home and home:IsA("BasePart") then
			task.defer(function()
				character:PivotTo(home.CFrame * CFrame.new(0, 4, 0))
			end)
		end
		local humanoid = character:WaitForChild("Humanoid") :: Humanoid
		humanoid.Died:Connect(function()
			dropCarried(player)
		end)
	end
	player.CharacterAdded:Connect(onCharacter)
	if player.Character then
		onCharacter(player.Character)
	end
end

Players.PlayerAdded:Connect(onPlayerAdded)
for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end

Players.PlayerRemoving:Connect(function(player: Player)
	dropCarried(player)
	save(player)
	local state = states[player]
	if state then
		for _, d in state.base:GetDescendants() do
			if d:IsA("Model") and d:GetAttribute("Slot") then
				d:Destroy()
			end
		end
		setLock(state, false)
		state.base:SetAttribute("OwnerId", nil)
		state.base:SetAttribute("OwnerName", nil)
	end
	-- anyone carrying this player's units keeps them
	states[player] = nil
end)

game:BindToClose(function()
	for _, player in Players:GetPlayers() do
		save(player)
	end
end)

-- Income: every owned unit pays its owner once a second
task.spawn(function()
	while true do
		task.wait(1)
		for player, state in states do
			local cash = cashOf(player)
			if cash then
				local total = 0
				for _, name in state.slots do
					local info = findUnit(name)
					total += if info then info.income else 0
				end
				cash.Value += total
			end
		end
	end
end)

-- The conveyor: a random unit every SPAWN_EVERY seconds, rarer ones less often
local totalWeight = 0
for _, info in UNITS do
	totalWeight += info.weight
end

local function pickUnit(): UnitInfo
	local roll = math.random() * totalWeight
	for _, info in UNITS do
		roll -= info.weight
		if roll <= 0 then
			return info
		end
	end
	return UNITS[1]
end

type Rider = { model: Model, info: UnitInfo, distance: number }
local riders: { Rider } = {}
local beltLength = (beltEnd.Position - beltStart.Position).Magnitude
local beltDirection = (beltEnd.Position - beltStart.Position).Unit

local function spawnRider()
	local info = pickUnit()
	local model = makeModel(info)
	model:PivotTo(CFrame.new(beltStart.Position + Vector3.new(0, 2.5, 0)))
	model.Parent = conveyor
	local rider: Rider = { model = model, info = info, distance = 0 }
	table.insert(riders, rider)
	local prompt = addPrompt(model, "Buy", info.name .. " ($" .. info.price .. ")", 0)
	prompt.Triggered:Connect(function(player: Player)
		local state, cash = states[player], cashOf(player)
		if not state or not cash or model.Parent ~= conveyor then
			return -- someone else bought it first
		end
		local index = freeSlot(state)
		if not index or cash.Value < info.price then
			return
		end
		cash.Value -= info.price
		model:Destroy()
		placeUnit(player, index, info.name)
	end)
end

task.spawn(function()
	while true do
		spawnRider()
		task.wait(SPAWN_EVERY)
	end
end)

RunService.Heartbeat:Connect(function(dt: number)
	-- move the conveyor units, and remove the ones that reached the end or were bought
	for i = #riders, 1, -1 do
		local rider = riders[i]
		rider.distance += BELT_SPEED * dt
		if rider.model.Parent ~= conveyor or rider.distance >= beltLength then
			rider.model:Destroy()
			table.remove(riders, i)
		else
			rider.model:PivotTo(CFrame.new(beltStart.Position + beltDirection * rider.distance + Vector3.new(0, 2.5, 0)))
		end
	end
	-- carried units float over the thief's head
	for player, state in states do
		local carried = state.carrying
		local character = player.Character
		local head = character and character:FindFirstChild("Head")
		if carried and head and head:IsA("BasePart") then
			carried:PivotTo(head.CFrame * CFrame.new(0, 3.5, 0))
		end
	end
end)

The script keeps one table per player, states, with their base, which unit sits on each slot, when their lock ends and what they are carrying. Every prompt handler reads that table, so the server always decides.

Step 3: Buying and income

spawnRider picks a unit with pickUnit, a weighted roll: with weights 50, 30, 14, 5 and 1, a Golden Noodle shows up about once in every hundred units. Each unit gets a Buy prompt whose handler checks three things before it takes any cash: the unit is still on the conveyor (two players can press at once), the buyer has a free slot, and they can afford it.

The Heartbeat loop moves every unit along the belt with PivotTo. The units are anchored and cannot be pushed, so physics never throws them off. The income loop runs once a second and adds up the income of every unit on a player's slots.

  • Faster belt: raise BELT_SPEED. More units: lower SPAWN_EVERY.
  • Bigger bases: add parts named Slot9, Slot10 and so on to a base's Slots folder. The script counts them.
  • Rebirths: a rebirth that resets cash for a multiplier fits here. Multiply total in the income loop by the player's multiplier.

Step 4: Stealing, locks and defending

Every unit in a base gets a Steal prompt with HoldDuration set to STEAL_HOLD. When it fires, startSteal refuses if the thief is the owner, the base is locked or the thief already carries something. Otherwise the unit leaves the owner's slot and a copy floats over the thief's head.

The thief has to reach their own Home pad: its Touched handler calls deliver, which puts the unit on a free slot. If the thief dies first, dropCarried sends it back to the owner. That is what makes defending matter.

How the lock works

Each base registers two collision groups, Base1_Barrier and Base1_Owner, and sets them not to collide. The barrier part goes in the first group and the owner's character parts in the second, so when the lock makes the barrier solid, it blocks everyone except the owner. Roblox allows 32 collision groups in a place, so this suits up to about 15 bases.

Give owners a way to fight back

Owners need a way to stop a thief, or every steal succeeds. A simple bat or slap tool that knocks players back and damages them works: when the thief's health hits zero, the unit returns. The sword script guide has a server-checked melee tool you can restyle as a bat.

Step 5: Saving

When a player leaves, save stores their cash and a list with the unit on each slot under the key player_<UserId>. When they join, the script reads it back and puts every unit on its old slot. BindToClose saves everyone when a server shuts down.

For a published game with many players, read the DataStore guide for retries and session locking. This script keeps saving short so the loop is easy to follow.

Step 6: Make it your own and test

  1. Make a model for each unit, name it exactly like its entry in UNITS, and put it in a Folder named Units in ReplicatedStorage. Give each model a PrimaryPart. The script uses your model instead of the block.
  2. Add a cash counter, a shop and daily rewards on screen. Roblox UI templates shows the game screens RoCode can add, and the shop GUI guide builds one by hand.
  3. Test with two players: in the Test tab, set the player count to 2 and start a Server and Clients test. Buy a unit with Player1, take it with Player2, then lock Player1's base and check Player2 cannot get in while Player1 can.

Common errors and fixes

If something does not work, open the Output window first and check these.

Infinite yield possible on Workspace:WaitForChild("Bases")

The map is missing or named differently. Add the BuildStealMap script, or check your folders are called Bases and Conveyor with Start and End parts inside.

Nothing happens when I press E on a unit

Check your Cash on the leaderboard against the price, and that your base has a free slot. Both checks fail without a message; add a notification there if you like.

The lock blocks me out of my own base

Your character parts must be in your base's owner group. The script sets it when your character spawns; if you changed the script, check onCharacter still runs for you.

My data does not save in Studio

Turn on Enable Studio Access to API Services in the experience settings (Security), in a place that is published. The Output shows "StealGame save failed" with the reason.

I get kicked with "This server is full"

There are more players than bases. Add bases (Base7, Base8) or lower Max Players.

Build it with RoCode

If you would rather describe the game than set it up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox and not connected to Steal a Brainrot: you describe the game in a web chat, and its Studio plugin creates the scripts and parts in your Explorer.

You type

Make a Steal a Brainrot style game with my own unit names: a conveyor of units to buy, a base for each player, cash per second, stealing with a hold prompt, a 30 second base lock and saving. Then add a cash HUD and a shop screen.

RoCode does
  • Writes the server scripts and builds the conveyor and bases, or uses the map you already have
  • Can add ready-made game screens, such as a coins HUD, a shop and daily rewards, in one step
  • Is told to compile-check each script with Check Script, and sets undo points around each batch of changes

You still test it, with two players for stealing and locks. Building a game takes several prompts: one for the loop, then one per feature. How RoCode builds a game.

Questions

How do you make a Steal a Brainrot game in Roblox Studio?

Build a conveyor that spawns units with Buy prompts, a base per player with slots, a loop that pays each owner every second, hold prompts that let other players take units from unlocked bases, and a lock that blocks everyone but the owner. Steps 1 and 2 have both scripts.

Can I use the real brainrot characters?

No. The names, models and sounds of a real game belong to its makers. Make your own units with your own names; a fresh theme also helps your game stand out.

How do I stop exploiters stealing from locked bases?

Check the lock on the server when the steal prompt fires, as startSteal does. A client can fire prompts, but it cannot change what the server decides.

How many players can a server have?

One per base. The test map has six bases; add more bases and raise Max Players together.

Do I need to know how to script?

It helps to read the code once, but you can paste both scripts as they are and change only the settings at the top. Or describe the game to RoCode and it builds it in your place.

Sources and further reading

How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 25 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.