FLOORED LAZZA 1.5: John's actual first car, '82 KA Laser hatch

Built from his photos: white 5-door wedge, black bumpers + belt-line rubbing
strip, amber corner indicators, wide dark grille bezel (pale headlights vanish
on white paint without it), and the crown jewel -- rear hatch louvres, five
slats that shed one by one on impact like a dropped pie. New optional spec
keys: louvres, indicators, side_strip, wide_grille; build_cars.py now takes
`-- <id>` to rebuild a single car; screenshot harness gains cam=rear.

Reversed-stats rule applied: 54 kW carby IRL -> 21.0 power / 860 kg / 7.8 grip
here. Second-quickest thing in the game behind the Excel, and the chuckable-est.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-28 16:16:32 +10:00
parent da63829e9e
commit 81ed13b1ff
6 changed files with 110 additions and 5 deletions

View File

@ -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). - 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 - Objects named `det_*` are detachable — the game rips them off on impact
(doors, mirrors, bumpers, bonnet, boot). (doors, mirrors, bumpers, bonnet, boot).
- Current fleet: Datto 120Y, Gemmy TX, Kingswood HQ, VL Terbo, XF Fungoon, Excel Hilton. - Current fleet: 30 shitboxes, from the Datto 120Y to the FLOORED LAZZA 1.5 ('82
Piss-take names on purpose: real model names and badges are trademarks. 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 To improve a silhouette: supply a side-on photo + front three-quarter photo, tweak the
profile points in the spec, rebuild, screenshot. profile points in the spec, rebuild, screenshot.

BIN
cars/lazza_ka/car.glb Normal file

Binary file not shown.

View File

@ -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

13
cars/lazza_ka/stats.tres Normal file
View File

@ -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

View File

@ -35,6 +35,12 @@ func _run() -> void:
game.cam.look_at(game.car.global_position + Vector3.UP * 0.4) game.cam.look_at(game.car.global_position + Vector3.UP * 0.4)
await process_frame await process_frame
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": elif opts.get("cam", "") == "top":
game.set_process(false) game.set_process(false)
var alt := float(opts.get("alt", "150")) var alt := float(opts.get("alt", "150"))

View File

@ -1,6 +1,7 @@
"""Procedural Aussie shitbox factory. """Procedural Aussie shitbox factory.
Run: /Applications/Blender.app/Contents/MacOS/Blender -b -P tools/build_cars.py Run: /Applications/Blender.app/Contents/MacOS/Blender -b -P tools/build_cars.py
(append `-- <id> [<id>...]` to rebuild only those cars)
Each car: side-silhouette profile extruded across the width (body + greenhouse), Each car: side-silhouette profile extruded across the width (body + greenhouse),
cylinder wheels, and separate det_* panel objects (doors, mirrors, bumpers, 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 os
import math import math
import sys
import bpy import bpy
import bmesh import bmesh
from mathutils import Matrix 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, # L, W, heights: nose/belt/tail, lengths: bonnet/boot, roof height + rear roof end,
# axle overhangs, wheel radius, paint, chrome bumpers, hatch rear. # 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 = { CARS = {
"datto_120y": dict(L=3.92, W=1.50, nose=0.62, belt=0.78, tail=0.72, "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, 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, ohang_f=0.85, ohang_r=0.95, wheel=0.29,
paint=(0.70, 0.72, 0.58, 1), chrome_bumpers=True, hatch=False, paint=(0.70, 0.72, 0.58, 1), chrome_bumpers=True, hatch=False,
doors=2, van=True), 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" # 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, "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, 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_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)) 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 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), 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)) (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))) (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 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)) 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 += [ 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_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_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_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)), 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"] ax_f, ax_r = f - s["ohang_f"], r + s["ohang_r"]
wx = hw - 0.10 wx = hw - 0.10
@ -337,9 +371,14 @@ def build_moke(car_id, s):
print("built", car_id, "->", path) 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(): for car_id, spec in CARS.items():
if only and car_id not in only:
continue
if spec.get("style") == "moke": if spec.get("style") == "moke":
build_moke(car_id, spec) build_moke(car_id, spec)
else: else:
build(car_id, spec) build(car_id, spec)
print("done: %d cars" % len(CARS)) built += 1
print("done: %d cars" % built)