OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / clearance_gate.gd

186 lines · gd ref:
extends SceneTree
## CLEARANCE BRUSHES, through the ENGINE BINDING — hyper-games #25 §2.
##
##     "$HYPER_TPS_GODOT" --headless --script scenes/test/clearance_gate.gd
##
## (A segfault at exit is the known godot 4.6 GDExtension atexit crash — harmless
## after RESULT prints, same as dc_level_gate.)
##
## tests/test_clearance_shell.cpp proves the FLOOD FILL treats a clearance volume
## as air the level owns. It links dc_level_shell directly and knows nothing about
## DCLevel, DCBrushStore or ClassDB, so everything between the store and GDScript
## is unproven by it — and that is where the plumbing is:
##
##   add_brush's new trailing argument reaching DCBrushStore at all
##   csg4_view DROPPING the brush, so the shape it encloses still renders
##   shell_view CARRYING it, so the passage is still subtracted
##   the pick filter, which is what makes the editor's clearance view possible
##   apply_journal round-tripping the flag, without which it does not replicate
##
## A clearance volume that renders is a solid block over your stairs; one that
## does not reach the shell does nothing at all. Both look like "it works" from
## the other side of the binding, so each is asserted separately here.
##
## SYNTHETIC, no START.MAP: this is about the binding, not about any level.

## A sealed room, then a prism leaving its +X wall — the same scene as
## tests/test_clearance_shell.cpp, so a disagreement between the two is a
## statement about the binding rather than about the geometry.
const WALLS := [
	[Vector3(0, 0, 0), Vector3(7, 0.5, 7)],        # floor
	[Vector3(0, 4.5, 0), Vector3(7, 5, 7)],        # ceiling
	[Vector3(0, 0.5, 0), Vector3(0.5, 4.5, 7)],    # -X
	[Vector3(6.5, 0.5, 0), Vector3(7, 4.5, 7)],    # +X
	[Vector3(0.5, 0.5, 0), Vector3(6.5, 4.5, 0.5)],# -Z
	[Vector3(0.5, 0.5, 6.5), Vector3(6.5, 4.5, 7)],# +Z
]
const TUNNEL_LO := Vector3(7, 0.5, 3)
const TUNNEL_HI := Vector3(13, 2.5, 4)

var _failures: Array[String] = []
var _frame := 0


func _check(label: String, ok: bool, detail: String) -> void:
	if not ok:
		_failures.append("%s (%s)" % [label, detail])
	print("  %-50s %s  %s" % [label, "OK  " if ok else "FAIL", detail])


static func _box(lo: Vector3, hi: Vector3) -> PackedFloat32Array:
	return PackedFloat32Array([
		-1, 0, 0, -lo.x,   1, 0, 0, hi.x,
		0, -1, 0, -lo.y,   0, 1, 0, hi.y,
		0, 0, -1, -lo.z,   0, 0, 1, hi.z,
	])


func _new_level(with_tunnel: int) -> Node3D:
	# with_tunnel: 0 none, 1 clearance, 2 ordinary solid.
	var lvl := ClassDB.instantiate("DCLevel") as Node3D
	root.add_child(lvl)
	for w: Array in WALLS:
		lvl.add_brush(_box(w[0], w[1]), 0, PackedInt32Array(), 0, false, "")
	if with_tunnel > 0:
		lvl.add_brush(_box(TUNNEL_LO, TUNNEL_HI), 0, PackedInt32Array(), 0, false,
			"t:1", with_tunnel == 1)
	lvl.rebuild()
	return lvl


func _initialize() -> void:
	pass


func _process(_delta: float) -> bool:
	_frame += 1
	if _frame < 3:
		return false                       # colliders need a world; see dc_level_gate

	print("clearance gate — DCLevel binding")

	var plain := _new_level(0)
	var clear := _new_level(1)
	var solid := _new_level(2)

	# ---- 1. the flag survives the binding ----------------------------------
	var ids: PackedInt32Array = clear.get_clearance_brush_ids()
	_check("get_clearance_brush_ids finds the one brush", ids.size() == 1,
		"%d" % ids.size())
	if ids.size() == 1:
		var b: Dictionary = clear.get_brush(ids[0])
		_check("get_brush reports clearance", bool(b.get("clearance", false)), "true")
		_check("and it is not a sky brush", not bool(b.get("sky", true)), "false")
	_check("an UNFLAGGED prism is not clearance",
		solid.get_clearance_brush_ids().size() == 0,
		"%d" % solid.get_clearance_brush_ids().size())
	_check("stats count it apart from geometry",
		int(clear.get_stats().get("clearance_brushes", -1)) == 1,
		"%s" % str(clear.get_stats().get("clearance_brushes", -1)))

	# ---- 2. csg4 DROPS it: the level looks exactly as if it were absent -----
	# The failure this rejects is the loud one — a clearance volume that clips
	# would eat the faces of the shape it encloses, and one that renders would be
	# a solid block where the passage is.
	var t_plain: int = plain.get_triangle_count()
	var t_clear: int = clear.get_triangle_count()
	var t_solid: int = solid.get_triangle_count()
	_check("clearance adds NO triangles", t_clear == t_plain,
		"%d vs %d" % [t_clear, t_plain])
	_check("an unflagged prism DOES add triangles", t_solid > t_plain,
		"%d vs %d" % [t_solid, t_plain])
	_check("clearance adds no collider",
		clear.get_collider_count() == plain.get_collider_count(),
		"%d vs %d" % [clear.get_collider_count(), plain.get_collider_count()])

	# ---- 3. shell_view CARRIES it: the passage is enclosed ------------------
	var sh_plain: Dictionary = plain.build_shell()
	var sh_clear: Dictionary = clear.build_shell()
	var air_gain: float = float(sh_clear.get("interior_volume", 0.0)) \
		- float(sh_plain.get("interior_volume", 0.0))
	var wall_gain: float = float(sh_clear.get("solid_volume", 0.0)) \
		- float(sh_plain.get("solid_volume", 0.0))
	_check("the shell gains the bore as AIR", air_gain > 10.0,
		"%+.1f m^3" % air_gain)
	_check("and gains no WALL", absf(wall_gain) < 1e-3, "%+.3f m^3" % wall_gain)
	_check("the shell is still sealed", bool(sh_clear.get("sealed", false)),
		"boundary_edges %d" % int(sh_clear.get("boundary_edges", -1)))
	_check("the shell grew a surface around it",
		int(sh_clear.get("triangles", 0)) > int(sh_plain.get("triangles", 0)),
		"%d vs %d" % [int(sh_clear.get("triangles", 0)), int(sh_plain.get("triangles", 0))])

	# ---- 3b. the wireframe splits by KIND, not by a flag on one mesh --------
	# The structure layer must not carry them: drawn in the same colour as a wall,
	# a box outline hanging in the air where a passage is reads as a wall that
	# failed to render.
	var e_plain: int = plain.get_brush_edges().size()
	var e_clear: int = clear.get_brush_edges().size()
	_check("the structure wireframe ignores clearance", e_clear == e_plain,
		"%d vs %d segments" % [e_clear, e_plain])
	_check("and the clearance layer has it",
		clear.get_clearance_edges().size() > 0,
		"%d segments" % clear.get_clearance_edges().size())
	_check("a level with no passages draws none",
		plain.get_clearance_edges().size() == 0,
		"%d segments" % plain.get_clearance_edges().size())

	# ---- 4. the pick filter — what the editor's clearance view rests on -----
	# Aimed straight down the bore from outside it. The default must MISS: a
	# clearance volume encloses the shape it serves, so a pickable one would
	# intercept every ray meant for the stairs inside it.
	var from := Vector3(20, 1.5, 3.5)
	var to := Vector3(0, 1.5, 3.5)
	var hit_default: Dictionary = clear.raycast(from, to)
	var hit_incl: Dictionary = clear.raycast(from, to, true)
	var default_brush: int = int(hit_default.get("brush", -1))
	var incl_brush: int = int(hit_incl.get("brush", -1))
	_check("by default the ray passes THROUGH clearance",
		not ids.has(default_brush), "hit brush %d" % default_brush)
	_check("include_clearance hits it", ids.has(incl_brush),
		"hit brush %d" % incl_brush)

	# ---- 5. it replicates: a journal fold rebuilds the same level -----------
	# Without this the flag is local decoration — a peer replaying the record
	# would build the stairs and none of their headroom.
	var replica := ClassDB.instantiate("DCLevel") as Node3D
	root.add_child(replica)
	var planes: Array = []
	for f: float in _box(TUNNEL_LO, TUNNEL_HI):
		planes.append(f)
	var stats: Dictionary = replica.apply_journal([{
		"author": "peer", "seq": 1, "kind": "brush.create", "undone": false,
		"planes": planes, "proj": [], "material_id": 0, "level_id": 0,
		"sky": false, "clearance": true,
	}])
	_check("the fold applied the create", int(stats.get("applied", 0)) == 1,
		"applied %d skipped %d" % [int(stats.get("applied", 0)), int(stats.get("skipped", 0))])
	_check("and the replica's brush IS clearance",
		replica.get_clearance_brush_ids().size() == 1,
		"%d" % replica.get_clearance_brush_ids().size())

	print("\nRESULT: %s" % ("PASS" if _failures.is_empty() else "FAIL"))
	for f: String in _failures:
		print("  - %s" % f)
	quit(0 if _failures.is_empty() else 1)
	return true