# BLOCKO3D 0.1.0-alpha: how to write a game (for AI models) BLOCKO3D is a small 3D fantasy console in the browser, like PICO-8 but 3D: a global JavaScript API, worlds made of blocks with 8 x 8 pixel textures, a 32-color palette, no lighting, a square 256 x 256 screen, and an IDE that is one web page: https://paloblanco.github.io/blocko3d/ This file is everything you need to write a complete, working game in one reply. Read it all before writing code. ## What to give the user Write **one JavaScript file**, the whole game, and these instructions with it: 1. Open https://paloblanco.github.io/blocko3d/ 2. In the **Demos** menu at the top, pick **Starter** (and say OK to replacing the current cart). 3. Open the **Code** tab, select all of its code, delete it, and paste the game in. 4. Press **Run** (or Ctrl+Enter). Click the game, then play: arrow keys move, Z or space is O (button 4), X is X (button 5). The Starter cart comes with art and sound already made (the **starter kit**, below), so your game needs no asset files. Build the levels in code with `map_new` and `map_box`. Do NOT write a `.b3d.json` file, and do NOT ask the user to paint maps, draw tiles or make models: everything comes from your code and the starter kit. ## Rules that matter - The language is **JavaScript**, not Lua. Use `let`, `function`, `for (let i = 0; i < n; i++)`, `===`, `+` for joining strings, `[x, y, z]` arrays and `{ a: 1 }` objects. There's no `local`, no `..`, no `~=`, no `ipairs`, and arrays start at 0. - **Everything is global:** call `map(0)` and `btn(4)` directly. There's no `import`, `require`, `engine.` or `window.` to add. - **Define `_init()`, `_update()` and `_draw()`.** `_update` runs 60 times a second, and `_draw` must draw everything, every frame (nothing stays on screen by itself). - **Space:** y is up, and one block is one unit. Block (x, y, z) fills x to x+1, y to y+1, z to z+1, so standing on a block at height y puts your feet at y + 1. - **Angles are in turns**, not degrees or radians: 1 is a full turn, 0.25 a quarter. `sin` and `cos` take turns. - **Many functions return arrays.** Unpack them: `let [x, y, z, nx, ny, nz, ground] = mpush(0, px, py, pz, 0.4)`. - **Colors are palette numbers 0-31**, never hex codes (0 black, 1 dark blue, 2 purple, 3 dark green, 4 brown, 5 dark gray, 6 light gray, 7 white, 8 red, 9 orange, 10 yellow, 11 green, 12 light blue, 13 lavender, 14 pink, 15 peach). - **Text** goes on top of the 3D view, at screen pixels 0-255 (the font is 7 x 10). ## The starter kit (the Starter cart) - **tiles:** 1 grass, 2 dirt, 3 stone, 4 brick, 5 wood, 6 sand, 7 water, 8 snow, 9 metal, 10 lava, 11 leaves, 12 log, 13 checker, 14 roof, 15 bounce pad, 16 gold - **vox models:** 0 hero, 1 hero step a, 2 hero step b, 3 guard step a, 4 guard step b, 5 gem, 6 coin, 7 flag, 8 tree - **model colors**, most used first, to recolor with `pal(color, new_color)`: hero 4 15 1 10, guard 6 15 5 8, gem 11 3 7, coin 9 10, flag 14 6 8 10, tree 3 11 4 - **sounds:** 0 jump, 1 coin, 2 hit, 3 step, 4 start, 5 win, 6 lose, 7 powerup (sounds 8-12 are the song's parts) - **music:** `music(0)` plays the song (it loops); `music(-1)` stops it - **maps:** none; make them in code (map_new, map_box) Use these numbers in code: `map_box(0, ..., 1)` for grass, `vox(6, x, y, z)` for a coin, `sfx(0)` to play the jump sound, `music(0)` for the song. A vox model is one block big, standing on its pivot: its bottom center at (x, y, z), facing +z. ## The API you need (all of it is global) The game loop is `_init()` (once), `_update()` (60 times a second) and `_draw()` (after each update). **Maps** are 3D grids of tile numbers; 0 is empty. - `map_new(w, h, d)`: a new empty map, w wide (x), h tall (y), d deep (z). Returns its number: the first one you make in the Starter cart is map 0. - `map_box(m, x0, y0, z0, x1, y1, z1, t)`: fill a box of map m with tile t. Both corners are included; t = 0 carves the box out. - `mset(m, x, y, z, t)` / `mget(m, x, y, z)`: set or read one block (whole grid numbers). - `map(m)`: draw map m. Call it in `_draw`, every frame. `map(m, true)` also draws the backs of faces, for tiles made see-through with `palt`. - `mpush(m, x, y, z, r)`: collision. Pushes a ball of radius r at (x, y, z) out of map m (or a list of maps, like `[0, 1]`). Returns `[x, y, z, nx, ny, nz, ground]`: the corrected position, the push direction, and the number of the map you're standing on, or -1 when in the air. - `mgetw(m, x, y, z)`: the block at a world point, as `[t, xi, yi, zi]`. Handy for "what am I standing on": `let [t] = mgetw(0, px, py - 0.5, pz)`. - `mray(m, ox, oy, oz, dx, dy, dz, maxd)`: the first block along a ray, `[t, xi, yi, zi, nx, ny, nz, d]`; t is 0 on a miss. - `map_set_pos(m, [x, y, z])` / `map_set_angle(m, [pitch, yaw, roll])`: move or turn a whole map (moving platforms, doors). Collision follows. **Models and shapes:** - `vox(n, x, y, z, pitch, yaw, roll, scale)`: draw vox model n. Everything after z is optional. To face a model the way it moves: `vox(0, x, y, z, 0, yaw_to(dx, dz))`. - `cube(x, y, z, size, c)`, `sphere(x, y, z, r, c)`, `line3(x1, y1, z1, x2, y2, z2, c)`: simple shapes in color c. **Camera and drawing:** - `cam_set_pos([x, y, z])` and `cam_set_lookat([x, y, z])`: place the camera each frame. - `cam_orbit(target, yaw, pitch, dist)`: the camera dist away from `[x, y, z]`, looking at it. Negative pitch looks down. - `cam_set_fov(f)`: field of view in turns (default 0.25). - `bg(c)`: the background color. - `outline(c)` / `outline()`: black outlines look good, `outline(0)`; they apply to draws after the call. - `pal(a, b)` / `pal()`: recolor color a as b for the following draws (a red guard into a blue one); `pal()` resets. - `clip(x0, y0, x1, y1)` / `clip()`: draw only in part of the screen (split screen: each `clip` gets its own camera). - `print(str, x, y, c, oc, sx)`: text at a pixel position, with an optional outline color oc and whole-number scale sx; it returns the x after the text. - `printc(str, y, c, oc, sx)`: centered text. - `rect(x0, y0, x1, y1, c)` / `rectfill(...)`: rectangles. **Input:** - `btn(i)`: held. `btnp(i)`: pressed this update. - Buttons: 0 left, 1 right, 2 up, 3 down, 4 O (keys Z, C, N, space), 5 X (keys X, V, M). - Players 1-3 are gamepads: `btn(i, p)`. - `stick(0)`: `[x, y]` from -1 to 1, right and down positive. It also follows the arrow keys, so `let [sx, sy] = stick(0)` works on a keyboard too. - Enter or P pauses: the engine handles that, so don't use it. **Sound:** `sfx(n)` plays sound n; `music(0)` starts the song, and `music(-1)` stops it. **Other:** - `save(value)` / `load()`: keep any JSON value between plays (a best time). `load()` gives `null` the first time. - `time()`: seconds since the start. - `rnd(n)`: a random number from 0 up to n. `rnd([a, b, c])`: a random element. - `flr`, `abs`, `sqrt`, `min`, `max`, `mid(a, b, c)` (clamp), `sgn`. - `dist(a, b)` (points as arrays), `dir3(yaw, pitch)` (a direction for an angle), `yaw_to(dx, dz)` (an angle for a direction), `angdiff(a, b)`, `lerp(a, b, t)`. - `printh(str)`: debug output to the IDE's console. The full reference is https://paloblanco.github.io/blocko3d/docs/API.md (the same as the IDE's Info tab). ## Mistakes to avoid - **Lua syntax**, or calling things through an object (`engine.map`): the game won't run. - **Degrees** for angles: use turns (90 degrees is 0.25). - **Guessing return values**: `mpush`, `mgetw`, `mray` and `stick` return arrays, as listed above. - **Forgetting to draw:** draw the map, every model and the HUD in `_draw` each frame, after setting the camera. - **Placing the player inside blocks:** a ball of radius 0.4 standing on a floor whose top is at y has its center at y + 0.4. - **Jumps that are too far:** with the example's physics (jump speed 0.2, gravity 0.01 a frame, run speed 0.09), a jump rises about 2 blocks and crosses about 3 blocks of gap on the level. Keep gaps at 1-2 blocks and steps at most 1 block up, or add a bounce pad. - **Tiles that don't exist**, or vox models past 8, or sounds past 12: only the starter kit is there. - **A camera that turns with the player:** the example keeps the camera looking along +z, so the controls never change direction. Stick with that unless the game needs more. ## A complete example: the Starter cart's own game This is the code already in the Starter cart. It's a working platformer: a level built from `map_box` calls, a walking hero with collision and jumping, coins, a patrolling guard, lava, a bounce pad, a flag, a HUD and a saved best time. Change it into the game the user wants, or use its parts. ```js // STARTER: a small platformer to turn into your own game. // The starter kit (change any of it in the Tile, Vox and Sfx tabs): // tiles 1 grass 2 dirt 3 stone 4 brick 5 wood 6 sand 7 water 8 snow // 9 metal 10 lava 11 leaves 12 log 13 checker 14 roof 15 bounce pad 16 gold // vox 0 hero 1 hero step a 2 hero step b 3 guard step a 4 guard step b // 5 gem 6 coin 7 flag 8 tree // sfx 0 jump 1 coin 2 hit 3 step 4 start 5 win 6 lose 7 powerup // music music(0) plays the song (patterns 0-1, sounds 8-12) // Controls: arrows or d-pad move, Z or space (button 4, O) jumps. let px, py, pz, vy, grounded // the hero: position (center of a 0.4 ball) and fall speed let facing = 0, walk = 0 // the hero's yaw (in turns) and walk-cycle clock let coins, got, guards let ticks = 0, won = false, best = null function _init() { build_level() // coins sit on the floor: y is the height of the top they sit on coins = [[7, 3, 15], [3, 5, 24], [12, 5, 24], [8, 4, 32], [3, 5, 38], [5, 10, 53]] .map(([x, y, z]) => ({ x: x + 0.5, y: y, z: z + 0.5, got: false })) got = 0 guards = [{ x: 3, z: 32.5, dir: 1 }] let data = load() if (data) best = data.best respawn() music(0) } // The level is map 0, built from boxes: map_box(map, x0, y0, z0, x1, y1, z1, tile). // A block at height y has its top at y + 1, so a floor box ending at y = 1 is stood on at y = 2. // The course runs along +z. function build_level() { map_new(16, 13, 60) // map 0: 16 wide (x), 13 tall (y), 60 long (z) map_box(0, 0, 0, 0, 15, 0, 59, 10) // lava under everything map_box(0, 0, 1, 0, 15, 1, 11, 1) // start meadow (top at 2) map_box(0, 0, 1, 2, 15, 1, 3, 13) // checkered start line map_box(0, 1, 2, 7, 1, 4, 7, 12) // a tree: log trunk... map_box(0, 0, 5, 6, 2, 6, 8, 11) // ...and leaves map_box(0, 6, 1, 14, 9, 2, 16, 3) // stepping stone (top at 3) map_box(0, 4, 1, 18, 11, 2, 19, 4) // brick stairs: top at 3... map_box(0, 4, 1, 20, 11, 3, 21, 4) // ...4 map_box(0, 1, 1, 22, 5, 4, 27, 5) // two wooden decks (top at 5) with lava between map_box(0, 10, 1, 22, 14, 4, 27, 5) map_box(0, 0, 1, 29, 15, 3, 35, 3) // stone yard where a guard walks (top at 4) map_box(0, 2, 1, 38, 4, 4, 39, 8) // snowy pillars in a row, each one higher (tops at 5, 6, 7) map_box(0, 2, 1, 41, 4, 5, 42, 8) map_box(0, 2, 1, 44, 4, 6, 45, 8) map_box(0, 5, 1, 44, 9, 6, 47, 6) // sandy ledge beside the last pillar (top at 7), with a bounce pad map_box(0, 7, 6, 46, 8, 6, 46, 15) map_box(0, 4, 1, 51, 11, 9, 57, 9) // high metal goal (top at 10) map_box(0, 4, 9, 51, 11, 9, 57, 16) // gold floor on top map_box(0, 4, 9, 51, 11, 9, 51, 13) // checkered finish edge } function respawn() { px = 8; py = 2.4; pz = 5; vy = 0 } function hit() { sfx(2) respawn() } function _update() { if (!won) ticks++ // move: the camera looks along +z, so up on the stick is +z and right is -x let [sx, sy] = stick(0) let dx = -sx * 0.09, dz = -sy * 0.09 px += dx; pz += dz if (abs(dx) + abs(dz) > 0.01) { facing = yaw_to(dx, dz) // turn the model's front (+z) toward the movement walk++ if (grounded && walk % 12 == 0) sfx(3) } else walk = 0 // jump and fall if (btnp(4) && grounded) { vy = 0.2; sfx(0) } vy -= 0.01 py += vy // collision: push the hero's ball out of map 0 let [x, y, z, nx, ny, nz, ground] = mpush(0, px, py, pz, 0.4) px = x; py = y; pz = z grounded = ground >= 0 if (grounded && vy < 0) vy = 0 if (ny < -0.5 && vy > 0) vy = 0 // bumped a ceiling // what's under the feet let [t] = mgetw(0, px, py - 0.5, pz) if (t == 10) hit() // lava if (t == 15 && grounded) { vy = 0.32; sfx(7) } // bounce pad if (py < 0) hit() // the guard walks back and forth across the stone yard for (let g of guards) { g.x += g.dir * 0.05 if (g.x < 1.5 || g.x > 14.5) g.dir = -g.dir if (dist([g.x, 4, g.z], [px, py, pz]) < 0.9) hit() } for (let c of coins) { if (!c.got && dist([c.x, c.y + 0.4, c.z], [px, py, pz]) < 0.8) { c.got = true got++ sfx(1) } } // reach the flag with every coin to win if (!won && got == coins.length && dist([8, 10, 54], [px, py, pz]) < 1.5) { won = true sfx(5) let secs = ticks / 60 if (best == null || secs < best) { best = secs; save({ best: best }) } } } function _draw() { bg(12) cam_set_pos([px, py + 3.5, pz - 6]) cam_set_lookat([px, py, pz + 3]) outline(0) map(0) // the hero, feet at the bottom of the ball; walk cycle 1, 0, 2, 0 let frame = walk == 0 ? 0 : [1, 0, 2, 0][flr(walk / 6) % 4] vox(frame, px, py - 0.4, pz, 0, facing) for (let g of guards) vox(3 + flr(time() * 6) % 2, g.x, 4, g.z, 0, g.dir > 0 ? 0.25 : 0.75) for (let c of coins) if (!c.got) vox(6, c.x, c.y, c.z, 0, time() / 2) vox(7, 8, 10, 54) vox(8, 13, 2, 8) outline() // the HUD, in screen pixels (256 x 256) print('coins ' + got + '/' + coins.length, 4, 4, 7, 0) print(flr(ticks / 6) / 10 + 's', 220, 4, 7, 0) if (got == coins.length && !won) printc('to the flag!', 20, 10, 0) if (won) { printc('YOU WIN!', 100, 10, 0, 3) printc('time ' + flr(ticks / 6) / 10 + 's best ' + flr(best * 10) / 10 + 's', 130, 7, 0) } } ``` ## Making it a different game Keep what works from the example (movement, `mpush` collision, the camera, the HUD) and change the rest: - **A race:** a looping track built from `map_box` pieces, and laps counted as the player passes checkpoints in order. Give computer runners **no physics**: each moves straight toward its next checkpoint at its own speed and stands on the floor under it (`let [t, xi, yi, zi, nx, ny, nz, d] = mray(0, x, 20, z, 0, -1, 0, 30)`, so the floor's top is `20 - d`), keeping its last height and hopping over lava or holes. Runners that jump with real physics fall in. Draw each runner as the hero with `pal(4, color)` before the `vox` and `pal()` after it, so each has its own color. - **Collecting or exploring:** more coins, a bigger map (up to 128 blocks a side), and keys that open doors (`map_box` with tile 0 to carve the door out). - **Survival:** enemies that walk toward the player, `yaw_to(dx, dz)` to face them, and a timer. - **Bigger worlds:** draw several maps, each placed with `map_set_pos`; moving platforms are maps moved with `map_set_pos` every frame. Keep the game in one file, and test it by reading it through once: every function you call is listed above.