extends Node
## GATE — COLLECTIONS ARE ROOM STATE (#120 F1): col.* records through LevelPersistence.
## P1 col.create / col.link recorded through the journal come back as the tree
## P2 a collection edit does NOT re-fold the level (the brush half is skipped) and no
## record is reported skipped (DCLevel never sees col.*)
## P3 undo of the link gesture unlinks; undo of the create removes the collection
## P4 the tree survives a full level fold (a brush edit after it)
## P5 FLATTEN keeps the collections: same names and nesting, and every member still
## names something live — a shape and a created object are re-minted under new
## dots by the flatten, so their members must be remapped
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_collection_persist.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_dev_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 < 14:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE collection_persist: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _run(map: Node3D) -> void:
var persist: Node = map.get("_persist")
var level: Node = map.get("level")
_ok("map_dev_test has a journal", persist != null and persist.has_method("collection_tree"))
if persist == null or not persist.has_method("collection_tree"):
return
persist.refold()
var g0 := "gate-group" # a brush object's group (members are "g:<group>")
# ---- P1 ----
persist.begin_gesture("New collection")
var col: String = persist.record("col.create", {"name": "Rooms", "parent": ""})
persist.end_gesture()
persist.begin_gesture("Link")
persist.record("col.link", {"target": col, "members": ["obj:scene:SpawnPoint1", "g:" + g0]})
persist.end_gesture()
var lf: Dictionary = persist.refold()
var t: Dictionary = persist.collection_tree()
_ok("P1 the collection is in the tree, named", t["cols"].has(col) and String(t["cols"][col]["name"]) == "Rooms",
t["cols"].keys())
_ok("P1 ... with both members", t["cols"].has(col) and (t["cols"][col]["members"] as Array).size() == 2,
t["cols"][col]["members"] if t["cols"].has(col) else [])
# ---- P2 ----
_ok("P2 the collection edit skipped the brush fold", String(lf.get("brush_fold", "")) == "skipped", lf.get("brush_fold"))
_ok("P2 no record reported skipped", int(lf.get("skipped", 0)) == 0, lf.get("skipped"))
# ---- P4 (before undo): a brush edit forces a full fold ----
var b0: int = (level.get_live_brush_ids() as PackedInt32Array)[0]
persist.begin_gesture("Move")
persist.record("brush.translate", {"brush_key": String(level.get_brush_key(b0)), "delta": [0, 0.5, 0]})
persist.end_gesture()
var lf2: Dictionary = persist.refold()
_ok("P4 a brush edit folds the level", String(lf2.get("brush_fold", "")) != "skipped", lf2.get("brush_fold"))
_ok("P4 ... and the tree is unchanged", str(persist.collection_tree()) == str(t))
_ok("P4 ... with nothing skipped", int(lf2.get("skipped", 0)) == 0, lf2.get("skipped"))
persist.undo_last()
persist.refold()
# ---- P3 ----
persist.undo_last() # the link
persist.refold()
var t3: Dictionary = persist.collection_tree()
_ok("P3 undoing the link empties the collection", t3["cols"].has(col) and (t3["cols"][col]["members"] as Array).is_empty(),
t3["cols"].get(col, {}))
persist.undo_last() # the create
persist.refold()
_ok("P3 undoing the create removes it", not persist.collection_tree()["cols"].has(col))
# ---- P5 ----
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})
var obj: String = map.create_object("marker", Transform3D(Basis(), Vector3(30, 1, 0)))
persist.refold()
var loose := -1
for b: int in (level.get_live_brush_ids() as PackedInt32Array):
if String(level.get_brush_group(b)) == "":
loose = b
break
map.select_brushes(PackedInt32Array([loose]))
var outer: String = CollectionOps.create_empty(map, "Outer")
var inner: String = CollectionOps.create_from_selection(map, "Inner", outer) # the loose brush, made an object
persist.begin_gesture("Link")
persist.record("col.link", {"target": outer, "members": ["g:" + shape, "obj:" + obj]})
persist.end_gesture()
persist.refold()
var r5: Dictionary = persist.flatten_map()
_ok("P5 the flatten ran", not r5.has("error"), r5)
var t5: Dictionary = persist.collection_tree()["cols"]
var by_name := {}
for d: String in t5:
by_name[String(t5[d]["name"])] = d
_ok("P5 both collections survive", by_name.has("Outer") and by_name.has("Inner"), by_name.keys())
if not (by_name.has("Outer") and by_name.has("Inner")):
return
_ok("P5 ... nested as before", String(t5[by_name["Inner"]]["parent"]) == String(by_name["Outer"]))
var shapes: Dictionary = persist.live_shapes()
var om: Array = t5[by_name["Outer"]]["members"]
var shape_ok := false
var obj_ok := false
for m: String in om:
if m.begins_with("g:") and shapes.has(m.substr(2)):
shape_ok = true
if m.begins_with("obj:") and map.object_node(m.substr(4)) != null:
obj_ok = true
_ok("P5 Outer's shape member names a live shape", shape_ok, om)
_ok("P5 Outer's object member names a live object", obj_ok, om)
var im: Array = t5[by_name["Inner"]]["members"]
_ok("P5 Inner's brush object still has its brush", im.size() == 1
and (level.get_group_brush_ids(String(im[0]).substr(2)) as PackedInt32Array).size() == 1, im)