Two proper venues off the real Ipswich pair, both built in code. Willowbank Dragway (levels/willowbank_dragway): a true 402 m dual-lane strip -- concrete launch pad, FLUX-rubbered lanes, crowd grandstand with canopy, red control tower, distance boards at 60ft/eighth/1000ft, checkered finish under a scoreboard gantry, sand trap and tyre wall, smashable SL_ light towers and a staging paddock of parked fleet cars. Between the lanes: a working christmas tree, six bulbs driven by game.gd through material_override emission. Drag mode grew the event around it. Every drag now stages at the tree (three ambers, green at 2.7 s); rolling >2 m early is a RED LIGHT that bogs your launch. The opposition is a six-rung ladder of preset locals (Nanna's Shopping Run through The Willowbank Widowmaker), each with its own fleet car, power and reaction time. Beating your current rung climbs the ladder and pays an escalating purse ($250-$1600, top dog repeats); crossing at all pays $50; and every car keeps a persistent quarter-mile PB worth $150 when beaten. Losing no longer ends the run early -- you still cross for your ET, with a 10 s grace if you dawdle. Ladder rung and PBs persist in the garage save (schema extended compatibly). Queensland Raceway (levels/qld_raceway): the National Circuit digitised from the official track map and scaled to its exact 3126 m lap. The road is one generated quad strip with trimesh collision; kerbs, gravel traps and tyre stacks derive automatically from centreline curvature, sponsor hoardings space themselves down the straights, and the SF straight gets the checkered line, gantry, pit wall and the mandatory red pit complex. Plus the pond. Race/cruise/rage all run on its RacePath. Trap for the diary: Godot front faces wind CLOCKWISE. The first cut of the road ribbon wound CCW and rendered invisible from above while colliding perfectly -- the smoke suite passed and the screenshot showed four cars gridded on open grass. Three new FLUX textures (crowd, dragstrip, sand; the sand prompt tripped Cloudflare's NSFW filter, of all things, and fell back to local). Smoke suite covers tree staging, the held rival, rung-0 identity, green-bulb emission, red-light innocence, PB/rung persistence and the 3126 m lap. Also swept stale shot_*.png.import debris and ignored the pattern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
353 lines
19 KiB
Markdown
353 lines
19 KiB
Markdown
# 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](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](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.
|
||
|
||
The tuner is also the event *validator*. An event that scores nothing across every
|
||
run isn't badly tuned, it's unplayable, and it prints `!!` for exactly that. Three
|
||
separate causes turned up that way, none visible by reading the generator:
|
||
convoy streams clipped so wide that you thread the intersection through a 20 m gap;
|
||
a launch point on the street centreline threading cleanly *between* the two convoy
|
||
lanes; and `int(L / headway)` flooring to one car on a short stream, leaving nine
|
||
convoy cars where the working events have twenty. Re-run it after any change to the
|
||
generator, the launch geometry, or convoy spacing — all three feed the numbers.
|
||
|
||
Read the spread it prints, not just the targets. **Cliffs Corner** and **Bridge
|
||
Approach** both show median == max at a low ceiling (~$4.7k), which means the run
|
||
reliably ends on the first thing you hit and the convoy is never reached — they're
|
||
winnable and honestly tuned, but they're scenery crashes rather than pile-ups. Both
|
||
sit on curved approaches where the launch street bends away from the intersection.
|
||
Worth hand-picking better crossings for those two.
|
||
- **Race** — 3 laps vs 3 rubber-banded rivals on the level's `RacePath`. The
|
||
proper venue is **Queensland Raceway** (`levels/qld_raceway`) — "the
|
||
Paperclip", the National Circuit digitised from the official track map and
|
||
scaled to its real 3126 m lap: three tilted parallel straights, the T2
|
||
hairpin, the inner loop, the T6 fishhook, kerbs and gravel traps derived
|
||
automatically from curvature, sponsor hoardings down the straights, the pond,
|
||
and the red pit complex. The road is one generated quad strip with trimesh
|
||
collision (wound clockwise: Godot culls CCW faces, and the first cut of the
|
||
ribbon was invisible from above while colliding perfectly).
|
||
- **Road Rage** — Burnout 3's headline mode: 90 seconds, wreck as many rivals
|
||
as you can. They recover and keep driving so the supply never dries up, and a
|
||
leash teleports strays back into play. Bronze/silver/gold at 4/8/12.
|
||
- **Oncoming** — in Cruise, Race and Road Rage, driving against the traffic
|
||
flow while actually on the road earns boost. The oldest deal in Burnout.
|
||
- **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.
|
||
|
||
The proper venue is **Willowbank Dragway** (`levels/willowbank_dragway`, built
|
||
in code like the carpark): a real 402 m with a concrete launch pad, rubbered
|
||
lanes, crowd grandstand, red control tower, sand trap and tyre wall — and a
|
||
working **christmas tree**. Every drag now stages at the tree: three ambers
|
||
half a second apart, launch on the green. Roll more than 2 m early and it's a
|
||
**RED LIGHT** — you launch bogged while the tree makes sure everyone saw.
|
||
|
||
The opposition is a **ladder** of six preset locals, weakest first: NANNA'S
|
||
SHOPPING RUN, P-PLATE PEPPER, MULLET MERV, TORQUE'N TEZZA, a SELF-DESCRIBED
|
||
BATHURST LEGEND and THE WILLOWBANK WIDOWMAKER. Each has its own car, power and
|
||
reaction time (Nanna sits at the green for most of a second; the Widowmaker
|
||
leaves with you). Beat your current rung to climb and collect the purse
|
||
($250 up to $1600, and the top dog repeats). Crossing at all pays $50, and
|
||
every car keeps a persistent **personal best ET** — beating it pays $150 and
|
||
is recorded in the garage save. Losing doesn't end the run early: you still
|
||
get to cross for your ET.
|
||
- **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.
|
||
|
||
## The Garage
|
||
|
||
Earn **bucks** playing (takedowns $150, outrun wins $400, escapes $500, derby
|
||
kills $100, medals up to $1000, plus a cut of crash-mode damage) and spend them
|
||
in the GARAGE on performance parts of deeply questionable provenance: the
|
||
K-MART MEGA WING, GO-FASTER STRIPES, the SERVO FLAME KIT, BLINGY MAGS, SERVO
|
||
NEONS, the BUNNINGS DRAINPIPE and the TURBO THRU THE BONNET. Every part gives a
|
||
real stat bump AND bolts a procedural eyesore onto the car -- progression you
|
||
can see from across the carpark. Fully kitted is roughly +35% power, +1.1 grip
|
||
and +0.55 boost.
|
||
|
||
**The Shed** is the second path: workshop tools bought once, globally --
|
||
SUPERCHEAP WHEEL RAMPS, the CASHIES CODE READER (one previous owner, screen
|
||
cracked), an ALDI SOCKET SET, a BUNNINGS AIR COMPRESSOR, the CASHIES CHAIN
|
||
HOIST (on wheels, definitely not stolen) and a SERVO ADDITIVE CRATE. Each tool
|
||
unlocks a per-car **tune-up** at labour prices: dodgy wheel alignment, check
|
||
engine delete ("the light WAS the problem"), 60 PSI in every tyre, a head skim
|
||
and lumpy cam, an octane booster habit -- and RETORQUE EVERYTHING, which is the
|
||
one thing parts can't buy: panels that hang on through bigger hits (it raises
|
||
the car's shed-a-panel and wreck thresholds). Parts buy bling; the shed buys
|
||
labour.
|
||
|
||
Purchases are per-car (tools are global) and persist in `user://garage.save`,
|
||
which also carries the drag **ladder rung** and per-car **quarter-mile PBs**.
|
||
Upgrades apply to a duplicate of the CarStats resource -- the .tres is shared
|
||
with traffic and parked spawns of the same model, and tuning your Lazza must
|
||
not secretly tune every Lazza in the city. Bolt-ons attach after the deformer
|
||
snapshots the mesh, so the wing survives every crash, as is right and proper.
|
||
|
||
## Crash deformation and damage
|
||
|
||
[src/deform.gd](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](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](tools/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](levels/woolies_carpark/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
|
||
```
|
||
|
||
Covers all seven modes (cruise, crash, junction, outrun, pursuit, drag, derby,
|
||
race) in about 30 s, printing a `.. stage` line as it enters each one.
|
||
|
||
Failures go through `_check()`, never bare `assert()`. A failed assert in a
|
||
headless `SceneTree` script halts the script but leaves the tree running, so the
|
||
process spins forever at 100% CPU instead of failing — it looks exactly like a
|
||
slow test. That cost 53 minutes of wall clock to spot once; the stage lines and
|
||
`_check()` exist so it can't happen twice.
|
||
|
||
Only one Godot instance may touch the project at a time: a second contends for
|
||
the `.godot` import lock and can kill the first mid-run.
|
||
|
||
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
|
||
```
|
||
|
||
## Sound
|
||
|
||
Every WAV in `assets/sounds/` is synthesized from maths by
|
||
[tools/gen_sounds.py](tools/gen_sounds.py) — sines, noise and envelopes, no
|
||
recordings, so the whole kit is licence-free by construction and reproducible
|
||
from its seed like everything else in this repo. (Run it through Blender: it's
|
||
only there for the bundled numpy, no bpy involved.)
|
||
|
||
Loop discipline: anything the game loops (engine, skid, boost, siren) is built
|
||
from integer-Hz components over exactly 1 s so the seam is mathematically
|
||
silent; noise beds get a crossfaded seam. Loop flags are set in code
|
||
([sfx.gd](src/sfx.gd)), not per-file import settings, so a regenerated wav never
|
||
silently loses its loop.
|
||
|
||
Wiring: every car runs a pitch-tracked engine loop (the arcade law: idle burble
|
||
at rest, screaming near top speed, a bump while boosting) plus a skid loop off
|
||
the slip value; the player adds a boost whoosh. Crashes pick small/big by jolt
|
||
strength with glass on a wreck, pursuit cops wail a two-tone siren (derby
|
||
brawlers don't), the Crashbreaker booms, the gearbox clunks or bogs, and event
|
||
results sting up or down.
|
||
|
||
If the kit ever wants sweetening beyond synthesis, use **CC0 sources only**
|
||
(kenney.nl, OpenGameArt filtered to CC0) — CC-BY means shipping attribution
|
||
forever and NC-licensed audio doesn't belong in this repo at all.
|
||
|
||
## Design doc
|
||
|
||
`docs/BurnoutInfinityShitbox.pdf` — research report on the original game. Still to come:
|
||
a DEM elevation pass (so the Story Bridge deck actually rises), hand-picked crossings
|
||
for the two scenery-crash junctions, and splitting wheel meshes out of the 13 imported
|
||
cars so they spin like the procedural fleet's do.
|