BeamNG.Earth
A 1:1 scale streamed Earth for BeamNG.drive: real elevation and satellite imagery, floating origin, LOD streaming from a companion server
Ocean Drive at dusk. Shoot for cash, outrun the heat, and spend it at Bullet Bros. Guns.
GamesJavaScript · HTMLGitHub @atlesqueOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Neon Bay '86: a low-poly 80s beach-city shooter for desktop browsers, built with Three.js
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
A low-poly 80s beach-city shooter that runs in desktop browsers. Shoot for cash, outrun the wanted level, and spend the money on better guns at Bullet Bros. Guns.Read the full README ↗A low-poly 80s beach-city shooter that runs in desktop browsers. Shoot for cash, outrun the wanted level, and spend the money on better guns at Bullet Bros. Guns.
Live: https://mbp-scrum-city.atlesque.dev (also https://mbp-scrum-city.pages.dev)
Pokémon: 35 species across 14 families, including Pikachu, the starters, Caterpie, Weedle, Oddish, Psyduck, Sandshrew, Diglett and Magnemite wander random free ground around the city, marked by green radar dots. They use flat-shaded box models with Pokédex heights in meters from the PokéAPI dataset, using one world unit per meter. Each evolved form uses its own Pokédex height. Start with five Poké Balls; approach within 3 m and press E to throw one (species-dependent catch chance, one ball per attempt). Caught teammates are protected from your bullets and explosions and do not block your shots. All Pokémon let aiming rays and bullets pass through, so they cannot intercept clicks intended for NPCs behind them. Damage from other sources reveals their health bar, and lower HP increases catch chance. Fainted wild Pokémon disappear; caught Pokémon keep their remaining HP and can be healed at the Center. Carry up to six Pokémon. Wild levels vary by species and evolutionary stage; evolved forms are rarer. Partners earn XP from NPCs you defeat together, become stronger up to level 50 and evolve at their level thresholds. Pikachu evolves with a $300 Thunder Stone and Gloom with a $300 Leaf Stone purchased at the Center. Levels, XP and evolved forms persist in your save. Original chiptune cries distinguish every species, with cues for throws, catches, escapes, attacks, recalls, fainting, healing, leveling and evolution. Nearby wild Pokémon call occasionally; these sounds follow the effects volume and sound settings.
Visit the red-roofed Pokémon Center (red ✚ on the radar and map, in a randomly chosen existing building, separate from the hospital). Press E at its door to buy five balls for $100, heal your team for free, or select a partner. B cycles healthy partners, then recalls; it can be rebound in Settings. Your selected partner follows on foot and pursues NPCs you hit at combat speed and uses its elemental move from the appropriate range, stopping when they die or move too far away. Opponents retaliate during these exchanges; fainted partners need Center healing. Partners wait while you drive, use roofs or enter the club. Balls, catches, health and selection save with your progress; older saves receive the starter balls.
Taking people down drops cash and raises your wanted level (1–5 stars).
Each star brings tougher enemies: police, police cars, SWAT, federal agents with a helicopter, then soldiers and minigun heavies.
Downed police, SWAT, feds, soldiers and heavies can also drop armor (blue) and ammo (yellow) for every gun you own that uses it. Tougher enemies drop more often; a heavy always drops both.
Some drop a weapon too: police now and then a nightstick, feds molotovs, soldiers grenades, and the Juggernaut always his minigun. Grenades (8) bounce and go off after a couple of seconds; molotovs (9) burst into a pool of fire that burns people and cooks parked cars. Weapons you pick up (street melee weapons included) are lost when you get wasted; only what you bought at the gun shop stays. Buy a picked-up weapon there to keep it.
Break line of sight with the law and the stars flash and fade one at a time.
Two gun shops (pink $ on the radar) sell an SMG, shotgun, assault rifle, minigun and rocket launcher, plus upgrades, ammo, health and armor. They close at 4+ stars.
Adventure bikes cruise the streets (white dots on the radar): the black BMW R 1300 GS 'Triple Black' and the Yamaha Ténéré 700 Rally in blue rally livery. Shoot or ram the rider off, walk up and press F to take the bike. Riderless bikes show as cyan squares. Ride into a car gently (nosing into a parked one, or catching up with one going your way) and the bike climbs up over its bonnet and roof and drops off the far side; hit one hard and you crash into it.
Every car can be driven: 80s sedans and the white Tesla Model Y. Traffic cars have someone at the wheel. Shoot them through the glass, or walk up to a slow or stopped car and press F to drag them out (that's a crime). Parked cars are free to take. Cars crash into each other: drive into one and it skids off, dented and spinning if you hit it off-centre, and can knock into the next one along.
Four more 80s cars share traffic and parking lots: the wedge-shaped Comet Turbo, hood-scooped Stallion V8, roof-racked Palm Estate, and yellow checker Neon Cab. Each can be stolen, driven, repaired and resprayed. The Comet is quick and responsive, the Stallion has a looser rear end, the Estate is slower and tougher, and the cab sits between them.
The Repair Tool (Battlefield style, $3,000 at the gun shop, T to take it out) fixes any vehicle: hold fire up close while aiming at a car, bike, e-step, the fire truck or a chopper you took, and the torch brings it back to full in about 5 seconds. A vehicle on fire is put out first; a wreck stays a wreck. Like the jetpack's fuel, its charge (the FIX gauge) runs down while it welds and fills back up once you let go. Car drivers sometimes carry one: they drop it when taken down, or leave it on the road when you pull them out of their car or scare them off it. A picked-up one is lost when you get wasted; a bought one stays.
A crash that sets a car or bike alight doesn't blow it up straight away: it burns for about 45 seconds (shooting it or a blast still sets it off sooner) and calls out the fire brigade. A fire truck races in with its siren going, pulls up short of the fire, and two firemen run their hoses out and put it out, leaving the car smoking but drivable. Take a fireman down and he drops his fire axe (a drop-only melee weapon, Q) that kills anyone in one blow, except the Juggernaut. While the truck is stopped or crawling you can climb into the cab with F and take it (stealing a fire engine is a crime, and the crew runs): it's slow and heavy, shoves cars out of its way, and goes up with a bigger bang.
Police cars and the fire truck have a siren: press H at the wheel to switch the siren and the flashing lights on or off. It keeps going after you get out.
The police chopper's pilot can be shot through the canopy (two rifle rounds, or one to the head). Its rotors wind down and it drops, but lands in one piece, on the street or on whatever roof is under it, crushing anyone it comes down on. Walk up and press F to fly it. It turns to face where you look; W/S fly forward and back, A/D sideways, Space climbs, C descends and Shift goes full speed. It has its own guns: 1 the minigun (a 250-round belt, 3 s to reload) and 2 the rockets (six in the pods, three off one after another per click, 1.6 s to reload), neither of which ever runs out of spare ammo. The searchlight follows the crosshair, down to the horizon but never above it. Shots and blasts land on the chopper, not on you. Flying it comes with a parachute: press F in the air and you jump straight out and the chute opens by itself; the empty chopper drops and, from any height, goes up when it hits the ground.
Palm trees, street lamps, beach umbrellas and the lifeguard huts on the beach can be knocked down: drive into them fast enough (umbrellas go at any speed, palms need a proper hit and slow you down) or set off an explosion nearby. A fallen lamp goes dark. The wreckage lies there a while, and the prop is back once you are well away.
Random overworld events start one at a time, followed by 20–40 seconds of quiet before waiting timers resume. Menus and interiors pause these timers too. Major disasters keep a 2–4 minute cooldown per event; smaller incidents return sooner: UFO abductions lift you 95 metres and drop you (Space opens the supplied parachute); Godzilla stomps through the city and charges a sweeping blue atomic breath; tsunamis sweep in from the ocean (get above 14 metres); a beach volcano erupts with marked lava impacts; meteor showers mark their landing zones; and tornadoes wreck vehicles and throw you into the air. Each has spatial sounds, advance warnings and a radar marker coloured by intensity. Shoot the UFO or Godzilla to drive it away. Ordinary buildings collapse into rubble and rebuild after 75 seconds once the area is clear; shops, landmarks and stair-accessible roofs remain usable. Events wait while indoors or in menus and cancel on death.
Seven more events span four intensity levels: cash rain drops twelve collectible $25 bundles, and street fireworks are harmless celebrations; burst steam mains burn and launch anyone standing on an erupting vent, while lightning storms mark their strikes three seconds ahead. At the catastrophic end, megaquakes shake and collapse multiple blocks, alien invasions send five attacking saucers under a shootable mothership, and orbital annihilation marks three enormous 60-metre blast zones before firing. Mild events take 35–70 seconds to return, moderate ones 70–120 seconds, and severe/catastrophic events keep the same 120–240 seconds. Titles and radar markers show the severity. Menus and interiors suspend all clocks; death cancels all active events.
Eight buildings around the city have a lit Roof door at street level (green squares on the radar). Press E there to take the stairs up to a fenced rooftop with a view over the city, and E at the stair hut's Exit door to come back down. VAC Gent, the Belpaire and the Herman Teirlinck have one too. A sniper rifle waits on every roof with a door, the rocket launcher on top of the Herman Teirlinck's tower, a jetpack on the Belpaire, and a parachute (a yellow paraglider) on each of the three. Like enemy drops, they are lost when you get wasted. A parachute is good for one jump; the roof has a new one a minute after you took it.
The Velvet Flamingo, a cabaret club with a pink neon sign and a red carpet, has a door on an Ocean Drive block near the spawn (a pink square on the radar). Press E at the door to go inside: a room of its own with three dancers in sequinned bikinis and knee boots working the poles on the stage, a bar, booths and tables full of regulars, a bouncer by the door and a house groove from the speakers. Everyone in there is a woman, from the dancers and the barmaid to the bouncer and the regulars. Walk up to the runway and press E to tip the stage ($20): bills flutter onto it, the performers acknowledge you, the crowd cheers, and the spotlights brighten. The DJ booth left of the stage takes $25 requests, cycling between Neon House, Midnight Disco and Havana After Dark. Tipping and requests have short cooldowns. Press E at the bar for a Flamingo Fizz ($20, +15 health). The host by the Private curtain, left of the exit, sells a private dance ($150): you sit in an armchair in the Pink Room, a small room of its own with one dancer on its pole, until E takes you back to the floor. Press E at the door to go back out. While you are inside, the city waits for you as it does while a gun shop's menu is open (traffic, the law and your stars stay as they were), and it's fists only: the bouncer turns you away at 4 stars or more.
Every flat roof can be stood on. Press Space again in the air to double jump, jump over a roof's railings or up onto them, and hop across to the next roof. Whatever stands on a roof can be climbed too, up to the Belpaire's pergola (bring the jetpack); a fall of more than about 5 m hurts. With the jetpack on, hold Space in the air to fly; let go and you drop, a little softer than without it at first but just as hard from high up, so hold it again to brake before you land. With a parachute on, jump off and press Space again in the air (once the double jump is spent, or straight away when you are already falling fast) to open it; you float down at a soft 4 m/s, steer with WASD, and the chute is spent when you land.
World map (M, or Map in the pause menu): the whole city with the radar's markers and the names of places. Click anywhere to set a waypoint; the radar points to it until you arrive.
Dying sends you to City General with a 10% hospital bill.
Guest or account. Play as a guest and your progress (cash, guns, upgrades, ammo) saves in this browser. Or press Sign in or create account on the title screen (or Sign in in the pause menu): you get an email link, no password, for an Atlesque Play account that works across every Atlesque game. Signed in, your progress saves to the account every 5 minutes, every time you get wasted, and when you close the page, so it follows you to any device. The first time you sign in, guest progress moves into the account, unless the account already has progress, which then wins.
Multiplayer (opt-in). Single player is the default. Press Multiplayer on the title screen, pick a name and Join the city to share it with up to 15 other players. Everyone in the city is one crew: you see each other walk, ride and drive, your bullets go past each other, and you share one wanted level, so the cops one of you draws come for whichever of you is nearest. The people on foot are shared too (shoot someone else's cop and the kill is yours), and the crew shows live on the radar and the world map, with names. You can ride along with whoever is driving: walk up to their car, truck, chopper (on the ground), bike or e-step and press F to take a passenger seat (four in a car, a truck or the chopper, two on a bike or a step), shoot from it, and press F again to get off. The city's traffic, the police chopper, the army tank and the UFO are the crew's too: you all see the same cars and the same chopper, and you can shoot them down, ram them or take a car out of the traffic together. The pause menu says who's in and has Leave the city; if you were in last time, the game takes you back in when it loads. There is one city for everyone, and when it's full (16) you play on alone.
Settings (pause menu or title screen): sound on/off, effects, music and city ambience volume, render quality, draw distance, animated waves, mouse sensitivity, invert look, field of view, camera shake, crowd and traffic density, km/h or mph, and the keys: a QWERTY or AZERTY preset, then any action rebound by clicking it and pressing the new key (a key already in use swaps over). On a first visit in Chrome or Edge an AZERTY keyboard is detected and picked for you. Settings save in this browser, separately from game progress.
E inside the gold depot ring. Deliver to three gold road rings within 1:50. Stop in each ring in the original vehicle with no wanted stars. The job panel shows the destination, distance and time; its gold radar marker remains separate from your manual waypoint. Return to the depot and press E to cancel. Leaving your ride lets the clock run; death, fire, a wreck or a missed deadline ends the job. Successful runs pay $450 plus time and vehicle-condition bonuses. Repairing mid-run does not erase damage penalties. Routes rotate on subsequent runs.E for a $300 repair and respray. It clears up to three wanted stars after you break line of sight and the escape timer has advanced at least three seconds. Emergency vehicles, burning cars, wrecks and four-star pursuits are refused. Fresh paint has a 12-second cooldown. F still exits the car in either service bay.These are the QWERTY defaults; the AZERTY preset uses ZQSD to move, A for melee and M (right of L) for the world map, and every key except the mouse, 1–9 and Esc can be changed under Settings → Controls. The arrow keys always move too.
| Key | Action |
|---|---|
| WASD | Move |
| Mouse | Look and aim (click to capture the mouse) |
| Left click / right click | Shoot (or punch and swing a melee weapon; every third punch is a kick) / zoom aim (with the sniper rifle, right click steps the scope through 2.25x, 9x and back out) |
| Shift / Space | Sprint / jump (again in the air to double jump, or to open a parachute; hold it to fly with the jetpack) |
| E | Catch a nearby wild Pokémon, enter a Pokémon Center or gun shop, take the stairs to a roof and back down, go into the club and back out, order at its bar |
| F | Get on or off a motorcycle, get in or out of a car or a downed police chopper, pull a driver out of their car (in the air, jump out of the chopper under a parachute) |
| On a bike: W / S, A / D, Shift, Space | Throttle / brake, lean, boost, rear brake. You can still shoot. |
| On a bike: hold C with W | Wheelie. Off the gas or on the rear brake the front comes down; boost while up and it climbs until it loops over and throws you off. |
| In a car: W / S, A / D, Shift, Space | Gas / brake, steer, boost, handbrake. Steer with the handbrake to drift; hold the gas to keep the slide going, countersteer to straighten out. |
| In the chopper: mouse, W / S, A / D, Space / C, Shift | Turn to where you look, forward / back, sideways, climb / descend, full speed. 1 minigun, 2 rockets (three per click). |
| 1–0, mouse wheel | Switch weapons (8 grenades, 9 molotovs, 0 the laser rifle, once you've picked them up) |
| Hold Q | Weapon wheel: the game slows down and the mouse points at a weapon. Melee weapons share the top slice and grenades and molotovs share another; point past one to pick from its outer ring, or scroll through it. Let go of Q (or click) to take it, right click to cancel. |
| Tap Q | Put the guns away: fists, then each melee weapon you own |
| T | Take out the repair tool, once you have one (hold left click on a vehicle up close to fix it) |
| R | Reload |
| H | Siren and flashing lights on / off (police car, fire truck) |
| N | Toggle the radio (Neon FM builds up with your wanted level: synths only at zero stars, drums from one star, more layers up to five) |
public/index.html holds the page, styles and HUD markup, and loads the game as native ES modules from public/js/ (no build step). Three.js (r186) loads from jsDelivr as an ES module in js/three.js, which sets it as the global THREE, fonts from Google Fonts, and sound is synthesized with the Web Audio API, apart from the gunshots, bullet impacts, gun reloads and the city's ambience (beds, surf, gulls, fountains), which are CC0 recordings in public/sfx/ (credited in public/sfx/CREDITS.md).
public/js/
main.js boot and the frame loop: updates every entity in the registry
core/ state, input, key bindings (keymap, controls), audio, save (versioned), event bus, helpers
entities/ the registry every person, vehicle, pickup and shop lives in
vehicles/ the shared vehicle layer: vehicle.js, kinds/ (bike, step, car, truck, heli), models/, traffic, helicopter
npcs/ NPC_TYPES, behaviours (wander, hunt, ride) and the Npc entity
pokemon/ blocky models, catch/team save rules, wild encounters and follower combat AI
shops/ SHOP_TYPES, item types and the shop entity and menu
game/ player, follow camera, interaction prompts, rewards, wanted level, population, pickups
world/ city layout, collision, radar, landmarks and .glb models
render/ ui/ characters/ combat/ data/
Everything in the world is an entity in entities/registry.js. Entities opt into traits (update, raycast, onShot, blast, pushOut, blip, interaction, shouldDespawn, dispose), so shooting, explosions, collisions, the radar and the E/F prompt work for anything new without touching those systems. Systems talk through core/events.js (npc:killed, vehicle:wrecked, shop:purchase, …); game/rewards.js turns those into cash, drops and heat.
Accounts live in a separate service, atlesque/mbp-games: the Atlesque Play Worker (sign-in, saved progress, admin page) at https://play.atlesque.dev (its old name, accounts.atlesque.dev, still answers the API and the room). Its README covers the API and setup. With it checked out next to this repo, node tests/account-shots.mjs <folder> plays the account flow against a local copy and saves screenshots. The game's side is core/account.js (sign-in, sync) and ui/account.js (the title and pause menu lines).
Multiplayer runs on the same service: one Durable Object room at wss://play.atlesque.dev/mp/scrum-city that relays messages between up to 16 players and hibernates (costs nothing) while it's empty (src/room.js there). Each game is the authority on its own player: net/net.js is the connection, net/protocol.js what goes over it (a state a few times a second, shots, blasts, the people near another player, the crew's heat), net/sync.js the game side (sending, taking hits, blowing up others' rockets here too, one wanted level), net/remote.js the other players as kind: 'remote' entities, net/ghosts.js the people other games run as kind: 'ghost' entities, game/crew.js who the law hunts, net/ride.js riding along in a crewmate's vehicle (seats per kind in vehicles/seats.js), and ui/multiplayer.js the menus. Each game runs the people it spawned; a bullet on someone else's goes to their game. The traffic, the chopper, the tank and the UFO are run by one game for the whole crew, the host (the one in the room longest who is out in a running city, handed on when they leave or tab away): net/world.js sends them out and draws the host's in everyone else's game as puppets (the real entity with the host's word for a brain), and net/world-protocol.js is the pure part (who hosts, what goes over the wire). Hits, rams and carjackings on a puppet go to the host; cash for a kill comes back to whoever made it. It's all client side, so the room needs no deploy for it. node tests/mp-world-shots.mjs <folder> plays it through in two browsers. With mbp-games checked out next to this repo, node tests/mp-shots.mjs <folder> plays two browsers against a local copy and saves screenshots.
Alongside the code in public/: the favicon set, site.webmanifest, the share image og-image.jpg, robots.txt and sitemap.xml.
A new vehicle. Add a file to public/js/vehicles/models/ whose default export describes it (id, kind, name, short, hp, engine, optional traffic: { weight, speed }, electric: true for an EV (a hum below 30 km/h, then only road noise, instead of an engine note), siren: true for a siren and flashing lights the player can switch with H (the mesh's lr/lb), hopOff: true to make getting off at speed a plain jump off instead of the bail-out roll and its damage (the regular e-steps), and the mesh or geometry its kind expects), then list it in models/index.js. kind: 'bike', 'step' (a stand-up e-step, see models/estep.js), 'car' or 'truck' gives it physics, seating, camera and crash rules; a model can override handling. A traffic weight above 0 puts it on the roads (bikes and steps get a rider each from the bikers and steppers rows in game/population.js; steps keep to the kerb, their kind's lane). Wheels spin when the mesh lists them as wheels (one object per wheel, origin on the axle, built with wheelAt from vehicles/wheels.js); carkit cars and bikes get this for free. A bigger car-like body is a new kind made with carKind(body, extra) from kinds/car.js (see kinds/truck.js); any other new kind of vehicle (a boat) is a new file in vehicles/kinds/ implementing the hooks documented at the top of kinds/bike.js. The repair tool fixes every vehicle made this way; anything else that should take it (like the fire brigade's truck) needs a health() and a repair(dt), documented at the top of game/repair.js.
A detailed car. The blue Tesla Model Y, the Mercedes EQA 250 and the BMW 530e are drawn with the car kit in models/carkit.js: in real metres, as side outlines (body, glass, roof) pushed across the car's width with rounded edges, plus trims and lamps laid onto the panels, all bent by one warp (narrower nose and tail, sides leaning in towards the roof). Lamps use an unlit material, so they glow at night. Each model builds its geometry once and every car of that model shares it. models/bmw5.js is a worked example. Preview a car at /dev/cars.html?car=<id> (drag to orbit; &night, &neutral for plain white light) and render the previews with node tests/car-shots.mjs. Keep a car to about 15–18k triangles.
A new NPC. Add an entry to NPC_TYPES in npcs/types.js (faction, behaviour, hp, look, cash, heat, drops, weaponDrops, leaveDrops for a driver pulled out of their vehicle, and weapon stats if armed). Spawn it with spawnNpc(id, x, z), through a wanted level's mix in data/wanted.js, or with a row in POPULATION in game/population.js. New behaviours go in npcs/behaviours.js.
A new melee weapon. Add a melee({ ... }) entry to WEAPONS in data/weapons.js with its damage, rate (seconds between swings), reach, arc, knock, hits and anim (punch, swing, chop, stab or saw, the moves in characters/swing.js); blade cuts, auto keeps cutting while the button is held. Give it a model in characters/melee-models.js (grip at the origin, running down -Y, tipped forward by tilt) and an icon in drawWeaponIcon (ui/hud.js). It shows up in the gun shop on its own; list it in STREET_MELEE in main.js to lie around the city too. Blows land in combat/melee.js and use the same hit zones as bullets; people reel or go down (meleeHit in npcs/npc.js), and civilians with fightBack may punch back (the brawl behaviour).
A new shop. Add an entry to SHOP_TYPES in shops/types.js (name, marker, storefront, catalogue) and place it in SHOP_SITES in world/city.js. New kinds of goods are an entry in ITEM_TYPES in shops/items.js with render and act.
More rooftop doors. ROOF_COUNT in world/rooftops.js sets how many buildings get one. They are chosen from the flat-roofed city buildings after the city is built (the first next to the spawn, then each as far from the others as it can be), without the seeded random, so the rest of the layout doesn't move. makeRoof lays out the door, railings, stair hut and air-con units; the player's P.roof says which roof they are on, and game/rooftop.js holds the door entity. A landmark's door roof is set in ROOFTOPS in world/landmarks.js (which solid's top, where the door and hut go, what waits up there). SURFACES lists every top that can be stood on, door or not (surfaceAt finds the one under the feet, collideRoofs keeps the feet out of huts and railings); add a flat top to it through flatRoofs. node tests/roof-shots.mjs <folder> takes screenshots of a door and its roof by day and night, node tests/rail-shots.mjs <folder> of standing on a railing and on the Belpaire's pergola.
The club. world/club.js lays it out: which building it moves into (pickClubSite, the nearest deco building without a roof door, chosen after the city is built so nothing else moves), its front, and the room in its own metres (ROOM, STAGE, POLES, BAR, BOOTHS, TABLES, SEATS) with the boxes that stop the feet and the camera (clubLayout); routineAt is the dancers' pole routine. game/club.js builds the room the first time you go in, in a THREE.Scene of its own that main.js renders instead of the city while P.club is set, and runs the people in it. The Pink Room is laid out by VIP and privateLayout and built by buildPinkRoom; the club's people are made with makeWoman there. A new piece of furniture is a box in build() there and, if it should stop the player, a block in clubLayout.
A new breakable prop. Build it inside prop(kind, x, z, [builders], cut => { ...; return collider }) from world/props.js (see palm, lamp and the beach props in world/city.js) and give the kind a line in PROPS there: whether it topples over or bursts into the pieces cut() separates, the closing speed a vehicle needs to break it, how much speed it takes off the vehicle, and how much of a blast's radius reaches it.
A new landmark. Add a model file to world/landmark-models/ and list it in LANDMARK_MODELS (it builds with the small kit in kit.js, in block-local metres with the front facing north, and marks its solid volumes with k.solid() and what stands on its roofs with k.roofPart(), so the player walks round it and can stand on it), then place it in LANDMARK_SITES in world/landmarks.js with the block it takes and the street it faces, and give it a name sign in SIGNS there. Preview it at /dev/landmarks.html?b=<type>. The three Flemish government buildings (VAC Gent, the Herman Teirlinck and the Belpaire building) are worked examples. A site can also point at a .glb file instead (see world/models.js).
A building made in another tool. Export it as glTF binary (.glb) into public/models/ and give its site a model: { block: [2, 3], type: 'vac', face: 'e', model: 'vac.glb' }. The game loads it before building the city (world/models.js, Three's GLTFLoader from the same CDN build) and draws it in place of the builder; if the file is missing or broken the site falls back to its builder (or a plain block when type is left out). For it to drop in cleanly:
face turns the front to the street.Windows_lit). Every emissive material glows faintly by day and fully at night, like the other buildings.collider… (they are not drawn) for what the player bumps into; without one the whole model's bounding box is used. Boxes are axis aligned, so an L-shaped building wants one box per wing.public/models/test-building.glb is a worked example, written by node tests/make-test-model.mjs.
A reload for a new gun. Add a line to RELOADS in data/reloads.js: the move (pistol, mag, pump, bolt, box or tube, keyframed in characters/reload.js) and a sound file in public/sfx/ (mono MP3, CC0 or your own, credited in public/sfx/CREDITS.md). A unit test fails until every gun with a magazine has one. A new move is a new list of keyframes in RELOAD_ANIMS.
A gunshot for a new gun. Add a line to SHOTS in data/shots.js with how many takes it has and how loud it is, and put the takes in public/sfx/ as shot-<id>-1.mp3, shot-<id>-2.mp3, ... (mono MP3 starting right on the shot, CC0 or your own, credited in public/sfx/CREDITS.md). Each shot plays a random take a touch faster or slower. A unit test fails until every gun has one.
A bullet impact surface. Bullets sound like what they land on: IMPACTS in data/impacts.js lists each surface (brick, sand, wood, water, metal, flesh) with its takes, volume and puff colour, as impact-<surface>-1.mp3, ... in public/sfx/. combat/surface.js says what each hit is made of: buildings brick, the street brick, the beach and parks sand, the sea water, palms and huts wood (PROP_SURFACE), people flesh, vehicles metal (an entity can set its own surface). Bullets go through castBullet and bulletImpact in combat/combat.js, which also stop them at palm trunks, lamp posts and huts.
A new setting. Add a row to SETTINGS in core/settings.js (a toggle, range or choice, on the Sound, Graphics or Gameplay tab), then read settings.<id> where it matters or apply it in apply() in ui/settings.js. The Settings screen, saving and validation pick it up on their own.
A new key. Add an action to ACTIONS and both presets in core/keymap.js, then check held('<id>') (in core/controls.js) every frame or actionFor(e.code) === '<id>' in the keydown handler in core/input.js. Use kb('<id>') for the key's name in prompts and tips so they follow the player's bindings. The Controls tab lists and rebinds it on its own.
A sound from something in the world. Pass where it comes from: Sound.shot(kind, vol, at(entity, height)) and the other one-shots take a world point (at, beside in core/spatial.js), and looping sources go through Sound.loops(kind, sources) once a frame. Each plays through its own HRTF panner relative to a listener at the player's head facing the camera, with inverse-distance falloff and distance dulling by the profile in HEAR. Sounds with no source (UI, the player's own gun) take no point. Placed sounds also send a share to the street's echo, set a few times a second from the walls round the player (world/acoustics.js).
An engine sound for a new vehicle. A vehicle sounds like its kind's engineVoice (a bike's twin, a car's four, a truck's diesel) unless its model sets engine.voice (the Model Y's whine); electric: true hums instead. The voices are synthesized in makeEngine in core/audio.js.
A sound for a place in the city. game/ambience.js keeps the city's own sounds: the day and night beds, the wind on the roofs, the surf and gulls, and SPOTS, the places heard a few steps away (park fountains, clubs and bars by their sign, arcades, the beach huts' radios, neon buzz at night). A new kind of place is a row in SPOTS with a recording from data/ambience.js or a groove drawn in GROOVES in core/audio.js, and a line in placeSpots saying where they stand.
Saves. When the save shape changes, bump SAVE_VERSION in core/save.js and add a migration from the previous version.
npm test # unit tests (Vitest): rules, shop items, save migration, registry cross-checks
node tests/club-cars-smoke.mjs # club tipping, DJ requests and all four new cars
node tests/city-services-smoke.mjs # bounded courier/garage gameplay check (single browser)
npm run test:smoke # plays the game in headless Chromium: walk, drive, ride, shoot, carjack, shop, go up to a roof, a 5-star chase
npm run test:smoke -- tank reload # only the smoke steps whose names contain those words
npm run shots -- shots/roof --at roof:0 --time day,night --cam game,side # screenshots for a PR (options at the top of tests/shots.mjs)
The registry tests catch a typo in a new vehicle, NPC or shop (an unknown kind, behaviour, weapon or item type). Opening the game with ?debug exposes its state as window.__neonbay. The smoke test splits its steps over two browsers at once and resets the player before every step, so one broken step fails alone; SMOKE_ONLY="<step name>|<step name>" node tests/smoke.mjs reruns exact steps in one browser. node tests/camera-shots.mjs screenshots the follow camera in a narrow alley, on foot and in a car, and prints how far it stays from the walls. CI runs the unit tests and the smoke test on every pull request and every push to main. Under ?debug the world catches up with the clock between the slow software-rendered frames, so the game runs at real speed in headless Chromium (add &slowclock, or SLOW_CLOCK=1 for the test scripts, to let it slow down with the frames instead). To run the smoke test locally, run npx playwright install chromium once first.
With the matching mbp-games backend deployed, the server schedules multiplayer world events for the whole room, with shared locations, attack patterns, boss health and UFO victims. Players joining mid-event catch up to the same timeline. Cash rain collects automatically on foot or in a vehicle, and each bundle pays only one player. Single-player events and clients connected to an older backend use the local scheduler: one automatic event at a time, followed by a 20–40 second recovery interval.
npm install
npm run dev # local server via wrangler pages dev
Deploys run automatically through the Cloudflare Pages GitHub integration (project mbp-scrum-city):
main deploys to production.There is no build step. Pages serves the public/ directory as is.
Because file URLs never change, public/_headers sends Cache-Control: no-cache so browsers revalidate every file on each load and pick up a new deploy straight away (unchanged files come back as a small 304).
Every change merged into main is one version: the 112th is v1.112. After each push to main the Changelog workflow
(.github/workflows/changelog.yml) runs scripts/changelog.mjs, which reads main's history (the PR title from each merge
or squash commit) and writes public/changelog.json and CHANGELOG.md, then commits them back as Changelog: v1.N.
That commit deploys like any other. The title screen and the settings footer show the version in small text, and the
settings "What's new" tab lists the changes. Don't edit either file by hand; npm run changelog rebuilds them locally.
| B |
| Switch Pokémon partner; after the last healthy partner, recall |
| M | World map: the whole city, north up, with everything the radar shows plus the names of the hospital, landmarks, gun shops and the club. The city waits while it is open. Drag or WASD to pan, scroll or + / − to zoom, click to set a waypoint (a purple diamond on the radar until you get there), click it again, right click or Backspace to clear it. M or Esc closes it; the pause menu has a Map button too. |
| Esc / P | Pause |