OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / outliner_model_gate.gd

144 lines · gd ref:
extends SceneTree
## THE ORGANIZER'S MODEL — hyper-games #28.
##
##     "$HYPER_TPS_GODOT" --headless --script scenes/test/outliner_model_gate.gd
##
## The model is UI-free, so all of it is testable without a panel — which is the
## point of the split. What is asserted here is the part that is easy to get
## subtly wrong and impossible to see in a screenshot:
##
##   HIDING INHERITS, and un-hiding a parent restores exactly the children that
##   were not hidden themselves.
##
##   AND IT NEVER TOUCHES THE SHELL. Hiding a boundary wall must not un-seal the
##   level; that is the failure that would fill every room with hillside.

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)],
]
const TUNNEL := [Vector3(7, 0.5, 3), Vector3(13, 2.5, 4)]

var _failures: Array[String] = []
var _frame := 0
var _map: Node3D = null
var _level: Node3D = null


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 _row(rs: Array, id: String) -> Dictionary:
	for r: Dictionary in rs:
		if r["id"] == id:
			return r
	return {}


func _build_scene() -> void:
	_map = Node3D.new()
	_map.name = "MapDCWorld"
	root.add_child(_map)
	_level = ClassDB.instantiate("DCLevel") as Node3D
	_level.name = "DCLevel"
	_map.add_child(_level)
	var terrain := Node3D.new()
	terrain.name = "DCTerrain"
	_map.add_child(terrain)
	var lights := Node3D.new()
	lights.name = "QuakeLights"
	_map.add_child(lights)
	for w: Array in WALLS:
		_level.add_brush(_box(w[0], w[1]), 0, PackedInt32Array(), 0, false, "")
	# One sky brush and one clearance volume, so both derived groups exist.
	_level.add_brush(_box(Vector3(2, 4.9, 2), Vector3(4, 5.1, 4)), 0,
		PackedInt32Array(), 0, true, "")
	_level.add_brush(_box(TUNNEL[0], TUNNEL[1]), 0, PackedInt32Array(), 0,
		false, "t:1", true)
	_level.rebuild()


func _process(_delta: float) -> bool:
	_frame += 1
	if _frame < 3:
		return false

	print("outliner model gate")
	_build_scene()
	var m := OutlinerModel.new(_map)
	# `level` is read in _init from map.get("level"), which a plain Node3D has
	# not got — the real map does. Set it the way the map would.
	m.level = _level

	# ---- the tree ---------------------------------------------------------
	var rs: Array = m.rows()
	_check("the map is the root row", not _row(rs, "node:.").is_empty(), "node:.")
	_check("scene objects are rows", not _row(rs, "node:DCTerrain").is_empty()
		and not _row(rs, "node:QuakeLights").is_empty(), "DCTerrain, QuakeLights")
	var sky: Dictionary = _row(rs, "derived:sky")
	var clr: Dictionary = _row(rs, "derived:clearance")
	var rest: Dictionary = _row(rs, "derived:rest")
	_check("Sky is a derived group", int(sky.get("count", 0)) == 1, "%d" % int(sky.get("count", -1)))
	_check("Clearance is a derived group", int(clr.get("count", 0)) == 1, "%d" % int(clr.get("count", -1)))
	_check("the six walls land in Unsorted", int(rest.get("count", 0)) == 6,
		"%d" % int(rest.get("count", -1)))
	_check("brush groups hang off the level, not the map",
		String(sky.get("parent", "")) == "node:DCLevel", String(sky.get("parent", "?")))
	# 1183 brushes must never become 1183 rows.
	var brush_rows := 0
	for r: Dictionary in rs:
		if int(r["kind"]) == OutlinerModel.Kind.BRUSH:
			brush_rows += 1
	_check("no per-brush rows by default", brush_rows == 0, "%d" % brush_rows)

	# ---- hiding, and the invariant that matters ---------------------------
	var shell0: Dictionary = _level.build_shell()
	var t0: int = _level.get_triangle_count()
	m.set_row_hidden("derived:rest", true)
	m.apply()
	_check("hiding a group hides its brushes",
		(_level.get_hidden_brush_ids() as PackedInt32Array).size() == 6,
		"%d hidden" % (_level.get_hidden_brush_ids() as PackedInt32Array).size())
	_check("and the level re-meshed", _level.get_triangle_count() < t0,
		"%d vs %d tris" % [_level.get_triangle_count(), t0])
	var shell1: Dictionary = _level.build_shell()
	_check("THE SHELL IS UNTOUCHED",
		int(shell1.get("triangles", 0)) == int(shell0.get("triangles", 0)),
		"%d tris" % int(shell1.get("triangles", 0)))
	_check("and still sealed", bool(shell1.get("sealed", false)), "yes")

	# A node row really hides the node.
	m.set_row_hidden("node:DCTerrain", true)
	m.apply()
	_check("hiding a node row hides the node",
		not (_map.get_node("DCTerrain") as Node3D).visible, "visible=false")
	m.set_row_hidden("node:DCTerrain", false)
	m.set_row_hidden("derived:rest", false)
	m.apply()
	_check("unhiding restores the mesh exactly", _level.get_triangle_count() == t0,
		"%d vs %d" % [_level.get_triangle_count(), t0])

	# COLLECTIONS are room state since #120 F1 (journalled, CollectionFold): tested by
	# gate_collection_fold / _persist / _edit / _keys, hide inheritance included (E14).

	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