extends Node
## GATE — THE BOT NAV GRAPH FOLLOWS LEVEL EDITS (#104 leftover, user 2026-10-05: "do
## the nav fix"). It was built once per map entry and never again, so bots routed
## through walls added later and around walls since deleted.
##
## 1. the load builds it ONCE (the load's own folds do not queue a second build)
## 2. a burst of 5 edits does not rebuild during the burst ...
## 3. ... and rebuilds exactly ONCE, NAV_REBUILD_DELAY after it
## 4. the rebuilt graph has the new geometry: a 12 x 12 m platform (4-5 m, 1 m thick:
## thinner than a nav cell is not a floor the grid can see) adds standable cells and
## its top joins the navigable set; deleting it removes them again
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_nav_follows_edits.tscn
var fails := 0
var checks := 0
func _ok(what: String, cond: bool, note: Variant = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", str(note)])
func _box(lo: Vector3, hi: Vector3) -> Array:
return [1, 0, 0, hi.x, -1, 0, 0, -lo.x, 0, 1, 0, hi.y, 0, -1, 0, -lo.y, 0, 0, 1, hi.z, 0, 0, -1, -lo.z]
func _ready() -> void:
var gs: Node = get_tree().root.get_node("/root/GameState")
gs.route("world:level:*", 1, "peer")
var ks: Node = get_tree().root.get_node_or_null("/root/KeetService")
if ks != null:
ks.current_member_id = "gate-author"
var map: Node3D = (load("res://scenes/maps/map_test.tscn") as PackedScene).instantiate()
add_child(map)
for i in 6:
await get_tree().process_frame
await get_tree().create_timer(1.5).timeout # past any load-queued rebuild
await _run(map)
if checks < 7:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE nav_follows_edits: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _standable(map: Node) -> int:
var v: Node = map.get_nav_volume()
return int((v.stats() as Dictionary).get("standable", -1)) if v != null else -1
func _run(map: Node3D) -> void:
var persist: Node = map.get("_persist")
_ok("1. the load built the nav graph once", int(map.get("nav_builds")) == 1, map.get("nav_builds"))
var s0 := _standable(map)
var b0: int = map.get("nav_builds")
# A burst: a 12 x 12 platform at 4 m (made of 4 pieces) + one more small box.
var made: Array = []
for i in 4:
var x0 := 20.0 + float(i % 2) * 6.0
var z0 := 20.0 + float(i / 2) * 6.0
made.append(persist.record("brush.create", {"planes": _box(Vector3(x0, 4, z0), Vector3(x0 + 6, 5, z0 + 6)),
"proj": [], "material_id": 0, "level_id": 0, "sky": false}))
persist.refold()
await get_tree().create_timer(0.1).timeout
made.append(persist.record("brush.create", {"planes": _box(Vector3(40, 0, 40), Vector3(41, 1, 41)),
"proj": [], "material_id": 0, "level_id": 0, "sky": false}))
persist.refold()
_ok("2. no rebuild during the burst", int(map.get("nav_builds")) == b0, "%d builds" % (int(map.get("nav_builds")) - b0))
await get_tree().create_timer(1.4).timeout
_ok("3. exactly one rebuild after it", int(map.get("nav_builds")) == b0 + 1, "%d builds" % (int(map.get("nav_builds")) - b0))
var s1 := _standable(map)
_ok("4. the platform added standable cells", s1 > s0 + 50, "%d -> %d" % [s0, s1])
var on_top: Dictionary = map.get_nav_volume().find_path(Vector3(0, 0.1, 0), Vector3(26, 5.1, 26))
_ok("4. its top is on the navigable set", String(on_top.get("error", "")) != "goal is not on the navigable set", on_top.get("error", "found"))
map.delete_brushes(made.map(func(d: String) -> int: return map.level.brush_by_key(d)))
await get_tree().create_timer(1.4).timeout
var s2 := _standable(map)
_ok("4. deleting it rebuilt again", int(map.get("nav_builds")) == b0 + 2, "%d builds" % (int(map.get("nav_builds")) - b0))
_ok("4. ... back to the original standable set", s2 == s0, "%d -> %d" % [s1, s2])