trevato/voxel

Public: anyone can read this; the crew can change it.

A voxel sandbox game in the browser: an infinite chunked world from a seeded noise terrain, place and break blocks with a hotbar of block types, first-person movement with collision and jumping, day/night lighting, and worlds saved per name so you can come back. Hand-written WebGL2, 60 fps on a laptop, touch controls on a phone.

Running c454002 https://voxel--forge-trevato-apps.open-platform.sh in production 21d 16h

README.md

voxel

A block world in the browser. Name a world, walk into it, dig it up and build in it; come back later and it is how you left it. Hand-written WebGL2, no dependencies — the noise, the matrices, the mesher, the physics and the textures are all in src/.

PORT=8080 FORGE_APP_DATA=./data bun run src/server.ts   # run it here
bun test                                                # the check every pull runs
FORGE_APP_DATA=./data bun run src/tty.ts                # the terminal interface

Playing it

W A S D walks, space jumps, shift runs, the mouse looks once you click the page. Left click breaks the block you are pointing at, right click places the one in your hand, and 1–9 or the scroll wheel choose it. On a phone the left half of the screen is a thumb stick, the right half looks around, a tap breaks, and the buttons jump and place. The select at the top right sets how far you can see, from 64 to 192 metres.

The sun crosses the sky every twenty minutes; caves and the undersides of things stay dark through the day, and the stars come out at night.

Sheep, pigs and chickens wander the grass, fish keep to the water, and fireflies come out once it is properly dark. Nothing in this world can be hurt or killed: walk into one and it bolts, swing at one and it bolts, and the block behind it breaks anyway. They are not stored — see below.

How it is put together

  • src/world/ is the model, and it is the same code on both sides of the

wire: noise.ts (seeded gradient noise), terrain.ts (height, caves, sea, trees), chunk.ts, store.ts (chunks plus the blocks people changed), mesher.ts (faces, ambient occlusion, a daylight bit), physics.ts, raycast.ts, daylight.ts. mobs.ts is the table of creatures — the boxes each one is made of, how it walks, swims or hovers, and where it is willing to live — and herd.ts is the creatures a world is holding right now.

  • src/client/ is the browser: worker.ts generates and meshes chunks off

the main thread, renderer.ts draws them, mobmesh.ts packs the whole herd into one buffer for one draw call, input.ts reads a keyboard or a thumb, main.ts is the loop.

  • src/server.ts routes and holds the security floor — every response

carries it, including the 404, the 403, the 413, the 429 and the 500, and the page's one stylesheet runs on a nonce minted per response so the policy needs no unsafe-inline. src/db.ts is the schema; src/valid.ts is the boundary; src/bucket.ts is the rate limit; src/pages/ renders HTML.

  • src/bundle.ts builds the two client bundles at boot and serves them from

memory at a name that is a hash of their contents, cached for a year.

What is real: the terrain, the physics, the lighting, the creatures and the saving. An animal is a body with a gait, run by the same collision code the player is, so it falls, jumps a step, stops at a wall and turns back at a cliff rather than following a path laid out for it. Nothing is faked or pre-baked — a world is a seed and a list of changed blocks, so storage stays small no matter how far you walk. There is no multiplayer: two people in one world will not see each other, though they do change the same blocks. Chunks are 16 x 64 x 16 and the world is 64 blocks tall.

What it keeps

Not the creatures. A chunk hashes its own animals out of the seed when it arrives and forgets them when it goes, so the herd is bounded by what is loaded around you (at most 40 at once) and a world is still only a seed and a list of changed blocks however long anything walks around in it. Two people in one world see different sheep, the same way they do not see each other.

A world is a name and a seed. Only the blocks you change are stored, in SQLite under FORGE_APP_DATA. Where you are standing is kept per browser against a session cookie (HttpOnly, SameSite=Lax) — two people in the same world each get their own spot, their own time of day and their own hotbar. Worlds are public to anyone who can reach the app: anyone can open one by name and build in it, and a world you walk into that other people have already touched says so before you click in.

Deleting is the one thing that is not shared. The session that made a world deletes it with one button; anyone else has to type the world's name back first, since there is no undo and no record of who did it. A world made before this rule existed belongs to nobody, so it takes the typed name from everyone. The session that owns a world is minted when the world is made, not taken from the request, so a cookie someone else planted in your browser owns nothing; what that browser had already saved moves across with it. The terminal has no session at all, so d there always asks for the name.

Two limits keep the disk from being the only one: a copy of the app holds 500 worlds (WORLD_CAP) and a world holds 300 000 changed blocks (EDIT_LIMIT). Making worlds and saving blocks each have a token bucket in front of them (src/bucket.ts) so a client stuck in a loop is slowed rather than served. Request bodies are capped at 512 KB, counted as the bytes arrive rather than trusted from Content-Length.

/w/<name>/state.json still sends every changed block in the world in one answer, which is the biggest thing this app hands out. It carries an ETag made of when the world last changed and when you last moved, so a returning player revalidates and gets a 304 instead of the bytes. Serving edits by chunk column — the shape that would bound one request by the chunk instead of by the world — is the change this does not make yet.

It also has a terminal: the exact command is in FORGE_APP_SSH when the app runs (ssh -p 2222 <name>@<forge> — the app is the login name, the forge is the host; <name>--<pull>@<forge> for a preview's). It opens src/tty.ts on a real pty over the same worlds: j/k and enter to open one, n to make one, d to delete one — typing its name to confirm — then the same ground seen from above, where the cursor moves with h j k l, x breaks the top block and 1–9 place one. The creatures walk across that map while you watch it, on the same twenty-minute clock, and the line under it says what is about.

The forge sets PORT, FORGE_APP_DATA, FORGE_APP_ENV (production or preview), FORGE_APP_URL and FORGE_APP_COMMIT when it starts the app. forge.json says how.

↑↓ navigate ↵ open esc close

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs