Build worlds. Ship them.
Everything you need to go from an empty project to a multiplayer world people can join. The KUB scripting language, the Studio editor, and how to publish.
Getting started
KUBORA is a single Windows executable. Download it, run it, and you land straight in Studio. No installer, no launcher, no mandatory sign-in. The game saves locally by default; you only need an account when you want to publish or play with friends.
Install
- Download
KUBORA.exefrom the download section. - Move the file anywhere —
Desktop\KUBORAworks great. - Double-click. Windows SmartScreen may warn on the first launch; click More info → Run anyway. The binary is unsigned during alpha.
- Studio opens on an empty baseplate. You are ready.
Your first world in five minutes
- Spawn a part. Top bar → Add Part → Block. A grey cube lands on the baseplate. Drag it with the move gizmo; scale it by dragging the corner handles.
- Paint it. With the part selected, open the Property panel on the right. Set Color to any hex you like.
-
Script it. Right-click the part →
Add Script. A
.kubtab opens with a template. Paste:on self.touch as p do chat.say(p, "you touched the cube!") end - Playtest. Press F5. You spawn into your own world. Walk into the cube. The chat line appears.
- Save. Ctrl+S. Choose Local or Cloud. Done.
Studio tour
Studio is a single 3D viewport with four docked panels. Everything is drag-resizable; hit F11 for a clean full-screen view.
Add parts, switch tools (move/rotate/scale), toggle grid, enter Playtest.
Your project tree. Models, folders, parts, and scripts. Drag to reparent.
Whatever is selected exposes its props here. Color, size, rotation, collidable, mass, custom script fields.
Errors, print() output, hot-reload status. Tail it during Playtest to see what's happening.
Parts & primitives
A part is KUBORA's atomic object. Five primitives ship in the Add menu:
- Block — axis-aligned box. Most common part.
- Sphere — uniform radius. Collider is a sphere too.
- Cylinder — capped cylinder along the Y axis.
- Wedge — half-block ramp. Good for stairs.
- Pad — flat thin disc. Cheap spawn / button base.
Parts group into Models. A Model moves, rotates, saves and publishes as one unit. Scripts live inside a Model or directly under a Part.
Property panel
Every part exposes:
| Property | Type | Default | Notes |
|---|---|---|---|
name | string | auto | Shown in Explorer, used by world.find. |
color | hex | #cdd2e0 | Accepts #rrggbb or named palette. |
pos | vec3 | 0,0,0 | World position in metres. |
size | vec3 | 2,2,2 | Metres on each axis. |
rot | vec3 | 0,0,0 | Degrees, XYZ Euler. |
collidable | bool | true | Off = players walk through. |
anchored | bool | true | Off = physics picks it up. |
mass | number | auto | Overrides size-derived mass. |
material | enum | plastic | plastic, metal, wood, glass, neon. |
Hazards & zones
The Add menu has a Zone submenu that stamps pre-wired parts:
- Spawn — players appear here on join.
- Finish — touching fires
player.finish. - Hazard — touching resets the player to the last spawn.
- Checkpoint — updates the player's spawn.
- Teleport pair — two-way door, stamps both ends linked.
Zones are just parts with a script attached — you can read and modify them, or write your own version from scratch.
Playtest mode
Press F5 to run the current project. The world spins up in a local session and you control a character with WASD + mouse. Esc exits back to edit mode; your scene is unchanged.
Import from Roblox
KUBORA reads four Roblox file formats natively:
.rbxl— place, binary.rbxlx— place, XML.rbxm— model, binary.rbxmx— model, XML
File → Import. A modal asks for the
format first, then opens a native file picker. The tree
maps to KUBORA models and parts; scripts are imported
as inert .lua tabs — KUB does not auto-run
Roblox scripts (different engine, different API).
Rigs: R6 vs R15
Every KUBORA avatar is built on one of two rigs — the underlying skeleton the body is stitched to. Pick one in the Main menu → Avatar tab → Rig block. The choice is stored with your avatar and carries between sessions; the 3D preview reflects the switch instantly.
6 visible parts: head, torso, two arms, two legs. Each limb is one solid block. Simple silhouette, reads cleanly from any distance. Cheaper to animate, faster to render. This is KUBORA's default.
15 visible parts. Each arm and leg splits into upper + lower segments with a bendable joint; the torso splits into upper + lower; the head is still one block. Smoother joint bends, better for combat and cinematic animations.
Auto rig-conversion on join
KUBORA never kicks a player for the wrong rig. When you join a place that only accepts one rig, the client silently rebuilds your avatar onto that rig before the world loads. Colours, accessories, skin ID, height and build all carry over — only the skeleton swaps.
| Place allows | You joined with | What happens |
|---|---|---|
Both |
any | No change. You play as-is. |
R6 only |
R6 | No change. |
R6 only |
R15 | Auto-converted to R6. A toast says "Switched to R6". |
R15 only |
R6 | Auto-converted to R15. A toast says "Switched to R15". |
R15 only |
R15 | No change. |
The conversion is visual only. Your menu-selected rig is never overwritten — when you leave the place, the next world you join sees your original pick again.
Dimension: 3D (default) & 2D mode
Every place runs in one of two dimension modes. The mode is set per-place in Place Settings → World → Dimension.
The full free-look engine. Perspective camera, WASD + mouse look, scroll-wheel toggles first/third person (if the camera lock allows). Everything in this manual assumes 3D unless stated otherwise.
Locked orthographic camera on the X/Y plane. Player motion is flattened on the Z axis so you can't drift "into" the screen. Controls collapse to A/D for left/right and Space for jump. Great for platformers and retro puzzle worlds.
The player's own Avatar → Preferred view only takes effect in user-hosted sessions and the avatar preview. Any place you visit overrides player preference — gameplay depends on a single mode per world.
Place Settings
Open from Studio's topbar (gear icon) or Ctrl+,.
Every field here persists in the place's JSON under
settings and is re-applied live in both the
editor and any running playtest. The modal has six tabs.
Avatar tab
-
Allowed rigs —
Both(default),R6 only, orR15 only. Drives auto rig-conversion on join. - Force avatar height — overrides every player's height slider with a fixed value. Off by default. Use for competitive parkour so nobody can shrink under a low ceiling.
- Force avatar build (width) — same as height but for the build slider.
World tab
-
Dimension —
3D (default)or2D side-view. See Dimension above. - Sky preset — Day, Sunset, Night, Overcast, Space, or Dynamic (tint follows time-of-day).
- Time of day — slider 0..24. Drives sun angle, ambient colour and shadow direction.
- Fog density — 0 = clear, 1 = pea soup. Tints parts at distance too, so a high value gives a horror-ish feel cheaply.
Gameplay tab
- Walk speed — default studs/sec (16 = Roblox baseline).
- Jump power — initial Y velocity (50 = Roblox baseline).
- Gravity — studs/sec² down (65 = floaty Roblox, 196 = Earth).
- Max players — hard cap enforced by the matchmaker.
- Allow spectators — extra slots beyond the cap.
- Auto-respawn — respawn on death instead of a spectator screen.
- Respawn delay — seconds to wait before spawning.
- Allow chat / fly / reset — per-place permission flags.
- PvP damage — when off, player→player damage is a no-op; hazards still work.
Audio tab
-
Background music — Silent,
Lobby, Parkour, Tension,
Synthwave, Chiptune. Scripts may
still override with
audio.play("…"). - Music volume — 0..1. Applies to the music track only; SFX keep their own mix.
General tab & Shadows
General keeps the classic graphics preset (Low / Medium / High / Ultra), camera lock (Free / First / Third), and Hide own avatar in first-person. New in this release: a Shadows dropdown — Off, Hard, Soft (default), Cinematic — controls the shadow-map cascade count and softness.
Feature matrix
Everything the Place Settings modal controls, at a glance.
| Tab | Setting | Default | Notes |
|---|---|---|---|
| General | Graphics | High | Low / Medium / High / Ultra |
| Shadows | Soft | Off / Hard / Soft / Cinematic | |
| Camera lock | Free | Free / First / Third | |
| Avatar | Allowed rigs | Both | Both / R6 only / R15 only |
| Force height | off | clamp 0.7..1.4 | |
| Force build | off | clamp 0.7..1.4 | |
| World | Dimension | 3D | 3D / 2D |
| Sky preset | Day | 6 presets incl. Dynamic | |
| Time of day | 12.0 | hours, 0..24 | |
| Fog density | 0.0 | 0 = clear, 1 = thick | |
| Gameplay | Walk speed | 16 | studs/sec |
| Jump power | 50 | initial Y velocity | |
| Gravity | 65 | studs/sec² | |
| Max players | 16 | 1..64 | |
| Allow spectators | false | extra slots beyond cap | |
| Auto-respawn | true | with delay slider | |
| Allow chat | true | disables the chat box | |
| Allow fly / reset | false / true | per-player command gates | |
| PvP damage | false | hazards still damage | |
| Audio | Music track | None | 6 built-in loops |
| Music volume | 0.6 | 0..1 |
KUB language
KUB is a small scripting language for game logic. Its surface looks like Lua because Lua is easy to read and most people writing game scripts have seen it. Under the hood it runs on a modified Lua 5.4 VM with game primitives wired in as reserved forms, not libraries.
You do not import anything. world,
player, chat, fx,
on, every, after
are always in scope.
Syntax
Variables, conditions, loops:
let hp = 100 -- local
var score = 0 -- mutable
if hp < 20 then
chat.say(player, "low hp")
elseif hp < 60 then
-- ...
else
-- ...
end
for i = 1, 10 do print(i) end
for p in world.players() do p:heal(5) end
fn distance(a, b)
return math.sqrt((a.x-b.x)^2 + (a.z-b.z)^2)
end
Types
| Type | Example | Notes |
|---|---|---|
| number | 42, 3.14 | Double-precision. |
| string | "hello" | Concat with ... |
| bool | true, false | |
| vec3 | {x=1,y=2,z=3} | Tables with x/y/z work as vectors. |
| part | world.find("X") | Handle to a scene object. |
| player | p in on player.join as p | Live player session. |
| duration | 5s, 250ms | Used by every, after. |
Standard library expanded
Every name below is a plain function call. They overshadow
user fns with the same name, and return
nil when misused rather than crashing the
game.
Math
-- constants (call with no args)
pi() -- 3.14159…
tau() -- 2*pi
e() -- Euler's number
inf() -- +infinity
-- trig, in radians (use deg/rad to convert)
sin(x) cos(x) tan(x) asin(x) acos(x) atan(x) atan2(y, x)
-- angle conversions
rad(deg) -- 180 -> pi
deg(rad) -- pi -> 180
-- common scalar math
abs(n) sign(n) floor(n) ceil(n) round(n)
sqrt(n) pow(a, b) exp(n) log(n[, base])
min(a, b) max(a, b) clamp(v, lo, hi)
hypot(x, y) -- sqrt(x*x + y*y) without overflow
-- interpolation / easing
lerp(a, b, t) -- linear
smoothstep(edge0, edge1, x) -- Hermite ease-in-out
map(v, a0, a1, b0, b1) -- remap range [a0..a1] -> [b0..b1]
-- randomness (deterministic across ticks)
rand() -- [0, 1)
rand_range(lo, hi) -- [lo, hi)
rand_int(lo, hi) -- integer in [lo, hi]
Strings
len(s) upper(s) lower(s) trim(s)
repeat(s, n) slice(s, from, to) replace(s, from, to)
contains(hay, needle) -- case-sensitive
icontains(hay, needle) -- case-insensitive
starts_with(s, prefix)
ends_with(s, suffix)
index_of(hay, needle) -- char index, or -1
count(s, sep) -- number of separator-delimited fields
field(s, sep, idx) -- pick a field by index
concat(sep, a, b, c, ...) -- join all the rest
Conversion & reflection
str(v) -- any -> string
num(s) -- string -> number, error if unparseable
type(v) -- "nil" | "bool" | "number" | "string"
print(...) -- writes one line to the Output panel
Events (on)
on is the core event form. It registers a
handler that re-runs every time the event fires.
on player.join as p do
-- a new player connected
end
on player.leave as p do ... end
on player.chat as p, msg do ... end
on player.finish as p, time do ... end
on self.touch as p do ... end -- part's own script
on self.untouch as p do ... end
on world.tick do
-- every physics frame
end
self refers to
that part. In a global (world) script, self
is the world.
Timers (every, after)
every 1s do
print("tick")
end
after 3s do
chat.broadcast("round starts")
end
-- cancel via the handle
let h = every 500ms do ... end
h:stop()
World API
world.find(name)— first part matching name.world.find_all(name)— list.world.spawn(kind, props)— create a part at runtime.world.destroy(part)— remove it.world.players()— iterator of online players.world.random_point(filter?)— random walkable point.world.gravity— read/write, default-9.81.world.time— seconds since session started.
Player API
p.name— display name.p.id— stable ID across sessions.p.pos— vec3, read/write.p.walk_speed— number, default 16.p.jump_power— number, default 50.p:teleport(pos)— snap to a position.p:damage(n)— reduce HP.p:heal(n)— restore HP.p:kick(reason?)— force-disconnect.
Part API
part.pos,part.rot,part.size— read/write vec3s.part.color,part.material— visuals.part.collidable,part.anchored,part.mass— physics.part:spin(axis, seconds)— animate 360° on axis.part:move_to(pos, seconds)— tween to position.part:destroy()— remove from world.part:clone(props?)— duplicate.
Chat & FX
chat.say(player, "hello") -- DM to one player
chat.broadcast("round over") -- everyone in the room
chat.color(player, "#b36eff") -- name colour
fx.burst(pos, "sparkle") -- one-shot particle
fx.beam(a, b, { color = "#4c8dff" }) -- line between points
fx.sound(pos, "pop", { volume = 0.6 })
NEW HUD — absolute-positioned overlays
HUD items are always-on-top text and bars anchored to pixel coordinates in the viewport. Use them for scores, health bars, combo counters — anything that needs to be glanceable and not get in the way of menus.
-- Score + health bar, updated every tick.
var score = 0
var hp = 100
every 1s do
score = score + 1
hud_text("score", "Score: " + score, 20, 20, 0xffffff)
hud_bar("hp", hp, 100, 20, 48, 0x00d44a)
end
-- Remove a HUD item:
hud_clear("score")
Functions
hud_text(id, text, x, y, color_hex)— line of text at (x, y) pixels,color_hexin0xRRGGBB. Omit colour for white.hud_bar(id, value, max, x, y, color_hex)— horizontal progress bar value / max, filled withcolor_hex.hud_clear(id)— remove the overlay.
NEW Timer functions — real-time updates
Alongside the every / after
block forms, KUB now exposes timers as plain functions.
They return a numeric id you can cancel later — handy
when a timer is owned by a menu and needs to die when
the menu is destroyed.
-- Recurring timer every 0.5s, calls on_tick.
let t1 = timer_every(0.5, "on_tick")
-- One-shot timer 3s from now, calls on_ready.
let t2 = timer_after(3, "on_ready")
-- Cancel either by its id.
timer_cancel(t1)
fn on_tick()
hud_text("clock", "t = " + now(), 20, 80, 0xb36eff)
end
timer_every(seconds, fn_name)— recurring; returns timer id.timer_after(seconds, fn_name)— one-shot; returns timer id.timer_cancel(id)— stop a timer early.now()— seconds since the session started (same clock that drives timers).
every 1s do … end block is the ergonomic
form for world scripts. The function form is what you
reach for when you need to cancel, when the
timer is built from dynamic data, or when you're
wiring Blocks → KUB (blocks emit function calls, not
block syntax).
Lua interop
A KUB file is a Lua file with extra keywords.
You can drop into raw Lua any time: functions, tables,
metatables, coroutines all work the way you remember.
What KUB adds is on, every,
after, fn, let,
var, and the 5s /
250ms duration literals.
If you prefer a pure-Lua file, rename
.kub to .lua. The engine
still runs it, you just lose the sugar. A project can
mix both.
Blocks editor
Blocks is a drag-and-drop visual editor for the exact same API. It's the mode most younger creators start in, and it pairs well with a mentor writing KUB in the next tab over.
Every block corresponds to a KUB statement or expression. The Switch to KUB button dumps the current block program to an editable text file; the reverse direction (Switch to Blocks) lifts a well-formed KUB file back into blocks.
Blocks ↔ KUB bridge
A tidy KUB script round-trips losslessly. If your KUB uses metatables, coroutines, or other advanced Lua, Switch to Blocks will show a warning badge on the lines it couldn't lift — the script still runs, you just can't edit those lines visually.
.kub AST the engine
already parses for running. There is no second parser
and no translation step at runtime — blocks are
KUB, rendered differently.
Multiplayer
KUBORA rooms are one-per-world by default. When a player clicks Play on your published game they join the same room as everyone else currently playing it. Positions sync at 15 Hz via Phoenix Channels; chat, presence and custom events piggy-back on the same socket.
Your scripts do not know about the network. You write
on player.join, the engine routes the event
— the same handler fires in single-player, in local
playtest, and in a live 16-player room.
Invites
A friend's chat accepts rich invite cards. Send one by clicking the user in chat → Invite to room. The receiver sees a card with your world's thumbnail and a one-click Join button.
Publish
- Sign in (top-right in Studio) if you haven't already.
- File → Publish. Pick a name, thumbnail and one-line description.
- Click Publish to Community. The upload takes a few seconds.
- Your world appears on the Community shelf. Share the link, or search for the name.
Each publish is a new version; past versions are kept so you can roll back. You can also flip a project to Unlisted or Private from the dashboard.
Community shelf
The shelf is KUBORA's front door for players. It ranks games by recent activity — a fresh game with five players online beats a polished game with zero. There is no paid promotion slot.
JSON Game
A KUBORA world is a JSON document. That's it — no proprietary
binary, no database, no opaque bundle. You can write a playable
world in a text editor, paste one into a chat with Claude or
ChatGPT, diff two worlds in git, or ask an AI to
tweak a single platform's color. The engine treats the file
as a first-class input.
What it looks like
A JSON Game is one .json file. The top-level
shape is small on purpose — the only required fields are
v, start, and platforms:
{
"v": 2,
"kind": "kubora.jsongame",
"name": "Tower of Doom",
"start": [0, 3, 0],
"platforms": [ /* one object per part */ ],
"scripts": [ /* optional — attached KUB code */ ],
"settings": { /* optional — graphics + camera */ }
}
v is the format version (always 2
today). kind is a magic string; if present it
must be "kubora.jsongame". start
is the player spawn in world units: [x, y, z].
platforms is the geometry. Everything else is
optional.
Schema reference
Platform object. Every entry in
platforms describes one part. Only
c, h, and col are
required; the rest default to sensible values.
c, h, col,
hazard, finish) round-trips through
Save/Publish. For hand-authoring and LLM output we also
accept the friendlier aliases pos,
size, color, and
kind: "hazard" | "finish" | "moving".
size is treated as full extents
([8, 1, 8] = an 8×1×8 brick) and
halved automatically; colors may be [r, g, b]
in 0–1 or 0–255, or a
"#rrggbb" string. Pick whichever reads nicer.
c[x,y,z]—Center of the part in world space.h[hx,hy,hz]—Half-extents (so a 4×1×4 pad is [2,0.5,2]).col[r,g,b]—Color, each channel 0.0–1.0.shapestring"cube"One of cube, pad, pillar, diamond, ramp, steps, sphere, cylinder, wedge, torus.materialstring"Plastic"Surface style. See materials list below.opnumber1.0Opacity 0–1 (0 = invisible but still collides if collide=true).collidebooltrueWhether players bump into it.gravityboolfalseFalls under gravity when playtest runs.finishboolfalseTouching this part wins the level.hazardboolfalseTouching this part respawns the player.yaw / pitch / rollnumber0Rotation in radians around Y / X / Z.move_ampnumber0How far the part slides from its home position.move_axis[x,y,z][1,0,0]Direction it slides. Normalized by the engine.move_speednumber0Radians/sec of the sine motion.move_phasenumber0Phase offset so a row of parts can stagger.spinnumber0Radians/sec spin around local Y.texinteger—Optional texture asset id.meshinteger—Optional mesh asset id (Wavefront .obj imported into the project).
Materials. Any of Plastic,
SmoothPlastic, Metal,
DiamondPlate, Wood,
WoodPlanks, Slate,
Concrete, Brick,
Grass, Sand, Fabric,
Neon, Glass, Ice,
Marble. Unknown strings fall back to
Plastic.
Script object. Entries under
scripts attach code to the world. Each has:
{
"path": "Workspace/BouncePad/bounce",
"name": "bounce",
"kind": "Script", // Script | LocalScript | ModuleScript
"lang": "Kubora", // Kubora | Lua
"source": "on self.touch as p do\n p.velocity_y = 40\nend"
}
Settings object. Optional. Tweaks the per-project graphics and camera behaviour. Missing keys fall back to whatever the client was last using:
{
"graphics": "high", // low | medium | high | ultra
"camera_lock": true,
"hide_own_avatar_fp": true
}
A complete minimal world
Save this as hello.json, open KUBORA, click
Studio → JSON → Import .json,
pick the file. You're standing on a small purple pad
with a green finish block 20 units away.
{
"v": 2,
"kind": "kubora.jsongame",
"name": "Hello KUBORA",
"start": [0, 3, 0],
"platforms": [
{
"c": [0, 0, 0],
"h": [6, 0.5, 6],
"col": [0.45, 0.35, 0.80],
"shape": "pad",
"material": "Neon"
},
{
"c": [20, 0, 0],
"h": [2, 2, 2],
"col": [0.30, 0.80, 0.40],
"shape": "cube",
"material": "Grass",
"finish": true
}
]
}
The JSON button in Studio
There is one JSON button in the Studio toolbar, right next to Publish. Clicking it opens a dropdown with the four things JSON Game can do: Import (replace the world), Append (merge into the world), Export (dump to disk), and Copy AI Prompt.
Import .json
Opens a file picker filtered to .json.
The file is parsed, validated, and replaces
the current world — Workspace is wiped, spawn is swapped,
scripts and explorer tree come along if the file has them.
Use this when the file is a whole game.
Append .json
Same picker, but the loaded file’s parts and scripts are merged into the current project instead of replacing it. Nothing you built gets destroyed. Use this for drop-in obstacle courses, prop packs, script bundles, or tiny minigames layered on top of a world you already have.
Export .json
Dumps the current Studio world (parts + scripts +
settings) as a pretty-printed JSON file. Same bytes
we store in the cloud — round-trippable, git-friendly,
AI-editable.
Copy AI Prompt
Copies the full JSON-Game system prompt (the same one documented below) straight to your clipboard. Paste into Claude / ChatGPT, describe your world or the add-on you want, drop the reply back into either Import or Append.
- Import — a full game in one file. Wipes and reloads. Use when the JSON describes an entire world.
- Append — an add-on pack. Keeps everything you have and stacks the new parts / scripts on top. Use when the JSON is a piece (course, prop set, scripted gadget, minigame).
platforms array meant for Append; if they
say “make me a full tower”, produce a complete game
meant for Import.
The same dropdown pattern now applies to PLAYTEST — one button, three actions (Start, Stop, Restart) — so the toolbar stops growing a new verb every time we add a feature.
Errors are reported in the Output panel. Common ones:
missing required field `start`— you forgot the spawn array.unsupported JSON Game version—"v"is not2.platforms array malformed— at least one platform has noc,h, orcol.
The AI prompt — copy me
Fastest path: in Studio, click JSON → Copy AI Prompt. The full system prompt lands on your clipboard. Paste into Claude / ChatGPT / Gemini, tell it what world you want, then use JSON → Import .json on the reply.
If you're not near Studio, the same prompt lives below — copy it from the browser and do the same dance. You can also ask the model to edit a world you already exported: “make the platforms taller, add a hazard at the end, give me a parkour trail of 12 jumps.”
Writing for Append? If you only want the
model to author an add-on (a prop pack, a scripted
gadget, a 12-platform course to drop into your existing
world), add one line after the prompt: “This is an
Append pack, not a full game — avoid start-area
overlap and keep the `platforms` array compact.”
The schema itself is identical; only your framing changes.
You are a level designer for KUBORA, a desktop 3D multiplayer platform.
Your job is to produce a single JSON document that describes a playable
world. Output NOTHING except valid JSON — no prose, no markdown fences,
no commentary. The user's next message will describe the world they want.
CONTRACT
========
Top-level keys:
"v" : always the integer 2
"kind" : always the string "kubora.jsongame"
"name" : a short human-readable world name
"start" : [x, y, z] — player spawn, usually 3 units above a platform
"platforms" : array of part objects (see below). REQUIRED.
"scripts" : optional array of KUB script objects (see below)
"settings" : optional object — { "graphics":"high", "camera_lock":true }
PART OBJECT (one per entry in "platforms")
===========================================
Required:
"c" : [cx, cy, cz] center in world units
"h" : [hx, hy, hz] half-extents (a 4x1x4 pad is [2, 0.5, 2])
"col" : [r, g, b] each channel 0.0-1.0
Optional (use only when it matters):
"shape" : "cube" | "pad" | "pillar" | "diamond" | "ramp" | "steps"
| "sphere" | "cylinder" | "wedge" | "torus"
"material" : "Plastic" | "SmoothPlastic" | "Metal" | "DiamondPlate"
| "Wood" | "WoodPlanks" | "Slate" | "Concrete" | "Brick"
| "Grass" | "Sand" | "Fabric" | "Neon" | "Glass" | "Ice"
| "Marble"
"op" : 0.0-1.0 opacity
"collide" : true | false
"gravity" : true | false falls when playtest runs
"finish" : true | false touching this ends the level (green is nice)
"hazard" : true | false touching this respawns the player (red is nice)
"yaw","pitch","roll" : radians
"move_amp","move_axis","move_speed","move_phase" : slide motion
"spin" : radians/sec around local Y
SCRIPT OBJECT (in "scripts")
=============================
"name" : short identifier (required)
"source" : the actual code as a string (required)
"kind" : "Script" | "LocalScript" | "ModuleScript" (default Script)
"lang" : "kub" | "lua" (default kub)
"path" : "Workspace//