Skip to content

Five On It — Technical design and delivery plan ​

Companion document: GAME_DESIGN.md (the GDD). This file is the engineering source of truth. When the two disagree, the GDD wins on what, this file wins on how. Code and guides cite its sections by number (§4, §5.4, §6.2, §6.4, §10, §11): keep the numbering when editing.

Status words: built (in the repository today, version 0.18.0), partial, planned. Estimates are stated in non-blank lines of code as npm run loc counts them (§11). Decisions are listed in §13.


1. Summary ​

  • What: a top-down, satirical open-city multiplayer sandbox, and a platform on which players create playable games (a level and its missions) by talking to Claude in Claude Code with the five-on-it MCP integration. One curated game ships today ("Smart City Pilot", on Riverbend) and V1 is to ship five; players make the rest (GDD §1, §11).
  • Simulation: fully 3D rigid-body physics (Rapier) driven by an ECS (bitECS). The same simulation code runs on the authoritative game server and in every client for prediction. Built.
  • Presentation: Babylon.js from a steep top-down camera; low-poly glTF models and kit pieces made in Blender. Built.
  • Client: one client, packaged as an Electron app and served to the browser. Built (the browser build is the development path today).
  • Game servers: headless Node processes running Rooms, each one a game being played. Today one process runs one level (built); rooms are planned.
  • Creation platform: a platform API (drafts, games, builds, validation, rooms, moderation), the MCP server on top of it, generator workers that run headless Blender, Postgres, Redis and object storage. Accounts, friends, parties and chat run on Nakama. The creator's own Claude authors in Claude Code; the platform may also run one tool-calling model, through OpenRouter or on Anthropic's API and under a hard monthly cap, for the game's owners on Steam who create in the game (decided 2026-10-16, planned: §9.8). Planned.
  • Language: TypeScript 6, strict, ESM, run directly by Node through type stripping; Python inside Blender for the kits, the models and the level generator.

2. Non-negotiable technical requirements ​

RequirementConsequence
Node 24+, npm workspaces, ESM, TypeScript 6 strict (noUncheckedIndexedAccess, exactOptionalPropertyTypes, erasableSyntaxOnly)Workspace packages resolve to src/index.ts; no build step for them. TypeScript stays on 6 while typescript-eslint and typedoc need its JavaScript API.
Identical simulation on client and serverA single shared packages/sim; one pinned Rapier version; fixed timestep; no Math.random or Date.now inside systems.
Server-authoritative multiplayerClients send inputs only. The server never accepts a position from a client.
Everything a player makes is dataLevel specs and missions are documents validated against schemas (Zod). No code a player or an AI writes ever runs, anywhere: the Blender worker runs only the repository's scripts, with the spec as input.
Creation goes through the MCP toolsThe MCP server's tools are the contract, for Claude Code (Claude's usage is the creator's subscription) and for the one model the platform is to run for the game's owners on Steam, under a hard monthly cap and never on a player's key (decided 2026-10-16, planned: §9.8).
Every published game plays the same foreverA game pins the versions of the block catalogue, the generator (with its Blender version) and the server it was built with; artifacts are stored by content hash.
Moderation is pluggableOne interface, chosen by configuration; off by default in local development (§9.9).
Content has budgets, code has a counterRooms enforce budgets (NPCs, vehicles, items, triangles). Code size has no limit; npm run loc counts it (TypeScript, JavaScript and Python) and every estimate is stated in its lines.
Every block is tested where it is usedA block ships with a mini test level and an end-to-end test (§10.2).
Quality gatesnpm run validate passes locally and in CI before merge (§10.4).

3. Stack ​

LayerChoiceStatus
RuntimeNode 24 (servers, tools), Electron 44 (desktop)built
LanguageTypeScript 6 strict, ESM, type stripping; Python in Blenderbuilt
ECSbitECS 0.4built
PhysicsRapier 3D (@dimforge/rapier3d-compat 0.21.0), exact version with a root overrides entrybuilt
RenderingBabylon.js 9built
UIReact 19 + i18next, plain HTML over the canvasbuilt
AudioHowler: a sound bank of events (sounds.json, files found by name, placeholders synthesized in code) and the radio (<audio> elements, stations live on a clock)built
BundlingVite (renderer, browser build), electron-builder (installers)built
TransportWebSocket (ws) for the reliable frames; a WebRTC data channel (werift in a worker thread on the server) for inputs and snapshots, the WebSocket its fallbackbuilt
SerializationHand-written binary codecs for inputs and snapshots; JSON validated by Zod 4 for everything elsebuilt / partial
Assets and levelsBlender 5.2 LTS (assets/blender: kits, models, level generators), the Babylon.js Editor for hand placement, apps/tools for the pipelinebuilt
Level generatorThe same Blender, headless (blender -b), in a worker containerplanned
Accounts and socialNakama (Heroic Labs, open source, self-hosted): authentication (device for guests, email, social providers), sessions, friends, parties, chat, notifications; the @heroiclabs/nakama-js clientplanned
Platform APIFastify, OpenAPI generated from the Zod schemas; also serves the web pages and the browser client (one origin)built
MCP serverThe official MCP TypeScript SDK, Streamable HTTP transport, OAuth 2.1 authorization; the client is Claude Codebuilt (OAuth 2.1 in 0.54.0)
DatabasePostgreSQL: one server, a database for Nakama and one for the platformbuilt (the Compose stack)
Job queueThe platform's own store: a jobs table claimed with FOR UPDATE SKIP LOCKED, leases renewed while a job runs, a fair order by owner (apps/api/src/queue.ts)built (builds)
Rate limitsRedisplanned
Object storageS3-compatible (MinIO locally), content-addressed level artifactsplanned
ModerationA pluggable Moderator: off, a word list, a Claude classifier (a small Claude model through the Claude API), or a chainplanned
TestsVitest (unit, integration), Playwright (smoke, core), Artillery (load)built / planned
Docstypedoc → VitePress, Mermaid diagramsbuilt
Packagingelectron-builder; the Release workflow publishes installers and the server bundlebuilt
InfraDocker images for api, mcp, server and worker (with Blender); Compose locally and for the first host, with Nakama, Postgres, Redis and MinIOplanned

4. Monorepo layout ​

five-on-it/
├── package.json  tsconfig.base.json  eslint.config.ts  vitest.config.ts  playwright.config.ts
├── build/                      # loc, e2e-plan, release, sast, dast, editor, pack-server
├── documents/                  # GAME_DESIGN.md, TECHNICAL_SPECS.md
├── docs/                       # VitePress guides; docs/api is generated
├── assets/blender/             # the geometry factory (Python, run in Blender): kits, models, level generators
│   └── levels/                 # citygen.py, curves.py; the spec-driven generator (generate.py)
├── tests/e2e/                  # Playwright specs, one folder per level; TEST-PLAN.md
├── tests/load/                 # (planned) Artillery scenarios
├── apps/
│   ├── client/                 # Electron + browser client: Babylon renderer, React overlay, client sim loop
│   ├── server/                 # authoritative game server; rooms (planned)
│   ├── tools/                  # level pipeline CLI, model hulls, kit library export
│   ├── level-editor/           # Babylon.js Editor project: hand-placed levels
│   ├── api/                    # platform API, the MCP's OAuth, web pages, the browser client's origin
│   ├── mcp/                    # MCP server: the creation tools over the API, for Claude Code
│   ├── worker/                 # build jobs: runs the generator in headless Blender, outputs → level data
│   └── nakama/                 # (planned) Nakama configuration and its runtime module (the chat moderation hook)
└── packages/
    ├── shared/                 # tick constants, prng, hash, math (SHARED)
    ├── world/                  # level data, ground queries, road graph, navigation grid, level registry (SHARED)
    ├── sim/                    # bitECS components and systems, Rapier (SHARED)
    ├── protocol/               # binary codecs, JSON messages, Transport (SHARED)
    ├── net-client/             # prediction, reconciliation, interpolation (client)
    ├── ai/                     # NPC brains (SHARED code, run by the server)
    ├── catalogue/              # (planned) the block catalogue and the level spec schema: definitions, docs for Claude (SHARED)
    └── missions/               # (planned) mission schema and interpreter (SHARED)

Rules:

  • packages/* are isomorphic: no DOM, no Node APIs, no Babylon or Electron imports; ESLint no-restricted-imports and the empty types list enforce it. The catalogue and the mission interpreter are isomorphic too; the generator is Python in Blender and runs only on workers (§9.3).
  • apps/* may depend on packages/*; packages never depend on apps.
  • Workspace packages resolve straight to src/index.ts; imports use explicit .ts extensions.

5. Simulation (packages/sim) ​

5.1 Loop ​

  • Fixed timestep 60 Hz. Server and client both run step(world, inputs).
  • The client renders at display rate, interpolating between sim states.
  • No wall clock inside systems. Time is the tick; randomness comes from createPrng in packages/shared.

5.2 Components ​

Built: transforms and velocities copied from Rapier, vehicles (class, wheels, drivetrain state, boost, damage), characters (stance, cover, seat, health, tumble), players and their inputs, weapons and shots, breakables, nail strips, NPC state for the AI. Planned, with the blocks that need them:

ComponentHoldsFor
Owner, Lockowner (nobody, player, NPC, mission), locked, key taglocks, keys, carjacking (GDD §6.3)
Seatseat anchors and poses per vehicleseats (GDD §6.3)
Inventory, ContainerOfitem slots, the container an item is initems and containers (GDD §7.4)
Interactablekind, prompt, range, conditionsthe interaction key (GDD §7.2)
Statusawake, stunned, knocked out, asleep, down, dead; timersnon-lethal play (GDD §8)
Awarenessalert phase, last seen, suspicionstealth (GDD §8)
Wantedheat, stars, last seen by policethe police (GDD §12)
MissionActormission, actor idthe mission interpreter
Replicatedpriority, last sent tickarea of interest (§6.3)

Room-wide state (not per entity): the environment (the clock and the weather, [NEXT], GDD §5.7), which the vehicle system reads for grip and the AI for sight; the mission state; items, contacts and apps, which are data rather than physics. All of it is owned by the server and hashed with the ECS.

5.3 Systems, in order ​

ai            (server) packages/ai: NPCs perceive, decide and plan within a budget, then write an input
input         apply inputs → vehicle and character intent
interaction   (planned) the interaction key: range and facing checks, then the interactable's action (carjacking included)
vehicle       Rapier vehicle controller: engine, brakes, steering, drift, wheelspin; grip from the weather (planned)
character     on-foot movement, cover, seats, statuses
physicsStep   world.step(), then poses into the components
collision     contacts → damage, health, knockdowns, breakables, heat
environment   (planned) the clock and the weather advance
mission       (planned) packages/missions: rules, objectives; writes commands and events (§9.5)
traffic       (planned) ambient traffic and pedestrians around players (profiles on packages/ai)
police        (planned) heat → stars → units
lifetime      decrement, destroy
replication   (server) snapshots per client, area of interest

5.4 Determinism contract ​

  • Same packages/sim build, same Rapier version, same settings; all integration parameters set once in createSimWorld.
  • The gates: packages/sim/src/determinism.integration.test.ts runs two worlds through one input log and compares state hashes; packages/ai has its own (hashAiState). The mission interpreter gets the same kind of gate; the generator gets one too (same spec, seed and Blender version, same artifact hash; §9.3).
  • Determinism is required between client and server on the same Node and Chromium majors; reconciliation corrects small drift elsewhere.

5.5 Vehicle handling ​

The model is in docs/guide/driving-model.md and the tuning in packages/sim/src/vehicle-classes.ts; vehicle.test.ts pins the envelope. Built: a Rapier ray-cast vehicle per class with drivetrain and engine placement, traction budgets and wheelspin, handbrake drift, boost; chassis colliders are the convex hulls of the models (npm run models:hulls, checked by models:check).

6. Networking ​

6.1 Model ​

Server-authoritative, with client-side prediction and reconciliation for the local player and interpolation for everyone else.

client  --(60 Hz) Input{seq, tick, fields}--> server
server  --(20 Hz) Snapshot{tick, lastSeq, entities[]}--> client
server  --(reliable) Events{mission, inventory, phone, prompts}--> client   (planned)

Chat, friends, parties and invitations do not go through the game server: they are Nakama's (§9.6).

6.2 Client prediction (packages/net-client) ​

  1. Apply local input at once to the controlled entity (the character on foot, the car it drives), step the local sim, keep the input pending.
  2. On a snapshot: set the local entity to the server's state, drop acknowledged inputs, replay the rest.
  3. Smooth the visual correction over about 150 ms.
  4. Render remote entities about 100 ms in the past; extrapolate briefly on loss.
  5. The local world mirrors every snapshot entity as a physics twin, so the predicted entity collides where the server will. Seat changes (getting in, carjacking) are not predicted.
  6. Only what the player can touch is simulated: on each snapshot every twin but the controlled entity, its trailer, the player's character and whatever may reach them before the next snapshot becomes a kinematic proxy at its snapshot pose (solid, hit by rays, never integrated), so a step and a replay cost what the player touches, not the crowd. A slow frame catches up three ticks at most.

6.3 Snapshots ​

  • Built (0.48.0, protocol 27): delta snapshots at 20 Hz (packages/protocol/src/snapshot-v2.ts): each client is sent what changed since the last snapshot it was sent (the WebSocket is ordered and reliable, so no acknowledgements), every entity quantized once a tick (a position in 1/256 m, as 16-bit steps when it moved little; a rotation in 32 bits; a velocity and a spin as 16-bit numbers), the client's own entities in full for its prediction; keyframes, a check every 60 frames and a resync; an area of interest of 250 m (players, props and doors always) whose far poses come every second or fourth snapshot; events only to the players near them; permessage-deflate on the wire.
  • Measured (npm run test:load, Performance): 16 players on the hillside with its 32 cars and 40 people at 21 kB/s per client decoded, 12 kB/s on the wire (263 kB/s with whole snapshots).
  • Planned: priority scoring within a byte budget per snapshot, dead reckoning (a pose sent only when the client's extrapolation strays).

6.4 Transport ​

  • Transport interface: send(channel: 'reliable' | 'unreliable', bytes), onMessage.
  • Phase 1 (built): one WebSocket; both channels reliable.
  • Phase 2 (built): a WebRTC data channel (unordered, no retransmits) for inputs and snapshots when the server offers it (FOI_RTC=1), signalled over the WebSocket, which keeps the reliable events and takes everything back when the channel fails or falls silent; the transport says when it is lossy. See Netcode.

6.5 Validation and anti-cheat ​

  • Inputs clamped; sequence numbers monotonic; input rate capped (built).
  • The server rejects impossible actions: entering or carjacking a car out of reach or moving too fast, using an interactable out of range, moving an item the player cannot reach.
  • No client-reported positions, ever.

6.6 Reliable events and the welcome (planned) ​

  • JSON messages validated by Zod on the reliable channel, ordered per room: mission state, objective markers, inventory changes, phone messages, interaction prompts, room notices.
  • The welcome carries a level's data in full, unless the client asks for a big one by its hash (the web client does): it then carries the hash and the length, and the page fetches the level from the game server once (/levels/<hash>.json, 7.7 MB for main-riverbend) and keeps it in the browser by that hash (apps/server/src/level-files.ts). With rooms it carries the game's artifact hash and URL instead; the client fetches the artifact once and caches it by hash.

7. Game servers and rooms (apps/server) ​

  • Today (built): one process runs one level (FOI_LEVEL) at 60 Hz with 20 Hz snapshots, a /health endpoint and debug commands; the desktop app embeds one.
  • Rooms (planned): a process hosts several rooms, each in its own worker thread with its own Rapier world and tick loop, so a slow room cannot stall the others. A room is opened from a game version: the server fetches the artifact (level data, missions, rules) from object storage by hash, caches it, starts the room and reports to the API.
  • Joining: the client signs in to Nakama (a guest's device account or a full account), asks the API to join a room with its Nakama session, and gets a short-lived signed join token; it connects to the server's room URL with it, and the server checks the signature with the API's public key. Self-hosted servers (the desktop app) keep a no-token mode for playing alone or on a LAN.
  • Lifecycle: a room closes a few minutes after its last player leaves. A run's room (planned, §9.5) closes 15 s after its result, or a minute after it opened if nobody came; the server reports the run's result to the API with a token the API made for that run alone, and idempotently (the same result twice is one); a run that never reports is lost and counts for nothing.
  • Budgets: each room enforces its game's budgets (NPCs, vehicles, items, triangles); target 60 Hz with 32 players and traffic in under 8 ms per tick on one core.
  • Operations: structured logs, metrics (tick time, entities, bandwidth per room), heartbeats to the API (rooms, players, capacity, build hash), graceful shutdown that notifies players.

8. Client (apps/client) ​

8.1 Renderer ​

  • Built: the level from the welcome (meshes instanced per placement, flat materials, no z-fighting, see §10.2), glTF actor models with tinted bodies, the camera (18° off vertical, height with speed), structures that fade over the player, a minimap, HUD and legend in React, the in-game UI in the scene (below), audio.
  • The UI in two layers (decided 2026-10-07, §13): anything that is part of playing the game (the interaction prompt and every label that follows something in the world, the phone, the mission panel and race readout, the inventory strip and the panels built on slots: containers, shops, garages) is drawn in the scene with Babylon's GUI (apps/client/src/gui), owned by the game loop and updated in the render loop: a world-attached label is projected every frame, never moved by a React render. React (with i18next, apps/client/src/ui) is used only for static overlays: the helper UI (legend, toolbar, forms, game pages, chat) and the stats and game-event overlay (HUD and hit log, Wanted stars and Heat, stealth badge, clock, minimap). The view logic (*-view.ts, mission-format.ts) and the translations are shared by both.
  • Planned: rigged characters with an animation state machine driven by the stances the server already sends (client-only, no netcode change); seat poses from the vehicle models' seat anchors; drift smoke and tyre marks from tyre slip (drawn above the road paint); falls along the hit; a free camera for previews; lighting for the clock and effects for the weather [NEXT].

8.2 Asset and level pipeline (apps/tools, assets/blender) ​

  • Built: Blender scripts make the kits and models (assets/README.md); level:recipe places kit pieces into an editor scene; level:export turns scenes into level data; level:check proves the exports current and the levels playable; models:hulls and models:check keep the vehicle colliders in step with the models; tests forbid coplanar faces in every kit and level.
  • Planned: a kit library export (every kit piece's footprint, anchors, tags, collider and budget in one versioned file) that the catalogue reads; a direct path from a generator's outputs (the level's kit and its recipe) to level data, without an editor scene (the glTF, kit and recipe readers exist). The editor stays the hand-placement tool for the curated games.

8.3 Desktop and browser ​

  • Electron: contextIsolation, no nodeIntegration, a small typed preload API (electron/bridge.ts); the menu plays online on a platform's server or local, the app starting its own server only to host (built, 0.42.0).
  • Browser: the same renderer built by Vite (built for development). In production the API serves it, together with the sign-in and game pages, from one origin, so cookies, Nakama sessions and join tokens need no cross-origin set-up. A build's preview link opens the built artifact in the browser client with a free camera; no server room is needed to look at a level.

9. The creation platform ​

9.1 Flow ​

creator in Claude Code ─► MCP tools (catalogue, draft, build, playtest) ─► level spec + missions (data)
   ─► worker: generator in headless Blender (spec + seed → level data) ─► validator (+ AI playtest)
   ─► the report goes back to Claude, which fixes the draft ─► preview link ─► playtest room
   ─► publish (version pinned) ─► rooms opened from the game ─► players

There is one front door for creating: Claude Code with the five-on-it MCP integration, for players and for the developer building the curated games alike. Nothing bypasses the validator.

9.2 Content model ​

  • Block catalogue (packages/catalogue): one definition per block: id, version, kind, look (kit piece or model), parameters (a Zod schema with ranges and defaults), anchors, events, actions, budget, a doc and an example for Claude, and the name of its mini test level. Exported as JSON Schema and Markdown for the MCP server; kit pieces register from the kit library with their tags (amenity=hospital).
  • Level spec (schema in packages/catalogue): what Claude writes: size, districts (mix, density, theme), roads (templates or a graph), landmarks, places, zones, paths, spawn rules, a seed.
  • Level data (packages/world, built): meshes, placements, roads with lanes and speeds, spawns, nail strips, dummies. Planned additions: places, zones, paths, anchors, interactables, NPC and vehicle spawners.
  • Mission: the document of GDD §9, validated and run by packages/missions (§9.5).
  • Game manifest: id, version, title, creator (a Nakama user id), visibility (private by link, public), the level (spec, seed, generator version, Blender version, artifact hash), missions, rules (player damage, police, respawn, maximum players, clock and weather [NEXT]), the catalogue version, the minimum server build.

9.3 Generator: headless Blender workers (apps/worker, assets/blender/levels) ​

  • Where it runs: a worker takes a build job from the queue, writes the validated level spec to a file, and runs blender -b --factory-startup --python assets/blender/levels/generate.py -- --spec … --seed … --out … with a time limit. The generator writes the level's own kit (terrain, roads, paint) and a recipe that places catalogue pieces; the worker turns both into level data with the readers of apps/tools, runs the validator, and stores the artifact by its hash.
  • What it is: the generic city code of assets/blender/levels/citygen.py (roads with height profiles, rounded junction corners, one conforming ground mesh, an occupancy map of footprints) driven by a spec instead of a hand-written script. The first template is a grid town (M1); hills, tunnels and bridges follow (M6), reusing what main_hillside.py (now curves.py, the level test-curves) did for the hillside; the hillside itself became a region spec in 0.41. One generator for everything: the curated games and the players' games are built the same way.
  • Speed: Blender starts and quits in about 2 s. The hillside takes 165 s today, 97 % of it in one brute-force check (reserved() in main_hillside.py, now curves.py); a bounding-box early exit brings it to 21 s with byte-identical output, and a spatial index should bring it lower (measurements in prompts/3-transition-to-user-generated-missions.md). M1 applies the fix; a small town should build in under 10 s.
  • Deterministic: the generator's randomness is random.Random(seed), with no clock; the worker image pins the Blender version. The gate: the same spec and seed give the same artifact hash, run by a CI job with Blender installed (§10.4).
  • Isolated: one job per Blender process, in a container with CPU, memory and time limits and no network; the spec is data and the scripts are the repository's, so no player code ever runs. Blender (GPL) runs as a separate program on our servers and is never shipped in the client.
  • Not in the browser: previews load the built artifact; the browser never generates.

9.3.1 Regions of tiles (packages/catalogue/src/region, apps/tools/src/region-level.ts) ​

Land is built from standard tiles without Blender: a grid of 64 m tiles whose heights are the levels (4 m each, 0 to 10) of their corners, shared between the tiles that meet there (RollerCoaster Tycoon's model). The spec names features (hills, mountains, plateaus, valleys, lakes, volcanoes, ski runs) and the solver:

  1. raises and lowers the corners they cover;
  2. spreads every raise outward a level per tile (two round a mountain), with the rim and the spawn apron pinned at 0;
  3. lowers until no two neighbours differ by more than their step.

So every tile is one of a handful of shapes (tiles.json), and the mesh, a vertex every 8 m blended from its corners, matches at every seam by construction. A feature cut short is an issue with its path, and the solver prints a map in text. Its output is what the Blender generator writes for a town (a level's own kit, a recipe, places), so level:recipe, level:export and the worker's in-memory export take it unchanged. Roads are laid over the tiles in five classes, through the middles of the tiles' edges, crossing at flat junctions and bridging what they cannot follow (0.35). Lakes (water a person wades and swims in and a car sinks in), volcanoes (lava in their craters) and ski runs came in 0.36, when the platform took region specs: level_spec_set solves one at once and answers with its map, and the worker (or the API, without one) builds it without Blender. Towns inside a region came in 0.37: the town generator builds each on the region's land (--relief), its streets between level junctions, its buildings on pads, its ground meeting the region's vertex for vertex. In 0.41 a volcano's lava came to flow down a flank (flow), every lake and volcano came dressed (a beach and a jetty; fumaroles and a lookout on the rim), a town could be lived-in (dressing), a region's start put the players in a town, and the level that ships, main-hillside, became a region. The official spec is Tiles.

9.4 Validator and AI playtests ​

  • Level checks: schema; every piece and block exists in the pinned catalogue; budgets; no coplanar faces (the search of packages/world/src/coplanar.test.ts); every spawn reaches every place over the roads or on foot; roads rest on what carries them (today's level:check rules, made a library).
  • Mission checks: schema; references resolve; at least one completion path; time limits; every repeating action capped; budgets (§9.5).
  • AI playtest: a headless room where an AI player (built on packages/ai, not an LLM) plays each mission's objectives in order; the result says completed or not, the time, and where it got stuck. Run as a job before publishing and nightly for the curated games.
  • Errors come back as a list of JSON paths, messages and hints, written for Claude to fix.

9.5 Mission runtime (packages/missions) ​

The interpreter runs on the server each tick after the physics step. It reads the tick's events (zone entered, item moved, status changed, app button pressed…), evaluates rules and objectives against the room's state, and emits actions: commands the sim applies at the start of the next tick (spawn, lock, give, set the Wanted level) and reliable events for the clients (objectives, markers, messages).

Rules come in two shapes, which together cover "if the player does this, then do that" and "while enemies are here, defend this territory":

  • when + if + do: edge-triggered. When an event matches and the condition holds, the actions run once (or every time, with repeat).
  • while + enter / every / exit: level-triggered. The condition is evaluated every tick (or at a set rate, 4 Hz by default); enter runs when it becomes true, every runs at an interval while it stays true, exit runs when it becomes false.

Conditions are expressions: all, any, not; comparisons (gt, eq, lt) over values; values are literals, flags, counters, timers, and queries over the world: count and exists of NPCs, vehicles, players or items filtered by tag, role, status, alert phase and place (in a zone, near a place within a radius), plus distance, health, timeLeft, phase, the clock and the weather [NEXT].

State: flags, counters and timers, each scoped to the mission, a party or a player.

Runs and teams (decided 2026-10-18; solo, co-op and versus runs built): a mission's play block says its mode (solo, coop, versus), its players and teams, joining in progress, the Respect it asks for, the lives, what a death does, the retries and friendly fire. In a run's room the run is held by a stand-in holder, id 0 (client ids start at 1), for every seat, so a player who leaves never takes the actors away; seats are bound to accounts, and a reconnect binds the new client id to its seat. Versus adds four things to the language: team on an objective (only its members advance it and see it), a winner (win:<team> in next, the action win), {team} in references and queries, and the verb catch (a target of the other team held stopped near a catcher). A character's party number is its team's index, so the game's rivals rule already means no friendly fire. Kills, credits and race progress are kept per seat, and the run's state is part of the runtime's state. Joining in progress (built): a live run of untilCheckpoint (until its first checkpoint) or always advertises its open seats in its room's status (RoomStatus.open), the API adds a seat at the end of the roster for a player Quick play placed (POST /rooms/:id/seats on the game server; MissionRuntime.addSeat), who comes in at the team's spawn or the last checkpoint; the result reports each seat's span (joinedS, leftS), and Respect pays it pro rata.

Objectives form a graph: sequences, parallel groups (all or any), and branches (next per outcome, done or failed). Some are state-based: defend (a zone, a while condition and a failIf), survive, sneak (a maximum alert phase), escort.

Example: defend the yard against at most three waves.

json
{
  "state": { "counters": { "waves": 0 } },
  "rules": [
    { "when": "player.used:alarm_panel", "if": { "eq": [{ "status": "guard_1" }, "asleep"] },
      "do": [{ "open": "vault_door" }, { "sendMessage": { "from": "Mo", "text": "Vault's open. Don't wake Gary." } }] },
    { "while": { "gt": [{ "count": { "npcs": { "tag": "raider", "in": "yard", "status": "awake" } } }, 0] },
      "enter": [{ "showText": "Raiders in the yard. Hold it." }],
      "every": { "seconds": 30, "if": { "lt": [{ "counter": "waves" }, 3] },
                 "do": [{ "spawn": { "group": "raiders", "count": 3, "at": "yard_gate" } }, { "add": { "counter": "waves", "value": 1 } }] } }
  ],
  "objectives": [
    { "id": "hold_yard", "verb": "defend", "zone": "yard",
      "while": { "gt": [{ "count": { "npcs": { "tag": "raider", "in": "yard", "status": "awake" } } }, 0] },
      "failIf": { "gt": [{ "count": { "npcs": { "tag": "raider", "in": "crates" } } }, 0] },
      "next": { "done": "deliver_crates", "failed": "escape_police" } }
  ]
}

Guarantees, so data can never hang or flood a room:

  • Actions take effect on the next tick, so a rule cannot trigger itself within a tick; there are no loops in the language, only every intervals.
  • Rules run in document order; per-tick budgets cap the rules evaluated and the actions applied.
  • Every spawn is capped by its group's maximum and the room's budget; the validator rejects a repeating action without a cap (a counter, a time limit or a group maximum).
  • The interpreter is deterministic and hashed with the room's state (the gate of §5.4). It is isomorphic, so tools and tests run it without a server.

Phone apps are data the interpreter serves: a button press is an ordinary input (appAction), checked by the server like any other action.

9.6 Platform API (apps/api) and accounts (Nakama) ​

Fastify, JSON validated by the same Zod schemas, OpenAPI generated from them.

  • Accounts run on Nakama: sign-in (a device account for guests, email, social providers), sessions, profiles, friends, parties, chat channels (a room's, a party's, direct messages) and notifications. The API trusts Nakama's session tokens (it checks them with the session key it shares with Nakama, or by asking Nakama) and keys its own data (quotas, drafts, games) by the Nakama user id. Guests can play public games and games shared by link; creating needs a full account.
  • The MCP's OAuth: the API is the OAuth 2.1 authorization server the MCP server needs; its sign-in page authenticates against Nakama, and the access token it issues carries the Nakama user id.
  • Nakama's runtime module (apps/nakama) adds one hook: messages on public room channels go through the moderation service before delivery (§9.9).
ResourceEndpoints (sketch)
CatalogueGET /catalogue, GET /catalogue/blocks/:id (definitions, docs, schemas per version)
DraftsPOST /drafts (empty, from a template, or a remix), GET and PATCH /drafts/:id (level spec, missions, rules; JSON Patch), POST /drafts/:id/build, POST /drafts/:id/playtest
JobsGET /jobs/:id (state, progress, the validator's report, the artifact hash, a preview link and image)
GamesPOST /drafts/:id/publish (private by link, or public), GET /games (search, featured, friends'), GET /games/:id/versions/:n, POST /games/:id/remix, ratings, reports
RoomsPOST /rooms (game version, visibility) → room id, server address, join token; POST /rooms/:id/join; GET /rooms/:id
RunsPOST /games/:id/missions/:mission/runs (a solo run, or the caller's party's), GET /runs/:id, POST /runs/:id/rejoin, POST /runs/:id/result (the game server, with the run's token)
LobbiesPOST /lobbies (from a playlist, remembered), GET /lobbies, GET /lobbies/:id, join, leave, ready, team, start, vote, kick, invite; POST /lobbies/quick (Quick play by playlist: a lobby, or a seat of a run under way); GET /missions (by mode, players, level); GET /playlists (each playlist's rotation across levels and its public lobbies)
StandingGET /me/standing (and its export), GET /players/:id/standing, GET /runs/:id/standing, POST /runs/:id/endorse, POST /runs/:id/rating, GET /leaderboard (seasonal); the admins' conduct and the moderators' upheld reports (respect-routes.ts)
Quotasthe account's builds, playtests and rooms today, against its limits (an admin's override first: /admin/…, the ?admin page)
Moderationinternal checks, the report queue, takedowns, an audit log
Webthe sign-in and OAuth pages, game pages with join links, the browser client (one origin)

Builds are jobs of a durable queue in the platform's store (apps/api/src/queue.ts: the jobs table, a claim locked with FOR UPDATE SKIP LOCKED and held by a lease, the owner who started fewest that day first), so a build outlives a restart of the API; the API's processes run them, a number at a time (FOI_BUILD_CONCURRENCY), the generator worker making the levels. The in-game model's creations are to be jobs of the same queue (§9.8). The daily quotas are counted in the same store, and an admin may override one account's. The rooms are placed by a small fleet component in the API that knows every game server's capacity from its heartbeats.

9.7 MCP server (apps/mcp) for Claude Code ​

  • Installing: a creator adds the server to Claude Code once, claude mcp add --transport http five-on-it https://…/mcp, and signs in when Claude Code asks (the OAuth flow of §9.6). For local development the same server runs over stdio against a local API.
  • A thin adapter over the public API: it has no rights the API does not grant the signed-in creator, and the same quotas apply.
  • Tools: catalogue_search, catalogue_get; draft_create, draft_get, draft_list; level_spec_set, level_spec_patch; mission_upsert, mission_remove; draft_build (returns the validator's report, build statistics, a preview link and a top-down image); draft_playtest; draft_publish; room_open (returns a join link); games_search, game_remix. Each tool's description and input schema are written for Claude: what it does, when to use it, an example.
  • Resources: the block docs, the schemas, the GDD's vocabulary and tone guide, the curated games' level specs and missions as examples.
  • Prompts (slash commands in Claude Code): "create a game", "add a mission", "fix the validator's errors".

9.8 The AI: the creator's Claude, and the platform's one model ​

  • In Claude Code (built): Claude Code runs the model, on the creator's subscription; the MCP server supplies the tools, the context (resources) and the checks. The repair loop is Claude's: the validator's report comes back as a tool result, and Claude edits the draft and builds again. This way stays.
  • In the game (decided 2026-10-16; it replaces "the platform never calls an LLM to author anything"; the jobs, the allowance, the budget, the runner and the page built with a scripted model, FOI_CREATOR=fake, and Steam's entitlement; OpenRouter live with its prices bounding each call, the moderation of briefs and outputs, the transcripts kept 30 days, the metrics and the evals built since, switched on by the operator once the evals chose a model; Claude on Anthropic's API with the operator's own key, FOI_CREATOR=claude, added 2026-10-21, priced at Anthropic's list prices): an owner of the game on Steam writes a short brief in the game, and the platform runs one tool-calling model through OpenRouter or on Anthropic's API on the same MCP tools, with the same checks and the same repair loop. Nobody else can use it: guests, free accounts and family-shared copies cannot.
  • Bounded by money first (built: the ledger and the day's budget, budget.ts). A hard cap on what the platform spends on the model in a month ($25 by default, a setting); when the day's share of it is spent, briefs wait for the next day and no allowance is used. The platform never takes a key a player brings: there is no bring-your-own-key, ever.
  • Every creation is a job (built: apps/api/src/creations.ts, apps/creator). It goes into the platform's durable job queue (the one builds already run in, §9.6) and runs asynchronously; the game shows its place in the queue, its steps, and a Cancel. Each account has a daily allowance of creations (5 a day by default), reset at midnight UTC and never carried over.
  • Admins may override an account's limits (built): its creations, builds and rooms a day, higher or lower (0 blocks), until a date, and whether it is exempt from the day's budget (a test account); every change goes to the audit log.
  • Our costs are compute (builds on the Blender workers, playtests, rooms) and the in-game model under its cap. Per-account daily quotas, kept in the platform's store, cover them.
  • The other model the platform may call is the moderation classifier, and only when that moderator is chosen (§9.9).

9.9 Moderation: pluggable ​

  • One interface: Moderator.check(texts, context) returns allow, hide or reject, with reasons. Implementations: off (allows everything), wordlist, claude (a small Claude model as a classifier), and chain (a word list first, then Claude for what passes). Tests use a fake.
  • Chosen by configuration: FOI_MODERATION=off|wordlist|claude|chain. Local development and the Compose file default to off, so nothing needs a key or a network; the server logs a warning at start when moderation is off. Production sets chain.
  • What is moderated: the texts of a game when it is made public (blocking: titles, blurbs, dialogue, messages, app screens, names), and chat on public room channels (through the Nakama hook; a message can be hidden after the fact). Private games, shared by link, and their rooms are not moderated (GDD §10.4).
  • Reports, takedowns, mutes and bans work everywhere; an audit log keeps every decision.
  • Prompt injection: whatever Claude sends becomes data only after schema validation; no tool runs code; tool inputs are validated like any API request.

9.10 Data ​

Postgres, one server: Nakama's database (accounts, friends, groups, chat history) and the platform's (drafts and their revisions, games and versions, artifacts with their hashes and generator versions, jobs, rooms, ratings, reports, the moderation log, usage and quotas, keyed by Nakama user id). Object storage: level artifacts by content hash, preview images. Redis: rate limits (planned); the job queue and the quotas live in the platform's database.

10. Testing strategy ​

10.1 Layers ​

LayerRunnerWhat it covers
UnitVitest unit, next to the codeSim systems on small synthetic worlds, the driving envelope, codecs, prediction, the AI's planning, the level pipeline, the coplanar search; planned: the catalogue's schemas, the validator's rules, the mission interpreter (rules, queries, objectives, budgets) as table-driven scenarios, inventory transfers, carjacking, the moderators, API handlers with fakes.
IntegrationVitest integrationSeveral components in one process: the determinism gates (sim, AI; planned: missions); the lag-simulation gate (apps/server/src/server.integration.test.ts: two bot clients at a simulated delay, corrections under 0.5 m); the chases on the AI level; planned: rooms with bots, every curated mission completed by an AI player, the API with Postgres and Nakama through testcontainers, the MCP tools through an in-process MCP client.
GeneratorA CI job with Blender (§10.4)Planned: the same spec and seed give the same artifact hash; every template's levels pass the validator; the build time of the reference specs.
ContractVitestPlanned: every block has a doc, a schema, an example that validates and a mini test level; every MCP tool's schema matches the API; the OpenAPI document is current.
End-to-end, smokePlaywright smokeThe built desktop app boots, connects and shows the HUD. Planned: the browser build, a guest sign-in, the API's health, an MCP handshake.
End-to-end, corePlaywright coreThe browser client against real game servers, one per level folder: the regression baseline of every mechanic on its mini test level (§10.2).
VisualPlaywrightScreenshots of each mini level against a baseline (planned: nightly).
LoadArtilleryPlanned (§10.3).
Securitybuild/sast.mjs, build/dast.mjsDependencies and secrets; planned: DAST against the API and the MCP server (auth, quotas, rate limits).

10.2 Mini test levels: the QA baseline ​

Every mechanic gets a tiny level that exercises it and nothing else, and end-to-end tests on it. Together they are the baseline every change is checked against: when a mechanic breaks, the failing level names it.

  • A mini level is a test-* level: an e2e folder tests/e2e/specs/levels/<name>/ with tagged specs, rows in tests/e2e/TEST-PLAN.md, and a contract test that pins the fixture (as packages/world/src/levels.test.ts does for today's test levels).
  • Small on purpose: a few hundred metres, few pieces, fixed spawns and seeds, so it boots in seconds and draws fast in the software renderer of GitHub's runners (14 to 45 fps on four cores, by level).
  • Built from a recipe today; from a level spec through the generator once it exists, so the mini levels test the generator as well.
  • Definition of done of a block: its doc and schema, its validator rules, its mini test level, a tagged end-to-end test, a plan row.
Mini levelMechanicsMilestoneStatus
test-core-gameplayDriving, prediction, two players, on foot, weapons, cover (the POC city)M0built
test-ai-pathfindingChasers on foot and at the wheel, the obstacle courseM0built
test-breakablesLight props knocked loose at speedM0built
test-generated-townA grid town built from a level spec by the generator (generate.py, grid-town), then playedM1built (E2E-80, E2E-81)
test-mission-basicsgoto, deliver, steal_vehicle, timers, zones, when rules, fail and completeM1built (E2E-70–72)
test-roomsGuest sign-in, opening, joining and leaving a room; two rooms on one serverM1covered without a level of its own: tests/e2e/specs/platform on test-mission-basics (E2E-85, E2E-86) and the server's http.integration.test.ts
test-drift-padTyre marks, smoke and dust from slip, a landing’s dust, hit sparks, riders on open seats (the driving envelope stays pinned by vehicle.test.ts)M2built early (E2E-100–105)
test-on-footWalk, run, jump, falls from ledges, crouch, cover at low and high walls, weapons, rides, interactions, guards, the cosmetic ragdoll: every animationM2built (E2E-260–279)
test-collisionsCars crashing into each other and into people at speed without sinking in, a rammer that pushes only on contact (the cars near the player drawn at its time), getting out beside a wall or in an alley, no steering in the air, the sprint at 35 km/h, a truck knocking a light piece loose slower than a carM2built (E2E-310–315)
test-interactionThe interaction key on doors (locked and not) and a lockerM2built early (E2E-135–137)
test-inventoryItems on the ground, use and drop, a crate, a car's trunk, a mission of items; weapons and ammunition as items, a game's loadout, the mission items dropped on death, the sixteen-slot strip (E2E-390–394, 399)M3built (E2E-130–134)
test-phoneMessages with replies, calls, a contact in person, an app that hands over a car's key within 10 m, a keypad door, a terminal, the map's waypointM3built (E2E-140–149)
test-locks-garagesLocks, keys, picking, vehicle spawners, a garage's respray and storage, a mission of them (carjacking is tested on test-core-gameplay and test-ai-pathfinding, E2E-90–92)M3built (E2E-160–169)
test-mission-logicwhen and while rules, queries, counters, branching objectives, defend with capped waves, survive, loseM3built early (E2E-95, E2E-96)
test-mission-verbsgive, escort, chase, photograph (the phone's camera), wait, escape from the police and from actors, destroy of vehicles and light pieces, objectives in parallel groups, the triggers "Wanted level changed" and "message read", the minimap's route line, a party's shared markersM3built (E2E-370–384)
test-stealthVision cones, hearing, alert phases, a camera, a keycard gate, a patrol and a post, the tranquilizer, a knockout, a body found, a mission completed unseenM4built (E2E-180–190)
test-compoundA walled compound generated from a level spec (the grid-town template's compounds): keycard gates, a gap, a way over the roof, a lab one walks into, four cameras, guards on shifts, breaks and rounds by the room clockM4built (E2E-250–257, 259)
test-non-lethalTranquilizer, knockout, waking up, a body foundM4covered on test-stealth
test-traffic-junctionAmbient traffic at a junction with traffic lights, T junctions withoutM5built early (E2E-110–115)
test-policeHeat, stars 1–3, losing the police, an arrest (the respray is on test-locks-garages); three stars (the pursuit tune, nail strips at the junctions ahead), the Heat of breaking property and of speeding past the police (E2E-395–398)M5built early (E2E-120–123)
test-safe-zonesA game's safe zone (no harm, no carjacking between players inside it) and the ghost after a respawnM6built (E2E-220–223; the rest of the social layer is in tests/e2e/specs/platform, E2E-210–219)
test-new-vehiclesTaxi, bus, ambulance, fire truck, garbage truck: classes, light bars, sirens, boarding the bus (the police car is on test-police)M5built (E2E-150–153)
test-buildingsEvery building family with its tags, colour variants and anchors, districts with themes and densities, shops at counters, the hospital's respawn and the police station's spawns, generated from a level specM6built (E2E-280–283, 307, 308)
test-raceA circuit of race gates with laps, a start grid and two rivalsM5built (E2E-290–292)
test-city-lifeZone kinds and restricted zones, spawn roles, a driver, a target and a VIP, the clock and schedules, the crowd by the hour, a gate and a barrier, a traffic light an app switches, a shop and one CashM5built (E2E-293–306)
test-fireCars smoking, catching fire and blowing up, a blast lighting the cars beside it (a row of sedans), bigger blasts for bigger vehicles, a crew leaving a car on fireM5built (E2E-461–464)
test-osm-buildingsThe buildings made from OpenStreetMap's tags: six districts by landuse, theme and wildcard, drawn as the osm-* kitsM6built (E2E-473)
test-plotsA region's rich ground: its tones, ground cover, every landuse kind dressed, a plot on every tile, a point of interest of each kind (a pond to wade in)M6built (E2E-469–471)
test-weatherThe clock and the weather: rain, snow and fog drawn, wet roads braking longer, lights at night and the brightness setting, a guard missing a player in the dark but not under a lamp, a mission's setWeather and skipTime—built in 0.52.0 (E2E-491–497)
test-tiles-tunnelA region's roads through the land in tunnels: a highway through a mountain in one long tunnel, a country road through a hill in two short ones; the portals, the roof fading over a car inside, the road continuous, the roof walked onM6built in 0.53.0 (E2E-498–500)
test-waterSwimming, drowning, a sinking car, a boatNEXTplanned

10.3 Load tests: Artillery (planned) ​

  • npm run test:load runs the Artillery scenarios in tests/load/.
  • Rooms: a small Artillery engine (or processor) that speaks the game's binary protocol through packages/protocol joins bots to N rooms of 32 on one server, with traffic and NPCs, and drives them through scripted inputs.
  • Platform: Artillery's HTTP engine against the API: browsing games, opening and joining rooms, queuing builds (with a stub generator, so the load test measures the queue and not Blender).
  • Measures: the server's tick time (p50, p95, from its metrics), bandwidth per client, memory per room, join time, the API's latency and error rate, the build queue's throughput. Targets: p95 under 10 ms per tick at 32 players; about 25 kB/s per client.
  • Runs nightly and before a release, not on every pull request.

10.4 Quality gates and CI ​

  • npm run validate (in CI and in the Release workflow; the pre-push hook runs npm run validate:push, its fast gates and the tests on two workers without coverage, and the pre-commit hook Prettier, the type-check and the spelling of the staged files): Prettier, ESLint, tsc, knip, cspell, the line counter (no limit; it rewrites the badges), the e2e plan gate, level:check, models:check, Vitest with coverage (90 % on sim and protocol, planned for missions; 70 % elsewhere), the builds, the docs.
  • Every pull request: validate, e2e smoke, the e2e gate (the core tier: every mini level's fast tests, 20 s at most each, packed by their recorded seconds into lanes side by side on the self-hosted runner and two of GitHub's GPU runners, within 15 minutes; .github/workflows/e2e.yml, docs/guide/quality-gates.md); the generator job (Blender in its container image) when assets/blender, apps/worker or the level spec change.
  • Nightly: the e2e long tier with the gate (the missions played through, the galleries, the tours of generated levels, every level's boot), the generator job, Artillery load tests, AI playtests of the curated games, visual baselines, DAST.
  • Release: everything above (the e2e gate and the long tier run before the release is published), plus the smoke test on the built installers.

Optional, later: faster frames on GitHub's runners without a GPU. The browser's tiers run on a self-hosted runner and GitHub's GPU runners, whose GPUs the browser draws on; GitHub's other runners, the last fallback, have none: headless Chromium draws WebGL with SwiftShader on the CPU, in the no-GPU mode (two thirds of the resolution, no antialiasing), on four cores it shares with the level servers, Vite and Playwright. That gives 14 fps on test-buildings and 30–45 on the small levels, where a real GPU holds 60 (npm run perf). The specs wait for states, not times, so a slow frame costs minutes and the odd retry, not correctness, and none of this is needed today. In order of cost:

  1. The production build: run the core tier against a production build served by vite preview (as npm run perf does) instead of the dev server: minified code and React's production mode give a few frames more, and test what ships.
  2. Fewer pixels: a smaller e2e viewport (960 × 540 instead of the 1280 × 720 of devices['Desktop Chrome'] in playwright.config.ts), or a lower software resolution behind a query parameter (SOFTWARE_SCALING in apps/client/src/renderer/render-quality.ts): roughly 1.5 to 2 times the frame rate on the heavy levels, which is where it counts.
  3. GitHub's GPU runners (the four-core Linux larger runner with an NVIDIA GPU, about four times the four-core runner's price per minute): 60 fps like the self-hosted runner, without depending on a machine being on. Worth it only if the self-hosted runner goes away.

11. Milestones ​

Each milestone ends with npm run validate green, its mini levels in the core tier, and a playable build. Estimates are in non-blank lines as npm run loc counts them (TypeScript, JavaScript and Python). Today (0.18.0 with this plan): 22,534 product lines, 3,705 of them Python under assets/blender, and 11,510 test lines. The counter has no limit; these numbers are what keeps scope honest. When a milestone's real count runs past its estimate, stop and decide.

#MilestoneScopeExitProduct linesTest lines
M0FoundationDone: monorepo and gates, sim, netcode, twelve vehicle classes, on foot, weapons, cover, chasers, levels from Blender kits, main-hillside, breakables, installers.—22,534 (actual)11,510 (actual)
M1Walking skeletonThe whole creation loop, thin (below).The developer, as a player, asks Claude in Claude Code with the five-on-it MCP integration for "a small town with a delivery mission"; it builds on the Blender worker, validates and publishes; a room opens; two players (desktop and browser, one a guest) finish the mission. Hillside gets its first mission. Deployable with Compose on one host.+8,900 to +12,200+5,000 to +7,000
M2Characters and feelRigs and animation, seats and poses, falls, drift smoke and tyre marks, the interaction key.test-drift-pad, test-on-foot, test-interaction green (the seats are covered on test-drift-pad).+3,000 to +4,500+2,000 to +3,000
M3Inventory, phone, locksItems and containers, the phone with contacts, apps, messages, calls and the map app, locks and keys, carjacking with fleeing passengers, garages, vehicle spawners; the mission language grows to its full shape (while rules, queries, counters, branching, defend)."Downtown job" playable.+5,500 to +7,500+4,000 to +5,500
M4Stealth and non-lethalAwareness, alert phases, statuses, the tranquilizer and the knockout, cameras, doors and keycards, patrols."The compound" playable without kills.+3,500 to +5,000+2,500 to +3,500
M5Living cityAmbient traffic and traffic lights, pedestrians with roles, the police up to 3 stars, six new vehicle classes, area of interest."Rush hour" playable; 32 bots with traffic within the tick budget.+6,000 to +8,000+3,500 to +5,000
M6Creator platformGame pages and the browser client (browse, preview links, ratings, remix, reports), friends, parties and chat on Nakama with moderation, quotas, AI playtests, the generator's hills, tunnels and bridges, "Delivery Wars".20 descriptions from new creators in Claude Code → at least 18 playable games.+5,500 to +8,500+3,500 to +5,500
M7Scale and ship (V1)Rooms across hosts, the unreliable channel and delta compression, Artillery load tests green, telemetry, crash reports, privacy and terms, signed installers.V1 released with the five curated games.+3,000 to +5,000+2,000 to +3,000
V1 total≈ 58,000 to 73,000≈ 34,000 to 44,000

M1 in detail (the slice to start with):

PieceWhereProduct lines
Block catalogue v0 (today's kit pieces, roads, zones, places, spawns, vehicle spawners) and the level spec schemapackages/catalogue800–1,000
The spec-driven generator: a grid-town template on citygen.py, with the bounding-box speed-upassets/blender/levels (Python)1,200–1,800
The generator worker: build jobs, the Blender process, its outputs → level dataapps/worker, apps/tools600–900
Kit library export (tags, footprints, anchors)apps/tools300–500
Validator v0: schemas, references, reachability, budgets (today's level checks as a library)packages/world, packages/missions500–700
Mission runtime v0: goto, deliver, steal_vehicle, zones, timers, text, when rulespackages/missions1,200–1,500
Reliable events, the welcome by artifact, rooms, join tokenspackages/protocol, apps/server900–1,200
Platform API v0: drafts, build jobs, games, rooms, the MCP's OAuth with Nakama sign-inapps/api1,600–2,000
Accounts on Nakama: the Compose service, guest and email sign-in in the client, session checksapps/client, apps/api300–600
MCP server v0: tools, resources and prompts for Claude Codeapps/mcp600–800
Client: a Games tab to list games and open or join a room, the mission tracker and markers, artifact fetch, the preview cameraapps/client700–900
Moderation v0: the pluggable interface, off and a word listapps/api200–300
Total8,900–12,200

The way to V1 (decided 2026-10-07) ​

At 0.27.0 the repository counts about 74,300 product lines and 44,200 test lines, past the 58,000 to 73,000 this table estimated for the whole of V1, with V1 not yet done. What is left is estimated at +16,000 to +25,000 product lines; the user chose to go on with the full list, in this order. The cuts that were offered (rooms across hosts and delta compression after V1, AI playtests as a scripted reachability check, accessibility in two steps) stay available if a wave runs past its estimate.

#WorkstreamScopeMini test levelProduct / test lines
AThe mission language, completeThe verbs give, escort, chase, photograph, wait, the rest of escape (from the police or from actors) and of destroy (vehicles and props); objectives in parallel groups; the triggers "Wanted level changed" and "message read"; the minimap's route line; a party seeing each other's mission markers Done on develop (test-mission-verbs, E2E-370–384; +1,905 product, +2,012 test lines).test-mission-verbs+2,500 to +3,500 / +2,000 to +2,500
BThe gameplay gapsWeapons as items in the slots (the number keys select slots, ammunition as items); a character dropping its mission items on death (by the game's rules); the police at three stars (faster cars, nail strips at the junctions ahead); the Heat of breaking property and of speeding past the police Done on develop (E2E-390–399; +1,191 product, +1,407 test lines).test-inventory, test-police+1,500 to +2,200 / +1,000 to +1,500
C"Downtown job" and "The compound"Their levels generated from level specs (a bank and its vault, a body shop, a rental lot; the compound's walls, gates and cameras), their missions as data, the guards' schedules, a npm run perf scenario each at 60 fps Downtown job done on develop (main-downtown: a 4 × 4 grid town of five themed districts, a walk-in bank with its vault, five missions; E2E-240–249; +207 product, +1,156 test lines and the game's content). The Compound done on develop (main-compound: a 3 × 2 grid town round Zenful Campus, a walled campus with keycard gates, a gap, an annex with fire stairs, a lab one walks into, four cameras and guards on shifts, breaks and rounds by the clock; three missions failed by a kill, Inés their help line; E2E-250–259, 412). Both games withdrawn with their cities in 0.58.0 until the cities are redrawn; the walled compound stays as the test level test-compound.the levels themselves+2,500 to +4,000 / +1,000 to +1,500
D"Rush hour" and "Delivery Wars", Hillside's missionsTaxi and ambulance runs against the clock; rival crews over districts; chases. Rush hour (0.53.0) and Hillside's missions were done, and withdrawn with their cities in 0.58.0 until the cities are redrawnthe levels themselves+1,500 to +2,500 / +1,000
EThe creator platform, completeA build's preview with a free camera, games pinned to the versions of the blocks and the generator, the MCP's OAuth, the kit library export, the welcome by artifact hash, the browser's friends' games and "your games" with drafts, AI playtestsplatform specs+4,000 to +6,500 / +2,500 to +3,500
FAccessibility and audioRemappable keys (decided 2026-10-12: the only accessibility setting; no colour-blind palettes, camera-shake toggle or text sizes for V1); phone sounds, interface blips, footstepse2e on the settings+500 to +800 / +300 to +500
GScale and ship (M7)Artillery load tests (32 players and traffic within the tick budget), rooms across hosts, the unreliable channel and delta compression, telemetry and crash reports, privacy and terms, signed installersthe load tests+3,000 to +5,000 / +2,000 to +3,000

The order: wave 1 A and B side by side (different packages), with a first Artillery run from G, as the open question of rooms as worker threads or processes (§13) needs its numbers; wave 2 C, one agent per game, on A's verbs; wave 3 D, on chase, escort, destroy and the parallel objectives; E and F whenever an agent is free (they touch the API, the MCP server and the client's settings); the rest of G last. Every wave also takes the known gaps of 0.27.0: the e2e specs that failed once (E2E-144, E2E-302), the GUI's own texture for the labels that follow the world, a reset event so cracked glass clears for everyone, people drawn at the predicted time near the player, mission and AI spawns that check the spot is free. Decided 2026-10-12: a car hit broadside at full throttle may tip over (the driving model's centre of mass stays, docs/guide/driving-model.md).

Landmarks and tiles, before the rest of V1 (decided 2026-10-08) ​

After 0.30.0 the user asked for two things before workstreams D to G.

  • Walk-in landmarks. A mall, a hospital, a mensa, a castle and a ski lodge, explored floor by floor by stairs, ladders, lifts, escalators and walkways, with falls that hurt.
  • Composable 3D land and roads. Standard 64 m tiles with 4 m height steps that snap together like RollerCoaster Tycoon 2's: hills, a lake (shallow water slows people and cars, deep water makes people swim and sinks cars), mountains, a volcano, a ski slope; country roads to four-lane highways, ramps, bridges and tunnels.
    • Claude names tiles or features, and a deterministic solver fills in the rest, so a level is always consistent and playable.
    • The system is published as an official spec (docs/guide/tiles.md).

Buildings come first. Each release is a minor one with its mini test level.

ReleaseScopeMini levelProduct / test lines
0.31.0Layered navigation: the floors over the ground the AI walks and the checks reach; places, zones and markers on their floortest-storeys+1,500 / +900
0.32.0Ladders, lifts, escalators, fall damage; the AI uses them; a climb cliptest-vertical+2,200 / +1,100
0.33.0An interior library; the mall, hospital, mensa, castle and ski lodgetest-interiors, test-landmarks+2,600 / +700
0.34.0tiles.json, the edge library, terrain tiles, the region template and its solver, the assembler, the conformance teststest-tiles-terrain+3,200 / +1,500
0.35.0RoadDef.kind; road tiles from track to highway, transitions, ramps, bridges, tunnels; routingtest-tiles-roads+3,000 / +1,300
0.36.0Water; the lake, volcano, ski slope and mountains; towns in regions; the tiles guide for creators; a main-* showcasetest-water+2,700 / +1,200

The total is about +15,200 product and +6,700 test lines. If a release runs over its estimate, cut in this order:

  1. interchanges and 3 × 3 highway bends;
  2. tunnels;
  3. biome variants;
  4. the mensa and the ski lodge;
  5. the AI on lifts and ladders;
  6. cars on the layered navigation.

Progress ​

Ticked as each piece is merged into develop (with the line count after it, as npm run loc prints it). M1 so far: 30,371 product lines (+7,837 since M0's 22,534; the estimate was +8,900 to +12,200) and 16,196 test lines (+4,686).

M1, the walking skeleton:

  • [x] Mission runtime v0 (packages/missions): the game document (places, zones, rules, missions), the objectives goto, steal_vehicle and deliver with time limits and damage limits, when/if/do rules with flags and timers, rewards, per-player missions started on join or on entering a zone, the validator, the bundled games (@foi/missions/content); the server's MissionHost and FOI_GAME; the mission and notice text frames; the client's mission panel, world marker and minimap ring; the mini level test-mission-basics with E2E-70–72.
  • [x] Hillside's first game (packages/missions/content/main-hillside.json): "Same-Hour Rental" and "Rally Return"; E2E-73. Two players on the same mission get their own mission vehicles side by side.
  • [x] Block catalogue v0 (packages/catalogue): the level spec's Zod schema, field for field what generate.py accepts (its tests read the Python and run a table of specs through both), and the entries Claude reads: the template, the building archetypes with their footprints, the props, the named places of a generated level, the vehicle classes, the mission actors, verbs, rules, events, conditions, values and actions; GET /catalogue, search, Markdown for the MCP server.
  • [x] The spec-driven generator on headless Blender (assets/blender/levels/generate.py: the grid-town template on citygen.py and the shared cityparts.py; a spec validated with one line per problem; deterministic by seed; a 3 × 2 town in about 3 s with Blender's start), the hillside's bounding-box speed-up (176 s → 24 s, byte-identical), the mini level test-generated-town with its named places (E2E-80, E2E-81). 27,001 product lines (4,459 of them Python), 14,223 test lines after it.
  • [x] The generator worker (apps/worker): a spec → headless Blender (generate.py, a time limit) → level data in memory (@foi/tools' exportRecipe, byte for byte what level:recipe and level:export give) and its places; POST /generate, one job at a time; spec drafts in the API (POST /drafts {spec}, PUT /drafts/:id/spec), generated on the worker (FOI_WORKER_URL) or in the API's process, cached by the spec's hash, their places merged into the game's; E2E-87 (with Blender).
  • [x] Kit library export (tags, footprints, anchors), built in 0.54.0: npm run kits:export (apps/tools/src/kit-library.ts) measures every piece of the shared kits on its glTF and writes packages/catalogue/src/kits.generated.json (kind, collider, footprint and bounds, triangles, tags, the sidecar's or derived from the kit, kind and id, anchors, breakable mass, roads, parts); the catalogue's kit-piece entries and the MCP server's catalogue_search and catalogue_get show them; level:check fails when the file drifts from the kits.
  • [x] The level checks as a library: checkLevel (lint and LEVEL_BUDGET) and spawnReach in @foi/world, validateReach in @foi/missions; every build checks a generated level and that the places the game uses (and its own) can be reached from the spawn points (0.42.0: a generated level's other places, a crater or a cliff's top among them, are only offered).
  • [x] Rooms on the game servers (RoomHost, the default room kept), opened from an artifact the API serves, joined with HS256 join tokens; idle rooms closed. (The welcome still carries the level in full; fetching it by hash comes later.)
  • [x] Platform API v0 (apps/api): sign-in, drafts, builds, playtests, publishing, games, rooms, artifacts, quotas; memory and file adapters. (Postgres, Redis and S3 adapters come with the Compose stack.)
  • [x] Accounts: the dev provider and the Nakama provider (REST sign-in, sessions checked with the shared key); the Games tab plays as the device's guest or signs in with an email (E2E-74). (The Nakama service itself comes with the Compose stack.)
  • [x] MCP server v0 (apps/mcp): 16 tools (catalogue, levels, drafts, level specs, games and missions, builds that wait and report the validator's issues, playtests, publishing, rooms, games), resources (the schemas, the missions guide, the tone guide, the bundled games), prompts (create_game, add_mission, fix_errors); stdio for local development (the repository's .mcp.json), Streamable HTTP on /mcp with the creator's bearer token (OAuth later).
  • [x] The client's Games tab: a guest session made once, the public games and their rooms, Play and Join; E2E-85, E2E-86.
  • [x] Moderation v0: the pluggable Moderator (off, wordlist, claude, chain), public games' texts checked at publishing.
  • [x] The Compose stack (deploy/): Postgres (the platform's JSONB store and Nakama's), Nakama 3.41, MinIO (S3 artifacts), the API, the game server, the Blender 5.2 worker (no network out, CPU and memory limits), the MCP server and Caddy giving one origin; npm run stack:up, stack:smoke (sign-in through Nakama, publish, rooms through the proxy, a level generated from a spec and played, the MCP server answering) and stack:down. (Redis comes with the first thing that uses it: the build queue and rate limits.)

Built ahead of their milestone (M3), on the M1 platform:

  • [x] The mission language v1 (packages/missions): counters (add, set, {counter:<id>} in texts), comparisons and exists over values (counters, time left and elapsed, counts of players, enemies and vehicles by zone, place, tag or group, distances, health), while rules (enter, every, exit), groups of enemies (chasers of the AI after the mission's player, waves by spawn capped by max, group:<id>.cleared|spotted), the objectives survive, defend, lose and eliminate with a hold clock on the tracker, branching (next); the validator's graph, cap and budget checks; test-mission-logic with three missions; E2E-95, E2E-96; the catalogue lists it all.

  • [x] Carjacking (packages/sim/src/carjack.ts): E at a stopped car's driver's door pulls the driver out (knocked down), puts the passengers out the far side and takes the wheel after a beat; a player at the wheel only where the game's playerDamage is on (free roam is); party members exempt (the hook is there); vehicle:<id>.carjacked for missions; NPCs flee the carjacker, then chase; pedestrians flee gunfire and blasts; the interaction prompt (Get in, Get in as passenger, Carjack) above the character; PROTOCOL_VERSION 4; E2E-90–92.

  • [x] Feel and effects (M2, apps/client): tyre slip worked out on the client for every car (no snapshot change), tyre marks (a capped ring buffer, fading over 20 s, lifted over the road paint), smoke on asphalt and dust off-road (the ground under each wheel from the level), a landing's dust, hit sparks; seat anchors on every vehicle model (pose and open or closed view) with the riders of open seats drawn in their pose; the character split into parts with a procedural stride, jump, sprawl and getting up (no rig yet); test-drift-pad, E2E-100–105.

  • [x] The police and the Wanted level (M5, packages/ai/src/wanted.ts, police.ts): Heat from crimes seen or heard (gunfire near people, hurting pedestrians, carjacking, wrecking cars, worse against officers), stars at 10, 25 and 45, decay out of sight, losing the police star by star; police cars of a new police car class (its model, light bar and hull) spawned out of sight, one at 1★ that tails and boxes in, two that ram at 2★, four crews that shoot at 3★; arrests with a fine; rules.police, the wanted value and setWanted action; the stars on the HUD; test-police, E2E-120–123.

  • [x] Ambient traffic and traffic lights (M5, packages/ai/src/traffic*.ts, packages/world/src/signals.ts): drivers of the AI on the lanes at a share of the road's speed, keeping their gap and queueing, a random way out at each junction, giving way where no light rules (the first to arrive goes), never entering a box they cannot leave; lights on a fixed plan per junction (a pure function of the level and the tick: no state on the wire, the client colours the lamps itself); carjacked drivers flee, gunfire panics them; stuck cars recycled out of sight; rules.traffic (the hillside's game asks for 32), FOI_TRAFFIC; an area of interest of 250 m in the snapshots; 4.6 ms a step on the hillside with 32 cars and eight bots; test-traffic-junction, E2E-110–115.

Built in 0.21.0:

  • [x] Game pages (M6, apps/api/src/games.ts, takedowns.ts, apps/client/src/ui): ratings (1–5 stars, one per account, never one's own), reports and takedowns by the moderators FOI_MODERATORS names (an audit log; a game taken down answers 410 and opens no rooms), featuring, versions and remix (a draft credited remixOf), search and sorts (new, top, featured); a preview rendered top-down by Cycles on the CPU after every generation (preview.py, never failing a build) and committed for the bundled levels; the page of games (?browse) and a game's page (?game=<id>: Play, Join, stars, a report, Remix handing the draft to Claude Code); the MCP's game_remix; E2E-170–176.
  • [x] Items, inventories, containers and doors (M3, packages/sim/src/items.ts, interaction.ts, doors.ts): items as data (tags, a stack size, actions; six common ones in the catalogue, a mission declares its own), eight slots per character, pickups, crates and lockers as level blocks (block:container) or mission actors, every car's trunk, transfers checked by the server; one interaction rule (interactionOf) behind E and its prompt (get in, carjack, pick up, open, a door, Locked) with F use, G drop and X next slot; doors that swing with their collider, locked by a tag (block:door); the mission language's item and container actors, give, take, lock, unlock, spawnItem, hasItem and the objectives pickup, collect, use and deliver of an item; test-inventory, test-interaction, E2E-130–137.
  • [x] The city's vehicles and pedestrians with roles (M5, packages/sim/src/vehicle-classes.ts, packages/world/src/pavements.ts, packages/ai/src/crowd.ts): the taxi, the bus, the ambulance, the fire truck and the garbage truck (eighteen classes), liveries and features (light bar, siren, roof sign, ladder) a class carries and the client draws, the siren a held input (G at the wheel; NPCs in an emergency class run theirs); traffic of a weighted mix of classes, buses stopping at the kerb, long vehicles straight on at junctions, gridlocks undone out of sight; pavements, crossings and doors laid from any level's roads, pedestrians as civilians, commuters, joggers and shopkeepers, kept around the players by the crowd (40 on a main-* level); 2.9 ms a step on the hillside with 32 cars and 40 pedestrians; PROTOCOL_VERSION 6; test-new-vehicles, E2E-150–157.

Built in 0.22.0:

  • [x] The phone (M3, packages/missions/src/phone.ts, packages/sim/src/interactables.ts, apps/client/src/ui): the phone an item in the last slot (rules.phone), its contacts, messages with replies, calls, installed apps, call log and waypoint entries the mission runtime keeps per player (deterministic, sent to that player alone); the panel over the running game (T, Esc, 1–9); dialogue trees by call or face to face (E Talk at an NPC with the contact role), apps as data with buttons and short forms behind if conditions (near, hasApp, formIs), terminals (E Use or Hack) and keypad doors; the actions message, call, addContact, removeContact, installApp, removeApp, waypoint, clearWaypoint, the objectives talk and unlock; unreachable dialogue nodes rejected by the validator, every phone text moderated at publishing; a character's role on the wire from ROLE_CLS_BASE up; PROTOCOL_VERSION 7; a fifth e2e shard; test-phone, E2E-140–149.

Built in 0.23.0:

  • [x] Stealth and non-lethal play (M4, packages/sim/src/status.ts, packages/ai/src/awareness.ts, sentry.ts, stealth.ts): statuses in the sim hash and the snapshot (stunned 3 s, knocked out 60 s, asleep 45 s, down, dead; a non-lethal hit never takes the last hit point; noise or a shake wakes a sleeper); the tranquilizer on 7 with darts as items and the head shot, the knockout from behind, searching pockets; awareness (sight with ray checks and cover, hearing by radius, memory) and the alert phases normal, suspicious, alert, search, caution, kept apart from the police's Heat and in the AI hash; security cameras swept by the tick; the block:npc spawner and the mission's npc actor (one actor split by role with the phone's contacts); the objectives incapacitate and sneak, the stealth hooks, the values and events; PROTOCOL_VERSION 8 (an entity of 80 bytes); test-stealth and its game "Clean Exit", E2E-180–190.
  • [x] Locks, keys, spawners and garages (M3, packages/sim/src/locks.ts, spawners.ts, garages.ts): an owner (nobody, a player, an NPC, a mission) and a lock on every vehicle, in the hash and the snapshot; keys as items tagged key:vehicle:<id> (the phone's RentaCar key opens its car), E Unlock with one, a held E picking without (a crime when seen); cars carjacked or picked become yours; the block:vehicle-spawner (a class or a mix, locked, its key, a respawn) and the block:garage (a respray that clears the Wanted level unless the police see it, storage per player in the room, a mission delivery); the mission language's vehicle owner, locked and key, the garage actor, lockVehicle, unlockVehicle, giveKey, unlock for vehicles and deliver to a garage; a reset counter the e2e helper waits on; PROTOCOL_VERSION 9 (an entity of 84 bytes, an input of 17); test-locks-garages, E2E-160–169.

Built in 0.24.0:

  • [x] The social layer (M6, apps/api/src/social*.ts, apps/server/src/room-social.ts, packages/sim/src/griefing.ts, apps/nakama): friends and presence behind one interface (dev in memory or on file, Nakama's friends), parties of 2–4 that land in a room together and share a mission's progress (one runtime stepped with every member's facts, the stealth book summed, rewards shared or split), room chat with quick-chat and emotes through the game server (public rooms checked by the API's moderation, eight lines per ten seconds), party chat and direct messages through the API's feed, the Nakama runtime module's hook on public room channels; mute, vote-kick, player reports; safe zones and the ghost in the simulation; playerDamage enforced for shots, blasts, run-overs, darts and knockouts (rivals meaning outside your party); the rules respawn, safeZones, ghostSeconds, partyRewards; seven e2e shards; PROTOCOL_VERSION 10; test-safe-zones, E2E-210–223.
  • [x] A steady frame rate (packages/sim/src/proxy.ts, packages/net-client/src/mirror.ts, apps/client/src/renderer/{models,remotes,view-rect,level-culling}.ts, build/perf.mjs): kinematic proxies for every twin the player cannot touch, three catch-up ticks at most, the sim's O(n²) passes made cheap and Rapier stepped without its per-body JS sweep, models merged at load into thin-instanced batches per tint, actors outside the top-down view not posed or drawn, the level in 32 m culling cells, pooled effects; npm run perf (a production build, the empty, city and chaos scenarios, frame times, draw calls, the server's step): chaos (64 cars, 26 chasers, the police at three stars, 160 people, the player firing and throwing grenades) from 1 fps to 60 with no frame over 16.8 ms on an RX 570, the server's step 5.1 ms on average; E2E-230.

Built in 0.25.0:

  • [x] The people's animations and a cosmetic ragdoll (M2, assets/blender/models/{character,animations}.py, apps/client/src/renderer/{clips,anim-state,figure,ragdoll}.ts, packages/sim/src/acts.ts): a ten-part rigid figure (head, forearms and shins added, about 230 triangles) and 73 keyframed clips authored in Blender and exported as data the client samples into the batches' per-part matrices (no Babylon animation groups, so the people stay thin-instanced); a pure state machine from what the client knows, crossfades of 0.15 s, one-shots handing back; an act byte per entity (what a person does with its hands) and the phone in the input, PROTOCOL_VERSION 11; a Verlet ragdoll on the client only (15 points, ground and scenery rays, at most twelve active, none out of view), flung along the hit and blended into the get-up where the server stands the person; the animation gallery (?anim-gallery); npm run perf still 60.0 fps with no frame over 16.8 ms in city and chaos; test-on-foot, E2E-260–279.
  • [x] Directions as the player sees them: north up the screen (+z), east to its right (−x), bearings clockwise from north on the HUD (compassBearing); the test levels' names and texts, the tests and the guides made to agree.
  • [x] The pre-commit hook spell-checks the files a commit adds or changes; the developer's testing guide (docs/guide/playtesting.md): every level, what to try by hand and what to see.

Built in 0.26.0:

  • [x] Districts and buildings (assets/blender/kits/civic.py, assets/blender/levels/{town,cityparts}.py, packages/catalogue/src/themes.json): the civic kit's 22 families (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) with 2–3 colour variants; tags like OpenStreetMap, looks and anchors on every building of both kits; districts with a theme (six), a title, a density (built share, setback, height) and landmarks, and a mix or landmark by tag; block:district and LevelData.districts; places with tags and names; the catalogue's districts, themes, families, tags and anchors; test-buildings.
  • [x] The world's systems (docs/guide/city.md): zone kinds (restricted zones raising the alert or the Heat), races (paths of gates, laps, rivals, the race objective and HUD), spawn points by role (players, police, traffic, guards, hospital), the NPC roles driver, target and VIP, the room's clock with schedules and the crowd by the hour, gates and barriers, setSignal, shops and one Cash; anchors wired (shops at counters, the hospital's respawn, the police station's spawns, the crowd's roles by district); PROTOCOL_VERSION 12; nine e2e shards; test-race, test-city-life, E2E-290–308.

Built in 0.27.0 (the user's list of thirteen fixes, 2026-10-07):

  • [x] Solid physics (packages/sim/src/{world,substeps}.ts, docs/guide/driving-model.md): contacts handed to the solver 10 cm before the touch, up to four physics steps a tick for the pairs whose gap shrinks fast, contact stiffness at 240 Hz, soft CCD on simulated cars, wheels that stand only on the level; crashes at 50–150 km/h sink under 4 cm instead of up to 0.76 m, a person hit under 1 cm instead of 16; the cars near the player's drawn at the predicted time (PredictedRemotes), so a rammer pushes only when it is drawn touching; exits, resets and spawns that look for a free spot; no steering in the air; the sprint at 35 km/h, fleeing at 30; light pieces knocked loose by the vehicle's kinetic energy; the cost within a few per cent of 0.26 (npm run perf); test-collisions, E2E-310–315; PROTOCOL_VERSION 13.
  • [x] The in-game UI in the scene (apps/client/src/gui, §8.1, decision 8 in §13): the interaction prompt and the car tag projected every frame, the phone, the mission panel and race readout, the inventory strip and the container, shop and garage panels drawn with Babylon's GUI; React only for the helper UI and the stats and game-event overlay; E2E-320–324.
  • [x] Damage and gunfire as games draw them (apps/client/src/renderer/{gunfire,tracers,blood,wreck-smoke}.ts): the dead and the wrecks in their own colours (no red wireframe), a wreck scorched and smoking from its engine bay for as long as it lasts, blood that follows the person hit and leaves stains, tracers from the muzzle of the gun in the hand with a flash, one streak per pellet; E2E-340–346.
  • [x] Car glass and doors (assets/blender/models/{vehicles,panels}.py, apps/client/src/renderer/{car-doors,glass,cracks}.ts): see-through windows with the people inside drawn, a crack where a shot hits the glass, doors that swing open and shut for whoever gets in or out (a separate part per door in the models, the bus's and the ambulance's leaves), a vehicle gallery (?vehicle-gallery); E2E-350–354.
  • [x] Getting in and out at the doors (packages/sim/src/{boarding,seats}.ts, vehicle-seats.json from the models): the character goes to its seat's door, faces it and gets in there; out by the car's speed (stepping out at a crawl, bailing out rolling and hurt up to 35 km/h, staying in faster), a wreck's crash hurting those inside, the dead staying seated until it stops; people out right at their door or the nearest free spot; the AI deciding only when to press the key; E2E-360–365; PROTOCOL_VERSION 14.

Built in 0.28.0 (the way to V1, waves 1 and 2, and the user's playtest of Downtown job):

  • [x] The mission language, complete (workstream A, packages/missions): the verbs give, escort, chase, photograph, wait, the rest of escape and destroy, objectives in parallel groups, the triggers wanted:* and message:<id>.read, the minimap's route line, a party's markers; test-mission-verbs, E2E-370–384.
  • [x] The gameplay gaps (workstream B): weapons as items in sixteen slots and a game's loadout, a character's mission items dropped where it dies, the police at three stars (faster cars, nail strips), the Heat of breaking property and of speeding past the police; E2E-390–399; PROTOCOL_VERSION 16.
  • [x] "Downtown job" (workstream C, main-downtown): a 4 × 4 grid town of five themed districts round a bank one walks into, its vault behind a gate, five missions; E2E-240–249.
  • [x] Never stuck (the user's playtest, 2026-10-08; with the checkpoints below +988 product, +1,034 test lines, to 78,632 and 49,835): every mission a help line (helpline: a contact always in the phone who answers with the hint of the objective under way; the tracker says who to call) and every objective a marker where its verb points nowhere; a talk or a give whose person is dead fails, so the mission starts again; a target that hid comes back out after 30 s, and the tranquilizer never scares one; banners mid-screen for YOU DIED, MISSION FAILED (with why), MISSION COMPLETE, LEVEL RESET and DISCONNECTED; a page that loses its server holds everyone still, offers no prompt and joins again once the server answers; the ammunition in a pouch beside the strip, each weapon's tile reading its rounds; Downtown job's hints, markers and Dana's help line; E2E-400–406; PROTOCOL_VERSION 17.
  • [x] Checkpoints (packages/missions/src/runtime.ts, apps/server/src/mission-host.ts): every objective done saves one (checkpoint: false turns it off; none after a mission's last), the interpreter keeping its own state, each member's phone and wallet, the host the world (the run's actors replayed from their spawn commands with the state they had, the players' places, seats, slots, Cash and Wanted level, the cars they sat in); dying fails the mission ("You died."), and a failure goes back to the last checkpoint, its clocks running again; E2E-407.

Built in 0.29.0 (the user's second playtest of Downtown job, 2026-10-08; +384 product, +349 test lines, to 79,016 and 50,184):

  • [x] Playtest fixes (packages/sim/src/{character,carjack,damage,pedestrians}.ts, packages/ai/src/{chaser,roles}.ts, apps/client/src/renderer/{game,remotes}.ts): a passenger's camera follows the car as it is drawn, no stall and leap at each snapshot (E2E-408); a slow car's shove is a step aside, 2 m/s at most, and a pedestrian off the pavements strolls; the legend's Player page sets the player's max health, 25 to 250 (E2E-409); kerbs and real steps up to 35 cm are taken in stride, up and down, and a character that stood and did not jump keeps to the ground (no hop off a grazed planter, a kerb's corner or a seam of the ground), the figure never jumping (E2E-411); a scared mission target cowers behind its desk in plain sight for 30 s, still a body to dart or search, instead of vanishing indoors (E2E-403); the police never ram a car standing still or a wreck, box it in, and once the player has sat still for three seconds an officer pulls them out and arrests them, holding fire while a colleague is in the line (E2E-410); PROTOCOL_VERSION 18.

Built in 0.30.0 (workstream C's second game; +1,591 product, +1,426 test lines, to 80,607 and 51,610):

  • [x] "The Compound" (main-compound, assets/blender/levels/compounds.py, assets/blender/kits/compound.py, packages/ai/src/schedule.ts): the third curated game, stealth at the scale of a real place. A generated town round Zenful Campus: walls, a main gate and a delivery gate locked by keycards, a gap in the west wall, an annex whose fire stairs climb over the north wall, a lab one walks into, four cameras. The guards keep shifts, breaks off guard (their sight cut short) and rounds by the room's clock. Three missions on a non-lethal loadout, each failed by a kill, with Inés as every mission's help line and a hint on every objective; its perf scenario; E2E-250–259, 412; PROTOCOL_VERSION 19.

Built in 0.31.0 (landmarks and tiles, release 1; +576 product, +290 test lines, to 81,183 and 51,900; estimated +1,500 / +900):

  • [x] Layered navigation (packages/world/src/nav-layers.ts, checks.ts; packages/ai/src/pathfinding.ts, steer-foot.ts):
    • Floors. The person's grid keeps its ground as node i of each cell. Over it come upper nodes: every floor, deck, bridge, roof and landing with headroom.
    • Edges. Each node knows its surface's height just inside its cell's edges. Two nodes link where those heights meet within a step: stairs lead up their length onto the floor they reach and are never climbed from the side, and a table is no floor.
    • The ground grid. A slope's samples stand on what carries them, and a vertical face is as tall as it is where it comes nearest, so the top of a flight of stairs is no longer a wall.
    • Chasers plan from the floor they stand on to their target's floor (the last sighting carries its height).
    • Reachability (spawnReach, validateReach) walks the layers, and a road's points take the node at the road's height, so the street under an overpass stays apart.
    • Places with a height are checked open and reachable on their floor, and people and cars spawned there stand on it. Zones on a floor hold only who is within 3 m of it; the marker and the checkpoint respawn use the floor.
    • test-storeys, E2E-413.

Built in 0.32.0 (landmarks and tiles, release 2; +925 product, +268 test lines, to 82,108 and 52,168; estimated +2,200 / +1,100):

  • [x] Going up and down (packages/sim/src/ladders.ts, lifts.ts, character.ts; packages/world's ladder and lift blocks, the escalator parameter of the stairs):
    • Ladders. E takes one at its foot or its top: no gravity, W up at 2 m/s (3 running), off onto the floor at the top, the jump key lets go. The climb act and clip.
    • Lifts. Kinematic platforms on a timetable that is a pure function of the tick. Whoever stands on one is carried and stays on their feet.
    • Escalators. A flight of stairs whose slope carries whoever stands on it at 0.7 m/s.
    • Falls. Landing from more than 4 m hurts by the speed, from 12 m kills. A game's rules.fallDamage switches them off.
    • Reach. Ladders and lifts are jumps of the layered navigation, followed by the level's reach checks. The AI does not use them yet.
    • test-vertical, E2E-414 to E2E-417.

Built in 0.33.0 (landmarks and tiles, release 3; +1,101 product lines (about 950 of them Python), +489 test lines, to 83,209 and 52,657; estimated +2,600 / +700):

  • [x] Walk-in landmarks (assets/blender/kits/interiors.py, landmarks.py; the generator's tables in cityparts.py; @foi/catalogue's archetypes, anchors and the themes' names):
    • The interiors library. One style for every walk-in building: storeys 4.5 m apart, slabs with holes, walls with doorways, railings, fittings. Each storey above the ground floor is a part of its own with no ceiling, so it fades over a player under it.
    • Five landmarks: a mega-mall, a general hospital, a mensa, a castle, a ski lodge. Their stairs, escalators, lifts, ladders, a ramp, shops, lockers and a door are blocks their anchors carry, and every floor has its places.
    • Navigation. A flight standing on a floor is a structure, no longer the ground that hid the floor under it. A floor's edge ignores a surface covered within headroom. A slope's samples are judged at the slope's height.
    • Lifts. A platform coming down onto someone in its shaft sets them on top of it, where it used to push them through the floor. A lift's lowest stop is level with the floor it serves.
    • Go to lists the game's places (debug commands on) and puts the player at one, on its floor.
    • test-landmarks and its bundled game (Landmarks), E2E-418 to E2E-421. Cut from the plan: a separate test-interiors level (test-vertical and test-landmarks cover it), the mall's roof car park for cars (the roof is walked), colour variants (one per landmark).

Built in 0.34.0 (landmarks and tiles, release 4; +910 product, +536 test lines, to 84,119 and 53,193; estimated +3,200 / +1,500):

  • [x] Terrain tiles and the region template (packages/catalogue/src/tiles.json, region/; apps/tools/src/region-level.ts, gltf-write.ts; Tiles):
    • The grid. Tiles are 64 m with 4 m levels from 0 to 10, and each corner's level is shared by the tiles that meet there. Seven shapes cover every tile, so seams match by construction.
    • The solver. It spreads features and pins the rim and the spawn apron at 0. A cut-short feature is reported as an issue with its path, and the solver prints a map in text.
    • The ground. A vertex every 8 m, the triangles coloured by biome (meadow, forest, alpine), trees and rocks strewn by the seed.
    • No Blender. npm run level:region writes the level's own kit (a glTF the tools write), its recipe and its places, as the town generator does.
    • test-tiles-terrain, E2E-422 and E2E-423. Changed from the plan:
      • Tiles are meshed in code from their corners instead of picked from a Blender kit, so any corner pattern works.
      • Biome by height and slope replaces the variants' wave-function collapse.
      • Cliffs are cut.
      • The platform's spec (level_spec_set) takes regions in 0.36.

Built in 0.35.0 (landmarks and tiles, release 5; +904 product, +348 test lines, to 85,023 and 53,541; estimated +3,000 / +1,300):

  • [x] Roads on tiles (packages/catalogue/src/region/roads.ts, bridges.ts; RoadDef.kind; Tiles):
    • Five classes, from a dirt track to a highway: width, lanes, speed, grade and smoothing.
    • Routing. Through the tiles' edge middles: straight, or a quarter bend of 32 m. The ends are the spawn apron, an edge, a tile, a feature or an earlier road; via names tiles to pass. Crossings are at right angles, joins land on a road's centre line, and a road with no way through says so.
    • Heights.
      1. A highway's and an avenue's profile closes over dips.
      2. Every profile is levelled to its class's grade.
      3. The ends stay on the land, and every junction is flat across the wider road.
    • Bridges and ground. A deck with railings and piers wherever a road runs 4 m over the land. Elsewhere the ground is graded to the roads, with embankments and cuttings, and vertices are every 4 m.
    • Paint. The game paints a highway's hard shoulders.
    • test-tiles-roads, E2E-424 and E2E-425. Cut: the tiles that taper one class into another (two classes meet at a socket), tunnels (a deep cutting instead), interchanges.

Built in 0.36.0 (landmarks and tiles, release 6; +706 product, +203 test lines, to 85,729 and 53,744; estimated +2,700 / +1,200):

  • [x] Water and lava (packages/sim/src/water.ts, sinking.ts; the water block, the level's water; Tiles):
    • People. Wading over the knees at half pace. Swimming deeper than the chest: afloat with the head out, slow, no firing, no jumping, climbing out where the bottom comes up. A fall into deep water hurts nobody.
    • Cars. A car whose middle sinks 0.6 m under the surface floods: wrecked, its people put out to swim. It settles on the bottom, slowed by the water.
    • Lava kills and wrecks.
    • The AI plans no path through deep water or lava (the navigation grid).
    • The client. The sheets (water see-through, lava orange), the minimap, a swim clip, the HUD's wading and swimming, the hit log. The level previews draw them too.
    • test-water, E2E-426 to E2E-428; PROTOCOL_VERSION 20.
  • [x] Lakes, volcanoes and ski runs in regions (packages/catalogue/src/region/water.ts, the solver, the paint): no road through water, nothing grows in it, a road to a lake ends on its shore.
  • [x] Regions on the platform. The level spec is a union on template. PUT /drafts/:id/spec solves a region at once and answers with its issues or its map, which level_spec_set shows. A region builds without Blender, in the worker or in the API. The catalogue gains template.region and tile.*, and the MCP server serves foi://guide/tiles.
  • [x] main-highlands, the tiles' showcase: an alpine region with a lake, a volcano, a summit with a ski run and a highway (its game, "Lava Selfie", came in 0.39.0).
  • Cut from the plan: towns inside a region (a grid town as a large tile whose streets meet the region's roads); the showcase is open land.

Built in 0.37.0 (towns inside regions; +736 product, +435 test lines, to 86,465 and 54,179; estimated +1,150 to +1,450 / +700 to +950):

  • [x] Towns on a region's land (packages/catalogue/src/region/towns.ts; assets/blender/levels/relief.py, town.py, generate.py --relief; @foi/tools' region-level.ts; Tiles):
    • The spec. A region's towns (up to 3): grid towns whose street lines run through the middles of their tiles, blocks of 64 or 128 m, a district on a terrace where asked, steep hills and valleys; a road ends at a town by its name.
    • The solver. Footprints clear of the rim, the apron, water, mountains, volcanoes and ski runs; a level a tile inside a town, two under a steep hill or valley, never on its outermost tiles or along an avenue; terraces held level; issues with their paths.
    • The town generator on the land. Every street a profile between level junctions, the ground between them a membrane held at the pavements and the region's seam, buildings on pads level with their doors (plinths where the ground falls), park squares level; the region's ground leaves the footprint and meets the town's vertex for vertex. Flat towns unchanged byte for byte.
    • The pipeline. level:region and the worker run Blender for the towns and merge each one's kit, placements (<town>-…) and places (<town>.…) into the region's; the API hands a region with towns to the worker (or to its own Blender).
    • test-tiles-town, its contract (levels-town.test.ts), E2E-429 to E2E-431; every vehicle class climbs 18 % (slopes.test.ts).

Built in 0.38.0 (the kit gallery; +496 product, +120 test lines, to 87,710 and 54,710):

  • [x] The kit gallery (/?kit-gallery, apps/client/src/renderer/kit-gallery.ts): no server, everything the MCP server can build a game of, labelled side by side: every building (a district mix's archetypes and their variants, with their upper floors) and prop of the downtown, civic, landmarks, city and compound kits, loaded from the kits the Vite config serves beside the page; the seven terrain shapes, the features and the biomes built by @foi/catalogue as a region is, with their water and lava; the five road classes. The address looks at a kit, a section or a thing (=civic, =features, =volcano). E2E-432 and E2E-433 in test-buildings.

Built in 0.39.0 (playtest fixes; +221 product, +254 test lines, to 87,931 and 54,964; estimated +350 to +500 / +350 to +450):

  • [x] The parking brake (packages/sim/src/vehicle.ts, Driving model): a vehicle spawned or left by its driver holds on its brakes and, on a slope, where it came to rest, until somebody drives it; protocol 21. parking.test.ts, E2E-434.
  • [x] The galleries' free camera (apps/client/src/renderer/gallery-camera.ts): pan, zoom and orbit anywhere in the kit, vehicle and animation galleries. E2E-435.
  • [x] The semi's trailer in the vehicle gallery. E2E-353.
  • [x] The highlands' game (packages/missions/content/main-highlands.json): "Lava Selfie". E2E-436.

Built in 0.40.0 (the menus; +2,150 product, +621 test lines, to 90,081 and 55,585; estimated +2,850 / +1,450):

  • [x] The main menu (apps/client/src/ui/MainMenu.tsx, renderer/menu.ts, menu-scene.ts, flames.ts): a title screen over a rented sedan burning (low-poly flames, smoke, a flickering light), Play (the levels that ship, their pictures and blurbs), the community's games, the galleries, Settings and Controls, Quit in the desktop app; quiet synthesized cues; the look of a tactical briefing, deadpan. A bare address and the desktop app open on it; ?play and ?server= go straight into the game. E2E-437 to E2E-439, E2E-16.
  • [x] Level rooms (apps/server/src/level-rooms.ts): /game?level=main-* joins a room of that level, opened on demand with its bundled game; FOI_LEVEL_ROOMS=0 turns it off.
  • [x] The pause menu (ui/PauseMenu.tsx, packages/protocol/src/pause.ts, protocol 22): Esc or the toolbar; a room whose one player paused stops ticking until they resume; with others it goes on. E2E-440 to E2E-442.
  • [x] Settings (ui/settings.ts, kept in the browser): the volumes, the look-ahead toward the cursor, the wheel's direction, the HUD, the minimap's size, the camera's height and its zoom out at speed (on or off, how far, how quickly), how long texts stay. E2E-443, E2E-560 to E2E-563.
  • [x] Changa for every text (apps/client/src/fonts/), loaded before anything draws text.

Built in 0.41.0 (the hillside rebuilt; +1,729 product, +418 test lines, to 91,810 and 56,015; estimated +1,890 / +650):

  • [x] main-hillside as a region (apps/level-editor/assets/kits/main-hillside.spec.json): 14 × 14 tiles, a town of five districts and an old town on the hills, a lake, a volcano, a highway, country roads and a dirt rally track; the players start in the town (a region's start). Its game of four missions. The old generator is test-curves (assets/blender/levels/curves.py). E2E-61, E2E-66, E2E-73, E2E-444, E2E-67.
  • [x] Lived-in towns (assets/blender/levels/dressing.py, a town's dressing): street furniture by theme, bus stops, car parks of parked cars, a building site, back yards, playgrounds and a market.
  • [x] The street kit (assets/blender/kits/street.py): about fifty pieces for the streets, yards, building sites, lakes and volcanoes, in the kit gallery. E2E-432.
  • [x] Lakes and volcanoes (packages/catalogue/src/region/dressing.ts, heights.ts, terrain.ts): a crater 8 m deep with its lava 2 m under the rim, a flow of lava down a flank, a lake's beach, jetty and boats, a volcano's lookout, boulders and fumaroles; the lava's crust and glow, smoke and steam near the camera (apps/client/src/renderer/lava-view.ts, vents.ts). E2E-445, E2E-446.

Built in 0.41.1 (physics fixes; +494 product, +443 test lines, to 92,304 and 56,458; a fix, not estimated):

  • [x] A client's ticks, on the server (apps/server/src/game-server.ts, packages/net-client/src/clock.ts, predictor.ts, apps/server/src/loop.ts, protocol 23): a client's input n is its tick n; the server runs exactly one of a client's ticks with each of its own, a tick whose input has not come in runs on the held input and is spent, so a late input is too late (its presses kept for the next tick); the client skips the numbers of the ticks a stall drops; the server catches up one tick per callback, reading the sockets in between. A hitch of the page no longer drives the car on twice (the lurch forward after a stall). server.integration.test.ts, E2E-448. See Netcode.
  • [x] Cars against people (packages/sim/src/car-hits.ts): after the step, from the speed the two closed at along the contact: a hit throws the person off the front faster than the car and up onto the bonnet, turning to come down lying; a body the car catches goes over its roof; one lying in the road is driven over; the car slows by the momentum it hands on; a body down lies flat and slides, never tumbling end over end. People near the player's car are drawn where the prediction has them, limp at the hit (Mirror.predictsDown). car-hits.test.ts, mirror.test.ts, E2E-447.
  • [x] A page sent to a server not listening yet waits for it (apps/client/src/renderer/net.ts): the desktop app opens its menu before its own server has booted the hillside (some 13 s here); a level chosen then joins once the server answers instead of staying disconnected. E2E-16.

Built in 0.42.0 (the platform's deployment, phases 1 to 4 of documents/DEPLOYMENT_PLAN.md; +1,046 product, +551 test lines, to 93,350 and 57,009, against +810 to +1,280 and +350 to +550 estimated):

  • [x] The Compose stack again, and its smoke grown (deploy/): every service healthy from empty volumes, ten steps green and four more (a city picked as the menu does, a test level refused; the production build's font, kits and previews; a region built and played without towns, and with one). Two platform faults it found: a region's towns lost their places in builds (writeRegion now answers the places its file holds), and a build checked that every generated place could be reached (a crater, a cliff's top) instead of the ones its missions use (placesUsed). Measured: 1.5 GB at rest, 100 to 150 MB a room, one core for every room (four light rooms 66 % of a desktop core, p99 14.8 ms), a city's room stalling the others for its navigation build.
  • [x] On the internet through ngrok (docs/guide/deploying.md): real secrets first, the smoke through the tunnel, a game made over MCP on HTTP and played, the desktop app on it.
  • [x] Every release carries the stack (build/pack-stack.mjs, the Release workflow's stack job): the images built and smoke-tested, pushed to GHCR when FOI_PUSH_IMAGES says so, the bundle a host installs with Docker alone; the Caddyfile mounted for the host to edit, security headers.
  • [x] Play online, play local (apps/client/src/ui/online.ts, electron/main.ts, bridge.ts): the platform asked its /info across origins (the API's CORS), its address typed or built in (FOI_PLATFORM_URL); the app's own server started only to host, friends joining at the address the pause menu shows; a server of another protocol says both releases (RELEASE_VERSION), a room's refusal says why. E2E-449 to 452.

Built in 0.43.0 (wave 1 of the user's list of 2026-10-11, docs and quick fixes; +1,053 product, +667 test lines, to 94,426 and 57,820, against +1,050 / +700 estimated):

  • [x] Making a game on Windows, and moderation (README.md, docs/guide/creating.md, deploying.md, the new moderation.md): PowerShell beside every shell block of the sign-in and claude mcp add (placeholders only), the step that publishes a draft (draft_publish, public), a guide to switching moderation on (wordlist, claude, chain, the moderators, what is and is not checked); deploy/preflight.mjs refuses an unknown mode or claude without its key and warns loudly when a stack with real secrets runs with moderation off.
  • [x] Firing from a car takes the pistol selected, with rounds (packages/sim/src/weapons.ts: weaponHeldAt, fireBlock): seated, the weapon held is the selected slot's only when it is the pistol; the HUD says why another does not fire (rifle · can't fire from a car (2: pistol)); a crew in a car selects its pistol (carSlot). Protocol 24. E2E-22, E2E-30 and E2E-390 reworded.
  • [x] Opening a trunk turns the player to face it (interaction.ts: openedFacing, character.ts). E2E-454.
  • [x] Steering wheels and front wheels turn (assets/blender/models/vehicles.py, models.ts): a steering wheel before the driver of everything on four wheels or more, one mesh per size, turned about its column; the front wheels turned about their hubs by the simulation's own angle (wheelAngle, steerLockAt); pieces of their own, shared by every model of the same shape; the hulls unchanged. E2E-455; E2E-353 extended.
  • [x] No legs under a car (vehicles.py, riders.ts): a seat whose hips sit inside a car's solid interior is marked legs: hidden and its rider drawn without legs (the compact's feet hung 40 cm below its body). E2E-456.
  • [x] The galleries, quicker to get around (gallery-camera.ts, the new gallery-ui.ts, kit-gallery.ts): a speed from a quarter to eight times kept per gallery, a drag that follows the ground from high up, keys typed into a field left alone; a panel that finds a thing by its name, goes to a section or a kit and maps the whole; labels for the nearest things big enough to read (150 at most); a walk-in building's stairs, escalators, lifts, shops and named places drawn at their anchors, [ and ] taking its storeys off and back. E2E-457 to E2E-460.

Built in 0.44.0 (wave 2 of the user's list of 2026-10-11, combat spectacle; +1,790 product, +1,061 test lines, to 96,216 and 58,881, against +2,300 / +1,400 estimated):

  • [x] Cars catch fire and blow up, in chains (the new packages/sim/src/fire.ts, run by the server's world alone, the rule vehicleFire): a car at 12 hit points or below catches fire (a blast lights it at 50, a bullet at 25), burns a fuse of 5 s plus a second per metre of its half length and blows up in a blast as big as it is (a car's 7 m, a bus's 11.5), killing whoever is still in it and throwing the wreck up; a blast lights the cars in the inner two thirds of its reach with fuses of 0.3 to 1.2 s, so a row goes up one after the other; the whole chain is its igniter's (15 Heat, heard); water puts a fire out; chasers, the police and the traffic leave a car on fire and never take one. The client draws smoke from 35 hit points, flames, the burning wreck (vehicle-fire.ts), the HUD's FIRE — get out!, a boom and a crackle, and the camera shakes. The mini level test-fire. Protocol 25 (burning, exploded). E2E-461 to E2E-464.
  • [x] Weapon models, rockets and explosions (the new assets/blender/models/weapons.py → weapons.glb, models.ts): every weapon drawn in the hands as its model (one-handed along the forearm, two-handed from the right fist toward the left), the shot leaving by the model's muzzle, the pistol held out of a seat's window for a moment after a shot; rockets flying as the rocket's model, nose first, trailing smoke, grenades tumbling; every blast drawn as a flash, a fireball, a shockwave, a smoke column and a scorch (the new explosion.ts, replacing the wire sphere). E2E-465 to E2E-467; E2E-260 extended.
  • [x] Officers throw the nail strips (packages/sim/src/nails.ts, packages/ai/src/nail-strips.ts): at three stars a strip comes with its unit, a police car parked at the kerb and an officer who throws it; it unrolls across the road from the kerb in a second (nailSystem, deterministic, hashed, the client's twin kept to the snapshot's length) and is reeled in once the player has passed, the unit leaving once nobody is near. Strips drawn as chains of red scissor links with spikes (spike-strip.ts); a flat tyre's wheel drawn squashed onto its rim, a tyre going flat popping and a flat rim at speed sparking (flat-tyres.ts). E2E-396 reworded, E2E-468.

Built in 0.45.0 (wave 3a of the user's list of 2026-10-11, its terrain half: varied terrain and plot details; +2,662 product, +541 test lines, to 98,878 and 59,424, against the wave's +6,050 / +1,800 estimated for terrain and the OSM buildings together, the buildings next):

  • [x] Eight biomes, rich ground (packages/catalogue/src/tiles.json, region/ground.ts, terrain.ts): farmland, steppe, desert, autumn and coast beside the meadow, the forest and the alpine; a region's details: rich paints its ground in the biome's tones by seeded value noise (moister and drier grass, dry patches, bare earth, flowers, two rocks) and grows its trees in groves of its own kinds; plain regions stay byte-identical.
  • [x] Plots and points of interest (region/plots.ts, the new nature kit of 60 pieces, assets/blender/kits/nature.py): every tile out in the country dressed by up to two composers, weighed by its biome and its tile; a pond dug into the land with water, a ruin, a camp, a viewpoint on a hilltop and an abandoned car, each a place, whole or not at all.
  • [x] Landuse (region/landuse.ts): fields with a farmyard, an orchard, a vineyard, a wood, a meadow, a heath, a quarry, a cemetery, allotments, a campsite, a park, a wetland, each painted, dressed and named; every place on a clear spot.
  • [x] The budget kept (apps/tools/src/region-level.ts: trimToBudget): what a region cannot hold is trimmed when it is written, the smallest details first, spread over the map.
  • [x] Ground cover (apps/client/src/renderer/ground-cover.ts): tufts, flowers and pebbles near the camera read off the ground's colours, three draw calls, a quarter as much without a GPU.
  • [x] The levels that ship: main-highlands and main-hillside rich, with landuse areas (the hillside's towns byte-identical); test-plots, the mini level (E2E-469 to E2E-471); E2E-472 (the rich highlands within the budget); the kit gallery's biomes in their rich tones, its landuse section and the nature kit (E2E-432).

Built in 0.46.0 (wave 3a of the user's list of 2026-10-11, its buildings half: OSM-driven variety; +2,367 product, +302 test lines, to 101,245 and 59,727; the whole wave +5,029 / +843 against +6,050 / +1,800 estimated):

  • [x] The tag snapshot (npm run osm:tags, apps/tools/src/osm-tags.ts): the 250 most used values of ten OpenStreetMap keys with their counts and descriptions, from taginfo, committed (packages/catalogue/src/osm/osm-tags.json, ODbL); never asked for at runtime.
  • [x] A recipe for every tag (packages/catalogue/src/osm/recipes.json, checked by recipes.ts): 125 buildings to generate, 34 hand-made archetypes, 51 props, 20 landuse areas, 5 districts' landuse, 15 skipped with why; the looks and the signs' 7 × 7 glyphs.
  • [x] The generator (assets/blender/kits/osm.py, osm_forms.py, osm_parts.py): eleven forms and their shapes, eight roofs, looks, accents, frontages, signs with their glyph on the roof read from above; six kits (osm-civic, -commercial, -residential, -industrial, -leisure, -facilities: 142 building variants, 19 facilities, 51,364 triangles in all) and buildings.generated.json, deterministic.
  • [x] Districts by landuse and wildcards (town-fields.ts, town.py, cityparts.py): a district's landuse, mixes of shop=*, family:<f>, landuse:<l> weighed by how common each building is; the themes high-street and farm-village; the existing towns byte-identical. The catalogue lists every generated building, the landuses and a lane road class (paved, one lane shared).
  • [x] Its level and its tests: test-osm-buildings (E2E-473), the kit gallery's six new kits and a building found by its tag (E2E-432 extended, E2E-474).

Built in 0.47.0 (wave 3b of the user’s list of 2026-10-11: vehicles, routines and actions; +2,135 product, +1,338 test lines, to 103,380 and 61,065, against +4,400 / +1,500 estimated):

  • [x] Spots (packages/world/src/spots.ts): the furniture brings its places with it (a bench's seats, a bus stop's queue, a vendor's place and its counter, a cash machine, a petrol station's pumps), in each kit piece's own frame, so no level file changes; one person a seat (SimWorld.spotTaken, hashed).
  • [x] Sitting, the bank, food and filling up (packages/sim/src/sitting.ts, bank.ts, fuel.ts): sit on a bench (the breath back to three quarters, the Heat cooling three times as fast unseen); a bank balance beside the Cash that the fines never touch, at the cash machines for a $2 fee; kiosks and market stalls that sell food (hot dogs, sodas, coffee, ice cream); fill up at a pump (the boost full, recharging twice as fast for two minutes). Protocol 26 (a third flags byte: sitting, fuelled; the inventory's bank). E2E-475 to E2E-478.
  • [x] The city's routines (pedestrians.ts, packages/ai/src/crowd.ts): seven roles appended (dog walker, vendor, smoker, queuer, sitter, officer on a beat, street sweeper), a third of the crowd by the hour at their free places; the beat officer the police's eyes; the queue boarding a bus that stands at its stop; buses stopping at the level's bus stops, couriers pulling in, the country's own traffic (RURAL_MIX). Drawn in six new clips, a dog walked on its lead and a sweeper's cart (companions.ts, models/companions.glb). E2E-479, E2E-480.
  • [x] Five classes (vehicles.py, vehicle-classes.ts): the pickup (its load bed a ten-slot trunk), the limo (six seats), the delivery scooter (a top box), the ice-cream van (pastels, a jingle at a crawl, a hatch that opens a counter when it stands) and the tractor (40 km/h, all pull); a trunk by class. E2E-481, E2E-482.
  • [x] Its level: test-city-actions (a high street with the furniture, a petrol station; its bundled game names places for Go to).

Built in 0.48.0 (wave 4a of the user's list of 2026-10-11, bandwidth and CPU; +1,158 product, +946 test lines, to 104,538 and 62,012, against +1,450–2,300 / +1,950–2,950 estimated):

  • [x] The load harness and the metrics (tests/load/run.ts, npm run test:load; apps/server/src/metrics.ts, GET /metrics with prom-client): bots over real WebSockets reading every snapshot with the client's decoder; the loop's step histogram, late steps and stalls, every room's figures, the process's.
  • [x] Delta snapshots (packages/protocol/src/snapshot-v2.ts, quantize.ts; protocol 27): what changed since the last frame a client was sent, in groups (a quantized position, as steps when small; a rotation in 32 bits; a velocity, a spin; what drives it, what a person does, its flags, who it is; the client's own pose in full), keyframes and a check every 60 frames, a resync when the client loses the thread; every entity quantized once a tick, every frame turned whole on the client, which pushes only the entities that changed (or stand still) to its interpolation.
  • [x] Tiers of the area of interest and backpressure (game-server.ts): far poses every second or fourth snapshot (a person sitting in a car every fourth), what entities are and do at once; events to the players near them; a backed-up connection skipped, then closed. permessage-deflate on (measured: half the wire, no more CPU).
  • [x] The navigation off the loop (packages/ai/src/system.ts: buildAiStatic, aiStaticOf; apps/server/src/nav-cache.ts): built in a worker, once per level, rooms opened when it is ready. Results in Performance: the hillside's client from 263 to 21 kB/s decoded, 12 kB/s on the wire. E2E-484.

Built in 0.49.0 (wave 4b of the user's list of 2026-10-11, the fleet on Hetzner and its monitoring; +1,931 product, +1,121 test lines, to 106,469 and 63,133, against +1,450–2,250 / +900–1,400 estimated):

  • [x] Admission and draining (apps/server/src/capacity.ts, http.ts, index.ts): no new room past FOI_CAPACITY players, while the loop's p95 step is past 8 ms with players on it or while draining (POST /rooms 503, a city's room to open refused full, never the city the default room plays); SIGTERM or POST /drain tells the players in the chat, closes them as restarting (4503) at the deadline and stops. The page says full and tries again in 30 s, or says the server restarts and plays again soon. E2E-485, E2E-486.
  • [x] Several processes a host (supervisor.ts, FOI_PROCESSES): consecutive ports, {port} in the settings, restarts with a backoff, SIGTERM passed on.
  • [x] The registry (fleet-client.ts; apps/api/src/fleet-registry.ts, fleet-routes.ts): a heartbeat every 5 s with the fleet's secret, forgotten after 30 s; internal addresses held to the fleet's networks and public ones to its domain; rooms placed by players over capacity, the next server when one refuses; GET /play?level= sends a city's players to its fullest room with a place left; the API's private address for the fleet's artifacts and moderation (FOI_API_FLEET_URL).
  • [x] The Hetzner scripts (deploy/fleet/): add-server (network, firewall, key, server, DNS record, files over SSH, waits for registration), remove-server (drains, then deletes), fleet-status (players, rooms, € a month and a room-hour).
  • [x] Monitoring (deploy/monitoring/, the monitoring profile): Prometheus (15 days; the fleet found through the registry), Grafana on /grafana with a login and three dashboards (fleet, rooms, API), the alerts; the API's own metrics (apps/api/src/metrics.ts).
  • [x] Fix: running into a car is no hit (packages/sim/src/car-hits.ts): a hit is the car's own speed into the person, less what the person was getting away at, no longer their speed together; a player getting in as a passenger from behind a parked car ran round its corner at 35 km/h, grazed the bumper and was knocked down, the way in given up and the driver charged with it (E2E-10 failing on CI).

Built in 0.50.0 (wave A of the iteration of 2026-10-12: the roadmap's decisions, side hits, the test plan's open rows, key remapping; +691 product, +691 test lines, to 107,172 and 63,845, against +900–1,300 / +1,000–1,300 estimated):

  • [x] Decisions (docs/guide/roadmap.md, documents/GAME_DESIGN.md): keyboard and mouse only (no gamepad), no test-seats level (the seats are covered on test-drift-pad), a car may tip over in a hard ram, remappable keys the one accessibility setting.
  • [x] Only a hard ram tips a car (packages/sim/src/vehicle.ts, vehicle-classes.ts SIDE_SCRUB, ROLL_STEADY): a car shoved sideways scrubs its tyres at about its weight instead of gripping at four times it, and a capped, damped roll spring holds it on its wheels; a T-bone at a coast and a slow ram shove it aside, a ram at full throttle from about 30–40 km/h tips it (vehicle.test.ts, "side hits").
  • [x] The test plan's open rows (E2E-05, -06, -11, -12, -15, -31, -46, -483): the ramps and LANDING, the bridge, tunnel and garage, the stairs, a pedestrian knocked down and killed, a crash and a wreck, a scheduled NPC lunching on a bench, a still frame against a baseline per platform and renderer (npm run test:e2e:snapshots, the e2e-snapshots workflow). New debug commands: a pedestrian stood ahead, the car's health, Go to x, z (@x,z,bearing); PROTOCOL_VERSION 28.
  • [x] Key remapping (apps/client/src/ui/bindings.ts, KeysList.tsx, renderer/keyboard.ts): every game key but Esc, Enter, the number keys and the mouse, changed on the menus' Controls screen (a key another action had swaps), kept in the settings; the keyboard takes a key to its action's default key, and every text names the player's keys (i18next's k.<action>). E2E-487.

Built in 0.51.0 (wave B of the iteration of 2026-10-12: privacy, terms, consent and telemetry; +1,293 product, +480 test lines, to 108,465 and 64,331, against +1,300–1,900 / +700–1,000 estimated):

  • [x] The privacy policy and the terms (apps/client/src/ui/LegalPage.tsx, legal.ts, the locales' legal): pages of their own (?privacy, ?terms) in English and Italian, written for review, naming who runs the platform as FOI_LEGAL_OPERATOR, FOI_LEGAL_CONTACT and FOI_LEGAL_COUNTRY say (the API's /info; neutral words without, and the stack's preflight says so); linked from the main menu, the pause menu, the game pages and the sign-in form, which says signing in accepts the terms and sends their version (the API's consents). E2E-490.
  • [x] Consent at the first start (ConsentDialog.tsx, Settings → Privacy): crash reports and anonymous statistics, both off until the player says yes; the answer kept and changed in the settings, a new anonymous install id on request. E2E-488.
  • [x] Crash reports and statistics (apps/client/src/ui/telemetry.ts, ErrorBoundary.tsx, electron/main.ts; apps/api/src/telemetry.ts): with consent, the page's errors, the overlay's error boundary, a lost WebGL context and the desktop app's main process (its page or its game server gone) report crashes, scrubbed of home folders, query strings, e-mails and tokens, once each; the session, the level, missions begun, done or failed and a minute's frame times batched. The API keeps them FOI_TELEMETRY_DAYS (90) without an address, a few a minute per install, groups the crashes by fingerprint for the moderators (GET /telemetry/crashes) and counts them for Prometheus (foi_client_*) and a fourth Grafana dashboard (clients). The game server and the API log an uncaught error at fatal before they stop. E2E-489.

Built in 0.52.0 (wave C of the iteration of 2026-10-12: dynamic time and weather; +2,161 product, +1,103 test lines, to 110,626 and 65,434, against +3,600–5,200 / +1,300–1,800 estimated):

  • [x] The model (packages/shared/src/weather.ts, clock.ts): the clock counts game minutes from the room's first midnight and only goes forward (setClockTime to the next such time, skipTime); the weather is a pure function of a seed, a climate and those minutes (fronts every half hour, mostly a four-hour regime's kind, by the climate's odds; a script over them; the roads' wetness and the snow worked out over the last six hours), so a skip lands in the weather of then; daylightAt.
  • [x] The level's sky (packages/catalogue/src/sky.ts, apps/tools/src/level-sky.ts, the worker): a level spec's climate and clock in every template, written into the level's sky at export (a region's biome, temperate for a town); twelve generated levels re-exported with theirs.
  • [x] Play (packages/sim/src/weather.ts, packages/ai/src/visibility.ts): sim.sky every tick, hashed; wet roads, mud and snow keep 0.8, 0.68 and 0.6 of the tyres' grip and brakes (traction half the loss: gently), a wreck burns out sooner in a downpour; the dark (where the room's sky darkens sight), fog and rain shorten sight, a street lamp (@foi/world's lamps.ts) brings it back, rain masks noise; the crowd thins in the rain, the traffic slows on wet roads, a skip puts people on schedules where their hour has them. The determinism gate runs in an autumn downpour.
  • [x] Missions (packages/missions): rules.weather (climate, start, intensity, changes), setWeather, skipTime (only the last hour's time: events), the weather condition and weather:<kind> events, in the catalogue.
  • [x] The server and the wire (apps/server/src/game-server.ts, mission-host.ts; PROTOCOL_VERSION 29): FOI_WEATHER (on but on test levels; the e2e servers off), the weather and skip frames, the debug weather and a skip of hours; the client's prediction world gets the clock and the weather.
  • [x] The picture (apps/client/src/renderer/daylight.ts, weather-view.ts, night-view.ts): the sun's arc, a blue night no darker than Settings → Display → Brightness at night (55 % by default), rain and snow round the camera, fog (the minimap clear), wet asphalt shining, snow on the lawns, lightning and thunder, rain and wind sounds; lit windows, pools of light under the lamps, headlights; the weather's icon by the clock, "3 hours later" over a fade, the View tab's picker. npm run perf's storm-night: 59.8 fps on the GPU, p99 16.8 ms.
  • [x] The mini level test-weather (its recipe and its game, Night Drive): E2E-491 to E2E-497. The guide: Time and weather.

Built in 0.53.0 (wave D of the iteration of 2026-10-12: composable tunnels and "Rush Hour"; +575 product, +674 test lines, to 111,207 and 66,128, against +2,200–3,600 / +1,350–1,500 estimated: the game is data, which the counter does not count, and the tunnels are the bridges' builder turned over):

  • [x] Tunnels in regions (packages/catalogue/src/region/roads.ts, terrain.ts, tunnels.ts; apps/tools/src/region-level.ts): a road's tunnels: "auto" lets the router keep its grade under land that rises 6.5 m or more over it instead of climbing or going round (under humps up to 256 m wide, climbing costing the search at most 0.4 a tile there), the land over it rebuilt as a roof (a structure that fades over a player inside, renderer/overhead.ts), portals of stone at either end, lamps inside; several on one road, through any feature; regions without it are byte for byte what they were. The mini level test-tiles-tunnel (E2E-498–500, its contract packages/world/src/levels-tunnel.test.ts). The guide: Tiles.
  • [x] The lighthouse (packages/catalogue/src/region/lighthouse.ts): a hill's lighthouse: true stands one on its top (a building of the region's own kit, its lamp room lit at night, a telescope beside it, the place <hill>_lighthouse).
  • [x] main-bayside (apps/level-editor/assets/kits/main-bayside.spec.json): a 16 × 16 coast region, the coast's climate from half past seven, a lived-in town of 5 × 3 blocks on a bay (the General Hospital, a car rental, a body shop, a hotel, a bus station), its beach and dock, a bypass through the headland in a tunnel, a link road, a coast road to the lighthouse; its contract packages/world/src/levels-bayside.test.ts, its picture on the main menu, npm run perf's bayside (60.0 fps on the GPU, p99 16.8 ms).
  • [x] "Rush Hour" (packages/missions/content/main-bayside.json, the fifth curated game): Rita dispatching, the RideNow app; Surge Pricing (a cab unlocked by the app, two fares escorted against the clock), Golden Hour (the ambulance, a swimmer from the beach, the rain coming in), Gridlock (a meter thief chased through the tunnel, the police at two stars), Night Shift (fourteen hours skipped, a race of three rivals by night), Last Call (the dawn fog, a parcel, a bench, a photo of the lighthouse). E2E-501 to E2E-506.
  • [x] Fixes (apps/server/src/nav-cache.ts, apps/client/src/renderer/main.ts, apps/client/vite.config.ts): the level rooms open under npm run dev (its node --watch workers report their modules first, which was taken for the navigation: every city of the menu said "This server has no such room"); a page its server turns away is no crash report; the dev server answers /api with 404 while no API listens.

Built in 0.54.0 (wave E of the iteration of 2026-10-12: the creator platform; +3,219 product, +2,379 test lines, to 114,426 and 68,507, against +2,800–4,400 / +1,700–2,300 estimated; the OAuth flow alone +1,452 / +1,244):

  • [x] A big level by its hash (apps/server/src/level-files.ts, apps/client/src/renderer/level-cache.ts; PROTOCOL_VERSION 30): a page that asks (?levels=hash) is welcomed with a level of 64 kB or more named by its SHA-256 and length; the game server serves its open rooms' levels at /levels/<hash>.json (immutable, gzipped, open to any page), the page keeps them in the browser by hash (the Cache API, else IndexedDB) and holds the frames that come while it downloads for the game. Bots, older clients and the smoke get the level inline. E2E-507 (main-bayside's welcome under 50 kB, the second game from the store).
  • [x] Games pinned to what built them (@foi/protocol's BuildManifest, @foi/missions' migrate.ts, @foi/world's migrateLevel, @foi/shared's migrate and compareReleases): every build's artifact and every published version say the release, the protocol, the formats and the generator (the town generator's scripts by hash, Blender's version); game documents and levels carry a schemaVersion migrated a step at a time wherever one comes in (every bundled game and level checked); a newer format or release line is refused, saying so; the fleet places a game only on servers of its release line or newer.
  • [x] The kit library export (npm run kits:export, apps/tools/src/kit-library.ts, packages/catalogue/src/kits.generated.json, kit-pieces.ts): every shared kit piece's footprint, triangles, tags (its sidecar's, else derived from its kit, kind and name), anchors and parts, as kit-piece catalogue entries the MCP's search and get show; level:check fails when it drifts from the kits.
  • [x] Your games and a build's preview (apps/client/src/ui/YourGames.tsx, renderer/build-preview.ts; the API's GET /games?owner=me, ?friends=1, GET /drafts with each draft's last build and playtest rooms): the page of games' Friends' and Your games; ?preview=<artifact> draws a build's level under the galleries' free camera with its places to fly to, linked from Your games and the MCP's draft_build. E2E-508, E2E-509.
  • [x] OAuth for the MCP server (apps/api/src/oauth.ts, apps/mcp/src/http.ts): the API as the OAuth 2.1 authorization server (metadata, dynamic registration, a sign-in and consent page, PKCE S256 only, rotating refresh tokens, revocation; its clients, codes, grants and refresh tokens in the memory, file and Postgres stores), the MCP server's protected-resource metadata and its 401 with WWW-Authenticate; claude mcp add --transport http five-on-it https://<site>/mcp with no header, the pasted token kept as the alternative; the stack's smoke runs the flow headless. E2E-516, E2E-517.

Built in 0.55.0 (wave F of the iteration of 2026-10-12: the unreliable channel; +2,821 product, +3,254 test lines, to 117,247 and 71,761, against +2,100–2,900 / +2,200–3,000 estimated; the tests over, their property test and the transport's fakes):

  • [x] Stage 0 (a spike, not merged): werift in a worker thread and Chromium open a negotiated, unordered, no-retransmit channel through peer-reflexive and host candidates with no STUN in about 350 ms, 0 lost at 60 frames a second, 11 % of a burst of 2,000; bundled standalone; full ICE. werift 0.24.4, pinned.
  • [x] The acked snapshots and the inputs four to a frame (@foi/protocol's snapshot-acked.ts kind 6, snapshot-reader.ts, kind 7 in codec.ts; @foi/net-client's InputHistory; apps/server/src/input-queue.ts; PROTOCOL_VERSION 31): over a transport that loses frames (Transport.lossy) each frame is the diff onto the one the client acknowledged, so every frame shown is the ordered one of its tick whatever was lost; events repeat until acknowledged (once each); inputs inserted by number, a press taken once whatever is duplicated; the property test (200 seeds × 300 frames through loss, duplicates, reordering and lost acknowledgements: never a resync, +4 bytes a frame at no loss); the lossy memory link; K simulates 0–20 % loss in the page. E2E-518.
  • [x] The WebRTC data channel (apps/server/src/rtc-*.ts, the worker bundled as rtc-worker.mjs; apps/client/src/renderer/rtc.ts; @foi/protocol's rtc.ts): the inputs and the snapshots on a channel the page offers once welcomed, the WebSocket kept for the rest and as the fallback (a failure, 1.5 s of silence, the server's close, 30 binary frames over the socket, the debug rtcDrop); FOI_RTC (off by default, on under npm run dev, off in Play local), its ports sliced per process, /health and foi_rtc_* metrics, an alert and dashboard panels; the Compose stack's UDP range (off by default) and the fleet's firewall; werift's DTLS record count moved past its handshake's, its SCTP's never-give-up worked round (the server resends once at most). E2E-520 to E2E-522.
  • [x] A client behind is sent one snapshot round in four once twelve acked frames wait unacknowledged, so its frames never outrun what the encoder keeps (a spiral of keyframes without it).
  • [x] The load harness over the channel and with loss (npm run test:load -- --rtc, --loss <p>): 32 bots on the hillside, the numbers in Performance. Not met: the plan's aim of a step p99 unchanged over the channel (27.4 ms against 21.8 for the ordered deltas on the same machine, the acked encoder copying each client's state every frame); the next step is an encoder that does not.

Built in 0.55.1 (four faults from the playtest of 2026-10-13, and the one found behind the first; +103 product, +421 test lines, to 117,351 and 72,184; fixes, not estimated):

  • [x] A twin is of its entity's kind (packages/net-client/src/mirror.ts): the server recycles entity ids, so a person's id can be a car's in the very next snapshot (a level reset, a mission started again, the traffic and the crowd coming and going), and the client kept the old twin: a car predicted as a person's capsule, which walks where the keys point and turns to the mouse (the playtest's taxi that "turns to the mouse pointer and does not accelerate in the right direction"), or the player's character read as a car, its ammunition pouch a trunk's four slots (OUT OF AMMO after a level reset). A twin is made anew when its id comes back as another kind, and the entity the player controls is followed by its twin, not only by its id. mirror.test.ts, E2E-525.

  • [x] The car stands until its driver is in (packages/sim/src/seats.ts, boarding.ts, carjack.ts; apps/client/src/renderer/game.ts, remotes.ts, riders.ts): getting in at a door takes the whole of its clip (EXIT.inTicks 30, half a second; it was a third, and the client replayed the clip on a figure left at the door while the car was already driven), a carjacker gets in the same way after its beat (getInAt), and the client draws nobody stepping in after the seat is taken. boarding.test.ts, carjack.test.ts, mirror.test.ts, E2E-523.

  • [x] A wreck keeps its paint (remotes.ts, game.ts, models.ts; a trailer's snapshot carries its tractor's exploded): it smokes and burns as the car it was; only a shell that has blown up is drawn charred. E2E-35, E2E-461, E2E-524.

  • [x] The dead stay in the car (boarding.ts' deadInSeat and putOutDead, character.ts, carjack.ts; the clip seat-dead, assets/blender/models/animations.py): killed in a seat, a person stays there, slumped, a body that stays for good, anyone else the ten seconds the dead lie, then back as the dead come back; off a two-wheeler the body falls; whoever takes the car by force pulls its dead out; the ambient traffic leaves a car whose driver died at the wheel with the body in it (packages/ai/src/traffic.ts). People coming back on the same tick never stand in each other (personSpotTaken looks at the bodies too; a point the server set gives way to the spots round it). boarding.test.ts, carjack.test.ts, traffic.test.ts, E2E-524.

  • [x] Rush Hour's cab stands at the kerb by the depot's car rental (cab.rank), clear to drive off onto the avenue: on the rental lot it stood between a tree, a bus shelter, a hydrant and a parked van and wedged on the first of them. E2E-501, E2E-502.

  • [x] One brought back while in a car (a mission started again round a player dead at the wheel, an arrest) is out of the car first (respawnCharacter).

Built in 0.56.0 (the playtest of 2026-10-13, its points on level design, on an empty world and on the night's lights: one flagship city at the new standard before the others are redone, +2,375 product and +368 test lines with what its pull request's CI asked for (a scene folder removed with retries on Windows, the smoke tier's own server only, a test that braked too late); the lights, +1,287 and +1,140; to 121,099 and 73,833; not estimated beforehand):

  • [x] A street network drawn freely (assets/blender/levels/network.py): streets as polylines cut onto the roads they meet, loops in parts, roundabouts as arcs between their arms, culs-de-sac, and problems(), which says what would break the ground before anything is built (a bend tighter than the road is wide, junctions under 16 m apart, roads meeting at under 35°).

  • [x] main-riverbend, the flagship city (riverbend.py on citygen.py): a river looping round an Old Town of lanes on a round market square, a downtown grid turned 17° and cut by a boulevard, three roundabouts, a harbour, a garden suburb of crescents and closes, the Heights' streets and a serpentine up to a lookout, a parkway round it all, seven bridges (decks between abutments, the ground cut where a road is over the water); 86 roads, 16.7 km, thirteen districts. Its contract (packages/world/src/levels-riverbend.test.ts) holds it to being no grid: no heading has 30 % of its roads, forty of them bend by more than 30°.

  • [x] Buildings in walks (riverbend.py's place_landmarks, place_buildings over cityparts.frontage): the landmarks first, the big before the small (weights by footprint), a closer walk, an infill of the narrow ones, the outbuildings in rows behind the fronts; a kind counts for less in a district the more of it stands there, one stadium and five of any shop at most, the countryside's kinds kept off the streets. About 900 buildings of 155 kinds.

  • [x] A city lived in (citylife.py, every footprint from the kit library): by district, street trees of fifteen kinds, the furniture by the kerb every dozen metres, 39 bus stops, 15 car parks, cars on the houses' drives and vans at the loading docks (140 parked), 3 building sites, container yards, what stands in the yards, 65 boats and 6 jetties, reeds, roadworks, billboards round the parkway, stop lines, manholes and cycle lanes. 6,900 placements of 386 kinds, 2,050 of them light pieces.

  • [x] The river is water: the generator works with its streets at 0 and lifts the city 4 m as its recipe is written (a level's ground is never under 0), one sheet of water under the whole level. E2E-547.

  • [x] "Smart City Pilot" (packages/missions/content/main-riverbend.json), four missions: a rental from the Docks to the Market Gate, a race once round the parkway against three rivals, a consultant escorted from the Town Hall to the stadium, the market's fountain photographed. The sixth city of the main menu. E2E-546.

  • [x] What a room can hold (packages/world/src/checks.ts, packages/sim/src/components.ts, apps/tools/src/region-level.ts): LEVEL_BUDGET 1,200,000 triangles and 10,000 pieces (it was 400,000 and 4,000), measured on the city (6 to 10 ms a frame on an RX 570, a server up in about 20 s); 4,096 entity ids (2,048); a generated region keeps its own budget, a third of it (REGION_BUDGET, its numbers as they were).

  • [x] The navigation built in half the time (packages/world/src/geometry.ts): every triangle's box on the ground is kept, and a point outside it by more than the reach asked is not measured against the triangle; the same grids and floors byte for byte on ten levels (the hillside's 13.6 s to 6.5, the city's 23 s to 11).

  • [x] A level file is never typed from its JSON (apps/tools/src/level-files.ts, allowArbitraryExtensions): the export writes a declaration beside every level file saying it is unknown, which TypeScript reads in place of the megabytes of JSON; every workspace that imports the registry type-checked them literally (2.2 GB and 78 s for one of them, past what a hosted runner has once the city was in; 0.6 GB and 25 s now). level:check holds the declarations.

  • [x] A frame never owes negative time (packages/net-client/src/clock.ts' frameElapsed): the first frame of a level that took seconds to build is stamped before the loop started; its negative time grew the correction smoothing without bound (the car drawn at 1e31 m, the ground cover looping for ever), which the city was the first level slow enough to show.

  • [x] A lamp knocked down lights nothing (packages/world/src/lamps.ts, packages/sim/src/breakables.ts' isPieceLoose, packages/ai/src/visibility.ts; apps/client/src/renderer/night-view.ts): its pool and its halo are gone and its head is dark where it lies, on the server too, where a guard sees no better there than in the dark; a level reset lights it again. Each lamp's head has a material of its own. E2E-526, E2E-527.

  • [x] Windows lit by the hour (lit-windows.ts, window-lights.ts): the windows found in every mesh's glass at load, a share of them lit by the hour (most at nine in the evening, a few at three, none by day), the same ones for every player (a hash of the placement, the window and the night), in five tones, a television's blue flickering; the lower storeys' light on the pavement; two draw calls for a level, nothing by day. E2E-528.

  • [x] Headlights and tail lights on every car somebody drives (car-lights.ts): two beams on the road ahead and a red glow behind, laid on the ground as it rises, one draw call, on the software tier too; a parked car has none. E2E-529.

  • [x] The lamps light the cars and people under them (actor-lamps.ts, with a GPU): three point lights at the lit lamps nearest where the camera looks, limited to the actors by a layer mask. Within the noise of the storm-night scenario on the reference machine (59.3 frames a second before and after); to be measured again on a quiet machine. E2E-530.

Next on the same points, each to be estimated before it starts: new kits by theme (roof details, shop fronts and street life the top-down camera reads), then the other cities redone on network.py.

Built in 0.57.0 (the playtest of 2026-10-13, its point on the dogs: "model them as own NPC and give them some behaviour"; +2,208 product, +2,843 test lines, to 123,307 and 76,676; not estimated beforehand):

  • [x] A dog is an NPC of its own (packages/sim/src/dogs.ts, character.ts' Character.body, packages/world/src/scents.ts): a character with the pedestrian role dog in a dog's body (a lower capsule, 25 kg, 15 hit points, 42 km/h flat out), hashed; PROTOCOL_VERSION 32. On a lead it stays within 2.2 m of its walker, ranges ahead, behind and to the sides, stops to sniff at lamp posts, trees, hydrants, bins and other dogs until the lead is taut, trots to catch up and sits when its walker stands; a stray wanders the pavements, sits, sniffs, follows somebody for a while and keeps off the road. Every one bolts from gunfire and blasts and hops aside from a car; a leashed dog that bolts pulls free and goes back to its walker once calm. dogs.test.ts; both determinism gates run with a leashed pair and a stray.
  • [x] The crowd brings them out (packages/ai/src/crowd.ts): a dog with every dog walker, up to three strays where there are pavements, on a random stream of their own (the people's spawns are as they were); a dog whose walker is gone is a stray.
  • [x] What a dog is not: no witness, no crime to hurt, no mission's kill nor a stealth one, nobody to search, carjack, talk to, put to sleep or knock out (a dart or a punch frightens it); last on a line of fire; no body a guard raises the alarm for.
  • [x] Drawn where the server has it (apps/client/src/renderer/dog-view.ts, remotes.ts, models.ts): the dog model in a batch with its limbs swung by its gait, sniffing and sitting poses, lying when dead, a lead from its walker's hand to its collar (taut or sagging), a bark within 45 m; no fixed offset from its owner any more. E2E-479, E2E-536 to E2E-539.
  • [x] E2E-69's sedan brakes with the foot brake before the handbrake after the hydrant (it ran off the level in one run in six on the release too).

Built in 0.57.0 too (the playtest of 2026-10-13, its point on a world too empty: "at least a tenfold amount of assets, variety"; the first new kit by theme, after the flagship city was dressed with the kits there were; estimated at +1,100 to +1,300 product and +150 to +250 test lines, built in +993 and +428 (the tests over their estimate by those of what was found on the way: the z-fighting search, the materials, the crossings, the roads' paint), to 124,300 and 77,104):

  • [x] The urban kit (assets/blender/kits/urban.py, 36 pieces): 22 things a roof carries, which nothing collides with (air conditioners, a ventilation plant, a cooling tower, vents, a stair head, a water tank, a skylight, solar panels, a dish, an antenna, a cell tower, a planter, a terrace, a chimney, a billboard, a pigeon loft, a pool, a greenhouse, pipes, a helipad), the harbour's (a barge, a tugboat, a fishing boat, a buoy, a ship-to-shore gantry crane) and the streets' (a food truck, a hot-dog cart, a fruit stand, a flower stall, a statue, a carousel, a skip, a portable toilet, a cable drum). In the kit gallery (E2E-432).
  • [x] Roofs in the kit library (apps/tools/src/roof-spots.ts, kit-library.ts, KitPieceInfo.roof): for each of the 138 buildings of the shared kits with a flat roof, its height and up to eight free circles on it, clear of its parapets, penthouses and signs, measured on its triangles; a generator dresses a roof without knowing the building.
  • [x] Riverbend dressed with it (citylife.py's rooftops, ships, buoys, dock_cranes): 1,101 roof pieces of 22 kinds by the building's family and its district's lean, nine ships and ten buoys on the harbour's reach, two cranes on the Docks' quay, a statue, a carousel, a food truck and carts on the squares, stands at the market, skips and cable drums on the building sites: 8,007 placements of 429 kinds and 943,986 triangles (6,892 of 390 and 847,652 before). Its contract holds every roof piece to a roof and every ship to water that floats it; E2E-548.
  • [x] No zebra crossing over another (citylife.py's crossing_distance): where a junction's arms meet at a shallow angle a crossing lies further out along its arm, clear of the next arm's carriageway, and two junctions a few metres apart share one; seen in a look round the city, held by its contract. And a road's painted lines stop where each line itself enters another road, not where the road's middle does (apps/client/src/renderer/roads.ts): edge lines no longer run on into a junction crossed at a shallow angle.
  • [x] The benchmark knows the city (npm run perf -- --scenario riverbend), and what it found is fixed: with the roofs dressed, 59.8 fps on the reference machine with three or four frames in 1,200 taking two refreshes, and a freeze of 150 ms the first time a wreck was drawn charred. Babylon looked through all 9,000 meshes of the scene for every material made while the game runs (a new paint, a wreck's soot, a dog's lead); apps/client/src/renderer/materials.ts makes them without the look, and two runs of three then drop no frame. The server's step is 7 ms of the tick's 16.7 with one player, most of it physics; with a full room of 32 (the load harness) it is 12.6 ms at the median and 27.5 at its p99, the hillside's 9.7 and 21.8: over the tick on the frames that send snapshots, on both levels.

Built in 0.58.0 (the plan of 2026-10-14 after the playtest of 2026-10-13: "the removal of everything level except the newly created riverbend", the five other cities with their curated games, the test levels kept; "a rebalancing of the e2e test suite since it takes way too much time now. i want to have a 15 min run at max", two tiers; "add 2 gpu runners on github to cut times"; estimated at −150 to −300 product and −2,900 to −3,400 test lines for the removal and +300 to +500 and −100 to +200 for the tiers, built in about −70 and −2,200 and about +210 and +150 (the second with what the first runs found), to 124,440 and 75,055: the removal took less out than estimated, as the compound stayed as a test level and eight tests of what Riverbend still has came back on it):

  • [x] Only Riverbend ships: main-hillside, main-downtown, main-highlands and main-bayside removed with their games (the hillside's four missions, "Downtown job", "Lava Selfie", "Rush Hour"), withdrawn until their cities are drawn again from a spec; main-riverbend is the server's default (DEFAULT_MAIN_LEVEL) and the main menu's one city; the benchmark's scenarios and the stack's smoke play it; the crowd's and the traffic's budget tests run on it with its game's forty cars (with the crowd, 0.86 ms of AI and 6.6 ms of simulation a tick).
  • [x] test-compound: main-compound's town under a test name, regenerated identical, the walled compounds' level (compounds.py keeps a level and its contract); its game is a test game of rules only (the night's clock, the loadout); its specs start their walks by Go to, its folder five minutes where it took ten. test-tiles-town's town is lived-in, so dressing.py keeps a level.
  • [x] What Riverbend still has, tested on it: its traffic and its lights (E2E-115), taxis and buses among it and a bus at a stop (154, 155), the crowd's roles and a shopkeeper at its door (156, 157), only what the camera sees drawn (230), a jetty's deck over the water (446), the level by its hash and from the browser's store (507).
  • [x] Two tiers (tests/e2e/support/lanes.ts, build/e2e-plan.mjs): @core, the gate on every pull request, each mechanic's fast tests at 20 s at most on the self-hosted runner (a test level's only one excepted), the tier 12 minutes at most in the runner's five lanes alone (10.2 by the recorded seconds); @long, 90 tests (the 82 recorded over 20 s and the eight above), every night on develop when it moved and in the Release workflow before publish.
  • [x] Three machines (.github/workflows/e2e.yml, called by CI and the Release workflow, build/e2e-route.mjs): the laptop's five lanes and two of GitHub's GPU runners (gpu-t4: a Tesla T4 on four processors, NVIDIA's image, at most two, $0.052 a minute; measured: 78 s of setup, a lane at 0.74 of the laptop's with three on a machine), the GPU runners alone when the laptop is off, GitHub's software runners only when neither is there; E2E_GPU_JOBS=0 switches them off.
  • [x] Lanes packed by measure: tests/e2e/timings.json (every test's seconds on the self-hosted runner, npm run e2e:timings from a run's reports), level folders packed longest first to the lane that finishes first at its machine's speed, every job computing the same plan; E2E_ONLY runs a folder or a file with its levels' servers only; the lanes serve a build of the web client (vite preview, the build four seconds), record a trace only on a retry and read the state every quarter of a second while they wait for it.
  • [x] Measured, and what the first runs found: the gate takes about 7 minutes (the laptop's five lanes 4.4 to 4.7 minutes, each GPU runner's three 3.7 to 4.7, the jobs 5.2 to 6.1 with their setup, about $0.62 a run), the long tier about 11; the whole CI run about 12½, the docs its slowest job (12.0), as validate (tests) moved to the four-core runner TESTS_RUNNER names (4.1 minutes, 14.6 on one worker) and validate (windows) runs in two halves (8.6 and 8.8, 19 in one piece). The runs found what the new layout exposed: Playwright's poll gives up when its reading throws (pollPoc reads "nothing yet" before the page has a game), readings taken too soon under quicker polls, steps and aims tuned to a slow page that come short at 60 fps, a serial describe split across the tiers, and tests on the core level that did not tell their own pedestrian from its crowd.

Built in 0.58.1 (a hotfix of the release workflow: 0.58.0 passed every job and was never published, as its publish job took every artifact of the run, the end-to-end tiers' reports with the installers, and refused to attach a folder; nothing was tagged, so 0.58.0's content ships as 0.58.1):

  • [x] publish takes only what a release ships (.github/workflows/release.yml): the installers (client-*), the server bundle and the stack bundle.
  • [x] verify on the four-core runner (TESTS_RUNNER): npm run validate in one job took 30.7 minutes on GitHub's 2-core runner, its tests on one worker.

Built in 0.58.2 (fixes after 0.58.1, no product change):

  • [x] The security scan green again: CI and the Release workflow pass the reusable e2e workflow only the secret it reads (RUNNER_STATUS_TOKEN), not every secret (secrets: inherit, refused by Semgrep since 0.58.0).
  • [x] Four long tests that passed only on their retry: the compound's lab door, the terrain level's walk up the mountain, and Riverbend's shopkeeper, twice (on a GPU runner's three lanes Riverbend's crowd server falls behind the clock, so its errands take longer).

Built in 0.58.3 (the README's badges, and the two failures they would have shown; no product change):

  • [x] Badges that draw in a private repository: GitHub shows a private repository's own badges to its members only, so the README's CI, Docs and Release badges are shields.io endpoint badges, read from a secret gist that .github/workflows/badges.yml writes when a run finishes (BADGES_GIST_ID, BADGES_GIST_TOKEN).
  • [x] The docs site fits on a runner again: every page of the API reference carried the whole typedoc sidebar (3,771 entries) and a map of every page's chunk, about 1.8 MB a page and 5.7 GB in all, and docs.yml had failed on a full disk since 0.57.0; the reference's sidebar lists its modules only (a module's page lists its members), the map is one file the pages share (VitePress's metaChunk), and the site is 320 MB (a guide's page 50 KB, a reference page 87 KB); it is packed for Pages only when there is a Pages to publish to.
  • [x] A Riverbend room's test on Windows: opening a room on the city took 68 s on the two-core Windows runner, over the test's minute; its tests have three.

Built in 0.59.0 (wave 3 of the plan of 2026-10-14: no screenshots to judge the levels and missions creators make; estimated at +1,200 to +1,500 product TypeScript lines, +100 to +200 of Python and +1,100 to +1,500 test lines, built in about +2,060 TypeScript, about 250 of them moved from the test helpers, +150 of Python and +290 test lines, to 126,674 and 75,353: more product than estimated, the repair and its reports in the worker and the API, the region's own overlap check and the roads' paint moved out of the client; fewer tests, as the bundled levels' gate is one loop over every level and the random specs one over seeds):

  • [x] The level audit (packages/world/src/audit/): auditLevel, rules a level passes with nobody looking at a picture of it, on each piece's core (the box round its lowest metre: 2,214 false pairs on Riverbend between whole pieces' boxes, the real ones between cores): props in each other, a prop inside a building, buildings in each other, a piece not resting on what is under it (floating, sunk, a boat off the water), a piece in a carriageway, z-fighting, paint over paint, roads apart (a warning); repairLevel, which drops what may be dropped (a faulty prop of a shared kit, paint under paint), never moves anything, three audits at most. The kit library knows what a piece rests on (rests: the boats on the water, the traffic cone on the road; a sidecar's field from foi_export.py), and the catalogue tells the audit what it knows of a piece (pieceFacts). The z-fighting search moved into product code and searches each distinct mesh once and across pieces only the planes two pieces' faces share: 2.3 s and about half a gigabyte on Riverbend, where it took 5 s and 1.3 GB (the plan's 1.5 s and 200 MB not reached).
  • [x] Every bundled level passes it (packages/catalogue/src/levels-audit.test.ts, replacing the per-level z-fighting loop and Riverbend's own street and crossing checks), what it found fixed in the generators, not by a repair: on Riverbend three cable drums in tower cranes' bases, nine pieces of the lookout floating 15 to 19 cm, two pines, two lamps and a hydrant on a junction's asphalt, 129 pairs of marks over each other; on test-curves fourteen guardrails and five more pieces off its slopes and four lamps in its plaza's road; on test-plots two boulders in a cabin and a ruin. The city generators read a piece's height off the ground as it was triangulated (cityparts.Ground), the placer keeps all but a cone off a road's surface, the markings leave out a mark under another (a cycle lane breaks at a crossing, in lengths of 2 m), a building site's cable drum stands clear of its crane, and a region's dressing leaves out a piece that would stand in another. Riverbend, test-curves and test-plots regenerated. (The grid towns' hydrant the plan named stands on its pavement: nothing to fix.)
  • [x] checkLevel gains two rules and the level gate runs it: fewer than 3,000 pieces a car knocks loose (the simulation's 4,096 entities; Riverbend has 2,064) and no road under water.
  • [x] The roads' lines give way to the level's paint (packages/world/src/road-paint.ts, roadPaint, which the client draws): no step of a line over a crossing, a stop line or a grate (1,296 steps on Riverbend); the hook's roadPaint says how many gave way and that none is drawn over paint (E2E-549).
  • [x] Every generated level is audited and repaired before its preview and its cache (apps/worker/src/audit.ts), a prop on a named place kept; the build says what was left out (14 pieces left out to keep the level clean (…); nothing to fix in the spec) and each fault left as the generator's, as warnings for this release (E2E-550); the worker logs a line per generation with the spec's hash, the API counts foi_level_repairs_total by rule and template.
  • [x] Random specs (@foi/catalogue/testing's drawers, npm run level:fuzz): the generator never throws, a refusal names paths the spec has, a level keeps no fault after its repair and loses at most 0.5 % of its pieces to it, one in ten built twice the same. Twelve regions on every pull request (fuzz.integration.test.ts, ten seconds), thirty grid towns and ten regions with towns every night in the worker's image (.github/workflows/fuzz.yml), the specs that ever failed on every release (apps/worker/fuzz-corpus/, two so far: the ruin on the strewn boulders, a swan's draft taken for being off the water). A hundred random regions pass.
  • [ ] Not done: a default footprint from the kit library for the placements that pass none (cityparts.py); the audit and the repair catch what it would have kept apart.

Built in 0.59.1 (what the first night of random specs showed; no product change but the audit's nature):

  • [x] The random regions with towns build: the first run of the nightly fuzz (by hand, its thirty grid towns built in 1.4 to 6.8 s each in the worker's image, nothing left out) built only two of its ten regions with towns, the drawer having put the town on a lake, a mountain's flanks or a landuse area; the town is drawn first now and the rest kept off its footprint (six of ten build), the plain regions drawn as before. The runner says which specs it built (their pieces, what was left out) and which were refused, and counts both.
  • [x] A city's server has two minutes to boot in the e2e lanes (a test level's one; both had 30 s): 0.59.0's long tier lost a lane of a GPU runner whose three lanes booted their servers at once, Riverbend's among them.
  • [x] A stone is of nature: a stone pile and a boulder lying into each other (a region with a town, one piece in 1,011) are a rock pile, as two boulders are.

Built in 0.60.0 (wave 4's first release of the plan of 2026-10-14, a city from a spec; estimated at +3,000 to +3,400 product TypeScript lines (the solver 2,300 to 2,800 of them), +500 of Python and +2,150 test lines, built in about +3,450 TypeScript (the solver about 3,180), +220 of Python and +1,130 test lines, to 130,371 and 76,508: over the TypeScript estimate by some 50 lines and the solver's by about 380, with the styles, the dressing and the MCP guide pulled forward from 0.61.0; the Python is Riverbend's engine moved, not written again, and the tests are a few loops over many cities rather than a test a case):

  • [x] The city template (packages/catalogue/src/city/, The city spec): a city drawn freely, as Riverbend is, written as data. Roads of six classes through points, splines or straight or with rounded or eased corners, starting and ending at points, at a roundabout's arms, on a grid's streets or onto another road (cut where they cross it, or stretched 60 m at most to reach it); turned grids whose lines run between their family's ends or to the road they name; rivers that widen into a basin, crossed by bridges, with quays along their banks; hills, a lane climbing one to its lookout; roundabouts and their islands (a green, a market, a lawn); a ring bounding the town; closes, crescents, cycle lanes; districts by a seed (the nearest on the same side of the water) or an outline, each in one of thirteen styles, Riverbend's quarters (styles.json); parks, greens and squares; the spawn and named places. Everything in the game's frame, bearings as on a compass.
  • [x] The solver (buildCity), at once and without Blender: the network drawn with a port of network.py and citygen.py's curves, operation for operation, its checks (network.py's four, and a road leaving the map, an end short of a road, two roads with no pavement between them, a river crossed under 30°, an end in the water, roads meeting over it, a street climbing a hill, a climb over 12 %), the districts laid over 4 m cells; then the plan Blender builds (plan.ts) and a map in text. Riverbend, written as a spec (fixtures/main-riverbend.spec.json), solves to the roads riverbend.py draws: all 86 within 1.8 mm, the same 131 junctions and 7 bridges, in 0.4 s. PUT /drafts/:id/spec answers a city with its issues at their paths or its map; level_spec_set says so, and foi://guide/city-spec is the guide.
  • [x] The vocabulary tried by Claude, before anything was built on it: ten cities described in a sentence each (a harbour town, a hill town with a serpentine, a grid turned 30° with two roundabouts, a river town of two banks, a radial old town, a garden suburb, two grids meeting at a boulevard, a city on a river's bend, a port, twin towns) written from the guide alone against the solver: clean in 1 to 3 runs each, 2 at the median (the plan's bar: four). What they stumbled on changed the vocabulary: parks' and districts' names take dashes, a district's outline takes a park's shapes, a closed road takes rounded corners, a road that cannot be drawn is said once (not again for every road that needs it), a road crossing a ring where two of its parts meet is no sharp corner; and the guide says the rest (the town without a ring, a grid line's ends, which bank is left, how a climb is measured, step).
  • [x] Blender builds the plan (assets/blender/levels/city.py, generate.py --plan): Riverbend's engine moved out of riverbend.py (the ground, the bridges, the frontage walks, the street furniture, the parks, the dressing), reading a plan: several rivers or none, hills, the districts' cells, the styles. riverbend.py draws its network and builds through it, and Riverbend comes out byte for byte the same. npm run level:city -- <spec> (--check: the solver alone) writes a city's kit, recipe and places; the worker and the API's own process solve a city and hand its plan to Blender (a 6-minute limit).
  • [x] test-city (320 m: a turned grid, a roundabout, a river's one bridge, a quay, a crescent, a close, a knoll with a lane up it, three districts; built in 10 s), its contract packages/world/src/levels-city.test.ts, the catalogue's example of the template: the car drives over the bridge (E2E-551) and round the roundabout (E2E-552); a game on a city generated through the platform plays on its places (E2E-553).
  • [x] The gates: two hundred random cities through the solver on every pull request (draws.test.ts, 3 s: refused at their own paths, or a sound plan and a map), twelve the solver accepts built nightly in the worker's image (fuzz.ts --template city); the junctions the solver finds held to city.py's on six cities where Blender runs (city-level.integration.test.ts).
  • [ ] Not done in this release: the presets of the squares and parks (a park has trees and bushes; Riverbend's things are its own), pinned landmarks, the creators' limits and the trial cities through the MCP tools (0.61.0); Riverbend from its spec and riverbend.py and network.py deleted (0.62.0; city.py still borrows network.py's Area and a few helpers).

Planned (decided 2026-10-18): multiplayer missions (GDD §13, §14), from 149,353 product lines and 97,863 test lines, in releases that each change nothing for a game without a play block:

  • [ ] The decision record: this section, GDD §13, §14 and §19, §13 decision 10.
  • [x] Runs with teams in rooms of their own (+5,000 to +6,700 product lines, +4,000 to +5,400 test lines; built: +4,939 and +4,664): the play block, co-op and versus together, the holder 0, catch, lives, retries, a minute to come back, the result reported and stored; the mini level test-versus; a co-op heist and robbers and chasers on Riverbend. In two pull requests: solo and co-op runs (+2,615 product lines and +2,040 test lines: the play block, the holder 0, lives, retries, a minute to come back, each seat's book and credit, the run's room and its result reported and stored, a mission's Play on its game's page, draft_playtest with a mission), then versus (+2,324 and +2,624: team on objectives, a winner with win:<team> and the action win or a draw, {team} in references and queries, catch and eliminate a team, the teams' events, a free-for-all and a race between the players placed per seat, lives per team, players out to watch, team spawns, spectating, forfeits, a dropped player's body kept in the world for their minute, the teams of a party filled in turn; the scoreboard, the winner, team colours and the spectator view; test-versus and its game, "Series A Withdrawal" and "Hostile Takeover" on Riverbend; runs without the platform on a debug server; FOI_MAX_RUNS, 2 runs a server by default, from four Riverbend run rooms measured beside the city, Performance).
  • [x] Lobbies and Quick play (+2,900 to +3,800, +2,300 to +3,000; built: +3,819 and +2,730, of which +684 and +712 for the review's guards: kicks a party cannot get round, a party's members held where they are, no run left behind, seats freed when their room says they are gone, games read again before every run, caps on lobbies and invitations, pushes gathered, a stranger's view without accounts): lobbies in the API (in memory, as the parties; the run's room opened at the start of a 10 s countdown, the join URLs sent at its end, the city busy asked again, a member still in another run left out), a party kept on one team, the host's start and kicks, the results and a vote on the next mission across levels (the same, the game's next, another level's, Replay), Quick play by playlist (co-op, versus, races, any), the missions' index, openRoster (a run for an explicit roster with teams); the lobby's page, Play together, a multiplayer mission's Play opening a lobby, a run's page going back to its lobby; P3's seams (the gate, the conduct pool).
  • [x] Respect, conduct and skill (+2,600 to +3,500, +2,200 to +2,900; built: +3,931 and +2,358, of which +488 and +482 for the review's fixes: the base by the run's length, a creator's missions counted together in the day's repeat, the version hashed as written, qualifying players only, memory bounded, payments, endorsements and upheld reports done again, vote-kicks and teams that cannot be turned into weapons): every settled run paid once in the API before its lobby shows the results (standing.ts' rules: the base by the run's own length and the difficulty, the outcome, presence, the contribution bonus, the day's repeat, a creator's mission paying once qualified, the conduct band; respect.ts keeping them, a ledger line per run and account), conduct (a clean run's +100; abandoning, team kills, harm past half a teammate's health, sabotage, idling, vote-kicks twice a week, upheld reports; 1,500 a run at most), the hidden skill (skill.ts, team Glicko-2) balancing a versus lobby's teams and Quick play's nearest lobby, endorsements, the missions' ratings, tier gates at a lobby's creation, join and run's start (a creator's held to tier 2 until it qualifies), the low-conduct pool from 16 players in Quick play, the seasonal leaderboard, the admins' conduct and upheld reports in the audit log, the data's export and erasing; the run's result carrying each seat's sabotage, the vote-kick that put it out and its worst harm to a teammate; the tables in every store (Postgres's migration 10); the tier badges, a run's Respect in its results with Endorse and the mission's stars, the account's standing, the leaderboard's page, the privacy policy's words; FOI_RESPECT=off|shadow|on.
  • [x] Continuous play across levels (+1,400 to +1,900, +1,200 to +1,600; built: +1,743 and +1,948, of which +380 and +485 for the review's fixes: a seat kept in the store before the server is asked and reconciled by account, a minimum span, the gate in the fill, a lobby never stranded, playlists cached, the room secret in constant time, Quick play's backfill one player at a time): the playlists first-class (playlists.ts, GET /playlists: Co-op, Versus, Races and Any, rotations across levels of the curated multiplayer missions and the published ones qualified for Respect), a lobby opened from one remembering it (its vote's offers from it, an unattended vote moving it on to another level); seats filled during a run (joinInProgress: untilCheckpoint until the first checkpoint, always: the open seats in the room's status, Quick play seating a player on the team with the fewest, POST /rooms/:id/seats, the late joiner at the team's spawn or the last checkpoint, the lobby told); Respect pro rata by each seat's span (joinedS, leftS), presence and idling judged against it, the leaver still charged; the phone's Jobs app in the city (invitations, lobbies needing players, missions of this level and the others, Quick play; offline without the platform); the Play together screen's playlists; "Open Plan" on test-versus and "Carpool Relay" on test-mission-basics; FOI_RUN_GRACE_S.

After V1 ([NEXT]): water and boats with the Harbour game (+4,000 to +6,000); the police at 4–5 stars; the fake web; free layouts described in words; creator Rep and cosmetics; hold-ups and distractions.

12. Risks and mitigations ​

RiskMitigation
Players' games are unplayableData only, strict schemas, the validator, AI playtests, Claude's repair loop on the validator's report, examples from the curated games.
Players' public games are offensiveModeration of every text strangers read, reports, takedowns, labelled creators; private games stay among the people who share the link.
Creating needs Claude CodePlanned (decision 9): the game's owners on Steam create in the game too, on the platform's one model under its monthly cap. Playing needs nothing but the game, and the curated and public games are open to guests. The MCP tools are plain MCP, so other clients might work, unsupported.
The in-game model costs more than plannedA hard monthly cap (a setting) with a daily share of it, a cap on every job, a daily allowance per account, the OpenRouter key's own monthly limit or the Claude workspace's spend limit as the backstop.
Generation is slowOne Blender process per job, the bounding-box fix and a spatial index, artifacts cached by hash, small templates first.
Blender in productionA pinned LTS version in its own container image, one isolated job per process with limits and no network, a determinism gate in CI, no player code ever run; GPL respected by running it as a separate program, never shipped in the client.
Nakama as a dependencyOpen source and self-hosted; used for identity and social only; the platform's data stays in its own database, keyed by user id.
Old games break as blocks evolveGames pin catalogue, generator, Blender and server versions; blocks are versioned; artifacts stay stored.
Scope creepEvery estimate in lines (§11); a milestone that runs past its estimate stops for a decision; the GDD's [V1] tags.
Client/server drift makes driving jitteryOne pinned Rapier build, determinism gates, visual smoothing.
Rooms full of traffic exceed the server's budgetArea of interest, entity priority, budgets per game, one room per worker thread, Artillery load tests.
Griefing in public roomsSafe zones, game rules for player damage and carjacking, mute, vote-kick, reports; in runs, conduct and the pool of low conduct.
A run's room costs as much as a city'sLighter traffic and crowds in runs, a cap on rooms per server, the room warmed up during the lobby's countdown, a load test of four Riverbend runs beside the hub before runs are switched on.
Respect is farmed or used to punishThe platform mints it from what the server measures, never a creator's number; diminishing returns; a creator's mission qualifies first; peers may endorse, never mark down; conduct follows patterns, not one crash, and recovers.
CI renders slowly (software rendering)Small mini levels, the no-GPU render mode, tests that wait for states and drawn frames instead of fixed times; later, if needed, the options in §10.4.

13. Decisions and open questions ​

Decided (2026-10-05):

  1. Creating happens in Claude Code with the five-on-it MCP integration, on the creator's own Claude subscription (§9.7). It said "only", and that the platform never runs an LLM to author; decision 9 replaced that on 2026-10-16.
  2. The level generator runs in headless Blender workers, one generator for curated and player-made games (§9.3).
  3. Accounts, friends, parties and chat run on Nakama; guests can play (§9.6).
  4. The browser client, the sign-in pages and the game pages are served from the API's origin (§8.3).
  5. Moderation is pluggable and off in local development; private games are not moderated (§9.9).
  6. Load tests use Artillery (§10.3).
  7. No voice chat, ever.

Decided (2026-10-07):

  1. The client's UI has two layers (§8.1): the in-game UI (prompts and labels in the world, the phone, missions, the inventory and its panels) is drawn in the scene with Babylon's GUI and updated every frame by the game loop; React only draws static overlays (the helper UI and the stats and game-event overlay). The prompt that followed the character moved on one frame in six as a React overlay and moves on every frame in the scene.

Decided (2026-10-16):

  1. The platform may run one tool-calling model through OpenRouter or on Anthropic's API (Claude, on the operator's key) to create games for the game's owners on Steam, in the game itself (§9.8). It runs under a hard monthly cap ($25 by default, a setting), never on a key a player brings, and every creation is an asynchronous job in the platform's queue with a daily allowance per account (5 a day, reset each day in UTC, never carried over). Admins may override any account's limits. Claude Code over MCP stays the other way to create. Built: the durable job queue, the quotas kept in the store, the admins' overrides and their page; planned: Steam ownership, the creation jobs and the model.

Decided (2026-10-18):

  1. Multiplayer missions (GDD §13, §14): a mission's play block makes it solo, co-op or versus; a multiplayer run is played in a room of its own the API opens for a lobby's players, and the game server reports its result to the API; lobbies live in the API (in memory, as the social hub does; runs and standing in the store) and take their players on across levels; players find them through public lobbies and Quick play. Respect is minted by the API from the reported runs, scaled by a hidden conduct score, with a hidden team skill rating (Glicko-2) for versus. Runs, lobbies, Quick play and Respect are built; continuous play across levels is planned (§11).

Open:

  1. Hosting and orchestration for the API, the workers and the game servers (one host with Compose for M1; decide before M7).
  2. Rooms as worker threads in shared processes, or one process per room (measure in M1's load test).
  3. Which Claude model the moderation classifier uses, and the quota numbers for builds, playtests and rooms (decide with real numbers from M1).
  4. Whether the generator job runs on every pull request or only on those touching the generator, once its run time is known.