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>
259 lines
9.3 KiB
GDScript
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)
|