ShitboxInfinity/levels/woolies_carpark/carpark.gd
m3ultra 82dae637a1 Scenery overhaul: real OSM street furniture, FLUX texture set v2, lighting pass
Builder now uses the furniture OSM already carries (footways, lamps, benches,
bins, bollards, bus stops, crossings): raised chamfered footpaths, shopfront
bands, awnings in three fabrics, parapets, layered fig/palm trees, zebra bars,
dashed centre lines, Queen St Mall winged canopies. ACES tonemap + SSAO + fog
in game.gd. flux_texture.sh gains size/luma clamping (FLUX renders "pale" as
blown-out white); gen_textures.sh + check_textures.py manage the set. Woolies
carpark rebuilt textured, parking the real fleet. Fixes: fractional OSM layer
tag crash, zero-length-normal UV projection, blood-red mall pavers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 14:13:29 +10:00

195 lines
7.2 KiB
GDScript

extends Node3D
## Woolies carpark, built in code rather than as a level.glb -- the parked
## shitboxes have to stay shuntable RigidBodies, which a static GLB can't carry.
## Everything else uses the same FLUX textures as the OSM levels.
##
## The Trolley Run junction (junctions.json) runs convoys along the aisles at
## z = 15/12 and z = -21/-24, from x = -70 to 70. Keep those lanes clear.
const TEX := "res://assets/textures/%s.jpg"
const BAY_ROWS := [-30.0, -12.0, 6.0, 24.0]
const BAY_X0 := -27.0
const BAY_STEP := 6.0
const BAYS := 10
func _ready() -> void:
var rng := RandomNumberGenerator.new()
rng.seed = 8
_slab(Vector3(0, -0.5, 0), Vector3(300, 1, 240), _mat("asphalt", 10.0)) # tarmac
for side in [-1, 1]: # kerb walls
_slab(Vector3(0, 0.4, side * 79.0), Vector3(240, 0.8, 2), _mat("concrete_ground", 3.0))
_slab(Vector3(side * 119.0, 0.4, 0), Vector3(2, 0.8, 160), _mat("concrete_ground", 3.0))
_bay_lines()
_store()
# park the actual fleet -- coloured boxes read as placeholder next to
# textured everything-else, and we already ship 29 shitbox models
var models: Array[String] = []
for id in Registry.cars():
var st: CarStats = load(Registry.cars()[id])
if st.model_path != "" and ResourceLoader.exists(st.model_path):
models.append(st.model_path)
models.sort() # deterministic
for row in BAY_ROWS.size():
for bay in BAYS:
if rng.randf() < 0.35:
continue
var pos := Vector3(BAY_X0 + bay * BAY_STEP, 0.75, BAY_ROWS[row])
_car(pos, models, rng)
for i in 6:
_light_pole(Vector3(-30.0 + i * 12.0, 0, 8.0))
for x in [-96.0, 96.0]: # planted islands
for j in 5:
_planter_island(Vector3(x, 0, -60.0 + j * 30.0), rng)
for p in [Vector3(-52, 0, 44), Vector3(52, 0, 44), Vector3(-52, 0, -46)]:
_trolley_bay(p)
_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 _car(pos: Vector3, models: Array[String], rng: RandomNumberGenerator) -> void:
var body := RigidBody3D.new()
body.mass = 300.0
var shape := CollisionShape3D.new()
var bs := BoxShape3D.new()
bs.size = Vector3(1.9, 1.3, 4.4)
shape.shape = bs
body.add_child(shape)
if models.is_empty():
var mi := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = bs.size
bm.material = _flat(Color.from_hsv(rng.randf(), 0.5, 0.7), 0.4)
mi.mesh = bm
body.add_child(mi)
else:
# model origin already sits at the 0.75 m ride height, same as the body's
body.add_child((load(models[rng.randi() % models.size()]) as PackedScene).instantiate())
body.position = pos
body.rotate_y(rng.randf_range(-0.05, 0.05) + (PI if rng.randf() < 0.5 else 0.0))
add_child(body)
func _bay_lines() -> void:
# painted bay dividers -- visual only, a 12 cm kerb of paint would be absurd
var paint := _flat(Color(0.86, 0.86, 0.80), 0.7)
for row in BAY_ROWS:
for bay in BAYS + 1:
var x := BAY_X0 - BAY_STEP * 0.5 + bay * BAY_STEP
_slab(Vector3(x, 0.02, row), Vector3(0.14, 0.04, 5.0), paint, false)
_slab(Vector3(BAY_X0 + BAY_STEP * (BAYS - 1) * 0.5, 0.02, row - 2.6),
Vector3(BAY_STEP * BAYS, 0.04, 0.14), paint, false)
func _store() -> void:
# the Woolies box at the far end -- what you're aiming at from the spawn
var front := -84.0
var mat_wall := _mat("facade_concrete", 0.09, 0.85)
var mat_shop := _mat("shopfront", 0.16, 0.3)
var mat_sign := _flat(Color(0.06, 0.32, 0.16), 0.6) # supermarket green
_slab(Vector3(0, 7.0, front - 14.0), Vector3(130, 14, 28), mat_wall)
_slab(Vector3(0, 2.1, front + 0.2), Vector3(120, 4.2, 0.6), mat_shop, false)
_slab(Vector3(0, 5.6, front + 0.3), Vector3(120, 2.4, 0.8), mat_sign, false)
_slab(Vector3(0, 4.3, front + 2.4), Vector3(126, 0.35, 5.0),
_mat("panel_white", 0.4, 0.5), false) # entry canopy
for x in [-58.0, -29.0, 0.0, 29.0, 58.0]:
_slab(Vector3(x, 2.15, front + 4.7), Vector3(0.3, 4.3, 0.3), _flat(Color(0.2, 0.2, 0.22), 0.5), false)
func _light_pole(base: Vector3) -> void:
var steel := _flat(Color(0.22, 0.23, 0.25), 0.5)
var pole := MeshInstance3D.new()
var cm := CylinderMesh.new()
cm.top_radius = 0.12
cm.bottom_radius = 0.17
cm.height = 9.0
cm.radial_segments = 8
cm.material = steel
pole.mesh = cm
pole.position = base + Vector3(0, 4.5, 0)
add_child(pole)
for s in [-1.0, 1.0]:
_slab(base + Vector3(s * 1.4, 8.9, 0), Vector3(2.0, 0.18, 0.6), steel, false)
_slab(base + Vector3(s * 2.2, 8.7, 0), Vector3(1.1, 0.22, 0.5),
_flat(Color(0.85, 0.84, 0.72), 0.4), false)
func _planter_island(pos: Vector3, rng: RandomNumberGenerator) -> void:
_slab(pos + Vector3(0, 0.25, 0), Vector3(5.0, 0.5, 11.0), _mat("planter", 0.6, 0.75))
_slab(pos + Vector3(0, 0.52, 0), Vector3(4.4, 0.06, 10.4), _mat("grass", 1.2), false)
for j in 2:
var at := pos + Vector3(0, 0, -2.6 + j * 5.2)
var trunk := MeshInstance3D.new()
var cm := CylinderMesh.new()
cm.top_radius = 0.13
cm.bottom_radius = 0.2
cm.height = 4.4
cm.radial_segments = 7
cm.material = _mat("bark", 1.4)
trunk.mesh = cm
trunk.position = at + Vector3(0, 2.7, 0)
add_child(trunk)
var crown := MeshInstance3D.new()
var sm := SphereMesh.new()
sm.radius = rng.randf_range(1.9, 2.5)
sm.height = sm.radius * 1.7
sm.radial_segments = 12
sm.rings = 7
sm.material = _mat("foliage", 0.55)
crown.mesh = sm
crown.position = at + Vector3(0, 5.6, 0)
add_child(crown)
func _trolley_bay(pos: Vector3) -> void:
var steel := _flat(Color(0.55, 0.56, 0.58), 0.45)
_slab(pos + Vector3(0, 2.5, 0), Vector3(3.0, 0.12, 9.0), _mat("panel_white", 0.5, 0.5), false)
for sx in [-1.3, 1.3]:
for sz in [-4.0, 4.0]:
_slab(pos + Vector3(sx, 1.25, sz), Vector3(0.16, 2.5, 0.16), steel, false)
for j in 5: # the trolleys themselves
_slab(pos + Vector3(0, 0.5, -3.2 + j * 1.6), Vector3(0.9, 1.0, 1.2), steel, false)
func _race_path() -> void:
# perimeter loop road around the parking rows -- race line + traffic route
var path := Path3D.new()
path.name = "RacePath"
var c := Curve3D.new()
var pts := [Vector3(45, 0, 55), Vector3(45, 0, -55), Vector3(-45, 0, -55), Vector3(-45, 0, 55)]
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() * 12.0
var to_next: Vector3 = (pts[(i + 1) % pts.size()] - p).normalized() * 12.0
c.add_point(p, to_prev, to_next)
path.curve = c
add_child(path)