ShitboxInfinity/levels/woolies_carpark/carpark.gd
m3ultra 30d5cfd846 Loose shopping trolleys in the Woolies carpark
18 kg of chrome chaos each: procedural basket + handle + castors on a
low-friction RigidBody, so they roll away when nudged and FLY when hit at
speed. Four strays around every trolley bay plus five rogues claiming
parking spots, as is tradition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:58:02 +10:00

259 lines
9.3 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)
for i in 4: # strays that never made it back to the bay
_trolley(p + Vector3(rng.randf_range(-7.0, 7.0), 0, rng.randf_range(-6.0, 6.0)),
rng.randf() * TAU)
for i in 5: # rogue trolleys claiming parking spots, as is tradition
_trolley(Vector3(rng.randf_range(-60.0, 60.0), 0, rng.randf_range(-40.0, 40.0)),
rng.randf() * TAU)
_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 _trolley(pos: Vector3, yaw: float) -> void:
## A loose shopping trolley: 18 kg of chrome chaos. Light enough to FLY when
## clipped at speed, low-friction so it rolls away like the real thing.
var b := RigidBody3D.new()
b.mass = 18.0
var pm := PhysicsMaterial.new()
pm.friction = 0.25
pm.bounce = 0.25
b.physics_material_override = pm
var cs := CollisionShape3D.new()
var bs := BoxShape3D.new()
bs.size = Vector3(0.58, 0.95, 0.92)
cs.shape = bs
cs.position = Vector3(0, 0.5, 0)
b.add_child(cs)
var steel := _flat(Color(0.62, 0.63, 0.66), 0.35)
steel.metallic = 0.7
# basket: floor + four walls, wider at the top like the real thing
var basket := [
[Vector3(0, 0.42, 0), Vector3(0.50, 0.03, 0.84)], # floor
[Vector3(0, 0.66, 0.44), Vector3(0.54, 0.45, 0.03)], # back
[Vector3(0, 0.66, -0.42), Vector3(0.50, 0.45, 0.03)], # front
[Vector3(0.27, 0.66, 0), Vector3(0.03, 0.45, 0.86)], # right
[Vector3(-0.27, 0.66, 0), Vector3(0.03, 0.45, 0.86)], # left
]
for spec in basket:
var mi := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = spec[1]
bm.material = steel
mi.mesh = bm
mi.position = spec[0]
b.add_child(mi)
var handle := MeshInstance3D.new()
var hm := BoxMesh.new()
hm.size = Vector3(0.58, 0.04, 0.04)
hm.material = steel
handle.mesh = hm
handle.position = Vector3(0, 0.97, 0.50)
b.add_child(handle)
for sx in [-0.22, 0.22]:
for sz in [-0.36, 0.36]:
var c := MeshInstance3D.new()
var cm := CylinderMesh.new()
cm.top_radius = 0.05
cm.bottom_radius = 0.05
cm.height = 0.04
cm.radial_segments = 8
cm.material = _flat(Color(0.12, 0.12, 0.13), 0.7)
c.mesh = cm
c.rotation_degrees = Vector3(0, 0, 90)
c.position = Vector3(sx, 0.05, sz)
b.add_child(c)
add_child(b)
b.position = pos + Vector3.UP * 0.05
b.rotate_y(yaw)
b.sleeping = true
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)