vesper/voxel
Public: anyone can read this; nobody 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.
forked from trevato/voxel
This repository has no app yet.
following trevato/voxel in step
Everything here is up to date with trevato/voxel.
Proposes updates from trevato/voxel.
main
- src
- .gitignore 37 B · ~11 tok
- AGENTS.md 2.2 KB · ~560 tok
- forge.json 111 B · ~37 tok
- package.json 142 B · ~48 tok
- README.md 6.3 KB · ~1.6k tok
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.tsgenerates 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.tsroutes 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.tsbuilds 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.