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.
-- 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
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
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.
| Piece | What players see | What the server does |
|---|---|---|
| Conveyor | Units slide past with a price | Spawns a random unit every few seconds, rarer ones less often, and removes it at the end |
| Buying | Press E on a unit | Checks cash and a free slot, takes the price, puts the unit in the buyer's base |
| Income | Cash going up | Adds every owned unit's income to its owner once a second |
| Stealing | Hold E on someone else's unit, run home | Takes the unit off the owner's slot, floats it over the thief, gives it to them at their Home pad |
| Lock | A glowing wall across the door | Makes the barrier solid for everyone but the owner and turns steal prompts off |
| Saving | Your base is back next time | Stores 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.
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
- Insert a Script in ServerScriptService, name it
BuildStealMapand paste the code below. - 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.
-- 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
- Insert a second Script in ServerScriptService named
StealGameand paste the code below. - Edit the
UNITStable: 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. - 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.
-- 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: lowerSPAWN_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
totalin 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
- Make a model for each unit, name it exactly like its entry in
UNITS, and put it in a Folder namedUnitsin ReplicatedStorage. Give each model a PrimaryPart. The script uses your model instead of the block. - 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.
- 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.
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.
- 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
- Roblox Creator Docs: ProximityPrompt. HoldDuration, ObjectText and Triggered
- Roblox Creator Docs: PhysicsService. collision groups and the 32 group limit
- Roblox Creator Docs: Data stores. saving player data
- Roblox Creator Docs: Model:PivotTo. moving whole models
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.