From eb7c189c9f2aca361ec92b727bec685631cf2046 Mon Sep 17 00:00:00 2001 From: type-two Date: Mon, 27 Jul 2026 21:29:42 +1000 Subject: [PATCH] Gameplay systems + procedural Blender car fleet Modes: Cruise / Crash Mode (60s damage dollars) / Race (3 laps, rubber-banded rivals). Burnout loop: boost earned by drift, near miss, smash; takedowns refill and grow the meter to 4x; hard crashes shed det_* panels and trigger Impact Time slow-mo. Traffic follows the level RacePath frozen-kinematic and goes dynamic when whacked. tools/build_cars.py builds six Aussie shitbox GLBs (profile extrusion + detachable doors/mirrors/bumpers/bonnet/boot). Smoke test covers all three modes headless. Co-Authored-By: Claude Fable 5 --- .gitignore | 1 + README.md | 64 +++++---- cars/datto_120y/car.glb | Bin 0 -> 33436 bytes cars/datto_120y/car.glb.import | 42 ++++++ cars/datto_120y/stats.tres | 13 ++ cars/excel_x3/car.glb | Bin 0 -> 32016 bytes cars/excel_x3/car.glb.import | 42 ++++++ cars/excel_x3/stats.tres | 13 ++ cars/gemmy_tx/car.glb | Bin 0 -> 33424 bytes cars/gemmy_tx/car.glb.import | 42 ++++++ cars/gemmy_tx/stats.tres | 6 +- cars/kingswood_hq/car.glb | Bin 0 -> 33464 bytes cars/kingswood_hq/car.glb.import | 42 ++++++ cars/kingswood_hq/stats.tres | 13 ++ cars/vl_terbo/car.glb | Bin 0 -> 33156 bytes cars/vl_terbo/car.glb.import | 42 ++++++ cars/vl_terbo/stats.tres | 8 +- cars/xf_falcon/car.glb | Bin 0 -> 33380 bytes cars/xf_falcon/car.glb.import | 42 ++++++ cars/xf_falcon/stats.tres | 13 ++ levels/woolies_carpark/carpark.gd | 15 +++ screenshot.png.import | 40 ++++++ src/car.gd | 124 +++++++++++++++-- src/car_stats.gd | 2 + src/game.gd | 184 ++++++++++++++++++++++++- src/main.gd | 9 +- src/main.tscn | 3 + src/rival.gd | 52 +++++++ src/rival.gd.uid | 1 + src/rival.tscn | 8 ++ src/traffic.gd | 73 ++++++++++ src/traffic.gd.uid | 1 + tests/screenshot.gd | 20 ++- tests/screenshot.gd.uid | 1 + tests/smoke.gd | 57 +++++++- tools/build_cars.py | 217 ++++++++++++++++++++++++++++++ 36 files changed, 1133 insertions(+), 57 deletions(-) create mode 100644 cars/datto_120y/car.glb create mode 100644 cars/datto_120y/car.glb.import create mode 100644 cars/datto_120y/stats.tres create mode 100644 cars/excel_x3/car.glb create mode 100644 cars/excel_x3/car.glb.import create mode 100644 cars/excel_x3/stats.tres create mode 100644 cars/gemmy_tx/car.glb create mode 100644 cars/gemmy_tx/car.glb.import create mode 100644 cars/kingswood_hq/car.glb create mode 100644 cars/kingswood_hq/car.glb.import create mode 100644 cars/kingswood_hq/stats.tres create mode 100644 cars/vl_terbo/car.glb create mode 100644 cars/vl_terbo/car.glb.import create mode 100644 cars/xf_falcon/car.glb create mode 100644 cars/xf_falcon/car.glb.import create mode 100644 cars/xf_falcon/stats.tres create mode 100644 screenshot.png.import create mode 100644 src/rival.gd create mode 100644 src/rival.gd.uid create mode 100644 src/rival.tscn create mode 100644 src/traffic.gd create mode 100644 src/traffic.gd.uid create mode 100644 tests/screenshot.gd.uid create mode 100644 tools/build_cars.py diff --git a/.gitignore b/.gitignore index 628385f..eb31a5d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .DS_Store *.blend1 screenshot.png +shot_*.png diff --git a/README.md b/README.md index 10b5e79..c6c0e7f 100644 --- a/README.md +++ b/README.md @@ -8,37 +8,49 @@ Burnout 3-style arcade racer, Aussie shitboxes, Woolies carparks. Godot 4.6 + Bl /Applications/Godot.app/Contents/MacOS/Godot --path . ``` -Controls: WASD/arrows drive, Shift drift, Space boost (earn it by drifting), R reset, Esc menu. +Controls: WASD/arrows drive, Shift drift, Space boost, R reset (or retry after Crash Mode), Esc menu. -## Add a car +## Modes -Make a folder `cars//` containing `stats.tres`: +- **Cruise** — free roam with traffic. +- **Crash Mode** — 60 seconds, rack up damage dollars ramming traffic and parked cars. +- **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//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: ``` -[gd_resource type="Resource" script_class="CarStats" load_steps=2 format=3] - -[ext_resource type="Script" path="res://src/car_stats.gd" id="1"] - -[resource] -script = ExtResource("1") -display_name = "Datto 120Y" -model_path = "res://cars/datto_120y/car.glb" -engine_power = 12.0 -mass = 900.0 -grip = 6.5 +/Applications/Blender.app/Contents/MacOS/Blender -b -P tools/build_cars.py ``` -Model is optional — no `car.glb` means you drive the placeholder box. Export the GLB from -Blender with the car facing **-Y** (Blender forward), roughly 1.8 x 1.4 x 4.2 m, origin at the -centre, wheels-bottom around z = -0.5. +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: Datto 120Y, Gemmy TX, Kingswood HQ, VL Terbo, XF Fungoon, Excel Hilton. + 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. Kerb weight and power figures go in +stats.tres if you want realism; otherwise vibes. ## Add a level Make a folder `levels//` containing `level.glb` (or `level.tscn`). Blender convention: -- Objects that should be solid: suffix the object name with `-col` (Godot generates collision on import). -- Add an Empty named `Spawn` where the car starts (its -Y/forward axis = drive direction). -- Export scene as glTF Binary to `levels//level.glb`. Done — it appears in the menu. +- 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//level.glb`. It appears in the menu. ## Tests @@ -47,8 +59,14 @@ Make a folder `levels//` containing `level.glb` (or `level.tscn`). Blender c /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. The build order the -report implies: 1) handling + boost loop (done, tune forever), 2) crash/damage + takedowns, -3) traffic, 4) Crash Mode in the carpark, 5) race modes + AI. +`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. diff --git a/cars/datto_120y/car.glb b/cars/datto_120y/car.glb new file mode 100644 index 0000000000000000000000000000000000000000..fdc0e2016ca45bd189d2456a724faa24d998f8c5 GIT binary patch literal 33436 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 literal 0 HcmV?d00001 diff --git a/cars/datto_120y/car.glb.import b/cars/datto_120y/car.glb.import new file mode 100644 index 0000000..02a2488 --- /dev/null +++ b/cars/datto_120y/car.glb.import @@ -0,0 +1,42 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://ce7ggjcjxdext" +path="res://.godot/imported/car.glb-e378097fb9e4310b2b03d3bb742810f8.scn" + +[deps] + +source_file="res://cars/datto_120y/car.glb" +dest_files=["res://.godot/imported/car.glb-e378097fb9e4310b2b03d3bb742810f8.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 diff --git a/cars/datto_120y/stats.tres b/cars/datto_120y/stats.tres new file mode 100644 index 0000000..ba81a7d --- /dev/null +++ b/cars/datto_120y/stats.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="CarStats" load_steps=2 format=3] + +[ext_resource type="Script" path="res://src/car_stats.gd" id="1"] + +[resource] +script = ExtResource("1") +display_name = "Datto 120Y" +model_path = "res://cars/datto_120y/car.glb" +engine_power = 11.0 +mass = 850.0 +grip = 6.5 +length = 3.92 +width = 1.50 diff --git a/cars/excel_x3/car.glb b/cars/excel_x3/car.glb new file mode 100644 index 0000000000000000000000000000000000000000..8a66afcceda9849a0156d494c921c754a92392bd GIT binary patch literal 32016 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 IbvWhzUz~g_6951J literal 0 HcmV?d00001 diff --git a/cars/gemmy_tx/car.glb.import b/cars/gemmy_tx/car.glb.import new file mode 100644 index 0000000..08711fe --- /dev/null +++ b/cars/gemmy_tx/car.glb.import @@ -0,0 +1,42 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dqken2gji8e23" +path="res://.godot/imported/car.glb-5e5ac440118a817d281119d908a29b56.scn" + +[deps] + +source_file="res://cars/gemmy_tx/car.glb" +dest_files=["res://.godot/imported/car.glb-5e5ac440118a817d281119d908a29b56.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 diff --git a/cars/gemmy_tx/stats.tres b/cars/gemmy_tx/stats.tres index b13925e..1294461 100644 --- a/cars/gemmy_tx/stats.tres +++ b/cars/gemmy_tx/stats.tres @@ -5,7 +5,9 @@ [resource] script = ExtResource("1") display_name = "Gemmy TX" -model_path = "" -engine_power = 14.0 +model_path = "res://cars/gemmy_tx/car.glb" +engine_power = 13.0 mass = 950.0 grip = 7.0 +length = 4.13 +width = 1.57 diff --git a/cars/kingswood_hq/car.glb b/cars/kingswood_hq/car.glb new file mode 100644 index 0000000000000000000000000000000000000000..0099516a097ad8901c95a87c13ee0ab3b1fbc835 GIT binary patch literal 33464 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 literal 0 HcmV?d00001 diff --git a/cars/kingswood_hq/car.glb.import b/cars/kingswood_hq/car.glb.import new file mode 100644 index 0000000..67bf016 --- /dev/null +++ b/cars/kingswood_hq/car.glb.import @@ -0,0 +1,42 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://cipr4m5ik2j0q" +path="res://.godot/imported/car.glb-6977f92ee10a58fcde53200be4b3a9fb.scn" + +[deps] + +source_file="res://cars/kingswood_hq/car.glb" +dest_files=["res://.godot/imported/car.glb-6977f92ee10a58fcde53200be4b3a9fb.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 diff --git a/cars/kingswood_hq/stats.tres b/cars/kingswood_hq/stats.tres new file mode 100644 index 0000000..ccf3e75 --- /dev/null +++ b/cars/kingswood_hq/stats.tres @@ -0,0 +1,13 @@ +[gd_resource type="Resource" script_class="CarStats" load_steps=2 format=3] + +[ext_resource type="Script" path="res://src/car_stats.gd" id="1"] + +[resource] +script = ExtResource("1") +display_name = "Kingswood HQ" +model_path = "res://cars/kingswood_hq/car.glb" +engine_power = 15.0 +mass = 1400.0 +grip = 5.5 +length = 4.81 +width = 1.88 diff --git a/cars/vl_terbo/car.glb b/cars/vl_terbo/car.glb new file mode 100644 index 0000000000000000000000000000000000000000..e1dd0feb7c8257c3abf9bd6f12492f57ae1dea47 GIT binary patch literal 33156 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 S1J1KQkMr|ualIC&-2Vf20*BxL literal 0 HcmV?d00001 diff --git a/cars/vl_terbo/car.glb.import b/cars/vl_terbo/car.glb.import new file mode 100644 index 0000000..ec58ed1 --- /dev/null +++ b/cars/vl_terbo/car.glb.import @@ -0,0 +1,42 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://bbi1igmm0lvko" +path="res://.godot/imported/car.glb-d562177d050e8bc9348a99036622f776.scn" + +[deps] + +source_file="res://cars/vl_terbo/car.glb" +dest_files=["res://.godot/imported/car.glb-d562177d050e8bc9348a99036622f776.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 diff --git a/cars/vl_terbo/stats.tres b/cars/vl_terbo/stats.tres index d0fb006..edc4476 100644 --- a/cars/vl_terbo/stats.tres +++ b/cars/vl_terbo/stats.tres @@ -5,7 +5,9 @@ [resource] script = ExtResource("1") display_name = "VL Terbo" -model_path = "" -engine_power = 18.0 -mass = 1300.0 +model_path = "res://cars/vl_terbo/car.glb" +engine_power = 19.0 +mass = 1250.0 grip = 5.0 +length = 4.72 +width = 1.72 diff --git a/cars/xf_falcon/car.glb b/cars/xf_falcon/car.glb new file mode 100644 index 0000000000000000000000000000000000000000..f2d854d453559a08817212f1e2de37cd673c78ed GIT binary patch literal 33380 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 void: _box(pos, Vector3(1.9, 1.4, 4.4), Color.from_hsv(rng.randf(), 0.5, rng.randf_range(0.4, 0.9)), true) for i in 6: # light poles _box(Vector3(-30.0 + i * 12.0, 3.0, 8.0), Vector3(0.4, 6.0, 0.4), Color(0.3, 0.3, 0.3)) + _race_path() + +func _race_path() -> void: + # perimeter loop road around the parking rows -- race line + traffic route + var path := Path3D.new() + path.name = "RacePath" + var c := Curve3D.new() + var pts := [Vector3(45, 0, 55), Vector3(45, 0, -55), Vector3(-45, 0, -55), Vector3(-45, 0, 55)] + for i in pts.size() + 1: + var p: Vector3 = pts[i % pts.size()] + var to_prev: Vector3 = (pts[(i - 1 + pts.size()) % pts.size()] - p).normalized() * 12.0 + var to_next: Vector3 = (pts[(i + 1) % pts.size()] - p).normalized() * 12.0 + c.add_point(p, to_prev, to_next) + path.curve = c + add_child(path) func _box(pos: Vector3, size: Vector3, color: Color, dynamic := false) -> void: var body: PhysicsBody3D = RigidBody3D.new() if dynamic else StaticBody3D.new() diff --git a/screenshot.png.import b/screenshot.png.import new file mode 100644 index 0000000..1865695 --- /dev/null +++ b/screenshot.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dti2g41yp6bom" +path="res://.godot/imported/screenshot.png-024a21af5d37bf0f0dd0e2bccdd149d0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://screenshot.png" +dest_files=["res://.godot/imported/screenshot.png-024a21af5d37bf0f0dd0e2bccdd149d0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/car.gd b/src/car.gd index 1ecd26b..d3ed3d5 100644 --- a/src/car.gd +++ b/src/car.gd @@ -1,9 +1,16 @@ class_name Car extends RigidBody3D ## Arcade raycast car: 4 suspension rays, body-level tyre model. +## Player (is_player) reads keys; AI sets the *_in fields (see rival.gd). +## Crashes are detected as velocity jolts -- big jolt sheds det_* panels off the +## model, bigger jolt = full wreck (game.gd adds slow-mo for the player). ## ponytail: body-level grip/drive forces -- go per-wheel only if the feel demands it. +signal crashed(dv: float) +signal wreck_started + @export var stats: CarStats +@export var is_player := true const REST := 0.55 # suspension rest length, m const STEER_ANGLE := 0.6 @@ -13,13 +20,28 @@ const BOOST_MULT := 1.8 const BOOST_DRAIN := 0.35 # meter/sec while boosting const DRIFT_EARN := 0.3 # meter/sec while sliding const DRAG := 0.012 # quadratic drag -> top speed = sqrt(power/DRAG) +const BOOST_CAP := 4.0 # takedowns grow the meter up to 4x (Burnout 3 rule) +const PART_DV := 4.0 # velocity jolt (m/s in one tick) that sheds a panel +const WRECK_DV := 9.0 # jolt that wrecks you + +var throttle_in := 0.0 +var steer_in := 0.0 +var drift_in := false +var boost_in := false +var power_scale := 1.0 # rubber-band hook, set by game.gd var steer := 0.0 -var boost := 0.0 # 0..1 +var boost := 0.0 +var boost_max := 1.0 var boosting := false var slip := 0.0 # sideways m/s +var wrecked := false var spawn_xform: Transform3D +var _prev_lv := Vector3.ZERO +var _impact_cd := 0.0 +var _parts: Array[Node] = [] + @onready var wheels: Array[Node] = [$FL, $FR, $RL, $RR] func _ready() -> void: @@ -28,20 +50,41 @@ func _ready() -> void: center_of_mass = Vector3(0, -0.3, 0) angular_damp = 3.0 spawn_xform = global_transform + var cs: CollisionShape3D = $Shape + cs.shape = cs.shape.duplicate() + (cs.shape as BoxShape3D).size = Vector3(stats.width, 1.0, stats.length) + for w: Node3D in wheels: + w.position.x = signf(w.position.x) * (stats.width / 2.0 - 0.15) + w.position.z = signf(w.position.z) * (stats.length / 2.0 - 0.55) if stats.model_path != "" and ResourceLoader.exists(stats.model_path): $Body.visible = false - add_child((load(stats.model_path) as PackedScene).instantiate()) + var model: Node3D = (load(stats.model_path) as PackedScene).instantiate() + add_child(model) + _parts = model.find_children("det_*", "Node3D", true, false) + else: + var bm := BoxMesh.new() + bm.size = Vector3(stats.width, 1.0, stats.length) + $Body.mesh = bm func _physics_process(delta: float) -> void: - if Input.is_action_just_pressed("reset"): - global_transform = spawn_xform - linear_velocity = Vector3.ZERO - angular_velocity = Vector3.ZERO + if is_player: + throttle_in = Input.get_axis("brake", "throttle") + steer_in = Input.get_axis("steer_right", "steer_left") + drift_in = Input.is_action_pressed("drift") + boost_in = Input.is_action_pressed("boost") + if Input.is_action_just_pressed("reset"): + global_transform = spawn_xform + linear_velocity = Vector3.ZERO + angular_velocity = Vector3.ZERO + _prev_lv = Vector3.ZERO + if wrecked: + throttle_in = 0.0 + steer_in = 0.0 + drift_in = false + boost_in = false - var throttle := Input.get_axis("brake", "throttle") - steer = lerpf(steer, Input.get_axis("steer_right", "steer_left") * STEER_ANGLE, STEER_SPEED * delta) - var drifting := Input.is_action_pressed("drift") - boosting = Input.is_action_pressed("boost") and boost > 0.0 + steer = lerpf(steer, steer_in * STEER_ANGLE, STEER_SPEED * delta) + boosting = boost_in and boost > 0.0 and not wrecked var space := get_world_3d().direct_space_state var grounded := 0 @@ -61,18 +104,71 @@ func _physics_process(delta: float) -> void: if grounded >= 2: var fwd := -global_basis.z var speed := fwd.dot(linear_velocity) - var power := stats.engine_power * (BOOST_MULT if boosting else 1.0) - apply_central_force(fwd * throttle * power * mass) + var power := stats.engine_power * power_scale * (BOOST_MULT if boosting else 1.0) + apply_central_force(fwd * throttle_in * power * mass) apply_torque(global_basis.y * steer * clampf(speed / 12.0, -1.0, 1.0) * mass * 14.0) slip = global_basis.x.dot(linear_velocity) - var grip := stats.grip * (DRIFT_GRIP_MULT if drifting else 1.0) + var grip := stats.grip * (DRIFT_GRIP_MULT if drift_in else 1.0) apply_central_force(-global_basis.x * slip * grip * mass) apply_central_force(-linear_velocity * linear_velocity.length() * mass * DRAG) if absf(slip) > 4.0 and speed > 8.0: - boost = minf(boost + DRIFT_EARN * delta, 1.0) + boost = minf(boost + DRIFT_EARN * delta, boost_max) if boosting: boost = maxf(boost - BOOST_DRAIN * delta, 0.0) + var jolt := linear_velocity - _prev_lv + _prev_lv = linear_velocity + _impact_cd = maxf(_impact_cd - delta, 0.0) + if jolt.length() > PART_DV and _impact_cd == 0.0: + _impact_cd = 0.4 # one collision = one crash event, not one per tick + _impact(jolt) + +func earn_boost(amount: float) -> void: + boost = minf(boost + amount, boost_max) + +func _impact(jolt: Vector3) -> void: + var dv := jolt.length() + crashed.emit(dv) + var side := (global_basis.inverse() * -jolt).normalized() + for i in 1 + int(dv > 8.0) + int(dv > 12.0): + _shed_panel(side) + if dv > WRECK_DV and not wrecked: + wrecked = true + wreck_started.emit() + get_tree().create_timer(2.2, true, false, true).timeout.connect(_recover) + +func _recover() -> void: + wrecked = false + if is_player: + boost = boost_max # Burnout rule: recovery refills the meter + +func _shed_panel(side: Vector3) -> void: + if _parts.is_empty(): + return + var best: Node3D = _parts[0] + var best_d := -INF + for p: Node3D in _parts: + var d := to_local(p.global_position).normalized().dot(side) + if d > best_d: + best_d = d + best = p + _parts.erase(best) + var rb := RigidBody3D.new() + rb.mass = 8.0 + var cs := CollisionShape3D.new() + var box := BoxShape3D.new() + var aabb := (best as MeshInstance3D).get_aabb() if best is MeshInstance3D else AABB(Vector3.ONE * -0.15, Vector3.ONE * 0.3) + box.size = aabb.size.clamp(Vector3.ONE * 0.06, Vector3.ONE * 3.0) + cs.shape = box + cs.position = aabb.get_center() + rb.add_child(cs) + get_parent().add_child(rb) + rb.global_transform = best.global_transform + best.reparent(rb) + rb.linear_velocity = linear_velocity + rb.angular_velocity = Vector3(randf_range(-8, 8), randf_range(-8, 8), randf_range(-8, 8)) + get_tree().create_timer(8.0).timeout.connect(rb.queue_free) + func speed_kmh() -> float: return linear_velocity.length() * 3.6 diff --git a/src/car_stats.gd b/src/car_stats.gd index 6ddce8b..79c1438 100644 --- a/src/car_stats.gd +++ b/src/car_stats.gd @@ -8,3 +8,5 @@ extends Resource @export var engine_power := 14.0 # forward accel, m/s^2-ish @export var mass := 1100.0 @export var grip := 6.0 # lateral grip; lower = slidier +@export var length := 4.2 # collision footprint, m +@export var width := 1.8 diff --git a/src/game.gd b/src/game.gd index 86d0b3f..33b6679 100644 --- a/src/game.gd +++ b/src/game.gd @@ -1,19 +1,45 @@ class_name Game extends Node3D -## Loads the chosen level, spawns the chosen car at the level's "Spawn" node, -## runs chase cam + HUD. Everything built in code -- game.tscn is just this script. +## Loads the chosen level, spawns cars, runs the chosen mode: +## cruise -- free roam with traffic +## crash -- 60s damage-dollar rampage (Crash Mode v0) +## race -- 3 laps vs rivals on the level's RacePath, rubber-banded +## Takedowns refill the boost meter and grow it (up to 4x, per Burnout 3). static var car_path := "" static var level_path := "" +static var mode := "cruise" + +const LAPS := 3 +const CRASH_TIME := 60.0 +const RIVAL_COUNT := 3 +const TRAFFIC_COUNT := {"cruise": 6, "crash": 10, "race": 4} var car: Car var cam: Camera3D +var race_path: Path3D +var rivals: Array[Rival] = [] +var traffic: Array[TrafficCar] = [] +var score := 0.0 +var time_left := CRASH_TIME +var over := false +var laps := {} # racer -> lap count +var prev_off := {} # racer -> last path offset +var near_cd := {} # traffic -> near-miss cooldown + var speed_label: Label var boost_bar: ProgressBar +var info_label: Label +var msg_label: Label +var msg_timer: SceneTreeTimer func _ready() -> void: var level: Node = (load(level_path) as PackedScene).instantiate() add_child(level) + for p in level.find_children("*", "Path3D", true, false): + race_path = p + if String(p.name).begins_with("Race"): + break car = (load("res://src/car.tscn") as PackedScene).instantiate() car.stats = load(car_path) @@ -22,6 +48,38 @@ func _ready() -> void: if spawn is Node3D: car.global_transform = spawn.global_transform car.global_position += Vector3.UP * 0.6 + car.crashed.connect(_player_crash) + car.wreck_started.connect(_player_wreck) + + if mode == "race" and race_path: + _grid(car, 0.0) + laps[car] = 0 + prev_off[car] = 0.0 + var pool := Registry.cars().values() + for i in RIVAL_COUNT: + var r: Rival = (load("res://src/rival.tscn") as PackedScene).instantiate() + r.stats = load(pool[randi() % pool.size()]) + r.path = race_path + add_child(r) + var off := race_path.curve.get_baked_length() - 8.0 * (i + 1) + _grid(r, off) + r.progress = off + laps[r] = 0 + prev_off[r] = off + r.wreck_started.connect(_rival_wrecked.bind(r)) + rivals.append(r) + car.spawn_xform = car.global_transform + + if race_path: + var n: int = TRAFFIC_COUNT.get(mode, 4) + for i in n: + var t := TrafficCar.new() + t.path = race_path + t.offset = race_path.curve.get_baked_length() * (i + 0.5) / n + t.speed = randf_range(9.0, 14.0) + add_child(t) + t.smashed.connect(_traffic_smashed) + traffic.append(t) cam = Camera3D.new() cam.fov = 72.0 @@ -48,17 +106,137 @@ func _ready() -> void: hud.add_child(speed_label) boost_bar = ProgressBar.new() boost_bar.position = Vector2(24, 72) - boost_bar.size = Vector2(220, 18) + boost_bar.size = Vector2(160, 18) boost_bar.show_percentage = false hud.add_child(boost_bar) + info_label = Label.new() + info_label.anchor_left = 1.0 + info_label.anchor_right = 1.0 + info_label.offset_left = -420.0 + info_label.offset_top = 24.0 + info_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + info_label.add_theme_font_size_override("font_size", 30) + hud.add_child(info_label) + msg_label = Label.new() + msg_label.anchor_left = 0.5 + msg_label.anchor_right = 0.5 + msg_label.anchor_top = 0.25 + msg_label.grow_horizontal = Control.GROW_DIRECTION_BOTH + msg_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + msg_label.add_theme_font_size_override("font_size", 48) + hud.add_child(msg_label) + +func _exit_tree() -> void: + Engine.time_scale = 1.0 + +func _grid(c: Car, off: float) -> void: + var L := race_path.curve.get_baked_length() + var pos := race_path.to_global(race_path.curve.sample_baked(fposmod(off, L))) + Vector3.UP * 0.8 + var ahead := race_path.to_global(race_path.curve.sample_baked(fposmod(off + 4.0, L))) + Vector3.UP * 0.8 + c.look_at_from_position(pos, ahead, Vector3.UP) + +func _physics_process(delta: float) -> void: + # near misses: close pass on live traffic at speed + for t in traffic: + near_cd[t] = maxf(near_cd.get(t, 0.0) - delta, 0.0) + if t.live and near_cd[t] == 0.0 and car.linear_velocity.length() > 15.0 \ + and car.global_position.distance_to(t.global_position) < 4.5: + near_cd[t] = 4.0 + car.earn_boost(0.12) + _msg("NEAR MISS", 32) + if mode == "race" and race_path and not over: + _race_tick() + +func _race_tick() -> void: + var L := race_path.curve.get_baked_length() + prev_off[car] = _lap_step(car, _nearest_off(car.global_position, prev_off[car], L), L) + for r in rivals: + prev_off[r] = _lap_step(r, r.progress, L) + var my_total: float = laps[car] * L + prev_off[car] + var pos := 1 + for r in rivals: + var rt: float = laps[r] * L + prev_off[r] + if rt > my_total: + pos += 1 + r.power_scale = clampf(1.0 + (my_total - rt) / 300.0, 0.8, 1.35) + info_label.text = "P%d/%d LAP %d/%d" % [pos, rivals.size() + 1, mini(laps[car] + 1, LAPS), LAPS] + if laps[car] >= LAPS: + over = true + _msg("FINISHED P%d\nEsc for menu" % pos, 48, true) + +func _lap_step(c: Car, off: float, L: float) -> float: + if prev_off[c] > 0.8 * L and off < 0.2 * L: + laps[c] += 1 + elif off > 0.8 * L and prev_off[c] < 0.2 * L: + laps[c] -= 1 + return off + +func _nearest_off(pos: Vector3, seed_off: float, L: float) -> float: + var best := seed_off + var best_d := INF + for i in 25: + var o := fposmod(seed_off - 4.0 + i, L) + var d := race_path.to_global(race_path.curve.sample_baked(o)).distance_squared_to(pos) + if d < best_d: + best_d = d + best = o + return best + +func _player_crash(dv: float) -> void: + if mode == "crash" and not over: + score += minf(dv, 30.0) * 120.0 + if dv > 6.0: + _msg("SMASH", 40) + +func _player_wreck() -> void: + Engine.time_scale = 0.35 # Impact Time + get_tree().create_timer(1.0, true, false, true).timeout.connect( + func() -> void: Engine.time_scale = 1.0) + _msg("WRECKED", 48) + +func _rival_wrecked(r: Rival) -> void: + if car.wrecked or car.global_position.distance_to(r.global_position) > 8.0: + return + car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP) + car.boost = car.boost_max + _msg("TAKEDOWN!", 56) + +func _traffic_smashed(impulse: float) -> void: + if mode == "crash" and not over: + score += impulse * 400.0 + _msg("$%d" % int(impulse * 400.0), 40) + car.earn_boost(0.2) + +func _msg(text: String, size: int, sticky := false) -> void: + msg_label.text = text + msg_label.add_theme_font_size_override("font_size", size) + if sticky: + msg_timer = null + return + var t := get_tree().create_timer(1.2, true, false, true) + msg_timer = t + t.timeout.connect(func() -> void: + if msg_timer == t: + msg_label.text = "") func _process(delta: float) -> void: if Input.is_action_just_pressed("ui_cancel"): get_tree().change_scene_to_file("res://src/main.tscn") return + if mode == "crash" and not over: + time_left -= delta + info_label.text = "$%d %d s" % [int(score), ceili(time_left)] + if time_left <= 0.0: + over = true + _msg("DAMAGE: $%d\nR to retry, Esc for menu" % int(score), 48, true) + if over and mode == "crash" and Input.is_action_just_pressed("reset"): + get_tree().reload_current_scene() + return var target := car.global_position + car.global_basis * Vector3(0, 2.4, 6.0) cam.global_position = cam.global_position.lerp(target, 1.0 - exp(-5.0 * delta)) cam.look_at(car.global_position + Vector3.UP * 1.2) cam.fov = lerpf(cam.fov, 84.0 if car.boosting else 72.0, 6.0 * delta) speed_label.text = "%d km/h" % roundf(car.speed_kmh()) + boost_bar.size.x = 160.0 * car.boost_max + boost_bar.max_value = 100.0 * car.boost_max boost_bar.value = car.boost * 100.0 diff --git a/src/main.gd b/src/main.gd index f631fce..62306bb 100644 --- a/src/main.gd +++ b/src/main.gd @@ -1,8 +1,11 @@ extends Control -## Menu: pick a car and a level from whatever folders exist, hit RACE. +## Menu: pick a car, a level and a mode from whatever exists, hit RACE. + +const MODES := [["cruise", "Cruise"], ["crash", "Crash Mode"], ["race", "Race"]] @onready var car_list: ItemList = $VBox/HBox/Cars @onready var level_list: ItemList = $VBox/HBox/Levels +@onready var mode_list: ItemList = $VBox/HBox/Modes var car_paths: Array = [] var level_paths: Array = [] @@ -17,10 +20,13 @@ func _ready() -> void: for id in levels: level_list.add_item(String(id).capitalize()) level_paths.append(levels[id]) + for m in MODES: + mode_list.add_item(m[1]) if not car_paths.is_empty(): car_list.select(0) if not level_paths.is_empty(): level_list.select(0) + mode_list.select(0) $VBox/Start.pressed.connect(_start) func _start() -> void: @@ -28,4 +34,5 @@ func _start() -> void: return Game.car_path = car_paths[car_list.get_selected_items()[0]] Game.level_path = level_paths[level_list.get_selected_items()[0]] + Game.mode = MODES[mode_list.get_selected_items()[0]][0] get_tree().change_scene_to_file("res://src/game.tscn") diff --git a/src/main.tscn b/src/main.tscn index 2fb7ba2..7032f47 100644 --- a/src/main.tscn +++ b/src/main.tscn @@ -31,6 +31,9 @@ size_flags_horizontal = 3 [node name="Levels" type="ItemList" parent="VBox/HBox"] size_flags_horizontal = 3 +[node name="Modes" type="ItemList" parent="VBox/HBox"] +size_flags_horizontal = 3 + [node name="Start" type="Button" parent="VBox"] text = "RACE" theme_override_font_sizes/font_size = 32 diff --git a/src/rival.gd b/src/rival.gd new file mode 100644 index 0000000..f66e4f8 --- /dev/null +++ b/src/rival.gd @@ -0,0 +1,52 @@ +class_name Rival +extends Car +## AI opponent: chases a lookahead point on the race path. Rubber-banding comes +## from game.gd setting power_scale. ponytail: no hostility index yet -- add when +## Road Rage mode lands. + +var path: Path3D +var progress := 0.0 + +var _stuck := 0.0 + +func _physics_process(delta: float) -> void: + if path: + _drive(delta) + super(delta) + +func _drive(delta: float) -> void: + var curve := path.curve + var L := curve.get_baked_length() + progress = _nearest(curve, L) + var look := 6.0 + linear_velocity.length() * 0.6 + var target := path.to_global(curve.sample_baked(fmod(progress + look, L))) + var local := to_local(target) + var angle := atan2(-local.x, -local.z) + steer_in = clampf(angle * 1.6, -1.0, 1.0) + throttle_in = 0.35 if absf(angle) > 1.2 else 1.0 + drift_in = absf(angle) > 0.7 and linear_velocity.length() > 14.0 + boost_in = boost > 0.4 and absf(angle) < 0.15 + + # wedged against something: put it back on the path + if linear_velocity.length() < 1.5 and not wrecked: + _stuck += delta + if _stuck > 3.0: + _stuck = 0.0 + var pos := path.to_global(curve.sample_baked(progress)) + Vector3.UP * 0.8 + var ahead := path.to_global(curve.sample_baked(fmod(progress + 4.0, L))) + Vector3.UP * 0.8 + look_at_from_position(pos, ahead, Vector3.UP) + linear_velocity = Vector3.ZERO + angular_velocity = Vector3.ZERO + else: + _stuck = 0.0 + +func _nearest(curve: Curve3D, L: float) -> float: + var best := progress + var best_d := INF + for i in 25: + var o := fposmod(progress - 4.0 + i, L) + var d := path.to_global(curve.sample_baked(o)).distance_squared_to(global_position) + if d < best_d: + best_d = d + best = o + return best diff --git a/src/rival.gd.uid b/src/rival.gd.uid new file mode 100644 index 0000000..8d93ebe --- /dev/null +++ b/src/rival.gd.uid @@ -0,0 +1 @@ +uid://b8s7pvlla46kh diff --git a/src/rival.tscn b/src/rival.tscn new file mode 100644 index 0000000..ea9b668 --- /dev/null +++ b/src/rival.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="PackedScene" path="res://src/car.tscn" id="1"] +[ext_resource type="Script" path="res://src/rival.gd" id="2"] + +[node name="Rival" instance=ExtResource("1")] +script = ExtResource("2") +is_player = false diff --git a/src/traffic.gd b/src/traffic.gd new file mode 100644 index 0000000..4a32352 --- /dev/null +++ b/src/traffic.gd @@ -0,0 +1,73 @@ +class_name TrafficCar +extends RigidBody3D +## Civilian traffic: frozen-kinematic body gliding along a Path3D loop; goes fully +## dynamic (and flies) when whacked hard enough. +## ponytail: no lane logic, no avoidance -- canon Burnout traffic is dumb. + +signal smashed(impulse: float) + +var path: Path3D +var offset := 0.0 +var speed := 11.0 +var live := true + +static var _model_pool: Array[String] = [] +static var _pool_built := false + +static func pick_model() -> String: + if not _pool_built: + _pool_built = true + for p in Registry.cars().values(): + var s: CarStats = load(p) + if s.model_path != "" and ResourceLoader.exists(s.model_path): + _model_pool.append(s.model_path) + return _model_pool.pick_random() if not _model_pool.is_empty() else "" + +func _init() -> void: + mass = 900.0 + freeze = true + freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC + contact_monitor = true + max_contacts_reported = 8 + var cs := CollisionShape3D.new() + var box := BoxShape3D.new() + box.size = Vector3(1.7, 1.2, 4.0) + cs.shape = box + add_child(cs) + var mp := pick_model() + if mp != "": + add_child((load(mp) as PackedScene).instantiate()) + else: + var mi := MeshInstance3D.new() + var bm := BoxMesh.new() + bm.size = box.size + var mat := StandardMaterial3D.new() + mat.albedo_color = Color.from_hsv(randf(), 0.4, 0.8) + bm.material = mat + mi.mesh = bm + add_child(mi) + body_entered.connect(_hit) + +func _physics_process(delta: float) -> void: + if not live or path == null: + return + var L := path.curve.get_baked_length() + offset = fmod(offset + speed * delta, L) + var pos := path.to_global(path.curve.sample_baked(offset)) + Vector3.UP * 0.75 + var ahead := path.to_global(path.curve.sample_baked(fmod(offset + 2.0, L))) + Vector3.UP * 0.75 + look_at_from_position(pos, ahead, Vector3.UP) + +func _hit(body: Node) -> void: + if not live or not body is RigidBody3D: + return + if body is TrafficCar and (body as TrafficCar).live: + return # live traffic rear-ending live traffic is not a crash, it's commuting + # limit_length: penetration ejections report silly velocities, cap the punt + var rel := ((body as RigidBody3D).linear_velocity - linear_velocity).limit_length(30.0) + if rel.length() < 3.0: + return + live = false + freeze = false + linear_velocity = rel * 0.7 + Vector3.UP * 2.5 + angular_velocity = Vector3(randf_range(-3, 3), randf_range(-3, 3), randf_range(-3, 3)) + smashed.emit(rel.length()) diff --git a/src/traffic.gd.uid b/src/traffic.gd.uid new file mode 100644 index 0000000..f0c0551 --- /dev/null +++ b/src/traffic.gd.uid @@ -0,0 +1 @@ +uid://cch61cwxv7g81 diff --git a/tests/screenshot.gd b/tests/screenshot.gd index e655027..6649fad 100644 --- a/tests/screenshot.gd +++ b/tests/screenshot.gd @@ -1,19 +1,27 @@ extends SceneTree ## Boot straight into the game and save a viewport screenshot (needs a window, not --headless): -## /Applications/Godot.app/Contents/MacOS/Godot --path . --script tests/screenshot.gd -## Writes screenshot.png to the project dir. Used to eyeball the build without a human. +## Godot --path . --script tests/screenshot.gd -- car=datto_120y mode=cruise out=shot.png frames=90 throttle=1 +## Defaults: first car, cruise, screenshot.png, 90 physics frames, throttle held. func _initialize() -> void: _run() func _run() -> void: - Game.car_path = Registry.cars().values()[0] + var opts := {} + for a in OS.get_cmdline_user_args(): + var kv := a.split("=") + if kv.size() == 2: + opts[kv[0]] = kv[1] + var cars := Registry.cars() + Game.car_path = cars.get(opts.get("car", ""), cars.values()[0]) Game.level_path = Registry.levels().values()[0] + Game.mode = opts.get("mode", "cruise") root.add_child((load("res://src/game.tscn") as PackedScene).instantiate()) await process_frame - Input.action_press("throttle") - for i in 90: + if opts.get("throttle", "1") == "1": + Input.action_press("throttle") + for i in int(opts.get("frames", "90")): await physics_frame - root.get_texture().get_image().save_png("res://screenshot.png") + root.get_texture().get_image().save_png("res://" + opts.get("out", "screenshot.png")) print("screenshot saved") quit(0) diff --git a/tests/screenshot.gd.uid b/tests/screenshot.gd.uid new file mode 100644 index 0000000..94044a2 --- /dev/null +++ b/tests/screenshot.gd.uid @@ -0,0 +1 @@ +uid://ci411yia72g6m diff --git a/tests/smoke.gd b/tests/smoke.gd index 3dcb1ae..2a6c333 100644 --- a/tests/smoke.gd +++ b/tests/smoke.gd @@ -1,7 +1,8 @@ extends SceneTree ## Headless smoke test: ## /Applications/Godot.app/Contents/MacOS/Godot --headless --path . --script tests/smoke.gd -## Registry finds content, game boots, car drives forward under throttle. +## Registry finds content; each mode boots and simulates; the car drives; traffic +## gets smashed when driven through. func _initialize() -> void: _run() @@ -14,16 +15,62 @@ func _run() -> void: for p in cars.values(): var s: CarStats = load(p) assert(s != null and s.engine_power > 0.0, "bad stats: %s" % p) + if s.model_path != "": + assert(load(s.model_path) is PackedScene, "bad model: %s" % s.model_path) Game.car_path = cars.values()[0] Game.level_path = levels.values()[0] - var game: Game = (load("res://src/game.tscn") as PackedScene).instantiate() - root.add_child(game) - await process_frame # _ready doesn't run until the main loop starts + + # cruise: throttle 3s, car must move + var game: Game = await _boot("cruise") + if game.car.stats.model_path != "": + assert(game.car._parts.size() >= 8, "model has no detachable det_* parts") var start: Vector3 = game.car.global_position Input.action_press("throttle") for i in 180: await physics_frame var dist := start.distance_to(game.car.global_position) assert(dist > 5.0, "car didn't move (%.1f m)" % dist) - print("smoke ok: %d cars, %d levels, drove %.1f m, %.0f km/h" % [cars.size(), levels.size(), dist, game.car.speed_kmh()]) + Input.action_release("throttle") + game.free() + + # crash mode: traffic spawned, ram the nearest one, score must rise + game = await _boot("crash") + assert(game.traffic.size() == Game.TRAFFIC_COUNT["crash"], "traffic missing") + var victim: TrafficCar = game.traffic[0] + await physics_frame # traffic takes position on its first physics tick + game.car.global_position = victim.global_position - victim.global_basis.z * 15.0 + game.car.look_at(victim.global_position) + Input.action_press("throttle") + for i in 240: + await physics_frame + if not victim.live: + break + Input.action_release("throttle") + assert(not victim.live, "rammed traffic never went dynamic") + assert(game.score > 0.0, "crash mode scored nothing") + var crash_score := game.score + game.free() + + # race: rivals spawn and make progress along the path + game = await _boot("race") + assert(game.rivals.size() == Game.RIVAL_COUNT, "rivals missing") + var p0: float = game.rivals[0].progress + for i in 240: + await physics_frame + var moved := false + for r in game.rivals: + if absf(r.progress - p0) > 3.0 or r.linear_velocity.length() > 3.0: + moved = true + assert(moved, "no rival is driving") + game.free() + + print("smoke ok: %d cars, %d levels | cruise %.1f m | crash $%d | race AI driving" % + [cars.size(), levels.size(), dist, int(crash_score)]) quit(0) + +func _boot(mode: String) -> Game: + Game.mode = mode + var game: Game = (load("res://src/game.tscn") as PackedScene).instantiate() + root.add_child(game) + await process_frame # _ready is deferred until the main loop runs + return game diff --git a/tools/build_cars.py b/tools/build_cars.py new file mode 100644 index 0000000..2f1d525 --- /dev/null +++ b/tools/build_cars.py @@ -0,0 +1,217 @@ +"""Procedural Aussie shitbox factory. + +Run: /Applications/Blender.app/Contents/MacOS/Blender -b -P tools/build_cars.py + +Each car: side-silhouette profile extruded across the width (body + greenhouse), +cylinder wheels, and separate det_* panel objects (doors, mirrors, bumpers, +bonnet, boot) that the game rips off on impact. Exports cars//car.glb. + +Convention: +Y is the nose in Blender (imports facing -Z, Godot-forward). +Ground is z=0 while building; the body root is dropped -0.75 at the end to +match the car controller's ride height. + +ponytail: silhouettes are hand-tuned point lists, not reference-traced. Feed me +side-on photos per car when a shape needs to get closer. +""" +import os +import math +import bpy +import bmesh +from mathutils import Matrix + +OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "cars") + +GLASS = (0.12, 0.16, 0.20, 1) +TYRE = (0.08, 0.08, 0.08, 1) +CHROME = (0.75, 0.76, 0.78, 1) +DARK = (0.15, 0.15, 0.15, 1) +LIGHT_F = (0.95, 0.92, 0.75, 1) +LIGHT_R = (0.7, 0.1, 0.08, 1) + +# L, W, heights: nose/belt/tail, lengths: bonnet/boot, roof height + rear roof end, +# axle overhangs, wheel radius, paint, chrome bumpers, hatch rear +CARS = { + "datto_120y": dict(L=3.92, W=1.50, nose=0.62, belt=0.78, tail=0.72, + bonnet=1.00, boot=0.85, roof=1.32, roof_rear=0.75, + ohang_f=0.72, ohang_r=0.80, wheel=0.27, + paint=(0.78, 0.62, 0.16, 1), chrome_bumpers=True, hatch=False), + "gemmy_tx": dict(L=4.13, W=1.57, nose=0.60, belt=0.76, tail=0.70, + bonnet=1.15, boot=0.95, roof=1.28, roof_rear=0.80, + ohang_f=0.80, ohang_r=0.85, wheel=0.28, + paint=(0.72, 0.30, 0.10, 1), chrome_bumpers=True, hatch=False), + "kingswood_hq": dict(L=4.81, W=1.88, nose=0.68, belt=0.85, tail=0.78, + bonnet=1.50, boot=1.30, roof=1.36, roof_rear=1.05, + ohang_f=0.90, ohang_r=1.05, wheel=0.31, + paint=(0.66, 0.74, 0.55, 1), chrome_bumpers=True, hatch=False), + "vl_terbo": dict(L=4.72, W=1.72, nose=0.56, belt=0.80, tail=0.82, + bonnet=1.30, boot=1.05, roof=1.33, roof_rear=0.90, + ohang_f=0.85, ohang_r=0.95, wheel=0.30, + paint=(0.92, 0.92, 0.90, 1), chrome_bumpers=False, hatch=False), + "xf_falcon": dict(L=4.91, W=1.86, nose=0.66, belt=0.84, tail=0.80, + bonnet=1.45, boot=1.25, roof=1.37, roof_rear=1.00, + ohang_f=0.90, ohang_r=1.00, wheel=0.30, + paint=(0.45, 0.55, 0.68, 1), chrome_bumpers=True, hatch=False), + "excel_x3": dict(L=4.10, W=1.62, nose=0.58, belt=0.80, tail=0.96, + bonnet=0.95, boot=0.30, roof=1.36, roof_rear=0.95, + ohang_f=0.78, ohang_r=0.70, wheel=0.27, + paint=(0.10, 0.55, 0.52, 1), chrome_bumpers=False, hatch=True), +} + + +def wipe(): + for obj in list(bpy.data.objects): + bpy.data.objects.remove(obj, do_unlink=True) + for coll in (bpy.data.meshes, bpy.data.materials): + for block in list(coll): + if block.users == 0: + coll.remove(block) + + +def material(name, rgba, metallic=0.0, rough=0.7): + mat = bpy.data.materials.new(name) + mat.use_nodes = True + bsdf = next(n for n in mat.node_tree.nodes if n.type == "BSDF_PRINCIPLED") + bsdf.inputs["Base Color"].default_value = rgba + bsdf.inputs["Metallic"].default_value = metallic + bsdf.inputs["Roughness"].default_value = rough + return mat + + +def add_obj(name, mesh, mat): + obj = bpy.data.objects.new(name, mesh) + obj.data.materials.append(mat) + bpy.context.collection.objects.link(obj) + return obj + + +def extrude_profile(name, profile, half_w, mat): + """Closed (y,z) polygon extruded from -half_w to +half_w along x.""" + n = len(profile) + verts = [(-half_w, y, z) for y, z in profile] + [(half_w, y, z) for y, z in profile] + faces = [list(range(n)), [2 * n - 1 - i for i in range(n)]] + for i in range(n): + j = (i + 1) % n + faces.append([i, j, n + j, n + i]) + mesh = bpy.data.meshes.new(name) + mesh.from_pydata(verts, [], faces) + bm = bmesh.new() + bm.from_mesh(mesh) + bmesh.ops.recalc_face_normals(bm, faces=bm.faces) + bm.to_mesh(mesh) + bm.free() + mesh.update() + return add_obj(name, mesh, mat) + + +def box(name, center, size, mat, rot_x=0.0): + mesh = bpy.data.meshes.new(name) + bm = bmesh.new() + bmesh.ops.create_cube(bm, size=1.0) + bmesh.ops.scale(bm, vec=size, verts=bm.verts) + if rot_x: + bmesh.ops.rotate(bm, cent=(0, 0, 0), matrix=Matrix.Rotation(rot_x, 3, "X"), verts=bm.verts) + bmesh.ops.translate(bm, vec=center, verts=bm.verts) + bm.to_mesh(mesh) + bm.free() + return add_obj(name, mesh, mat) + + +def wheel(name, x, y, r, mat): + mesh = bpy.data.meshes.new(name) + bm = bmesh.new() + bmesh.ops.create_cone(bm, cap_ends=True, segments=14, radius1=r, radius2=r, depth=0.24) + bmesh.ops.rotate(bm, cent=(0, 0, 0), matrix=Matrix.Rotation(math.pi / 2, 3, "Y"), verts=bm.verts) + bmesh.ops.translate(bm, vec=(x, y, r), verts=bm.verts) + bm.to_mesh(mesh) + bm.free() + return add_obj(name, mesh, mat) + + +def build(car_id, s): + wipe() + paint = material("paint", s["paint"], rough=0.45) + paint_dk = material("paint_dark", tuple(c * 0.8 for c in s["paint"][:3]) + (1,), rough=0.5) + glass = material("glass", GLASS, rough=0.2) + tyre = material("tyre", TYRE, rough=0.9) + chrome = material("chrome", CHROME, metallic=1.0, rough=0.25) + dark = material("dark", DARK, rough=0.8) + bumper_mat = chrome if s["chrome_bumpers"] else paint_dk + + L, W = s["L"], s["W"] + f, r, hw = L / 2, -L / 2, W / 2 + bottom = 0.22 + ws_base = f - s["bonnet"] # windscreen base y + boot_y = r + s["boot"] # rear window base y + nose2 = s["nose"] + 0.02 + + body_profile = [ + (f, bottom + 0.15), (f, s["nose"]), (f - 0.3, nose2), (ws_base, s["belt"]), + (boot_y, s["belt"]), (r + 0.05, s["tail"]), (r, s["tail"] - 0.05), + (r, bottom + 0.15), (r + 0.2, bottom), (f - 0.2, bottom), + ] + body = extrude_profile("body", body_profile, hw, paint) + + rr_y = r + 0.12 if s["hatch"] else boot_y + rr_z = s["tail"] - 0.03 if s["hatch"] else s["belt"] + gh_profile = [ + (ws_base, s["belt"] - 0.02), + (ws_base - 0.55, s["roof"]), + (boot_y + s["roof_rear"], s["roof"]), + (rr_y, rr_z), + ] + parts = [extrude_profile("glass", gh_profile, hw * 0.84, glass)] + + for side, sx in (("l", -1), ("r", 1)): + door_z = (bottom + 0.3 + s["belt"]) / 2 + door_h = s["belt"] - bottom - 0.32 + span = (ws_base - 0.05) - (boot_y + 0.05) + half = span / 2 + parts.append(box("det_door_f%s" % side, (sx * hw, ws_base - 0.05 - half / 2, door_z), + (0.06, half - 0.06, door_h), paint_dk)) + parts.append(box("det_door_r%s" % side, (sx * hw, boot_y + 0.05 + half / 2, door_z), + (0.06, half - 0.06, door_h), paint_dk)) + parts.append(box("det_mirror_%s" % side, (sx * (hw + 0.09), ws_base + 0.05, s["belt"] + 0.10), + (0.14, 0.09, 0.12), dark)) + parts.append(box("det_bumper_f", (0, f + 0.05, bottom + 0.16), (W + 0.06, 0.15, 0.16), bumper_mat)) + parts.append(box("det_bumper_r", (0, r - 0.05, bottom + 0.16), (W + 0.06, 0.15, 0.16), bumper_mat)) + + # bonnet plate follows the bonnet slope + by1, bz1, by2, bz2 = f - 0.35, nose2, ws_base + 0.03, s["belt"] + ang = math.atan2(bz2 - bz1, by2 - by1) + parts.append(box("det_bonnet", ((0, (by1 + by2) / 2, (bz1 + bz2) / 2 + 0.02)), + (W * 0.7, math.hypot(by2 - by1, bz2 - bz1) - 0.1, 0.04), paint_dk, rot_x=-ang)) + if not s["hatch"]: + ty1, tz1, ty2, tz2 = boot_y - 0.03, s["belt"], r + 0.1, s["tail"] + ang = math.atan2(tz2 - tz1, ty2 - ty1) + parts.append(box("det_boot", ((0, (ty1 + ty2) / 2, (tz1 + tz2) / 2 + 0.02)), + (W * 0.7, math.hypot(ty2 - ty1, tz2 - tz1) - 0.1, 0.04), paint_dk, rot_x=-ang)) + + trim = [ + box("trim_grille", (0, f + 0.01, s["nose"] - 0.14), (W * 0.5, 0.05, 0.12), dark), + box("trim_light_fl", (-W * 0.33, f + 0.02, s["nose"] - 0.13), (0.24, 0.05, 0.13), material("lf", LIGHT_F)), + box("trim_light_fr", (W * 0.33, f + 0.02, s["nose"] - 0.13), (0.24, 0.05, 0.13), material("lf2", LIGHT_F)), + box("trim_light_rl", (-W * 0.32, r - 0.02, s["tail"] - 0.14), (0.26, 0.05, 0.12), material("lr", LIGHT_R)), + box("trim_light_rr", (W * 0.32, r - 0.02, s["tail"] - 0.14), (0.26, 0.05, 0.12), material("lr2", LIGHT_R)), + ] + + ax_f, ax_r = f - s["ohang_f"], r + s["ohang_r"] + wx = hw - 0.10 + wheels = [wheel("wheel_fl", -wx, ax_f, s["wheel"], tyre), wheel("wheel_fr", wx, ax_f, s["wheel"], tyre), + wheel("wheel_rl", -wx, ax_r, s["wheel"], tyre), wheel("wheel_rr", wx, ax_r, s["wheel"], tyre)] + + for obj in parts + trim + wheels: + obj.parent = body + body.location.z = -0.75 # node origin sits at the controller's ride height + + out_dir = os.path.join(OUT, car_id) + os.makedirs(out_dir, exist_ok=True) + path = os.path.join(out_dir, "car.glb") + for obj in bpy.data.objects: + obj.select_set(True) + bpy.ops.export_scene.gltf(filepath=path, export_format="GLB", use_selection=True) + print("built", car_id, "->", path) + + +for car_id, spec in CARS.items(): + build(car_id, spec) +print("done: %d cars" % len(CARS))