Stage 2: Sphere Staircase + scaled rewards
Finish Stage 1. You should have a climbing wall, a Stage 2 SpawnLocation that pays 1 coin, and a TycoonEconomy script inside ServerScriptService.
a sphere staircase, the Stage 3 checkpoint, and a generalized leaderstats script that scans every SpawnLocation
how to loop over a folder of parts and read each one's attribute value to drive different behavior
a tycoon where reaching Stage 2 pays 2 coins and reaching Stage 3 pays 3 — automatically, with no per-pad code
60-second demo before campers start:
- Open a finished Stage 2 file. Press Play. The top-right shows
Coins 0. - Touch the Stage 2 pad →
Coins 2. Touch the Stage 3 pad →Coins 5(2 + 3). - Explain: "Last stage you wrote one script for one pad. This stage that one script handles every pad in the obby — and pays each one differently based on its attribute."
The big idea
Stage 1's script knew about exactly one pad: Stage 2's. It had stage2Pad hardcoded. That works for one pad. It doesn't scale to ten.
Today you refactor — keep the same script, but replace the single-pad logic with a loop that wires every SpawnLocation it can find. The loop also reads each pad's StageNumber attribute and pays that many coins. So Stage 2 pays 2, Stage 3 pays 3, Stage 10 will pay 10. One script, ten pads, ten different rewards.
This is the moment the StageNumber attribute starts earning its keep. You set it in Setup ("just an attribute"); now it controls the entire payout schedule. Same gesture, completely new power.
- refactor
- rewrite working code without changing what it does — usually to make it shorter, clearer, or easier to extend
- ipairs
- the Lua loop helper for going through a list of things in order (1, 2, 3, ...)
- GetChildren
- returns a list of every direct child of a part or service — useful for grabbing 'all SpawnLocations in Workspace'
- table indexed by Player
- a regular Lua table where each player object is a key — perfect for per-player state
Build it
Step 1 — Build the sphere staircase
Three spheres climbing up from the Stage 2 SpawnLocation. Same shape as the base obby's Stage 2.

Build this partJumpSphere_Short
SphereOpen recipe
JumpSphere_Short
Sphere- Size
- 4 × 4 × 4
- Color
- Bright orange
- Material
- Neon
- Anchored
- ✓ Yes
- Place
- In front of the Stage 2 red pad
Build this partJumpSphere_Mid
SphereOpen recipe
JumpSphere_Mid
Sphere- Size
- 4 × 4 × 4
- Color
- Bright orange
- Material
- Neon
- Anchored
- ✓ Yes
- Place
- Next to JumpSphere_Short, 3 studs higher
Build this partJumpSphere_Tall
SphereOpen recipe
JumpSphere_Tall
Sphere- Size
- 4 × 4 × 4
- Color
- Bright orange
- Material
- Neon
- Anchored
- ✓ Yes
- Place
- Next to JumpSphere_Mid, 3 more studs higher
Press ▶ Play and confirm you can climb all three. Round tops are unforgiving — aim for centers.
Step 2 — Wire the Stage 3 checkpoint
Build this partSpawnLocation (Stage 3 — top of the sphere climb)
BlockOpen recipe
SpawnLocation (Stage 3 — top of the sphere climb)
Block- Size
- 6 × 1 × 6
- Color
- Bright green
- Material
- Plastic
- Anchored
- ✓ Yes
- Place
- On top of JumpSphere_Tall
Also: check AllowTeamChangeOnTouch. Uncheck Neutral. Set TeamColor to Bright green.
Tag this SpawnLocation with a StageNumber attribute set to 3 — same gesture as Stage 1.
In Teams, insert a new Team named Stage 3. Set its TeamColor to Bright green. Uncheck AutoAssignable.
Step 3 — Refactor TycoonEconomy to scan all SpawnLocations
Open TycoonEconomy in ServerScriptService. Stage 1's script knew about one pad. Today you'll grow it to handle every pad — but in three passes so you can see each piece light up before adding the next.
Pass 1 — Switch from a per-player flag to a nested record
Stage 1 used local hasReachedStage2 = {} — a simple per-player flag (one boolean per player). For multiple stages, you need per-player AND per-stage tracking. Find Stage 1's line:
local hasReachedStage2 = {}
Replace it with:
local rewardedStages = {} -- rewardedStages[player][stageNumber] = true once paid
In the PlayerAdded handler, find hasReachedStage2[player] = false and replace with:
rewardedStages[player] = {}
Add a PlayerRemoving cleanup so the table doesn't grow forever as players come and go. Put this after the PlayerAdded block:
Players.PlayerRemoving:Connect(function(player)
rewardedStages[player] = nil
end)
Press ▶ Play. Walk to the red Stage 2 pad. It still pays +1 (Stage 1's old Touched handler is still attached — we'll replace it in Pass 3). Open Output. You should see no red errors. Press Stop.
This pass changed the data structure but kept the behavior. The next pass replaces the lookup.
Pass 2 — Replace the single-pad lookup with a scan loop that prints every pad it finds
Find Stage 1's pad-finding block:
local stage2Pad = nil
for _, part in ipairs(workspace:GetChildren()) do
if part:IsA("SpawnLocation") and part:GetAttribute("StageNumber") == 2 then
stage2Pad = part
break
end
end
print("Found Stage 2 pad:", stage2Pad and stage2Pad.Name or "NOT FOUND")
Delete that whole block. Replace it with a scanner that lists every tagged pad — no Touched handler yet, just a print so you can confirm what the loop finds:
for _, part in ipairs(workspace:GetChildren()) do
if part:IsA("SpawnLocation") and part:GetAttribute("StageNumber") then
print("Found pad with StageNumber:", part:GetAttribute("StageNumber"))
end
end
Press ▶ Play. Output prints something like:
Found pad with StageNumber: 1
Found pad with StageNumber: 2
Found pad with StageNumber: 3
(Order may vary.) If a pad is missing, its StageNumber attribute isn't set — go back to Step 2.1.
Also delete Stage 1's Touched handler — the stage2Pad.Touched:Connect(...) block. (Without it, no pad pays anything yet. Pass 3 fixes that.) Press Stop.
Pass 3 — Inside the loop, wire each pad's Touched handler with the per-stage payout
Extend the loop. For each pad it finds, connect a Touched handler that pays stageNumber coins the first time that pad is reached:
for _, part in ipairs(workspace:GetChildren()) do
if part:IsA("SpawnLocation") and part:GetAttribute("StageNumber") then
local stageNumber = part:GetAttribute("StageNumber")
part.Touched:Connect(function(otherPart)
local character = otherPart.Parent
local player = Players:GetPlayerFromCharacter(character)
if not player then return end
if not rewardedStages[player] then return end
if rewardedStages[player][stageNumber] then return end
rewardedStages[player][stageNumber] = true
player.leaderstats.Coins.Value = player.leaderstats.Coins.Value + stageNumber
end)
end
end
Press ▶ Play. Climb the wall to the red pad — Coins 0 → 2. Climb the spheres to the green pad — Coins 2 → 5. Reset. Coins stay at 5; both pads are claimed for this session.
Reaching Stage 1's pad doesn't pay anything — you spawn there, you don't touch it after spawning. That's correct. Stage 1 is the starting point; only stages 2 onward award coins.
Understand it
The rewardedStages table is a nested table. The outer keys are players. Each player's value is its own table, where the keys are stage numbers and the values are true once that stage paid out. Why nested? Because each player needs their own record — without that, the first player to reach Stage 2 would "use up" the reward for everyone.
The GetAttribute check inside the loop is important. If you put a part in Workspace that isn't a SpawnLocation — or a SpawnLocation that doesn't have a StageNumber — the loop skips it. The script doesn't crash; it just ignores anything that doesn't match the pattern. That makes adding new stages (Stage 3, 4, 5, ...) zero-cost: tag the SpawnLocation, the script picks it up.
The payout is stageNumber itself. The attribute value is the price. Stage 2 → 2 coins. Stage 10 → 10 coins. If you wanted Stage 10 to pay 100, you wouldn't change the script — you'd change Stage 10's attribute. Code stays generic; data drives behavior. This pattern (data + generic code) is how almost every real game economy works.
The early returns (if not player then return end, etc.) are guard clauses. Each one says "if this isn't the situation we care about, bail out before doing anything." Lua doesn't have early return keywords — you just write return from the function. Reading top to bottom, the function says: "is this a player? is their record set up? have they already been paid? If all three are no/no/no, pay them."
How this script wires every pad in one pass
One PlayerAdded handler. One PlayerRemoving cleanup. One loop that connects every SpawnLocation it finds. The loop is what makes the script generic — adding Stage 3, 4, 5 needs zero new code.
local Players = game:GetService("Players")
local rewardedStages = {}
Players.PlayerAdded:Connect(function(player)
local stats = Instance.new("Folder")
stats.Name = "leaderstats"
stats.Parent = player
local coins = Instance.new("IntValue")
coins.Name = "Coins"
coins.Value = 0
coins.Parent = stats
rewardedStages[player] = {}
end)
Players.PlayerRemoving:Connect(function(player)
rewardedStages[player] = nil
end)
for _, part in ipairs(workspace:GetChildren()) do
if part:IsA("SpawnLocation") and part:GetAttribute("StageNumber") then
local stageNumber = part:GetAttribute("StageNumber")
part.Touched:Connect(function(otherPart)
local character = otherPart.Parent
local player = Players:GetPlayerFromCharacter(character)
if not player then return end
if not rewardedStages[player] then return end
if rewardedStages[player][stageNumber] then return end
rewardedStages[player][stageNumber] = true
player.leaderstats.Coins.Value = player.leaderstats.Coins.Value + stageNumber
end)
end
end
Line 3Nested table for per-player progress.
Outer keys are Player objects. Inner keys will be stage numbers. So rewardedStages[player][3] tells you whether THIS player has been paid for Stage 3 yet.
Line 16Initialize the inner table on join.
rewardedStages[player] = {} creates an empty record. Without this line, the script would crash the first time a player touched a pad (nil indexing).
Lines 19–21Cleanup when the player leaves.
Setting rewardedStages[player] = nil lets Lua garbage-collect the player's record. In a long-running server with many joins, skipping this would slowly leak memory.
Lines 23–25The generic loop.
Walk every direct child of Workspace. Skip anything that isn't a SpawnLocation. Skip SpawnLocations without a StageNumber. Anything that survives both filters is a pad we care about. This runs ONCE when the script starts.
Lines 28–31Three guard clauses.
Bail out if: the touching thing wasn't a character with a player, the player's record isn't set up yet (they haven't joined fully), or this player has already been paid for this stage. All three protect the line below.
Lines 33–34Mark and pay.
Flip the rewarded flag to true (so future touches do nothing), then add stageNumber to the player's Coins. The amount = the attribute value. Same line works for Stage 2, Stage 5, Stage 10.
Try this
Try this
Three short experiments. Predict before you run, then test your guess.
Change the Stage 3 SpawnLocation's StageNumber attribute from 3 to 100. Press Play. Climb to the green pad. Predict what the Output / coin counter shows. Were you right?
Comment out line 19-21 (the PlayerRemoving block). Press Play. Walk around. Stop. Press Play again. Walk to the pads. Does anything visibly break? (Hint: it wouldn't until a player rejoins many times — but the memory leak is real. Why is the cleanup still worth writing?)
Right now coins reset when you stop the game. In Stage 4 you'll save them between sessions with DataStoreService. Look at the script. Which one line will need to change so that loading a saved Coins value happens before the player can start earning more?
Test your stage
- Press ▶ Play from the Start Platform. Top-right shows
Coins 0. - Climb to the red pad. Counter flips to
Coins 2. - Climb the spheres to the green pad. Counter flips to
Coins 5(2 + 3, not 2 + 1). - Reset. Counter stays at 5. Both pads are "done" for this session.
- Check Stage 1's pad on the Start Platform — it does NOT pay anything when you spawn. (You spawn at it; you don't reach it.)
- Design check. Does the bigger reward at Stage 3 feel earned? If Stage 2 felt easier than Stage 3 but paid less, the economy reads correctly. If not, tune the obstacles, not the payout.
If it breaks
- Stage 2 still pays 1 coin instead of 2. You left the old Stage 1 script next to the new one. Open ServerScriptService — there should be ONE script named
TycoonEconomy. If there are two, delete the older one. - Output prints
attempt to index nil with 'leaderstats'. A player touched a pad before PlayerAdded finished setting up their leaderstats. The guardif not rewardedStages[player] then return endshould prevent this — re-check that line exists. - Stage 3 pad pays the right amount but the player respawns at Stage 1. Color mismatch — same bug as the base obby. Open Stage 3's SpawnLocation, the Stage 3 Team, confirm both TeamColors are exactly Bright green.
- Stage 3's pad does NOTHING (no coin, no respawn). Probably missing
AllowTeamChangeOnTouchchecked ORNeutralstill checked. Re-check those two SpawnLocation properties. - The loop only finds Stage 2, not Stage 3. Stage 3's SpawnLocation is parented somewhere other than direct-child-of-Workspace. The loop uses
workspace:GetChildren()— that only returns direct children. If you placed Stage 3 inside a Model or Folder, either move it to Workspace or changeGetChildrentoGetDescendants(which scans everything).
The conceptual jump for Stage 2 is the nested table — rewardedStages[player][stageNumber]. Some 10–13s have never seen a table-of-tables. Walk every camper through what's in rewardedStages after one player joins and reaches Stage 2:
rewardedStages = {
[PlayerObject] = { [2] = true }
}
Two players, both reach Stage 2, only one reaches Stage 3:
rewardedStages = {
[Player1] = { [2] = true, [3] = true },
[Player2] = { [2] = true }
}
- The script feels like less work than Stage 1 (it's shorter), but it's secretly the more powerful version. Tell campers this explicitly — they're doing real engineering, not just typing more code.
- Common typo:
:GetAttribute("Stagenumber")(lowercase n) or"stagenumber". Lua is case-sensitive on attribute names. The error will benilreturned fromGetAttributeand theiffilter will quietly skip the pad. - This is the first stage where deleting the OLD script matters. Some campers will keep adding Scripts and end up with stage1Script + stage2Script both connecting to Touched — coins will pay twice per pad. Walk the room and check Explorer.
- 45 minutes total. Sphere build + checkpoint takes 15. Script refactor + testing takes 30.