OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_bot_creature_body.gd

139 lines · gd ref:
extends SceneTree
## GATE — CREATURE BODIES on registered bots (creatures → bots, #114 phase 3).
## A spawner object registers bots with a crawler / hopper / serpent body; every peer
## builds the same creature from the registry row and the scheduler ticks it.
##   C1  register(body=crawler, at=P): a GroundCrawler stands at P in its container,
##       scheduled, and the row carries P (so every peer builds it there)
##   C2  move() steers it (set_target) and writes the position row
##   C3  the authority REVIVES a hopper row at the row's position, scheduled
##   C4  a REMOTE peer renders a serpent row as a scheduled creature at its position,
##       steers it from the position row, and drops it (unscheduled) when the row goes
##   C5  despawn frees the body and takes it out of the scheduler first
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_bot_creature_body.gd

const P := Vector3(3, 0, 3)
const PH := Vector3(-5, 0, 2)
const PS := Vector3(8, 4, -6)

var ok := true
var checks := 0
var _frame := 0
var _br: Node
var _map: Node3D
var _real_state: Object


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 _held(n: Variant) -> bool:
	return n is Node3D and is_instance_valid(n) and (_br.scheduler().get("_e") as Dictionary).has(n)


func _node(id: String) -> Variant:
	return (_br.get("_bots") as Dictionary).get(id, {}).get("node")


func _process(_d: float) -> bool:
	_frame += 1
	if _frame == 1:
		return false
	if _frame == 2:
		_run()
		return false
	if _frame == 4:
		_after_free()
		_br.state = _real_state
		_br.map_container = null
		if checks < 19:          # a gate that died early must not read as a pass
			ok = false
			print("FAIL: only %d checks ran -- the gate died before its assertions" % checks)
		print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
		quit(0 if ok else 1)
		return true
	return false


var _c1_node: Node3D
var _c4_node: Node3D


func _run() -> void:
	_br = root.get_node("/root/BotRegistry")
	_map = Node3D.new()
	_map.name = "GateMapCreatures"
	root.add_child(_map)
	_br.map_container = _map
	_real_state = _br.state
	_br.state = load("res://scenes/test/permissive_state.gd").new()

	# ---- C1 ----
	var r: Dictionary = _br.register("cb-crawler", ["map"], "crawler", {"kind": "none"}, P)
	_p(bool(r.get("ok", false)), "C1 register(body=crawler, at) ok", r)
	var n: Variant = _node("cb-crawler")
	_c1_node = n as Node3D
	_p(n is GroundCrawler and _map.is_ancestor_of(n), "C1 a GroundCrawler in its container", n)
	_p(n is Node3D and (n as Node3D).position.distance_to(P) < 0.01, "C1 standing at P",
		(n as Node3D).position if n is Node3D else "none")
	_p(_held(n), "C1 scheduled (tiers + sleep)")
	var row: Variant = _br.state.get_state("world:bots:registry:cb-crawler")
	_p(row is Dictionary and str((row as Dictionary).get("pos")) == str([P.x, P.y, P.z]), "C1 the row carries P", row)

	# ---- C2 ----
	var to := P + Vector3(4, 0, 0)
	_p(bool(_br.move("cb-crawler", to).get("ok", false)), "C2 move() accepts a creature body")
	_p(n is Node3D and (n.get("_wander_target") as Vector3).distance_to(to) < 0.01, "C2 and steers it there",
		n.get("_wander_target") if n is Node3D else "none")
	_p(str(_br.state.get_state("world:bots:pos:cb-crawler")) == str([to.x, to.y, to.z]), "C2 and writes the position row")

	# ---- C3: the authority revives a declared hopper ----
	_br._on_state_changed("world:bots:registry:cb-hopper",
		{"body": "hopper", "brain": {"kind": "none"}, "pos": [PH.x, PH.y, PH.z]}, 0)
	var h: Variant = _node("cb-hopper")
	_p(h is Hopper and (h as Node3D).position.distance_to(PH) < 0.01, "C3 the authority revives a hopper at its row's position",
		(h as Node3D).position if h is Node3D else h)
	_p(_held(h), "C3 scheduled")

	# ---- C4: a peer that is NOT the authority renders ----
	var game := Node.new()
	game.name = "Game"
	root.add_child(game)
	game.set_multiplayer_authority(999)
	_p(not _br._is_authority(), "C4 (setup) this peer is not the authority")
	_br._on_state_changed("world:bots:registry:cb-serpent",
		{"body": "serpent", "brain": {"kind": "script_file", "path": "res://bots/roam.gd"}, "pos": [PS.x, PS.y, PS.z]}, 0)
	var s: Variant = (_br.get("_remote") as Dictionary).get("cb-serpent")
	_c4_node = s as Node3D
	# x / z from the row; the HEIGHT is the serpent's own (FlyingSerpent._init_positions
	# flies at its seeded preferred_altitude — the same on every peer).
	_p(s is FlyingSerpent and Vector2((s as Node3D).position.x - PS.x, (s as Node3D).position.z - PS.z).length() < 0.01,
		"C4 a remote serpent over its row's position", (s as Node3D).position if s is Node3D else s)
	_p(_held(s), "C4 scheduled on the rendering peer too")
	var lbl: Label3D = (s as Node).get_node_or_null("Label") if s is Node else null
	_p(lbl != null and lbl.text.contains("script_file") and not lbl.text.contains("{"), "C4 its label names the brain kind",
		lbl.text if lbl != null else "none")
	var tgt := PS + Vector3(0, 0, 5)
	_br._on_state_changed("world:bots:pos:cb-serpent", [tgt.x, tgt.y, tgt.z], 1)
	_p(s is FlyingSerpent and (s.get("_wander_target") as Vector3).distance_to(Vector3(tgt.x, (s.get("_wander_target") as Vector3).y, tgt.z)) < 0.01,
		"C4 steered from the position row", s.get("_wander_target") if s is Node3D else "none")
	_br._on_state_changed("world:bots:registry:cb-serpent", null, 0)
	_p(not (_br.get("_remote") as Dictionary).has("cb-serpent") and not (_br.scheduler().get("_e") as Dictionary).has(_c4_node),
		"C4 the row going drops it, unscheduled")
	root.remove_child(game)
	game.free()

	# ---- C5 ----
	_br.despawn("cb-crawler")
	_br.despawn("cb-hopper")
	_p(not (_br.scheduler().get("_e") as Dictionary).has(_c1_node), "C5 despawn takes the body out of the scheduler")
	_p(_br.state.get_state("world:bots:registry:cb-crawler") == null, "C5 and an explicit despawn retires the row")


func _after_free() -> void:
	_p(not is_instance_valid(_c1_node), "C5 the crawler body is freed")