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>
81 lines
2.6 KiB
GDScript
81 lines
2.6 KiB
GDScript
extends SceneTree
|
|
## Headless check of the one schema everything runs on.
|
|
## Run: godot --headless --path . -s test/selfcheck.gd
|
|
|
|
var fails := 0
|
|
|
|
|
|
func check(cond: bool, msg: String) -> void:
|
|
if not cond:
|
|
fails += 1
|
|
printerr("FAIL: " + msg)
|
|
|
|
|
|
func _initialize() -> void:
|
|
var Ex = load("res://src/exposable.gd")
|
|
|
|
# solar-style: transition after duration, not before; sustain; decay
|
|
var solar = Ex.new().setup("solar", "unpowered", [
|
|
{"from": "unpowered", "to": "powered", "element": "light",
|
|
"duration": 0.5, "decay_to": "unpowered", "decay_s": 1.5},
|
|
])
|
|
solar.expose("light", 0.3)
|
|
check(solar.state == "unpowered", "no transition before duration met")
|
|
solar.expose("light", 0.3)
|
|
check(solar.state == "powered", "transition at duration")
|
|
# sustain: keep exposing, decay must not fire
|
|
for i in 3:
|
|
solar._process(1.0)
|
|
solar.expose("light", 0.016)
|
|
check(solar.state == "powered", "sustained exposure refreshes decay")
|
|
# stop exposing: decays back
|
|
solar._process(1.6)
|
|
check(solar.state == "unpowered", "decays back after exposure stops")
|
|
|
|
# wrong element is ignored
|
|
solar.expose("water", 5.0)
|
|
check(solar.state == "unpowered", "wrong element ignored")
|
|
|
|
# progress bleed-off: partial exposure resets after idle gap
|
|
var solar2 = Ex.new().setup("solar2", "unpowered", solar.transitions)
|
|
solar2.expose("light", 0.3)
|
|
solar2._process(1.0)
|
|
solar2.expose("light", 0.3)
|
|
check(solar2.state == "unpowered", "partial progress bleeds off after gap")
|
|
|
|
# fuse-style permanence
|
|
var fuse = Ex.new().setup("fuse", "intact", [
|
|
{"from": "intact", "to": "blown", "element": "electricity",
|
|
"duration": 0.1, "permanent": true},
|
|
])
|
|
fuse.expose("electricity", 0.2)
|
|
check(fuse.state == "blown" and fuse.locked, "permanent transition locks")
|
|
fuse.force_state("intact")
|
|
check(fuse.state == "blown", "locked state refuses force_state")
|
|
|
|
# bucket-style: fill, then leak empty
|
|
var bucket = Ex.new().setup("bucket", "empty", [
|
|
{"from": "empty", "to": "full", "element": "water",
|
|
"duration": 3.0, "decay_to": "empty", "decay_s": 18.0},
|
|
])
|
|
for i in 40:
|
|
bucket.expose("water", 0.1)
|
|
check(bucket.state == "full", "bucket fills under stream")
|
|
bucket._process(19.0)
|
|
check(bucket.state == "empty", "bucket leaks back to empty")
|
|
|
|
# force_state clears decay
|
|
var b2 = Ex.new().setup("b2", "empty", bucket.transitions)
|
|
for i in 40:
|
|
b2.expose("water", 0.1)
|
|
b2.force_state("empty")
|
|
check(b2.decay_remaining() < 0, "force_state clears decay")
|
|
b2._process(0.1)
|
|
check(b2.state == "empty", "poured bucket stays empty")
|
|
|
|
if fails == 0:
|
|
print("SELFCHECK PASS (11 checks)")
|
|
else:
|
|
printerr("SELFCHECK: %d FAILURES" % fails)
|
|
quit(1 if fails > 0 else 0)
|