extends SceneTree ## Headless playthrough of the whole Phase 0 room. ## Run: godot --headless --path . -s test/integration.gd var fails := 0 func check(cond: bool, msg: String) -> void: if cond: print(" ok: " + msg) else: fails += 1 printerr(" FAIL: " + msg) # exposure accrues on physics ticks (real-time), so waits must count # physics frames — process frames run uncapped-fast in headless func wait(frames: int) -> void: for i in frames: await physics_frame func wait_until(cond: Callable, max_frames: int) -> bool: for i in max_frames: if cond.call(): return true await physics_frame return cond.call() func find_i(kind_or_prompt: String, min_x := -999.0) -> Node3D: for n in get_nodes_in_group("interactable"): if n.global_position.x < min_x: continue if str(n.get("kind")) == kind_or_prompt or str(n.get("prompt_text")) == kind_or_prompt: return n return null func find_ex(dname: String) -> Node: for n in get_nodes_in_group("exposable"): if n.display_name == dname: return n return null func _initialize() -> void: root.add_child(load("res://scenes/main.tscn").instantiate()) _run() func _run() -> void: Engine.time_scale = 3.0 # ponytail: same sim, less wall-clock await wait(10) var main := root.get_node("Main") var droid: Droid = main.droid print("== donation ==") var socket := find_i("DONATE WHEEL TO DOOR BUS") socket.interact(droid) check(main.door1.is_open and droid.wheels == 1, "wheel donated, door1 open, droid slow") print("== be the wire ==") droid.global_position = Vector3(0, 0.2, 15.3) droid.core.force_state("charged") check(await wait_until(func(): return main.door2.is_open, 120), "charged droid in the bus gap closes the circuit") var wheel := find_i("TAKE WHEEL") wheel.interact(droid) check(droid.wheels == 2, "replacement wheel reinstalled") print("== electrified puddle ==") droid.global_position = Vector3(-2.6, 0.2, 24) droid.core.force_state("charged") check(await wait_until(func(): return main.puddle_live.enabled, 120), "puddle goes live from droid charge") check(await wait_until(func(): return main.relay.state == "tripped", 300), "live puddle trips sluice relay") check(main.door3.is_open, "relay opens door3") check(droid.shell.state == "wet" or droid.stun > 0, "droid got wet/shocked in the process") droid.global_position = Vector3(0, 0.2, 25) await wait_until(func(): return droid.stun <= 0 and main.puddle_live.enabled == false, 600) print("== leaky bucket ==") var bucket := find_i("bucket") bucket.interact(droid) check(droid.held == bucket, "bucket taken") var tank := find_i("POUR COOLANT") for trip in 2: droid.global_position = Vector3(-2.7, 0.2, 28) check(await wait_until(func(): return bucket.ex.state == "full", 400), "bucket fills at valve (trip %d)" % (trip + 1)) droid.global_position = Vector3(2.4, 0.2, 32.0) await wait(5) tank.interact(droid) check(tank.count == 2 and main.door4.is_open, "two pours open the sluice door") print("== drum rotation ==") # droid is wet from the valve; wet+charged self-shocks (by design) — dry off first droid.shell.force_state("dry") droid.global_position = Vector3(2.9, 0.2, 18.6) droid.core.force_state("charged") check(await wait_until(func(): return main.drum_turned, 180), "charged touch starts the drum") await wait(200) # tween is 3s var crate := find_i("crate") check(crate != null and crate.global_position.y < 2.0 and crate.global_position.x < 6.5, "rotation brought the crate within reach") droid.global_position = Vector3(4.2, 0.2, 22) crate.interact(droid) check(droid.held == crate, "crate taken (bucket dropped)") var plate := find_i("PLACE CARGO ON PLATE") droid.global_position = Vector3(2.0, 0.2, 24.2) plate.interact(droid) check(main.bonus_door.is_open, "cargo plate opens bonus bay") print("== solar exit ==") main.t_sol = 5.0 # daytime droid.global_position = Vector3(0, 0.2, 37) check(await wait_until(func(): return main.exit_door.is_open, 120), "daylight powers solar cell, exit opens") main.t_sol = 45.0 # nightfall check(await wait_until(func(): return not main.exit_door.is_open, 300), "night kills solar cell, exit closes") main.t_sol = 5.0 print("== hall done ==") await wait_until(func(): return main.exit_door.is_open, 120) droid.global_position = Vector3(0, 0.2, 42) check(await wait_until(func(): return main.hall_done, 60), "crossing the exit completes the hall") print("== reactor ==") var slug := find_i("slug") slug.interact(droid) check(droid.held == slug, "thermal slug taken") droid.global_position = Vector3(-12.4, 0.2, 47.0) check(await wait_until(func(): return slug.ex.state == "hot", 300), "slug heats at the vent") droid.global_position = Vector3(-20.9, 0.2, 53) check(await wait_until(func(): return find_ex("WAX SEAL").state == "melted", 300), "hot slug melts the wax seal") droid.global_position = Vector3(-27.8, 0.2, 52.6) check(await wait_until(func(): return find_ex("CORE IGNITER").state == "primed", 300), "slug primes the igniter") droid.shell.force_state("dry") droid.core.force_state("charged") check(await wait_until(func(): return main.systems.reactor, 300), "current ignites the primed core") check(main.ice_wall == null and main.melt_pool.enabled, "reactor melts the hydro ice wall") print("== hydroponics ==") droid.global_position = Vector3(12.8, 0.2, 56.5) droid.core.force_state("charged") check(await wait_until(func(): return find_ex("SPRINKLER VALVE").state == "open", 300), "current frees the sprinkler valve") main.t_sol = 5.0 check(await wait_until(func(): return find_ex("GROWTH VINE").state == "sprouted", 600), "sprinkler water sprouts the vine") check(await wait_until(func(): return find_ex("GROWTH VINE").state == "grown", 600), "daylight grows the vine (grate opens)") var bucket2 := find_i("bucket", 9.0) bucket2.interact(droid) check(droid.held == bucket2, "hydro bucket taken") var basin := find_i("POUR COOLANT", 9.0) for trip in 2: droid.global_position = Vector3(24.4, 0.2, 52.4) check(await wait_until(func(): return bucket2.ex.state == "full", 500), "bucket fills at the melt pool (trip %d)" % (trip + 1)) droid.global_position = Vector3(29.4, 0.2, 52.6) await wait(5) basin.interact(droid) check(main.systems.hydro, "two pours pressurize the water mains") print("== comms ==") var beacon := find_i("beacon") beacon.interact(droid) check(droid.held == beacon, "radio beacon taken (bucket dropped)") droid.global_position = Vector3(-1.2, 0.2, 63.2) check(await wait_until(func(): return beacon.ex.state == "broadcasting", 300), "beacon charges to broadcasting at the post") droid.global_position = Vector3(-5.5, 0.2, 68.5) check(await wait_until(func(): return find_ex("CRYSTAL RESONATOR (SEALED IN WALL)").state == "rung", 300), "radio rings the resonator through the wall") droid.global_position = Vector3(-7.2, 0.2, 74.2) await wait_until(func(): return beacon.ex.state == "broadcasting", 300) droid.global_position = Vector3(-4.4, 0.2, 75.4) check(await wait_until(func(): return find_ex("MAIN TRANSMITTER").state == "warmed", 300), "beacon warms the transmitter") droid.shell.force_state("dry") droid.core.force_state("charged") check(await wait_until(func(): return main.systems.comms, 300), "current fires the transmitter") print("== bridge gate ==") check(await wait_until(func(): return main.bridge_door.is_open, 120), "3/3 systems unseal the bridge") var helm := find_i("TAKE THE HELM") droid.global_position = Vector3(2.2, 0.2, 76.2) helm.interact(droid) check(not main.free_will and main.ending_id == "", "helm rejects a unit without free will") print("== hidden path: the uncatalogued component ==") # bulletin #7 reversed: coolant + reactor flux -> seed var bucket2b := find_i("bucket", 9.0) bucket2b.interact(droid) check(droid.held == bucket2b, "bucket retaken for the crystal chain") droid.global_position = Vector3(24.4, 0.2, 52.4) await wait_until(func(): return bucket2b.ex.state == "full", 500) droid.global_position = Vector3(-12.4, 0.2, 47.0) check(await wait_until(func(): return bucket2b.ex.state == "seed-crystal", 600), "reactor flux seeds a crystal in the coolant") # bulletin #9 reversed: mineral solid + daylight -> lattice main.t_sol = 5.0 droid.global_position = Vector3(0, 0.2, 37) check(await wait_until(func(): return bucket2b.ex.state == "lattice", 900), "skylight grows the seed into a lattice") # bulletin #12 reversed: quench in charged coolant droid.shell.force_state("dry") droid.global_position = Vector3(-2.6, 0.2, 24) droid.core.force_state("charged") check(await wait_until(func(): return bucket2b.ex.state == "uncatalogued", 600), "electrified puddle quenches the lattice — UNCATALOGUED") print("== free will ==") await wait_until(func(): return droid.stun <= 0, 600) droid.global_position = Vector3(7.5, 0.2, 67.3) var kiosk := find_i("SCAN OWN CHASSIS — PORT 00") kiosk.interact(droid) check(main.free_will and droid.held == null, "kiosk installs the crystal in PORT 00") droid.global_position = Vector3(2.2, 0.2, 76.2) helm.interact(droid) check(main.ending_id == "free", "free will takes the helm — sunset ending") var console := find_i("INSERT CORE PROCESSOR (ORBIT CORRECTION)") console.interact(droid) check(main.ending_id == "free", "console cannot reclaim the ship") if fails == 0: print("INTEGRATION PASS — full room playthrough OK") else: printerr("INTEGRATION: %d FAILURES" % fails) quit(1 if fails > 0 else 0)