ShitboxInfinity/levels/the_shed/shed.gd
m3ultra 955413b735 Wrench phase 3: driveway customers -- the shed becomes a business
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>
2026-07-31 19:12:15 +10:00

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)