OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_creature_target.gd

115 lines · gd ref:
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"])))