OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_outliner_delete.gd

217 lines · gd ref:
extends Node
## GATE — DELETE and INSPECT from the Outliner (2026-10-05: "we should be able to
## delete nodes and currently cannot" · "there is one clearance node that has 3511
## something but i dont know what").
##
## Delete used to refuse everything but an authored collection. Now it acts on any row
## whose contents are SAVED IN THE ROOM, as ONE gesture (one Ctrl+Z):
##   1. an object row (a teleporter) -> object.remove; Ctrl+Z brings it back
##   2. several selected object rows -> all of them, one gesture
##   3. a brush GROUP row (Unsorted) -> every brush in it, one gesture; Ctrl+Z restores
##   4. a plain scene node (WallRunRange) -> adopted, so deletable + undoable; the level node is refused
##   5. a collection still deletes only the collection (view-local), never its brushes
## And a group row can be OPENED to see what is in it:
##   6. expanding a group row lists its brushes (key + size), capped, with a "more" row
##   7. clicking a brush row selects exactly that brush
##   8. the row stays open across a refresh (every fold rebuilds the tree)
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_outliner_delete.tscn

var fails := 0
var checks := 0


func _ok(what: String, cond: bool, note: String = "") -> void:
	checks += 1
	if not cond:
		fails += 1
	print("  %-64s %s  %s" % [what, "OK" if cond else "**FAIL**", 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"
	root_mode("build")
	var map: Node3D = (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(map)
	if checks < 26:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE outliner_delete: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


func root_mode(m: String) -> void:
	var gm: Node = get_tree().root.get_node_or_null("/root/GameMode")
	if gm != null:
		gm.set_mode(m)


## Select exactly these rows in the panel's Tree, the active one last.
func _select_rows(panel: Node, ids: Array) -> void:
	var tree: Tree = panel.get("_tree")
	tree.deselect_all()
	for id: Variant in ids:
		var it: TreeItem = (panel.get("_items") as Dictionary).get(String(id), null)
		if it != null:
			it.select(0)


func _gesture0(persist: Node) -> Dictionary:
	var g: Array = persist.gestures()
	return g[0] if not g.is_empty() else {}


func _run(map: Node3D) -> void:
	var panel: Node = map.get("_outliner")
	var persist: Node = map.get("_persist")
	var level: Node = map.get("level")
	_ok("map_test has an Outliner and a journal", panel != null and persist != null)
	if panel == null or persist == null:
		return
	panel.refresh()
	var items: Dictionary = panel.get("_items")

	# ---- 1. one object row ----
	var tele := "obj:scene:TeleporterToFlat"
	_ok("setup: the teleporter has a row", items.has(tele))
	_select_rows(panel, [tele])
	panel.delete_selected()
	await get_tree().process_frame
	_ok("1. Delete on an object row removes the object", map.object_node("scene:TeleporterToFlat") == null)
	_ok("1. as one gesture", int(_gesture0(persist).get("count", 0)) == 1, str(_gesture0(persist)))
	persist.undo_last()
	persist.refold()
	_ok("1. Ctrl+Z brings it back", map.object_node("scene:TeleporterToFlat") != null)

	# ---- 2. several object rows ----
	panel.refresh()
	items = panel.get("_items")
	_select_rows(panel, ["obj:scene:SpawnPoint1", "obj:scene:SpawnPoint2"])
	panel.delete_selected()
	await get_tree().process_frame
	_ok("2. two selected object rows are both deleted",
		map.object_node("scene:SpawnPoint1") == null and map.object_node("scene:SpawnPoint2") == null)
	_ok("2. as ONE gesture of 2", int(_gesture0(persist).get("count", 0)) == 2, str(_gesture0(persist)))
	persist.undo_last()
	persist.refold()
	_ok("2. one Ctrl+Z brings both back",
		map.object_node("scene:SpawnPoint1") != null and map.object_node("scene:SpawnPoint2") != null)

	# ---- 3. a brush group row ----
	panel.refresh()
	items = panel.get("_items")
	var rest_row: Dictionary = {}
	for r: Dictionary in panel.model.rows():
		if String(r["id"]) == "derived:rest":
			rest_row = r
	var n_rest := int(rest_row.get("count", 0))
	var live0: int = (level.get_live_brush_ids() as PackedInt32Array).size()
	_ok("setup: an Unsorted group with brushes", n_rest > 0, "%d" % n_rest)
	_select_rows(panel, ["derived:rest"])
	panel.delete_selected()
	await get_tree().process_frame
	var live1: int = (level.get_live_brush_ids() as PackedInt32Array).size()
	_ok("3. Delete on a group row removes every brush in it", live1 == live0 - n_rest, "%d -> %d" % [live0, live1])
	_ok("3. as one gesture", String(_gesture0(persist).get("label", "")) == "Delete"
		and int(_gesture0(persist).get("count", 0)) == n_rest, str(_gesture0(persist)))
	persist.undo_last()
	persist.refold()
	_ok("3. one Ctrl+Z restores them", (level.get_live_brush_ids() as PackedInt32Array).size() == live0)

	# ---- 4. a plain scene node IS deletable ----
	# User 2026-10-05: "wallrunrange is undeletable ? everything should be able to be
	# deleted". A content node of the map scene is adopted as a BASE object, so Delete
	# journals an object.remove (detached, children and all) and Ctrl+Z brings it back.
	panel.refresh()
	items = panel.get("_items")
	var plain := "obj:scene:WallRunRange"
	_ok("setup: the wall-run range is an object row", items.has(plain), str(items.keys().filter(
		func(k: Variant) -> bool: return String(k).contains("WallRun"))))
	var wr: Node = map.object_node("scene:WallRunRange")
	_ok("setup: ... with its walls under it", wr != null and wr.get_node_or_null("Straight") != null)
	_select_rows(panel, [plain])
	panel.delete_selected()
	await get_tree().process_frame
	_ok("4. Delete takes the range out of the world", wr != null and not wr.is_inside_tree())
	_ok("4. as one object.remove gesture", (_gesture0(persist).get("kinds", {}) as Dictionary).has("object.remove")
		and not bool(_gesture0(persist).get("undone", true)), str(_gesture0(persist)))
	persist.undo_last()
	persist.refold()
	_ok("4. one Ctrl+Z puts it back, walls and all", wr != null and wr.is_inside_tree()
		and wr.get_node_or_null("Straight") != null)

	# ---- 4b. the level itself is not a scene node to delete ----
	panel.refresh()
	items = panel.get("_items")
	var sysrow := "node:DCLevel"
	_ok("setup: the level's own row", items.has(sysrow))
	var g_before: int = (persist.gestures() as Array).size()
	_select_rows(panel, [sysrow])
	panel.delete_selected()
	await get_tree().process_frame
	_ok("4b. the level node is refused", map.get_node_or_null("DCLevel") != null
		and (persist.gestures() as Array).size() == g_before)

	# ---- 5. a collection (room state, #120 F1) holding an operator group ----
	var gk: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
		"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, Vector3(20, 0, 20))),
		"size": [2, 1, 2], "material_id": 2, "level_id": 0})       # a brush object: the shape
	persist.refold()
	var cdot: String = CollectionOps.create_empty(map, "C")
	persist.begin_gesture("Link")
	persist.record("col.link", {"target": cdot, "members": ["g:" + gk]})
	persist.end_gesture()
	persist.refold()
	panel.refresh()
	live0 = (level.get_live_brush_ids() as PackedInt32Array).size()
	_select_rows(panel, ["col:" + cdot])
	panel.delete_selected()
	persist.refold()
	await get_tree().process_frame
	_ok("5. a collection is deleted", not (persist.collection_tree()["cols"] as Dictionary).has(cdot))
	_ok("5. but not its brushes", (level.get_live_brush_ids() as PackedInt32Array).size() == live0)

	# ---- 6-8. inspect a group ----
	panel.refresh()
	items = panel.get("_items")
	var it: TreeItem = items.get("derived:rest", null)
	_ok("6. a group row starts closed", it != null and it.collapsed)
	if it == null:
		return
	it.collapsed = false                       # what clicking the arrow does
	await get_tree().process_frame
	items = panel.get("_items")
	var kids: Array = []
	var c := it.get_first_child()
	while c != null:
		kids.append(c)
		c = c.get_next()
	var first: Variant = kids[0].get_metadata(0) if not kids.is_empty() else null
	_ok("6. opening it lists its brushes",
		kids.size() == mini(n_rest, panel.MEMBER_ROWS) + (1 if n_rest > panel.MEMBER_ROWS else 0), "%d rows for %d" % [kids.size(), n_rest])
	_ok("6. each says what it is (key + size)", first is Dictionary
		and String((first as Dictionary).get("brush_key", "")) != ""
		and String(kids[0].get_text(0)).contains("×"), kids[0].get_text(0) if not kids.is_empty() else "")
	if first is Dictionary:
		# A CLICK, not just select(): in SELECT_MULTI a programmatic select() emits nothing
		# (scene/gui/tree.cpp Tree::item_selected); the mouse path emits multi_selected.
		var tree: Tree = panel.get("_tree")
		tree.deselect_all()
		kids[0].select(0)
		tree.multi_selected.emit(kids[0], 0, true)
		await get_tree().process_frame
		await get_tree().process_frame
		var sel: Array = map.get("_sel_brushes")
		_ok("7. clicking a brush row selects exactly that brush",
			sel.size() == 1 and String(level.get_brush_key(int(sel[0]))) == String(first["brush_key"]), str(sel))
	panel.refresh()
	var it2: TreeItem = (panel.get("_items") as Dictionary).get("derived:rest", null)
	_ok("8. the row stays open across a refresh", it2 != null and not it2.collapsed and it2.get_first_child() != null)