Two proper venues off the real Ipswich pair, both built in code. Willowbank Dragway (levels/willowbank_dragway): a true 402 m dual-lane strip -- concrete launch pad, FLUX-rubbered lanes, crowd grandstand with canopy, red control tower, distance boards at 60ft/eighth/1000ft, checkered finish under a scoreboard gantry, sand trap and tyre wall, smashable SL_ light towers and a staging paddock of parked fleet cars. Between the lanes: a working christmas tree, six bulbs driven by game.gd through material_override emission. Drag mode grew the event around it. Every drag now stages at the tree (three ambers, green at 2.7 s); rolling >2 m early is a RED LIGHT that bogs your launch. The opposition is a six-rung ladder of preset locals (Nanna's Shopping Run through The Willowbank Widowmaker), each with its own fleet car, power and reaction time. Beating your current rung climbs the ladder and pays an escalating purse ($250-$1600, top dog repeats); crossing at all pays $50; and every car keeps a persistent quarter-mile PB worth $150 when beaten. Losing no longer ends the run early -- you still cross for your ET, with a 10 s grace if you dawdle. Ladder rung and PBs persist in the garage save (schema extended compatibly). Queensland Raceway (levels/qld_raceway): the National Circuit digitised from the official track map and scaled to its exact 3126 m lap. The road is one generated quad strip with trimesh collision; kerbs, gravel traps and tyre stacks derive automatically from centreline curvature, sponsor hoardings space themselves down the straights, and the SF straight gets the checkered line, gantry, pit wall and the mandatory red pit complex. Plus the pond. Race/cruise/rage all run on its RacePath. Trap for the diary: Godot front faces wind CLOCKWISE. The first cut of the road ribbon wound CCW and rendered invisible from above while colliding perfectly -- the smoke suite passed and the screenshot showed four cars gridded on open grass. Three new FLUX textures (crowd, dragstrip, sand; the sand prompt tripped Cloudflare's NSFW filter, of all things, and fell back to local). Smoke suite covers tree staging, the held rival, rung-0 identity, green-bulb emission, red-light innocence, PB/rung persistence and the 3126 m lap. Also swept stale shot_*.png.import debris and ignored the pattern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
262 lines
9.9 KiB
GDScript
262 lines
9.9 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
|
|
|
|
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)
|
|
_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 _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)
|