extends Node
## GATE — BRUSH OBJECTS: regroup, join, separate (#120 F1, docs/node-tree-and-stacks.md
## §2.1). Needs dc-terrain with Kind::BrushRegroup (brush.regroup, DCBrush::base_group).
## R1 Ctrl+G with a LOOSE brush: it becomes an object of its own (group = its key) and
## the new collection holds it; one gesture
## R2 a split AFTER that keeps both halves in the object, so in the collection
## R3 undo the split, then Ctrl+Z the Ctrl+G: the brush is loose again, no collection
## R4 Ctrl+J joins two loose brushes into the ACTIVE one's object
## R5 P separates one again: a new object, still in the collections the old one was in
## R6 join / separate refuse a shape's pieces, writing nothing
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_brush_regroup.tscn
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 < 16:
print(" **FAIL** only %d checks ran -- setup died before the assertions" % checks)
fails += 1
print("\nGATE brush_regroup: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)
func _group(key: String) -> String:
var id := int(level.brush_by_key(key))
return String(level.get_brush_group(id)) if id > 0 else "<none>"
func _loose_keys() -> Array:
var out: Array = []
for b: int in (level.get_live_brush_ids() as PackedInt32Array):
if String(level.get_brush_group(b)) == "" and not bool((level.get_brush(b) as Dictionary).get("clearance", false)):
out.append(String(level.get_brush_key(b)))
out.sort()
return out
func _run() -> void:
persist = map.get("_persist")
level = map.get("level")
persist.refold()
var loose := _loose_keys()
_ok("setup: three loose brushes", loose.size() >= 3, loose)
if loose.size() < 3:
return
var k0 := String(loose[0])
# ---- R1 ----
map.select_brushes(PackedInt32Array([int(level.brush_by_key(k0))]))
var n0: int = (persist.gestures() as Array).size()
var col: String = CollectionOps.create_from_selection(map, "Walls")
persist.refold()
_ok("R1 the loose brush is an object of its own (group = its key)", _group(k0) == k0, _group(k0))
var t: Dictionary = persist.collection_tree()["cols"]
_ok("R1 the collection holds it", t.has(col) and t[col]["members"] == ["g:" + k0], t.get(col, {}))
_ok("R1 ... as one gesture", (persist.gestures() as Array).size() == n0 + 1)
# ---- R2 ----
var id0 := int(level.brush_by_key(k0))
map.select_brushes(PackedInt32Array([id0]))
var c: Vector3 = map.selection_centre()
persist.begin_gesture("Cut")
var split_dot: String = persist.record("brush.split", {"brush_key": k0, "n": [1, 0, 0], "d": c.x})
persist.end_gesture()
persist.refold()
_ok("R2 the split's back half is in the object too", _group(split_dot) == k0, _group(split_dot))
_ok("R2 ... so the object has two brushes", (level.get_group_brush_ids(k0) as PackedInt32Array).size() == 2,
(level.get_group_brush_ids(k0) as PackedInt32Array).size())
# ---- R3 ----
persist.undo_last() # the split
persist.undo_last() # the Ctrl+G
persist.refold()
_ok("R3 Ctrl+Z: the brush is loose again", _group(k0) == "", _group(k0))
_ok("R3 ... and the collection is gone", not (persist.collection_tree()["cols"] as Dictionary).has(col))
# ---- R4 ----
var k1 := String(loose[1])
var k2 := String(loose[2])
map.select_brushes(PackedInt32Array([int(level.brush_by_key(k1)), int(level.brush_by_key(k2))]))
_ok("setup: the active brush is the last one picked", String(level.get_brush_key(int(map.get("_sel_brush")))) == k2)
var moved := CollectionOps.join(map)
persist.refold()
_ok("R4 Ctrl+J: both brushes are in the active one's object", _group(k1) == k2 and _group(k2) == k2,
"%s %s (%d moved)" % [_group(k1), _group(k2), moved])
# ---- R5 ----
map.select_brushes(PackedInt32Array([int(level.brush_by_key(k2))]))
var col2: String = CollectionOps.create_from_selection(map, "Joined") # the object is in a collection
persist.refold()
map.select_brushes(PackedInt32Array([int(level.brush_by_key(k1))]))
var ng := CollectionOps.separate(map)
persist.refold()
_ok("R5 P: the brush is a new object", ng != "" and _group(k1) == ng and _group(k2) == k2,
"%s %s %s" % [ng, _group(k1), _group(k2)])
var m2: Array = persist.collection_tree()["cols"][col2]["members"]
_ok("R5 ... still in the collection its source was in", m2.has("g:" + ng) and m2.has("g:" + k2), m2)
# ---- R6 ----
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()
var piece: int = (level.get_group_brush_ids(shape) as PackedInt32Array)[0]
map.select_brushes(PackedInt32Array([int(level.brush_by_key(k1)), piece]))
var g6: int = (persist.gestures() as Array).size()
_ok("R6 join refuses a shape's piece", CollectionOps.join(map) == -1)
_ok("R6 separate refuses it too", CollectionOps.separate(map) == "")
_ok("R6 ... and nothing was written", (persist.gestures() as Array).size() == g6)
_ok("R6 the shape is still its own object", _group(String(level.get_brush_key(piece))) == shape)