SHITBOX INFINITY + The Shed: a drivable home base

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>
This commit is contained in:
m3ultra 2026-07-31 15:07:34 +10:00
parent 324dbdf625
commit 3e9a8616ab
16 changed files with 524 additions and 70 deletions

View File

@ -1,6 +1,6 @@
# Burnout Infinity: Shitbox # Shitbox Infinity
Burnout 3-style arcade racer, Aussie shitboxes, Woolies carparks. Godot 4.6 + Blender, native Apple Silicon. **Shitbox Infinity** (nee Burnout Infinity: Shitbox) — Burnout 3-style arcade racer, Aussie shitboxes, Woolies carparks. Godot 4.6 + Blender, native Apple Silicon.
## Run ## Run
@ -150,6 +150,15 @@ a complete toolless gearbox swap headless.
Purchases are per-car (tools are global) and persist in `user://garage.save`, Purchases are per-car (tools are global) and persist in `user://garage.save`,
which also carries the drag **ladder rung**, per-car **quarter-mile PBs** and which also carries the drag **ladder rung**, per-car **quarter-mile PBs** and
outstanding **faults**. outstanding **faults**.
**The Shed is also a place** (`levels/the_shed`): a drivable suburban home base
— brick house, paling fence, hills hoist mid-spin with the washing still on it,
wheelie bins at the gate, and the corrugated double shed on its slab. The tools
you own stand in it as the real TRELLIS'd cashies models (unowned tools leave
an oil-stained outline where they'll go), the rest of the library is permanent
clutter, and the pegboard is FLUX. Roll into the painted bay in Cruise and hold
still a beat — the garage opens. No RacePath, no traffic: the one quiet level
in the game, on purpose.
Upgrades apply to a duplicate of the CarStats resource -- the .tres is shared Upgrades apply to a duplicate of the CarStats resource -- the .tres is shared
with traffic and parked spawns of the same model, and tuning your Lazza must with traffic and parked spawns of the same model, and tuning your Lazza must
not secretly tune every Lazza in the city. Bolt-ons attach after the deformer not secretly tune every Lazza in the city. Bolt-ons attach after the deformer

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b67bkapeh5dad"
path="res://.godot/imported/paling_fence.jpg-413cd2729baf979db92de2cdb1d58d9c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/textures/paling_fence.jpg"
dest_files=["res://.godot/imported/paling_fence.jpg-413cd2729baf979db92de2cdb1d58d9c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dd2vnfcb68rlc"
path="res://.godot/imported/pegboard.jpg-79f8a70d390d1050db6a7667d1c09f84.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/textures/pegboard.jpg"
dest_files=["res://.godot/imported/pegboard.jpg-79f8a70d390d1050db6a7667d1c09f84.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://levels/the_shed/shed.gd" id="1"]
[node name="TheShed" type="Node3D"]
script = ExtResource("1")
[node name="Spawn" type="Marker3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12, 0.5, 30)

282
levels/the_shed/shed.gd Normal file
View File

@ -0,0 +1,282 @@
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)

View File

@ -0,0 +1 @@
uid://b3hopg6xetmiy

View File

@ -14,26 +14,7 @@ const STRIP_HALF := 12.0 # wall-to-wall half width
const FINISH := -402.0 const FINISH := -402.0
const SHUTDOWN_END := -640.0 const SHUTDOWN_END := -640.0
## Real Cashies stock, TRELLIS'd into GLBs (tools/cc_to_glb.py). Scale is the ## Cashies tool props live in src/tool_props.gd (shared with The Shed).
## longest dimension in metres; mass makes clipping an engine crane at 80
## a different event to punting a grease gun.
const TOOL_SPECS := {
"angle_grinder": {"size": 0.35, "mass": 3.0},
"batt_charger": {"size": 0.28, "mass": 4.0},
"car_ramps": {"size": 0.95, "mass": 9.0},
"chain_hoist": {"size": 0.45, "mass": 12.0},
"compressor": {"size": 0.55, "mass": 14.0},
"engine_crane": {"size": 1.6, "mass": 70.0},
"engine_stand": {"size": 0.95, "mass": 30.0},
"grease_gun": {"size": 0.4, "mass": 2.0},
"jack_stands": {"size": 0.45, "mass": 8.0},
"jerry_can": {"size": 0.48, "mass": 6.0},
"obd_reader": {"size": 0.22, "mass": 1.0},
"socket_set": {"size": 0.4, "mass": 5.0},
"toolbox": {"size": 0.7, "mass": 18.0},
"trolley_jack": {"size": 0.65, "mass": 20.0},
"welder": {"size": 0.5, "mass": 16.0},
}
func _ready() -> void: func _ready() -> void:
var rng := RandomNumberGenerator.new() var rng := RandomNumberGenerator.new()
@ -270,53 +251,12 @@ func _gums(rng: RandomNumberGenerator) -> void:
func _paddock_tools(rng: RandomNumberGenerator) -> void: func _paddock_tools(rng: RandomNumberGenerator) -> void:
## Strew the paddock with the TRELLIS'd Cashies tool library -- real ## Strew the paddock with the TRELLIS'd Cashies tool library -- real
## secondhand clutter, every piece a shuntable RigidBody. Scale comes from ## secondhand clutter, every piece a shuntable RigidBody (ToolProps).
## TOOL_SPECS (TRELLIS output is unit-ish), base sat on the ground.
var dir := DirAccess.open("res://assets/tools")
if dir == null:
return
var names: Array[String] = []
for f in dir.get_files():
if f.ends_with(".glb"):
names.append(f.get_basename())
names.sort() # deterministic layout
var i := 0 var i := 0
for n in names: for n in ToolProps.library():
var scene := load("res://assets/tools/%s.glb" % n) as PackedScene var spot := Vector3(24.0 + (i % 4) * 4.5 + rng.randf_range(-0.8, 0.8), 0.0,
if scene == null:
continue
var spec: Dictionary = TOOL_SPECS.get(n, {"size": 0.5, "mass": 8.0})
var model := scene.instantiate() as Node3D
add_child(model) # into the tree first so global AABBs are real
var aabb := AABB()
var first := true
for mi in model.find_children("*", "MeshInstance3D", true, false):
var b: AABB = (mi as MeshInstance3D).global_transform * (mi as MeshInstance3D).get_aabb()
aabb = b if first else aabb.merge(b)
first = false
remove_child(model)
if first:
model.queue_free()
continue
var s: float = float(spec["size"]) / maxf(aabb.size.x, maxf(aabb.size.y, aabb.size.z))
var body := RigidBody3D.new()
body.mass = float(spec["mass"])
body.set_meta("cc_tool", n)
var cs := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = (aabb.size * s).clamp(Vector3.ONE * 0.08, Vector3.ONE * 3.0)
cs.shape = box
cs.position = aabb.get_center() * s
body.add_child(cs)
model.scale = Vector3.ONE * s
body.add_child(model)
add_child(body)
var spot := Vector3(24.0 + (i % 4) * 4.5 + rng.randf_range(-0.8, 0.8),
0.02 - aabb.position.y * s,
-16.0 - float(i / 4) * 7.0 + rng.randf_range(-0.8, 0.8)) -16.0 - float(i / 4) * 7.0 + rng.randf_range(-0.8, 0.8))
body.global_position = spot if ToolProps.spawn(self, n, spot, rng.randf() * TAU) != null:
body.rotate_y(rng.randf() * TAU)
body.sleeping = true
i += 1 i += 1
func _race_path() -> void: func _race_path() -> void:

View File

@ -2,7 +2,7 @@ config_version=5
[application] [application]
config/name="Burnout Infinity: Shitbox" config/name="Shitbox Infinity"
run/main_scene="res://src/main.tscn" run/main_scene="res://src/main.tscn"
config/features=PackedStringArray("4.6", "Forward Plus") config/features=PackedStringArray("4.6", "Forward Plus")

11
push19.log Normal file

File diff suppressed because one or more lines are too long

View File

@ -135,6 +135,10 @@ var _road_off := 0.0 # player's tracked offset along the road line
var _oncoming_run := 0.0 var _oncoming_run := 0.0
var _oncoming_toast := 0.0 var _oncoming_toast := 0.0
var garage_bay: Node3D = null # The Shed's bay: roll in, stop, garage opens
var bay_t := 0.0
var garage_requested := false
var car: Car var car: Car
var cam: Camera3D var cam: Camera3D
var race_path: Path3D var race_path: Path3D
@ -189,6 +193,8 @@ func _ready() -> void:
level.add_child(path) level.add_child(path)
race_path = path race_path = path
garage_bay = level.find_child("GarageBay", true, false)
car = (load("res://src/car.tscn") as PackedScene).instantiate() car = (load("res://src/car.tscn") as PackedScene).instantiate()
car.stats = load(car_path) car.stats = load(car_path)
# place BEFORE add_child: teleporting a continuous_cd body after it enters # place BEFORE add_child: teleporting a continuous_cd body after it enters
@ -710,6 +716,23 @@ func _physics_process(delta: float) -> void:
_outrun_tick(delta) _outrun_tick(delta)
if mode == "cruise": if mode == "cruise":
_pursuit_tick(delta) _pursuit_tick(delta)
if mode == "cruise" and garage_bay != null and not car.wrecked:
# The Shed's bay: roll in, hold still a beat, and the garage opens
var bd := car.global_position.distance_to(garage_bay.global_position)
if bd < 4.0 and car.linear_velocity.length() < 1.5:
var was_t := bay_t
bay_t += delta
if was_t < 0.3 and bay_t >= 0.3:
_msg("HOLD STILL, GRABBING THE TOOLBOX...", 30)
if bay_t >= 1.4 and not garage_requested:
garage_requested = true
Garage.ui_stats_path = Game.car_path
Sfx.shot_2d("clunk", -4.0)
# headless suites have no current_scene to swap; the flag is the test
if get_tree().current_scene != null:
get_tree().change_scene_to_file("res://src/garage.tscn")
else:
bay_t = 0.0
if mode == "drag" and not over: if mode == "drag" and not over:
_drag_tick(delta) _drag_tick(delta)
if mode == "rage" and not over: if mode == "rage" and not over:

View File

@ -31,7 +31,7 @@ offset_bottom = -40.0
theme_override_constants/separation = 20 theme_override_constants/separation = 20
[node name="Title" type="Label" parent="VBox"] [node name="Title" type="Label" parent="VBox"]
text = "BURNOUT INFINITY: SHITBOX" text = "SHITBOX INFINITY"
horizontal_alignment = 1 horizontal_alignment = 1
theme_override_font_sizes/font_size = 52 theme_override_font_sizes/font_size = 52
theme_override_colors/font_color = Color(1, 0.82, 0.35, 1) theme_override_colors/font_color = Color(1, 0.82, 0.35, 1)

75
src/tool_props.gd Normal file
View File

@ -0,0 +1,75 @@
class_name ToolProps
## The TRELLIS'd cashies tool library (assets/tools/*.glb, tools/cc_to_glb.py)
## as spawnable props. TRELLIS output is unit-ish, so every class carries its
## real-world size (longest dimension, metres) and an honest mass -- clipping
## the 70 kg engine crane at speed is a different event to punting a grease gun.
const SPECS := {
"angle_grinder": {"size": 0.35, "mass": 3.0},
"batt_charger": {"size": 0.28, "mass": 4.0},
"car_ramps": {"size": 0.95, "mass": 9.0},
"chain_hoist": {"size": 0.45, "mass": 12.0},
"compressor": {"size": 0.55, "mass": 14.0},
"engine_crane": {"size": 1.6, "mass": 70.0},
"engine_stand": {"size": 0.95, "mass": 30.0},
"grease_gun": {"size": 0.4, "mass": 2.0},
"jack_stands": {"size": 0.45, "mass": 8.0},
"jerry_can": {"size": 0.48, "mass": 6.0},
"obd_reader": {"size": 0.22, "mass": 1.0},
"socket_set": {"size": 0.4, "mass": 5.0},
"toolbox": {"size": 0.7, "mass": 18.0},
"trolley_jack": {"size": 0.65, "mass": 20.0},
"welder": {"size": 0.5, "mass": 16.0},
}
static func library() -> Array[String]:
var out: Array[String] = []
var dir := DirAccess.open("res://assets/tools")
if dir == null:
return out
for f in dir.get_files():
if f.ends_with(".glb"):
out.append(f.get_basename())
out.sort()
return out
static func spawn(parent: Node3D, name: String, pos: Vector3, yaw: float,
meta_key := "cc_tool", meta_val := "") -> RigidBody3D:
## Instantiate one tool as a shuntable RigidBody, base sat on pos.y.
## parent must already be inside the tree (global AABBs are measured live).
var path := "res://assets/tools/%s.glb" % name
if not ResourceLoader.exists(path):
return null
var scene := load(path) as PackedScene
if scene == null:
return null
var spec: Dictionary = SPECS.get(name, {"size": 0.5, "mass": 8.0})
var model := scene.instantiate() as Node3D
parent.add_child(model) # into the tree first so global AABBs are real
var aabb := AABB()
var first := true
for mi in model.find_children("*", "MeshInstance3D", true, false):
var b: AABB = (mi as MeshInstance3D).global_transform * (mi as MeshInstance3D).get_aabb()
aabb = b if first else aabb.merge(b)
first = false
parent.remove_child(model)
if first:
model.queue_free()
return null
var s: float = float(spec["size"]) / maxf(aabb.size.x, maxf(aabb.size.y, aabb.size.z))
var body := RigidBody3D.new()
body.mass = float(spec["mass"])
body.set_meta(meta_key, meta_val if meta_val != "" else name)
var cs := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = (aabb.size * s).clamp(Vector3.ONE * 0.08, Vector3.ONE * 3.0)
cs.shape = box
cs.position = aabb.get_center() * s
body.add_child(cs)
model.scale = Vector3.ONE * s
body.add_child(model)
parent.add_child(body)
body.global_position = Vector3(pos.x, pos.y - aabb.position.y * s + 0.02, pos.z)
body.rotate_y(yaw)
body.sleeping = true
return body

1
src/tool_props.gd.uid Normal file
View File

@ -0,0 +1 @@
uid://dbvs01y1dxiet

View File

@ -101,6 +101,29 @@ func _run() -> void:
_check(ui.done, "repair never finished (stuck on step %d)" % ui.step_index) _check(ui.done, "repair never finished (stuck on step %d)" % ui.step_index)
_check(not Garage.faults("datto_120y").has("gearbox"), "DIY fix didn't clear the fault") _check(not Garage.faults("datto_120y").has("gearbox"), "DIY fix didn't clear the fault")
ui.free() ui.free()
# THE SHED home base: owned tools materialise (sockets were bought above,
# ramps were not), and rolling into the bay and holding still opens the garage
print(" .. shed")
Game.level_path = levels["the_shed"]
gg = await _boot("cruise")
_check(gg.garage_bay != null, "shed has no GarageBay")
var owned_props: Array = gg.find_children("*", "RigidBody3D", true, false).filter(
func(n): return n.has_meta("shed_tool"))
_check(owned_props.size() == 1 and owned_props[0].get_meta("shed_tool") == "sockets",
"owned-tool display wrong (%d shed_tool props)" % owned_props.size())
var bay_at: Vector3 = gg.garage_bay.global_position
var bay_xf := Transform3D(Basis.IDENTITY, bay_at + Vector3.UP * 0.75)
PhysicsServer3D.body_set_state(gg.car.get_rid(), PhysicsServer3D.BODY_STATE_TRANSFORM, bay_xf)
gg.car.linear_velocity = Vector3.ZERO
gg.car._prev_lv = Vector3.ZERO
for i in 150:
await physics_frame
gg.car.linear_velocity = Vector3.ZERO
if gg.garage_requested:
break
_check(gg.garage_requested, "rolling into the bay never opened the garage")
gg.free()
Game.level_path = levels.get("woolies_carpark", levels.values()[0])
Garage._reset_for_test() # later stages must run stock Garage._reset_for_test() # later stages must run stock
# cruise: throttle 3s, car must move # cruise: throttle 3s, car must move