extends Node
## GATE — OBJECTS AND COLLECTIONS ON KEYS AND IN THE OBJECT MENU (#120 F1, Blender 2.79's
## keys). Each action runs through Keymap.run — the path a menu click takes (it replays the
## bound key into the map) — so this covers the key and the menu row at once.
## K1 the Object menu lists parent, join, separate and the four collection commands
## K2 Ctrl+G with a shape selected: a new collection holding it
## K3 M opens a picker: + New Collection first, then the collections; picking one moves
## the selection there
## K4 Shift+M's pick links it into a second collection (now in both)
## K5 Ctrl+Alt+G lists ONLY the collections the selection is in; picking one unlinks it
## K6 Ctrl+J joins two loose brushes; P separates one again
## K7 F2 with a collection row selected opens its name for editing
## K8 H / Shift+H / Alt+H work on OBJECTS (audit bug 7): H hides the selected object,
## Shift+H the other objects (not the selection's parent), Alt+H shows them all
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_collection_keys.tscn
const TopBar := preload("res://scenes/game/editor_top_bar.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 < 21:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE collection_keys: %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 _run_action(a: String) -> void:
Keymap.run(a)
await _frames(3)
persist.refold()
await _frames(1)
func _cols() -> Dictionary:
return persist.collection_tree()["cols"]
func _named(name: String) -> String:
for d: String in _cols():
if String(_cols()[d]["name"]) == name:
return d
return ""
func _pick(text: String) -> void:
var q: Node = map.get("_q_menu")
q.choose_text(text)
await _frames(4)
persist.refold()
await _frames(1)
func _run() -> void:
persist = map.get("_persist")
level = map.get("level")
# ---- K1 ----
var obj_menu: Array = []
for m: Array in TopBar.MENUS:
if String(m[0]) == "Object":
obj_menu = m[1]
var want := ["builder.parent", "builder.clear_parent", "builder.join", "builder.separate",
"builder.collection_new", "builder.collection_move", "builder.collection_link", "builder.collection_remove"]
_ok("K1 the Object menu lists all eight", want.all(func(a: String) -> bool: return obj_menu.has(a)), obj_menu)
var shape: 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})
persist.refold()
# ---- K2 ----
map.select_shape(shape)
var q: Node = map.get("_q_menu")
# Ctrl+G ASKS FOR THE NAME first (user 2026-10-18); Esc makes nothing.
var n0 := _cols().size()
await _run_action("builder.collection_new")
_ok("K2 Ctrl+G asks for the name", q.name_open())
q.cancel_name()
await _frames(3)
persist.refold()
_ok("K2 Esc: nothing made", _cols().size() == n0, _cols().size() - n0)
await _run_action("builder.collection_new")
q.submit_name("A")
await _frames(4)
persist.refold()
var a := _named("A")
_ok("K2 Enter: a collection NAMED A holding the shape", a != "" and _cols()[a]["members"] == ["g:" + shape],
_cols())
var b := CollectionOps.create_empty(map, "B")
persist.refold()
# ---- K3 ----
map.select_shape(shape)
await _run_action("builder.collection_move")
var texts: Array = q.choice_texts()
_ok("K3 M opens the picker: + New Collection, then A and B", q.is_open() and texts == ["+ New Collection", "A", "B"], texts)
await _pick("B")
_ok("K3 picking B moves the shape there (out of A)", _cols()[b]["members"] == ["g:" + shape]
and (_cols()[a]["members"] as Array).is_empty(), [_cols()[a]["members"], _cols()[b]["members"]])
# + New Collection asks for the name too, then moves the selection into it.
map.select_shape(shape)
await _run_action("builder.collection_move")
await _pick("+ New Collection")
_ok("K3 + New Collection asks for the name", q.name_open())
q.submit_name("N")
await _frames(4)
persist.refold()
var nn := _named("N")
_ok("K3 ... and moves the shape into the new collection N", nn != "" and _cols()[nn]["members"] == ["g:" + shape]
and (_cols()[b]["members"] as Array).is_empty(), _cols().get(nn, {}))
map.select_shape(shape)
await _run_action("builder.collection_move")
await _pick("B") # back to B for K4 / K5
# ---- K4 ----
map.select_shape(shape)
await _run_action("builder.collection_link")
await _pick("A")
_ok("K4 Shift+M links it into A too (in both)", (_cols()[a]["members"] as Array).has("g:" + shape)
and (_cols()[b]["members"] as Array).has("g:" + shape))
# ---- K5 ----
var c := CollectionOps.create_empty(map, "C") # the shape is NOT in C
persist.refold()
map.select_shape(shape)
await _run_action("builder.collection_remove")
texts = q.choice_texts()
_ok("K5 Ctrl+Alt+G lists only A and B (not C)", texts == ["A", "B"], texts)
await _pick("A")
_ok("K5 picking A takes it out of A only", not (_cols()[a]["members"] as Array).has("g:" + shape)
and (_cols()[b]["members"] as Array).has("g:" + shape))
_ok("K5 (C still exists, empty)", _cols().has(c))
# ---- K6 ----
var loose: Array = []
for x: int in (level.get_live_brush_ids() as PackedInt32Array):
if String(level.get_brush_group(x)) == "":
loose.append(x)
_ok("setup: two loose brushes", loose.size() >= 2)
if loose.size() < 2:
return
var k0 := String(level.get_brush_key(int(loose[0])))
var k1 := String(level.get_brush_key(int(loose[1])))
map.select_brushes(PackedInt32Array([int(loose[0]), int(loose[1])]))
await _run_action("builder.join")
_ok("K6 Ctrl+J joins them into the active one's object",
String(level.get_brush_group(int(level.brush_by_key(k0)))) == k1
and String(level.get_brush_group(int(level.brush_by_key(k1)))) == k1)
map.select_brushes(PackedInt32Array([int(level.brush_by_key(k0))]))
await _run_action("builder.separate")
var g0 := String(level.get_brush_group(int(level.brush_by_key(k0))))
_ok("K6 P separates it into a new object", g0 != "" and g0 != k1, g0)
# ---- K7 ----
var panel: Node = map.get("_outliner")
panel.refresh()
var tree: Tree = panel.get("_tree")
tree.deselect_all()
((panel.get("_items") as Dictionary)["col:" + b] as TreeItem).select(0)
Keymap.run("builder.rename")
await _frames(3)
var editing := false
for ch: Node in tree.get_children(true):
if ch is Popup and (ch as Popup).visible:
editing = true
_ok("K7 F2 opens the selected collection's name for editing", editing)
# ---- K8 ----
var p8: String = map.create_object("marker", Transform3D(Basis(), Vector3(60, 1, 0)))
var c8: String = map.create_object("marker", Transform3D(Basis(), Vector3(61, 1, 0)))
map.parent_objects([c8], p8)
persist.refold()
panel.refresh()
map.select_object("scene:SpawnPoint1")
await _run_action("builder.hide")
_ok("K8 H hides the selected object", not (map.object_node("scene:SpawnPoint1") as Node3D).is_visible_in_tree())
await _run_action("builder.unhide_all")
_ok("K8 Alt+H shows it again", (map.object_node("scene:SpawnPoint1") as Node3D).is_visible_in_tree())
map.select_object(c8)
await _run_action("builder.hide_unselected")
_ok("K8 Shift+H hides the other objects", not (map.object_node("scene:SpawnPoint2") as Node3D).is_visible_in_tree())
_ok("K8 ... but not the selected child, nor its parent", (map.object_node(c8) as Node3D).is_visible_in_tree()
and (map.object_node(p8) as Node3D).is_visible_in_tree())
await _run_action("builder.unhide_all")
_ok("K8 Alt+H shows everything", (map.object_node("scene:SpawnPoint2") as Node3D).is_visible_in_tree())
panel.model.save(String(panel.get("_save_key"))) # leave the saved view as found