Visit The Shed and one of the locals is waiting on the driveway with a
broken shitbox and cash: their actual car model parked there under a
floating Label3D job sign -- who they are, what they reckon is wrong
("it makes a noise at church speed"), the honest fault name, and the
offer. Pull up beside it and hold still to take the job; the wrench
minigame opens on THEIR car.
The roster is the extended universe: Nanna, P-Plate Pepper, Mullet
Merv, Torque'n Tezza, the Self-Described Bathurst Legend, Your Mate
Dazza (don't tell Shazza), Karen from Number 12, and the Widowmaker,
who says nothing and pays cash. Roster and fault rotate
deterministically off jobs_done.
Settlement: full pay at 60+, a $50 tip at 85+, half below 60 -- and
SHED REP moves with the workmanship (+3 clean, -6 botched, cap 40),
scaling the next offer from 70% of shop rate at rep 0 to 100% at cap.
Rep shows on the menu and garage header. Customer jobs bank NO quality
buffs: the customer's car id might be a car the player owns, and
fixing Merv's Kingswood must not secretly tune yours -- the wrench
consumes Garage.ui_customer and routes _finish to customer_done()
instead of clear_fault/set_quality.
The shed guards the spawn (parent is Game and mode == cruise) so the
wrench scene's backdrop copy of the level never summons a customer
mid-job. The spare parked shitbox moves to the kerb; the driveway
belongs to commerce now.
Smoke: customer waits after a shed visit, holding beside the spot takes
the job (flag-guarded for headless), the driven wrench job pays, rep
rises, the driveway clears, and the customer's own-car quality stays
untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
315 lines
12 KiB
GDScript
315 lines
12 KiB
GDScript
extends Node3D
|
|
## THE SHED -- home base. A suburban backyard on a quiet cul-de-sac: brick
|
|
## house, paling fence, hills hoist mid-spin, and the corrugated double shed
|
|
## on its slab. The tools you actually OWN (Garage.tools()) stand in it as the
|
|
## real TRELLIS'd cashies models; unowned tools are an oil-stained outline of
|
|
## where they'll go. Roll into the marked bay and stop -- game.gd opens the
|
|
## garage. Cruise mode only ever spawns here quietly: no RacePath, no traffic,
|
|
## just you, the lawn, and the neighbours' opinion of your exhaust.
|
|
|
|
const TEX := "res://assets/textures/%s.jpg"
|
|
|
|
## shed-catalogue tool id -> {model, pos, yaw}. Bench-top items carry y.
|
|
const TOOL_SPOTS := {
|
|
"ramps": {"model": "car_ramps", "pos": Vector3(10.5, 0.07, -9.6), "yaw": 0.1},
|
|
"reader": {"model": "obd_reader", "pos": Vector3(9.0, 0.99, -19.8), "yaw": 0.4},
|
|
"sockets": {"model": "socket_set", "pos": Vector3(11.5, 0.99, -19.8), "yaw": -0.2},
|
|
"compressor": {"model": "compressor", "pos": Vector3(8.2, 0.07, -17.5), "yaw": 1.2},
|
|
"hoist": {"model": "engine_crane", "pos": Vector3(15.2, 0.07, -17.0), "yaw": -0.5},
|
|
"additives": {"model": "jerry_can", "pos": Vector3(8.0, 0.07, -13.5), "yaw": 2.1},
|
|
}
|
|
## always-there garnish: the rest of the library, scattered like a real shed
|
|
const CLUTTER := [
|
|
["toolbox", Vector3(16.1, 0.07, -11.5), 1.9],
|
|
["trolley_jack", Vector3(11.0, 0.07, -15.5), 0.3],
|
|
["jack_stands", Vector3(15.8, 0.07, -9.8), 2.6],
|
|
["welder", Vector3(7.8, 0.07, -15.8), 1.6],
|
|
["batt_charger", Vector3(14.0, 0.99, -19.8), -0.3],
|
|
["angle_grinder", Vector3(16.0, 0.99, -19.7), 0.9],
|
|
["grease_gun", Vector3(13.0, 0.07, -16.5), 4.1],
|
|
["chain_hoist", Vector3(7.4, 0.07, -19.3), 0.6],
|
|
["engine_stand", Vector3(19.8, 0.0, -12.0), 5.4],
|
|
]
|
|
|
|
var hoist: Node3D
|
|
|
|
func _ready() -> void:
|
|
var rng := RandomNumberGenerator.new()
|
|
rng.seed = 4074 # the postcode of paradise
|
|
|
|
_slab(Vector3(-3, -0.5, 5), Vector3(120, 1, 120), _mat("grass", 10.0)) # world
|
|
_slab(Vector3(0, 0.015, 31), Vector3(88, 0.03, 14), _mat("asphalt", 8.0), false) # street
|
|
_slab(Vector3(0, 0.02, 22), Vector3(88, 0.04, 2.2), _mat("footpath", 2.0), false) # footpath
|
|
_slab(Vector3(12, 0.025, 8), Vector3(8, 0.05, 32), _mat("concrete_ground", 3.0), false) # driveway
|
|
_fence()
|
|
_house()
|
|
_shed()
|
|
_hills_hoist()
|
|
_tools()
|
|
_neighbours()
|
|
_gums(rng)
|
|
|
|
# wheelie bins by the gate, a spare shitbox on the driveway (game.gd fills)
|
|
for i in 2:
|
|
var m := Marker3D.new()
|
|
m.name = "WB_%d" % i
|
|
m.position = Vector3(18.8, 0, 17.0 - i * 1.4)
|
|
add_child(m)
|
|
# the spare shitbox parks on the street now -- the driveway is for customers
|
|
var pp := Marker3D.new()
|
|
pp.name = "PP_kerb"
|
|
pp.position = Vector3(-8, 0, 30.5)
|
|
pp.rotate_y(PI * 0.5 + 0.04)
|
|
add_child(pp)
|
|
# a customer waits on the driveway (real play only: the wrench scene uses
|
|
# this level as backdrop and must never summon one mid-job)
|
|
if get_parent() is Game and Game.mode == "cruise":
|
|
_customer()
|
|
|
|
func _customer() -> void:
|
|
var cust := Garage.customer_arrive()
|
|
if cust.is_empty():
|
|
return
|
|
var cars := Registry.cars()
|
|
if cars.has(cust["car"]):
|
|
var st: CarStats = load(cars[cust["car"]])
|
|
if st.model_path != "" and ResourceLoader.exists(st.model_path):
|
|
var m := (load(st.model_path) as PackedScene).instantiate()
|
|
add_child(m)
|
|
m.position = Vector3(12, 0.72, 6.5)
|
|
m.rotate_y(0.12)
|
|
var spot := Marker3D.new()
|
|
spot.name = "CustomerSpot"
|
|
spot.position = Vector3(12, 0, 6.5)
|
|
add_child(spot)
|
|
var sign := Label3D.new()
|
|
sign.text = "%s\n\"%s\"\n%s -- $%d" % [cust["name"], cust["blurb"],
|
|
Garage.FAULTS[cust["fault"]]["name"], cust["pay"]]
|
|
sign.font_size = 64
|
|
sign.pixel_size = 0.004
|
|
sign.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
|
sign.modulate = Color(1, 0.85, 0.4)
|
|
sign.outline_size = 12
|
|
sign.position = Vector3(12, 2.7, 6.5)
|
|
add_child(sign)
|
|
|
|
func _process(delta: float) -> void:
|
|
if hoist:
|
|
hoist.rotate_y(delta * 0.35) # there is always a bit of wind
|
|
|
|
## ---- the yard -------------------------------------------------------------
|
|
|
|
func _fence() -> void:
|
|
var pal := _mat("paling_fence", 0.55, 0.9)
|
|
for seg in [
|
|
[Vector3(-30, 0.85, -4), Vector3(0.08, 1.7, 48)], # west
|
|
[Vector3(24, 0.85, -4), Vector3(0.08, 1.7, 48)], # east
|
|
[Vector3(-3, 0.85, -28), Vector3(54, 1.7, 0.08)], # back
|
|
[Vector3(-11.5, 0.85, 20), Vector3(37, 1.7, 0.08)], # front west of gate
|
|
[Vector3(20.5, 0.85, 20), Vector3(7, 1.7, 0.08)], # front east of gate
|
|
]:
|
|
_slab(seg[0], seg[1], pal)
|
|
|
|
func _house() -> void:
|
|
# single-storey: brick tiled SPARSE (0.045) or the place reads as an office
|
|
var brick := _mat("facade_brick", 0.045, 0.85)
|
|
_slab(Vector3(-12, 1.9, 8), Vector3(24, 3.8, 20), brick)
|
|
_slab(Vector3(-12, 1.7, 18.1), Vector3(18, 1.2, 0.15), _mat("facade_glass", 0.25, 0.3), false)
|
|
# corrugated hip-ish roof: two tilted sheets meeting at a ridge
|
|
for s in [-1.0, 1.0]:
|
|
var roof := MeshInstance3D.new()
|
|
var bm := BoxMesh.new()
|
|
bm.size = Vector3(25.2, 0.14, 11.0)
|
|
bm.material = _mat("corrugated_iron", 1.3, 0.6)
|
|
roof.mesh = bm
|
|
roof.position = Vector3(-12, 4.9, 8 + s * 4.9)
|
|
roof.rotation_degrees = Vector3(-s * 14.0, 0, 0)
|
|
add_child(roof)
|
|
# porch: slab, posts, and a light for coming home at night
|
|
_slab(Vector3(-6, 0.1, 19.3), Vector3(8, 0.2, 2.6), _mat("concrete_ground", 2.0), false)
|
|
for px in [-9.5, -2.5]:
|
|
_slab(Vector3(px, 1.5, 20.2), Vector3(0.14, 3.0, 0.14), _flat(Color(0.85, 0.83, 0.78), 0.7), false)
|
|
_slab(Vector3(-6, 3.05, 19.9), Vector3(0.3, 0.14, 0.3), _flat(Color(0.9, 0.87, 0.6), 0.4, Color(0.9, 0.8, 0.4)), false)
|
|
if Game.time_of_day == "night":
|
|
var ol := OmniLight3D.new()
|
|
ol.light_color = Color(1.0, 0.85, 0.55)
|
|
ol.light_energy = 1.4
|
|
ol.omni_range = 10.0
|
|
ol.position = Vector3(-6, 2.8, 19.6)
|
|
add_child(ol)
|
|
|
|
func _shed() -> void:
|
|
## Double garage, not a warehouse: 10 x 12 m, open front, slab, pegboard.
|
|
var iron := _mat("corrugated_iron", 1.4, 0.6)
|
|
_slab(Vector3(12, 0.035, -15), Vector3(10.6, 0.07, 12.6), _mat("concrete_ground", 2.5))
|
|
_slab(Vector3(7, 1.7, -15), Vector3(0.1, 3.4, 12.6), iron) # west wall
|
|
_slab(Vector3(17, 1.7, -15), Vector3(0.1, 3.4, 12.6), iron) # east wall
|
|
_slab(Vector3(12, 1.85, -21.2), Vector3(10.2, 3.7, 0.1), iron) # back wall
|
|
_slab(Vector3(12, 2.55, -20.9), Vector3(9.6, 1.9, 0.06), _mat("pegboard", 0.4, 0.9), false)
|
|
# workbench along the back
|
|
_slab(Vector3(11.5, 0.45, -19.9), Vector3(8.8, 0.9, 1.2), _flat(Color(0.32, 0.26, 0.2), 0.8))
|
|
_slab(Vector3(11.5, 0.93, -19.9), Vector3(9.0, 0.08, 1.35), _flat(Color(0.55, 0.48, 0.38), 0.6), false)
|
|
# skillion roof, high at the front so the ute fits
|
|
var roof := MeshInstance3D.new()
|
|
var bm := BoxMesh.new()
|
|
bm.size = Vector3(11.4, 0.12, 13.6)
|
|
bm.material = iron
|
|
roof.mesh = bm
|
|
roof.position = Vector3(12, 3.9, -15)
|
|
roof.rotation_degrees = Vector3(-3.2, 0, 0)
|
|
add_child(roof)
|
|
# fluoro batten under the roof -- and a real light at night
|
|
_slab(Vector3(12, 3.35, -15), Vector3(0.2, 0.08, 4.2), _flat(Color(0.95, 0.96, 0.9), 0.3, Color(0.9, 0.93, 0.85)), false)
|
|
if Game.time_of_day == "night":
|
|
var ol := OmniLight3D.new()
|
|
ol.light_color = Color(0.92, 0.95, 0.88)
|
|
ol.light_energy = 2.2
|
|
ol.omni_range = 14.0
|
|
ol.position = Vector3(12, 3.1, -15)
|
|
add_child(ol)
|
|
# the bay: painted outline + the marker game.gd watches
|
|
var paint := _flat(Color(0.86, 0.86, 0.8), 0.7)
|
|
for sx in [9.5, 14.5]:
|
|
_slab(Vector3(sx, 0.075, -14.2), Vector3(0.12, 0.02, 6.4), paint, false)
|
|
_slab(Vector3(12, 0.075, -17.4), Vector3(5.1, 0.02, 0.12), paint, false)
|
|
var bay := Marker3D.new()
|
|
bay.name = "GarageBay"
|
|
bay.position = Vector3(12, 0, -14.2)
|
|
add_child(bay)
|
|
|
|
func _hills_hoist() -> void:
|
|
var steel := _flat(Color(0.6, 0.61, 0.63), 0.4)
|
|
steel.metallic = 0.6
|
|
_cyl(Vector3(-14, 1.3, -20), 0.05, 2.6, steel)
|
|
hoist = Node3D.new()
|
|
hoist.position = Vector3(-14, 2.55, -20)
|
|
add_child(hoist)
|
|
for i in 4:
|
|
var arm := MeshInstance3D.new()
|
|
var bm := BoxMesh.new()
|
|
bm.size = Vector3(3.4, 0.05, 0.05)
|
|
bm.material = steel
|
|
arm.mesh = bm
|
|
arm.rotation_degrees = Vector3(0, 45.0 + i * 90.0, 4.0)
|
|
hoist.add_child(arm)
|
|
for i in 2: # wire squares
|
|
var r := 1.0 + i * 0.55
|
|
for j in 4:
|
|
var wire := MeshInstance3D.new()
|
|
var wm := BoxMesh.new()
|
|
wm.size = Vector3(r * 1.41, 0.015, 0.015)
|
|
wm.material = steel
|
|
wire.mesh = wm
|
|
wire.position = Vector3(0, 0.06 + i * 0.02, 0).rotated(Vector3.UP, 0)
|
|
wire.rotation_degrees = Vector3(0, j * 90.0, 0)
|
|
wire.position = Vector3(cos(deg_to_rad(j * 90.0 + 90.0)), 0.08, sin(deg_to_rad(j * 90.0 + 90.0))) * r * 0.0
|
|
wire.position.y = 0.08
|
|
wire.transform.origin += wire.basis.z * r * 0.7
|
|
hoist.add_child(wire)
|
|
# the washing: towels that have seen some things
|
|
for c in [[Color(0.75, 0.5, 0.45), 0.0], [Color(0.5, 0.6, 0.75), 1.7], [Color(0.85, 0.82, 0.7), 3.6]]:
|
|
var tow := MeshInstance3D.new()
|
|
var tm := BoxMesh.new()
|
|
tm.size = Vector3(0.7, 0.55, 0.02)
|
|
tm.material = _flat(c[0], 0.9)
|
|
tow.mesh = tm
|
|
tow.rotation_degrees = Vector3(0, rad_to_deg(float(c[1])), 0)
|
|
tow.position = Vector3(cos(float(c[1])), -0.35, sin(float(c[1]))) * 1.2
|
|
hoist.add_child(tow)
|
|
|
|
func _tools() -> void:
|
|
## Owned catalogue tools materialise as the real models; unowned leave an
|
|
## oil-stained promise on the slab. Clutter is permanent, like real clutter.
|
|
for id in TOOL_SPOTS:
|
|
var spot: Dictionary = TOOL_SPOTS[id]
|
|
if Garage.has_tool(id):
|
|
ToolProps.spawn(self, spot["model"], spot["pos"], float(spot["yaw"]), "shed_tool", id)
|
|
else:
|
|
_slab(Vector3(spot["pos"].x, 0.078, spot["pos"].z), Vector3(0.7, 0.01, 0.5),
|
|
_flat(Color(0.09, 0.08, 0.06), 0.3), false)
|
|
for c in CLUTTER:
|
|
ToolProps.spawn(self, c[0], c[1], float(c[2]))
|
|
|
|
func _neighbours() -> void:
|
|
## Across the road: enough suburbia to feel watched.
|
|
for i in 3:
|
|
var x := -34.0 + i * 30.0
|
|
_slab(Vector3(x, 1.8, 47), Vector3(18, 3.6, 14),
|
|
_mat("facade_brick" if i != 1 else "facade_concrete", 0.045, 0.85))
|
|
var roof := MeshInstance3D.new()
|
|
var bm := BoxMesh.new()
|
|
bm.size = Vector3(19.5, 0.14, 15.5)
|
|
bm.material = _mat("corrugated_iron", 1.3, 0.6)
|
|
roof.mesh = bm
|
|
roof.position = Vector3(x, 4.2, 47)
|
|
roof.rotation_degrees = Vector3(6, 0, 0)
|
|
add_child(roof)
|
|
_slab(Vector3(x, 0.85, 39.2), Vector3(18, 1.7, 0.08), _mat("paling_fence", 0.55, 0.9))
|
|
|
|
func _gums(rng: RandomNumberGenerator) -> void:
|
|
for at in [Vector3(-26, 0, -22), Vector3(20, 0, -25), Vector3(-27, 0, 14),
|
|
Vector3(-38, 0, 24), Vector3(34, 0, 23)]:
|
|
var h := rng.randf_range(5.5, 8.5)
|
|
_cyl(at + Vector3(0, h * 0.5, 0), 0.2, h, _mat("bark", 1.4))
|
|
var crown := MeshInstance3D.new()
|
|
var sm := SphereMesh.new()
|
|
sm.radius = rng.randf_range(2.4, 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, h + sm.radius * 0.35, 0)
|
|
add_child(crown)
|
|
|
|
## ---- helpers --------------------------------------------------------------
|
|
|
|
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, emit := Color.BLACK) -> StandardMaterial3D:
|
|
var m := StandardMaterial3D.new()
|
|
m.albedo_color = color
|
|
m.roughness = rough
|
|
m.metallic = 0.0
|
|
if emit != Color.BLACK:
|
|
m.emission_enabled = true
|
|
m.emission = emit
|
|
m.emission_energy_multiplier = 1.2
|
|
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)
|
|
|
|
func _cyl(pos: Vector3, r: float, h: float, mat: Material) -> void:
|
|
var mi := MeshInstance3D.new()
|
|
var cm := CylinderMesh.new()
|
|
cm.top_radius = r
|
|
cm.bottom_radius = r * 1.2
|
|
cm.height = h
|
|
cm.radial_segments = 8
|
|
cm.material = mat
|
|
mi.mesh = cm
|
|
mi.position = pos
|
|
add_child(mi)
|