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")