diff --git a/README.md b/README.md index ee0fc77..183e5e8 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,10 @@ Model convention (any GLB works, hand-modelled or generated): - 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. +- 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. diff --git a/cars/lazza_ka/car.glb b/cars/lazza_ka/car.glb new file mode 100644 index 0000000..ea28e06 Binary files /dev/null and b/cars/lazza_ka/car.glb differ diff --git a/cars/lazza_ka/car.glb.import b/cars/lazza_ka/car.glb.import new file mode 100644 index 0000000..3b18828 --- /dev/null +++ b/cars/lazza_ka/car.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://bbottmecxqhu1" +path="res://.godot/imported/car.glb-edc9533c079c72a53d09ab51d6c7dcf4.scn" + +[deps] + +source_file="res://cars/lazza_ka/car.glb" +dest_files=["res://.godot/imported/car.glb-edc9533c079c72a53d09ab51d6c7dcf4.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +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 +gltf/texture_map_mode=1 diff --git a/cars/lazza_ka/stats.tres b/cars/lazza_ka/stats.tres new file mode 100644 index 0000000..4d5f338 --- /dev/null +++ b/cars/lazza_ka/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 = "FLOORED LAZZA 1.5" +model_path = "res://cars/lazza_ka/car.glb" +engine_power = 21.0 +mass = 860.0 +grip = 7.8 +length = 3.96 +width = 1.61 diff --git a/tests/screenshot.gd b/tests/screenshot.gd index f602b12..6ab6a7e 100644 --- a/tests/screenshot.gd +++ b/tests/screenshot.gd @@ -35,6 +35,12 @@ func _run() -> void: game.cam.look_at(game.car.global_position + Vector3.UP * 0.4) await process_frame await process_frame + elif opts.get("cam", "") == "rear": + game.set_process(false) + game.cam.global_position = game.car.global_position + game.car.global_basis * Vector3(2.6, 1.6, 6.5) + game.cam.look_at(game.car.global_position + Vector3.UP * 0.4) + await process_frame + await process_frame elif opts.get("cam", "") == "top": game.set_process(false) var alt := float(opts.get("alt", "150")) diff --git a/tools/build_cars.py b/tools/build_cars.py index 39df500..01e3213 100644 --- a/tools/build_cars.py +++ b/tools/build_cars.py @@ -1,6 +1,7 @@ """Procedural Aussie shitbox factory. Run: /Applications/Blender.app/Contents/MacOS/Blender -b -P tools/build_cars.py + (append `-- [...]` to rebuild only those cars) Each car: side-silhouette profile extruded across the width (body + greenhouse), cylinder wheels, and separate det_* panel objects (doors, mirrors, bumpers, @@ -15,6 +16,7 @@ side-on photos per car when a shape needs to get closer. """ import os import math +import sys import bpy import bmesh from mathutils import Matrix @@ -30,7 +32,9 @@ 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. -# Optional: doors=2, bonnet_color (two-tone accent), roof_cap (vinyl roof), spoiler. +# Optional: doors=2, bonnet_color (two-tone accent), roof_cap (vinyl roof), spoiler, +# louvres (hatch-glass slat stack, each slat sheds on impact), indicators (amber +# corner blinkers), side_strip (black rubbing strip along the belt line). 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, @@ -84,6 +88,13 @@ CARS = { ohang_f=0.85, ohang_r=0.95, wheel=0.29, paint=(0.70, 0.72, 0.58, 1), chrome_bumpers=True, hatch=False, doors=2, van=True), + # John's actual first car: '82 KA Laser 1.5 hatch, white, rear louvres. + # Gutless IRL (54 kW carby) -> reversed-stats rule makes it a weapon here. + "lazza_ka": dict(L=3.96, W=1.61, nose=0.58, belt=0.79, tail=0.90, + bonnet=1.02, boot=0.35, roof=1.37, roof_rear=0.72, + ohang_f=0.79, ohang_r=0.72, wheel=0.26, + paint=(0.92, 0.92, 0.89, 1), chrome_bumpers=False, hatch=True, + louvres=True, indicators=True, side_strip=True, wide_grille=True), # mokes use the open-tub builder: style="moke" "mini_merk": dict(style="moke", L=3.05, W=1.42, nose=0.60, tub=0.66, canopy=1.38, bonnet=0.85, ohang_f=0.55, ohang_r=0.55, wheel=0.26, @@ -215,6 +226,19 @@ def build(car_id, s): 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)) + if s.get("louvres") and s["hatch"]: + # venetian-blind slat stack over the hatch glass; every slat is its own + # det_ part, so a rear-ender sheds them one by one like a dropped pie + ty, tz = boot_y + s["roof_rear"], s["roof"] # glass top edge + ly, lz = rr_y, rr_z # glass bottom edge + g_ang = math.atan2(tz - lz, ty - ly) + for i in range(5): + t = 0.14 + i * 0.18 + sy = ty + (ly - ty) * t + sz = tz + (lz - tz) * t + 0.045 + parts.append(box("det_louvre_%d" % i, (0, sy, sz), + (W * 0.80, 0.20, 0.022), dark, rot_x=-g_ang * 0.45)) + if s.get("van"): # cargo box from cab rear to tail, plus rip-off tailgate parts.append(box("van_box", (0, (boot_y + r) / 2, (s["belt"] + s["roof"]) / 2), (W * 0.94, boot_y - r - 0.05, s["roof"] - s["belt"]), paint)) @@ -240,13 +264,23 @@ def build(car_id, s): (W * 0.86, cap_f - cap_r + 0.1, 0.035), material("vinyl", s["roof_cap"], rough=0.9))) if s.get("spoiler"): # bootlid ducktail trim.append(box("trim_spoiler", (0, r + 0.22, s["tail"] + 0.05), (W * 0.8, 0.28, 0.09), paint)) + grille_w = W * (0.78 if s.get("wide_grille") else 0.5) # wide = lights sit in a dark bezel band trim += [ - box("trim_grille", (0, f + 0.01, s["nose"] - 0.14), (W * 0.5, 0.05, 0.12), dark), + box("trim_grille", (0, f + 0.01, s["nose"] - 0.14), (grille_w, 0.05, 0.14), 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)), ] + if s.get("indicators"): + amber = material("amber", (0.95, 0.55, 0.05, 1)) + trim.append(box("trim_ind_l", (-W * 0.44, f + 0.02, s["nose"] - 0.13), (0.11, 0.05, 0.12), amber)) + trim.append(box("trim_ind_r", (W * 0.44, f + 0.02, s["nose"] - 0.13), (0.11, 0.05, 0.12), amber)) + if s.get("side_strip"): + strip_len = L * 0.80 + for sx in (-1, 1): + trim.append(box("trim_strip_%s" % ("l" if sx < 0 else "r"), + (sx * (hw + 0.015), -L * 0.02, 0.52), (0.03, strip_len, 0.055), dark)) ax_f, ax_r = f - s["ohang_f"], r + s["ohang_r"] wx = hw - 0.10 @@ -337,9 +371,14 @@ def build_moke(car_id, s): print("built", car_id, "->", path) +only = set(sys.argv[sys.argv.index("--") + 1:]) if "--" in sys.argv else set() +built = 0 for car_id, spec in CARS.items(): + if only and car_id not in only: + continue if spec.get("style") == "moke": build_moke(car_id, spec) else: build(car_id, spec) -print("done: %d cars" % len(CARS)) + built += 1 +print("done: %d cars" % built)