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>
128 lines
4.3 KiB
GDScript
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)
|