extends SceneTree
## Gate: two bots do not spawn inside each other.
##
## register() wrote [0.0, 0.0, 0.0] for every position body and added the node
## without touching its transform, so every body started at the container's
## origin. A bot with brain=none never moves, so the whole matrix stacked into
## one box at the origin and looked like a single bot.
##
## The offset is DERIVED FROM THE ID, not assigned, for two reasons:
## - it needs no API change and no caller has to think about placement
## - it must agree across peers. A remote peer builds the marker before any
## position row arrives, so if the two sides picked different spots the body
## would visibly jump the moment the first row landed.
##
## The hash is computed here rather than with GDScript's hash(), which is not
## promised to be stable across engine versions -- and "stable across peers"
## includes peers on different builds.
var ok := true
var _ran := false
func _p(c: bool, m: String) -> void:
print(("ok " if c else "FAIL: ") + m)
if not c:
ok = false
func _process(_delta: float) -> bool:
if _ran:
return true
_ran = true
_run()
print("GATE bot_spawn_spread: %s" % ("PASS" if ok else "FAIL"))
if not ok:
quit(1)
return true
func _run() -> void:
var br: Node = root.get_node_or_null("BotRegistry")
_p(br != null, "BotRegistry autoload present")
if br == null:
return
var argc := -1
for m: Dictionary in br.get_method_list():
if String(m.get("name", "")) == "_spawn_offset_for":
argc = (m.get("args", []) as Array).size()
break
_p(argc >= 1, "BotRegistry exposes _spawn_offset_for(id) — got %d args" % argc)
if argc < 1:
return
var ids := ["mx-pos-map", "mx-pos-room", "mx-pos-session", "a", "b", "beacon", "patrol"]
var seen: Dictionary = {}
var positions: Array[Vector3] = []
for id: String in ids:
var p: Vector3 = br._spawn_offset_for(id)
positions.append(p)
var key := "%.3f,%.3f,%.3f" % [p.x, p.y, p.z]
_p(not seen.has(key), "%s gets its own spot (%s)" % [id, key])
seen[key] = id
# ---- deterministic: the same id must always land in the same place ----
for id: String in ids:
_p(br._spawn_offset_for(id) == br._spawn_offset_for(id),
"%s is deterministic" % id)
# ---- bounded and above the floor ----
for i in positions.size():
var p: Vector3 = positions[i]
var flat := Vector2(p.x, p.z).length()
_p(flat > 0.5, "%s is off the exact origin (%.2f)" % [ids[i], flat])
_p(flat < 40.0, "%s is not flung across the map (%.2f)" % [ids[i], flat])
_p(p.y > 0.0, "%s sits above the floor, not half-buried (y=%.2f)" % [ids[i], p.y])
# ---- and far enough apart to be separate boxes, not a pile ----
var min_sep := 1e9
for i in positions.size():
for j in range(i + 1, positions.size()):
min_sep = min(min_sep, positions[i].distance_to(positions[j]))
_p(min_sep > 0.6, "closest pair is further apart than a marker is wide (%.2f)" % min_sep)
# ---- register() must actually USE it ----
var fake_map := Node3D.new()
fake_map.name = "GateMapSpread"
root.add_child(fake_map)
br.map_container = fake_map
var real_state: Object = br.state
br.state = load("res://scenes/test/permissive_state.gd").new()
_p(bool(br.register("sp_a", ["map"], "position").get("ok", false)), "sp_a registers")
_p(bool(br.register("sp_b", ["map"], "position").get("ok", false)), "sp_b registers")
var pa: Variant = br.state.get_state("world:bots:pos:sp_a")
var pb: Variant = br.state.get_state("world:bots:pos:sp_b")
_p(pa is Array and pb is Array, "both wrote a position row")
if pa is Array and pb is Array:
_p(str(pa) != str(pb), "and the two rows are NOT the same point (%s vs %s)" % [str(pa), str(pb)])
_p(str(pa) != "[0, 0, 0]", "sp_a did not spawn at the bare origin")
var na: Node3D = fake_map.get_node_or_null(^"Bot_sp_a") as Node3D
var nb: Node3D = fake_map.get_node_or_null(^"Bot_sp_b") as Node3D
_p(na != null and nb != null, "both nodes exist")
if na != null and nb != null:
_p(na.position != nb.position, "and the NODES are apart too, not just the rows")
_p(na.position != Vector3.ZERO, "the node is placed, not left at the container origin")
# SELF-CHECK: a constant offset would satisfy "deterministic" and "above the
# floor" and fail only the distinctness checks — so assert directly that the
# function discriminates on its input.
_p(br._spawn_offset_for("x") != br._spawn_offset_for("y"),
"self-check: different ids give different offsets, so it reads the id")
br.despawn("sp_a")
br.despawn("sp_b")
br.state = real_state