OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_meta_collection.gd

242 lines · gd ref:
extends Node
## GATE — SEVERAL COLLECTIONS, ONE NEW PARENT (user 2026-10-18: "in the organizer i can
## select a parent node and all of its children but i can't shift select many parent nodes
## and subsequent children ... i want to be able to select several groups and turn them
## into a new meta group that keeps the inheritance but now has another parent
## collection"). Rows are picked the way the mouse picks them: select + multi_selected.
##   M1  Shift-selecting two collection rows selects ALL their brushes (a child
##       collection's too), not just the last row's; both rows stay lit
##   M2  Ctrl+G wraps them: a new collection under their common parent, both moved into it
##       with their subtrees (a child collection keeps its parent), members untouched, and
##       nothing linked twice into the new one
##   M3  one Ctrl+Z puts it all back
##   M4  a selected collection INSIDE another selected one rides along (not pulled out)
##   M5  different parents: the new collection goes to Scene
##   M6  a brush object selected with them (not inside them) is linked into the new one
##   M7  + Collection with several collections selected wraps them too
##
## RUN AS A SCENE: CI=true "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_meta_collection.tscn

const CollectionFold := preload("res://scenes/maps/collection_fold.gd")

var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var level: Node
var panel: 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 meta_collection: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


func _frames(n: int) -> void:
	for i in n:
		await get_tree().process_frame


func _cols() -> Dictionary:
	return persist.collection_tree()["cols"]


func _parent(c: String) -> String:
	return String(_cols()[c]["parent"]) if _cols().has(c) else "<gone>"


## The collections not in `before` (what the last action made).
func _new_since(before: Dictionary) -> Array:
	var out: Array = []
	for d: String in _cols():
		if not before.has(d):
			out.append(d)
	return out


## Select these rows as Shift / Ctrl-clicks do: each select() then multi_selected; the
## panel reads the whole selection one deferred pass later.
func _pick_rows(row_ids: Array) -> void:
	panel.refresh()
	var tree: Tree = panel.get("_tree")
	tree.deselect_all()
	var items: Dictionary = panel.get("_items")
	for rid: String in row_ids:
		var it: TreeItem = items.get(rid, null)
		if it == null:
			_ok("setup: row %s exists" % rid, false)
			continue
		it.select(0)
		tree.multi_selected.emit(it, 0, true)
	await _frames(3)


## Ctrl+G, then the name prompt confirmed (Enter) with `name`.
func _ctrl_g(name := "Collection") -> void:
	Keymap.run("builder.collection_new")
	await _frames(3)
	var q: Node = map.get("_q_menu")
	if q.name_open():
		q.submit_name(name)
		await _frames(4)
	persist.refold()
	await _frames(1)


func _undo() -> void:
	persist.undo_gesture(persist.newest_live_gesture())
	persist.refold()
	await _frames(2)


## A loose brush made a brush object of its own in a new collection named `name`.
func _col_with(name: String, b: int, parent: String) -> String:
	map.select_brushes(PackedInt32Array([b]))
	var d := CollectionOps.create_from_selection(map, name, parent)
	persist.refold()
	return d


func _brushes_of(cs: Array) -> Array:
	var out: Array = []
	var t: Dictionary = persist.collection_tree()
	for c: String in cs:
		for m: String in CollectionFold.members_of(t, c, true):
			if m.begins_with("g:"):
				for b: int in (level.get_group_brush_ids(m.substr(2)) as PackedInt32Array):
					if not out.has(b):
						out.append(b)
	out.sort()
	return out


func _run() -> void:
	persist = map.get("_persist")
	level = map.get("level")
	panel = map.get("_outliner")
	var loose: Array = []
	for x: int in (level.get_live_brush_ids() as PackedInt32Array):
		if String(level.get_brush_group(x)) == "":
			loose.append(String(level.get_brush_key(x)))
	_ok("setup: five loose brushes", loose.size() >= 5, loose.size())
	if loose.size() < 5 or panel == null:
		return
	var by := func(k: String) -> int: return int(level.brush_by_key(k))
	var p := CollectionOps.create_empty(map, "P")
	persist.refold()
	var a := _col_with("A", by.call(loose[0]), p)
	var b := _col_with("B", by.call(loose[1]), p)
	var b1 := _col_with("B1", by.call(loose[2]), b)
	var d := _col_with("D", by.call(loose[3]), "")
	_ok("setup: P{A, B{B1}}, D", _parent(a) == p and _parent(b) == p and _parent(b1) == b and _parent(d) == "",
		[_parent(a), _parent(b), _parent(b1), _parent(d)])
	var a_members: Array = (_cols()[a]["members"] as Array).duplicate()

	# ---- M1 ----
	await _pick_rows(["col:" + a, "col:" + b])
	var sel: Array = Array(map.get("_sel_brushes"))
	sel.sort()
	var want := _brushes_of([a, b])
	_ok("M1 two collection rows select ALL their brushes (B1's too)", sel == want and want.size() >= 3, [sel, want])
	_ok("M1 both rows stay lit", panel.selected_collections() == [a, b], panel.selected_collections())

	# ---- M2 ----
	var before := _cols().duplicate()
	await _ctrl_g("Rooms")
	var made := _new_since(before)
	_ok("M2 Ctrl+G made one collection", made.size() == 1, made)
	if made.size() != 1:
		return
	var n: String = made[0]
	_ok("M2 named as typed in the prompt", String(_cols()[n]["name"]) == "Rooms", _cols()[n]["name"])
	_ok("M2 it sits where they were (under P)", _parent(n) == p, _parent(n))
	_ok("M2 A and B moved into it", _parent(a) == n and _parent(b) == n, [_parent(a), _parent(b)])
	_ok("M2 B1 is still B's child (the subtree came along)", _parent(b1) == b, _parent(b1))
	_ok("M2 A's members untouched", _cols()[a]["members"] == a_members, _cols()[a]["members"])
	_ok("M2 nothing linked twice into the new one", (_cols()[n]["members"] as Array).is_empty(), _cols()[n]["members"])

	# ---- M3 ----
	await _undo()
	_ok("M3 one Ctrl+Z: the new one is gone, A and B back under P",
		not _cols().has(n) and _parent(a) == p and _parent(b) == p and _parent(b1) == b,
		[_cols().has(n), _parent(a), _parent(b), _parent(b1)])

	# ---- M4 ----
	await _pick_rows(["col:" + b, "col:" + b1])
	before = _cols().duplicate()
	await _ctrl_g()
	made = _new_since(before)
	var n4: String = made[0] if made.size() == 1 else ""
	_ok("M4 B and its own child B1 selected: B moves, B1 stays inside B",
		n4 != "" and _parent(b) == n4 and _parent(b1) == b and _parent(n4) == p, [made, _parent(b), _parent(b1)])
	await _undo()

	# ---- M5 ----
	await _pick_rows(["col:" + a, "col:" + d])
	before = _cols().duplicate()
	await _ctrl_g()
	made = _new_since(before)
	var n5: String = made[0] if made.size() == 1 else ""
	_ok("M5 different parents (P, Scene): the new one goes to Scene",
		n5 != "" and _parent(n5) == "" and _parent(a) == n5 and _parent(d) == n5, [made, _parent(n5) if n5 != "" else ""])
	await _undo()

	# ---- M6 ----
	var k4: String = loose[4]
	persist.begin_gesture("setup")
	persist.record("brush.regroup", {"brush_keys": [k4], "group": k4})
	persist.end_gesture()
	persist.refold()
	panel.refresh()
	var row4: String = panel.call("_row_of_brush", by.call(k4), level)
	_ok("setup: the brush object has a row of its own", row4 == "group:" + k4, row4)
	await _pick_rows(["col:" + a, row4])
	before = _cols().duplicate()
	await _ctrl_g()
	made = _new_since(before)
	var n6: String = made[0] if made.size() == 1 else ""
	_ok("M6 a brush object picked with them is linked in; A's own is not",
		n6 != "" and _cols()[n6]["members"] == ["g:" + k4] and _parent(a) == n6,
		_cols()[n6]["members"] if n6 != "" else made)
	await _undo()

	# ---- M7 ----
	await _pick_rows(["col:" + a, "col:" + b])
	before = _cols().duplicate()
	panel.call("_on_new_collection")
	await _frames(2)
	var q: Node = map.get("_q_menu")
	_ok("M7 + Collection asks for the name", q.name_open())
	q.submit_name("Meta")
	await _frames(4)
	persist.refold()
	await _frames(1)
	made = _new_since(before)
	var n7: String = made[0] if made.size() == 1 else ""
	_ok("M7 + Collection with several selected wraps them too, named",
		n7 != "" and _parent(n7) == p and _parent(a) == n7 and _parent(b) == n7
		and String(_cols()[n7]["name"]) == "Meta", [made])
	await _undo()