OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_q_stack.gd

221 lines · gd ref:
extends Node
## GATE — THE Q MENU'S STACK OPERATIONS (#120 L2 §13.4–13.5, stack_ops.gd).
##   Q1  Change cut: the rows offer the other kinds; picking one changes the op's kind
##   Q2  Extract: target ∩ cutter (as BEFORE the cut) becomes ONE new hull shape in an
##       "Extract" collection; the cut stays on (the hole is still there)
##   Q3  Apply up to here (first of two cuts): the first is baked (op.apply), the target is
##       plain brushes keeping its name, the second cut stays LIVE and still cuts
##   Q4  ... one Ctrl+Z: the shape is live again with both cuts
##   Q5  BoxCutter Y: the drawn shape ∩ the target becomes a new shape, nothing is cut
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_q_stack.tscn

const StackOps := preload("res://scenes/maps/stack_ops.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 < 19:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE q_stack: %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 _cut(kind: String, cutter: String, target: String) -> String:
	persist.begin_gesture("cut")
	var d: String = persist.record("op.create", {"op": kind, "cutter": cutter, "targets": [target]})
	persist.end_gesture()
	persist.refold()
	return d


func _count(group: String, p: Vector3) -> int:
	var n := 0
	for b: int in (level.get_group_brush_ids(group) as PackedInt32Array):
		var d: Dictionary = level.get_brush(b)
		if bool(d.get("clearance", false)) or bool(d.get("cutter", false)):
			continue
		var pl: PackedFloat32Array = d["planes"]
		var inside := true
		for i in pl.size() / 4:
			if Vector3(pl[i * 4], pl[i * 4 + 1], pl[i * 4 + 2]).dot(p) > pl[i * 4 + 3] + 1.0e-4:
				inside = false
				break
		if inside:
			n += 1
	return n


func _kind(op: String) -> String:
	return String(LevelPersistence.OpFold.resolve_states(persist.records(false)).get(op, {}).get("kind", ""))


func _col_named(name: String) -> Array:
	var out: Array = []
	var t: Dictionary = persist.collection_tree()
	for cd: String in (t.get("cols", {}) as Dictionary):
		if String(t["cols"][cd]["name"]) == name:
			out.append(cd)
	return out


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

	# ---- Q1 change cut ----
	var t1 := _box(Vector3(300, 0, 0), Vector3(4, 2, 4))
	var c1 := _box(Vector3(301, -1, 1), Vector3(2, 4, 2))
	var o1 := _cut("subtract", c1, t1)
	var rows: Array = StackOps.change_rows(map, t1)
	var texts: Array = rows.map(func(r: Array) -> String: return String(r[0]))
	_ok("Q1 Change cut offers the other four kinds", rows.size() == 4 and not str(texts).contains("→ subtract"), texts)
	for r: Array in rows:
		if String(r[0]).ends_with("→ slice"):
			(r[1] as Callable).call()
	_ok("Q1 ... picking slice changes the op", _kind(o1) == "slice", _kind(o1))

	# ---- Q2 extract ----
	var t2 := _box(Vector3(310, 0, 0), Vector3(4, 2, 4))
	var c2 := _box(Vector3(311, 1, 1), Vector3(2, 2, 2))      # dips 1 into the top
	var o2 := _cut("subtract", c2, t2)
	var made: Array = StackOps.extract(map, t2, o2)
	_ok("Q2 Extract makes one new shape", made.size() == 1, made)
	if made.size() == 1:
		var lo := Vector3.INF
		var hi := -Vector3.INF
		for b: int in (level.get_group_brush_ids(String(made[0])) as PackedInt32Array):
			for c: Vector3 in FaceKit.corners(level.get_brush(b)["planes"]):
				lo = lo.min(c)
				hi = hi.max(c)
		_ok("Q2 ... it is target ∩ cutter: x 311..313, y 1..2, z 1..3",
			lo.is_equal_approx(Vector3(311, 1, 1)) and hi.is_equal_approx(Vector3(313, 2, 3)), [lo, hi])
	var ex := _col_named("Extract")
	_ok("Q2 ... in an Extract collection", ex.size() >= 1 and (persist.collection_tree()["cols"][ex[-1]]["members"] as Array).has("g:" + String(made[0]) if made.size() == 1 else ""))
	_ok("Q2 ... and the cut is still on (the hole)", _count(t2, Vector3(312, 1.5, 2)) == 0)

	# ---- Q3 / Q4 apply up to here ----
	var t3 := _box(Vector3(320, 0, 0), Vector3(6, 2, 4))
	var ca := _box(Vector3(320.5, -1, 1), Vector3(1, 4, 2))
	var cb := _box(Vector3(324.5, -1, 1), Vector3(1, 4, 2))
	var oa := _cut("subtract", ca, t3)
	var ob := _cut("subtract", cb, t3)
	var n3: int = StackOps.apply_up_to(map, t3, oa)
	var live: Dictionary = persist.live_shapes()
	var states: Dictionary = LevelPersistence.OpFold.resolve_states(persist.records(false))
	_ok("Q3 Apply up to the first cut: the target is baked (no longer a live shape)", n3 > 0 and not live.has(t3), n3)
	_ok("Q3 ... the first cut is applied, the second is not", bool(states[oa]["applied"]) and not bool(states[ob]["applied"]))
	_ok("Q3 ... both holes are there (the second cut still cuts the baked pieces)",
		_count(t3, Vector3(321, 1, 2)) == 0 and _count(t3, Vector3(325, 1, 2)) == 0
		and _count(t3, Vector3(323, 1, 2)) == 1, [_count(t3, Vector3(321, 1, 2)), _count(t3, Vector3(325, 1, 2)), _count(t3, Vector3(323, 1, 2))])
	persist.undo_last()
	persist.refold()
	_ok("Q4 one Ctrl+Z: the shape is live again, both cuts live", (persist.live_shapes() as Dictionary).has(t3)
		and not bool(LevelPersistence.OpFold.resolve_states(persist.records(false))[oa]["applied"]))

	# ---- Q6 the Q menu and the keys ----
	map.select_shape(t2)
	await get_tree().process_frame
	var acts: Array = []
	for sec: Array in (map.q_sections() as Array):
		for row: Array in (sec[1] as Array):
			acts.append(String(row[0]))
	_ok("Q6 Q on a shape with cuts offers Change cut, Extract, Apply up to here",
		acts.has("builder.change_cut") and acts.has("builder.extract_cut") and acts.has("builder.apply_up_to"), acts)
	var ax := InputEventKey.new()
	ax.keycode = KEY_X
	ax.physical_keycode = KEY_X
	ax.alt_pressed = true
	ax.pressed = true
	get_viewport().push_input(ax)
	await get_tree().process_frame
	var q: Node = map.get("_q_menu")
	_ok("Q6 ... Alt+X opens Change cut", q.is_open())
	(q.get("_menu") as PopupMenu).hide()
	map.select_brushes(PackedInt32Array())

	# ---- Q5 BoxCutter Y ----
	map._ensure_ops()
	var op: Node = map.get_node("BoxCutter")
	var t5 := _box(Vector3(340, 0, 0), Vector3(4, 2, 4))
	var nops := (persist.records(false) as Array).filter(func(r: Dictionary) -> bool: return String(r.get("kind", "")) == "op.create").size()
	op.set_tool_setting("depth", 0)
	op.place(Vector3(341, 2, 1), Vector3.UP, Vector3(341, 2, 1), Vector3(343, 2, 3), t5)
	var k := InputEventKey.new()
	k.keycode = KEY_Y
	k.physical_keycode = KEY_Y
	k.pressed = true
	op.handle_key(k)
	var nops2 := (persist.records(false) as Array).filter(func(r: Dictionary) -> bool: return String(r.get("kind", "")) == "op.create").size()
	_ok("Q5 Y extracts the drawn shape ∩ the target, and cuts nothing", nops2 == nops and _count(t5, Vector3(342, 1, 2)) == 1
		and (_col_named("Extract").size() >= 2), [nops2 - nops, _count(t5, Vector3(342, 1, 2)), _col_named("Extract").size()])

	# ---- S: the stack scroll preview (§13.5, step 8) — LOCAL, never journaled ----
	var t8 := _box(Vector3(360, 0, 0), Vector3(6, 2, 4))
	var c8a := _box(Vector3(360.5, -1, 1), Vector3(1, 4, 2))
	var c8b := _box(Vector3(364.5, -1, 1), Vector3(1, 4, 2))
	_cut("subtract", c8a, t8)
	_cut("subtract", c8b, t8)
	map.select_shape(t8)
	var nrec := (persist.records(false) as Array).size()
	StackOps.scroll(map, -1)
	_ok("S1 scroll back one: the first cut shows, the second does not",
		_count(t8, Vector3(361, 1, 2)) == 0 and _count(t8, Vector3(365, 1, 2)) == 1)
	StackOps.scroll(map, -1)
	_ok("S2 back again: no cuts show", _count(t8, Vector3(361, 1, 2)) == 1 and _count(t8, Vector3(365, 1, 2)) == 1)
	_ok("S3 nothing was journaled", (persist.records(false) as Array).size() == nrec, (persist.records(false) as Array).size() - nrec)
	StackOps.scroll(map, 1)
	StackOps.scroll(map, 1)
	_ok("S4 forward twice: both cuts again", _count(t8, Vector3(361, 1, 2)) == 0 and _count(t8, Vector3(365, 1, 2)) == 0)
	StackOps.scroll(map, -1)
	map.select_brushes(PackedInt32Array())
	await get_tree().process_frame
	_ok("S5 a selection change clears the preview", _count(t8, Vector3(365, 1, 2)) == 0
		and (not persist.has_meta("preview_off") or (persist.get_meta("preview_off") as Dictionary).is_empty()))
	map.select_shape(t8)
	await get_tree().process_frame
	var kb := InputEventKey.new()
	kb.keycode = KEY_BRACKETLEFT
	kb.physical_keycode = KEY_BRACKETLEFT
	kb.ctrl_pressed = true
	kb.alt_pressed = true
	kb.pressed = true
	get_viewport().push_input(kb)
	await get_tree().process_frame
	_ok("S6 Ctrl+Alt+[ scrolls the selected shape's stack back", _count(t8, Vector3(365, 1, 2)) == 1)
	StackOps.scroll_clear(map)