OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_level_nav.gd

120 lines · gd ref:
extends SceneTree
## GATE — the STARTING MAP (map_test, a map_dc_level) gives bots navigation.
## (Run on map_dev_test since 2026-10-06: map_test became the minimal start map with no
## walls; map_dev_test is the old start map, the same map_dc_level nav source.)
##
## User 2026-10-04: bots walk over / through obstacles on the starting map. Cause:
## map_test had NO nav source at all (DCLevel is a plain Node3D; only map_flat built
## a DCNavVolume), so the A* brain logged "no nav source" and stood still, and the
## other brains moved without one. Asserts, on the real map scene:
##   N1  the map has a child answering nav_find_path
##   N2  a route from one side of Wall1 to the other goes AROUND it: it exists, and
##       no segment crosses the wall's footprint
##   N3  how long the graph takes to build (printed; the budget is set from it)
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_level_nav.gd

var ok := true
var _frames := 0
var _map: Node = null


## Bots find the world through /root/Game.current_map (see gate_nav_end_to_end).
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 _initialize() -> void:
	var game := StubGame.new()
	game.name = "Game"
	root.add_child(game)
	_map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
	game.add_child(_map)
	game.current_map = _map


func _process(_dt: float) -> bool:
	_frames += 1
	if _frames < 30:            # deferred level import + deferred nav build
		return false
	_run()
	print("GATE level_nav: %s" % ("PASS" if ok else "FAIL"))
	quit(0 if ok else 1)
	return true


func _nav() -> Node:
	for c in _map.get_children():
		if c.has_method("nav_find_path"):
			return c
	return null


## Does the 2D segment a-b (x, z) pass through the axis-aligned rectangle?
static func _segment_hits_rect(a: Vector2, b: Vector2, lo: Vector2, hi: Vector2) -> bool:
	var steps := 64
	for i in steps + 1:
		var p := a.lerp(b, float(i) / steps)
		if p.x > lo.x and p.x < hi.x and p.y > lo.y and p.y < hi.y:
			return true
	return false


func _run() -> void:
	var nav := _nav()
	_p(nav != null, "N1 map_test has a child answering nav_find_path")
	if nav == null:
		return
	# N3 — a fresh build, timed.
	if _map.has_method("nav_rebuild"):
		var t0 := Time.get_ticks_usec()
		var chunks: int = _map.nav_rebuild()
		print("N3 nav_rebuild: %d chunks in %.1f ms" % [chunks, (Time.get_ticks_usec() - t0) / 1000.0])
	# N2 — around Wall1 (its collider, read from the scene, not assumed). Read from a
	# FRESH, unbaked instance of map_test.tscn: since 2026-10-05 the live map bakes its
	# scene boxes into base brushes and removes the nodes (map_dc_level
	# _bake_scene_boxes), so Wall1/Shape no longer exists under _map.
	var src: Node = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
	var wb := src.get_node_or_null(^"Wall1") as Node3D
	var gb := src.get_node_or_null(^"Ground") as Node3D
	var wall := src.get_node_or_null(^"Wall1/Shape") as CollisionShape3D
	var ground := src.get_node_or_null(^"Ground/GroundShape") as CollisionShape3D
	_p(wall != null and ground != null, "N2 the scene has Wall1 and the ground")
	if wall == null or ground == null:
		src.free()
		return
	var root_xf: Transform3D = (_map as Node3D).global_transform
	var wc := (root_xf * wb.transform * wall.transform).origin
	var wh := (wall.shape as BoxShape3D).size * 0.5
	var gy := (root_xf * gb.transform * ground.transform).origin.y + (ground.shape as BoxShape3D).size.y * 0.5
	src.free()
	# Either side of the wall's thin axis, at its middle, on the ground.
	var thin := 0 if wh.x < wh.z else 2
	var off := Vector3.ZERO
	off[thin] = wh[thin] + 2.0
	var from := Vector3(wc.x, gy + 0.1, wc.z) - off
	var to := Vector3(wc.x, gy + 0.1, wc.z) + off
	var r: Dictionary = nav.nav_find_path(from, to)
	_p(bool(r.get("found", false)), "N2 a route exists from %s to %s (%s)" % [from, to, r.get("error", "")])
	if not bool(r.get("found", false)):
		return
	var pts: Array = [from]
	for w in (r.get("waypoints", PackedVector3Array()) as PackedVector3Array):
		pts.append(w)
	pts.append(to)
	var lo := Vector2(wc.x - wh.x, wc.z - wh.z)
	var hi := Vector2(wc.x + wh.x, wc.z + wh.z)
	var crossed := 0
	for i in pts.size() - 1:
		var a: Vector3 = pts[i]
		var b: Vector3 = pts[i + 1]
		if _segment_hits_rect(Vector2(a.x, a.z), Vector2(b.x, b.z), lo, hi):
			crossed += 1
	_p(crossed == 0, "N2 no segment of the %d-point route crosses Wall1's footprint (%d do)" % [pts.size(), crossed])