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)