The dealgod.pro products database holds ~82k secondhand tool listings with photos (69k cached on the VPS). This turns the good ones into game assets: 15 tool classes curated from 45 candidates (socket set, air compressor, chain block, car ramp, OBD reader, trolley jack, engine crane, engine stand, toolbox, angle grinder, welder, jerry can, battery charger, jack stands, grease gun), each run through the MODELBEAST farm by the new tools/cc_to_glb.py: bg_remove_local cutout, then trellis_mac image->3D (1024 tier). Retries with backoff after a transient queue 401; the jerry can made trellis_mac exit 2 on two different photos and went through hunyuan3d_mlx instead (--operator flag), which ate it happily. Raw TRELLIS output is ~20MB per prop, so tools/slim_glb.py (headless Blender) decimates to 18k tris and re-exports with 1024 JPEG textures: the library lands at 27MB total for 15 props instead of ~300MB. The Willowbank paddock strews the whole library as RigidBodies -- TOOL_SPECS in dragway.gd gives each class its real-world size (TRELLIS output is unit-ish; the AABB is measured live and scaled) and an honest mass, so clipping the 70kg engine crane is a different event to punting the grease gun. The loader is directory-driven: drop a new GLB into assets/tools/ and it appears in the paddock next boot. Textures inherit whatever branding was in the source photos -- fine for secondhand background clutter, flagged in the README for anything that gets promoted to hero status. Smoke suite asserts every GLB in the library spawns as a prop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
335 lines
13 KiB
GDScript
335 lines
13 KiB
GDScript
extends Node3D
|
|
## Willowbank-ish quarter mile, built in code like the carpark. A real 402 m of
|
|
## strip plus shutdown, sand trap and tyre wall, with a working christmas tree
|
|
## (game.gd drives the bulbs via their material_override emission).
|
|
##
|
|
## Geometry contract with game.gd's drag mode: DragStart sits at (0,0,0) in the
|
|
## .tscn and DragEnd at (0,0,-402); the player spawns at x=-2, the rival at
|
|
## x=+2, both facing -Z. The tree stands between the lanes at z=-6, visual only
|
|
## -- collision there would turn a wandering launch into a head-on with a pole.
|
|
|
|
const TEX := "res://assets/textures/%s.jpg"
|
|
|
|
const STRIP_HALF := 12.0 # wall-to-wall half width
|
|
const FINISH := -402.0
|
|
const SHUTDOWN_END := -640.0
|
|
|
|
## Real Cashies stock, TRELLIS'd into GLBs (tools/cc_to_glb.py). Scale is the
|
|
## longest dimension in metres; mass makes clipping an engine crane at 80
|
|
## a different event to punting a grease gun.
|
|
const TOOL_SPECS := {
|
|
"angle_grinder": {"size": 0.35, "mass": 3.0},
|
|
"batt_charger": {"size": 0.28, "mass": 4.0},
|
|
"car_ramps": {"size": 0.95, "mass": 9.0},
|
|
"chain_hoist": {"size": 0.45, "mass": 12.0},
|
|
"compressor": {"size": 0.55, "mass": 14.0},
|
|
"engine_crane": {"size": 1.6, "mass": 70.0},
|
|
"engine_stand": {"size": 0.95, "mass": 30.0},
|
|
"grease_gun": {"size": 0.4, "mass": 2.0},
|
|
"jack_stands": {"size": 0.45, "mass": 8.0},
|
|
"jerry_can": {"size": 0.48, "mass": 6.0},
|
|
"obd_reader": {"size": 0.22, "mass": 1.0},
|
|
"socket_set": {"size": 0.4, "mass": 5.0},
|
|
"toolbox": {"size": 0.7, "mass": 18.0},
|
|
"trolley_jack": {"size": 0.65, "mass": 20.0},
|
|
"welder": {"size": 0.5, "mass": 16.0},
|
|
}
|
|
|
|
func _ready() -> void:
|
|
var rng := RandomNumberGenerator.new()
|
|
rng.seed = 1320 # a respectable ET
|
|
|
|
_slab(Vector3(0, -0.5, -280), Vector3(420, 1, 900), _mat("grass", 14.0))
|
|
# the strip: asphalt base, concrete launch pad, rubbered lanes down-track
|
|
_slab(Vector3(0, -0.45, -290), Vector3(STRIP_HALF * 2, 1, 740), _mat("asphalt", 9.0))
|
|
_slab(Vector3(0, 0.065, -40), Vector3(STRIP_HALF * 2, 0.03, 80), _mat("concrete_ground", 4.0), false)
|
|
for lx in [-4.0, 4.0]:
|
|
_slab(Vector3(lx, 0.062, -320), Vector3(7.5, 0.02, 480), _mat("dragstrip", 5.0), false)
|
|
# centre line (double yellow) and edge lines
|
|
var yellow := _flat(Color(0.85, 0.72, 0.18), 0.7)
|
|
var white := _flat(Color(0.88, 0.88, 0.84), 0.7)
|
|
for cx in [-0.18, 0.18]:
|
|
_slab(Vector3(cx, 0.07, -305), Vector3(0.12, 0.02, 670), yellow, false)
|
|
for ex in [-10.8, 10.8]:
|
|
_slab(Vector3(ex, 0.07, -305), Vector3(0.14, 0.02, 670), white, false)
|
|
|
|
# concrete walls with alternating red/white caps, like every strip on earth
|
|
var conc := _mat("concrete_ground", 3.0)
|
|
for sx in [-1.0, 1.0]:
|
|
_slab(Vector3(sx * STRIP_HALF, 0.475, -315), Vector3(0.5, 0.95, 690), conc)
|
|
for i in 40:
|
|
var z := 20.0 - i * 17.0
|
|
_slab(Vector3(sx * STRIP_HALF, 0.99, z), Vector3(0.54, 0.08, 8.0),
|
|
_flat(Color(0.78, 0.12, 0.10) if i % 2 == 0 else Color(0.9, 0.9, 0.88), 0.6), false)
|
|
|
|
_tree()
|
|
_start_line()
|
|
_finish_line()
|
|
_boards()
|
|
_grandstand()
|
|
_tower()
|
|
_sand_trap()
|
|
|
|
# return road up the right-hand side, back to the staging paddock
|
|
_slab(Vector3(19.0, 0.04, -290), Vector3(8, 0.02, 700), _mat("asphalt", 9.0), false)
|
|
|
|
# staging paddock: PP_ markers become shuntable parked fleet cars (game.gd)
|
|
for i in 8:
|
|
var m := Marker3D.new()
|
|
m.name = "PP_pad%d" % i
|
|
m.position = Vector3(20.0 + 5.5 * (i % 4), 0.0, 22.0 + 8.0 * (i / 4))
|
|
m.rotate_y(PI * 0.5 + rng.randf_range(-0.08, 0.08))
|
|
add_child(m)
|
|
# lighting towers as SL_ markers: game.gd builds them as smashable street
|
|
# lights that really cast light at night. Yes, you can flatten them.
|
|
var sl := 0
|
|
for z in [-80.0, -220.0, -360.0, -500.0]:
|
|
for sx in [-1.0, 1.0]:
|
|
var m := Marker3D.new()
|
|
m.name = "SL_%d" % sl
|
|
sl += 1
|
|
m.position = Vector3(sx * (STRIP_HALF + 2.0), 0.0, z)
|
|
add_child(m)
|
|
# wheelie bins behind the wall because there is always a row of wheelie bins
|
|
for i in 6:
|
|
var m := Marker3D.new()
|
|
m.name = "WB_%d" % i
|
|
m.position = Vector3(14.5, 0.0, 8.0 - i * 3.0)
|
|
add_child(m)
|
|
|
|
_gums(rng)
|
|
_paddock_tools(rng)
|
|
_race_path()
|
|
|
|
func _mat(tex: String, scale: float, rough := 0.9) -> StandardMaterial3D:
|
|
var m := StandardMaterial3D.new()
|
|
m.albedo_texture = load(TEX % tex)
|
|
m.uv1_scale = Vector3(scale, scale, scale)
|
|
m.uv1_triplanar = true
|
|
m.roughness = rough
|
|
m.metallic = 0.0
|
|
return m
|
|
|
|
func _flat(color: Color, rough := 0.85) -> StandardMaterial3D:
|
|
var m := StandardMaterial3D.new()
|
|
m.albedo_color = color
|
|
m.roughness = rough
|
|
m.metallic = 0.0
|
|
return m
|
|
|
|
func _slab(pos: Vector3, size: Vector3, mat: Material, collide := true) -> void:
|
|
var body: PhysicsBody3D = StaticBody3D.new()
|
|
var mi := MeshInstance3D.new()
|
|
var bm := BoxMesh.new()
|
|
bm.size = size
|
|
bm.material = mat
|
|
mi.mesh = bm
|
|
body.add_child(mi)
|
|
if collide:
|
|
var shape := CollisionShape3D.new()
|
|
var bs := BoxShape3D.new()
|
|
bs.size = size
|
|
shape.shape = bs
|
|
body.add_child(shape)
|
|
body.position = pos
|
|
add_child(body)
|
|
|
|
func _tree() -> void:
|
|
## The christmas tree: pole + backboard + six named bulbs. game.gd finds
|
|
## them by name and flips material_override.emission_enabled. Visual only.
|
|
var steel := _flat(Color(0.22, 0.23, 0.25), 0.5)
|
|
_slab(Vector3(0, 1.1, -6), Vector3(0.14, 2.2, 0.14), steel, false)
|
|
_slab(Vector3(0, 2.0, -6), Vector3(0.55, 1.75, 0.10), _flat(Color(0.10, 0.10, 0.12), 0.6), false)
|
|
var bulbs := [
|
|
["TreeStage", 2.68, Color(0.9, 0.9, 0.8)],
|
|
["TreeAmber1", 2.38, Color(1.0, 0.68, 0.05)],
|
|
["TreeAmber2", 2.12, Color(1.0, 0.68, 0.05)],
|
|
["TreeAmber3", 1.86, Color(1.0, 0.68, 0.05)],
|
|
["TreeGreen", 1.58, Color(0.15, 0.95, 0.25)],
|
|
["TreeRed", 1.30, Color(0.95, 0.10, 0.08)],
|
|
]
|
|
for b in bulbs:
|
|
var mi := MeshInstance3D.new()
|
|
var sm := SphereMesh.new()
|
|
sm.radius = 0.105
|
|
sm.height = 0.21
|
|
sm.radial_segments = 12
|
|
sm.rings = 6
|
|
mi.mesh = sm
|
|
var m := StandardMaterial3D.new()
|
|
m.albedo_color = Color(b[2]) * 0.35 # dim housing until it fires
|
|
m.roughness = 0.4
|
|
m.emission_enabled = false
|
|
m.emission = Color(b[2])
|
|
m.emission_energy_multiplier = 3.5
|
|
mi.material_override = m
|
|
mi.name = String(b[0])
|
|
mi.position = Vector3(0, float(b[1]), -6.06)
|
|
add_child(mi)
|
|
|
|
func _start_line() -> void:
|
|
var white := _flat(Color(0.9, 0.9, 0.86), 0.7)
|
|
_slab(Vector3(0, 0.07, 0), Vector3(STRIP_HALF * 2 - 1.6, 0.02, 0.4), white, false)
|
|
# burnout box water patches behind the line
|
|
for lx in [-4.0, 4.0]:
|
|
_slab(Vector3(lx, 0.068, 8.0), Vector3(5.0, 0.015, 9.0),
|
|
_flat(Color(0.10, 0.11, 0.13), 0.15), false)
|
|
|
|
func _finish_line() -> void:
|
|
# checkered strip + scoreboard gantry
|
|
for row in 2:
|
|
for i in 11:
|
|
var c := Color(0.05, 0.05, 0.06) if (i + row) % 2 == 0 else Color(0.92, 0.92, 0.9)
|
|
_slab(Vector3(-10.0 + i * 2.0, 0.07, FINISH - row * 2.0),
|
|
Vector3(2.0, 0.02, 2.0), _flat(c, 0.7), false)
|
|
var steel := _flat(Color(0.25, 0.26, 0.28), 0.5)
|
|
for sx in [-1.0, 1.0]:
|
|
_slab(Vector3(sx * (STRIP_HALF + 0.8), 3.5, FINISH), Vector3(0.35, 7.0, 0.35), steel)
|
|
_slab(Vector3(0, 7.1, FINISH), Vector3(STRIP_HALF * 2 + 2.5, 0.45, 0.45), steel, false)
|
|
for sx in [-5.0, 5.0]:
|
|
_slab(Vector3(sx, 5.9, FINISH), Vector3(4.5, 2.0, 0.3), _mat("billboard_ad3", 0.22, 0.4), false)
|
|
|
|
func _boards() -> void:
|
|
# distance boards: 60ft, eighth, 1000ft -- the numbers that matter
|
|
var posts := _flat(Color(0.85, 0.85, 0.8), 0.6)
|
|
for z in [-18.3, -201.2, -305.0]:
|
|
for sx in [-1.0, 1.0]:
|
|
_slab(Vector3(sx * (STRIP_HALF + 1.2), 1.0, z), Vector3(0.16, 2.0, 0.16), posts, false)
|
|
_slab(Vector3(sx * (STRIP_HALF + 1.2), 2.2, z), Vector3(1.3, 0.9, 0.1), posts, false)
|
|
|
|
func _grandstand() -> void:
|
|
## Six stepped tiers with FLUX crowd panels on the risers and a white canopy.
|
|
var conc := _mat("concrete_ground", 3.0)
|
|
for i in 6:
|
|
var x := -16.5 - i * 2.6
|
|
var y := 0.55 + i * 0.85
|
|
_slab(Vector3(x, y * 0.5, -80), Vector3(2.6, y, 120), conc)
|
|
_slab(Vector3(x + 1.28, y + 0.75, -80), Vector3(0.08, 1.5, 118), _mat("crowd", 7.0, 0.9), false)
|
|
var steel := _flat(Color(0.3, 0.31, 0.33), 0.5)
|
|
for zi in 5:
|
|
_slab(Vector3(-24.0, 3.8, -128 + zi * 24.0), Vector3(0.3, 7.6, 0.3), steel, false)
|
|
_slab(Vector3(-25.5, 7.6, -80), Vector3(20.0, 0.25, 124), _mat("panel_white", 0.5, 0.5), false)
|
|
|
|
func _tower() -> void:
|
|
## The red control tower: every Queensland circuit building is this shade.
|
|
var red := _flat(Color(0.72, 0.10, 0.08), 0.55)
|
|
var glass := _mat("facade_glass", 0.2, 0.25)
|
|
_slab(Vector3(21.0, 6.0, -14), Vector3(9.0, 12.0, 8.0), red)
|
|
_slab(Vector3(21.0, 10.4, -9.95), Vector3(8.2, 2.6, 0.2), glass, false)
|
|
_slab(Vector3(21.0, 12.3, -14), Vector3(9.6, 0.4, 8.6), _flat(Color(0.16, 0.17, 0.19), 0.6), false)
|
|
_slab(Vector3(21.0, 7.4, -9.9), Vector3(8.2, 1.6, 0.15), _mat("billboard_ad", 0.3, 0.4), false)
|
|
# paddock awnings behind it
|
|
for i in 3:
|
|
_slab(Vector3(30.0, 2.9, -30.0 - i * 14.0), Vector3(7.0, 0.2, 10.0), _mat("panel_white", 0.5, 0.5), false)
|
|
for sx in [-3.0, 3.0]:
|
|
for sz in [-4.5, 4.5]:
|
|
_slab(Vector3(30.0 + sx, 1.45, -30.0 - i * 14.0 + sz), Vector3(0.15, 2.9, 0.15),
|
|
_flat(Color(0.5, 0.5, 0.52), 0.5), false)
|
|
|
|
func _sand_trap() -> void:
|
|
_slab(Vector3(0, 0.055, -668), Vector3(STRIP_HALF * 2, 0.03, 56), _mat("sand", 5.0), false)
|
|
# tyre wall: fat black cylinders in a row, then an honest collision slab
|
|
var rubber := _flat(Color(0.07, 0.07, 0.08), 0.85)
|
|
for i in 17:
|
|
var mi := MeshInstance3D.new()
|
|
var cm := CylinderMesh.new()
|
|
cm.top_radius = 0.62
|
|
cm.bottom_radius = 0.62
|
|
cm.height = 1.15
|
|
cm.radial_segments = 10
|
|
cm.material = rubber
|
|
mi.mesh = cm
|
|
mi.position = Vector3(-11.2 + i * 1.4, 0.58, -694)
|
|
add_child(mi)
|
|
_slab(Vector3(0, 1.0, -695.2), Vector3(STRIP_HALF * 2 + 2, 2.0, 1.0), rubber)
|
|
|
|
func _gums(rng: RandomNumberGenerator) -> void:
|
|
for i in 26:
|
|
var sx := -1.0 if i % 2 == 0 else 1.0
|
|
var at := Vector3(sx * rng.randf_range(30.0, 80.0), 0, rng.randf_range(-620.0, 40.0))
|
|
var trunk := MeshInstance3D.new()
|
|
var cm := CylinderMesh.new()
|
|
cm.top_radius = 0.14
|
|
cm.bottom_radius = 0.24
|
|
cm.height = rng.randf_range(5.0, 8.0)
|
|
cm.radial_segments = 7
|
|
cm.material = _mat("bark", 1.4)
|
|
trunk.mesh = cm
|
|
trunk.position = at + Vector3(0, cm.height * 0.5, 0)
|
|
add_child(trunk)
|
|
var crown := MeshInstance3D.new()
|
|
var sm := SphereMesh.new()
|
|
sm.radius = rng.randf_range(2.2, 3.4)
|
|
sm.height = sm.radius * 1.6
|
|
sm.radial_segments = 10
|
|
sm.rings = 6
|
|
sm.material = _mat("foliage", 0.55)
|
|
crown.mesh = sm
|
|
crown.position = at + Vector3(0, cm.height + sm.radius * 0.4, 0)
|
|
add_child(crown)
|
|
|
|
func _paddock_tools(rng: RandomNumberGenerator) -> void:
|
|
## Strew the paddock with the TRELLIS'd Cashies tool library -- real
|
|
## secondhand clutter, every piece a shuntable RigidBody. Scale comes from
|
|
## TOOL_SPECS (TRELLIS output is unit-ish), base sat on the ground.
|
|
var dir := DirAccess.open("res://assets/tools")
|
|
if dir == null:
|
|
return
|
|
var names: Array[String] = []
|
|
for f in dir.get_files():
|
|
if f.ends_with(".glb"):
|
|
names.append(f.get_basename())
|
|
names.sort() # deterministic layout
|
|
var i := 0
|
|
for n in names:
|
|
var scene := load("res://assets/tools/%s.glb" % n) as PackedScene
|
|
if scene == null:
|
|
continue
|
|
var spec: Dictionary = TOOL_SPECS.get(n, {"size": 0.5, "mass": 8.0})
|
|
var model := scene.instantiate() as Node3D
|
|
add_child(model) # into the tree first so global AABBs are real
|
|
var aabb := AABB()
|
|
var first := true
|
|
for mi in model.find_children("*", "MeshInstance3D", true, false):
|
|
var b: AABB = (mi as MeshInstance3D).global_transform * (mi as MeshInstance3D).get_aabb()
|
|
aabb = b if first else aabb.merge(b)
|
|
first = false
|
|
remove_child(model)
|
|
if first:
|
|
model.queue_free()
|
|
continue
|
|
var s: float = float(spec["size"]) / maxf(aabb.size.x, maxf(aabb.size.y, aabb.size.z))
|
|
var body := RigidBody3D.new()
|
|
body.mass = float(spec["mass"])
|
|
body.set_meta("cc_tool", n)
|
|
var cs := CollisionShape3D.new()
|
|
var box := BoxShape3D.new()
|
|
box.size = (aabb.size * s).clamp(Vector3.ONE * 0.08, Vector3.ONE * 3.0)
|
|
cs.shape = box
|
|
cs.position = aabb.get_center() * s
|
|
body.add_child(cs)
|
|
model.scale = Vector3.ONE * s
|
|
body.add_child(model)
|
|
add_child(body)
|
|
var spot := Vector3(24.0 + (i % 4) * 4.5 + rng.randf_range(-0.8, 0.8),
|
|
0.02 - aabb.position.y * s,
|
|
-16.0 - float(i / 4) * 7.0 + rng.randf_range(-0.8, 0.8))
|
|
body.global_position = spot
|
|
body.rotate_y(rng.randf() * TAU)
|
|
body.sleeping = true
|
|
i += 1
|
|
|
|
func _race_path() -> void:
|
|
# loop for cruise traffic: down the strip, back up the return road
|
|
var path := Path3D.new()
|
|
path.name = "RacePath"
|
|
var c := Curve3D.new()
|
|
var pts := [Vector3(0, 0, 40), Vector3(0, 0, -620), Vector3(19, 0, -620), Vector3(19, 0, 40)]
|
|
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() * 8.0
|
|
var to_next: Vector3 = (pts[(i + 1) % pts.size()] - p).normalized() * 8.0
|
|
c.add_point(p, to_prev, to_next)
|
|
path.curve = c
|
|
add_child(path)
|