The game is now called Shitbox Infinity (project name, window title, title screen, README). The Gitea repo keeps its name until John renames it -- gitea redirects, and the remote gets a set-url then. The Shed (levels/the_shed) is the payoff for the whole garage arc: a drivable suburban block on a quiet cul-de-sac. Single-storey brick house with a corrugated roof and a porch light that works at night; paling fence (new FLUX texture) with a gate gap for the driveway; hills hoist mid-spin with three towels on it; wheelie bins at the gate and a spare fleet shitbox parked on the street; gums; neighbours across the road so you feel watched. The shed itself is a 10x12 corrugated double garage on a slab: FLUX pegboard wall, workbench, fluoro batten (a real light at night), and a painted bay. The tools you OWN (Garage.tools()) stand in it as the real TRELLIS'd cashies models -- sockets and the code reader ON the bench, the engine crane by the wall, ramps at the bay mouth -- while unowned tools leave an oil-stained outline of where they'll go. The rest of the tool library is permanent clutter via the new shared ToolProps helper (dragway.gd refactored onto it too). Roll into the bay in Cruise, hold still 1.4 s, and game.gd opens the garage -- diegetic shopping. Headless suites have no current_scene to swap, so the trigger sets garage_requested first and the smoke suite asserts on that: owned-tool display (sockets present, ramps absent on the test save), bay marker found, bay trigger fires. Suite covers 9 levels now. Scale lessons baked in: facade_brick at 0.14 tiling reads as a three-storey office (0.045 reads domestic), and corrugated iron at 0.35 has metre-wide ridges (1.4 reads like roofing). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
283 lines
11 KiB
GDScript
283 lines
11 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)
|
|
var pp := Marker3D.new()
|
|
pp.name = "PP_drive"
|
|
pp.position = Vector3(12, 0, 3)
|
|
pp.rotate_y(0.06)
|
|
add_child(pp)
|
|
|
|
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)
|