OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_outliner_locked.gd

139 lines · gd ref:
extends SceneTree
## GATE — the Outliner's SYSTEM rows are LOCKED and TOGGLEABLE (hyper-games #109,
## 2026-10-12: "canvaslayer, NavVolume, EntityLinks cannot be deleted, also what is
## entitylinks?" — user: "show them as locked, make them toggleable").
##
## A plain node row lives in the map's .tscn or is built at runtime (the bot nav grid,
## the teleporter link lines, the command palette): there is no record to remove, so
## Delete stays refused. What changes is that the row SAYS so before you try, and says
## what the node is.
##
##   1. NavVolume, the command palette, DCLevel and SceneObjects rows are locked and
##      carry a note; object rows are not locked
##   2. a locked row reads as locked in the tree, and its tooltip is its note
##   3. Delete on a locked row refuses, the node stays, and the status line explains why
##   4. the palette (editor UI) has no eye; NavVolume has one
##   5. EntityLinks: its eye hides it, and re-entering build mode keeps it hidden
##      (map_dc_level forced it visible on every mode change); unhiding brings it back
##   0. THE SYSTEM FILTER (#120 F1): off, NavVolume / EntityLinks / the palette are not
##      in the tree (the model still lists them); DCLevel and SceneObjects always are;
##      on, they are. The rest of this gate runs with it on.
##
## RUN: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 "$GODOT" --headless --path . --script res://scenes/test/gate_outliner_locked.gd

var _frame := 0
var _map: Node = null
var fails := 0
var checks := 0


func _p(c: bool, m: String, got: Variant = "") -> void:
	checks += 1
	if not c:
		fails += 1
	print("  %-66s %s  %s" % [m, "OK" if c else "**FAIL**", str(got)])


func _process(_d: float) -> bool:
	_frame += 1
	if _frame == 1:
		root.get_node("/root/GameMode").set_mode("build")
		_map = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
		root.add_child(_map)
		return false
	if _frame < 40:
		return false
	_run()
	if checks < 14:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE outliner_locked: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	quit(0 if fails == 0 else 1)
	return true


func _rows_by_id(model: Variant) -> Dictionary:
	var out := {}
	for r: Dictionary in model.rows():
		out[String(r["id"])] = r
	return out


func _palette_id(rows: Dictionary) -> String:
	for id: String in rows:
		var n: Node = _map.get_node_or_null((rows[id] as Dictionary).get("node_path", NodePath("")) as NodePath)
		if n is CanvasLayer:
			return id
	return ""


func _select(panel: Node, id: String) -> TreeItem:
	var tree: Tree = panel.get("_tree")
	tree.deselect_all()
	var it: TreeItem = (panel.get("_items") as Dictionary).get(id, null)
	if it != null:
		it.select(0)
	return it


func _run() -> void:
	var panel: Node = _map.get("_outliner")
	var model: Variant = panel.get("model") if panel != null else null
	_p(panel != null and model != null, "setup: the map has an Outliner")
	if model == null:
		return
	var rows := _rows_by_id(model)
	var pal := _palette_id(rows)
	_p(pal != "", "setup: the command palette has a row", pal)

	# 1. locked + noted
	for id: String in ["node:NavVolume", pal, "node:DCLevel", "node:SceneObjects"]:
		var r: Dictionary = rows.get(id, {})
		_p(bool(r.get("locked", false)) and String(r.get("note", "")) != "",
			"1. %s is locked with a note" % id, String(r.get("note", "<none>")))
	var obj: Dictionary = rows.get("obj:scene:SpawnPoint1", {})
	_p(not obj.is_empty() and not bool(obj.get("locked", false)), "1. an object row (SpawnPoint1) is not locked")

	# 0. the System filter
	panel.set_show_system(false)
	var items0: Dictionary = panel.get("_items")
	_p(not items0.has("node:NavVolume") and not items0.has("node:EntityLinks") and not items0.has(pal),
		"0. filter off: no NavVolume / EntityLinks / palette rows")
	_p(rows.has("node:NavVolume"), "0. ... though the model still lists them")
	_p(items0.has("node:DCLevel") and items0.has("node:SceneObjects"), "0. DCLevel and SceneObjects always show")
	panel.set_show_system(true)
	_p((panel.get("_items") as Dictionary).has("node:NavVolume"), "0. filter on: they are back")

	# 2. reads as locked
	panel.refresh()
	var it: TreeItem = (panel.get("_items") as Dictionary).get("node:NavVolume", null)
	var note := String((rows.get("node:NavVolume", {}) as Dictionary).get("note", "?"))
	_p(it != null and it.get_text(0).contains("locked"), "2. the NavVolume row reads as locked", it.get_text(0) if it else "")
	_p(it != null and it.get_tooltip_text(0).contains(note), "2. its tooltip is its note", it.get_tooltip_text(0) if it else "")

	# 3. delete refuses and explains
	_select(panel, "node:NavVolume")
	var n: int = panel.delete_selected()
	var status: Label = panel.get("_status")
	_p(n == 0 and _map.get_node_or_null("NavVolume") != null, "3. Delete refuses; NavVolume stays", n)
	_p(status != null and status.text.contains("locked") and status.text.contains(note),
		"3. the status line says it is locked and what it is", status.text if status else "")

	# 4. eyes
	var pit: TreeItem = (panel.get("_items") as Dictionary).get(pal, null)
	_p(pit != null and not pit.is_editable(1), "4. the palette row has no eye")
	_p(it != null and it.is_editable(1), "4. the NavVolume row has an eye")

	# 5. EntityLinks: hide survives a mode change
	var links: Node3D = _map.call("_ensure_entity_links")
	model.set_row_hidden("node:EntityLinks", true)
	model.apply()
	_p(links != null and not links.visible, "5. the eye hides EntityLinks")
	var gm: Node = root.get_node("/root/GameMode")
	gm.set_mode("play")
	gm.set_mode("build")
	_p(links != null and not links.visible, "5. re-entering build mode keeps it hidden", links.visible if links else "")
	model.set_row_hidden("node:EntityLinks", false)
	model.apply()
	_p(links != null and links.visible, "5. unhiding brings it back (build mode)")