Skip to content

AI ​

How the NPCs chase. Everything about it lives in packages/ai (the model) and its profiles.ts (the numbers), on top of three pieces of packages/world: the road graph (road-graph.ts), the navigation grid (nav-grid.ts) and the layered navigation over it (nav-layers.ts). This page explains the model so the numbers mean something; the Levels guide explains the road block the graph is built from.

The whole feature is developed and tested on its own level, test-ai-pathfinding (FOI_LEVEL=test-ai-pathfinding; the server's default is main-riverbend): a road loop with a cross street and a one-way spur, four inner buildings, and an obstacle course in the north (a pillar field, two zigzag walls with offset gaps, a low wall, a deck with a ramp and stairs, an alley cross too narrow for a car).

The seam: NPCs write inputs ​

An NPC is an ordinary character nobody owns (owner 0), spawned by the server into the same simulation as the players. Every tick, before the simulation steps, aiSystem runs each NPC's brain in entity order and the brain ends by writing a PlayerInput, exactly what a client sends for its player: on foot throttle along +z and steer along +x, run, aim, the slot of its pockets it holds, fire, handbrake to jump. The character system does the walking, the weapon system the shooting, the physics the pushing.

Weapons are items in an NPC's pockets, as a player's are in their inventory (arms.ts, Items): a chaser that shoots is given its profile's weapon and a full stack of its ammunition when it is spawned (and a pistol with its rounds when its crew shoots out of the windows), a guard its own, every police officer a pistol whatever the stars; an unarmed one carries none. Each tick its input selects the slot holding the weapon its profile shoots; once the ammunition is spent it selects a slot holding no weapon and fights with its fists, at arm's length. Its body's pockets give the gun up to a player who searches it. Nothing in the AI moves a body, and nothing in packages/sim knows the AI exists; a seated NPC forwards its input to its car like a player would.

That keeps three things true at once. The server never trusts a position (the AI runs there only; clients see NPCs as snapshot entities like anyone else). The determinism gate covers the AI (packages/ai/src/determinism.integration.test.ts runs two worlds and compares hashSimState and hashAiState; the AI draws its randomness from its own createPrng stream, never the clock). And whatever a player can do, an NPC can do with the same numbers, so the driving model and the on-foot model stay the one source of truth.

Perception ​

A chaser sees with a cone and a ray: a target within sightRange and sightHalfAngle of its facing (its aim on foot, its car's heading seated), with nothing between its eyes and the target's centre (its car's when seated), the same ray a shot is traced with. Cover hides: a character hidden at a wall is no target. It hears the shots and blasts of the last tick within hearingRange, and a hit on it or its car names the shooter.

A profile that tracks knows where its assigned target is at all times (a test chaser dropped behind you, a police radio later); it still closes in until it has the target in its line of sight, and it never shoots through a wall. What it saw last is remembered (position and velocity) for loseSightSeconds; a target it brought down is watched from the approach distance, never shot at, until it is up again or out of sight.

Behaviour ​

A small state machine per NPC; the readout names the mode:

ModeDoingLeaves when
idleStanding at home, facing its spawn direction.Something is seen or heard → alert.
alertFacing the stimulus for reactionSeconds (shorter with the aggression).→ chase if the target is fresh in memory, else search.
chaseHeading for the target, leading its motion by 0.3 s and offset to its side by flankOffset (each chaser its own side); with it in sight, keeping approachDistance (stepping back under it if backOff), firing within fireRange.Out of sight for loseSightSeconds → search.
searchWalking to where the target was last seen or heard, then looking around a quarter turn at a time for searchSeconds.Seen again → chase; time up → return or idle (giveUp).
returnWalking home.Within 1.5 m of home → idle; something seen or heard → alert.
fleeCarjacked (pulled out at the wheel, or put out as a passenger): running from the carjacker, around what the rays find, never shooting, for fleeSeconds counted from when it is on its feet.Time up → chase, after the carjacker (an armed crew fights back).

Down (knocked over by a car, shot) the NPC is deaf to its input; dead, it comes back at its home with full health or is removed, as respawn says. A car hits every character the same way (packages/sim, car-hits.ts, after the physics step, from the speed at which the car closed on it along the contact, less what it was getting away at: someone running into a car is stopped by it, never hit): faster than 20 km/h it knocks the character over, throws it off the front and onto the bonnet, and hurts it in proportion to the closing speed over that (a crash is a hit with the driver as its owner, so the player's log shows it), and a dead one keeps lying where it fell, in its own colours; a body a car catches up with goes over it, and one lying in the road is driven over; slower, the character steps aside instead of being pushed along the ground. A chaser that survives the blow tumbles for its profile's downSeconds, then stands up and chases on; a character whose centre ends up inside a car (a pedestrian under a sliding wreck) is set down beside it where it is free (never into a wall or another car), so nobody gets stuck under a car. Nobody on foot outruns the player: every person sprints at 35 km/h at most (the chasers, the guards, the police on foot), a bystander running away at 30. Hostile NPCs are drawn orange (players are blue, remote players green) and flagged hostile in the snapshots, so the client and the specs tell a chaser from a pedestrian.

Flee (GAME_DESIGN §5.4). A player can carjack a chaser's car at its driver's door while it is stopped or crawling (packages/sim, carjack.ts): the driver is pulled out and tumbles, the crew is put out of the far side. The simulation records a carjack event; before its NPCs decide, aiSystem reads the events of the last step, and every NPC that sat in that car at its last update and is now on foot starts to flee its carjacker (startFlee; whoever is in its beat at the car's door, carjackerOf). The flee is fleeSeconds of running away (from the carjacker, or from where it stood once it is gone; it runs off with their car, so usually from the car), counted from when the NPC is on its feet, so a driver first tumbles for its downSeconds; then the NPC chases the carjacker, armed or not. The pedestrians are the simulation's (pedestrians.ts, see Pedestrians), and so is their flee: one within 15 m of a shot's muzzle, of where the shot ends or of a blast runs away from it for three seconds, then goes back to its pavement (a shopkeeper ducks inside).

Finding the way ​

Two structures, both built once per level:

  • The navigation grid (buildNavGrid): 2 m cells over the ground; every building, structure and wall lower than head height, grown by a clearance, blocks the cells it covers (nine sample points per cell, so a thin wall between two cell centres still counts); ramps and stairs are slopes (walkable, dearer), with a blocked strip behind a high edge; roads are flagged; cells next to a wall are flagged. Two grids from one function: a person's (0.5 m clearance) and a car's (1.6 m, which closes the alley cross and keeps cars off walls). On a slope each sample stands on what carries it there, so the top of a flight of stairs is no wall, and a vertical face is as tall as it is where it comes nearest (the side of a flight of stairs, not its top step).
  • The layered navigation (buildNavLayers): the person's grid with the floors over its ground. Every cell keeps its ground as node i and gains an upper node for each other surface someone can stand on there: an upper storey's floor, a deck, a bridge, a roof, a landing. An upper node is the up-facing top of a structure or a building more than a step over the ground, with 1.8 m of headroom and not inside a solid, blocked and near a wall as cells are. Each node knows its surface's height just inside its cell's four edges, and two nodes of neighbouring cells are linked when both are free and their surfaces meet within a step (0.5 m) where the cells meet. So a flight of stairs leads up its length onto the floor it reaches, never from its side; the street under a bridge stays apart from the road on it; and a table is no floor anyone gets onto. A diagonal step is taken only where it can be walked round both ways. A flight of stairs whose foot stands on a floor is a structure, a floor of its own over the floors under it (one on the ground is ground); a surface under another within headroom (a courtyard under the first steps of a flight) is no floor at a cell's edge, and a slope's samples are judged at the slope's own height. Ladders and lifts are jumps between two nodes (a ladder's foot and the floor at its top, a lift's stops one with another): the level's reach checks follow them, so a roof reached by a ladder alone is no island, but no AI climbs or rides yet; an escalator is a flight of stairs and walked as one. The car grid stays flat.
  • The road graph (buildRoadGraph): the authored road blocks as nodes (ends and junctions, snapped within a tolerance) and lanes as directed edges with their offset from the centre line, length and speed limit.

One A* (astar) serves both as search graphs: a binary heap keyed (f, h, id) so ties pop identically in two worlds, typed-array scratch allocated once, and a budget of expansions per plan (a spent budget returns the path to the node nearest the goal, and the NPC plans again next time). Grid paths are string-pulled (smoothGridPath) so a chaser cuts across open ground instead of walking cell to cell. The world grants plansPerTick plans per tick in entity order; the others wait a tick. /health reports the AI step time; the budget integration test pins the cap.

On foot (steer-foot.ts) the chaser plans over the layered navigation, from the floor it stands on to the floor of its goal (the height its target was last seen at, so a chase goes up the stairs after a player on a deck; without one, the goal's floor nearest its own height). It replans when its plan is missing, due (repathTicks, staggered by entity) or aimed more than 4 m from where the goal moved to; the path's flat stretches are pulled straight, its stairs, ramps and floors walked cell by cell (a straight line says nothing of a floor's edge or the side of a flight of stairs); follows the path a waypoint at a time (a longer lookahead running); steers around the cars and people the grid knows nothing about with three short rays; and notices when it wants to move but does not: a sidestep, then a jump if the profile jumps, then a plan around the spot that stuck it.

At the wheel ​

A chaser in a car runs the same modes; steer-vehicle.ts turns them into driving:

  • The route. A clear straight line on the car's grid (up to 60 m) is driven as it is. Otherwise, with the car near a road and the goal within 8 m of one, the roads are followed: from the node ahead on the car's stretch (or the one behind after a U-turn, when that is shorter by more than the U-turn's penalty) through the road graph to the node nearest the goal's, the lane centres as waypoints, the last leg straight; unless the stretch ahead ends in a dead end or the roads go more than twice as far as the crow flies, in which case the car's grid is searched like a walk (roads cost 1, open ground 3, cells next to walls are out of bounds). Routes are replanned on the profile's repath schedule, or at once when the goal moves more than 8 m, within the same planning budget as the walks.

  • Pure pursuit. The car steers at a point on the route 0.6 s ahead (6 m at least, 30 m at most): the steering angle follows from the bearing error and the wheelbase, scaled by the class's lock, which shrinks with speed as it does for a player.

  • Speed. The lowest of: the profile's share of the top speed; what the bend being steered allows at 7 m/s² of lateral acceleration; the speed each corner ahead on the route can be taken at, brought back along the route by a 12 m/s² braking plan; the gap law behind a target (the target's speed plus 0.8 of the gap beyond the approach distance per second, so a tail settles at its distance and a pursuit pulls up behind); and what stops the car by the end of the route. The throttle answers the speed error, the handbrake comes on in sharp bends when the profile allows it, the boost on straights.

  • The ram. Within 12 m of a car it chases, roughly aligned and fast enough, a profile that rams drives straight at it, at its rear quarter on the side it is already on when it pits, which spins the car. A ram is one blow: once its bumper touches the car (its box within 0.6 m of the other's) the driver backs off for 1.25 s and lines up again, so it never pins a car and pushes it along the road (a police car at three stars once drove a player's car off the level that way, its crew never getting out).

  • Backing round and recovery. A goal behind the car (a dead end, a U-turn against a one-way road) is reached with a reverse arc first. A car that asks for speed and gets none for three quarters of a second (a wall at the bumper counts triple) reverses out for a second with the wheels turned the other way; three such stalls within ten seconds, a wreck, a target on foot within 12 m, or a route that ends short of a target on foot (an alley, a gap in a wall the car's grid closes) make a profile that leaves cars get out and go on on foot.

  • Taking a car. On foot, a profile that enters cars whose target drives off beyond its approach distance plus 20 m walks to the nearest free car within 25 m (empty, not wrecked, not the target's) and takes the wheel.

  • The crew. A profile's crew says how many ride in the car when it is spawned in one (the driver included, clamped to the seats of the class); every passenger is a chaser of its own that rides along (ride in chaser.ts). Three tactics, each a switch of the profile: with shootsFromCar the driver fires forward and the passengers out of the windows at whoever of the targets they see; with ram (and pit) the driver rams as above; with dismounts the driver pulls up near the target with the car nearly stopped, and the whole crew gets out and fights on foot. A passenger also gets out when the driver's seat empties or the car is wrecked, if the profile leaves cars or dismounts. Getting out is the simulation's, by the car's speed (exitMode in packages/sim/src/seats.ts): the AI only chooses when to press the key. A wreck's crew (and a scared traffic driver, braking hard) press at once: they stay in while the car goes faster than 35 km/h, bail out rolling at their doors below it and step out at a crawl; any other crew (following its driver out, parked to walk on, a VIP leaving another's car) waits for the car to crawl and steps out at its doors. Nobody is ever set down in front of a moving car. Getting into a car (a chaser taking a free one, a VIP its leader's) runs to the seat's door first (boarding.ts).

Dropped from the legend's Spawn in a car, a chaser appears 30 m back along the road the player is on (in the lane that runs toward them), or on open ground behind when the player is off the roads, at the height of the ground there. The HUD line marks it car; the readout says it is seated.

Traffic ​

The ambient traffic (GAME_DESIGN §5.6) is drivers of the same kind: characters nobody owns at the wheel of the city's vehicles, drawn from a weighted mix (CLASS_MIX: compacts and sedans mostly, some SUVs, taxis and vans, a bus now and then, the odd sports car, muscle car or box truck, an occasional garbage truck; never what the police or the emergency services drive), each writing its input before the step like any NPC (traffic.ts, traffic-drive.ts, traffic-route.ts). It is developed and tested on its own level, test-traffic-junction: a loop of road with an avenue and a street crossing under four traffic lights, and four T junctions without.

  • How many, and where. The server sets the number of cars (setTraffic): FOI_TRAFFIC or the GameServer option traffic, else the game's rules.traffic (none unless a game asks; Riverbend's game asks for 40), else 32 on a main-* level played without a game and none on a test-* level. Cars appear one a tick at most, on a lane between 60 and 220 m from the nearest player (out of sight, 14 m clear of other cars and 7 m of anybody on foot, so no car is ever made around a person, clear of the junctions' boxes, never on a dirt track), and go once farther than 260 m from every player; with nobody on the server there are none. A level's spawn points of the traffic (the traffic role, and a game's places of that role) change where: cars come out there only (a car park's exit, a tunnel's mouth), on the lane nearest each point heading its way, within 220 m of a player and 14 m clear of other cars; a game's spawn areas of the traffic (zones of the kind spawn) put them on the lanes inside those circles instead.
  • The way. A car follows the road graph's lanes: along a road the same lane, at each junction a way out drawn at random and then a lane of it (never straight back; a U-turn only at a dead end); a bus or a truck (longer than 7 m) keeps straight on wherever a way out lines up, since its turns sweep the lanes beside it and the cars waiting in them. traffic-route.ts turns the lanes ahead (140 m of them, planned again when 70 m are left) into the polyline it steers along: at a bend the two lanes' ends meet halfway; at a junction the lane coming in is cut where it enters the junction's box, the lane going out where it leaves it, and a curve through where their lines cross joins the two. Every point carries the speed its bend allows (4 m/s² sideways), so a hairpin and a right turn through a junction are braked for alike. The wheel is pure pursuit of a point half a second ahead.
  • The speed. A driver commands an acceleration, never a throttle (DRIVE in traffic-drive.ts). Its free speed is the road's limit times its own cruising share (0.82 to 0.95, drawn from the traffic's random stream; a scared driver's 1.4 times the limit), approached as the Intelligent Driver Model does, at 2.5 m/s² at most (a bus or a truck at 1.5) and ever more gently as it nears it. What it slows for are targets: whatever is in front on the route (a car, a trailer, a person standing or lying) is followed by the model's interaction term (a gap of 2.5 m plus 0.9 s of speed, the closing speed weighed in at the comfortable braking of 4 m/s², so cars follow, queue and stop 2.5 m short); a bend ahead (each route point's cap) by the braking that meets its speed at its point, blended in by the square of its share of the planned 3 m/s² and exact beyond it, planned a quarter of a second early for the foot's lag; a stop (a red light, a junction given way at, a box it could not leave, a bus's stop, the route's end) as a standing car queued behind, its rear just past the line, met by the braking the last metres need so the front rests 0.4 m short. The command is bounded (never below the emergency reserve of 8 m/s²) and changes by 8 m/s³ at most, 40 into an emergency: no car bangs its throttle or stands on its brakes, and a car measured every 50 ms, as the netcode probe measures it, changes its speed by more than 5 m/s² in under half a per cent of the intervals (one in ten before), never flips from hard braking to hard acceleration within a tenth of a second (every second before, over the city), and launches at a driver's 2.5 m/s² instead of its engine's (10 for a compact, 25 for the sports car). The throttle and the brake are then whatever the class's engine and brakes need for that acceleration (pedal: the engine's force tapering with speed less the rolling brake of the undriven wheels and the air's damping, the brakes an impulse per wheel per tick, inverted from vehicle-classes.ts, not tuned), so a bus and a sedan both drive smoothly within their means; a car shoved backwards brakes to a stop first. The decision at an amber light is made once per approach and kept: stop when braking at 3.5 m/s² does it, else go through; a red that even the emergency braking could not stop for before the line is gone through too (braking would only stop the car in the box). A long vehicle's pursuit point is never nearer than 1.2 axle distances, so a bus turns its wheel gently, and a bus pulling in to its stop eases 0.8 m toward the kerb over 10 m, never nearer the kerb than 0.3 m.
  • Junctions. A junction is where three or more ways meet (findJunctions, @foi/world); its box reaches the widest road's half-width and 1.5 m around it, and its stop lines are where the box begins. A car coming up to the line notes when it arrived, from which approach and which way it goes through. It stops at the line when the box beyond could not take it (a slow car in it or just past it), when a light is red or amber with room to stop, or when someone else has the way: a car in the box whose way it would cross, or one that came up to its line first and goes across its path (where a light rules, only the cars whose light is not red). Two cars from opposite sides that both go straight on or turn right, or both turn left, go together; so do two right turns. Without a light, a car that has waited ten seconds goes anyway.
  • Traffic lights. A traffic light piece of the level (downtown/traffic-light) within 20 m of a junction, nearer to it than to any other, rules it (buildSignals): the junction's approaches fall into two groups by their axis, and the groups take turns on a fixed plan, green 12 s, amber 3 s, all red 2 s while the box clears, then the other group's green, each junction shifted by its own offset. The plan is a pure function of the level and the tick (signalColour), so the drivers on the server and the lamps on every client agree without a byte on the wire; the client lights a cap on each light's signal head in the colour of the approach whose lane passes under it. Players do not have to obey.
  • Switched by hand. A mission (its setSignal action, an app's button) switches the signalled junction nearest a place (switchSignal): green for one of its two ways (north-south or east-west) and red for the other, red both ways, or flashing amber, for so many seconds or until switched back (auto: its plan again). The drivers obey it as they obey the plan, and treat a flashing junction as one without lights (they give way). The overrides are the only state of the lights: the server sends them to every player in a signals text frame when they change (the junction, the state, the green group, the tick it ends), the lamps show them (a flashing one blinking amber), hashTraffic folds them in, and an expired one is dropped.
  • Trouble. A car held behind something that never moves (a parked car, a wreck, a player on foot) moves over to the lane beside its own when there is one going its way and it is clear; one that asks for speed and gets none backs off for a second; two cars nose to nose (one swung wide on a turn), or each held by the other (one knocked across the other's way), back off by turns; a jam inside a box is crept through. A car that stays stuck (three stalls in twenty seconds, a second on its side or roof, two seconds off its route, twenty seconds behind something that never moves, twenty seconds held at a junction's line by the others: a gridlock) is taken off the road as soon as no player can see it (55 m), and the count brings a new one elsewhere; a flipped car or a wreck in sight is left, its driver walking off.
  • Bus stops, couriers, the ice-cream van. On a level with bus stops (spots of kind bus_stop), a bus stops at each one on its kerb side instead, its front door by the queue's head (nextBusStop), and passes those across the road; a delivery scooter pulls in now and then as a bus does on a level without stops, an ice-cream van too, standing there half a minute (its hatch open). On a country road or a dirt track the cars come from RURAL_MIX (pickups, tractors, SUVs and the odd car) instead of CLASS_MIX, which has the pickup, the delivery scooter and, rarely, a limo and an ice-cream van among the town's.
  • Buses. A bus pulls in to the kerb now and then: its next stop is a distance it counts down as it drives (40 to 140 m after it appears, then every 140 to 280 m, drawn from the traffic's stream), moved on past any junction it would fall near (20 m clear of the line and of the box's exit); it brakes for it, steers 0.8 m toward the kerb in the rightmost lane, stands four to seven seconds (the readout's stop) and goes on. The car behind it does not count it as queueing: on a road with a lane beside it moves over and passes. Bus lines (the same stops, a timetable) are for later.
  • Carjacked. A player can carjack an ambient car at its driver's door while it stands (the simulation's carjack): the driver tumbles out, gets up and runs from the carjacker for four seconds, then walks off and goes once out of sight; the car is the carjacker's and goes once left empty and far from everyone.
  • Fire. A driver whose car catches fire (or is wrecked) gets out and runs from it, as from a scare; a chaser's crew and the police leave a car on fire at once, whatever their profile, and never get into one.
  • Gunfire. A shot (its muzzle or where it ends), a blast or a bullet's hit within 18 m of a car, or on it or its driver, scares the driver (a crash does not): at a crawl it bails out (six times in ten) and runs from the noise, leaving the car where it stands; moving it mostly speeds away for six seconds at 1.4 times the limit, ignoring lights and junctions and braking only for what is in front, and bails out if it is boxed in.

The readout (trafficReadout, sent with the chasers' twice a second while debug commands are on) tells every car's mode (drive, follow or queue behind a car moving or nearly stopped, red, giveway, panic, a bus's stop), speed, position and the junction it comes up to with its approach's group, and every driver on foot (flee, walk); the client keeps it in window.__poc().traffic with the lamps as drawn. hashTraffic folds the cars, the walkers and the left cars into hashAiState, and the determinism gate runs with traffic. /health reports the cars under ai.traffic.

The loop: change a number in TRAFFIC (traffic.ts), DRIVE (traffic-drive.ts) or ROUTE (traffic-route.ts), run npm run test:unit -- packages/ai (a car cruising its lane, stopping behind a parked car, a red light and a queue, giving way at a T, the spawns, a carjack, a scare, a flipped car) and npm run test:integration -- packages/ai (a dozen cars on test-traffic-junction for two minutes, the determinism with traffic, Riverbend's 40 cars, and the accelerations gate traffic-smooth.integration.test.ts: sixteen cars' speeds every 50 ms for two minutes, their 99th percentile under 4 m/s², under a tenth of a per cent over 5, no hard flip), then FOI_LEVEL=test-traffic-junction FOI_TRAFFIC=16 npm run dev:web and watch the cross. The same accelerations are a metric of the netcode probe (npm run test:netcode -- record …, then replay: the server's own accelerations per kind, no netcode in them), so a recording on any server, the live platform included (--no-health), tells how the cars drive there.

Pedestrians ​

The city's pedestrians (GAME_DESIGN §5.4 and §5.6: roles, patrol and schedule) walk its pavements. Three parts, each where it belongs:

  • The pavements (@foi/world, pavements.ts) are laid from the roads alone, so any level with streets has them: beside every paved road, on both sides, a line of nodes 2.2 m beyond its edge every 6 m, kept where a person could stand (open ground at the road's height, flat enough, off every carriageway; a node steps out or in by up to 1.8 m round a lamp or a tree), linked along each side where the way is clear. Around a junction each arm gets a crossing two metres back from its box (a zebra), and wherever two roads meet the pavement ends join round the corner. A door is the place 1.2 m in front of the middle of a building's front (the side its model faces), linked to the nearest node. Walking them is Dijkstra over the links (pavementDistances, cached per goal) and its next step (nextPavementNode). Riverbend has about 5,000 nodes (nearly half of them one walk), 361 crossings and a door at 568 of its 886 buildings, built in about a second once per level.
  • The walking (@foi/sim, pedestrians.ts) is the simulation's pedestrian system, run by the server before each step on the world's random stream, writing each pedestrian's input. A civilian strolls (1.5 m/s), a random way at each node and a rest at one in four; a commuter walks briskly by the shortest way between its start and a place 60 to 220 m off, waits two to four seconds at the kerb before each crossing and a while at either end; a jogger runs (3.4 m/s) a loop of three corners 40 to 140 m from its start; a shopkeeper stands at its door facing the street, steps out along the pavement to the next node every six to twelve seconds and goes back to it. Each role has a shirt the client draws (grey, an apron's cream, hi-vis green, a dark suit: the snapshot's cls of a character is its role). Everyone runs from gunfire as before (at 30 km/h, a bystander's pace: FLEE_PACE of a sprint), then goes back to the nearest pavement; a shopkeeper ducks inside instead (runs to its door and is inside). Knocked down, a pedestrian makes for the pavement again once up. Off the pavements (a level without roads, such as the test city) a pedestrian wanders the open cross around the centre at a civilian's stroll.
  • The crowd (@foi/ai, crowd.ts) decides who is out, the way the traffic keeps its cars: the server sets how many (setCrowd; FOI_PEDESTRIANS or the GameServer option pedestrians; a standalone server keeps 40 on a main-* level and 24 on a test-* one, a platform room 8). One a tick at most appears on a pavement node 30 to 120 m from the nearest player, never on a scrap of pavement under six nodes nor on top of somebody, with a role from ROLE_MIX (civilians 5, commuters 3, shopkeepers 2 where a door is free, joggers 1.5); one farther than 160 m from every player goes, and with nobody on the server they all do. A shopkeeper who went inside is taken off the street and its door stays empty 45 s before another takes it. hashCrowd folds the crowd into hashAiState; the determinism gate runs with one.
  • By district: a pedestrian's spot is chosen first, then its role, from the roles the level's district there suggests with their weights (LevelData.districts[].pedestrians, the generator's from the district's theme; a role it leaves out never comes there), else from ROLE_MIX, then by the hour; when none of the district's roles is out at that hour, the city's mix.
  • The city's routines (ROUTINE_MIX, ROUTINE_SHARE): about a third of the newcomers come out for a routine instead of a role of the mix, by the hour (routineWeights; a district's mix that names routines gets those): a dog_walker (twice as many mornings and evenings) strolls slower than a civilian, its dog on a lead beside it (the dogs, below); a street_sweeper (05:00 to 09:30) sweeps along at a crawl; a foot_officer walks a beat, a loop as a jogger's at a walk, waiting at the crossings, and is the police's eyes on a wanted player (police.ts' look); a smoker (07:30 to 23:00) takes a door nobody keeps, smokes there half a minute or a minute and goes in; a vendor (shop hours) stands beside a kiosk's or a market stall's counter nobody serves; a queuer (while the buses run) waits at the end of a bus stop's line, four at most a step apart, facing the road, and boards a bus that stands at the stop (walks to its front door and is inside); a sitter (07:00 to 21:00) walks from the nearest pavement to a bench nobody sits on nor makes for, sits half a minute to a minute and a half (@foi/sim's sitting.ts, the seat its own as a player's), then strolls on. A routine with no free place comes from the ordinary mix instead. Each is drawn in its own clip (walk-dog while the walker's dog is on its lead, sweep, smoke, idle-vendor, sit-bench; one queuer in three eats) and shirt, a cart before a sweeper.
  • The dogs (@foi/sim, dogs.ts; its numbers in DOGS) are NPCs of their own: characters in a dog's body (character.ts' BODIES: a capsule as wide as a person's and 0.7 m high, 25 kg, a trot at a person's walk, a run of 42 km/h, 15 hit points) with the pedestrians' last role, dog, run by the pedestrian system like everybody else: an input, nothing more. On a lead (the link is kept both ways, the dog's owner and the walker's pet, and holds only while both agree: entity ids are recycled) a dog keeps within 2.2 m of its walker at no fixed place. It ranges to a place in its walker's frame drawn anew every 1.5 to 5 s (up to 1.6 m ahead, 1.1 m behind, 0.7 to 1.25 m to either side; the other side four times in ten, going round its walker, never through it, and off the carriageway), dawdles to fall behind rather than trot back, stops to sniff four decisions in ten (at another dog within 3 m, else at the nearest lamp post, tree, hydrant, bin, bench or sign, @foi/world's scents.ts, else at the ground) for 2 to 5 s or until the lead has been taut a second and a quarter, then trots to catch up, and sits once its walker has stood 3.5 s. The walker is its pair (walkerPace): six tenths of its pace while the dog sniffs or lags, a wait on a taut lead or behind a dog nose down in its way, and a wait for a dog off its lead. A stray (three at most, or a dog whose walker is gone or dead) trots the pavements alone at 1.8 m/s, never over a crossing while there is another way, standing, sitting and sniffing on its way, and follows a person within 8 m a step behind for 5 to 12 s, as far as the kerb. Fear: gunfire or a blast within 22 m (a person runs from 15), or a hit that did not kill it, and it bolts for 2.5 s flat out, off its lead; calmed down (1.5 to 3.5 s) it goes back to a walker alive within 45 m and is on its lead again (15 s on the way at most), else it stays a stray. A car faster than 3 m/s about to pass within 0.7 m of it in the next 0.9 s makes it jump aside: a dash across the car's way, with a hop. Hurt, it is no person: shot or run over it lies dead where it fell and never comes back at a post (a slow hit tumbles it a second and a half, and it bolts once up); it is no witness, hurting it no crime and nobody's kill (wanted.ts, stealth.ts), its body nothing a guard raises the alarm over (sentry.ts); nobody searches it, knocks it out, stuns it or puts it to sleep, no gate stays open for it, and a shot meant for somebody behind it passes over it (it comes last on the line of fire); the traffic brakes for it as for anybody. It barks now and then (the bark act). The crowd (crowd.ts) brings a dog out with every dog walker and keeps the strays (one for every eight people the hour wants, where there are pavements) in a list of its own (listCrowdDogs, dogReadout): outside the number of people the server sets and the people's readout. A dog goes with its walker when that is taken off the street (one that ran off more than 20 m is freed, a stray from then on), with the distance like anybody, and a dead one once no player is within 30 m. On the wire a dog is a character whose cls is the role dog, its lead in the bytes a character had no use for (leadWire: what it does, whether it is on its lead, its walker's entity; protocol 32); the client gives its twin the dog's body and draws it (dog-view.ts: its gait by its speed, its head down to sniff, sitting, lying on its side when dead, the lead from its walker's hand to its collar).
  • By the hour (the room clock, CROWD_HOURS): all of the crowd is out while the shops are open (08:00 to 20:00), 65 per cent in the evening and the early morning, 30 per cent at night (23:00 to 05:30); while more are out than the hour wants, one a tick who stands beyond 30 m of every player goes home (taken off the street, never in front of anyone). Joggers run only in the morning (06:00 to 09:00) and the evening (17:00 to 21:00), at twice their weight; shopkeepers keep shop hours, and at closing they go in at their doors; commuters are twice as many at the rush hours, the morning's (06:30 to 10:00) walking from the pavement to a building's door on the same stretch of pavement, where they go in (work), the evening's (16:30 to 20:00) coming out of a door and walking home to a pavement a walk away, where they go in too; at other hours a commuter walks back and forth as before. The readout tells a commuter's way (work, home, about).

The readout (twice a second while debug commands are on) tells every pedestrian of the crowd with its role, its mode (walk, stand, wait, flee, return, duck, inside), where it is and where its home is (a shopkeeper's door); the client keeps it in window.__poc().crowd (and the dogs it has from the snapshots in window.__poc().dogs: each one's walker, lead, mode and pose). /health reports the crowd under ai.crowd and its dogs under ai.dogs. On Riverbend forty pedestrians cost the server 0.22 ms a tick to walk and the crowd a share of the AI's 0.86 ms, beside its forty cars (crowd.integration.test.ts prints it, measured on 0.58.0); the whole step, the simulation's 6.6 ms included, stays near 7.7 ms.

The loop: change a number in ROLE_WALK or PAUSE (packages/sim/src/pedestrians.ts), DOGS (packages/sim/src/dogs.ts), CROWD (crowd.ts) or PAVEMENT (packages/world/src/pavements.ts), run npm run test:unit -- packages/world packages/sim packages/ai (the pavements of a crossroads, each role on them, the dogs on a lead and astray, the crowd's spawns and despawns) and npm run test:integration -- packages/ai (Riverbend's crowd and its time), then npm run dev:web and get out of the car.

Roles and schedules ​

Beyond the chasers, the guards and the crowd, an NPC may have a role (GAME_DESIGN §5.4; roles.ts), as a mission's actor (npc with role, see Missions) or a level's NPC spawner (block:npc with its role, see Levels):

  • A driver sits at the wheel of a car of its class and drives a route of places over the roads (the road graph, its lanes and lights, like the traffic: traffic-route.ts steers it, at 55 per cent of the car's top speed unless it says), looping or once and then parked (arrived), or follows an actor (a car or a person) ten metres behind. A racer is a driver a mission's race sent off: it waits for the start, then drives its gates in order at the car's full speed, boosting, ignoring the lights and the junctions, braking only for what is in front, lap after lap, and counts its gates (passed) and when it finished.
  • A target goes about its rounds on foot (its patrol, its schedule) until it is threatened: a gun aimed at it from 18 m or nearer (within 0.3 rad, in sight), a shot or a blast within 22 m, a hit, a guard within 25 m on alert. It then runs to its refuge and goes in (safe: off the street, indoors).
  • A VIP waits for a player (or a member of their party) to come within 3 m, then follows them on foot (walking within 2.5 m, running beyond 6 m) and into their car by a free passenger door when they drive (its leader, riding), and out again with them.

A schedule (schedule.ts) gives any NPC a day: hours from a time of the room clock, the one last begun in force (yesterday's last before today's first): goto a place and stay, keep a post at it, walk a patrol, go home: walk to its place (a door) and vanish indoors (the simulation's setIndoors: its body off, out of the snapshots), to come back out of it at the next hour, take a break at a place, off guard, its phone out (a sentry's sight cut to 40 % of its range and 60° wide while it is calm), or sit on the free seat nearest a place (a bench's: @foi/sim's sitting.ts), its brain resting while it sits, up again when the hour is over, when it is knocked down or when the alarm goes. A plain hour lasts until the next plain one (a shift); a timed one (for minutes, recurring every so many) interrupts it and hands it back, and ends at the next plain hour. A level's NPC spawner has it as text (06:00 post 9 -12; 06:50/60+15 break 4 -10; 18:00 home 45 -35), a mission's actor as places. A guard or a civilian on alert follows its alert, not its schedule; and since a sentry of stealth watches for intruders (a player in its view turns it suspicious), a clerk in a public street is a target on a schedule, not a civilian.

Restricted zones (restricted.ts): a game's zones of the kind restricted. A player entering one that raises the alert turns the room's guards and civilians within 20 m of its edge suspicious at once, toward them; still inside four seconds on, they are alerted. One that raises the Heat is a crime, trespass (10 Heat), when a pedestrian or an officer within 30 m sees the entry. The server hands the zones over (setRestrictedZones); who stands where is hashed.

Spawn points (see the City guide): the police's units come out of the nearest police spawn point clear of cars (setPoliceSpawns), else anywhere in the police's spawn areas, else on the roads around the player as before; the traffic out of its spawn points (above); a mission's groups of enemies out of the guards' spawn points within 40 m of their place.

The readout tells every NPC with a role or a schedule (its role, where it is, whether it is indoors, its schedule's hour, a target's threatened and safe, a VIP's leader and riding, a racer's passed and finished); the client keeps it in window.__poc().city.roles. Roles, schedules and the restricted zones fold into hashAiState, and the determinism gate runs a city with them (a racer, a scheduled guard going home, a target running to its refuge, the cross switched by hand, the traffic and a crowd, the clock's pace changed halfway).

Profiles: the tuning table ​

BehaviourProfile is the one place a chaser is tuned, in the style of the vehicle classes. The presets:

PresetIn one lineApproachSightFiresIn a carDownGives up after
tailKeeps its distance, never shoots, tracks you: a cruiser on your tail.15 m60 m / 120°noalone, never shoots3 s10 s out of sight
pursuitRuns you down, shoots a pistol within 20 m, steps back when you come too near, takes a car.6 m40 m / 120°pistol, 20 malone, shoots from the car3 s8 s out of sight
aggressiveComes right up, rifle, fans out to your side, rams and spins cars; 150 hp and respawns.3 m50 m / 150°rifle, 30 mcrew of 2, shoots from the car, rams, pits2 s15 s out of sight
guardSees only its cone (the one the client draws: 12 m, 90°), does not track, falls on any hit, respawns.8 m12 m / 90°pistol, 15 malone, shoots from the car, keeps it4 s5 s, then home

The switches shootsFromCar, ram, pit, dismounts and the crew are the car tactics (no preset dismounts; the panel turns it on); downSeconds is how long a knocked-down chaser tumbles before it gets up; fleeSeconds how long a carjacked one runs before it turns on the carjacker (tail 4 s, pursuit 3 s, guard 2.5 s, aggressive 1.5 s); health and fallsOnAnyHit how tough it is.

clampProfile completes a partial profile from a base and holds every number to its range, so a tuned profile is always valid.

The loop ​

  1. Change a number in profiles.ts (or the model in chaser.ts / steer-foot.ts).
  2. npm run test:unit -- packages/ai for the fast checks, npm run test:integration -- packages/ai for the chases on the AI level (a pursuit round a building that holds its distance, one that shoots and then watches the body, a guard that loses a target and goes home, the alley cross and the zigzag walls; at the wheel: round the corner onto the south road, a tail's gap, out of the spur's dead end, a wreck left on foot, an aggressive ram, a free car taken; the determinism and budget gates).
  3. npm run dev:web and play: E to get out, open the legend's Chasers tab. Its three pages (Style, Car, Player, turned with the arrows or the page name) hold: the style chips (click one; the help text under them says how that style behaves and how its chasers react), the armed switch; the car tactics (shoots from the car, rams, gets out and fights), the crew size and the car class; and your own infinite health, so chasers shoot at you while you watch what they do, or a max health of your own (25 to 250, a player's being 100) to see how long you last. Spawn on foot drops the chaser 25 m behind you (on open ground, facing you); Spawn in a car (stay in your car for it) sends it up the road behind you with its crew: a tail keeps its 15 m, an aggressive rams, a crew shoots out of the windows. The HUD line reads chasers 1 · #45 chase 14 m, and the minimap colours its dot by mode (blue idle, yellow alert, red chase, orange search, green return, purple flee). Walk up to a parked chaser car's driver's door from the side and press E: the crew flees, then comes after you. Walk behind an inner building and wait: search, then return. Cross the zigzag walls and watch it find the gaps. Drive into one at speed and watch it tumble, get up and come on. Clear removes every chaser.

The row is a debug channel: a text frame (DebugCommand in packages/protocol) the server honours while its debug commands are on, which they are on a test-* level and with FOI_DEBUG=1. The command can tune any number or switch of the profile it names (tune: { approachDistance: 9, fire: false }), which the server clamps; the panel's switches are that, and its crew and carClass say what car comes and how full. The invincible command flags the player's character so that no damage lands on it or on the car it drives (the HUD reads health ∞); it survives a level reset. The teleport command (the View tab's Go to) puts the player, in the car they drive or on foot, on a road of the level by its name: halfway along it, in its right-hand lane, facing along it. npm run dev and npm run dev:web run the server with FOI_DEBUG=1, so the panel works on main-riverbend too. FOI_CHASERS_FOOT=3 npm run dev:server pre-spawns three near the last spawn points; FOI_CHASER_PROFILE picks their profile and FOI_CHASER_CREW the crew of those spawned in cars. While debug commands are on the server reads out every chaser twice a second (AiReadout): mode, target, distance, position, whether it drives; that is what the HUD, the minimap and the end-to-end specs read (window.__poc().ai).

Stealth ​

The sentries of stealth (GAME_DESIGN §8) are NPCs of the same kind with a brain of their own (sentry.ts, run by aiSystem instead of the chaser's): a guard or a civilian of an NPC spawner (a level's block, a mission's npc actor) on its rounds, a patrol or a post, whose awareness (awareness.ts: a sight cone and ray that cover hides, hearing of the last step's sounds by their radius, a memory of the stimulus) runs the alert phases normal, suspicious ("?"), alert ("!"), search and caution; it calls the guards near it, chases and shoots, sweeps where it lost the player, finds bodies and shakes them awake. The book of stealth (stealth.ts) keeps the security cameras watching (they spot and call the guards), the bodies reported, the room's alert and each player's counts (seen, alarms, kills) for the missions' hooks; aiSystem looks at the cameras and the kills before the NPCs decide and counts who is seen after. The sentries are left out of the chasers' readout and read out on their own (stealthReadout). It is all in the Stealth guide, with its numbers (STEALTH, SENTRY, AWARENESS, CAMERA) and its level, test-stealth; npm run test:unit -- packages/ai runs stealth.test.ts, and the determinism gate runs a compound through a whole alert cycle.

A dart or a knockout (a non-lethal hit) never makes a chaser that falls on any hit fall at once: the status it brings lays it down in its own time (status.ts in packages/sim).

The police ​

The Wanted level and the police (GAME_DESIGN §12), up to three stars for now: wanted.ts (the crimes, the Heat, the stars) and police.ts (the system), on the chasers above. The server runs policeSystem every tick before aiSystem, reading the simulation's events of the last step; the police are on in free roam and in every game unless its rules say "police": false (FOI_POLICE=0 turns them off on a server, as the end-to-end tier does everywhere but on their own level). Everything is per player, in client order, and hashed with the NPCs (hashAiState), so the determinism gate covers it.

Crimes count when somebody saw or heard them: a pedestrian or an officer within 30 m (15 m of the muzzle or of where a shot ends, for gunfire), or the victim; speeding, when an officer notices it. Only players commit crimes.

CrimeHeat
Gunfire or a blast near people (once a second: a burst is one)6
Hitting a pedestrian (a shot, a punch, a car); killing one10; 10 more
Carjacking12
Picking a car's lock (locks.ts)8
Wrecking a car with somebody else in it8
Breaking property in front of a witness: a light piece knocked loose, somebody else's car wrecked with nobody in it, a security camera shot down2 each
Speeding past the police: at the wheel over one and a half times the road's limit (and over 54 km/h), an officer noticing it (within its 60 m in the open, whichever way it faces; once every 5 s)3
Hitting a police officer; killing one15; 15 more
Shooting at a police car (once a second)6
A car blowing up, the player's fire (or a chain of it, fire.ts), heard by a witness15

Stars. The Heat (at most 60) crosses 10, 25 and 45 into one, two and three stars at once. With no officer seeing the player and no new crime for 3 s it cools by one a second, never below the stars it holds. The stars come down only by losing the police: unseen and without a crime for 10 s at one star, 15 s at two, 20 s at three, one star goes (and the Heat to its floor), and the clock starts again. Cover hides a player from the police as from any chaser.

Sight and the radio. An officer sees with the police profile's cone (60 m, 240° wide) and its ray. The moment one sees a wanted player, every officer after that player hears where they are, and the clocks of cooling and losing start over. An officer that loses the player searches where they were last seen and, after giving up, goes there again while the player is still wanted.

Units. A wanted player is sent one police car at one star, two at two and four at three (one every 1.5 s, eight in a room at most), each with its officer (two at three stars). A car comes on a lane of the roads 45 to 120 m from the player, nearest 70 m, facing toward them, clear of other cars, out of the player's sight when the roads allow and away from the player's other units, and heads for where the police think the player is. When the stars drop, the units no longer needed (the newest first) give up: blind to everyone, they drive home and are gone after 10 s, or once 60 m from every player.

StarsPolice carsOfficers eachWhat they do (policeProfile)
111Tail at 4 m; box in a car that stops (one cruiser ahead, the next behind, then the sides); never shoot; get out to arrest a player on foot or pull them out of a car that sits still
221The same, and ram and spin a moving car (PIT), boosting
342The same in faster cars (the pursuit tune), and pull up, get out and shoot their pistols; nail strips laid on the road ahead

Three stars (police.ts, nail-strips.ts). The cars after a player at three stars have the pursuit tune (@foi/sim's PURSUIT_TUNE, set per vehicle and carried by the snapshots): an interceptor's engine, a quarter more force, a fifth more top speed (a police car's reaches the sports car's) and a tenth more tyre, so they keep up with what they chase; the cars already out are fitted the moment the third star comes, the tune comes out again when it goes, and the AI drives to the tuned car's top speed. Nail strips are laid ahead of a player at three stars who drives faster than 8 m/s, one at a time and one every 8 s at most: walking the road graph from the car the way it goes, the first junction 40 to 160 m ahead (a node where three roads or more meet), the strip across the road the car comes along 5 m short of it, the whole road wide, so whichever way the player turns they cross it; with no junction in reach, the first node past 40 m (a bend); nothing while the roads part nearer than that (the player's choice). A strip comes with its unit: a police car parked at the kerb on the right of the traffic, 7 m back, and an officer on the kerb by the strip's end, facing the road, who throws it (the throw act); it unrolls from the kerb to the whole road wide in a second. The strips are the simulation's (nails.ts: entities with a pose and a length, no collider, any tyre that rolls over one at its height goes flat, the client's prediction included). The officer reels a strip in (the pickup act, over 1.2 s) once its player has come within 8 m of it and gone 14 m past, after 30 s, or when the player drops below three stars, and the unit goes once no player is within 70 m, or 20 s after the strip is in; a level reset takes strips and units away at once. The officer is the police's (hurting one is dear); the car is parked, anyone's to take, and stays if a player drives off in it.

Boxing in is a switch of the profile (boxIn): a driver of the kind whose target sits in a car that has (nearly) stopped within 35 m drives to a side of it instead of keeping its gap or ramming: ahead, behind, left, right, by its rank among those boxing the same target.

A car that sits still. No police car rams a car going slower than 3 m/s, nor a wreck: ramming a car that has stopped only shoves it down the road, out of the officers' sight. Once a police car has reached its side of a car slower than 1 m/s (or a wreck), at any star, its crew gets out. When the player has sat in a car standing still for three seconds (sittingTicks, WANTED.pullOutTicks), an officer walks to the driver's door, holding its fire, and pulls them out. A player who gets in and drives off at once gets away: a carjack by force, which an officer may do whatever the game says about carjacking players, a wreck's driver too. Pulled out, the player is knocked down by the police and so arrested (below). No friendly fire: an officer holds its fire while the first thing on its line of fire is a colleague, an officer or a police car with one in it.

Arrests. At one star, a player on foot who stands still (under 1 m/s, not hidden) within 5 m of an officer who sees them for three quarters of a second is arrested; at any star, so is a player the police knock down (a cruiser running them over, an officer's shot, an officer pulling them out of a car). The player is put back at the nearest spawn point no car or person stands on, with full health, the stars and the Heat are cleared, the units give up, and a fine is charged: $100, $150 a star and $10 a point of Heat. The server sends it with the player's Wanted level (wanted text frames: the stars, the Heat, whether the police see them, and the fine of an arrest); the client shows it under the stars and in the hits log.

Resprays. A respray in a garage (the simulation's respray event, see Vehicles) clears the player's Wanted level and its Heat (resprayClears), unless an officer sees them or saw them within the last five seconds: the police saw the car go in, and the new paint fools nobody. The units no longer needed give up as when the stars come down.

On the client. Officers are drawn navy, never orange: they are not chasers, and the server leaves them out of the chaser readout. Police cars wear their livery (black, white doors and roof) and a light bar whose two blue lamps flash in turn; on the minimap the police flash blue and red. The stars sit top centre (★★☆), flashing while an officer sees the player (blue instead of flashing with reduced motion); the HUD's health line adds wanted ★★☆ heat 27 SEEN. window.__poc() reads wanted (the stars, the Heat, seen, the arrests and the last fine) and police (the cars, those with the pursuit tune, the officers, whether the light bars are loaded, and the nail strips laid: where, how long, how far). A strip is drawn as long as the snapshot says it is: a chain of red scissor links with a steel spike at each joint (spike-strip.ts), growing across the road as it unrolls.

Missions. A mission reads the player's stars as the value { "wanted": true } and sets them with the action setWanted (0 calls the police off); see Missions.

Playing with it. The legend's Chasers tab, Player page, has a button per Wanted level (a debug command, like the chasers). test-police is the police's own level: a road loop around a block of two buildings, range dummies by the south road and a parked car further along it; npm run test:unit -- packages/ai runs wanted.test.ts, police.test.ts and nail-strips.test.ts on it, and its end-to-end folder covers the crimes, losing the police, an arrest, a player pulled out of a car that sits still, the Heat of property and of speeding, the faster cars and the nail strips of three stars.

What comes next ​

A tuning pass by driving (the presets' numbers are a starting point), chasers and guards up ladders and onto lifts (the reach checks already follow them), the car grid over decks and ramps, overtaking in the oncoming lane for the traffic, escorts and chases as mission verbs on the roles (escort, chase), bus lines on a timetable; the police at four and five stars (roadblocks, a drone), and police that keep out of safe zones. Gangs are profiles on this foundation, not new systems, as the police are, and the traffic its drivers; a mission's enemies already are (its chasers actors, spawned by the server's mission host with a preset, after the mission's player, never respawning: see Missions).