apps/client
Electron around a Vite renderer. electron/main.ts opens the window: straight into the game on FOI_SERVER_URL when set, else on the main menu (with FOI_PLATFORM_URL passed as ?api=, for testing). It starts the app's own game server (the bundled dist-electron/server.mjs, a utility process on port 7777, FOI_PORT) only when the player hosts on Play local, on the level picked, and uses a server of this version already on the port as it is. electron/preload.ts exposes the bridge electron/bridge.ts types (window.foi): the Electron version, the local server's host, stop and status, and Steam's ticket and openId, each one IPC call. A release built with FOI_PLATFORM_URL carries that platform (vite.config.ts's define).
Steam. electron/steam.ts starts Steam before the app is ready, through steamworks.js (pinned to 0.4.0, imported lazily, kept out of the main bundle by vite.config.ts): with the game's app id (FOI_STEAM_APP_ID, baked into the main bundle when it is built; a run's own environment overrides it, 0 for none) a packaged app Steam did not start asks Steam to start it (restartAppIfNecessary) and quits, the overlay is on, and window.foi.steam.ticket() hands the page a fresh GetAuthTicketForWebApi("five-on-it-api") ticket in hex for POST /auth/steam. Without an app id, the module (another platform) or Steam running there is no ticket, never a crash: window.foi.steam.openId(base) opens Steam's own sign-in page in a window of the app's (electron/steam-openid.ts: sandboxed, no preload, its own persist:steam session), catches the platform's redirect back to <api>/auth/steam/openid/app before it loads, and hands the page the one-time code to exchange. The page (src/ui/steam.ts, the main menu's Play online, the account bar, a refused city) signs in with the ticket, else in that window, when the player picks "Sign in through Steam"; once it has on this device it signs in again by itself with the ticket whenever its session is gone, until the player signs out. Play local never asks Steam or the platform anything. The end-to-end tests' seam: an unpackaged app given FOI_STEAM_FAKE_TICKET (a fake Steam ticket, fake:<steamid64>:<own|shared|none>) hands that out and never loads steamworks.js.
Packaging. electron-builder's files (in package.json's build) leave every node_modules out (the renderer and the main process are bundled) but steamworks.js: its index.js and its dist of the platform being built, unpacked from the asar (asarUnpack) so the .node and the steam_api library beside it (found there by $ORIGIN, @loader_path and Windows' altered search path) are real files; Windows and Linux also get steam_api64.dll and libsteam_api.so next to the executable (extraFiles), as steamworks.js asks. The Steam build of the app is the same build made with FOI_STEAM_APP_ID (the release's steam job, build/steam-build.mjs: Releasing).
Everything else is src/renderer:
| File | What | Tested by |
|---|---|---|
main.ts | Loads the physics WASM, reads ?server=, connects (asking for a big level by its hash), waits for the welcome, loads the level it names (level-cache.ts) holding the frames that come meanwhile, starts the game; ?browse and ?game=<id> show the game pages instead (src/ui/BrowsePage.tsx) | e2e |
menu.ts | The main menu (?menu, a bare address): the burning car behind it, the React menu over it, the settings applied, its cues; window.__menu() | e2e |
menu-scene.ts | The main menu's backdrop: a rented sedan burning in a car park at night (the wreck drawn scorched, flames.ts, smoke, a flickering light), a slow low orbit | e2e |
flames.ts | Fire, low-poly: shards rising white-yellow, orange, red, soot, swelling and shrinking (flameColor, flameScale), one pool of thin instances | unit, e2e |
ui-sounds.ts | The menus' cues (move, select, back, the pause's squelch, the title's start): the sound bank's ui.* events | e2e |
game.ts | The loop: 60 Hz prediction tick (each tick a frame of the last inputs, InputHistory: four over a transport that loses frames, with the newest snapshot tick shown as its acknowledgement; the acknowledgement alone every quarter second while the room is frozen), snapshots read through one SnapshotReader (whole, ordered or acked deltas; a stale one dropped) → reconcile/interpolate, camera, the HUD published to the overlay store (10 Hz), the in-game UI fed as things change (src/gui: the prompt and the car tag placed on every frame, the phone, the mission panel, the strip and the panels on slots), window.__poc() (with the server's chaser, traffic, guard and camera readout, the player's stealth, the lamps as drawn and the in-game UI on screen), the crashes and knocks the prediction feels heard, shaken and logged on their frame and the server's hits of the same blow matched to them, window.__impact() (the player's car at an impact, frame by frame, for the impact probe) | e2e |
scene.ts | Babylon engine/scene (right-handed, a sky light and a sun), the level drawn from its data under one node (flat-shaded pieces baked into batches per cell, level-batches.ts; street lamps and loose light pieces instanced, the ground unless the level models its own everywhere), the collision wireframe toggle, the box actors | e2e |
roads.ts | The roads: ribbons (asphalt or dirt, lit like the level's ground) and their paint (edge, centre and lane lines that stop at junctions) | e2e |
water-view.ts | The level's water and lava: a flat sheet over each surface's rectangle at its height, water a translucent blue, lava in lava-view.ts's materials; the ground higher than a sheet shows through it | e2e |
lava-view.ts | Lava alive: a crater's sheet glowing through a crust of dark cracks that crawls over it, the ground painted lava (a volcano's flow) glowing unlit, both pulsing slowly | lava-view.test, e2e |
vents.ts | What rises off a volcano: smoke off its crater's lava and steam off its fumaroles, into the effects' plume, only within 250 m of the camera | vents.test, e2e |
night-view.ts | The city at night, lit as it gets dark: a pool of light on the ground round every lit street lamp's foot and a halo at its head (thin instances of one disc; a lamp knocked down is out, its head's material its own so it stays dark), a faint sheen on the glass, the windows (window-lights.ts), the cars' lights (car-lights.ts), the player's headlights as a spotlight and the lamps' light on the actors with a GPU (actor-lamps.ts); stats for __poc().night. | night-view.test |
lit-windows.ts | Which windows light at night, pure: windowsOf finds a level mesh's windows in its glass (the face that looks out of the building, never a slab's buried back; a band or a wall of glass cut into rooms and storeys), litShare the share lit by the hour (homes, and shops' fronts), isWindowLit a window's place in it by a hash of its placement, its index and the night, windowTone and tvFlicker its light. | lit-windows.test |
window-lights.ts | The lit windows as drawn: a pane of light a hair in front of each lit window's glass and a soft patch of its light on the pavement under the ground floor's and the first floor's, two meshes of thin instances for the whole level, written when the lit windows change; a light piece's window dark once knocked loose, a fading storey's fading with it. | window-lights.test |
car-lights.ts | The cars' lights on the road at night: two soft beams ahead of every car that somebody drives (one for a two-wheeler, none for a parked car, a wreck, a bicycle or a trailer) and a red glow behind it, laid on the ground the probe finds, thin instances of one mesh; beamAt, the beam's shape. | car-lights.test |
actor-lamps.ts | With a GPU, the street lamps' light on the cars and the people: three point lights made once, standing at the lit lamps nearest to where the camera looks, lighting the actors' meshes alone (draw.ts's ACTOR_LAYER), each fading out before it moves on. | actor-lamps.test |
weather-view.ts | The weather as drawn every frame: the light (daylight.ts), the EXP2 fog (the minimap apart), rain and snow as thin instances round the camera's target, fixed in the world and carried by the wind, wet asphalt darker and shining, mud darker, snow whitening loose ground, a storm's lightning and its thunder; windHeard. | weather-view.test |
daylight.ts | The light of the room's hour and weather (skyLight): the sun's arc and colour, the sky light dimming into a blue night no darker than the player's nightBrightness, clouds dimming it, the fog's density; noon under a clear sky is the scene's own light. | daylight.test |
vertical-view.ts | The ladders and the lifts drawn: a ladder's two rails and a rung every 30 cm, a lift's platform where the local world has it every frame | e2e |
draw.ts | The drawing helpers the scene, the roads, the models and the effects share: an unlit material, one that adds its colour to the picture (glowMaterial), a mesh of raw triangles, the cache materials and meshes are shared through, and the layer of the actors' meshes (ACTOR_LAYER: the lamps' lights light them alone) | e2e |
materials.ts | Materials made while the game runs without Babylon's look through the scene (it asks every sub-mesh of every mesh for its material when a new one's settings change: 10 ms in a city of 9,000 pieces): litMaterials, copies of a material made once (a new paint, a wreck's soot), and multiMaterialOf, a multi-material given its materials at once (the models' batches, the level's pieces) | |
overhead.ts | Structures held over the ground (a tunnel's roof, a bridge's deck) drawn see-through while the player is under one | e2e |
render-quality.ts | How the engine draws: antialiased at full resolution on a GPU; on a software rasterizer (no GPU, a CI runner) without antialiasing at two thirds of the resolution, said on the HUD | render-quality.test |
models.ts | The vehicle and character models (glTF), loaded once: cloned for the player's own character on foot, and for everyone else (every car, the player's too) merged at load into batches drawn as thin instances (a model's paint, one mesh per tint; the rest, a sub-mesh per flat material; its glass, see-through, after everything opaque; the charred paint and burnt rest and glass of a wreck that has blown up (a wreck keeps its paint until then); the character part by part, posed by its figure), so the scene holds a few dozen meshes however crowded the city; a car with a door ajar drawn as its body without doors and each door turned on its hinge; a car's front wheels turned about their hubs and its steering wheel about its column as it steers (pieces of their own, shared by every model whose part has the same shape); the body tinted (or painted in its livery), the lamps and signs glowing, the light bars flashing (light-bars.ts); a wheel with a flat tyre squashed onto its rim; the vehicles' seat anchors and doors read once; the weapons (weapons.glb) put in the fists as a figure is posed (weaponFrame), their muzzles read once (where a shot leaves), the tip of each fist for a figure without one | models.test |
light-bars.ts | The light bars' patterns by livery (the police's blue lamps in turn, the ambulance's wig-wag, the fire truck's double flashes): which lamps are lit at a time, a pure function, nothing on the wire | light-bars.test |
signal-lamps.ts | The traffic lights' lamps: a lit cap on each light's signal head, red, amber or green as its junction's fixed plan says at the tick drawn (@foi/world's signals: no state on the wire), dark once knocked loose | e2e |
effects.ts | Short-lived effects: gunshots (a muzzle flash, a tracer streak per bullet or pellet, tracers.ts), a punch's swing (lines), blasts (a pool of spheres made once), blood (blood.ts: a mist that follows the person hit, drops, stains), sparks, tyre smoke and dust, a wreck's plume (pools of thin instances), the tyre marks' and the stains' meshes (lifted over the road, above its paint, unlit, see-through, never writing depth); every pool capped, nothing made while the game runs | effects.test |
wheel-effects.ts | Every car on screen, every frame: each wheel's slip lays marks and puffs smoke or dust at a rate that grows with the slide; a fall of a few metres a second ends in a ring of dust | wheel-effects.test |
ground-cover.ts | Ground cover near the camera, for the eye only: tufts of grass, flowers and pebbles read off the terrain's colours (coverOf), never on a road, under water, on a deck or in a building, made a cell of 32 m at a time by the cell's own stream (coverCell) and drawn as three meshes of thin instances; a quarter as much without a GPU | ground-cover.test, e2e |
wreck-smoke.ts | Every wreck on screen, every frame, for as long as it is there: a dark plume of puffs rising out of its engine bay (the class's engine placement), thick and black at first, thinner and greyer once it settles, drifting on the wind | wreck-smoke.test |
explosion.ts | Every blast, low-poly (a rocket's, a grenade's, a car blowing up): a white-hot flash, a fireball of flame shards bursting out and rising, a shockwave ring over the ground, a smoke column and debris, a scorch left on the ground; pools made once, sized by the blast's reach | explosion.test, e2e |
vehicle-fire.ts | Cars smoking and on fire, as the snapshots carry them (fire.ts in the simulation): smoke out of the engine bay of a car low on hit points, flames growing out of it once it burns, the whole wreck in flames after it blew up | vehicle-fire.test, e2e |
camera-shake.ts | The camera jolted by a blast, by how near and how big it was, and by a crash of the player's own car, by its hit points (impact), dying away in a moment (not when the system asks for reduced motion) | camera-shake.test |
flat-tyres.ts | Flat tyres, every frame, for every car on screen: a tyre going flat pops (rubber smoke, sparks), a flat rim rolling at speed throws sparks (sparkRate); models.ts draws the wheel squashed onto its rim | flat-tyres.test, e2e |
spike-strip.ts | The police's spike strips: a chain of red scissor links with a steel spike at each joint, as long as the snapshot says (growing as it unrolls, shrinking as it is reeled in), every link an instance of one mesh | spike-strip.test, e2e |
gunfire.ts | The snapshots' shots, hits and blasts and the player's own trigger turned into effects: a tracer from the muzzle of the gun in the shooter's hand (the weapon model's muzzle as the figure is posed; from a car, its window), a streak per pellet, impacts (sparks, dust), blood along the shot that made the wound | gunfire.test |
tracers.ts | The tracers as data: each weapon's look (length, width, speed, colour, flash), streaks that fly from the muzzle to the impact turned to face the camera, drawn once at least | tracers.test |
blood.ts | Blood as data: a burst of mist kept in the hit person's own frame (it follows them while they are drawn), drops thrown with the person's velocity that land and stain the ground, an account of how each burst followed its person | blood.test |
slip.ts | Each tyre's slip read from the car's motion: sideways (v + ω × r along the car's left), wheelspin (a driven axle's grip below the class's own), locked by the handbrake | slip.test |
marks.ts | The tyre marks as a capped ring buffer of quads, chained per wheel, fading over 20 s; another ring of them keeps the blood stains (a splat each, an uneven blot) | marks.test |
particles.ts | A capped pool of particles (oldest first), drifting with the wind when it has one, and the size curves of the puffs, the sparks, a wreck's plume and the blood's mist | particles.test |
riders.ts | Seated characters drawn (by the batches) at their vehicle model's seat anchor, in its pose, on a two-wheeler or behind a car's glass (without their legs where the seat sits inside a car's solid interior, so none pokes out under it), slumped when killed there (the dead stay in the car); hidden with their vehicle out of view | riders.test |
car-doors.ts | The cars' doors: a seat taken or left in the snapshots (or a carjack's beat) swings its doorway open and shut, eased, a fraction of a second each way; what each car's draw is handed | car-doors.test, e2e |
glass.ts | A vehicle model's glass as panes (coplanar triangles sharing edges, each with its plane, frame and outline), the first pane a line enters or leaves, a crack kept inside its pane, and the crack's picture | cracks.test |
cracks.ts | Cracked windows from the shot events: a shot traced against the car it stopped on (or out through the glass from a seated shooter's seat), the cracks kept per car until it is mended, reset or gone, drawn as one thin-instanced decal mesh | cracks.test, e2e |
figure.ts | A character's figure animated by the clips the state machine picks: a base clip, an upper clip over it, one-shots on top (a recoil, a punch, a landing it notices itself), a crossfade at every change, a stride played by the distance covered with the legs turned the way it moves, getting up turned upright over half a second; the pose kept for the batches, set on a clone's part nodes | figure.test |
clips.ts | The clips file (models/animations.json, made in Blender by assets/blender/models/animations.py) parsed and checked against the figure's parts; sampling a clip into a pose (linear, as Blender's curves), blending two poses (the crossfade) | clips.test |
anim-state.ts | The people's animation state machine, pure: from speed, footing, status, seat, cover, weapon, the act byte, role and a guard's alert to a base and an upper clip; the one-shots of the acts; every state and its clip | anim-state.test |
ragdoll.ts | The light cosmetic ragdoll: Verlet points at the joints with distance and hinge limits, the ground and (by rays) the walls and cars, seeded from the capsule's velocity, drifting toward it, settling and freezing; at most twelve active, the farthest laid flat beyond; the parts' world matrices for the batches | ragdoll.test |
dog-view.ts | How a dog is drawn (the simulation runs it, @foi/sim's dogs.ts), pure maths from what its snapshot says: its pose by what it does (dogPoseName: standing, a trot, a run, its head down to sniff, sitting on its haunches, a hop, lying on its side when dead or knocked down), blended over a few frames, its gait by the distance it covers (diagonal pairs at a trot, a longer gallop at a run, its head nodding, its tail wagging) as the turns of its model's limbs and its body's matrix (DogLook); its heading turned no faster than a dog turns (turnToward); and its lead (Leads, leadPoints, leadSag): a line from its walker's hand to its collar, straight at the lead's length, sagging to the ground nearer. The batches draw the dog's legs, head and tail as pieces of their own, each turned about its joint by a draw (models.ts' LIMBS, DrawOptions.limbs). | |
anim-gallery.ts | The animation gallery (?anim-gallery, ?anim-gallery=walk,seat for some): every clip on a labelled mannequin, no server; a row of ragdolls knocked down with R; window.__gallery() | e2e |
vehicle-gallery.ts | The vehicle gallery (?vehicle-gallery, ?vehicle-gallery=sedan for one): every class with a person in every seat, its doors swinging (H holds them open), its steering wheel and front wheels turning from lock to lock, its windscreen cracked, the semi with its trailer, under the galleries' free camera, no server; window.__vehicleGallery() | e2e |
gallery-camera.ts | The galleries' free camera: an orbit camera over the ground, W A S D (or the arrows) pan it the way the view faces (Shift four times as fast), the wheel or Q E zoom, the left button orbits, the right pans with the ground under the mouse, 1–3 set the height, F goes back to the focus; its speed (a quarter to eight times, + and -) kept per gallery in the browser; keys typed into a field never move it | e2e |
gallery-ui.ts | The galleries' panel: the camera's speed slider, a search that looks at a thing found by its name (/), a list of the sections and kits to go to and a map of the whole gallery (a click looks there) | gallery-ui.test |
build-preview.ts | A build's level before anyone plays it (?preview=<artifact or build>): the artifact from the page's API, kept by its hash (level-cache.ts), drawn by buildLevel under the galleries' free camera and panel, the game's places and spawns to find and fly to; window.__preview() | build-preview.test |
kit-gallery.ts | The kit gallery (?kit-gallery, ?kit-gallery=civic or =volcano to look at a kit, a section or a thing): every building and prop of the generators' kits (downtown, civic, landmarks, city, compound and street, served beside the page under kits/ by the Vite config's kits plugin), the region's terrain shapes, features and biomes built by @foi/catalogue, the road classes, labelled (the nearest big enough to read, 150 at most), no server; a walk-in building's inside (stairs, escalators, lifts, shops and named places where its anchors stand), [ and ] taking its storeys off and back; window.__kitGallery() | e2e |
remotes.ts | An ActorNode (drawn by the batches; a box before the models arrive, and for a projectile) + RemoteBuffer per other car or person (a dog, a character of the role dog, is the dog model posed by dog-view.ts: no figure, no dot on the minimap, its lead drawn to its walker's hand, its barks told to the game) (a loose light piece moves its own instance in the level, put back when it is planted again; one near the player drawn where the local world has it, from the tick the prediction knocks it loose: @foi/net-client's PredictedPieces); how cover is drawn; a pedestrian in its role's shirt, a car of a livery or a respray in its paint; the nearest wailing siren; every one placed each frame, only those in view (view-rect.ts) posed (figure.ts) and drawn; a player back from lying dead drawn see-through, a clone of its own, while a ghost; a car near the player's drawn at the local time where the prediction meets it (@foi/net-client's PredictedRemotes) | e2e |
view-rect.ts | The rectangle of ground the camera sees on each frame, from where it is (so it grows as the camera zooms out at speed; its view's corners met below and above the player's level, widened by a margin): what the actors, their figures, the tyres' effects and the shots are culled against | view-rect.test |
level-cache.ts | The level a welcome names by hash: from the browser's store (the Cache API, else IndexedDB) when it was played before, else downloaded from the game server beside the socket (/levels/<hash>.json, the HUD saying how far), its bytes checked against the hash, kept and parsed; __poc().levelSource | level-cache.test |
level-batches.ts | Static batching: every standing piece of the level baked at load into a few meshes per 64 m cell and surface (paved, loose ground, glass, lamp-like), its colours carried by its vertices and a shader plugin lighting them as their own materials did; a light piece's triangles folded away while it lies loose (its own instance draws it) | level-batches.test |
level-culling.ts | The level's meshes sorted once into 32 m cells by the ground they cover, the scene handed only those of the cells the camera sees (and everything that is not the level); what never moves has its world matrix frozen | level-culling.test |
frame-stats.ts | What a frame costs, per second (mean and worst work time, most ticks a frame) and the scene's counters (draw calls, active and total meshes), for __poc().render and the benchmark's window.__perf() | e2e |
social.ts | The page's social side (createSocial): the room's chat and roster from the game server, friends, the party and the feed from the platform (which takes the page to the room its party's leader opened), what the chat and the Social tab do, and socialDebug for window.__poc().social. See Social. | |
jobs.ts | The phone's Jobs app's side (createJobs): the platform asked what there is to play (the playlists, this level's missions, the invitations) while its page is shown and every 5 s while it stays, and the player's choice done (a lobby's page, Quick play into a lobby or straight into a run under way, a solo mission's run, a mission's lobby); offline without the platform. See Phone. | |
net.ts | Browser WebSocket as a Transport; waitForWelcome; byHashUrl and holdMessages (the frames that come while the level downloads, held for the game); a socket refused (its server still starting, the desktop app's own on a big level) waits for the server to answer and joins then (opened, whenServerBack), one turned away stays put and says why (closeCode: a room full, no such room, voted out, a join token run out; refusalOf) | e2e |
rtc.ts | withRtc(socket): the WebSocket transport with a WebRTC data channel beside it: offered once the welcome says the server offers one (?rtc=0 refuses it), the inputs on it while it is open (lossy, via udp), and back to the WebSocket on a timeout, a failure, 1.5 s of silence (not in a frozen room) or the server's close, offering again only after a channel that had opened. The HUD's connected via udp or ws, __poc().net.transport and net.rtc. See Netcode. | |
camera.ts | Camera math: the zoom out at speed (SpeedZoom: the car's speed filtered, quicker out than back in, eased into the height by a smoothstep, as the settings turn it on and tune it), the pace's rise on foot, look-ahead (LookAhead: a filtered velocity, taken at once at a blow with the lead it loses held and let go, impact), the lean toward the cursor on foot, smoothing (CameraFollower, carried by the velocity before it eases, so it trails a car by nothing and stops with it) | camera.test |
stealth-view.ts | Stealth on screen: the glyphs over people's heads (? suspicious or searching, ! alert, z asleep, ✱ knocked out or stunned), the security cameras (a housing turned to its sweep, its lens red while it watches, its cone with the view toggle) | e2e |
locks-view.ts | What the client shows of cars' locks and the garages, from its local world: the car looked at and its tag, a garage's offer at the wheel, the cars near for __poc() | |
contact-view.ts | How the player's drawn car meets the drawn cars around it, for __poc().contact: the gap between their boxes on the ground, the deepest overlap, the drawn gap at every touch the prediction feels | contact-view.test |
items-view.ts | Items on the ground (small boxes coloured by what they are, bobbing), containers (crates and lockers), its garages' bays and its parking bays (slabs painted on the floor) and the level's doors (panels where the local world has them, red while locked); the nail strips, the level's and the police's (spike-strip.ts) | |
input.ts | Keys → VehicleInput | input.test |
keyboard.ts | DOM key state sampled per tick (taps are never lost) and counted one-shot presses; a key typed into a field of the in-game UI is left to it; keys go through the player's bindings (ui/bindings.ts) | e2e |
latency.ts | LatencySimulator: delays frames both ways (L key) and loses 0, 2, 5, 10 or 20 % of those a lossy transport may lose, with up to 30 ms of jitter (K key: the snapshots and the input frames; the transport then says it is lossy and the page asks the server for the acked snapshots) | latency.test |
hud-format.ts | The HUD text (the car's class, layout and fittings, its siren) | hud-format.test |
audio.ts | The game's sounds on the sound bank (src/audio/, Sound and the radio): the engine loop (pitch and volume follow the car), the tyre scrub while drifting, a siren's wail, the horn (G), the shots, hits and carjacks, music (M, quiet while the radio plays), a dog's bark, the weather | e2e |
audio-synth.ts | The placeholder sounds, synthesized as WAV data URIs (the bank's synth:<name>) | audio-synth.test |
sound-board.ts | The sound board (?sound-board, the galleries' Sounds and the radio): every sound, what it plays and where a file goes to replace it, the radio's stations to listen to | e2e (E2E-573) |
Tyre slip for every car, from motion. The marks and the smoke need each wheel's slip for every car on screen, not only the player's. A snapshot already carries what it takes: the chassis's linear and angular velocity (in its pose), the handbrake, which wheels touch the ground and both axles' grip (the grip travels for prediction's sake). slip.ts reads the slip from those, the same way for the predicted car and for a remote one, so they look alike and no field was added to the snapshot (no protocol change, no bytes per car per snapshot): the velocity of each tyre's contact point is v + ω × r, its component along the car's left is the sideways slide (the simulation's drift slip, read at every wheel), and a driven axle's grip below the class's own is wheelspin, since the simulation lowers it toward wheelspinGrip as the tyres spin. A remote car's reading is a snapshot behind (50 ms at most), which an effect cannot show.
Drawing a crowd. A frame's cost must not grow with the crowd: the remotes are drawn by the models' batches (thin instances: an actor is a matrix in a few meshes, nothing per actor in the scene graph), only those in the camera's view are posed and drawn and handed to the tyres' effects, the shots and blasts outside it are not drawn, the level is handed to the scene by the cells the camera sees, the effects are pools made once, and a frame runs three ticks at most (frameTicks). npm run perf measures it; see Performance.
The people's figures. The character model is a hierarchy of ten rigid parts (assets/blender/models/character.py), animated by keyframed clips made in Blender (animations.py → models/animations.json) that the client samples itself into per-part turns (clips.ts), picked by a pure state machine (anim-state.ts) and played by each person's figure (figure.ts); going down, a person goes limp in a cosmetic ragdoll (ragdoll.ts). The batches draw them all as thin instances, a matrix a part. See Animation. The vehicle models carry their seats as empties (<class>-seat-<n>, with a pose and a view): models.ts reads them once, riders.ts draws whoever sits in an open one.
Every text of the game and its menus is set in Changa (src/fonts/: the variable face and its license, fonts.css, fonts.ts waits for it before anything draws text).
The UI has two layers. What is part of the game is drawn in the scene with Babylon's GUI in src/gui (see its README): the prompt and the car tag, placed over their things on every frame, the phone, the mission panel and a race's readout, the inventory strip and the container's, shop's and garage's panels; the game loop owns it, and a click on its panels is never a shot. The static overlays are plain web code in src/ui (see its README): React components laid over the canvas (Overlay.tsx: the HUD block, the Wanted stars, the stealth badge, the clock, the chat, the legend card with its tabs and the language, music, HUD and reset buttons, and the minimap), translated with i18next (i18n.ts, locales/*.json, read by both layers), styled by overlay.css (tokens, a grid that keeps the three apart at any window size, breakpoints) and mounted by mount.tsx. The game loop never imports React: it publishes its numbers into the store (store.ts, read with useSyncExternalStore), and the overlay never reads the simulation. Add a section by adding a component and a tab in Legend.tsx; add a language by adding a locale file and listing it in i18n.ts. The legend, its button and the minimap take the mouse and the HUD lets it through: the game counts presses and wheel turns on the canvas only (renderer/mouse.ts), so a click on the overlay is never a shot, and a key pressed in one of the overlay's fields or controls is the overlay's (renderer/keyboard.ts); a mouse click on a button or a box hands the keyboard back to the game.
npm run -w apps/client dev runs Vite and launches Electron; dev:web (FOI_WEB=1) runs the renderer in a browser on port 5173. npm run -w apps/client build produces dist/ (renderer) and dist-electron/ (main, preload); package runs electron-builder on top. build:web builds the renderer alone for the browser, the page the Compose stack serves (Deploying). The settings the build and the app read (FOI_WEB, FOI_WEB_PORT, FOI_SERVER_PORT, FOI_API_PORT, FOI_VITE_CACHE_DIR, FOI_PLATFORM_URL, FOI_PORT, FOI_SERVER_URL, FOI_STEAM_FAKE_TICKET) are in the root .env.example's section for them; FOI_STEAM_APP_ID in deploy/.env.example. A page that comes over HTTP without ?server= joins /game on its own origin (Vite's dev server and the stack's proxy pass it to the game server) and reaches the API on /api; /?browse is the page of games and /?game=<id> a game's page, on the same origin (TECHNICAL_SPECS §8.3).