exposure/test/integration.gd
type-two d414ccd6e4 Phase 0 grey-box: playable room, one schema, 19-check headless playthrough
Exposable (the one state-machine schema) + ElementField (distance-based
emitters) drive everything: wall components, the droid's shell/core, the
leaky bucket. Room chain: wheel donation -> be-the-wire -> electrified
puddle/relay -> leaky-bucket ferry -> rotating cargo drum (bonus) ->
solar-gated exit on a 60s day/night cycle. Calculator-green HUD with
PORT 00 tease.

test/selfcheck.gd: 11 schema checks. test/integration.gd: full-room
headless playthrough, 19 checks, all passing.

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

128 lines
4.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) -> Node3D:
for n in get_nodes_in_group("interactable"):
if str(n.get("kind")) == kind_or_prompt or str(n.get("prompt_text")) == kind_or_prompt:
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("== win ==")
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.won, 60), "crossing the exit wins Phase 0")
if fails == 0:
print("INTEGRATION PASS — full room playthrough OK")
else:
printerr("INTEGRATION: %d FAILURES" % fails)
quit(1 if fails > 0 else 0)