ShitboxInfinity/tests/smoke.gd
m3ultra 3e9a8616ab 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>
2026-07-31 15:07:34 +10:00

408 lines
18 KiB
GDScript

extends SceneTree
## Headless smoke test:
## /Applications/Godot.app/Contents/MacOS/Godot --headless --path . --script tests/smoke.gd
## Registry finds content; each mode boots and simulates; the car drives; traffic
## gets smashed when driven through.
func _initialize() -> void:
_run()
func _run() -> void:
var cars := Registry.cars()
var levels := Registry.levels()
_check(not cars.is_empty(), "no cars found")
_check(not levels.is_empty(), "no levels found")
for p in cars.values():
var s: CarStats = load(p)
_check(s != null and s.engine_power > 0.0, "bad stats: %s" % p)
if s.model_path != "":
_check(load(s.model_path) is PackedScene, "bad model: %s" % s.model_path)
# datto is procedural (has det_* parts); imported models legitimately have none
Game.car_path = cars.get("datto_120y", cars.values()[0])
# the carpark is the controlled arena; city levels have walls that complicate the ram test
Game.level_path = levels.get("woolies_carpark", levels.values()[0])
# garage economy: scratch save (NEVER the real one), earn/buy roundtrip,
# stats + bolt-ons applied to the player, then reset so later stages run stock
print(" .. garage")
Garage.save_path = "user://garage_smoketest.save"
Garage._reset_for_test()
_check(Garage.bucks() == 0, "scratch garage not empty")
Garage.earn(4000)
_check(Garage.bucks() == 4000, "earn didn't bank")
_check(Garage.buy("datto_120y", "spoiler"), "couldn't buy the wing")
_check(Garage.buy("datto_120y", "turbo"), "couldn't buy the turbo")
_check(not Garage.buy("datto_120y", "spoiler"), "bought the same wing twice")
_check(not Garage.buy("datto_120y", "exhaust"), "bought a $2000 pipe with $500")
_check(Garage.bucks() == 500, "change is wrong (%d)" % Garage.bucks())
var base_power: float = (load("res://cars/datto_120y/stats.tres") as CarStats).engine_power
var gg: Game = await _boot("cruise")
_check(gg.car.stats.engine_power > base_power * 1.15, "turbo didn't boost power")
_check(gg.car.stats.grip > (load("res://cars/datto_120y/stats.tres") as CarStats).grip + 0.5,
"wing added no grip")
_check(gg.car.stats.resource_path == "", "upgrades mutated the SHARED stats resource")
gg.free()
# the shed: tunes are gated on tools, and retorque hardens the panels
Garage.earn(2000)
_check(not Garage.buy_tune("datto_120y", "retorque"), "tuned without owning the tool")
_check(Garage.buy_tool("sockets"), "couldn't buy the socket set")
_check(Garage.buy_tune("datto_120y", "retorque"), "tool owned but tune refused")
_check(not Garage.buy_tune("datto_120y", "retorque"), "same tune sold twice")
gg = await _boot("cruise")
_check(gg.car.part_dv > Car.PART_DV + 1.0, "retorque didn't harden panels (%.1f)" % gg.car.part_dv)
gg.free()
# raceday persistence: ladder rung and per-car quarter-mile PBs
_check(Garage.drag_rung() == 0, "fresh save not on ladder rung 0")
Garage.drag_advance()
_check(Garage.drag_rung() == 1, "ladder didn't advance")
_check(Garage.record_drag_pb("datto_120y", 14.5), "first ET not a PB")
_check(not Garage.record_drag_pb("datto_120y", 15.0), "slower ET counted as a PB")
_check(Garage.record_drag_pb("datto_120y", 13.9), "faster ET refused as PB")
_check(absf(Garage.drag_pb("datto_120y") - 13.9) < 0.01, "PB not stored")
# faults: break it, boot wounded, pay the shop for one, DIY the other
print(" .. repair")
Garage.add_fault("datto_120y", "gearbox")
Garage.add_fault("datto_120y", "gasket")
_check(Garage.faults("datto_120y").size() == 2, "faults didn't record")
gg = await _boot("cruise")
_check(gg.car.stats.engine_power < base_power * 0.9, "wounded car boots at full power")
_check(gg.car.leaking, "gearbox-faulted car isn't leaking")
gg.free()
Garage.earn(1000)
_check(Garage.shop_fix("datto_120y", "gasket"), "shop wouldn't take the money")
_check(not Garage.faults("datto_120y").has("gasket"), "shop fix didn't clear the fault")
# DIY the gearbox: play the whole minigame headless through its API
Garage.ui_stats_path = "res://cars/datto_120y/stats.tres"
Garage.ui_fault = "gearbox"
var ui = (load("res://src/repair.tscn") as PackedScene).instantiate()
root.add_child(ui)
await process_frame
_check(ui.steps.size() >= 6, "gearbox job suspiciously short (%d steps)" % ui.steps.size())
var right := -1
for i in ui.light_btns.size():
if ui.light_btns[i]["light"] == Garage.FAULTS["gearbox"]["light"]:
right = i
ui.pick_light((right + 1) % 3)
_check(ui.penalty_turns > 0.0, "wrong diagnosis cost nothing")
ui.pick_light(right)
_check(ui.step_index == 1, "right light didn't advance the job")
var guard := 0
while not ui.done and guard < 12:
guard += 1
var st: Dictionary = ui.steps[ui.step_index]
match st["kind"]:
"twist":
ui.twist(float(st["dir"]) * ((float(st["turns"]) + ui.penalty_turns) * TAU + 0.1))
"pump":
for i in int(st["clicks"]):
ui.pump()
"drag":
ui.drop_at(ui.drag_target.get_global_rect().get_center())
_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")
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
# cruise: throttle 3s, car must move
print(" .. cruise")
var game: Game = await _boot("cruise")
if game.car.stats.model_path != "":
_check(game.car._parts.size() >= 8, "model has no detachable det_* parts")
var start: Vector3 = game.car.global_position
Input.action_press("throttle")
for i in 180:
await physics_frame
var dist := start.distance_to(game.car.global_position)
_check(dist > 5.0, "car didn't move (%.1f m)" % dist)
Input.action_release("throttle")
game.free()
# crash mode: traffic spawned, ram the nearest one, score must rise
print(" .. crash")
game = await _boot("crash")
_check(game.traffic.size() == Game.TRAFFIC_COUNT["crash"], "traffic missing")
var victim: TrafficCar = game.traffic[0]
await physics_frame # traffic takes position on its first physics tick
Input.action_press("throttle")
for attempt in 8: # re-aim: victim keeps moving along the loop
if not victim.live:
break
var to_victim := (victim.global_position - game.car.global_position)
game.car.global_position = victim.global_position - victim.global_basis.z * 8.0
game.car.look_at(victim.global_position)
to_victim = (victim.global_position - game.car.global_position).normalized()
game.car.linear_velocity = to_victim * 18.0
for i in 30:
await physics_frame
if not victim.live:
break
Input.action_release("throttle")
_check(not victim.live, "rammed traffic never went dynamic")
_check(game.score > 0.0, "crash mode scored nothing")
var crash_score := game.score
game.free()
# junction: convoys spawn, driving straight in causes the crash -> settle ->
# crashbreaker -> tally
var jcts := Registry.junctions().filter(func(j): return j.level_id == "woolies_carpark")
_check(not jcts.is_empty(), "no woolies junction event")
Game.junction = jcts[0].data
print(" .. junction")
game = await _boot("junction")
_check(game.traffic.size() >= 8, "junction convoys missing (%d)" % game.traffic.size())
Input.action_press("throttle")
for i in 950:
await physics_frame
if game.jstate == "settle" and not game.breaker_used:
Input.action_release("throttle")
Input.action_press("boost")
elif game.breaker_used:
Input.action_release("boost")
if game.over:
break
Input.action_release("throttle")
Input.action_release("boost")
_check(game.jstate != "run", "never crashed into the convoy")
_check(game.breaker_used, "crashbreaker never fired")
_check(game.over and game.score > 0.0, "junction never tallied (score %d)" % int(game.score))
var jct_score := game.score
Game.junction = {}
game.free()
# outrun: roaming rivals exist in cruise; pulling alongside at speed arms the
# duel, and the win path pays out and clears state
game = await _boot("cruise")
_check(game.rivals.size() == Game.ROAM_RIVALS, "no roaming rivals in cruise")
var rr: Rival = game.rivals[0]
await physics_frame
game.car.global_position = rr.global_position + Vector3(3, 0.3, 0)
game.car.linear_velocity = Vector3(0, 0, -12) # above the 8 m/s arming speed
for i in 5:
await physics_frame
if game.outrun_rival != null:
break
_check(game.outrun_rival == rr, "outrun didn't arm on proximity")
var bm0: float = game.car.boost_max
game._outrun_end(true, "test")
_check(game.outrun_rival == null and game.outruns_won == 1, "outrun win didn't settle")
_check(game.car.boost_max > bm0, "outrun win paid no boost")
_check(game.outrun_cd > 0.0, "no re-arm cooldown after outrun")
game.free()
# pursuit: smashing raises heat, heat summons cops, fleeing loses them
print(" .. pursuit")
game = await _boot("cruise")
for i in 10:
game.add_heat(Game.HEAT_PER_SMASH)
_check(game.cops.size() >= Game.COPS_MIN, "heat didn't summon cops")
game.car.global_position += Vector3(900, 0, 900)
for i in 700:
await physics_frame
if game.cops.is_empty():
break
_check(game.escapes == 1 and game.cops.is_empty(), "escape never resolved")
_check(game.heat_cd > 0.0, "no cooldown after escape")
game.free()
# drag: the Willowbank quarter mile -- tree staging, ladder rival, then the box
Game.level_path = levels["willowbank_dragway"]
print(" .. drag")
game = await _boot("drag")
_check(absf(game.drag_len - 402.0) < 5.0, "quarter mile measures %.0f m" % game.drag_len)
_check(game.rivals.size() == 1, "drag had no opponent")
_check(game.drag_state == "stage", "no tree staging phase")
_check(game.drag_rung_now == 0, "fresh ladder not starting at rung 0")
_check(game.rivals[0].stats.resource_path.contains("morry"),
"rung 0 rival isn't Nanna (%s)" % game.rivals[0].stats.resource_path)
_check(game._tree_bulbs.size() == 6, "christmas tree bulbs missing (%d)" % game._tree_bulbs.size())
# TRELLIS'd cashies tools: once the library exists, the paddock is strewn
var tool_dir := DirAccess.open("res://assets/tools")
if tool_dir != null:
var glbs := 0
for f in tool_dir.get_files():
if f.ends_with(".glb"):
glbs += 1
if glbs > 0:
var strewn := 0
for n in game.find_children("*", "RigidBody3D", true, false):
if n.has_meta("cc_tool"):
strewn += 1
_check(strewn == glbs, "tool library has %d GLBs but %d spawned" % [glbs, strewn])
await physics_frame
_check(game.rivals[0].power_scale == 0.0, "rival not held at the tree")
# burnout in the box: DRIFT+GAS warms the tyres and must NOT roll the beams
Input.action_press("drift")
Input.action_press("throttle")
for i in 80:
await physics_frame
if game.drag_state != "stage":
break
_check(game.tyre_temp > 0.25, "burnout warmed nothing (%.2f)" % game.tyre_temp)
_check(not game.jumped, "burnout rolled the beams (red light)")
_check(game.car.burnout_on, "burnout fx never engaged")
Input.action_release("drift")
Input.action_release("throttle")
# hold still like a disciplined launch: no red light, wait out the ambers
var tree_spin := 0
while game.drag_state != "run" and tree_spin < 400:
await physics_frame
tree_spin += 1
_check(game.drag_state == "run", "tree never went green")
_check(not game.jumped, "stationary car scored a red light")
var green_m := (game._tree_bulbs["TreeGreen"] as MeshInstance3D).material_override as StandardMaterial3D
_check(green_m.emission_enabled, "green bulb didn't light")
Input.action_press("throttle")
for i in 10:
await physics_frame
_check(game.revs > 0.0, "revs never climbed")
var g0: int = game.gear
# shift the moment revs enter the window. First gear redlines in ~1.6 s, so
# idling here first pins revs at 1.0 and the shift correctly scores as blown.
var spin := 0
while game.revs < Game.SHIFT_LO and spin < 600: # bounded: never hang the suite
await physics_frame
spin += 1
_check(game.revs >= Game.SHIFT_LO and game.revs < Game.REDLINE,
"revs %.2f not inside the shift window [%.2f, %.2f)" % [game.revs, Game.SHIFT_LO, Game.REDLINE])
Input.action_press("boost")
await physics_frame
Input.action_release("boost")
await physics_frame
_check(game.gear == g0 + 1, "shift didn't change gear")
_check(game.clean_shifts == 1 and game.blown_shifts == 0,
"in-window shift scored as blown (%d/%d)" % [game.clean_shifts, game.blown_shifts])
# grenade the box: three panic shifts nowhere near the window
for i in 3:
game.revs = 0.10
Input.action_press("boost")
await physics_frame
Input.action_release("boost")
await physics_frame
_check(game.box_blown, "gearbox survived three blown shifts (%d strikes)" % game.box_strikes)
_check(game.car.leaking, "grenaded box isn't leaking oil")
var locked: int = game.gear
Input.action_press("boost")
await physics_frame
Input.action_release("boost")
await physics_frame
_check(game.gear == locked, "blown box still shifts")
# and cook the coolant: past 1.0 the head gasket lets go
game.eng_temp = 1.05
await physics_frame
_check(game.gasket_blown, "overcooked engine kept its head gasket")
# finishing the run sends the carnage home as persistent faults
game._drag_finish(true)
_check(Garage.faults("datto_120y").has("gearbox") and Garage.faults("datto_120y").has("gasket"),
"carnage didn't follow the car home as faults")
Garage._reset_for_test() # don't leave a wounded car for later stages
Input.action_release("throttle")
game.free()
Game.level_path = levels.get("woolies_carpark", levels.values()[0])
# derby: opponents spawn in a ring and each hunts something
print(" .. derby")
game = await _boot("derby")
_check(game.derby.size() == Game.DERBY_CARS, "derby field wrong size")
await physics_frame
for d in game.derby:
_check(d.target != null and d.target != d, "derby car has no/self target")
game.free()
# road rage: rivals spawn, a takedown near the player counts and pays
print(" .. rage")
game = await _boot("rage")
_check(game.rivals.size() == Game.RAGE_RIVALS, "rage field wrong size")
var prey: Rival = game.rivals[0]
await physics_frame
game.car.global_position = prey.global_position + Vector3(3, 0.2, 0)
await physics_frame
prey.wreck_started.emit()
_check(game.rage_downs == 1, "rage takedown didn't count (%d)" % game.rage_downs)
game.time_left = 0.05
for i in 10:
await physics_frame
_check(game.over, "rage never tallied")
game.free()
# oncoming: rolling against the road tangent at speed earns boost
game = await _boot("cruise")
await physics_frame
var curve := game.race_path.curve
var road_pt := game.race_path.to_global(curve.sample_baked(20.0))
var tan := (game.race_path.to_global(curve.sample_baked(24.0)) - road_pt).normalized()
# Teleport via the physics server: setting global_position on a
# continuous_cd body SWEEPS it there, slamming through everything en route
# -- the first version of this test wrecked the car before it could earn.
var xf := Transform3D(Basis.looking_at(-tan, Vector3.UP), road_pt + Vector3.UP * 0.75)
PhysicsServer3D.body_set_state(game.car.get_rid(), PhysicsServer3D.BODY_STATE_TRANSFORM, xf)
game.car.wrecked = false
game.car.boost = 0.0
game.car._prev_lv = -tan * 16.0 # don't let the velocity jump read as a crash
game.car.linear_velocity = -tan * 16.0
for i in 45:
await physics_frame
game.car.linear_velocity = -tan * 16.0 # hold course against drag
_check(game.car.boost > 0.05, "oncoming earned no boost (%.3f)" % game.car.boost)
game.free()
# race: grid up at Queensland Raceway -- the paperclip is ~3126 m of Path3D
Game.level_path = levels["qld_raceway"]
print(" .. race")
game = await _boot("race")
var lap: float = game.race_path.curve.get_baked_length()
_check(absf(lap - 3126.0) < 60.0, "paperclip lap measures %.0f m" % lap)
_check(game.rivals.size() == Game.RIVAL_COUNT, "rivals missing")
var p0: float = game.rivals[0].progress
for i in 240:
await physics_frame
var moved := false
for r in game.rivals:
if absf(r.progress - p0) > 3.0 or r.linear_velocity.length() > 3.0:
moved = true
_check(moved, "no rival is driving")
game.free()
print("smoke ok: %d cars, %d levels | cruise %.1f m | crash $%d | junction $%d | outrun arms+pays | pursuit spawns+escapes | quarter mile stages+shifts | derby %d cars | paperclip race AI driving" %
[cars.size(), levels.size(), dist, int(crash_score), int(jct_score), Game.DERBY_CARS])
quit(0)
func _check(cond: bool, msg: String) -> void:
## NOT assert(): a failed assert in a headless SceneTree script halts the
## script but leaves the tree running, so the process spins forever instead
## of failing. One bad assertion cost 53 minutes of wall clock before this.
if cond:
return
push_error(msg)
print("SMOKE FAIL: %s" % msg)
quit(1)
func _boot(mode: String) -> Game:
Game.mode = mode
var game: Game = (load("res://src/game.tscn") as PackedScene).instantiate()
root.add_child(game)
await process_frame # _ready is deferred until the main loop runs
return game