Driving model
Everything about how a car moves lives in two files of packages/sim: vehicle.ts (the model) and vehicle-classes.ts (the numbers). This page explains the model so the numbers mean something.
The pieces
A car is a dynamic chassis (its collider the convex hull of its class's model, the body and its wheels above the body's floor, so it wraps what is drawn: packages/sim/src/vehicle-hulls.json, written from apps/client/public/models/vehicles.glb by npm run models:hulls; the body is modelled to the class's chassis box and may rise above it with a cabin) and a Rapier DynamicRayCastVehicleController with four wheels: front-left, front-right, rear-left, rear-right. Each wheel is a ray cast down from wheelAttachY through suspensionRest + wheelRadius (so a class's ride height, the chassis centre over the ground with the wheels just touching, is suspensionRest + wheelRadius − wheelAttachY: a car spawns and changes class at that height, every class with its wheels as high over the ground); where it hits, a spring (suspensionStiffness, suspensionCompression, suspensionRelaxation, capped by maxSuspensionForce) pushes the chassis up and friction impulses push it along and sideways.
The centre of mass sits centreOfMassDrop below the chassis centre and is shifted toward the engine (see Engine placement). It is low on purpose: the controller applies the engine at the contact patches, below the centre of mass, so a car that accelerates at 2 g would otherwise lift its nose and stop steering.
Every tick the vehicle system turns the player's input into wheel parameters, then lets the controller apply its impulses before the physics step.
Throttle axis
One axis, -1..1, GTA2 style:
- Pushing with the direction of travel applies
maxEngineForce, tapered with the square of the speed ratio: nearly full in the speeds where corners happen, zero atmaxSpeed(that taper is the top-speed limiter;linearDampingis a light air drag on top). - Pushing against the direction of travel (faster than 0.5 m/s) brakes every wheel with
maxBrakeForce; below that it reverses withmaxReverseForceup tomaxReverseSpeed. - Released:
coastBrakeon every wheel, so the car rolls to a stop in a few seconds.
Chassis bodies never sleep: a sleeping chassis stops the controller (no suspension forces, so no engine force gets through), ignores reverse input and keeps a stale velocity that would travel in snapshots. Rapier's brake values are impulses per tick per wheel, clamped by the tyre's friction circle (suspensionForce × dt × frictionSlip), and arcade on purpose: 200 stops the compact from 80 km/h in about 7 m, and reverse runs to maxReverseSpeed, 108 km/h in every class, with a reverse engine sized to get there in a few seconds.
Drivetrain and wheelspin
drivetrain says which axle the engine force goes to: front, rear, or all (an even split). Each driven axle then has to put its share through its tyres, and that is where the classes get their character.
An axle's traction budget is its friction circle, frictionSlip × the load the axle carried on the last tick (the same circle Rapier clamps the wheel impulses with), minus what the axle already spends on cornering (last tick's side impulses). A quarter of the circle is always left for driving. Up to the budget the demand goes through unchanged. Beyond it the tyres spin: VehicleState.wheelspin ramps from 0 to 1 as the demand goes from 1× to 1.4× the budget, and two things happen:
- the axle's side grip drops toward
wheelspinGrip(see Grip and drifting for what grip means): power oversteer on a rear-wheel drive, power understeer on a front-wheel drive; - the force that reaches the road falls to half the budget, so feathering the throttle is faster than flooring it.
Because cornering eats into the budget, a car that holds in a straight line steps out when floored at the exit of a corner, and the lighter the driven axle (see below) the sooner. Weight transfer is real too: accelerating loads the rear, which is why a rear-wheel drive hooks up as it gathers speed and a front-wheel drive loses traction off the line.
The handbrake cuts the engine on the rear wheels (Rapier ignores a wheel's brake while it has engine force), so a front- or all-wheel drive keeps pulling with the front while the rear is locked. That is what makes the handbrake turn of a front-wheel drive keep its speed.
Boost
Double-tap the throttle and hold it: while the boost meter lasts (boostSeconds, 2 s) the engine force is multiplied by the class's boostFactor, and the meter refills in boostRecharge seconds whenever it is not draining, driving on or not; a boost that ran the meter dry does not start again until the key is let go of and double-tapped anew. The factors are sized against each class's traction budget so that every car is pushed past what its driven tyres put down, and while boosting the spinning tyres keep putting their whole budget on the road instead of half of it: the boost therefore always accelerates harder than the plain engine (the muscle, which spins anyway, gains the most) and only costs side grip. What that does depends on the layout: the rear-wheel-drive sedan (2×), sports (1.6×) and muscle (1.5×) step out at the exit of a corner for a proper power drift, the front-wheel-drive compact (2.6×) and the all-wheel-drive SUV (1.8×) spin their fronts and push wide instead, and the rear-engined buggy (2.2×) loosens its rear but mostly rockets away. Boost only applies while accelerating forward; the HUD shows the meter and BOOST.
Engine placement
enginePlacement (front, mid, rear) shifts the centre of mass by 20 % of the half wheelbase toward the engine, and a mid engine gets 80 % of the yaw inertia of the uniform box. Everything else follows from the physics:
| Placement | Load | What you feel |
|---|---|---|
| front | ~60 % on front | Sharp turn-in and a light tail: easiest to power-slide on a rear-wheel drive, plows on a front-wheel drive |
| mid | even | Rotates quickest; the rear has enough load to put the power down |
| rear | ~60 % on rear | Traction off the line, lazier turn-in; once the heavy tail lets go it swings wide |
The rear-axle slip the drift model reads is measured from the shifted centre of mass, so a steady grippy corner reads near zero for every placement.
Steering
The input is slewed toward the target at steerRate (fraction of lock per second), and the lock itself shrinks from steerLock at standstill to steerLockHighSpeed at maxSpeed. Both matter for feel: the slew gives weight to the wheel, the speed-dependent lock keeps the car controllable at speed without a stability assist. Note that the lock is rarely what limits a turn: past about 50 km/h the tyres are, so frictionSlip is the number that decides how tight a car corners, and an oversized lock only scrubs speed.
Grip and drifting
Rapier's sideFrictionStiffness is not a friction coefficient: each tick the side impulse removes that fraction of the wheel's lateral velocity. So 1.0 is instant, perfect grip; 0.04 lets the rear slide with a half-life of a few hundred milliseconds. The model keeps a grip value per axle (VehicleState.gripFront, VehicleState.gripRear) and moves each between targets:
| Situation | Grip target |
|---|---|
| Handbrake held | rear: handbrakeRearGrip |
Handbrake released, rear-axle slip above driftSlipThreshold | rear: driftRearGrip |
| A driven axle's tyres spin | that axle: toward wheelspinGrip, by how hard |
| Otherwise | gripFront / gripRear |
The lowest applicable target wins. Drops are instant; recovery ramps at gripRecovery per second (0.15 → 1 in about 0.35 s). The slip is measured at the rear axle (lateral velocity at the centre of mass minus yaw rate × the distance to the rear axle), because during a proper drift the chassis centre barely moves sideways while the rear does.
Two Rapier quirks the model works around:
- A wheel's brake is ignored while it has engine force, so the handbrake cuts the engine on the rear wheels.
handbrakeForceis therefore a mild drag, not the thing that breaks traction; the grip drop is. - The front wheels keep full grip during a handbrake drift; that is what produces the rotation (and the catch when you steer into it).
The parking brake
A vehicle nobody drives is parked (VehicleState.parked, packages/sim/src/vehicle.ts): every vehicle the simulation spawns (a mission's car, a vehicle spawner's, a garage's, the player's own at the start) and every car its driver gets out of. Driving it (any throttle) lets the brake off. While parked and slower than 2 m/s, every wheel (and the trailer's) brakes at the class's full brake force with its grip kept, unlike the handbrake, which drops the rear grip for a drift; and on a slope (from a 2 % grade) the locked tyres hold the car where it came to rest, up to 0.3 g of grip (a 30 % street), since the wheel brakes alone let a car creep down a hill. A shove harder than that slides it, and it is held again where it stops; a car bailed out of at speed rolls on until it is down to a crawl. The flag travels in the snapshots (protocol 21), so the client's twins hold where the server's car does. Pinned by packages/sim/src/parking.test.ts (every class on an 18 % street, facing up and down) and E2E-434.
The classes
Twenty-three classes, cycled with V: six cars, three trucks, three two-wheelers, the police car and the city's own (the taxi, the bus, the ambulance, the fire truck and the garbage truck, after the police car in that order), then the pickup, the limo, the delivery scooter, the ice-cream van and the tractor. The compact is the base; the others are deltas of it (or of the class they are built on) in vehicle-classes.ts.
| Class | Engine / drive | Mass | Engine | Grip (frictionSlip) | Character |
|---|---|---|---|---|---|
| compact | front / front | 1100 kg | 11 kN | 4.0 | Nimble, too weak to spin its tyres; handbrake drifts; plows when floored at the limit |
| sedan | front / rear | 1400 kg | 18 kN | 3.6 | Balanced, holds unless provoked |
| sports | mid / rear | 1250 kg | 32 kN | 4.2 | Fast and grippy, rotates quickest, steps out when floored out of a corner |
| muscle | front / rear | 1650 kg | 36 kN | 3.2 | Far more engine than rear tyre: burnouts, power drifts, donuts; feather it to hook up |
| suv | front / all | 2100 kg | 22 kN | 3.5 | Heavy, planted, slow to turn |
| buggy | rear / rear | 850 kg | 12 kN | 3.8 | Tiny, traction off the line, pendulum tail on the handbrake |
| van | front / rear | 2400 kg | 20 kN | 3.4 | A tall box on four wheels; the smallest truck, three seats |
| box truck | front / rear | 6000 kg | 45 kN | 3.2 | Six wheels on three axles (two dual), eight metres long; slow, wide turns, strong brakes |
| semi | front / rear | 7500 + 9000 kg | 70 kN | 3.2 | A three-axle tractor towing a two-axle trailer on a ball joint: eighteen tyres; jackknifes when shoved |
| scooter | rear / rear | 170 kg | 1.5 kN | 3.0 | Two wheels, nimble, 50 km/h |
| bicycle | mid / rear | 95 kg | 0.45 kN | 2.8 | Pedal power, one seat, 36 km/h |
| motorbike | mid / rear | 260 kg | 4 kN | 3.4 | Two wheels and a real engine; spins its rear tyre off the line and out of corners |
| police car | front / rear | 1500 kg | 21 kN | 3.9 | The sedan turned up: quicker (187 km/h), as planted when floored; black and white, a flashing light bar |
| taxi | front / rear | 1450 kg | 17 kN | 3.7 | The sedan in yellow with a chequered band and a lit roof sign; a little lazier off the line, 166 km/h, five seats |
| bus | rear / rear, two axles (dual rear) | 11,000 kg | 60 kN | 3.2 | Twelve metres, seventeen seats; slow off the line (about 2 s to 36 km/h) and at the top (80 km/h), a turning circle more than twice the sedan's |
| ambulance | front / rear | 3200 kg | 30 kN | 3.5 | A box on the van's chassis, white with a red band; quicker than the van (150 km/h), a light bar and a siren, four seats |
| fire truck | front / rear, three axles (two dual) | 9500 kg | 72 kN | 3.2 | Red with white bands, a ladder on the roof, a light bar and a siren; pulls like a lighter truck, as wide in the turns, four seats |
| garbage truck | front / rear, three axles (two dual) | 12,000 kg | 64 kN | 3.2 | The heaviest rigid truck: a cab and a green compactor; slow (86 km/h) and wide, three seats |
| pickup | front / all | 2200 kg | 22 kN | 3.5 | The SUV’s running gear under a crew cab of four and an open load bed (its trunk: ten slots) |
| limo | front / rear | 2600 kg | 26 kN | 3.6 | A black saloon stretched to 6.6 m, six seats; lazy off the line and slow to turn |
| delivery scooter | rear / rear | 185 kg | 1.5 kN | 3.0 | The scooter with a top box (three slots), one seat, 47 km/h |
| ice cream van | front / rear | 2900 kg | 19 kN | 3.4 | A van in pastels with a cone on its roof, slower than the van; its hatch opens a counter when it stands |
| tractor | front / rear | 2600 kg | 13 kN | 3.6 | All pull and no speed (40 km/h), big rear wheels drawn, one seat in a roll frame; no trunk |
The pursuit tune (PURSUIT_TUNE in vehicle-classes.ts, GAME_DESIGN §12: the police's faster cars at three stars) is fitted to a vehicle, not a class (Vehicle.pursuit, in the hash and the snapshot, so the client's twin drives the same car): an interceptor's engine and tyres, a quarter more engine force, a fifth more top speed and a tenth more tyre friction, the chassis, the brakes and the steering the class's own (classOf gives the tuned numbers). A police car so fitted reaches the sports car's top speed (223 km/h) and still keeps its rear tyres when floored; vehicle.test.ts pins it beside the plain police car's envelope.
A class's features are flags the client reads (VehicleClass.features, GAME_DESIGN §6.1): a light bar flashes in its livery's pattern (apps/client/src/renderer/light-bars.ts: the police's two blue lamps in turn, the ambulance's red and white wig-wag with a double flash of both, the fire truck's reds in double flashes, one side then the other; the lamps are materials of the models shared by every car of the livery, so a pure function of the time lights them and nothing goes on the wire), a siren wails while its driver asks for it (the input's siren flag: the horn key G switches it at the wheel, an NPC at the wheel always runs it; the car's siren in the snapshot, heard by everyone within 150 m, fainter with the distance; the horn key in any other car only honks), a roof sign is lit (the taxi's), a ladder rides the roof (the fire truck's). The livery paints the model's body instead of the owner's colour: the police's white doors and roof over its black, the taxi's yellow, the ambulance's white, the fire truck's red. vehicle.test.ts pins the new classes' envelope: the taxi behind the sedan off the line, the bus slower than the box truck to 10 m/s with a turning circle more than twice the sedan's, the ambulance ahead of the van and faster at the top, the fire truck's six wheels down through a full-lock turn and quicker than the garbage truck, the garbage truck the heaviest rigid truck and the slowest at the top, the siren running only for a class with one and its driver asking.
Glass, doors and the people inside
All of it is the client's, read from what the snapshots already carry; nothing goes on the wire.
- The glass of every vehicle (
glass, the windscreen, andtint, the other windows, inassets/blender/models/vehicles.py) is drawn see-through, the windscreen lighter (30 % opaque) than the rest (50 %), in batches of its own after everything opaque (apps/client/src/renderer/models.ts). Behind it the model has holes in its body and a grey interior up to the window sills, so a ray through any window ends on a floor, a seat or a person, never on the road under the car. - The people inside are drawn in their seats as the riders of the open seats are (
riders.ts): a car's front row sits with its head mostly under the windscreen, where the steep camera sees it, every head under its roof; a bus's and a truck cab's people sit on cushions. - The doors are separate parts of the model, hinged at their front edge (a bus's two doorways and an ambulance's back doors are two leaves each), and each seat names the doorway it is reached by. When the snapshots show someone take or leave a seat (a player, the player's own character, an NPC, a crew getting out), the doorway the person used swings out about 65° in a fifth of a second, stays open while the person steps in or out and shuts again (
car-doors.ts): the one nearest where it got out (its seat's, or the one across when its own was blocked), or where it stood getting in; a door already opens as someone starts getting in at it (theenteract) and stays open through its half second. A carjack holds the driver's door open through its half-second beat, and the carjacker getting in after it as anybody does. A car with a door ajar is drawn as its body without doors and each door turned on its hinge; a shut car costs nothing more. The buggy and the two-wheelers have none. - Cracks. A window a shot stops on, or one a seated shooter fires out through, shows a star of cracks where it did, for as long as that car exists (
glass.tstraces the shot's line against the model's panes, the car placed as the snapshot has it and as it stood up to three ticks before for a moving one;cracks.tskeeps the cracks and draws them, one thin-instanced decal mesh for every car). A car whose health comes back (a repair, a respray, a reset, a respawn), that jumps to a spawn, whose class changes or that goes has clean glass again, and so has every car after a level reset; a car its own driver resets withRtoo. A shot from outside at a standing person's height usually stops on a car's door, below the glass: the windows crack from drive-bys, from shots at the taller vehicles and from above.
/?vehicle-gallery (no server) shows every class with a person in every seat, its doors swinging and its windscreen cracked, the semi with its trailer, from the game's camera at first and then anywhere (W A S D pan, the wheel zooms, the mouse orbits, 1–3 its height, F back; ?vehicle-gallery=<class> looks at one, H holds the doors open).
What the tyres leave behind
The client draws the slip the model makes (apps/client/src/renderer, nothing here touches the simulation), for every car on screen, the player's and everyone else's:
- Slip per wheel (
slip.ts): each tyre's contact point moves at the chassis's velocity plus its spin about the centre (v + ω × r). What of that runs along the car's left axis is the tyre sliding sideways (the drift slip of Grip and drifting, read at every wheel): it counts from 1 m/s and in full from 5 m/s. A driven axle whose grip sits below the class's own is spinning (the model lowers it towardwheelspinGripby how hard the tyres spin), so a burnout smokes standing still. The handbrake drags the locked rear tyres along the road while the car rolls. A wheel off the ground slides on nothing. A remote car's slip comes from its snapshot (the pose with both velocities, the handbrake, the wheels' contact, the grips), so no snapshot carries a slip of its own. - Tyre marks (
marks.ts): a wheel sliding hard enough lays a quad 22 cm wide (12 cm for a two-wheeler, 32 cm for a truck) along its path every 40 cm or more, each starting on the edge the last one ended on, so a slide reads as one mark around the bend. Black on paved ground, brown ruts on loose ground; they lie 5 cm over the surface (over a road's ribbon at 2 cm and its paint at 3.2 cm, never in a plane with either), stay whole for ten seconds and fade out over the next ten. At most 1,200 quads lie on the ground; past that the oldest gives way. - Smoke and dust (
effects.ts): a sliding wheel puffs up to twelve puffs a second, by how hard it slides: light grey tyre smoke on paved ground, dust on loose ground. Which it is comes from the ground under the wheel (ground.ts): a road decides first (adirtroad is loose), else the level's triangle there by its colour (earth, grass and gravel loose; asphalt, pavements, concrete and paint not), and the level's plane is paved. A puff is a flat-shaded low-poly ball that swells, drifts with a quarter of the car's speed, rises and shrinks away in about a second and a half. - Landing dust: a car that comes down after a quarter of a second in the air, falling at 3 m/s or more, throws up a ring of dust around itself.
- Hit sparks: a shot that stops on a wall (level geometry, nobody's body, found by tracing the shot in the client's world) or hits a car throws a burst of hot shards; one that strikes loose ground throws up a puff of dust. A person hit bleeds (see below).
The puffs and the sparks are thin instances of one mesh each (one draw call apiece), the marks one mesh: the frame rate holds on a software renderer. test-drift-pad is their mini level (an asphalt skidpad with a painted circle, a dirt patch, a run up to a small ramp, a shipping container to shoot at); E2E-100 to E2E-105 check them.
Collisions
Cars and people meet as solid bodies: they never sink into each other by more than a few centimetres for a step, nor pass through. Measured before (crashes.test.ts runs the same crashes): two sedans head-on at 50 km/h each sank 0.50 m into each other (0.76 m at 100) and stayed entangled for nearly three seconds, a parked car rear-ended at 150 km/h took 0.29 m, a dummy hit at 60 km/h was found 16 cm inside the car. Rapier made a contact's points only 2 mm before the touch, so two cars closing half a metre a tick met already deep in each other (its hard continuous collision detection only wakes for a body crossing its own thickness in a step, about 140 km/h); and a contact it predicted further away kept a single point for the solver, whose blow at a corner spun the car into the other. The world (createSimWorld, the only place its integration parameters are set) now:
- hands the solver every point of a contact 10 cm before the touch (
normalizedPredictionDistance), so bodies closing up to that much a step stop at the touch with the whole contact; - cuts a tick into up to four physics steps (240 Hz,
substeps.ts) when a car closes on another body faster than that: on another car or a trailer, or on a person on foot. What counts is how fast the gap between them shrinks: their velocities along the line between their nearest points (Rapier's distance query, asked only of a pair whose boxes on the ground, widened a little for the hulls, may meet within the tick), with what their spins may add (a car that tips, rolls or spins brings its other corners round). Of a person only the car's part counts: a person running or tumbling into a car meets it as it meets a wall, by its own pace, and a capsule touches a car at one point however it is found. The tick takes as many steps as the fastest such pair needs to close at most 10 cm a step, so the contacts are found again before the bodies sink into each other and a car turned by a blow is caught before a corner of it swings deep into the other. Cars queueing, scraping past each other or shoving each other along, a crowd by a parked car: none of them closes fast, and the tick takes one step. The bodies are swept in order along x, so the decision costs a few dozen box tests a tick; a client leaves out its proxies far from everything it simulates. Against the level one step is solid at any speed (a sedan into a wall at 150 km/h sinks under a centimetre). The systems still run once a tick (the inputs, the vehicle controllers' suspension and tyres, the walking), so the driving envelope is the one it was; - pushes overlapping bodies apart at 240 Hz (
contact_natural_frequency; Rapier's is 30): a car pressed against another by its engine no longer holds 6 to 11 cm into it; - gives every simulated car soft CCD (
setSoftCcdPrediction, half a metre a step,SOFT_CCD_REACH): what a car will hit within the step becomes a contact before it gets there; the hard CCD stays for the fastest. A client's proxy cars, which the step never moves, look ahead for nothing.
The deepest overlap of head-on, rear-end, T-bone, ramming and garbage-truck crashes at 50, 100 and 150 km/h is now under 4 cm, a person hit at any speed under 1 cm (crashes.test.ts pins 5 cm and 2 cm). A car's wheels stand on the level (and loose things), never on another car or a person (WHEEL_GROUPS): a wheel ray that met another car's body pushed its car up onto it with the whole spring of its suspension. It costs little: in the chaos benchmark (64 cars of traffic, 60 pedestrians, 18 chasers, the police at three stars, grenades) 95 % of ticks take one step, and the server's step and the client's frame are within a few per cent of what they were (see Performance). Counting every pair that moved fast in any direction, as the first cut of this did, took two steps or more in 62 % of those ticks and cost the server a third more.
Side hits (decided 2026-10-12: only a hard ram tips a car). Gripping at four times its weight (frictionSlip), a car pushed at its doors would pivot over its far wheels at the slightest shove, so two things hold it down:
- The tyres scrub (
SIDE_SCRUBinvehicle-classes.ts): while the car's centre slides sideways faster than 1 m/s and more than twice as fast as it rolls forward (a slip angle past about 63°, never a drift, whose centre barely moves sideways), every tyre's friction circle is its load, not four times it. Shoved, the car slides. - A roll steadier (
ROLL_STEADY): a damped spring on the roll angle while a wheel is down, its torque capped, letting go past 60°; past that, until upside down, a damper alone, so a car that goes over mostly comes to rest on its side.
A T-bone at a coast (under about 40 km/h) and a slow ram at full throttle shove the car aside on its wheels; a ram at full throttle from about 30 to 40 km/h on tips it onto its side, and the hardest onto its roof. A rolled car is set upright at a spawn by R. vehicle.test.ts pins it ("side hits"), and no car rolls in a full-lock turn.
What gunfire and wrecks leave behind
The client draws the shots, the hits and the wrecks too (apps/client/src/renderer: gunfire.ts, tracers.ts, blood.ts, wreck-smoke.ts, explosion.ts, vehicle-fire.ts, flat-tyres.ts, drawn by effects.ts); none of it changes what a shot did, which is the server's (packages/sim/src/weapons.ts, fire.ts).
The weapons in the hand (
models.ts, the models inmodels/weapons.glbfromassets/blender/models/weapons.py): whoever shows a weapon (players, guards, officers, chasers) holds its model as the figure is posed that frame, its grip in the right fist: the pistol, the tranquilizer and a grenade along the right forearm, the shotgun, the rifle and the launcher from the right fist toward the left when the hands are apart and ahead, else the way the torso faces (weaponFrame). From a seat the hands stay on the wheel until a shot, then hold the pistol out of the window for a moment. A rocket flies as the rocket's model, nose first along its flight, trailing smoke; a grenade tumbles as its body turns.Tracers (
tracers.ts): a shot is a short, bright, thin streak that flies from the muzzle to where the shot stopped at a few hundred metres a second (seen for a few frames, never less than one), its head bright and its tail fading, a quad turned about its own line to face the camera and added to the scene's light. Each gun has its own: the pistol's a 2 m yellow streak, the rifle's 4 m long, wider and white-hot, the shotgun's a short thin streak per pellet (its pellets after the first are traced in the client's world with the weapon's spread), the tranquilizer's dart slow and pale.The muzzle (
gunfire.ts): a streak starts at the muzzle of the gun in the shooter's hand, as the figure is posed that frame: the weapon model's own muzzle (its<name>-muzzleempty, read once bymodels.ts), else the tip of the fist that holds the barrel's end and the gun's length on toward the impact. From a car the shot leaves by the window: from the seated shooter's hand along the shot to the cabin's side or its windscreen, whichever it meets first (an open seat's pistol is held out in the open). A muzzle flash, a star of flame pointing along the shot, burns there for a few hundredths of a second. The player's own shots are drawn at the trigger, from the local world; another's from the snapshot's shot event, whose shooter is the character of its owner (an NPC holding its weapon) standing nearest the shot's origin.Blood (
blood.ts): a person hit by a shot, a blast or a car bleeds, more for a harder hit (a punch, a dart or a hit that hurt nobody does not): a burst of red mist along the shot (out of the exit wound, a little back out of the entry; the direction is that of the snapshot's shot whose line passes nearest the wound), a few drops that fall, and a stain on the ground under the person and where each drop lands, whole for fifteen seconds and gone after thirty. The mist lives in the person's own frame, as an emitter parented to the body does in an engine: its particles are kept as offsets from where the person is drawn, turned with them, so the burst stays on a person who runs on instead of hanging where the round struck; the wound is moved from the snapshot's place to where the person is drawn (a remote is drawn a little in the past, the player's own character a little ahead). The drops leave the body: they carry the person's velocity, as anything thrown from a moving body does. A person out of sight (in a closed car) does not bleed into view.The dead and the wrecks: the dead lie as their ragdoll leaves them, in their own colours; one killed in a car is drawn slumped in its seat (a rider killed on a two-wheeler that blows up is thrown off it at once). A wreck is drawn as the car it was, its paint untouched: it only smokes and burns. Once it has blown up, its shell is charred: the paint scorched toward soot, its other materials (glass, trim, lamps, which no longer glow, the light bar) burnt the same way.
Explosions (
explosion.ts): every blast (a rocket's, a grenade's, a car blowing up) is a white-hot flash swelling to most of its reach, a fireball of flame shards bursting out and rising, a shockwave ring racing over the ground past its reach, a column of dark smoke and a few bursts of sparks, and a scorch left on the ground for 45 seconds, all sized by the blast's reach; the camera shakes by how near and how big it was (camera-shake.ts; not when the system asks for reduced motion).Cars smoking and on fire (
vehicle-fire.ts): a car at 35 hit points or below smokes out of its engine bay, lighter at first, thicker and darker the lower it gets; on fire, flames grow out of the bay over four seconds under black smoke; blown up, the whole wreck burns while the snapshot says it does, then only smokes. The fire crackles and a blast booms (audio.ts); the HUD saysFIRE — get outto a player in a car on fire. A car whose body sits in water (@foi/sim'svehicleInWater: the surface over the bottom of its chassis, wading deep, flooded or sunk; not a sheet shallower than its ground clearance) neither smokes nor burns: driven in smoking or on fire, its smoke and flames die away within 0.4 s, before the server's word that the fire is out arrives (the puffs already out drift off and fade as they do), and come back only once it is out of the water; the HUD says neitherSMOKINGnorFIREthere.Flat tyres (
flat-tyres.ts,models.ts): a flat tyre's wheel is drawn squashed onto its rim; a tyre going flat pops in a puff of rubber smoke and sparks, and a flat rim rolling faster than 4 m/s throws sparks, more the faster.Wreck smoke (
wreck-smoke.ts): a wreck on screen smokes for as long as it is there: dark puffs rising out of its engine bay (over the front axle of a front-engined car, behind the cabin of a mid-engined one, over the back of a rear-engined one), billowing out to four times their first size as they climb for three to five seconds and drifting off on the wind. Thick and black at first, it settles over the first eight seconds into a thinner grey; a wreck first seen already wrecked smokes as an old one. A wreck in water does not smoke (its plume dies away as it goes in). Nothing like the tyres' light grey smoke, low, wide and gone in a second and a half.
Every one of these is a pool made once (the streaks one mesh, the flashes, the mist, the drops and the plume thin instances, the stains a ring of quads like the tyre marks), capped, and only drawn for what the camera sees. E2E-340 to E2E-346 check them.
Crashes
A velocity that changes by more than 3 m/s within one tick is an impact (braking at four g changes it by half a metre per second a tick): a wall, another car, a hard landing. It costs the car 5 hit points per m/s above that, so a 23 m/s (83 km/h) wall hit wrecks it; the HUD logs it as a crash. On the player's screen the crash is the prediction's: heard, shaken and logged on the frame the car stops, the camera stopping with it, a round trip before the server's word (An impact is the prediction's). A reconciliation or a reset is not an impact. R and a class change repair the car.
A reset or a class change asked for during the half-second cooldown is kept and applied the moment it ends, so one press always counts, and only once.
Fire and explosions
A car brought low catches fire, burns and blows up, and its blast lights the cars beside it (fire.ts, run by the server's world alone, the rule vehicleFire; a client draws what the snapshots carry, burning and exploded):
- Catching fire. Anything with an engine (not a bicycle) catches fire at 12 hit points or below, whatever brought it there (a crash, a shot, lava, a wreck); a blast lights a car it leaves at 50 or below, a bullet at 25 or below. Water puts a fire out, a blown-up wreck's blaze too, and nothing catches fire in it, however low it is: a car is in water when the surface is over the bottom of its body (its centre less its half height: wading deep enough to wet its chassis, afloat, flooded or sunk; a sheet shallower than its ground clearance, about 0.45 m for a car, is no water to it). Lava wrecks a car and sets it alight.
- Burning. The fire takes a hit point a second off the car and 3 off everyone still in it, and its fuse runs: 5 s plus a second per metre of the vehicle's half length (a car's about 7 s, a bus's about 11). A car wrecked while it burns (shot to pieces) blows up within a second and a half.
- Blowing up. A blast as big as the vehicle (3 m plus 1.75 m per metre of its half length: a car's about 7 m, a box truck's over 9, a bus's 11.5 at most), 100 hit points at its centre fading to nothing at its edge, shoves and hurts everything in reach, kills whoever is still aboard, whatever the vehicle, and throws the wreck up, tumbling. In a car the dead stay slumped in their seats; a two-wheeler's riders (a scooter, a motorbike, a delivery scooter) are thrown off it as it goes up, dead, with its speed, and the blast flings them on. Only infinite health (the legend's debug switch) and the griefing rules (a ghost just back from the dead, another player's fire where players may not hurt each other: Social) spare anyone aboard. Its wreck burns 20 s, then smoulders as any wreck does.
- Chains. A car within two thirds of a blast's reach catches fire whatever its hit points, with a short fuse (0.3 s at the centre to 1.2 s at the edge of that inner part); one further out only if the blast left it low. So a row of parked cars goes up one after the other.
- Whose fire. Whoever set a car alight owns its fire, its blast and every fire its blast lights in turn; a car blowing up where the police or a witness hears it is a crime of 15 Heat for its owner (AI). Everyone gets out of a car on fire: chasers and the police leave theirs at once and never take one, the traffic's drivers bail out and run.
R, a class change and a garage's respray put a fire out with the repair. A game can switch fire off (its rule vehicleFire). test-fire is the level to try it on.
Tyres and nail strips
Every raycast wheel has a tyre that can go flat (VehicleState.tyres, a bitmask). A flat tyre sags (its radius drops to 70 %), grips half as well, drags its corner (a brake of 10 % of the class's maxBrakeForce, so the car pulls toward it) and takes 15 % off the top speed (four flats leave 40 %). Tyres pop on nail strips: half-metre-wide strips the level lays across a road (LevelData.nailStrips, no collider), and the strips the police throw across a road at three stars (nails.ts: entities with a pose and a length, carried by the snapshots with a twin in every client's world, kept to the snapshot's length; one pops a tyre within 1.5 m of its height, so a strip on a bridge leaves the street under it alone). A thrown strip unrolls from the kerb, its end there staying put, to the whole road wide in a second, and is reeled in the same way (nailSystem, every tick before the cars, so a tyre meets the strip as long as it is then). Every tick the path each wheel's contact point took during the last step (its velocity times the step) is swept against the strips, so a wheel that crosses one goes flat at once however fast it goes; a two-wheeler's two raycast wheels of an axle go together. A test of where the wheel landed would not do: at 60 Hz a car above 30 m/s moves further than the strip's width in one tick and would step over it about every other time. R (a reset) and a class change put new tyres on. The flats travel in the snapshot with the other integrating state, so prediction keeps them.
Pedestrians and punches
A pedestrian touched by a car (or trailer) doing more than 20 km/h is knocked down: for ten seconds it is an inanimate box, its rotations freed and some friction on, toppled the way it was pushed (a bullet pushes nothing, so it is toppled forward: a knocked-down character always ends up lying, never standing dead like a pin) and deaf to its input; then it stands up on whatever lies under it (a ray finds the ground, so a box that fell on a deck or against a wall is not lifted into the air) and walks on. Pedestrians spawn on open ground only (isOpenGround), never inside a structure. Fists do the same to a pedestrian within 1.6 m in the front half-circle (and take 10 hit points), and shove anyone else standing there. Gunfire scares them: a wandering pedestrian within 15 m of a shot's muzzle, of where the shot ends (a punch is no gunfire) or of a blast runs away from it at 30 km/h for three seconds, turning a quarter turn aside when it runs into something, then wanders again (pedestrians.ts, on the server, from the world's seeded generator); a range dummy stays at its post.
Shooting, health and cover
The mouse gives a shot its yaw only, but a shot looks for targets along that line (aimTargets): people on foot and cars that are not wrecked whose centre lies within their width of the vertical plane through the aim and in sight from the muzzle (a wall in the way hides them), nearest first, whatever their height. The input's target (the scroll wheel; it wraps) picks one, and the shot turns and pitches to its centre of mass, so a dummy on a tower or a pedestrian in a pit is hit from the street; with nobody on the line it flies level along the aim. The first pellet goes straight at the pick, the rest take the weapon's spread around it; whatever body a pellet's ray hits first takes the damage (a wall in the way stops it). Rockets and grenades still fly level.
A gun spends its ammunition, items in the shooter's inventory, one a shot, and does not fire without; the weapon held is the selected slot's (Items).
Every character and every vehicle has hit points (damage.ts): players 100, pedestrians 25 (one pistol shot), cars 100. Pistol 25, shotgun 10 per pellet, rifle 20, fists 10; a rocket's blast 80 and a grenade's 70 at the centre, fading to nothing at 7 m, the shooter included. A pedestrian falls on any hit; anyone at zero falls like a knocked-down pedestrian and, when the ten seconds are up, respawns with full health, a pedestrian at its post and a player at the nearest spawn point. A car at zero is wrecked: it answers nobody (no engine, no steering, the handbrake on), cannot be entered, is drawn in its own paint scorched dark and smokes, and a reset or a class change repairs it; long before that it catches fire (Fire and explosions). A trailer passes its hits to the tractor. Every hit is an event the HUD logs.
Cover: with a wall or a car (never a person) within 0.6 m of the capsule in any direction (the aim is probed first, then every 45° around it; the nearest wins), Q pins the character to it, hidden: it cannot shoot or be shot, and is not a target. The ray's hit normal, reversed, is kept as the direction into the cover (Character.coverYaw), so an oblique aim still pins the character square to the wall. If a second ray at head height clears the obstacle the cover is low (a parapet, a car's hood) and hiding is a crouch, drawn half as tall; against tall cover the character is drawn flattened against the wall (turned to face it, thin and wide). Pinned, the movement keys move the character along the cover at 3 m/s, a crouching pace (the input is projected onto the cover's direction, so it never walks away from it), and it stops where the cover ends: before each step a ray from the capsule's leading edge into the cover must still hit it. Cover that is no longer in front of the character (a car that drove off, a shove) is let go of.
The right mouse button pops the character up (it shoots and is hit as usual) and hides it again; Q lets go. At tall cover, popping up first looks for the cover's end: rays into the cover from points 0.25 m apart along it, up to 1 m, on the side the aim leans to and then the other. Where one finds nothing the cover ends, and the character steps out past it at 6 m/s, ignoring the movement keys meanwhile, until the whole capsule is clear of the end (0.4 m); out past the edge (Character.peek) it has a line of fire at what the wall hid. Hiding again steps it back behind the cover the same way, and a capsule that sticks out past the end tucks itself behind it. Nothing about this is remembered between ticks: each tick decides from where the character stands relative to its cover, so the predicting client and the server agree however late the inputs arrive. With no end in reach it pops up where it stands, as at low cover. A pedestrian hit, a car entered or a death clears cover.
The fixture level's shooting range (west of its spawn rows, LevelData.dummies) is built for this: behind a 1.1 m wall the line x = 22 holds three dummies at three heights, a 3 m high wall stands beside it, and a walkway with a parapet looks down on two more.
The grenade
With the grenade selected, the click pulls the pin and starts the fuse (3 s); letting go throws it toward the cursor, a 30° lob sized to land at the cursor's distance, up to 50 m, with what is left of the fuse; held past the fuse it goes off in the hand. Either way one grenade of the stack is used up (it is its own ammunition), and the last leaves the fists. Switching weapons drops a lit grenade silently. The HUD shows the seconds left while the pin is out.
Trucks, trailers and two-wheelers
A class carries any number of axles (axles, front to rear; the steered ones are the front group for grip and drive, the rest the rear). Dual rear axles are drawn and counted as two tyres per wheel. The semi's class has a trailer: a second body with its own raycast wheels (no engine, no steering, the tractor's brakes), hitched to the tractor through a Rapier spherical joint at the fifth wheel, with contacts between the two bodies disabled. The joint sits at the tractor's centre-of-mass height, so the trailer's push in a corner cannot roll the cab; the trailer's own centre of mass sits two metres below its box, so it does not roll either. Two-wheelers are the arcade kind: a raycast vehicle with one wheel per axle on the centre line (a car with a single track), held upright by a self-righting torque about its forward axis, a critically damped spring on the roll angle (UPRIGHT_FREQUENCY, 10 rad/s). Nothing can tip it, which is the point; the lean into corners is drawn by the renderer from the speed and the yaw rate (the lean a rider needs to balance v·ω/g), not simulated. The motorbike still spins out when thrown into a full-lock turn at speed, like a car would. The rider, and a pillion behind, sit on the model's seat anchors and lean with the bike (riders.ts); so do the buggy's driver and passenger, whose seats are open to the sky.
Landing
While all four wheels are off the ground the model remembers how fast the car is falling. At touchdown, a fall faster than 4 m/s dulls the steering and the engine to landingControl of their value for up to landingStun seconds (the full stun at 10 m/s, proportionally less below), so a jump is committed to in the air and the car settles before it answers again; the HUD shows LANDING meanwhile. Brakes and the handbrake keep working. Heavy cars stun longer, the buggy barely.
On foot
The player is a character (character.ts): a dynamic capsule with locked rotations whose horizontal velocity, while it stands on something, moves toward what the input asks at 30 m/s² (45 m/s² when stopping): walking is 4 m/s (14 km/h), sprinting 9.7 m/s (35 km/h, a sprinter's pace, and the fastest any person runs: the chasers, the guards and the police on foot use the same sprint, so none outruns the player), a jump 5.5 m/s upward on a fresh press of the jump key while standing on something. Standing means ground within 15 cm below the capsule (a ray cast down under its centre, or under the rim of its feet when it stands on an edge) that it is not leaving (its velocity along the ground's normal under 1 m/s: walking up a slope stands, a jump's first tick does not). In the air, after a jump or off a ledge, the horizontal velocity is the one it had when its feet left the ground: the movement keys neither steer it nor speed it up until it lands (the facing still follows the aim). A bystander running away (a pedestrian from gunfire, a car's driver from a carjacker, a mission's target to its refuge) holds FLEE_PACE of the stick and runs at 30 km/h. Being a dynamic body, a car shoves it aside instead of stopping dead on it; a slow car's shove is a step aside, 2 m/s across the car's way at most however long it pushes, never a dash down the street.
Before it moves, the capsule is cast along its velocity for one tick: a wall, a car or any other steep surface in the way (normal less than 0.5 upright) removes the velocity component into it, so the character slides along obstacles instead of pushing into them and popping on top. Floors and slopes are left to the solver, which is how the editor's stairs work: every such staircase is drawn as treads but walked as the wedge through the step noses (34°), so the capsule simply climbs and descends it. A real step (a kerb, a stair of a modelled building, a doorstep: up to 35 cm, STEP_HEIGHT) is taken in stride: when the cast meets something fixed and steep that the capsule would clear 35 cm higher, with ground ahead no higher than that, the character is lifted onto it at once and held level for a fifth of a second, still standing; going down, a character that stood on the last tick and finds ground no more than 35 cm below is set down on it. Either way it never leaves the ground, so the figure plays no jump or fall at a kerb; a low wall, a crate or a 50 cm ledge is no step and blocks. And a character that stood on the last tick, and neither jumped nor was thrown (a blast, a punch), keeps to the ground: it rises no faster than the slope under it climbs (groundFollowing). The solver's push off an edge the capsule grazes (a planter under a tree, the corner of a kerb at a junction, a seam of the ground) used to launch a sprinter half a metre into the air. The ground under the feet is the flattest any of the five probe rays finds, so a ray that strikes a stair's edge, and gets a normal tilted off it, does not read as leaving the ground.
Ladders, lifts, escalators, falls.
- Ladders (
ladders.ts, a level'sladders):Eat a ladder's foot, or at its top, takes it.- The climber hangs a step back from the wall, facing it, with no gravity.
Wclimbs at 2 m/s (3 withShift) andSgoes down. - At the top it steps off onto the floor beyond the wall's edge; at the foot it stands. The jump key lets go, pushed back off the wall.
- Nobody fires while climbing. The snapshots carry it as the
climbact, and a client's twin finds its ladder by where it hangs.
- Lifts (
lifts.ts, a level'slifts):- A lift is a kinematic platform riding from its lowest stop up through each of its stops and back, standing 4 s at each, at 1.5 m/s.
- Its height is a pure function of the tick, so the server, the prediction and the drawing agree with nothing on the wire.
- A character standing on it is carried: standing is measured relative to the platform, and the rule that keeps a character to the ground lets it rise with the platform.
- A platform coming down onto someone in its shaft sets them on top of it (
clearOfLifts): pressed into the floor of its pit, they would be pushed through it.
- Escalators (a flight of stairs with
escalator: upordown): whoever stands on the slope is carried along its length at 0.7 m/s, walking over the steps as they move. - Falls:
- In the air, a character notes how fast it comes down. Landing from more than 4 m takes health by that speed, all of it from 12 m (then it falls dead).
- A game's
rules.fallDamage: falseswitches falls off. - Thrown down and tumbling is no fall.
E is the interaction key, and what it does is worked out in one place (interactionOf in interaction.ts), by the server, which acts on it, and by the client, which shows it as the prompt above the character. Next to a car going at a crawl (8 km/h or less: nobody gets into a moving car; within 3 m of the chassis, beyond its half length; the nearest first, never a wreck): an empty driver's seat is taken (Get in); standing beside the driver's door (on its flank, between its bumpers; on a bus's kerb side, nearer its front doorway than its middle one) of a car with a driver, the driver is pulled out (Carjack); anywhere else, or when its driver may not be carjacked, the free passenger seat nearest by way of its door is taken (Get in as passenger). Seat 0 is the driver's and forwards the player's input to the car, the others ride along.
Every seat has its door (seats.ts): the doorway the model gives it (vehicle-seats.json, written by npm run models:hulls from the seat empties the client seats people at and the doors it swings, checked by models:check), where its opening is: on the seat's flank level with the seats it serves (the driver's on the left, a middle seat's on the kerb's side), the bus's two on the kerb side (its first two rows by the front one, the rest by the middle one), the ambulance's back seats by its back doors, behind it. A seat with no doorway (a two-wheeler's, the buggy's) is got on and off beside it, either side for one on the centre line. Getting in takes a moment (boarding.ts): the character runs to the seat's door (round the car's nose or tail when the door is on the far side; across the row, through the other door, when its own is blocked), facing the door as it comes up to it, gets in there for half a second, facing it (the enter act, the whole of its clip), and is seated; the seat is kept for it meanwhile, its keys count for nothing (the car stands until its driver is in it), and it gives up when the car moves off, is wrecked, the seat is taken, it is knocked down or the door stays out of reach for three seconds. Getting out with E goes by the car's speed over the ground (exitMode): at 8 km/h or less the character steps out at its seat's door, just outside the body (20 cm from the flank), facing out, on the ground below (down from a bus's floor), and stands there a third of a second (the exit act); faster, up to 35 km/h, it bails out: set down at its door (or behind the car, never in front of it) with the car's velocity and a push of 1.5 m/s away from it, knocked down to tumble (half its ragdoll time at a crawl, all of it at 35 km/h) and hurt by the speed as a car hitting it that fast would (6 hit points per m/s above 20 km/h: 25 at 35 km/h); faster than 35 km/h, or in the air, it stays in. The door asked for must be free: the capsule there touches nothing solid (a wall, another car, a person, one set down the same tick included) and nothing stands between the car and it (a fence along the door); else the nearest free spot along the same flank, half a metre at a time, then the same across the car, then in front of the car and behind it (each half a metre higher if a kerb is in the way). With every way blocked, the character stays in its seat (the doors are blocked); one that must get out (pulled out by a carjacker, put out of a stored car, its car gone) is set down at its door. A character found inside a car's box (a person under a sliding wreck) is set down the same way, on the nearer side where it is free. A reset (R), a respawn, an arrest, a new player and a level's vehicle spawner pass over a spawn point a car or a person stands on. Each class says how many seats it has; switching to a smaller class ejects the passengers it has no seat for.
A wreck's crash hurts the people in it: an impact of a car shot to pieces (or the one that wrecks it) takes from each occupant what a car hitting them on foot at that change of velocity would (6 hit points per m/s above 20 km/h), so a violent one kills them in their seats. The dead stay in the car, slumped in their seats (no shots, the car answering nobody): a body that stays, for good; anyone else for the ten seconds the dead lie, then back at a spawn or a post as the dead come back (a crew killed together never on top of each other: each at a spawn of its own, or beside the point their mission set). Off a two-wheeler, which holds nobody, the body falls once the machine has all but stopped. Whoever takes such a car by force pulls its dead out, to lie at their doors (a body at the wheel is anybody's to pull out, a player's too), and an ambient car whose driver died at the wheel is left where it stands with the body in it, both gone once out of sight. The living get out as above. A whole car's crash hurts nobody inside.
A carjack (GAME_DESIGN §6.3) pulls the driver out at the driver's door (a step clear of the carjacker when it stands right at it) and knocks them down for their tumble time (the knockdown a fast car deals, without the damage: 3 s for a player); everyone else aboard steps out at their own door, standing (a member of the carjacker's party would stay aboard: Character.party). The carjacker stands at the door for a beat of half a second (30 ticks; facing the door, no walking, no shots, the wheel kept for it), then, if the car is still there, whole, driverless and within reach, gets in where it stands as anybody does at a door (half a second more, the enter act), and has the wheel; knocked down meanwhile, it lets go. A player at the wheel can be carjacked only where players hurt each other: the game's rules.playerDamage is a world option (createSimWorld(level, { playerDamage })), on in free roam; with off or rivals (no rivals yet) the key offers a ride instead. Everything is in the simulation and deterministic; the determinism gate replays a carjack. A carjack event names the car and the carjacker's client: the server's missions raise vehicle:<actor>.carjacked from it, the AI makes the people put out flee, and the HUD logs it (you carjacked the compact, player 2 carjacked your car). The client never predicts getting in, out or a carjack: the snapshot's jacking flag and its act byte (board, enter, exit) hold the twin still through the beats and the next snapshots move and seat it.
The fixture level
test-core-gameplay, authored in the Babylon.js Editor project (apps/level-editor) and pinned by the fixture contract test (packages/world/src/levels.test.ts): a 5 × 5 grid of blocks with box buildings, open avenues through the middle and a plaza at the crossing. On it, to test height, jumps and stairs:
- two jump ramps on the north-south avenue, climbing away from the plaza; every ramp is a solid wedge (a convex hull), so a car arriving from behind meets a wall instead of wedging underneath;
- a bridge 6 m over the avenue with 14° ramps at both ends: drive under it, over it, or off it fast enough;
- a tunnel on the east avenue, 4 m high;
- a parking garage with a first floor at 4 m and a roof at 8 m, a ramp per level (alternating sides, through holes in the floor above) and a staircase from the street up through the first floor;
- a terrace on the plaza, 2.4 m up, with stairs on two sides.
What the tests pin
vehicle.test.ts is the safety net for tuning. On a flat level it checks, among other things:
- the compact: 0 → 50 km/h in under 3 s, top speed within 80–105 % of
maxSpeed, driving straight stays straight; - braking cuts cruising speed to under a quarter in 1 s, then reverses past 54 km/h within 4 s; releasing the throttle coasts to a stop;
- positive steer yaws positively (left), the slew takes several ticks to reach full lock;
- a full-lock turn at speed keeps all four wheels down and the car upright;
- the handbrake brings rear grip to
handbrakeRearGrip, slip above the threshold, and a visibly larger rotation than steering alone; release recovers grip and ends the slide; - the ramp launches the car and it lands on its wheels; a building stops it;
- reset teleports upright to the nearest spawn and is rate limited;
- the drivetrain: a front-wheel drive keeps accelerating with the handbrake on, a rear-wheel drive does not;
- boost multiplies the engine while the meter lasts, the meter refills with the key still held while the boost waits for a new double tap, makes the SUV faster and the sedan spin its rear tyres, and does nothing unless accelerating forward;
- the engine placement loads the axle it sits over; a mid engine shifts nothing;
- the muscle car spins its rear tyres when floored and not when feathered, and the feathered car pulls away; the compact never spins off the line;
- donuts: full lock and full throttle keep the muscle sliding in a slow circle while the sedan drives a fast one;
- power oversteer out of a corner on the muscle, none on the sedan; power understeer on the compact at its cornering limit;
- a class switch keeps pose and velocity, cycles on the one-shot input with a cooldown and wraps around;
- a hard landing dulls steering and engine for a moment, a hop does not; the car drives through the tunnel, over and under the bridge, and up the garage ramp; the back of a jump ramp is a wall, not a trap.
character.test.ts pins the on-foot envelope: walking and sprinting speeds and how fast they are reached (35 km/h at most, 30 at a bystander's pace), jumping on a fresh press only, a flight that keeps its take-off velocity whatever the keys say, sliding along a parked car instead of climbing it, walking up and down the terrace stairs, walking and running over a kerb and up and down a staircase of real steps without ever leaving the ground (a 50 cm ledge blocks), sprinting across the generated town's street past a corner's traffic light and a tree's planter without a hop, getting out where it is free, and everything about seats; boarding.test.ts pins getting in and out at the doors (the door reached and faced, the speeds of stepping out, bailing out and staying in, the bus's doors, a wreck's crash). crashes.test.ts pins the collisions: the deepest overlap of cars crashing into cars and into people.
Change a number, run npm run test:unit -- packages/sim, then drive. If a test fails and the new feel is right, update the test's numbers in the same change: they document the intended envelope, they are not sacred.
Current numbers (compact)
| Parameter | Value | Feel |
|---|---|---|
mass | 1100 kg | with 11 kN of engine ≈ 0–100 km/h in 4 s |
maxSpeed | 42 m/s | 151 km/h, reached at about 90 % of it |
steerLock / high | 0.62 / 0.38 rad | tight at walking pace, calm at speed |
steerRate | 7.5 /s | full lock in 0.13 s |
frictionSlip | 4.0 | about 5 m of turning radius at 50 km/h |
handbrakeRearGrip | 0.04 | 110° of rotation in 0.75 s at 50 km/h |
driftRearGrip | 0.15 | the slide survives the release |
driftSlipThreshold | 4 m/s | below this the rear hooks up again |
wheelspinGrip | 0.15 | front grip while the fronts spin (rare) |
gripRecovery | 2.5 /s | straight again ~0.4 s after release |
centreOfMassDrop | 0.6 m | no wheelies, no rollovers |
landingStun | 0.6 s | dead for 0.6 s after a 10 m/s landing |
landingControl | 0.3 | 30 % steering and engine meanwhile |
boostFactor | 2.6 | boost spins the fronts, pushes wide |
boostSeconds | 2 s | a full meter lasts 2 s |
boostRecharge | 5 s | empty to full in 5 s, driving on or not |
maxBrakeForce | 200 | 80 km/h to a stop in about 7 m |
maxReverseSpeed | 30 m/s | 108 km/h backwards, reached in ~5 s |
angularDamping | 1.2 | spins die down instead of carrying on |
SIDE_SCRUB.slip | 1.0 | a car shoved sideways slides, never pivots |
ROLL_STEADY | 6 rad/s, 20 rad/s² | a T-bone at a coast never tips a car |
Next: tune the feel of each class by driving it; test-new-vehicles (a straight street and a row of spawns) is where the city's classes are driven and boarded (E2E-150 to E2E-153).