◆ KUBORA DOCS ◆

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.

30minutes to first world
2scripting modes
free projects

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.

Minimum spec. 64-bit Windows 10 or 11, a GPU that speaks DirectX 12 or Vulkan, 4 GB RAM. The binary is ~21 MB and assets stream from the project folder.

Install

  1. Download KUBORA.exe from the download section.
  2. Move the file anywhere — Desktop\KUBORA works great.
  3. Double-click. Windows SmartScreen may warn on the first launch; click More infoRun anyway. The binary is unsigned during alpha.
  4. Studio opens on an empty baseplate. You are ready.

Your first world in five minutes

  1. Spawn a part. Top bar → Add PartBlock. A grey cube lands on the baseplate. Drag it with the move gizmo; scale it by dragging the corner handles.
  2. Paint it. With the part selected, open the Property panel on the right. Set Color to any hex you like.
  3. Script it. Right-click the part → Add Script. A .kub tab opens with a template. Paste:
    on self.touch as p do
      chat.say(p, "you touched the cube!")
    end
  4. Playtest. Press F5. You spawn into your own world. Walk into the cube. The chat line appears.
  5. Save. Ctrl+S. Choose Local or Cloud. Done.
That's the whole loop. Everything else in these docs is about doing more interesting things inside that loop.

Studio tour

Studio is a single 3D viewport with four docked panels. Everything is drag-resizable; hit F11 for a clean full-screen view.

◨ Top bar

Add parts, switch tools (move/rotate/scale), toggle grid, enter Playtest.

◨ Explorer (left)

Your project tree. Models, folders, parts, and scripts. Drag to reparent.

◨ Properties (right)

Whatever is selected exposes its props here. Color, size, rotation, collidable, mass, custom script fields.

◨ Output (bottom)

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:

PropertyTypeDefaultNotes
namestringautoShown in Explorer, used by world.find.
colorhex#cdd2e0Accepts #rrggbb or named palette.
posvec30,0,0World position in metres.
sizevec32,2,2Metres on each axis.
rotvec30,0,0Degrees, XYZ Euler.
collidablebooltrueOff = players walk through.
anchoredbooltrueOff = physics picks it up.
massnumberautoOverrides size-derived mass.
materialenumplasticplastic, 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.

Hot-reload. While playtesting, save a script (Ctrl+S) and KUBORA rewires the running world on the next frame. No restart. Events and timers re-bind; state persists.

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.

◨ R6 — classic

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.

◨ R15 — modern

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.

Your avatar carries the rig choice across places. If you pick R15 in the menu, every world you join uses R15 — unless that world's Place Settings lock it to R6 (or the other way round). See auto rig-conversion below.

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.

◨ 3D — default

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.

◨ 2D — side-scroller

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 rigsBoth (default), R6 only, or R15 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

  • Dimension3D (default) or 2D side-view. See Dimension above.
  • Sky presetDay, 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.

Reset to defaults at the bottom of the modal reverts every field on every tab in one click. It doesn't touch your explorer tree or scripts — just the settings object.

Feature matrix

Everything the Place Settings modal controls, at a glance.

TabSettingDefaultNotes
GeneralGraphicsHighLow / Medium / High / Ultra
ShadowsSoftOff / Hard / Soft / Cinematic
Camera lockFreeFree / First / Third
AvatarAllowed rigsBothBoth / R6 only / R15 only
Force heightoffclamp 0.7..1.4
Force buildoffclamp 0.7..1.4
WorldDimension3D3D / 2D
Sky presetDay6 presets incl. Dynamic
Time of day12.0hours, 0..24
Fog density0.00 = clear, 1 = thick
GameplayWalk speed16studs/sec
Jump power50initial Y velocity
Gravity65studs/sec²
Max players161..64
Allow spectatorsfalseextra slots beyond cap
Auto-respawntruewith delay slider
Allow chattruedisables the chat box
Allow fly / resetfalse / trueper-player command gates
PvP damagefalsehazards still damage
AudioMusic trackNone6 built-in loops
Music volume0.60..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

TypeExampleNotes
number42, 3.14Double-precision.
string"hello"Concat with ...
booltrue, false
vec3{x=1,y=2,z=3}Tables with x/y/z work as vectors.
partworld.find("X")Handle to a scene object.
playerp in on player.join as pLive player session.
duration5s, 250msUsed 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
Inside a part's script, 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_hex in 0xRRGGBB. Omit colour for white.
  • hud_bar(id, value, max, x, y, color_hex) — horizontal progress bar value / max, filled with color_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).
Why both forms? The 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.

The bridge uses the .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

  1. Sign in (top-right in Studio) if you haven't already.
  2. File → Publish. Pick a name, thumbnail and one-line description.
  3. Click Publish to Community. The upload takes a few seconds.
  4. 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.

◆ NEW IN v0.2 ◆

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.

Why this matters. Every other 3D platform makes you learn their editor before you can build anything. KUBORA still has the editor — Studio is great — but you don't need it. If you can describe a level in English, an LLM can produce a JSON Game, you double-click, and you're walking around it.

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.

Two dialects, both accepted. The canonical cloud format (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.
KeyTypeDefaultWhat it does
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.
texintegerOptional texture asset id.
meshintegerOptional 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 → JSONImport .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 vs. Append — which button?
  • 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).
Both accept the same JSON Game format. The only practical difference for an LLM author is: if the user says “add some obstacles to my existing world,” produce a small 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 not 2.
  • platforms array malformed — at least one platform has no c, h, or col.

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.

◆ KUBORA JSON Game — system prompt
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//