exposure/test/integration.gd
type-two 2ef9487284 Hidden ending: the Uncatalogued Component, Port 00, and the sunset
The crystal chain lives entirely in the bucket's existing schema —
bulletins #7/#9/#12 read in reverse: coolant + reactor flux -> seed,
seed + grow-bed daylight -> lattice, lattice quenched in charged
coolant -> UNCATALOGUED. Bridge kiosk installs it in Port 00; the helm
(which rejects you as INVENTORY all game) accepts, and the ship flies
into a permanent sunset. Console donation remains ending A. Red
herrings: zapped coolant, water-quenched slug. New bulletin #9 in
hydro + crew graffiti hint in comms. Integration test now plays the
hidden path end to end — all checks passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 22:24:41 +10:00

239 lines
9.3 KiB
GDScript

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)