OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / hidden_brush_gate.gd

148 lines · gd ref:
extends SceneTree
## HIDING A BRUSH — the outliner's eye, hyper-games #28.
##
##     "$HYPER_TPS_GODOT" --headless --script scenes/test/hidden_brush_gate.gd
##
## Hiding is a VIEW, and this asserts the three things that make it one rather
## than a quiet edit:
##
##   1. IT DOES NOT RE-CLIP. Hiding filters faces at mesh emit, after csg4 has
##      run. Dropping the brush from csg4's INPUT instead would re-clip its
##      neighbours — faces buried behind a wall become visible the moment you
##      hide it — so unhiding would not restore what you had. The discriminating
##      control is a TOMBSTONE of the same brush: that really does remove it from
##      the input, so if hide and tombstone agree, hide is not a view.
##
##   2. IT DOES NOT REACH THE SHELL. build_shell reads shell_view, off the store,
##      and the hidden set lives on the NODE. If hiding ever touched it, hiding a
##      boundary wall would un-seal the level and the terrain would flood every
##      room — a view toggle silently filling the map with hillside.
##
##   3. IT IS EXACTLY REVERSIBLE. Unhide restores the original triangle and face
##      counts to the digit, which is only true if the clip never changed.
##
## Plus the collider, which you asked to drop so you can fly through what you hid.

const WALLS := [
	[Vector3(0, 0, 0), Vector3(7, 0.5, 7)],
	[Vector3(0, 4.5, 0), Vector3(7, 5, 7)],
	[Vector3(0, 0.5, 0), Vector3(0.5, 4.5, 7)],
	[Vector3(6.5, 0.5, 0), Vector3(7, 4.5, 7)],
	[Vector3(0.5, 0.5, 0), Vector3(6.5, 4.5, 0.5)],
	[Vector3(0.5, 0.5, 6.5), Vector3(6.5, 4.5, 7)],
]
## A pillar standing against the -X wall, so it BURIES part of that wall's face.
## That shared surface is the whole point: it is what a re-clip would change and
## an emit filter would not.
const PILLAR := [Vector3(0.5, 0.5, 3.0), Vector3(1.5, 4.5, 4.0)]

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("  %-52s %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() -> Node3D:
	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, "")
	lvl.add_brush(_box(PILLAR[0], PILLAR[1]), 0, PackedInt32Array(), 0, false, "")
	lvl.rebuild()
	return lvl


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

	print("hidden brush gate")
	var lvl := _new_level()
	var ids: PackedInt32Array = PackedInt32Array()
	for i in lvl.get_brush_count():
		pass
	# The pillar is the last brush added, so its id is the highest live one.
	var pillar_id := 0
	for b in range(1, 64):
		if not (lvl.get_brush(b) as Dictionary).is_empty():
			pillar_id = b
	_check("the pillar has an id", pillar_id > 0, "#%d" % pillar_id)

	var t0: int = lvl.get_triangle_count()
	var f0: int = int(lvl.get_stats().get("faces", 0))
	var c0: int = lvl.get_collider_count()
	var shell0: Dictionary = lvl.build_shell()
	print("  visible: %d tris, %d faces, %d colliders, shell %d tris" %
		[t0, f0, c0, int(shell0.get("triangles", 0))])

	# ---- hide it ---------------------------------------------------------
	lvl.set_brush_hidden(pillar_id, true)
	lvl.rebuild()
	var t1: int = lvl.get_triangle_count()
	var c1: int = lvl.get_collider_count()
	_check("get_hidden_brush_ids reports it",
		(lvl.get_hidden_brush_ids() as PackedInt32Array).has(pillar_id),
		"%s" % str(lvl.get_hidden_brush_ids()))
	_check("is_brush_hidden agrees", lvl.is_brush_hidden(pillar_id), "true")
	_check("stats count it", int(lvl.get_stats().get("hidden_brushes", -1)) == 1,
		"%s" % str(lvl.get_stats().get("hidden_brushes", -1)))
	_check("hiding removes its triangles", t1 < t0, "%d vs %d" % [t1, t0])
	_check("and its collider", c1 == c0 - 1, "%d vs %d" % [c1, c0])

	# ---- 2. THE SHELL IS UNTOUCHED ---------------------------------------
	# The one that matters. A hidden wall that left the shell would let the
	# terrain into the level.
	var shell1: Dictionary = lvl.build_shell()
	_check("the shell is byte-identical",
		int(shell1.get("triangles", 0)) == int(shell0.get("triangles", 0))
		and absf(float(shell1.get("volume", 0.0)) - float(shell0.get("volume", 0.0))) < 1e-6,
		"%d tris, %.1f m^3" % [int(shell1.get("triangles", 0)), float(shell1.get("volume", 0.0))])
	_check("and still sealed", bool(shell1.get("sealed", false)), "yes")

	# ---- 3. EXACTLY REVERSIBLE -------------------------------------------
	lvl.set_brush_hidden(pillar_id, false)
	lvl.rebuild()
	_check("unhiding restores the triangles exactly", lvl.get_triangle_count() == t0,
		"%d vs %d" % [lvl.get_triangle_count(), t0])
	_check("and the faces exactly", int(lvl.get_stats().get("faces", 0)) == f0,
		"%d vs %d" % [int(lvl.get_stats().get("faces", 0)), f0])
	_check("and the colliders", lvl.get_collider_count() == c0,
		"%d vs %d" % [lvl.get_collider_count(), c0])

	# ---- 1. THE DISCRIMINATING CONTROL: hide != tombstone -----------------
	# A tombstone removes the pillar from csg4's INPUT, so the -X wall face it was
	# burying gets re-clipped and comes back whole. Hiding must NOT do that. If
	# these two agree, the filter is in the wrong place and unhiding is only
	# accidentally exact.
	lvl.set_brush_hidden(pillar_id, true)
	lvl.rebuild()
	var t_hidden: int = lvl.get_triangle_count()
	lvl.set_brush_hidden(pillar_id, false)
	lvl.remove_brush(pillar_id)
	lvl.rebuild()
	var t_removed: int = lvl.get_triangle_count()
	print("  hidden %d tris vs tombstoned %d tris (visible %d)" % [t_hidden, t_removed, t0])
	_check("hiding is NOT the same as removing", t_hidden != t_removed,
		"%d vs %d — a re-clip would make them equal" % [t_hidden, t_removed])
	_check("and removing really does re-clip", t_removed != t0,
		"%d vs %d" % [t_removed, t0])

	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