extends SceneTree
## END TO END on map_flat: real map -> collision -> nav graph -> path -> bot walks.
##
## Every link in this chain has its own gate. THE CHAIN did not, and that is the
## gap this closes — each piece was proven in isolation while nothing had ever run
## the whole thing in one process.
##
## map_flat on purpose. It has NO DCTerrain and no density field anywhere, so if
## navigation worked only there it would work nowhere the bots actually are. That
## the same libdc code serves a BoxShape3D ground slab is what taking a distance
## function rather than a voxel grid bought.
var ok := true
var _ran := false
## ⛔ current_map MUST BE A DECLARED PROPERTY, and the map must hang off a node
## named "Game". That is the path bots use to find anything — Engine main loop ->
## /root/Game -> current_map -> a child answering nav_find_path. The first version
## of this gate added the map straight to root, so the wandering bot's lookup found
## nothing and it chose zero destinations. The gate was testing a bot that could not
## see the world.
class StubGame extends Node3D:
var current_map: Node = null
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 nav_end_to_end: %s" % ("PASS" if ok else "FAIL"))
if not ok:
quit(1)
return true
## Every BoxShape3D collider under `n`, as (world centre, world half-extents).
func _collect_boxes(n: Node, centres: PackedVector3Array, halves: PackedVector3Array) -> void:
if n is CollisionShape3D:
var cs := n as CollisionShape3D
if cs.shape is BoxShape3D:
var b := cs.shape as BoxShape3D
var t := cs.global_transform
centres.push_back(t.origin)
# Axis-aligned only. A rotated collider would need its OBB, and map_flat
# has none — asserted below rather than assumed.
halves.push_back(b.size * 0.5)
for c in n.get_children():
_collect_boxes(c, centres, halves)
func _run() -> void:
_p(ClassDB.class_exists("DCNavVolume"),
"DCNavVolume exists — if not, the GDExtension is stale, rebuild all three targets")
if not ClassDB.class_exists("DCNavVolume"):
return
# ---- 1. the REAL map ----
var ps: PackedScene = load("res://scenes/maps/map_flat.tscn")
_p(ps != null, "map_flat.tscn loads")
if ps == null:
return
var game := StubGame.new()
game.name = "Game"
root.add_child(game)
var map: Node = ps.instantiate()
game.add_child(map)
game.current_map = map
var centres := PackedVector3Array()
var halves := PackedVector3Array()
_collect_boxes(map, centres, halves)
print(" collected %d box colliders from the real map" % centres.size())
_p(centres.size() > 0, "the map has box collision to navigate over")
if centres.size() == 0:
return
# ---- 2. add what the builder bot stacks, so composition is exercised ----
# A 10 x 10 x 20 block sitting on the ground, as bots/builder.gd places.
var block_centre := Vector3(14.0, 5.0, 0.0)
centres.push_back(block_centre)
halves.push_back(Vector3(5.0, 5.0, 10.0))
# ---- 3. build the graph ----
var vol: Node = ClassDB.instantiate("DCNavVolume")
root.add_child(vol)
vol.set_boxes(centres, halves)
var region := AABB(Vector3(-32, -8, -32), Vector3(64, 64, 64))
var populated: int = vol.rebuild(region, 4, 16, 0.4, 1.8)
var st: Dictionary = vol.stats()
print(" graph: %d chunks populated of 64, %d links, %d regions, %d standable, cell %.2f m"
% [populated, int(st["links"]), int(st["regions"]),
int(st["standable"]), float(st["cell_size"])])
_p(populated > 0, "chunks with ground in them produced navigation")
_p(int(st["links"]) > 0, "and neighbouring chunks LINKED — the graph is connected")
_p(int(st["standable"]) > 0, "with walkable cells")
# ---- 4. a route across the map ----
var from := Vector3(-20.0, 0.5, -20.0)
var to := Vector3(20.0, 0.5, 20.0)
var r: Dictionary = vol.find_path(from, to)
print(" path %s -> %s : found=%s nodes=%d waypoints=%d %s"
% [str(from), str(to), str(r.get("found")), int(r.get("nodes", 0)),
(r.get("waypoints", PackedVector3Array()) as PackedVector3Array).size(),
str(r.get("error", ""))])
_p(bool(r.get("found", false)), "a route across the map is found")
if not bool(r.get("found", false)):
return
_p(int(r.get("nodes", 0)) > 1, "and it crosses more than one chunk")
# ---- 5. THE BOT WALKS IT ----
var brain_script: GDScript = load("res://bots/pathwalker.gd")
_p(brain_script != null, "pathwalker.gd loads")
if brain_script == null:
return
var b: Object = brain_script.new()
b.set("bot_id", "e2e")
b.set("goal", to)
b.set("speed", 8.0)
b.set("_pos", from)
b.set("_have_pos", true)
# Hand it the route this graph actually produced — not an invented one.
var wp: PackedVector3Array = r.get("waypoints", PackedVector3Array())
wp.append(to)
b.set("_waypoints", wp)
b.set("_index", 0)
var start_pos: Vector3 = b.get("_pos")
var ticks := 0
while ticks < 2000 and int(b.get("_index")) < wp.size():
b.call("_bot_tick", 0.05)
ticks += 1
var end_pos: Vector3 = b.get("_pos")
print(" bot walked %.1f m in %d ticks: %s -> %s"
% [start_pos.distance_to(end_pos), ticks, str(start_pos), str(end_pos)])
_p(end_pos.distance_to(start_pos) > 1.0, "the bot MOVED")
_p(end_pos.distance_to(to) < 1.5,
"and arrived at the goal the graph routed it to (%.2f m away)" % end_pos.distance_to(to))
# ---- 6. the map wires its OWN nav volume, and WANDER re-runs the search ----
_p(map.has_method("nav_rebuild"), "map_flat builds its own nav volume")
if map.has_method("nav_rebuild"):
var populated2: int = map.nav_rebuild()
var own: Node = map.get_nav_volume()
_p(own != null and populated2 > 0,
"and it populates from the map's OWN colliders (%d chunks)" % populated2)
if own != null:
_p(own.has_method("nav_find_path"),
"it answers nav_find_path, so a bot need not know which map it is in")
var rp: Vector3 = own.random_standable_point()
_p(rp != Vector3.ZERO, "it can offer a random standable destination (%s)" % str(rp))
# The wandering bot must choose a NEW goal and route to it — the thing
# that makes the search visible as a search rather than one trusted run.
var w: Object = brain_script.new()
w.set("bot_id", "wander")
w.set("wander", true)
w.set("wander_pause", 0.0)
w.set("speed", 40.0)
w.set("_pos", rp)
w.set("_have_pos", true)
# Drive it long enough to finish several legs.
var goals: Array[Vector3] = []
for _i in range(4000):
w.call("_bot_tick", 0.05)
var gnow: Vector3 = w.get("goal")
if gnow != Vector3.ZERO and (goals.is_empty() or goals[-1] != gnow):
goals.append(gnow)
if goals.size() >= 3:
break
print(" wander chose %d distinct destinations, legs=%d"
% [goals.size(), int(w.get("_legs"))])
_p(goals.size() >= 2,
"the wandering bot chose more than one destination — A* re-ran live")
_p(int(w.get("_legs")) >= 1, "and completed at least one leg to get there")
# SELF-CHECK: a goal in mid-air must be refused, or "found" above means nothing.
var air: Dictionary = vol.find_path(from, Vector3(0.0, 40.0, 0.0))
_p(not bool(air.get("found", true)),
"self-check: a goal floating in the air is refused (%s)" % str(air.get("error", "")))
vol.queue_free()
game.queue_free()