The dealgod.pro products database holds ~82k secondhand tool listings with photos (69k cached on the VPS). This turns the good ones into game assets: 15 tool classes curated from 45 candidates (socket set, air compressor, chain block, car ramp, OBD reader, trolley jack, engine crane, engine stand, toolbox, angle grinder, welder, jerry can, battery charger, jack stands, grease gun), each run through the MODELBEAST farm by the new tools/cc_to_glb.py: bg_remove_local cutout, then trellis_mac image->3D (1024 tier). Retries with backoff after a transient queue 401; the jerry can made trellis_mac exit 2 on two different photos and went through hunyuan3d_mlx instead (--operator flag), which ate it happily. Raw TRELLIS output is ~20MB per prop, so tools/slim_glb.py (headless Blender) decimates to 18k tris and re-exports with 1024 JPEG textures: the library lands at 27MB total for 15 props instead of ~300MB. The Willowbank paddock strews the whole library as RigidBodies -- TOOL_SPECS in dragway.gd gives each class its real-world size (TRELLIS output is unit-ish; the AABB is measured live and scaled) and an honest mass, so clipping the 70kg engine crane is a different event to punting the grease gun. The loader is directory-driven: drop a new GLB into assets/tools/ and it appears in the paddock next boot. Textures inherit whatever branding was in the source photos -- fine for secondhand background clutter, flagged in the README for anything that gets promoted to hero status. Smoke suite asserts every GLB in the library spawns as a prop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
385 lines
16 KiB
GDScript
385 lines
16 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()
|
|
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
|