OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_mod_copies.gd

232 lines · gd ref:
extends Node
## GATE — COPY MODIFIERS (#120 F2, docs/node-tree-and-stacks.md §3, ModFold).
##   X1  mirror (x, offset 0): a copy reflected across the shape's low x face
##   X2  array (3 along x): two more copies, one size apart each
##   X3  radial (4 about y through the cursor): copies turned 90° about the pivot
##   X4  a stack, mirror then array: the array copies the mirrored pair (6 pieces)
##   X5  editing a modifier moves its copies (the copies are renamed, so the store does
##       not restore the old planes)
##   X6  off -> no copies; on -> back; remove -> gone; Ctrl+Z -> back
##   X7  two arrival orders of the same records give the same copy names
##   X8  a cut on a mirrored shape cuts the mirrored copy too (its cutter is mirrored)
##   X9  Make Real bakes the copies with the pieces
##   X10 a flatten keeps the modifiers (their targets remapped to the re-minted shapes)
##   X11 THE FIXED STAGE ORDER: a mirror added AFTER the cut still comes before it — the
##       mirrored half is cut the same way (the record order does not decide the stages)
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_mod_copies.tscn

const ModFold := preload("res://scenes/maps/mod_fold.gd")
const ShapeFold := preload("res://scenes/maps/shapes/param/shape_fold.gd")

var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var level: Node


func _ok(what: String, cond: bool, note: Variant = "") -> void:
	checks += 1
	if not cond:
		fails += 1
	print("  %-64s %s  %s" % [what, "OK" if cond else "**FAIL**", str(note)])


func _ready() -> void:
	var gs: Node = get_tree().root.get_node_or_null("/root/GameState")
	gs.route("world:level:*", 1, "peer")
	var keet: Node = get_tree().root.get_node_or_null("/root/KeetService")
	if keet != null:
		keet.current_member_id = "gate-author"
	get_tree().root.get_node("/root/GameMode").set_mode("build")
	map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
	add_child(map)
	for i in 6:
		await get_tree().process_frame
	await _run()
	get_tree().root.get_node("/root/GameMode").set_mode("play")
	if checks < 18:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE mod_copies: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


func _box(at: Vector3, size: Vector3) -> String:
	var d: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
		"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, at)),
		"size": [size.x, size.y, size.z], "material_id": 2, "level_id": 0})
	persist.refold()
	return d


func _solids(dot: String) -> Array:
	var out: Array = []
	for b: int in (level.get_group_brush_ids(dot) as PackedInt32Array):
		var d: Dictionary = level.get_brush(b)
		if not bool(d.get("clearance", false)) and not bool(d.get("cutter", false)):
			out.append(b)
	return out


## The x-ranges (min, max) of a shape's solid pieces, sorted: one per piece.
func _xranges(dot: String) -> Array:
	var out: Array = []
	for b: int in _solids(dot):
		var lo := INF
		var hi := -INF
		for c: Vector3 in FaceKit.corners(level.get_brush(b)["planes"]):
			lo = minf(lo, c.x)
			hi = maxf(hi, c.x)
		out.append([snappedf(lo, 0.001), snappedf(hi, 0.001)])
	out.sort()
	return out


func _run() -> void:
	persist = map.get("_persist")
	level = map.get("level")
	persist.refold()

	# ---- X1 mirror ----
	var s := _box(Vector3(40, 0, 40), Vector3(2, 1, 2))
	var m1: String = map.add_modifier("mirror", s)
	_ok("X1 mirror: two pieces, the copy across the low x face", _xranges(s) == [[38.0, 40.0], [40.0, 42.0]], _xranges(s))

	# ---- X4 stack: + array 3 along x ----
	var m2: String = map.add_modifier("array", s, {"count": 3})
	_ok("X4 mirror then array: 6 pieces", _solids(s).size() == 6, _solids(s).size())
	_ok("X4 ... the array copies the mirrored pair", _xranges(s) == [[38.0, 40.0], [40.0, 42.0], [40.0, 42.0], [42.0, 44.0], [42.0, 44.0], [44.0, 46.0]],
		_xranges(s))

	# ---- X5 edit ----
	map.set_modifier(m2, {"count": 2})
	_ok("X5 count 3 -> 2: 4 pieces", _solids(s).size() == 4, _solids(s).size())
	map.set_modifier(m1, {"offset": 2.0})
	_ok("X5 mirror at x = 42: the copy moved", _xranges(s) == [[40.0, 42.0], [42.0, 44.0], [42.0, 44.0], [44.0, 46.0]], _xranges(s))

	# ---- X6 off / on / remove / undo ----
	map.set_modifier(m1, {}, false)
	map.set_modifier(m2, {}, false)
	_ok("X6 both off: just the box", _solids(s).size() == 1)
	map.set_modifier(m2, {}, true)
	_ok("X6 array on again: 2", _solids(s).size() == 2)
	map.remove_modifier(m2)
	_ok("X6 removed: 1", _solids(s).size() == 1)
	persist.undo_last()
	persist.refold()
	_ok("X6 Ctrl+Z: the array is back", _solids(s).size() == 2)

	# ---- X2 array alone ----
	var a := _box(Vector3(60, 0, 60), Vector3(1, 1, 1))
	map.add_modifier("array", a, {"count": 3})
	_ok("X2 array of 3 along x, one size apart", _xranges(a) == [[60.0, 61.0], [61.0, 62.0], [62.0, 63.0]], _xranges(a))

	# ---- X3 radial ----
	var r := _box(Vector3(80, 0, 80), Vector3(1, 1, 1))
	map.add_modifier("radial", r, {"count": 4, "pivot": [80.0, 0.0, 80.0]})
	_ok("X3 radial of 4: 4 pieces", _solids(r).size() == 4)
	var quad := {}
	for b: int in _solids(r):
		var c := Vector3.ZERO
		var cs := FaceKit.corners(level.get_brush(b)["planes"])
		for v: Vector3 in cs:
			c += v
		c /= float(cs.size())
		quad["%d,%d" % [signi(int(roundf((c.x - 80.0) * 2))), signi(int(roundf((c.z - 80.0) * 2)))]] = true
	_ok("X3 ... one in each quadrant about the pivot", quad.size() == 4, quad.keys())

	# ---- X12 grid (#120 L1 "box grid"): counts per frame axis, one size apart ----
	var gd := _box(Vector3(100, 0, 100), Vector3(1, 1, 1))
	var gm: String = map.add_modifier("grid", gd, {"count": [2, 1, 3]})
	_ok("X12 grid 2 x 1 x 3: 6 pieces", gm != "" and _solids(gd).size() == 6, _solids(gd).size())
	var cells := {}
	for b: int in _solids(gd):
		var lo := Vector3(INF, INF, INF)
		for v: Vector3 in FaceKit.corners(level.get_brush(b)["planes"]):
			lo = lo.min(v)
		cells["%d,%d,%d" % [roundi(lo.x), roundi(lo.y), roundi(lo.z)]] = true
	var want := ["100,0,100", "100,0,101", "100,0,102", "101,0,100", "101,0,101", "101,0,102"]
	var got: Array = cells.keys()
	got.sort()
	_ok("X12 ... one per cell, one size apart on x and z", got == want, got)

	# ---- X7 determinism ----
	var recs: Array = persist.records(false)
	var shape_recs: Array = recs.filter(func(x: Dictionary) -> bool: return ShapeFold.handles(String(x["kind"])))
	var mod_recs: Array = recs.filter(func(x: Dictionary) -> bool: return ModFold.handles(String(x["kind"])))
	var st := ShapeFold.resolve_states(shape_recs)
	var names1: Array = ModFold.expand(mod_recs, ShapeFold.expand(shape_recs), st).map(func(x: Dictionary) -> String: return String(x["author"]))
	var rev := mod_recs.duplicate()
	rev.reverse()
	var names2: Array = ModFold.expand(rev, ShapeFold.expand(shape_recs), st).map(func(x: Dictionary) -> String: return String(x["author"]))
	names1.sort()
	names2.sort()
	_ok("X7 another arrival order, the same copy names", names1 == names2 and not names1.is_empty())

	# ---- X10 flatten ---- (before X8: a flatten refuses while a live cut exists)
	var total := func() -> int:
		var n := 0
		for d: String in (persist.live_shapes() as Dictionary):
			n += _solids(d).size()
		return n
	var before: int = total.call()
	var r10: Dictionary = persist.flatten_map()
	_ok("X10 a flatten keeps every modifier's copies", not r10.has("error") and total.call() == before,
		[before, total.call(), r10.get("error", "")])
	# The shapes were re-minted: find them again by place.
	for d: String in (persist.live_shapes() as Dictionary):
		var fr: Transform3D = persist.shape_state(d)["frame"]
		if fr.origin.is_equal_approx(Vector3(60, 0, 60)):
			a = d

	# ---- X8 a cut on a mirrored shape ----
	var t := _box(Vector3(100, 0, 100), Vector3(2, 1, 2))
	map.add_modifier("mirror", t)
	var cutter := _box(Vector3(101, 0.5, 100.5), Vector3(0.5, 1, 1))
	persist.begin_gesture("Cut")
	persist.record("op.create", {"op": "subtract", "cutter": cutter, "targets": [t]})
	persist.end_gesture()
	persist.refold()
	var left := 0                      # pieces of the mirrored half (x < 100)
	var right := 0
	for xr: Array in _xranges(t):
		if float(xr[1]) <= 100.001:
			left += 1
		else:
			right += 1
	_ok("X8 the original half is cut into pieces", right > 1, _xranges(t))
	_ok("X8 ... and the mirrored half the same way (its cutter mirrored)", left == right, [left, right])

	# ---- X11 stage order: the cut first, the mirror after ----
	var t2 := _box(Vector3(120, 0, 120), Vector3(2, 1, 2))
	var cutter2 := _box(Vector3(121, 0.5, 120.5), Vector3(0.5, 1, 1))
	persist.begin_gesture("Cut")
	persist.record("op.create", {"op": "subtract", "cutter": cutter2, "targets": [t2]})
	persist.end_gesture()
	persist.refold()
	map.add_modifier("mirror", t2, {"offset": 0.0})
	var l2 := 0
	var r2 := 0
	for xr: Array in _xranges(t2):
		if float(xr[1]) <= 120.001:
			l2 += 1
		else:
			r2 += 1
	_ok("X11 a mirror added after the cut still mirrors before it: halves cut alike", r2 > 1 and l2 == r2, [l2, r2])

	# ---- X9 make real ----
	var n9 := _solids(a).size()
	map.make_real(a)
	persist.refold()
	var baked := 0
	for b: int in (level.get_live_brush_ids() as PackedInt32Array):
		var d: Dictionary = level.get_brush(b)
		var lo := INF
		for v: Vector3 in FaceKit.corners(d["planes"]):
			lo = minf(lo, v.x)
		if lo >= 59.999 and lo <= 62.001 and FaceKit.corners(d["planes"]).size() == 8 and not (persist.live_shapes() as Dictionary).has(String(d.get("group", ""))):
			baked += 1
	_ok("X9 Make Real bakes the array's copies too", n9 == 3 and baked >= 3, [n9, baked])