OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_stack_panel.gd

205 lines · gd ref:
extends Node
## GATE — THE STACK PANEL (docs/cutting-and-operations.md §9b).
##
##   1. ROWS: a wall with two live cuts -> the panel shows the generator + 2 op rows, in
##      stack order
##   2. EYE: switching op 1 off makes its door solid again, and its cutter STAYS a
##      cutter (wire) — one op.set, one Ctrl+Z brings the cut back
##   3. KIND: op 2 subtract -> intersect (op.set)
##   (no ORDER section: subtract / intersect / slice commute as sets — §9b)
##   5. REMOVE: ✕ deletes an op (op.remove) — one row fewer; Ctrl+Z restores it
##   6. CUTTER: selecting a cutter shows its "cuts" row
##   8. MODIFIERS (#120 F2), through the widgets: Add modifier ▸ Mirror adds a row BEFORE
##      the cuts; its axis dropdown sets y; a chamfer row comes AFTER the cuts (the fixed
##      stage order); the eye switches a modifier off; ✕ removes it
##   9. GENERATOR row: the dropdown assigns stairs; a plain brush shows Make parametric,
##      and pressing it makes a shape
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_stack_panel.tscn

var fails := 0
var checks := 0


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")
	var map: Node3D = (load("res://scenes/maps/map_test.tscn") as PackedScene).instantiate()
	add_child(map)
	for i in 6:
		await get_tree().process_frame
	await _run(map)
	get_tree().root.get_node("/root/GameMode").set_mode("play")
	if checks < 24:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE stack_panel: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


func _solid(level: Node, p: Vector3) -> bool:
	return not (level.raycast(p + Vector3(0, 0, 6), p + Vector3(0, 0, -6)) as Dictionary).is_empty()


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


func _kinds(persist: Node, since: int) -> Array:
	var out: Array = []
	for r: Dictionary in (persist.records() as Array):
		if int(r["seq"]) > since:
			out.append(String(r["kind"]))
	out.sort()
	return out


## The op rows the panel is showing: [{dot, kind, on}] in order.
func _rows(panel: Node) -> Array:
	panel.call("refresh")
	return panel.call("op_rows")


func _run(map: Node3D) -> void:
	var persist: Node = map.get("_persist")
	var level: Node = map.get("level")
	var panel: Node = map.get("_stack_panel")
	_ok("the Stack panel exists", panel != null)
	if panel == null or not map.has_method("op_stack"):
		_ok("the map has the stack API (op_stack, set_op, move_op, remove_op)", false)
		return
	const W := Vector3(60, 0, 60)
	var wall: String = _box(persist, W, Vector3(8, 4, 1))
	var c1: String = _box(persist, W + Vector3(1, -1, -1), Vector3(2, 4, 3))   # door at x 1..3
	var c2: String = _box(persist, W + Vector3(5, -1, -1), Vector3(2, 4, 3))   # door at x 5..7
	persist.record("op.create", {"op": "subtract", "cutter": c1, "targets": [wall]})
	persist.record("op.create", {"op": "subtract", "cutter": c2, "targets": [wall]})
	persist.refold()
	map.select_shape(wall)

	# ---- 1. rows ----
	var rows: Array = _rows(panel)
	_ok("1. the subject is the wall", String(panel.call("subject")) == wall, panel.call("subject"))
	_ok("1. two op rows", rows.size() == 2, rows)
	_ok("1. ... in stack order (c1's op first)", rows.size() == 2 and String(rows[0]["cutter"]) == c1, rows)
	_ok("1. both doors are air", not _solid(level, W + Vector3(2, 1, 0)) and not _solid(level, W + Vector3(6, 1, 0)))
	if rows.size() != 2:
		return
	var op1 := String(rows[0]["dot"])
	var op2 := String(rows[1]["dot"])

	# ---- 2. eye ----
	var s0: int = persist._max_seq_seen()
	panel.call("press", op1, "eye")
	_ok("2. eye off: ONE op.set", _kinds(persist, s0) == ["op.set"], _kinds(persist, s0))
	_ok("2. door 1 is solid again", _solid(level, W + Vector3(2, 1, 0)))
	_ok("2. door 2 is still air", not _solid(level, W + Vector3(6, 1, 0)))
	_ok("2. c1 is STILL a cutter (wire, not solid)", (level.get_cutter_brush_ids() as PackedInt32Array).size() == 2)
	_ok("2. the row reads off", not bool(_rows(panel)[0]["on"]))
	persist.undo_last()
	persist.refold()
	_ok("2. Ctrl+Z: door 1 is air again", not _solid(level, W + Vector3(2, 1, 0)))

	# ---- 3. kind ----
	panel.call("press", op2, "kind", "intersect")
	_ok("3. op 2 reads intersect", String(_rows(panel)[1]["kind"]) == "intersect", _rows(panel))
	# subtract c1 then intersect c2: only the overlap with c2 remains (x 5..7), minus nothing.
	_ok("3. intersect after subtract keeps only c2's overlap", _solid(level, W + Vector3(6, 1, 0))
		and not _solid(level, W + Vector3(4, 1, 0)))

	# ---- 5. remove ----
	var s5: int = persist._max_seq_seen()
	panel.call("press", op2, "remove")
	_ok("5. ✕: ONE op.remove", _kinds(persist, s5) == ["op.remove"], _kinds(persist, s5))
	_ok("5. one row left", _rows(panel).size() == 1, _rows(panel))
	persist.undo_last()
	persist.refold()
	_ok("5. Ctrl+Z: two rows again", _rows(panel).size() == 2, _rows(panel))

	# ---- 7. the WIDGETS, not just press(): the eye CheckBox and the kind OptionButton ----
	panel.call("refresh")
	var row0: Node = (panel.get("_rows") as Node).get_child(0)
	var eye: CheckBox = row0.get_child(0)
	eye.button_pressed = false                   # what a click does: toggled(false)
	await get_tree().process_frame
	_ok("7. clicking the eye switches op 1 off", not bool(_rows(panel)[0]["on"]), _rows(panel))
	var row0b: Node = (panel.get("_rows") as Node).get_child(0)
	var kind: OptionButton = row0b.get_child(1)
	kind.item_selected.emit(2)                   # pick "slice"
	await get_tree().process_frame
	_ok("7. the kind dropdown sets op 1 to slice", String(_rows(panel)[0]["kind"]) == "slice", _rows(panel))

	# ---- 6. cutter ----
	map.select_shape(c2)
	panel.call("refresh")
	var cuts: Array = panel.call("cut_rows")
	_ok("6. the cutter shows what it cuts", cuts.size() == 1 and String(cuts[0]["target"]) == wall, cuts)

	# ---- 8. modifiers ----
	map.select_shape(wall)
	panel.call("refresh")
	var add: OptionButton = panel.get("_add")
	_ok("8. Add modifier is offered for a shape", add.visible and add.item_count == 1 + (panel.get("MOD_KINDS") as Array).size())
	add.item_selected.emit(1)                    # Mirror
	await get_tree().process_frame
	var mods: Array = panel.call("mod_rows")
	_ok("8. a mirror modifier", mods.size() == 1 and String(mods[0]["kind"]) == "mirror", mods)
	var rows_box: Node = panel.get("_rows")
	_ok("8. its row comes first, before the two cuts", rows_box.get_child_count() == 3
		and String((rows_box.get_child(0).get_child(1) as Label).text).contains("Mirror"))
	(rows_box.get_child(0).get_child(2) as OptionButton).item_selected.emit(1)   # axis Y
	await get_tree().process_frame
	_ok("8. the axis dropdown sets y", String((panel.call("mod_rows")[0]["params"] as Dictionary).get("axis", "")) == "y",
		panel.call("mod_rows"))
	add.item_selected.emit(1 + (panel.get("MOD_KINDS") as Array).find("chamfer"))
	await get_tree().process_frame
	rows_box = panel.get("_rows")
	_ok("8. a chamfer row comes after the cuts", rows_box.get_child_count() == 4
		and String((rows_box.get_child(3).get_child(1) as Label).text).contains("Chamfer"))
	(rows_box.get_child(0).get_child(0) as CheckBox).button_pressed = false        # mirror's eye
	await get_tree().process_frame
	_ok("8. the eye switches the mirror off", not bool(panel.call("mod_rows")[0]["on"]))
	rows_box = panel.get("_rows")
	var x: Button = rows_box.get_child(3).get_child(rows_box.get_child(3).get_child_count() - 1)
	x.pressed.emit()                             # chamfer's ✕
	await get_tree().process_frame
	_ok("8. ✕ removes the chamfer", (panel.call("mod_rows") as Array).size() == 1)

	# ---- 9. generator ----
	var gen: HBoxContainer = panel.get("_gen")
	var ob: OptionButton = gen.get_child(1)
	var stairs_i := -1
	for i in ob.item_count:
		if ob.get_item_text(i) == "Stairs":
			stairs_i = i
	ob.item_selected.emit(stairs_i)
	await get_tree().process_frame
	_ok("9. the generator dropdown assigns stairs", String(persist.shape_state(wall).get("type", "")) == "stairs")
	persist.begin_gesture("Add")
	var bdot: String = persist.record("brush.create", {"planes": Array(ShapeKit.box_piece(Vector3(90, 0, 90), Vector3(91, 1, 91))["planes"]),
		"proj": [], "material_id": 1, "level_id": 0, "sky": false})
	persist.end_gesture()
	persist.refold()
	map.select_brushes(PackedInt32Array([int(level.brush_by_key(bdot))]))
	panel.call("refresh")
	gen = panel.get("_gen")
	var mp: Node = gen.get_child(1)
	_ok("9. a plain brush shows Make parametric", mp is Button and (mp as Button).text == "Make parametric")
	var n9: int = (persist.live_shapes() as Dictionary).size()
	(mp as Button).pressed.emit()
	await get_tree().process_frame
	_ok("9. ... which makes it a shape", (persist.live_shapes() as Dictionary).size() == n9 + 1)