Street lamps, bollards and bins are no longer baked scenery. The builder emits SL_/BO_/WB_ markers off the OSM nodes and game.gd spawns them as RigidBodies with a low centre of mass, so clipping a lamp at 100 topples it instead of driving through a painted pole. ~206 physics bodies per level (58 parked cars, 60 lamps, 88 bollards/bins), all asleep until hit. Verified by ramming one: 23.6 deg tilt, launched at 18 m/s. Fixes a self-inflicted trap from the last commit: level_*.jpg are Godot's extraction of the GLB's embedded textures, NOT Blender output. build_all cleared them to avoid orphans but left level.glb.import, so Godot's cache decided nothing had changed and never re-extracted -- queen_st_mall shipped with 0 of its 31 textures, loading fine but rendering untextured. The .import now goes with them. Also confirmed the Wheel of Brisbane pivot really does turn (0.086 rad over 120 frames, 0.000 m centre drift) -- it was previously untested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
190 lines
9.4 KiB
Markdown
190 lines
9.4 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`; generate more from real intersections with
|
|
`tools/osm_junction.py <area.json> <out.json> <lat0> <lon0> "Name|Street A|Street B"`.
|
|
- **Race** — 3 laps vs 3 rubber-banded rivals on the level's `RacePath`.
|
|
|
|
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 and trigger Impact Time slow-mo.
|
|
|
|
## 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
|
|
```
|
|
|
|
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.
|