Levels
A level is everything the simulation builds its static world from: a set of low-poly meshes and the placements of those meshes (where each stands, how it is turned and scaled, what it is for the navigation and how it collides), the roads (polylines with lanes and a speed, no collider of their own: what the AI drives on and the maps draw; a smooth bend is one road sampled every few degrees), the spawn points, the nail strips and the range dummies (LevelData in packages/world). The ground is an implicit slab the size of groundHalfSize with its top at y = 0. The same triangles are what the server collides with (a convex hull or a triangle mesh per placement), what the client draws (flat-shaded, in the colours the mesh carries) and what the navigation reads (surfaceHeight, isOpenGround, the navigation grid), so what you see is what you hit.
Levels are scenes of the Babylon.js Editor, exported to level files, and named by what they are for: test-<words> is a test level (every one has a folder of end-to-end specs; test-core-gameplay, the POC city, is also the fixture of every unit test of the city, and test-ai-pathfinding, a road loop with an obstacle course, the fixture of the AI) and main-<words> is a level that ships to players (today one, main-riverbend: the flagship city of "Smart City Pilot", drawn freely round a river's loop, see Scene names). The server runs one (FOI_LEVEL; by default main-riverbend by name, else the first main-* in name order, else the first test-*) and sends it to every client in the welcome message; the client builds its prediction world, its scene and its minimap from that, and can draw the collision meshes as a wireframe over the level with the Colliders box on the legend's View tab.
apps/level-editor/assets/<name>.scene/ the editor's save (source of truth)
│ npm run level:export (reads the scene folder: transforms, scripts, geometry, colours)
├──▶ packages/world/levels/<name>.json level data: meshes, placements, roads, points
└──▶ packages/world/levels/index.ts the registry (generated): the entry @foi/world/levels
│ FOI_LEVEL=<name>
▼
apps/server ──welcome { level | its hash }──▶ apps/client (physics, scene, minimap from the same data)Directions
The game's frame is right-handed, in metres, +y up, and the camera looks down on it from the −z side: the screen shows +z up and +x on the left. Whatever is shown or written for people reads the way the screen does, as on a map:
| Compass | Game frame | On the screen and the minimap |
|---|---|---|
| north | +z | up |
| east | −x | right |
| south | −z | down |
| west | +x | left |
- Degrees for people are compass bearings: 0° north, clockwise as the screen shows it, so 90° is east, 180° south and 270° west. The HUD's facing on foot is one (
compassBearingin@foi/shared). - The code keeps the engine's yaw:
atan2(dx, dz), 0 facing +z and +π/2 facing +x, so the yaw turns anticlockwise on the screen and a bearing is the yaw with its sign changed. Positions stay(x, z); never change a sign to make a coordinate read like a map. - Names and texts use the compass the same way: a road, a piece, a place, a mission's line, a test or a guide that says east means −x, the screen's right (
test-curves' east hill, a volcano's"flow": "east",test-police'seastroad). A car facing north has its left, the driver's door, at +x: to the west. - In the editor and in Blender east is also on the right: the export mirrors
x(the editor's +x is the game's −x), and the kits' scripts work in Blender's top view (X right, Y up), whose X becomes the game's −x.
Install the editor and open the project
Install the Babylon.js Editor 5.5.2, the version apps/level-editor/package.json pins babylonjs-editor-tools to: the Windows installer or the build for another platform. It is a desktop app, not an npm package, so npm install ends with the download link while it is missing (build/editor.mjs). npm run level:editor then opens the project; it finds the editor where its installer puts it (%LOCALAPPDATA%\Programs\BabylonJS Editor or %ProgramFiles% on Windows, /Applications or ~/Applications on macOS, ~/Applications/BabylonJS Editor-5.5.2.AppImage on Linux) or at BABYLONJS_EDITOR_PATH, and prints the download link otherwise. Opening the folder apps/level-editor from the editor's dashboard does the same. The project is already set up: project.bjseditor, the scenes under assets/, the block scripts under src/scripts/blocks/ and babylonjs-editor-tools hoisted in the repository's node_modules, and babylonjs-editor-cli, a development dependency the editor itself adds to the project for its own packing and playing (the pipeline never calls it; knip is told to leave it be). The editor's own Play button only previews the scene; the game is played with npm run level:play.
Pieces: blocks and kit pieces
A level is made of pieces: instances of source meshes the designer places, moves, turns and scales. Two kinds of source exist:
- The twenty-one unit blocks under the
_blocksgroup (parked 30 m below the ground), each carrying a block script (src/scripts/blocks/<kind>.ts; the script's presence on a node is what the export reads, nothing runs at play time; its fields are numbers or, for the interactables, texts). Boxes, a wedge, stairs, a strip, markers, a road slab and the interactables (a door, a gate or a barrier, a container, a security camera, an NPC spawner, a vehicle spawner, a garage, a shop's counter) and a district's area (its name, title, theme and the pedestrians it suggests; the generator writes one per block of a town): enough for a city of blocks. - Kit pieces: meshes modelled in Blender (buildings, bridges, platforms, props, terrain, road pieces with their centre lines), installed in a scene under the
_kitsgroup with what they are (metadata.foion the source mesh: its kind, how it collides, the roads it carries) bynpm run level:recipe, see Kits and recipes. The export reads their geometry and colours from the scene folder like any other mesh.
Place an instance (right-click a source → create instance, or copy an existing piece), move it with the gizmo, scale it to size, name it <kind>-NNN. The blocks:
| Kind | Scaling means | Rotation | Becomes |
|---|---|---|---|
| ground | [size, 1, size], square; one per level, at the origin | none | groundHalfSize = size / 2 |
| building | the box's full size [sx, sy, sz], centred on the node | yaw, then pitch around its x axis | a placed box, kind building, a convex hull: the cars crash into it, nobody walks through it |
| structure | same as building | yaw and pitch | a placed box, kind structure: decks, floors, walls, railings, driven on, under, between; not ground (a bridge has a street under it) |
| terrain | same as building | yaw only | a placed box, kind terrain: ground, walked and driven on; a cliff where it ends (the navigation grid blocks the edge, isOpenGround sees the cliff from below), a ramp against it is its slope |
| ramp | [width, height, length]; origin = centre of the low edge | yaw only; climbs along the node's +z | a placed wedge, kind terrain: a solid slope, nothing gets under it |
| stairs | like a ramp; steps on the script (default 20); escalator (up, down) makes it carry whoever stands on it | yaw only | two placements: the treads, drawn and not collided with, and the hidden slope under them (<name>:slope), walked and not drawn |
| nail-strip | [length, 1, 1]; long axis +x, on the ground | yaw only | a nail strip: pops the tyres; no collider |
| spawn | never scaled | yaw only; the arrow's nose is +z | a spawn point, 1.2 m up for a car; role on the script: empty (or players) for the players, police (their units come out there), traffic (the ambient cars come out there), guards (a mission's enemies come out there), hospital (where a player who went down comes back). See the City guide |
| dummy | never scaled | yaw only | a range dummy's post, 0.9 m above what it stands on; shot at, comes back |
| road | [width, 1, length]; forward lanes run along the node's +z | yaw; pitch to climb | a two-point road: lanes (default 1), lanesBack (default 1; 0 = one-way), speed in m/s (default 14) on the script; no collider; pitched, the slab lies along its slope and the road climbs |
| door | [width, height, thickness]; origin = middle of the doorway's floor | yaw only; the panel lies along the node's x | a door of the level (doors): shut, it blocks its doorway; the interaction key swings it open about its −x end, away from the opener, its collider with it; lock on the script (a tag, key:door:office) keeps it shut for anyone without an item of that tag; kind gate (slides aside) or barrier (an arm that lifts, its height the arm's) opens by itself for a driver who carries what unlocks it (keycard, ticket). See Items |
| container | never scaled | yaw only; a locker opens from its +z | a container of the level (containers): kind crate, locker or pickup (items lying on the ground), items (ids with an optional count, first_aid*2 cash*50) and lock on the script; laid out by the server with the level and on a reset |
| camera | never scaled; origin = the lens | yaw only: the middle of its sweep | a security camera of the level (cameras): sweep (degrees either way), range (m) and period (s of a sweep there and back) on the script; its cone sweeps on a plan of the tick; it feeds the guards' alert, gunfire destroys it. See Stealth |
| npc | never scaled; origin = where it stands | yaw only: where a post faces | an NPC spawner (npcs): role (guard, civilian, driver, target, vip), weapon (a weapon's name, none), items (its pockets, as a container's), patrol (x z points in the game's frame, ; between them; empty for a post; a driver's route, a target's rounds), loop (1 loops, 0 back and forth), turn (degrees a post looks either way), vehicle (a driver's car class), refuge (x z: where a target runs to) and schedule (HH:MM goto|post|home|break x z; HH:MM patrol x z x z …; a timed hour HH:MM/every+for, 00:35/60+15 break x z) on the script; spawned by the server with the level. See Stealth and the City guide |
| vehicle-spawner | never scaled; origin = middle of the bay, on its floor | yaw only; the car faces the node's +z | a parking bay of the level (spawners): class (a class, or several separated by commas: a mix in turn), owner (nobody, npc), locked (1 or 0), key (the tag its key answers to; key:vehicle:<name> when empty), respawn (s; 0 never) on the script; the server parks its cars (see Vehicles) |
| garage | [width, height, depth]; origin = middle of the bay's floor | yaw only; its door at the node's +z end | a garage of the level (garages): respray and store, the prices in dollars (−1 for none), on the script; a car standing in its bay is offered a respray and storage (see Vehicles); the walls around it are placed apart |
| shop | never scaled; origin = the counter's foot | yaw only; it serves from its +z | a shop's counter of the level (shops): items (item ids, spaces between them) on the script; the interaction key opens its stock at the game's prices for the player's Cash (see Items) |
| ladder | [1, height, 1]: how high it climbs; origin = the middle of its foot, against its wall | yaw only; the climber faces +z (the wall) | a ladder of the level (ladders): the interaction key at its foot or its top climbs it |
| lift | [width, 1, depth] of its platform; origin = the platform's top at its lowest stop | yaw only | a lift of the level (lifts): stops (heights over the lowest, ascending: 4.2 8.4), ridden up and down on a fixed timetable, standing a while at each |
| water | [width, 1, depth] of its surface; origin = the middle of the surface | quarter turns only | a surface of the level (water): kind water (a person wades over the knees and swims over the chest, a car whose middle sinks under it floods and is wrecked) or lava (it kills and wrecks); the ground under it is its bottom, the ground higher than it its shore |
Units are metres (1 editor unit = 1 m). The editor scene is left-handed and the game is right-handed, so the export mirrors x: a piece the game sees at (x, y, z) with yaw yaw stands in the editor at (−x, y, z) with yaw −yaw; pitch and sizes are the same, and a mesh's triangles are mirrored with it. The mirror lives in one place (apps/tools/src/frame.ts); in the editor, just build the city as you see it.
Groups (transform nodes buildings, structures, terrain, …) are for tidiness: the export composes every piece's world transform through its parents. Groups may be moved and turned, never scaled. No roll anywhere; only boxes and roads may be pitched.
Floors, stairs and what the AI walks
The guards, the chasers and the checks walk the layered navigation: the ground in 2 m cells and, over it, every floor someone can stand on (the up-facing top of a structure or a building more than half a metre over the ground, with 1.8 m of headroom: an upper storey, a deck, a bridge, a roof). Two neighbouring cells connect where their surfaces meet within half a metre at the edge between them. To make a floor reachable by them:
- Lead stairs (or a ramp) up its length onto the floor, the top of the flight level with the floor's edge; a flight is never climbed from its side.
- Make the flight at least 2 m wide, better 4 m: narrower stairs against a wall (the compound annex's fire stairs) are walked by players but not by the AI, nor counted reachable.
- Leave a gap in a parapet as wide as the flight where it arrives.
- Give a place on a floor its
y(the floor's height): it is checked open and reachable there, and spawned onto it. - A flight whose foot stands on a floor (more than half a metre over the ground) is a floor of its own, a structure, not the ground: the floor under it stays a floor. A ladder or a lift joins the floors at its ends for the checks (no AI climbs or rides yet).
- Keep passages and landings 4 m wide with nothing in them, railings included: the cells are 2 m wide, and one that touches a railing or a wall is blocked.
test-storeys shows them: an office of two storeys (its ground floor sealed) up 4 m wide stairs, and an overpass carrying a road over the street. test-landmarks shows whole buildings: the landmarks kit's five, every storey a part of its own, their ways up blocks their anchors carry.
Roads, bends and hills
Roads are polylines: a road block is a two-point road, and the road graph joins roads whose centre lines touch or cross (a crossing, a T, two ends that meet), with a node at every bend of a polyline and one lane edge per direction between nodes. A smooth bend made of road blocks is a chain of short slabs end to end (keep every chord longer than half the road's width plus two metres, or the junction snapping reaches two chords away); a kit road piece carries a real curve. A hill is terrain boxes (its terraces) with ramps leaning against them (its slopes); the roads on a slope are pitched to the slope, and the level gate wants every road within half a metre of what carries it (the ground, a terrace, a ramp, a deck), so a road that hangs over a ramp's foot or sinks into a terrace is reported. surfaceHeight in packages/world says how high the ground is anywhere (the plane, or the highest up-facing terrain triangle: a terrace, a slope, a hillside); isOpenGround reads walls relative to it and knows the inside of a solid; the navigation grid gives every cell its ground height and blocks the cliffs; the server drops its chasers on it. Real hills are kit terrain: a region's tiles (see Tiles), or, for the curves a region's roads cannot make, a generator such as assets/blender/levels/curves.py (test-curves), the worked example, one terrain mesh graded flat under its roads (bends and a serpentine as curves sampled every few degrees, ramp streets with a straight grade, terrace avenues on bridges over a ravine, a rally road left on the bumps). Do not use scene links and do not move or scale the sources under _blocks or _kits.
Kits and recipes
The kits are the geometry Blender makes: apps/level-editor/assets/kits/<kit>.glb (one mesh per piece, flat colours as materials) with a sidecar <kit>.foi.json (every piece's kind, collider and the centre lines of a road piece; render: false for a collider nobody sees, which the export writes without colours, so the game never draws it; breakable, a light piece's mass in kg, which its placements carry: a vehicle with the kinetic energy of a mid-size sedan at 60 km/h (a truck at a crawl, a compact at 68 km/h) knocks the piece loose and drives on, a slower one stops against it), written by the scripts under assets/blender/ (see assets) and committed. No two faces of a piece may lie in one plane facing the same way with different colours (they would flicker): packages/world/src/coplanar.test.ts searches every level, within pieces and across them, and apps/tools/src/kits.test.ts every kit piece (see the rule in assets). city holds buildings with facades (scaled by the level like the unit blocks they replace), a slab, a terrace, a ramp, stairs with their hidden slope, a pier and a bridge deck, trees, lamps, benches, barriers, containers, a bus shelter, a fountain and a billboard; roads holds street pieces (kerbs and sidewalks; the tarmac is drawn by the game from the centre line) with their centre lines: straights, bends of two radii, a T and a crossroads; downtown holds buildings modelled at their real size (towers of glass and of stone, offices, apartment houses with balconies, corner blocks and rows of shops with awnings, townhouses, warehouses, hotels, cafés, two or three colour variants each, placed unscaled with their front facing the street) and the furniture of streets and hills (street trees, pines, street lamps, traffic lights, bins, hydrants, bollards, planters, a kiosk, guardrails, rocks, a radio mast, an obelisk, picnic tables, telescopes, a tunnel portal's frame); its trees, lamps, traffic lights and portal frame collide as their own triangles, not as a convex hull that would wrap a canopy or an arm. civic holds the building families downtown lacks, at their real size and placed the same way (a police station, a hospital, a fire station, a town hall, a school, a courthouse, a library, a post office, a supermarket, a mall, a bank one walks into (its vault behind a steel gate; its upper floors a part of the kit placed with it, a structure the game fades over a player inside), a gas station, a body shop, a car rental, a restaurant, a bar, a detached house, a factory, a depot, a power station, a bus station, a car park); those with a bay, a lot or a canopy collide as their triangles. Every building of both kits says in its sidecar what it is (tags, OpenStreetMap's: amenity=hospital), the look its variant leans to (look) and its named points (anchors), which the generator turns into places and blocks (see assets). street holds what dresses a lived-in town, a lake's shore and a volcano (street furniture, yards and parks, building works, parked cars, a car park's pad, a jetty, boats, reeds, fumaroles, a seismograph's hut; see assets). A generated level has a kit of its own, named after it: test-curves' holds its terrain, the tunnel's roof, the paint (zebra crossings, parking bays) and one piece per road carrying its centre line; a region's holds its ground, its bridges and its roads' pieces, with one more per town in it (test-tiles-town-old-town: the town's ground, paint and roads). The tools turn the glTF frame into the game's with one rule (game = (−x, y, −z)) and into the editor's through the usual mirror.
The kit library is what the catalogue (and so Claude, through the MCP server) knows of the shared kits, every kit but a level's own: npm run kits:export reads each one's glTF and sidecar and writes packages/catalogue/src/kits.generated.json, a line per piece with its kit and id, kind, collider, footprint (width along x across its front, depth along z, height, in metres, measured on its triangles in the game's frame) and bounds around its origin, triangle count, tags, anchors (in the game's frame), breakable mass, roads and parts. A piece whose sidecar gives no tags (most props, structures and terrain) gets words derived from its kit, its kind and its id, with a few categories (city/oak is tagged city, prop, oak, tree), so a search for a tree finds it. Run it after regenerating a kit and commit the file with the kit: level:check builds the library again and fails when the file drifts. Each piece is a kit-piece entry of the catalogue (kit-piece.street.bench), which catalogue_search lists with its size and tags and catalogue_get shows with its bounds and its anchors.
A recipe (<name>.recipe.json, beside the kits) is a level described by what it places, in the game's frame: its kits, its placements (kit/piece or block:<kind>, with a position, a yaw, a pitch, a scale, the lanes and speed of a road piece and a block script's params: a door's lock, a container's kind, items and lock), its roads (polylines laid as road blocks), spawns, strips and dummies. npm run level:recipe -- apps/level-editor/assets/kits/<name>.recipe.json --scene <name> builds the editor scene from it: the kits installed under _kits, one instance per placement under the group of its kind; then npm run level:export as for any scene. npm run level:dress -- <level> --out <file.recipe.json> writes the recipe that places the city kit's pieces at the coordinates of a block level's blocks (a tall building a tower, a low one a shop or a house, a slab, a terrace, a ramp, a flight of stairs with its slope): that is how the two test levels were made from the POC's block levels, and their recipes are kept beside the kits. A level built from a recipe is still an ordinary scene: pieces can be added or moved in the editor, and level:port brings an exported level back into a scene with its kits.
Generating a level from a spec
A level spec describes a level in a few lines of JSON and the generator builds it: assets/blender/levels/generate.py, run in a headless Blender, reads the spec, hands it to its template and writes the level's own kit (<name>.glb and .foi.json: the ground, the paint, one hidden piece per road carrying its centre line), its recipe (<name>.recipe.json) and its named places (<name>.places.json: { "places": { "<id>": { "x", "y", "z", "yaw" } } } in the game's frame, y the ground's height there) into --out:
"C:\Program Files\Blender Foundation\Blender 5.2\blender.exe" -b --factory-startup --python assets/blender/levels/generate.py -- --spec apps/level-editor/assets/kits/test-generated-town.spec.json --out apps/level-editor/assets/kits
npm run level:recipe -- apps/level-editor/assets/kits/test-generated-town.recipe.json --scene test-generated-town
npm run level:exportEvery template takes the level's sky too: a climate (the weather's odds; a region's biome by default, temperate for a town) and a clock ({ start, pace }, the time a game on the level starts at and its pace). Neither changes the geometry: the export writes them into the level's sky from the spec beside the kits (apps/tools/src/level-sky.ts), and a level built of blocks has none, so it is always clear, at noon (see Time and weather).
A region spec (land of 64 m tiles shaped by hills, mountains, plateaus and valleys) is built without Blender: npm run level:region -- <spec.json> writes the same four files (see Tiles), then level:recipe and level:export. A city spec (a city drawn freely, as Riverbend is: The city spec) is solved by @foi/catalogue's solver and its plan built by Blender (assets/blender/levels/city.py, Riverbend's engine): npm run level:city -- <spec.json> writes the same four files (--check runs the solver alone and prints the spec's problems or its map), then level:recipe and level:export. Blender's template is grid-town (town.py): a town on a grid of kerbed blocks. Its spec: template ("grid-town"), name (test-<words>, main-<words> or a draft's draft-<id>), seed (a whole number from 0, default 1; --seed <n> overrides it), blocks ([along x, along z], 1 to 8 each), blockSize (metres between street centre lines, 40 to 120, default 64), streetWidth (8 to 16, default 11), avenues ({ "x": [...], "z": [...] }: the street lines, counted from 0 along the game's x and z, that are 16 m avenues with two lanes each way), districts (a list of { "name", "blocks": [[i, j], ...], "mix": { "<archetype or tag>": weight }, "theme", "title", "density": { "built", "setback", "height" }, "landuse", "landmarks": [...] }; blocks no district lists belong to the first one; the archetypes are the buildings of the downtown kit (tower-glass, tower-stone, office, apartment, corner, shops, townhouse, warehouse, hotel, cafe) and of the civic kit (police-station, hospital, fire-station, town-hall, school, courthouse, library, post-office, supermarket, mall, bank, gas-station, body-shop, car-rental, restaurant, bar, house, factory, depot, power-station, bus-station, car-park) and the walk-in landmarks of the landmarks kit (mega-mall, general-hospital, mensa, castle, ski-lodge: big, so a district that names one wants 120 m blocks), and a mix or a landmark may name an OpenStreetMap-style tag a building carries (amenity=hospital, building=office) for all of them, besides the 125 buildings made from OpenStreetMap's tags (see Buildings from OpenStreetMap's tags); see below), parks (blocks that are a lawn with a cross of paths, a fountain, benches and trees), compounds (up to four blocks that are walled compounds, see below) props (lamps, trees, trafficLights, hydrants, each true by default) and dressing (none by default: the lamps, trees, hydrants and traffic lights alone, so a town without it is the same byte for byte; lived-in: assets/blender/levels/dressing.py, on a random stream of its own, mostly the street kit: furniture within 1.2 m of the kerb by district theme (bins, benches, mailboxes, newspaper boxes; parking meters in a gig-depot or city-hall district; bike racks and e-scooters in a tech-campus; phone booths and advertising pillars in an old-town), a bus stop with a shelter on the avenues every 120 m or so, street signs at the corners, a billboard on every road coming into the town, back yards by theme (fences, sheds, trampolines, washing lines, barbecues…), car parks (a painted pad of bays, cars parked in them, a pay machine), one construction site (a fenced yard, a cabin, a cement mixer, pallets of bricks, a scaffold, a tower crane), and in a park a playground or, in an old town, market stalls; the parked cars are breakable props of 1,100 to 1,800 kg that a hard hit shoves loose). One road per street line (ns-<i> along z at line i of x, ew-<j> along x), the outer ring's corners rounded, every junction's kerbs rounded with traffic lights and zebra crossings, buildings along every street of a block never touching each other or the pavement, eight spawn points in two rows on the southern street. Its places are junction_<i>_<j>, park_<i>_<j>, <district>.<archetype>_<n> (on the pavement in front of each building's entrance, facing it; dashes become underscores; with the building's tags and, in a themed district, its invented name), <district>.<archetype>_<n>.<anchor> (each anchor of the building: its entrance, a counter, a hospital's respawn and ambulance bay, a bank's ATM and vault, a bus station's stops…), a lived-in town's bus_stop_<n>, carpark_<n>, site and an old town's market, and spawn_<n>. A spec that is not valid is refused with one line per problem (<field path>: <message>) and exit code 1. All randomness is the spec's seed: the same spec gives the same files byte for byte; a 3 × 2 town takes about 1 s in the generator, 3 to 5 s with Blender's start. The spec's schema, for the platform and Claude, is @foi/catalogue's LevelSpec, held to the Python by its tests; the platform runs the same generator on a draft's spec through the worker (apps/worker), which turns Blender's output into level data in memory (@foi/tools' exportRecipe, the level level:recipe and level:export would give) — see Platform.
The level audit
Every level is held to geometric rules nobody needs a picture to check (@foi/world's auditLevel, its README): no prop standing in another or inside a building, no building in another, every piece resting on what is under it (no bench floating over a hilltop, no tree sunk into a bank, every boat on the water), nothing in a carriageway but what is made for the road, no z-fighting, no paint over paint, and a road the spawn's road cannot reach said as a warning. The bundled levels pass it as they are (packages/catalogue/src/levels-audit.test.ts, what a test level does on purpose named with its reason): a generator that breaks a rule is fixed in the generator, never in the scene nor by a repair. A generated level is repaired instead (apps/worker/src/audit.ts): a faulty prop of a shared kit dropped, paint under other paint left out, nothing ever moved, and the build says what it left out; a fault no repair clears is the generator's to fix. The generators keep to the rules as they build: the city generators read a piece's height off the ground as it was triangulated (cityparts.Ground, not the function the ground was built from, which agrees with it only at its vertices), the placer keeps everything but a traffic cone off a road's surface, the markings leave out a mark under another (cityparts.resolve_paint: a cycle lane breaks at a crossing), and a region's dressing leaves out a piece that would stand in another. A kit piece that rests on something other than the ground says so in its sidecar (rests: water for the boats, road for the traffic cone).
Buildings from OpenStreetMap's tags
Besides the hand-made buildings, a town may be built of 125 buildings the generator makes from OpenStreetMap's tags, in six kits (osm-civic, osm-commercial, osm-residential, osm-industrial, osm-leisure, and osm-facilities for 19 props: a vending machine, an EV charger, a toilet block, a court, bleachers, a pumpjack…). Where they come from:
npm run osm:tagsasks taginfo for the most used values of ten keys (building,amenity,shop,leisure,landuse,tourism,office,craft,historic,man_made) and writes them, with their counts and the wiki's descriptions, topackages/catalogue/src/osm/osm-tags.json(250 values; © OpenStreetMap contributors, ODbL). The snapshot is committed; the game never asks taginfo anything, andnpm run osm:tags -- --checkchecks it without the network.packages/catalogue/src/osm/recipes.jsonsays what each tag becomes: a building (its form — a shopfront, a house, a terrace, a block, a hall, a shed, a canopy, a kiosk, a chapel, a tower or a lot —, size, storeys, roof, look, accent colour, sign and its 7 × 7 glyph, frontage, anchors and variants), one of the hand-made archetypes (amenity=bankis the bank), a facility (a prop:amenity=benchis the city kit's bench), a region's landuse area, a district's landuse, or nothing (skip, with why).@foi/catalogue'sosm/recipes.tschecks it against the snapshot.assets/blender/kits/osm.py(withosm_forms.pyandosm_parts.py) builds the kits from the recipes, headless (blender -b --factory-startup --python assets/blender/kits/osm.py -- --out apps/level-editor/assets/kits), and writespackages/catalogue/src/osm/buildings.generated.json, which the catalogue (GENERATED_BUILDINGS) and the town generator (cityparts.py, after its own tables) read. A building is named after its tag (shop=bakery→shop-bakery, its piecesbld-shop-bakery-aand-b) and carries its glyph on its roof, read from the game's camera above.
A district names them as it names any building, by name or by tag ("shop=bakery": 1), or by a wildcard whose weight its buildings share by how common each is in the world: shop=* (every building with a shop tag), family:industrial, landuse:retail. A district given a landuse (retail, commercial, residential, industrial, civic, leisure, farm) and no mix of its own is built of that landuse's buildings (landuse:<it>). The themes and the towns that name none of this are the same, byte for byte. test-osm-buildings is their level: a 3 × 2 town whose six districts are a high street, a farm village, works, homes, a street of shops (shop=*) and a leisure quarter, generated from its spec (apps/level-editor/assets/kits/test-osm-buildings.spec.json) like test-generated-town.
Scene names
A scene's name is the level's name everywhere (the folder assets/<name>.scene, the level file, FOI_LEVEL), and it says what the level is for:
| Name | Means | End-to-end tests |
|---|---|---|
test-<words> | A test level: a scenario the tests need (test-core-gameplay is the POC city with its ramps, garage and shooting range) | Required: a folder tests/e2e/specs/levels/<name>/ with at least one tagged spec, run by the core tier on a server of its own. test-core-gameplay is also the fixture of every unit test and is frozen by packages/world/src/levels.test.ts |
main-<words> | A level that ships to players | Optional folder; every exported level is boot-tested by tests/e2e/specs/levels.spec.ts |
test-mission-verbs is the mission verbs' level: an avenue with a street off it for each mission of its bundled game (packages/missions/content/test-mission-verbs.json: a give to a person and into a crate, a photograph, a wait, an escort, chases on foot and by car, escapes from the police and from actors, a destroy, a parallel group), the diner's road ending at a hydrant in its lane and the market street running north straight and clear for a chase; built from its recipe, its contract in packages/world/src/levels.test.ts. test-breakables is the light pieces' level: two straight roads on open ground end a few metres short of hydrants, a long one to reach 60 km/h on and a short one that cannot; built from its recipe like the other test levels. test-collisions is the collisions' level: an open road for cars to ram the player's on, a road along a wall 0.3 m from a car's driver's door (wall-side) and one between two walls 0.3 m from either flank (alley), for getting out where it is free; built from its recipe, its contract in packages/world/src/levels.test.ts. test-safe-zones is the griefing controls' level: a plaza with both spawns, a street leading north out of it and planters at the plaza's corners, built from its recipe, its bundled game (packages/missions/content/test-safe-zones.json) making the plaza a safe zone 14 m across; its contract in packages/world/src/levels.test.ts. test-police is the police's level: a two-way road loop (its corners cut at 45°) around an office and a warehouse that hide one side of the block from the other, the players' spawns on the south road and three range dummies on its outer pavement to commit crimes near; built from its recipe, its contract in packages/world/src/levels.test.ts. test-drift-pad is the level of what the tyres leave behind (tyre marks, smoke, dust, a landing's dust, hit sparks) and of the seats: an asphalt skidpad 44 m across with a painted circle, a dirt patch with a dirt track across it, a run up to a small ramp and a shipping container to shoot at; assets/blender/levels/drift_pad.py writes its own kit (the pad, the patch, the roads' markers) and its recipe, then level:recipe and level:export as for test-curves. test-city is the city template's level: a 320 m town on a river's south bank (a grid of two by two blocks turned 15°, its avenue running east to a roundabout of three arms, a street from it north over the river's one bridge, a quay along the south bank and a road along the north one, a crescent, a close, a knoll with a lane climbing to its lookout, three districts in Riverbend's styles, a paved square), generated from its spec (apps/level-editor/assets/kits/test-city.spec.json, also the catalogue's example of the template) with npm run level:city, then level:recipe and level:export; change the spec, the solver or city.py, never the scene, and keep its contract in packages/world/src/levels-city.test.ts green. test-generated-town is the generator's level: a 3 × 2 grid town (an avenue, two districts, a park) generated from its spec (apps/level-editor/assets/kits/test-generated-town.spec.json, see Generating a level from a spec); change the spec or the template and generate again, never the scene; its contract is in packages/world/src/levels.test.ts. test-ai-pathfinding is the AI's level: a 120 m road loop with a cross street and a one-way spur ending at a wall, four blocks inside the loop, and north of it a pillar field, a zigzag of walls with gaps, a low wall, a deck with a ramp and stairs, and four small blocks around a 4 m alley cross. It is built from its recipe (npm run level:recipe -- apps/level-editor/assets/kits/test-ai-pathfinding.recipe.json --scene test-ai-pathfinding), never edited in the editor; its contract is in packages/world/src/levels.test.ts too, and so is test-core-gameplay's. test-compound is the walled compounds' level, the proof that a creator's spec can ask for one: a 3 × 2 grid town of 110 m blocks (an avenue, four themed districts, a park) round Zenful Campus, a compound on the middle block north of the avenue with its gate on the avenue, its delivery gate east, the gap west, the annex across its north wall and high security; generated from its spec (apps/level-editor/assets/kits/test-compound.spec.json) like test-generated-town, its bundled game (packages/missions/content/test-compound.json, "Zenful Campus") a test game: the night's clock (21:40, at half pace), a loadout (the tranquilizer and the pistol), a few places and no missions, so the lab's sliding door stays locked by a keycard (the lab's guard carries one); change the spec, the template or the kits, never the scene, and keep its contract in packages/world/src/levels-compound.test.ts green. test-curves is the first hillside's curves, kept for driving feel on what a region's roads cannot make (a one-way roundabout, a crescent, a sinuous park drive, a wavy boulevard, a tunnel whose roof fades, a serpentine of hairpins up to a lookout, a bumpy dirt rally track, a bridge over a gully); it has no game. It is generated in Blender: blender -b --factory-startup --python assets/blender/levels/curves.py writes its kit and test-curves.recipe.json into apps/level-editor/assets/kits/, then npm run level:recipe -- apps/level-editor/assets/kits/test-curves.recipe.json --scene test-curves and npm run level:export; change the generator, not the scene, and keep its contract in levels.test.ts green. The generator stands on assets/blender/levels/citygen.py: roads are polylines with a height profile (pinned to the roads they meet, the grade limited and eased near junctions and bridge ends, no bend tighter than the road's half-width allows), junctions get rounded kerb corners built on the road edges themselves, and the whole ground is one constrained Delaunay triangulation in which every road band, corner, zone (pavement, plaza, park, pond, lot) and the tunnel's cutting is a constraint, each layer at its own height, a vertical wall where two meet at different heights (a kerb, a retaining wall, an abutment, a pond's bank): one mesh with no surface over another, so nothing flickers, and the client's ribbon of a road lies exactly on the ground built for it (generated roads keep every point of their centre line, thin false). Buildings and props are placed on an occupancy map of footprints, never touching each other or the pavement.
test-stealth is the stealth level (see Stealth): a walled compound north of its spawn with a gate locked by a keycard, a camera over the wall beside it, a gatehouse and a yard behind an inner wall, the gatekeeper at a post west of it with the keycard in its pockets, a guard patrolling the yard, two guards facing each other in a yard to the east, darts beside the car; built from its recipe out of blocks (walls, a door, a camera, NPC spawners, a pickup), its contract in packages/world/src/levels.test.ts, its game's Clean Exit; its end-to-end server runs without pedestrians.
test-on-foot is the people's animations' level (see Animation), directions as the player sees them (north up the screen, east to its right): a flat yard around the spawn, which faces north; west of it three ledges 1, 2.5 and 5 m high, each up its own ramp from the south, with open ground north of them to fall onto; east of it a low and a high wall for cover and, further north-east, a room with a door; an item to pick up and a crate a few steps north, darts south-west, a bicycle, a motorbike and the bus parked south-east; a guard at a post south-west facing west, one patrolling north-west, one south-east facing east to put to sleep; a dummy straight north in the car's way and one north-east of the spawn to punch. Built from its recipe out of city kit ramps and terraces and blocks, its contract in packages/world/src/levels.test.ts; its game (packages/missions/content/test-on-foot.json) adds a coach to talk to and a kiosk, south-west of the spawn; its end-to-end server runs without pedestrians.
test-inventory and test-interaction are the items' levels (see Items): the first lays a first-aid kit and a crate of cash a few steps from its spawn (the player's car serves for the trunk), its game's Parcel Run three parcels to collect and deliver; the second walls a room in north of its spawn, its front door locked by the office key lying beside the path (an item its game defines), an unlocked side door and a locker inside. Both are built from their recipes out of blocks (walls, doors and containers, the doors' and containers' texts in each placement's params), their contracts in packages/world/src/levels.test.ts; their end-to-end servers run without pedestrians. test-phone is the phone's level (see Phone), built the same way: an office walled in north of its spawn with one door, which its game's door actor locks by a keypad's code; its game puts a contact to talk to, a kiosk and a rental car on the open ground around it. test-locks-garages is the locks' and garages' level (see Vehicles), built the same way: a street with three locked cars of its vehicle spawners on its north side (a key lying by one, another's in a crate, a range dummy watching the third), a garage south of it with a driveway into its bay, cash by the spawn; its end-to-end server runs with police and without pedestrians.
test-race is the races' level (see the City guide): a closed circuit of eight roads around a 120 × 140 m infield (four straights and four cut corners, 12 m wide), the players' spawns on the south road west of the finish, facing east, in the lane beside the rivals' grid; its game puts four race gates at the middles of the straights and races the player against two rival drivers. test-city-life is the city's blocks' level: a ring road with an avenue and a street crossing under four traffic lights at the origin; spawn points of every role (the players and a hospital south-west of the cross, the police and the traffic on the ring, the guards in a compound); a walled compound south-east behind a gate locked by a keycard (the keycard outside), a car park north-west behind a barrier locked by a parking ticket, a shop's counter and $100 by the players' spawn, a garage west of them, the apartments' door on the street's north pavement; a shop clerk on a schedule, a taxi driver on a route round the ring, a target with its refuge at the apartments' door, a range dummy; short one-way drives (the gate's, the barrier's, the garage's, the lawn's) for the legend's Go to. Its game starts the clock at 08:00, restricts the compound's yard (the alert) and a lawn by the dummy (the Heat), has a VIP to drive to a hotel and a CityOS app that switches the lights. Both are built from their recipes, their contracts in packages/world/src/levels.test.ts; the race's end-to-end server runs without pedestrians, the city's with police and eight ambient cars.
test-traffic-junction is the traffic's level: a 200 × 160 m loop of road with chamfered corners, an avenue and a street crossing at the origin under four traffic lights (downtown/traffic-light, one at each corner, its arm over the approach on its side), and four T junctions where the avenue and the street meet the loop, without lights; four buildings in the quarters and two spawn points on open ground beside the cross, off the roads; built from its recipe, its contract in packages/world/src/levels.test.ts. Its end-to-end server runs sixteen ambient cars and no pedestrians (tests/e2e/support/levels.ts). test-new-vehicles is the city's vehicles' level: one straight two-way street 160 m long and a row of four spawn points on open ground east of it, far enough apart for a bus or a fire truck beside a car and the semi's trailer behind; built from its recipe, its contract in packages/world/src/levels.test.ts; its end-to-end server runs without pedestrians. Every level with paved roads has pavements for its pedestrians, laid from the roads (see AI): leave a couple of metres of open ground beside a street for them, and face a building's front toward the street to give it a door a shopkeeper keeps. A traffic light placed within 20 m of a junction rules it (see AI): place one at each corner, its arm over the lanes coming in.
A district of a spec takes a theme, one of eight satires (packages/catalogue/src/themes.json, which the generator reads too): tech-campus, wellness-quarter, luxury-waterfront, gig-depot, old-town, city-hall, high-street (a market town's shopping street of small shops of every kind) and farm-village (farms, barns, a chapel, a pub, set back and sparse). The theme gives the district its default mix and density, a title (one of its own, the seed's pick), the invented names of its buildings (<brand> <noun>: "Dashr Fulfilment Hub", "Provisional City Hall"), the looks its buildings' variants lean to (a variant's look in its kit's sidecar) and the pedestrians' roles it suggests (written into the level's districts for the crowd). Its density, field by field over the theme's: built (0.2 to 1) is the share of a street built, the walk skipping the rest after each building and trees filling the gaps; setback (0 to 10 m) puts the fronts behind the pavement, the lots laid as lawns; height (low, mixed, high) leans the mix to buildings up to 13 m or from 17 m. Its landmarks are buildings it must have, each once, placed before its mix wherever its streets have room (the avenues first; one without room is reported). Its title is what the minimap writes over it. The names come from a random stream of their own, and a spec without the new fields draws the same numbers as before. Where a building's anchor carries a block, the generator puts it there: a garage in a body shop's open bay (respray $150, storage free), vehicle spawners of locked cars on a car rental's lot, a depot's van bays and a car park's bays, a shop at a supermarket's counter (snacks and first-aid kits), a gas station's (snacks and a jerry can) and each of a shop row's three (snacks, first-aid kits, parking tickets), a spawn point of the role hospital at a hospital's respawn anchor and of the role police on a police station's two car bays (see the City guide), a door (a gate 4 m wide locked by keycard:vault) in a bank's vault and a security camera in its strongroom. A building may name parts in its kit's sidecar, pieces placed with it at its origin and turned as it is: a bank's upper floors, which fade over a player in its hall. Every block of the grid becomes a district block (block:district), so the level's districts hold each district's name, title, theme, roles and areas (the minimap tints and titles them).
A compound (compounds, assets/blender/levels/compounds.py with the compound kit, assets/blender/kits/compound.py) is a block of the grid behind a wall: { "name", "block": [i, j], "title", "theme", "gate", "delivery", "gap", "roof", "security" }. gate is the side of its main gate (north, south, east or west: pillars, a barrier and a door for people on foot, both locked by keycard, a guardhouse, a drive to the forecourt); delivery and gap, the two walls beside the gate's, a sliding delivery gate (keycard) with a loading shed, its dock and a locked van, and four metres of wall fallen in; roof (false by default), a two-storey annex built across the back wall with fire stairs up its outside and down its inside; security (low, medium by default, high), how many cameras watch it (the gate's and the dock's, the forecourt's from medium, the lab's at high) and how many guards (the lab's and a clerk from medium, a round of the yard at high). Inside: the headquarters at the back, a forecourt and its sculpture, a lab one walks into on the gap's side (its door a gate locked by keycard, its roof a part that fades over a player inside, the prototype in its pod), a car park of locked cars, the guards' break shelter, hedges and planters for cover. Its guards are NPC spawners on schedules by the room clock (Stealth): two gatekeepers in day and night shifts and the dock's guard, then by its security the lab's (a keycard), a clerk in office hours and a round of the yard, each guard's break a quarter of an hour once an hour at a minute of its own. Its places are <name>.gate, .gap, .roof_stairs, .lab, .lab_pod, .post_lab, .break_lab, .camera_<k> and the rest the module lists, and its block is a district of its own (its title on the minimap, its theme). A compound needs blocks of 104 m or more (its wall of 60 to 100 m behind a lawn); it may not be a park or another compound's block, and its gap is not its delivery's wall. Every piece goes on the town's occupancy map (one that does not fit is an error), the streets outside keep their trees clear of the gates, the gap and the annex, and the wall's openings are 4 m, which the navigation grid's 2 m cells let a guard walk through.
test-buildings is the buildings' level: a 3 × 3 grid town of 120 m blocks built from its spec (apps/level-editor/assets/kits/test-buildings.spec.json), six districts of the six themes (the city hall on the west blocks, the tech campus south of the middle and in the north-east, the gig-economy depot district on the east blocks, the old town in the middle, the wellness quarter in the north-west, the luxury waterfront north of the middle), every building of both kits at least once as a landmark, its main street the 16 m avenue road-ew-1 between the middle and southern rows, with a building of every family along it; its contract in packages/world/src/levels.test.ts (every building with its tags and name, the districts, the setbacks, every anchor a place, the garage and the parked cars where the anchors say); its end-to-end server runs without pedestrians.
main-riverbend is the level that ships and the server's default, the flagship city, the level of "Smart City Pilot": a city drawn freely, generated in Blender by a script of its own (assets/blender/levels/riverbend.py: its network, its districts and its parks, built by city.py, the engine the city template builds every city with). Its spec, which the city solver draws road for road within a few millimetres of the script (packages/catalogue/src/city/fixtures/main-riverbend.spec.json), will replace the script. Four modules make it:
- The network (
network.py): every street is a polyline drawn where it should run (a spline through a few points, an arc, a bank of the river set back) and cut onto the roads it meets, so nothing has to be computed by hand:net.road(name, points, style, start_on, end_on),net.spanbetween two roads,net.loopfor a ring in parts,net.roundabout(arcs between its arms, one way),net.cul_de_sacwith its turning circle.net.problems()says what would break the ground before anything is built: a bend tighter than the road is wide, two junctions less than 16 m apart, two roads meeting at less than 35°. - The city (
city.py): a plan (its roads, roundabouts, rivers, hills, districts and their styles frompackages/catalogue/src/city/styles.json, parks, places) built as the next two say: a city spec's plan from the solver, or Riverbend's fromriverbend.py. - The ground and the buildings (
citygen.py,cityparts.py): one conforming triangulation in which every road band, rounded corner, pavement strip, square, park and the river's channel is a constraint, so no two surfaces overlap; bridges where a road is over the water, a deck between abutments; the buildings walked along every street in turn, the landmarks first, then the big ones, a closer walk, an infill of the narrow ones and the outbuildings behind the fronts, a kind counting for less in a district the more of it the district has. - The life (
citylife.py): per district, the trees along the pavements, the furniture by the kerb every dozen metres, bus stops, car parks, building sites, container yards, cars on the houses' drives, what stands in the yards; boats, jetties and reeds on the river; roadworks, billboards, and paint (stop lines, manholes, cycle lanes); and theurbankit's pieces: what the roofs carry (the kit library gives every building with a flat roof the free spots on it,roofinkits.generated.json, measured byapps/tools/src/roof-spots.ts: the height of its highest large flat and up to eight circles clear of its parapets, penthouses and signs;rooftopsfills four in five of them by the building's family and its district's lean, air conditioners and vents everywhere, dishes and chimneys on the houses, cooling towers on the works, a pool or a helipad once on a roof wide enough), barges, tugs, fishing boats and buoys on the harbour's reach, cranes on its quay, a statue, a carousel and carts on the squares.
The city: a river in from the west, north round a peninsula and out to the south-east; the Old Town on the peninsula (a round market square, seven bowed lanes, a wandering ring); downtown south of it, a grid turned 17° off the map's axes and cut by a boulevard to a roundabout; Eastside, a garden suburb of crescents and closes, the weavers' quarter and the Tanneries across the river; the Docks on the harbour's basin; the Heights' streets either side of a hill with a serpentine up to a lookout; the Common; a parkway round it all and seven bridges. 86 roads, 16.7 km; thirteen districts; about 900 buildings of 155 kinds; 8,007 placements, 943,898 placed triangles. A level's ground is never under 0 (its base is a floor there), so the generator works with its streets at 0 and its river's bed below them, and lifts the whole city 4 m as it writes the recipe: the river is one sheet of water under the whole level, the ground over it its banks. blender -b --factory-startup --python assets/blender/levels/riverbend.py (about three minutes; RIVERBEND_DRAFT=1 leaves the buildings out), then npm run level:recipe -- apps/level-editor/assets/kits/main-riverbend.recipe.json --scene main-riverbend, npm run level:export and npm run level:previews -- main-riverbend. Change the generator, never the scene, and keep its contract packages/world/src/levels-riverbend.test.ts green; its game's places are held to the generated ones by packages/missions/src/content.test.ts.
Words are lower-case letters and digits joined by dashes. npm run level:new and the export refuse any other name, and level:check reports it. The default level, the one a server runs with no FOI_LEVEL, is main-riverbend by name (DEFAULT_MAIN_LEVEL in packages/world/src/level-names.ts), else the first main-* in name order, else the first test-*: a new main-* level never takes Riverbend's place by sorting first.
The registry packages/world/levels/index.ts is generated by the export (one import per level file, and the default) and is the entry @foi/world/levels: the server and the tests import it, the client never does. packages/world/levels/ belongs to the pipeline: do not edit or delete there by hand. To rename a level, close the editor, rename the scene folder, rewrite the meshes/*.json files' delayLoadingFile paths (they name the folder) and project.bjseditor, then export: the export removes what the old name left behind. To delete one, delete the scene folder and export. An ad-hoc .json level for FOI_LEVEL=<path> lives anywhere else.
The loop
npm run level:play -- test-core-gameplay # export, start the game on the level, re-export on every savelevel:play exports once, starts npm run dev:web with FOI_LEVEL=test-core-gameplay and watches the editor project: each save in the editor re-exports the level (one line per scene, or the list of problems, each naming its node); reload the page (the server restarts on the new level file since node --watch picks up the JSON) to drive the change. By hand:
| Command | What |
|---|---|
npm run level:export [-- <scene>] | Export every scene folder (or one) to packages/world/levels/<name>.json and rewrite the registry. No editor, no CLI: the scene folder is read as it is saved. |
npm run level:watch | The same, again after every save. |
npm run level:check | The gate (also in validate and CI): every scene is named by the convention, every committed level file is what its scene folder exports (byte for byte), every level is playable, nothing is left from a gone scene, the registry is current, the skill file's two copies agree, the kit library is what the kits give, every level passes checkLevel (its lint, its budgets, no road under water). |
npm run level:port -- test-core-gameplay --scene test-<name> | Build an editor scene from a bundled level or a .json level file, the kits its pieces come from installed; a road of more than two points is ported as a chain of slabs. |
npm run level:new -- test-<name> | An editor scene with the blocks, a 100 m ground and one spawn point; the name must follow the convention. |
npm run level:recipe -- <file.recipe.json> --scene <name> | Build an editor scene from a recipe: its kits installed under _kits, one instance per placed piece or block, its roads, spawns, strips and dummies (see Kits and recipes). |
npm run level:dress -- <level> [--out <file.recipe.json>] | The recipe that places the city kit's pieces at the coordinates of a block level's blocks, for level:recipe. |
npm run kits:export | The kit library (packages/catalogue/src/kits.generated.json): every piece of the shared kits with its footprint, tags and anchors (and a building's roof: its height and the free spots on it), for the catalogue (see Kits and recipes); after regenerating a kit. |
npm run level:blocks | assets/blocks/library.babylon: the eighteen sources alone, to drag into a scene that lacks them. |
npm run level:editor | Open the project in the editor, found where its installer puts it or at BABYLONJS_EDITOR_PATH; prints the download link otherwise. |
Commit the scene folder, the level file and the registry together; level:check fails when they drift.
In the game
The server picks the level with FOI_LEVEL: a bundled name (any exported level: main-riverbend, the default, test-ai-pathfinding, test-breakables, test-buildings, test-city, test-city-actions, test-city-life, test-collisions, test-compound, test-core-gameplay, test-curves, test-drift-pad, test-fire, test-generated-town, test-interaction, test-inventory, test-landmarks, test-locks-garages, test-mission-basics, test-mission-logic, test-mission-verbs, test-new-vehicles, test-on-foot, test-osm-buildings, test-phone, test-plots, test-police, test-race, test-safe-zones, test-stealth, test-storeys, test-tiles-roads, test-tiles-terrain, test-tiles-town, test-tiles-tunnel, test-traffic-junction, test-vertical, test-water or test-weather today) or the path of a .json level file (FOI_LEVEL=/path/to/my-town.json npm run dev:server; a relative path is taken from apps/server, where the npm scripts run the server); an unknown name exits listing the names. /health reports the level, the HUD's connection line names it, and window.__poc().level exposes it to the end-to-end tests. What to try on each level by hand, and where its things are, is in Testing by hand.
The client draws the level from the data it received: every placed mesh flat-shaded in its colours under a sun and a sky light, the roads as ribbons along their centre lines (asphalt, or brown for a road whose surface is dirt) with their paint (edge lines, a dashed centre line or a solid yellow one between two carriageways of two lanes or more, dashed lane lines, all stopping where they enter another road), the nail strips, and a dark ground with a grid just under y = 0 unless a terrain piece of the level reaches every edge (its own ground then shows, a pond below y = 0 included). A structure held over the ground (a tunnel's roof, a bridge's deck, a car park's deck) is drawn see-through while the player is under it. The legend's Go to (View tab) puts the player on any road of the level, halfway along it in its right-hand lane, on a server whose debug commands are on (the teleport debug command). The Colliders box on the legend's View tab (and ?colliders=1 in the page's URL) draws the collision meshes as a wireframe over the level (green terrain, teal structures, blue buildings, orange props; the treads of stairs and road surfaces have none), to see what the physics sees; __poc().colliders exposes it.
AI assistance
- In the editor: enable the experimental features in the editor's preferences and open the AI assistant panel; it starts Claude Code (or another agent) in
apps/level-editorwith the editor's MCP server (scene hierarchy, instances, transforms, scripts, screenshots, batches) and installs the editor's skills. The repository's skillfive-on-it-levels(apps/level-editor/.claude/skills/, a copy of.claude/skills/) tells the agent the pieces, the units, the mirror rule and the recipe (get_scene_hierarchy→create_instancebatches →save_scene→npm run level:export→npm run level:check). - In the repository:
.mcp.jsondeclares the official Babylon.js node-editor servers (node material, node geometry, render graph, particles, GUI, flow graph, smart filters, from@babylonjs/mcp-servers, a root dev dependency) for Claude Code, approved per project;.vscode/mcp.jsonthe same for VS Code. Blender is driven through its own MCP server (see the README): the kit pieces are modelled there.
Troubleshooting
level:checksays the level file is not what the scene exports: the editor saved after the last export;npm run level:export.- is rolled / is scaled / is pitched: the gizmo turned or scaled something the level format cannot hold; reset that axis in the inspector (roll is never allowed; spawns, dummies and strips are not scaled across; only boxes and roads pitch).
- is not on the ground: a road hangs over or sinks into what should carry it (a ramp's foot, a terrace), or a spawn is not about a metre over its ground; pitch the road to the slope, or move it.
- its group … is scaled: a transform node was scaled; set it back to 1 and scale the pieces instead.
- a second ground / the ground must be centred: one ground, at the origin, square.
- its source … is not a block or a kit piece: a mesh without a block script and without
metadata.foi; attach the script, or install the piece from its kit. - Mirrored: a position typed from a level file went in without negating
x(and the yaw). - is not a valid level name: scenes are named
test-<words>ormain-<words>; rename the scene (see Scene names) and export. - no scene of that name / index.ts is out of date: a scene was renamed or deleted;
npm run level:exportremoves what it left behind and rewrites the registry. Cannot find module …/levels/<name>.json: a level file was deleted by hand while the registry still imports it; restore it (git checkout) or remove its import line frompackages/world/levels/index.ts, thennpm run level:export.