Go to file
m3ultra 0c13c82eac Pursuit, drag and derby modes; crash-damage paint; junction fixes
Three new modes, all our own code:
- Pursuit (Simpsons Hit & Run's meter): smashing in cruise fills heat, at full
  the police hunt you. Open 250 m for 6 s to escape, get pinned 4 s to be busted.
  Heat decays, so picking a fight is a choice not a timer.
- Drag (NFSU2's): the gearbox IS the event. The builder finds each level's
  longest genuinely straight run of wide road (442-628 m across the five OSM
  levels) and drops DragStart/DragEnd, so no level needs hand-authored coords.
- Derby: five opponents in the carpark, last one running. Reuses the pursuit AI,
  each hunting a different car so it's a brawl not a mob.

Crash damage now shows: total control-point travel drives paint dulling,
roughness and a scratch layer, so scuffs and crumple can never disagree.

Junction generator fixes, all found by measurement rather than inspection:
- streams[:6] truncated after street A's ways, so junctions where A yielded 3+
  usable ways shipped with ZERO cross traffic. Interleaved before capping;
  every event now has 2-4 crossing streams.
- RADIUS 170 -> 95 spread the convoy over a 340 m window, so you threaded the
  intersection through a 20 m gap. Margaret x Edward was unwinnable: 27 convoy
  cars alive, zero collisions, $0.
- Launch points now walk the street to exactly SPAWN_DIST and pick the busiest
  shared node, and _clear_spawn() unblocks a buried start. Albert x Adelaide
  went from $0 (drove 1 m into a building) to $91k.
- build_all_junctions.sh records the street pairs, which existed nowhere before.

Fixes from an adversarial review (23 confirmed findings), the worst being:
- Derby could only ever be LOST: wrecked opponents un-wrecked 2.2 s later via
  Car._recover, so "last one running" was unreachable and the kill bounty could
  be farmed to the boost cap off one revived car. They now hold their wreck.
- Drag reported WON to a beaten player: the AI's lookahead wrapped on a 2-point
  path, U-turning before the line so its distance-to-finish grew again.
- The scratch layer used BLEND_MODE_MIX, whose mask defaults to white -- it
  replaced the paint entirely rather than scuffing it. Now MUL.
- Drag on a level with no strip left the car at the origin, half-buried, unable
  to end. Cop spawns used an unflattened basis. Stale "WANTED" HUD text.

Test harness: smoke covers all seven modes, and failures now exit instead of
hanging -- a failed assert in a headless SceneTree halts the script but leaves
the tree spinning, which cost 53 minutes of wall clock to notice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 23:07:41 +10:00
assets/textures Pursuit, drag and derby modes; crash-damage paint; junction fixes 2026-07-28 23:07:41 +10:00
cars FLOORED LAZZA 1.5: John's actual first car, '82 KA Laser hatch 2026-07-28 16:16:32 +10:00
docs Engine v0: Godot 4.6 data-driven arcade racer scaffold 2026-07-27 21:03:45 +10:00
levels Pursuit, drag and derby modes; crash-damage paint; junction fixes 2026-07-28 23:07:41 +10:00
src Pursuit, drag and derby modes; crash-damage paint; junction fixes 2026-07-28 23:07:41 +10:00
tests Pursuit, drag and derby modes; crash-damage paint; junction fixes 2026-07-28 23:07:41 +10:00
tools Pursuit, drag and derby modes; crash-damage paint; junction fixes 2026-07-28 23:07:41 +10:00
.gitignore Scenery overhaul: real OSM street furniture, FLUX texture set v2, lighting pass 2026-07-28 14:13:29 +10:00
project.godot Engine v0: Godot 4.6 data-driven arcade racer scaffold 2026-07-27 21:03:45 +10:00
README.md Pursuit, drag and derby modes; crash-damage paint; junction fixes 2026-07-28 23:07:41 +10:00
shot_albert_parklands.png.import Scenery overhaul: real OSM street furniture, FLUX texture set v2, lighting pass 2026-07-28 14:13:29 +10:00
shot_bridge.png.import Crash Mode junctions: scripted convoys, Aftertouch, Crashbreaker, medals 2026-07-28 00:14:33 +10:00
shot_busted_leyland.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_casserola_dx.png.import Reversed stats pass + real-model import pipeline (CASSEROLA DX, MORRY) 2026-07-27 22:24:36 +10:00
shot_casserola_ke80.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_chaebol_90.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_chaebol_92.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_chaebol_x1.png.import Reversed stats pass + real-model import pipeline (CASSEROLA DX, MORRY) 2026-07-27 22:24:36 +10:00
shot_chaebol_x2.png.import Reversed stats pass + real-model import pipeline (CASSEROLA DX, MORRY) 2026-07-27 22:24:36 +10:00
shot_crash.png.import TURDRUNNER SLUR 500, FLOORED XD LOLTOP, DADSON 420YO repaint 2026-07-27 21:41:37 +10:00
shot_dadson_240z.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_datto_120y.png.import MINI MERK, MAXI MERK (6 wheels), HARDON SHAGGEN panel van 2026-07-27 21:48:37 +10:00
shot_deform_after.png.import Crash deformation: control-hull vertex displacement from the design PDF 2026-07-28 19:01:43 +10:00
shot_deform_before.png.import Crash deformation: control-hull vertex displacement from the design PDF 2026-07-28 19:01:43 +10:00
shot_derelica.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_floored_xd.png.import MINI MERK, MAXI MERK (6 wheels), HARDON SHAGGEN panel van 2026-07-27 21:48:37 +10:00
shot_hardon_cmere.png.import Reversed stats pass + real-model import pipeline (CASSEROLA DX, MORRY) 2026-07-27 22:24:36 +10:00
shot_hardon_effex.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_hardon_eh.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_hardon_hr.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_hardon_shaggen.png.import CHAEBOL HLYR dynasty (X1/X2/X3) + HARDON CMERE 2026-07-27 22:06:42 +10:00
shot_jct_woolies.png.import Crash Mode junctions: scripted convoys, Aftertouch, Crashbreaker, medals 2026-07-28 00:14:33 +10:00
shot_jct.png.import Crash Mode junctions: scripted convoys, Aftertouch, Crashbreaker, medals 2026-07-28 00:14:33 +10:00
shot_kangaroo_point.png.import Scenery overhaul: real OSM street furniture, FLUX texture set v2, lighting pass 2026-07-28 14:13:29 +10:00
shot_kingswood_hz.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_kp2.png.import Scenery v4: traffic signals, power lines, local FLUX fallback 2026-07-28 17:21:35 +10:00
shot_kp.png.import Scenery v4: traffic signals, power lines, local FLUX fallback 2026-07-28 17:21:35 +10:00
shot_lazza_f.png.import Scenery v3: smashable street life, Brisbane colour, Southbank landmarks 2026-07-28 16:42:14 +10:00
shot_lazza_r.png.import Scenery v3: smashable street life, Brisbane colour, Southbank landmarks 2026-07-28 16:42:14 +10:00
shot_leyland_morry.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_maxi_merk.png.import CHAEBOL HLYR dynasty (X1/X2/X3) + HARDON CMERE 2026-07-27 22:06:42 +10:00
shot_mini_merk.png.import CHAEBOL HLYR dynasty (X1/X2/X3) + HARDON CMERE 2026-07-27 22:06:42 +10:00
shot_minicab.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_morry.png.import Reversed stats pass + real-model import pipeline (CASSEROLA DX, MORRY) 2026-07-27 22:24:36 +10:00
shot_probe1.png.import Brisbane: Queen Street Mall crash-run level from OpenStreetMap 2026-07-27 23:33:51 +10:00
shot_probe2.png.import Brisbane: Queen Street Mall crash-run level from OpenStreetMap 2026-07-27 23:33:51 +10:00
shot_qsm_top.png.import Brisbane: Queen Street Mall crash-run level from OpenStreetMap 2026-07-27 23:33:51 +10:00
shot_qsm.png.import Brisbane: Queen Street Mall crash-run level from OpenStreetMap 2026-07-27 23:33:51 +10:00
shot_queen_st_mall.png.import Scenery overhaul: real OSM street furniture, FLUX texture set v2, lighting pass 2026-07-28 14:13:29 +10:00
shot_sb1.png.import Scenery v3: smashable street life, Brisbane colour, Southbank landmarks 2026-07-28 16:42:14 +10:00
shot_sb_bridge.png.import Smashable street furniture; fix levels losing every texture on rebuild 2026-07-28 18:53:05 +10:00
shot_sb_top.png.import Scenery v3: smashable street life, Brisbane colour, Southbank landmarks 2026-07-28 16:42:14 +10:00
shot_signals.png.import Scenery v4: traffic signals, power lines, local FLUX fallback 2026-07-28 17:21:35 +10:00
shot_smash.png.import Smashable street furniture; fix levels losing every texture on rebuild 2026-07-28 18:53:05 +10:00
shot_southbank.png.import Crash Mode junctions: scripted convoys, Aftertouch, Crashbreaker, medals 2026-07-28 00:14:33 +10:00
shot_story_bridge.png.import Scenery overhaul: real OSM street furniture, FLUX texture set v2, lighting pass 2026-07-28 14:13:29 +10:00
shot_turdeno.png.import Batch-import the whole shitboxes stash: 13 more real models 2026-07-27 22:47:38 +10:00
shot_turdrunner_slur.png.import MINI MERK, MAXI MERK (6 wheels), HARDON SHAGGEN panel van 2026-07-27 21:48:37 +10:00
shot_wheel.png.import Scenery v3: smashable street life, Brisbane colour, Southbank landmarks 2026-07-28 16:42:14 +10:00
shot_woolies_carpark.png.import Scenery overhaul: real OSM street furniture, FLUX texture set v2, lighting pass 2026-07-28 14:13:29 +10:00

Burnout Infinity: Shitbox

Burnout 3-style arcade racer, Aussie shitboxes, Woolies carparks. Godot 4.6 + Blender, native Apple Silicon.

Run

/Applications/Godot.app/Contents/MacOS/Godot --path .

Controls: WASD/arrows drive, Shift drift, Space boost, R reset (or retry after Crash Mode), Esc menu.

Modes

  • Cruise — free roam with traffic, plus two rivals lapping the circuit. Pull alongside one at speed and it's an Outrun (NFSU2 rules, our code): first to gap the other by 250 m wins. Straight-line distance decides when it ends — so ducking down a side street is a real escape — but path order decides who won, so reversing away from the circuit is a loss. Winning pays takedown-tier boost. R mid-duel is a forfeit.

  • Crash Mode — 60 seconds, rack up damage dollars ramming traffic and parked cars.

  • Crash Junctions — pick a "CRASH JCT" entry in the level list: launch into scripted cross-traffic convoys at a real intersection, then Aftertouch-steer your wreck (arrows) and detonate the Crashbreaker (space). Damage tally + bronze/silver/gold. Events live in levels/<id>/junctions.json, regenerated in one go by tools/build_all_junctions.sh — which is also the only record of which two streets each event is, so keep the spec list in it current.

    Medal targets come from tools/tune_junctions.gd, which plays every event headless with varied launches and sets bronze/silver/gold at the 35th/65th/88th percentile of what was actually achieved. They were previously one uniform 15/35/60k guess across every event, which measurement showed was nonsense — achievable damage ranges from a couple of thousand to nearly $100k depending on the intersection. Percentiles rather than the max, so gold is a good run and not a freak one.

    Godot --headless --path . --script tools/tune_junctions.gd -- runs=14
    

    Two traps worth knowing. It reloads the level for every run, so a full pass is slow — budget tens of minutes. And nothing else may touch Godot or the levels while it runs: a second Godot instance contends for the .godot import lock, and a level rebuild mid-flight silently feeds it half-written levels, which reads as "this event scores zero" rather than as an error.

  • Race — 3 laps vs 3 rubber-banded rivals on the level's RacePath.

  • Drag — NFSU2's event, our code: a straight-line run down the level's longest genuinely straight stretch of wide road (the builder finds it and drops DragStart/DragEnd markers, so no level needs hand-authored coordinates). The gearbox is the game — revs climb, space shifts, and shifting inside the window pays while an early shift or sitting on the limiter bogs you. One opponent alongside.

  • Derby — Destruction Derby in the Woolies carpark: five opponents, last one running wins. They use the pursuit AI, each hunting a different car so it's a brawl rather than a mob, and leaving the ring for 8 s forfeits.

  • Pursuit — not a menu mode; it happens to you in Cruise. Smashing things fills a heat meter, and at full heat the police turn up and hunt you. Open 250 m for 6 s to lose them, or get pinned for 4 s and you're busted. Heat decays if you behave, so picking a fight is a choice rather than a timer.

The boost loop is Burnout 3's: earn boost by drifting, near-missing traffic, and smashing things. Wrecking a rival near you = TAKEDOWN — refills the meter and grows it (up to 4x). Hard crashes shed body panels, crumple the bodywork and trigger Impact Time slow-mo.

Crash deformation and damage

src/deform.gd implements the control-hull model from the design PDF: a 3×3×5 lattice of control points encloses the car, an impact shoves the nearby ones inward, and every mesh vertex follows by an inverse-distance-weighted blend (W_i = 1/‖p_j c_i‖^α, α = 2).

The trick that makes it free: those weights depend only on original positions, so they're precomputed once per car and each impact is a plain weighted sum — 0.3 ms, no distance maths, no per-frame cost. Panel travel is clamped (MAX_PULL) per the PDF's bounding-box safety limits so bodywork crumples instead of folding through itself. Only the player and rivals carry a deformer; traffic and parked cars are plain bodies.

Paint damage rides the same numbers: total control-point travel drives the albedo dulling toward dirty grey, the roughness climbing, and (past 35% damage) a scratch detail layer switching on — so the scuffs and the crumple can never disagree. Meshes and materials are deep-copied per car; share either and denting one Kingswood dents every Kingswood in the level.

Cars

A car is a folder: cars/<id>/stats.tres + optional car.glb. Models are built procedurally — specs live in tools/build_cars.py, one dict per car (silhouette profile, dimensions, paint). Rebuild all GLBs with:

/Applications/Blender.app/Contents/MacOS/Blender -b -P tools/build_cars.py

Model convention (any GLB works, hand-modelled or generated):

  • Nose faces +Y in Blender (imports as Godot -Z forward).
  • Node origin 0.75 m above ground level (the controller's ride height).
  • Objects named det_* are detachable — the game rips them off on impact (doors, mirrors, bumpers, bonnet, boot).
  • Current fleet: 30 shitboxes, from the Datto 120Y to the FLOORED LAZZA 1.5 ('82 Laser hatch, rear louvres that shed slat-by-slat — John's actual first car, built from his photos). Piss-take names on purpose: real model names and badges are trademarks.

To improve a silhouette: supply a side-on photo + front three-quarter photo, tweak the profile points in the spec, rebuild, screenshot.

Stats are low-key reversed: the shitboxier the car was in real life, the better it performs here. The Excel X3 is the fastest car in the game; the muscle cars are all bark.

Downloaded models (from shitboxes/, gitignored + .gdignore'd) convert via:

/Applications/Blender.app/Contents/MacOS/Blender -b -P tools/import_shitbox.py -- "<file>" <car_id> <length_m> [yaw_deg]

Imported cars don't shed det_* panels until someone cuts their doors out in Blender.

Real-world levels from OpenStreetMap

Real street grids, our art style, clean collision, ODbL-licensed. Fetch a bbox from the OSM API, convert, build (queen_st_mall was made exactly like this):

curl -A 'BurnoutShitbox/1.0' 'https://api.openstreetmap.org/api/0.6/map?bbox=<minlon>,<minlat>,<maxlon>,<maxlat>' -o area.xml
python3 tools/osm_xml_to_json.py area.xml area.json   # see git history / osm_level_args.py
ARGS=$(python3 tools/osm_level_args.py area.json "<spawn way name>" "<loop street 1,2,3,4>")
Blender -b -P tools/build_level_osm.py -- area.json <level_id> <origin_lat> <origin_lon> $ARGS

Don't reconstruct from Street View imagery (ToS + melted collision). Use photos as reference to hand-detail hero landmarks on the OSM base.

Levels so far: woolies_carpark, queen_st_mall, southbank, albert_parklands, story_bridge (procedural steel truss along Bradfield Highway), kangaroo_point.

Area JSONs are cached in ~/.cache/burnout_osm (override with $OSM_CACHE); tools/build_all_levels.sh rebuilds every level from them.

What the builder makes of the OSM data

Beyond the street grid and building footprints, it uses the furniture OSM already carries — street lamps, benches, bins, bollards, bus stops and the whole footway network — so most of the detail is real Brisbane, not invented:

  • Buildings extrude with a glazed shopfront band at street level, a cantilevered awning over the footpath (three canvas colours, varied heights, ~70% of buildings — a whole street at one height in one colour reads as a painted stripe), a roof parapet, and a plant room on anything tall.
  • Footpaths come from the OSM footway ways, raised 14 cm with a chamfered kerb. The chamfer is deliberate: a hard vertical edge at 120 km/h reads as a crash to the jolt detector in car.gd.
  • Roads get dashed centre lines; footway=crossing becomes zebra bars laid flat on the road instead of a raised strip you'd launch off.
  • Trees are a trunk plus three offset welded spheres (figs) or radiating fronds (palms), scattered along kerbs on top of the ones OSM maps — Brisbane has far more street trees than are tagged.
  • Smashables: the builder drops marker empties and game.gd fills them at load with RigidBodies — PP_ kerbside/carpark parking spots become shuntable fleet cars, and SL_/BO_/WB_ (OSM street lamps, bollards, bins) become topple-able street furniture with a low centre of mass so a clipped lamp post goes over rather than helicoptering. All asleep until rammed, and skipped entirely in junction mode to keep those puzzles deterministic. Lamps and bollards are deliberately not baked into the level mesh — a static pole you drive through is worse than no pole.
  • Brisbane colour: red busway asphalt, jacaranda-purple canopy on ~1 in 5 street trees, Queenslander weatherboard + corrugated iron on building=house footprints, fictional shop-fascia signs above awnings and rooftop billboards in three designs (FLUX gibberish lettering on purpose — no real brands).
  • Traffic signals from highway=traffic_signals — pole, mast arm and two lantern heads, with emissive lenses so they read as lit in a shaded canyon.
  • Power poles + sagging wires down one side of residential streets, the single most Australian thing a suburban street can have.
  • Southbank landmarks (hardcoded per level id): the Wheel of Brisbane placed off its mapped ticket office, and the Grand Arbour's curling steel tendrils + bougainvillea following the actual mapped footway.
  • Queen Street Mall's winged canopies grow along pedestrian ways whose name starts with the canopy argument. Note OSM splits the mall into a 15 m stub named "Queen Street Mall" plus the 438 m pedestrian way named just "Queen Street", hence prefix matching.

Variation is seeded off OSM ids, so rebuilds are reproducible.

Textures

assets/textures/ is FLUX-generated. Regenerate the whole set with tools/gen_textures.sh or one with tools/gen_textures.sh <name>; creds are read from .env, never printed.

Cloudflare Workers AI first, then the local MODELBEAST farm as an automatic fallback (flux_local.py, operator flux_local). This is not optional nicety: Cloudflare's free tier is 10,000 neurons/day and one full set regen exhausts it, after which every call 429s. The fallback is transparent — gen_textures.sh output looks identical either way.

Two things worth knowing before you re-prompt:

  • FLUX returns a near-white blank sheet for anything described as pale ("off white metal panel" came back at 240/255 however it was worded), and a blown-out albedo renders as a featureless void in game. The spec table's last column is a target mean luma that clamps this deterministically.
  • tools/check_textures.py reports mean/contrast for the set and flags the duds. Run it after regenerating.

The builder box-projects UVs at a per-material metres-per-tile scale; facades pick glass/brick/concrete/heritage/sandstone by height, building tag and id hash.

woolies_carpark is the one level still built in GDScript rather than as a GLB (carpark.gd) — its parked shitboxes have to stay shuntable RigidBodies, which a static GLB can't carry. It picks its parked cars at random from the real fleet. Its Trolley Run junction runs convoys along the aisles at z = 15/12 and z = -21/-24, so keep those lanes clear.

Add a level

Make a folder levels/<id>/ containing level.glb (or level.tscn). Blender convention:

  • Solid objects: suffix the name with -col (Godot generates collision on import).
  • An Empty named Spawn where the car starts.
  • A curve/path named RacePath (loop) enables Race mode and traffic.
  • Export glTF Binary to levels/<id>/level.glb. It appears in the menu.

Tests

/Applications/Godot.app/Contents/MacOS/Godot --headless --path . --import
/Applications/Godot.app/Contents/MacOS/Godot --headless --path . --script tests/smoke.gd

Visual check (opens a window, saves a PNG):

/Applications/Godot.app/Contents/MacOS/Godot --path . --script tests/screenshot.gd -- car=datto_120y mode=crash frames=280 out=shot.png

Design doc

docs/BurnoutInfinityShitbox.pdf — research report on the original game. Still to come: crash deformation (control-hull vertex displacement, equations in the PDF), oncoming-lane boost, Road Rage mode, sound, real level GLBs from Blender.