extends SceneTree
## GATE — bot bodies get SIMULATION TIERS + SLEEP (creatures → bots, #114 phase 1).
## Creatures and bots are one concept (user 2026-10-13); the tiers were the creature
## spawner's alone, so a placed bot's body ticked every physics frame however far away.
## T1 a placed bot's body is scheduled by BotRegistry (it stops ticking itself)
## T2 near an observer it is NEAR and ticks
## T3 with every observer far away it SLEEPS: zero ticks
## T4 back near, it ticks again
## T5 a body rebuilt (body kind changed) swaps schedulers cleanly: old out, new in
## T6 deleting the bot takes its body out of the scheduler
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_bot_tiers.gd
const HOME := Vector3(4, 0.5, 4)
const FAR := Vector3(4000, 0.5, 4000)
var ok := true
var checks := 0
var _frame := 0
var _map: Node
var _reg: Node
var _dot := ""
var _obs: Node3D
var _step := 0
var _wait := 0
var _ticks0 := 0
var _old_body: Node3D
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 _body() -> Node3D:
var n: Node = _map.object_node(_dot)
return n.body() if n != null and n.has_method("body") else null
func _sched() -> CreatureScheduler:
return _reg.scheduler()
## Untyped: a freed body must be askable too (the scheduler must not hold it).
func _held(b: Variant) -> bool:
return b != null and (_sched().get("_e") as Dictionary).has(b)
func _ticks(b: Node3D) -> int:
return int(_sched().tick_count.get(b, 0))
## Every observer to `at`: the stand-in player and, if the map has one, the camera.
func _look_from(at: Vector3) -> void:
_obs.global_position = at
var cam := root.get_viewport().get_camera_3d()
if cam != null:
cam.global_position = at
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)
_obs = Node3D.new()
_obs.name = "GateObserver"
root.add_child(_obs)
_obs.add_to_group("player")
return false
if _frame < 30:
return false
# PHYSICS frames, not process frames: headless, process frames run far faster than
# the 60 Hz physics tick the scheduler (and its 0.5 s retier) runs on.
if Engine.get_physics_frames() < _wait:
return false
_step += 1
match _step:
1: _t1()
2: _t2()
3: _t3_start()
4: _t3_check()
5: _t4()
6: _t5()
7: _t6()
8: _t6_check()
_:
if checks < 11: # 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
_wait = Engine.get_physics_frames() + 45 # > RETIER_INTERVAL (0.5 s) at 60 Hz: a retier has run
return false
func _t1() -> void:
_reg = root.get_node("/root/BotRegistry")
_look_from(HOME)
_dot = _map.create_object("bot", Transform3D(Basis(), HOME))
_map.set_object_props(_dot, {"brain": "idle", "body": "crawler"})
var b := _body()
_p(b is GroundCrawler, "T1 the bot has a crawler body", b)
_p(_held(b), "T1 BotRegistry schedules it")
_p(b != null and bool(b.get("scheduled")), "T1 and it no longer ticks itself")
func _t2() -> void:
var b := _body()
_p(_sched().tier_of(b) == CreatureScheduler.Tier.NEAR, "T2 near an observer it is NEAR", _sched().tier_of(b))
_p(_ticks(b) > 0, "T2 and ticks", _ticks(b))
func _t3_start() -> void:
_look_from(FAR)
func _t3_check() -> void:
var b := _body()
_p(_sched().tier_of(b) == CreatureScheduler.Tier.SLEEP, "T3 every observer far away: it SLEEPS",
_sched().tier_of(b))
_ticks0 = _ticks(b)
func _t4() -> void:
var b := _body()
_p(_ticks(b) == _ticks0, "T3 zero ticks while asleep", [_ticks0, _ticks(b)])
_look_from(HOME)
_ticks0 = _ticks(b)
func _t5() -> void:
var b := _body()
_p(_sched().tier_of(b) == CreatureScheduler.Tier.NEAR and _ticks(b) > _ticks0, "T4 back near: it ticks again",
[_sched().tier_of(b), _ticks0, _ticks(b)])
_old_body = b
_map.set_object_props(_dot, {"body": "hopper"})
var nb := _body()
_p(nb is Hopper and _held(nb), "T5 a rebuilt body (hopper) is scheduled", nb)
_p(not _held(_old_body), "T5 and the old crawler is out of the scheduler")
func _t6() -> void:
_old_body = _body()
_map.delete_objects([_dot])
func _t6_check() -> void:
_p(not _held(_old_body), "T6 deleting the bot takes its body out of the scheduler")