ShitboxInfinity/levels/qld_raceway/raceway.gd
m3ultra 21032191a3 Raceday: Willowbank quarter mile + the Queensland Raceway Paperclip
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>
2026-07-31 11:43:08 +10:00

374 lines
13 KiB
GDScript

extends Node3D
## Queensland Raceway's National Circuit -- "the Paperclip" -- built in code.
## The centreline is digitised from the official track map (pixel coords) and
## scaled so the lap is exactly 3126 m, the real National Circuit length.
## Three tilted parallel straights, two outer hairpins, the inner loop and the
## T6 fishhook. Corners get kerbs, sand traps and tyre stacks automatically
## from curvature; straights get billboard hoardings.
##
## Race mode picks up the RacePath (same curve) and grids the field on it.
const TEX := "res://assets/textures/%s.jpg"
const LAP_M := 3126.0
const ROAD_W := 13.0
const STEP := 4.0 # road-mesh sampling, metres
## National Circuit red line off the track map, in image pixels (x, y-down).
## Drive order: SF heading west -> T1 -> west straight -> T2 hairpin -> top
## straight east -> east 180 -> middle straight west -> inner loop left ->
## third straight east -> T6 fishhook -> front straight -> SF.
const CENTRE_PX := [
[680, 795], [300, 800], [150, 782], [85, 720], [70, 620], [68, 400],
[85, 290], [130, 235], [220, 215], [700, 262], [1290, 335],
[1345, 372], [1352, 420], [1300, 452], [900, 470], [460, 485],
[400, 505], [375, 545], [385, 590], [430, 622], [520, 640],
[800, 680], [1080, 718], [1150, 720], [1200, 748], [1195, 782],
[1140, 795],
]
var curve: Curve3D
func _ready() -> void:
var rng := RandomNumberGenerator.new()
rng.seed = 3126
curve = _build_curve()
var samples := _samples()
# grass world sized off the track's bounding box
var lo := Vector3(INF, 0, INF)
var hi := Vector3(-INF, 0, -INF)
for s in samples:
lo.x = minf(lo.x, s["pos"].x); lo.z = minf(lo.z, s["pos"].z)
hi.x = maxf(hi.x, s["pos"].x); hi.z = maxf(hi.z, s["pos"].z)
var mid := (lo + hi) * 0.5
var span := hi - lo
_slab(Vector3(mid.x, -0.5, mid.z), Vector3(span.x + 260, 1, span.z + 260), _mat("grass", 18.0))
_road(samples)
var corners := _corners(samples)
_kerbs(samples)
for c in corners:
_corner_dressing(c, samples)
_hoardings(samples, corners)
_start_finish(samples)
_pits(samples)
# the pond, as seen from every aerial photo of the place
var pond := Vector3(lo.x + span.x * 0.25, 0.03, lo.z + span.z * 0.37)
_slab(pond, Vector3(55, 0.03, 38), _flat(Color(0.13, 0.25, 0.30), 0.15), false)
_gums(rng, samples, lo, hi)
var path := Path3D.new()
path.name = "RacePath"
path.curve = curve
add_child(path)
var spawn := Marker3D.new()
spawn.name = "Spawn"
var t0: Dictionary = samples[3]
spawn.transform = Transform3D(Basis.looking_at(t0["tan"], Vector3.UP),
t0["pos"] + Vector3.UP * 0.5)
add_child(spawn)
func _build_curve() -> Curve3D:
# raw pixel loop -> catmull tangents -> measure -> scale to LAP_M exactly
var raw: Array[Vector3] = []
for p in CENTRE_PX:
raw.append(Vector3(p[0], 0, p[1]))
var c := Curve3D.new()
var n := raw.size()
for i in n + 1:
var p := raw[i % n]
var t: Vector3 = (raw[(i + 1) % n] - raw[(i - 1 + n) % n]) * 0.2
c.add_point(p, -t, t)
var scale := LAP_M / c.get_baked_length()
var sc := Curve3D.new()
for i in c.point_count:
sc.add_point(c.get_point_position(i) * scale,
c.get_point_in(i) * scale, c.get_point_out(i) * scale)
return sc
func _samples() -> Array:
## [{pos, tan, side, dist}] every STEP metres. side points to the LEFT of
## the drive direction.
var out: Array = []
var L := curve.get_baked_length()
var d := 0.0
while d < L:
var p := curve.sample_baked(d)
var q := curve.sample_baked(fmod(d + 2.0, L))
var tn := (q - p)
tn.y = 0.0
tn = tn.normalized() if tn.length_squared() > 0.0001 else Vector3.FORWARD
out.append({"pos": p, "tan": tn, "side": Vector3.UP.cross(tn).normalized(), "dist": d})
d += STEP
return out
func _road(samples: Array) -> void:
## One long quad strip with real collision -- cars live on this surface.
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
var h := Vector3.UP * 0.05
var n := samples.size()
for i in n:
var a: Dictionary = samples[i]
var b: Dictionary = samples[(i + 1) % n]
var al: Vector3 = a["pos"] + a["side"] * ROAD_W * 0.5 + h
var ar: Vector3 = a["pos"] - a["side"] * ROAD_W * 0.5 + h
var bl: Vector3 = b["pos"] + b["side"] * ROAD_W * 0.5 + h
var br: Vector3 = b["pos"] - b["side"] * ROAD_W * 0.5 + h
var va: float = a["dist"] * 0.14
var vb: float = (a["dist"] + STEP) * 0.14
# Godot front faces wind CLOCKWISE seen from the normal side: the first
# cut of this strip wound CCW and the entire road was culled from above
for v in [[al, Vector2(0, va)], [br, Vector2(1.8, vb)], [ar, Vector2(1.8, va)],
[al, Vector2(0, va)], [bl, Vector2(0, vb)], [br, Vector2(1.8, vb)]]:
st.set_normal(Vector3.UP)
st.set_uv(v[1])
st.add_vertex(v[0])
var mesh := st.commit()
var m := StandardMaterial3D.new()
m.albedo_texture = load(TEX % "asphalt")
m.roughness = 0.92
mesh.surface_set_material(0, m)
var body := StaticBody3D.new()
var mi := MeshInstance3D.new()
mi.mesh = mesh
body.add_child(mi)
var cs := CollisionShape3D.new()
cs.shape = mesh.create_trimesh_shape()
body.add_child(cs)
add_child(body)
func _curvature(samples: Array, i: int) -> float:
## signed heading change per metre; positive = turning left
var n := samples.size()
var a: Vector3 = samples[i]["tan"]
var b: Vector3 = samples[(i + 1) % n]["tan"]
return a.signed_angle_to(b, Vector3.UP) / STEP
func _corners(samples: Array) -> Array:
## contiguous runs of |curvature| > threshold, min 6 samples (24 m)
## -> [{from, to, sign}] as sample indices
var out: Array = []
var n := samples.size()
var run_start := -1
var run_sign := 0.0
for i in n:
var k := _curvature(samples, i)
if absf(k) > 0.010:
if run_start < 0:
run_start = i
run_sign = signf(k)
else:
if run_start >= 0 and i - run_start >= 6:
out.append({"from": run_start, "to": i - 1, "sign": run_sign})
run_start = -1
if run_start >= 0 and n - run_start >= 6:
out.append({"from": run_start, "to": n - 1, "sign": run_sign})
return out
func _kerbs(samples: Array) -> void:
## red/white kerb blocks along both edges wherever the road actually turns
var red := _flat(Color(0.78, 0.12, 0.10), 0.7)
var white := _flat(Color(0.90, 0.90, 0.86), 0.7)
var n := samples.size()
for i in n:
if absf(_curvature(samples, i)) < 0.008 or i % 2 != 0:
continue
var s: Dictionary = samples[i]
for e in [-1.0, 1.0]:
var at: Vector3 = s["pos"] + s["side"] * e * (ROAD_W * 0.5 + 0.7) + Vector3.UP * 0.045
var mi := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = Vector3(1.4, 0.07, STEP * 2.0)
bm.material = red if (i / 2) % 2 == 0 else white
mi.mesh = bm
mi.position = at
mi.basis = Basis.looking_at(s["tan"], Vector3.UP)
add_child(mi)
func _corner_dressing(c: Dictionary, samples: Array) -> void:
## outside of every real corner: a sand trap; inside: a tyre stack
var midi := int((int(c["from"]) + int(c["to"])) / 2.0)
var s: Dictionary = samples[midi]
# turning left (+sign) means the outside is the RIGHT edge (-side)
var out_dir: Vector3 = s["side"] * (-1.0 if float(c["sign"]) > 0.0 else 1.0)
var span := (int(c["to"]) - int(c["from"])) * STEP
if span > 40.0: # only proper corners earn a gravel trap
var at: Vector3 = s["pos"] + out_dir * (ROAD_W * 0.5 + 13.0)
var mi := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = Vector3(minf(span * 0.9, 60.0), 0.04, 26.0)
bm.material = _mat("sand", 5.0)
mi.mesh = bm
mi.position = Vector3(at.x, 0.035, at.z)
mi.basis = Basis.looking_at(s["tan"], Vector3.UP)
add_child(mi)
var rubber := _flat(Color(0.07, 0.07, 0.08), 0.85)
var inn: Vector3 = s["pos"] - out_dir * (ROAD_W * 0.5 + 3.5)
for j in 5:
var mi := MeshInstance3D.new()
var cm := CylinderMesh.new()
cm.top_radius = 0.58
cm.bottom_radius = 0.58
cm.height = 1.05
cm.radial_segments = 9
cm.material = rubber
mi.mesh = cm
mi.position = inn + s["tan"] * (j - 2.0) * 1.3 + Vector3.UP * 0.53
add_child(mi)
func _hoardings(samples: Array, corners: Array) -> void:
## sponsor boards down the straights, FLUX gibberish and all
var ads := ["billboard_ad", "billboard_ad2", "billboard_ad3"]
var n := samples.size()
var since := 0.0
var adi := 0
for i in n:
since += STEP
if since < 130.0 or absf(_curvature(samples, i)) > 0.004:
continue
since = 0.0
var s: Dictionary = samples[i]
var at: Vector3 = s["pos"] + s["side"] * (ROAD_W * 0.5 + 9.0)
var post := _flat(Color(0.3, 0.31, 0.33), 0.5)
for j in [-3.2, 3.2]:
_boxat(at + s["tan"] * j + Vector3.UP * 0.9, Vector3(0.18, 1.8, 0.18), post, s["tan"])
_boxat(at + Vector3.UP * 1.9, Vector3(8.0, 1.9, 0.12), _mat(ads[adi % 3], 0.24, 0.4), s["tan"])
adi += 1
func _start_finish(samples: Array) -> void:
var s: Dictionary = samples[0]
for row in 2:
for i in 9:
var c := Color(0.05, 0.05, 0.06) if (i + row) % 2 == 0 else Color(0.92, 0.92, 0.9)
var at: Vector3 = s["pos"] + s["side"] * (-6.0 + i * 1.5) + s["tan"] * float(row) * 1.5
_boxat(Vector3(at.x, 0.06, at.z), Vector3(1.5, 0.02, 1.5), _flat(c, 0.7), s["tan"])
var steel := _flat(Color(0.25, 0.26, 0.28), 0.5)
for e in [-1.0, 1.0]:
_boxat(s["pos"] + s["side"] * e * (ROAD_W * 0.5 + 1.2) + Vector3.UP * 3.5,
Vector3(0.35, 7.0, 0.35), steel, s["tan"])
_boxat(s["pos"] + Vector3.UP * 6.6, Vector3(ROAD_W + 4.5, 1.6, 0.3),
_mat("billboard_ad2", 0.22, 0.4), s["tan"])
func _pits(samples: Array) -> void:
## the red pit complex on the outside of the front straight: garage row,
## pit wall, paddock awnings, a couple of transporters
var s: Dictionary = samples[mini(20, samples.size() - 1)] # ~80 m up-track of SF
var out_dir: Vector3 = -s["side"] # outside of the circuit on the front straight
var red := _flat(Color(0.72, 0.10, 0.08), 0.55)
var base: Vector3 = s["pos"] + out_dir * (ROAD_W * 0.5 + 22.0)
_boxat(base + Vector3.UP * 4.0, Vector3(14, 8.0, 95), red, s["tan"], true)
_boxat(base + Vector3.UP * 8.2, Vector3(15, 0.4, 98), _flat(Color(0.16, 0.17, 0.19), 0.6), s["tan"])
_boxat(base - out_dir * 7.1 + Vector3.UP * 5.9, Vector3(0.2, 2.2, 88),
_mat("facade_glass", 0.2, 0.25), s["tan"])
_boxat(base - out_dir * 7.1 + Vector3.UP * 2.0, Vector3(0.25, 3.4, 88),
_mat("shopfront", 0.2, 0.4), s["tan"])
# pit wall between track and pit lane
_boxat(s["pos"] + out_dir * (ROAD_W * 0.5 + 2.5) + Vector3.UP * 0.45,
Vector3(0.4, 0.9, 130), _mat("concrete_ground", 3.0), s["tan"], true)
# paddock: white awnings + transporters behind the garages
var wt := _mat("panel_white", 0.5, 0.5)
for j in 4:
_boxat(base + out_dir * 16.0 + s["tan"] * (-33.0 + j * 22.0) + Vector3.UP * 2.9,
Vector3(9, 0.2, 7), wt, s["tan"])
for j in 2:
_boxat(base + out_dir * 27.0 + s["tan"] * (-12.0 + j * 26.0) + Vector3.UP * 1.9,
Vector3(2.5, 3.8, 13.5), _flat(Color(0.82, 0.83, 0.86), 0.4), s["tan"], true)
func _gums(rng: RandomNumberGenerator, samples: Array, lo: Vector3, hi: Vector3) -> void:
## eucalypt scatter anywhere that's 25 m clear of the racing line
var pts: Array[Vector3] = []
for i in range(0, samples.size(), 3):
pts.append(samples[i]["pos"])
var placed := 0
var tries := 0
while placed < 40 and tries < 400:
tries += 1
var at := Vector3(rng.randf_range(lo.x - 90, hi.x + 90), 0, rng.randf_range(lo.z - 90, hi.z + 90))
var ok := true
for p in pts:
if Vector2(at.x - p.x, at.z - p.z).length_squared() < 625.0:
ok = false
break
if not ok:
continue
placed += 1
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.5)
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.4, 3.6)
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 _boxat(pos: Vector3, size: Vector3, mat: Material, along: Vector3, collide := false) -> void:
var mi := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = size
bm.material = mat
mi.mesh = bm
if collide:
var body := StaticBody3D.new()
var cs := CollisionShape3D.new()
var bs := BoxShape3D.new()
bs.size = size
cs.shape = bs
body.add_child(cs)
body.add_child(mi)
body.position = pos
body.basis = Basis.looking_at(along, Vector3.UP)
add_child(body)
else:
mi.position = pos
mi.basis = Basis.looking_at(along, Vector3.UP)
add_child(mi)
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 := 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)