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