extends SceneTree
## GATE — creature bodies driven by TARGET (bot MVP, 2026-10-04).
## C1 crawler walks to a target and STOPS there (no wander takeover, no drift)
## C2 hopper hops to a target and bounces in place within reach
## C3 serpent flies to a target and circles it, at hover height above it
## C4 a far target TELEPORTS the body (spawn / teleport), a near one walks
## C5 no target: each body still wanders on its own (unchanged default)
## On a real map_flat (a floor to stand on), with real frames.
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_creature_target.gd
var ok := true
var checks := 0
var _frame := 0
var _map: Node
var _c: Dictionary = {} # name -> creature
var _start: Dictionary = {} # name -> start position
var _probe: Dictionary = {} # name -> position at the "stopped?" probe
var _arrived_at := -1
func _p(c: bool, m: String, got: Variant = "") -> void:
checks += 1
print(("ok " if c else "FAIL: ") + m + ("" if str(got) == "" else " (" + str(got) + ")"))
if not c:
ok = false
func _xz(v: Vector3) -> Vector2:
return Vector2(v.x, v.z)
func _head(n: String) -> Vector3:
var c: Node3D = _c[n]
if c.get("_segment_positions") != null and (c.get("_segment_positions") as Array).size() > 0:
return (c.get("_segment_positions") as Array)[0]
if c.get("_base_position") != null:
return c.get("_base_position")
return c.global_position
func _spawn(n: String, node: Node3D, at: Vector3) -> void:
node.name = n
_map.add_child(node)
node.global_position = at
var rng := RandomNumberGenerator.new()
rng.seed = 1234
node.initialize(rng)
_c[n] = node
func _process(_d: float) -> bool:
_frame += 1
if _frame == 1:
_map = (load("res://scenes/maps/map_flat.tscn") as PackedScene).instantiate()
root.add_child(_map)
return false
if _frame == 20:
_spawn("crawler", GroundCrawler.new(), Vector3(0, 0.5, 0))
_spawn("hopper", Hopper.new(), Vector3(20, 0.2, 0))
_spawn("serpent", FlyingSerpent.new(), Vector3(-20, 0, 0))
_spawn("wander", GroundCrawler.new(), Vector3(0, 0.5, 30))
for n: String in _c:
_start[n] = _head(n)
(_c["crawler"] as Node3D).set_target(Vector3(6, 0, 0))
(_c["hopper"] as Node3D).set_target(Vector3(25, 0, 0))
(_c["serpent"] as Node3D).set_target(Vector3(-20, 0, 6))
return false
# Record the frame the crawler first reports arrival, and where it was 30 frames
# later — drift is measured from THERE, so "had not arrived yet" and "keeps moving
# after arriving" are told apart rather than guessed.
if _frame > 20 and _arrived_at < 0 and bool((_c["crawler"] as Node3D).get("_arrived")):
_arrived_at = _frame
if _arrived_at > 0 and _frame == _arrived_at + 30:
_probe["crawler"] = _head("crawler")
if _frame == 20 + 60 * 8: # ~8 s of physics frames
for n: String in _c:
if n != "crawler":
_probe[n] = _head(n)
return false
if _frame < 20 + 60 * 10:
return false
_check()
print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
quit(0 if ok else 1)
return true
func _check() -> void:
# C1
var ch := _head("crawler")
_p(_xz(ch).distance_to(Vector2(6, 0)) < 1.2, "C1 crawler reached its target", _xz(ch))
_p(_arrived_at > 0 and _probe.has("crawler"), "C1 arrival recorded", "frame %d (%.1f s)" % [_arrived_at, (_arrived_at - 20) / 60.0])
if _probe.has("crawler"):
_p(_xz(ch).distance_to(_xz(_probe["crawler"])) < 0.05, "C1 and STOPPED after arriving (no drift)",
"%.3f m over %d frames" % [_xz(ch).distance_to(_xz(_probe["crawler"])), _frame - _arrived_at - 30])
_p(bool((_c["crawler"] as Node3D).get("_arrived")), "C1 reports arrived")
# C2
var hh := _head("hopper")
_p(_xz(hh).distance_to(Vector2(25, 0)) < 2.0, "C2 hopper hopped to its target", _xz(hh))
# C3: circling, within orbit range, at hover height
var sh := _head("serpent")
_p(_xz(sh).distance_to(Vector2(-20, 6)) < 10.0, "C3 serpent went to its target and circles it", _xz(sh).distance_to(Vector2(-20, 6)))
_p(absf(sh.y - FlyingSerpent.BOT_HOVER) < 4.0, "C3 at hover height above the target", sh.y)
# C4: teleport on a far target, walk on a near one
var cr: Node3D = _c["crawler"]
cr.set_target(Vector3(60, 0, 60))
_p(_xz(_head("crawler")).distance_to(Vector2(60, 60)) < 1.0, "C4 a far target places the body there", _xz(_head("crawler")))
var before := _head("crawler")
cr.set_target(before + Vector3(3, 0, 0))
_p(_xz(_head("crawler")).distance_to(_xz(before)) < 0.01, "C4 a near target does NOT teleport")
# C5
_p(_xz(_head("wander")).distance_to(_xz(_start["wander"])) > 2.0, "C5 with no target a body still wanders",
_xz(_head("wander")).distance_to(_xz(_start["wander"])))