OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_duplicate_structure.gd

189 lines · gd ref:
extends Node
## GATE — SHIFT+D KEEPS STRUCTURE (#120 F1, outliner audit bug 2: a copy lost its group,
## its parent and its collections).
##   D1  two brushes of one object copy into ONE new object (not loose, not the source's)
##   D2  ... and the new object is in the source object's collections
##   D3  a shape's copy is in the source shape's collections
##   D4  an object's copy keeps its parent
##   D5  a parent and its child copied together: the child's copy hangs under the
##       PARENT's copy
##   D6  an object's copy is in the source's collections
##   D7  one Ctrl+Z takes a duplicate back, links and all
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_duplicate_structure.tscn

var fails := 0
var checks := 0
var map: Node3D
var persist: Node
var level: Node
var stack: 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 < 10:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE duplicate_structure: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
	get_tree().quit(0 if fails == 0 else 1)


func _dup() -> void:
	map.duplicate_selection()
	stack.cancel()                     # leave the copy where it is
	persist.refold()
	await get_tree().process_frame


func _in(member: String) -> Array:
	return CollectionOps.CollectionFold.collections_of(persist.collection_tree(), member)


func _run() -> void:
	persist = map.get("_persist")
	level = map.get("level")
	stack = map.call("_ensure_ops")
	persist.refold()
	var loose: Array = []
	for b: int in (level.get_live_brush_ids() as PackedInt32Array):
		if String(level.get_brush_group(b)) == "":
			loose.append(b)
	_ok("setup: two loose brushes", loose.size() >= 2)
	if loose.size() < 2:
		return
	# One object of two brushes (Ctrl+J), in collection W.
	map.select_brushes(PackedInt32Array([int(loose[0]), int(loose[1])]))
	CollectionOps.join(map)
	persist.refold()
	var g := String(level.get_brush_group(int(loose[1])))
	map.select_brushes(PackedInt32Array([int(loose[0])]))
	var w := CollectionOps.create_from_selection(map, "W")
	persist.refold()

	# ---- D1 / D2 ----
	map.select_brushes(PackedInt32Array([int(loose[0]), int(loose[1])]))
	await _dup()
	var copies: Array = map.get("_sel_brushes")
	var cg := String(level.get_brush_group(int(copies[0]))) if not copies.is_empty() else ""
	_ok("D1 the copies form ONE new object", copies.size() == 2 and cg != "" and cg != g
		and String(level.get_brush_group(int(copies[1]))) == cg, "%s vs %s" % [cg, g])
	_ok("D2 ... in the source's collection", _in("g:" + cg) == [w], _in("g:" + cg))
	persist.undo_last()
	persist.refold()
	_ok("D7 one Ctrl+Z takes the copies and the link back", _in("g:" + cg).is_empty()
		and (level.get_group_brush_ids(cg) as PackedInt32Array).is_empty())

	# ---- D3 ----
	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()
	map.select_shape(shape)
	CollectionOps.link_selection(map, w)
	persist.refold()
	map.select_shape(shape)
	await _dup()
	var sc := String(map.selected_shape())
	_ok("D3 the shape's copy is in W too", sc != "" and sc != shape and _in("g:" + sc) == [w], [sc, _in("g:" + sc)])

	# ---- D4 / D5 / D6 ----
	var parent: String = map.create_object("marker", Transform3D(Basis(), Vector3(30, 1, 0)))
	var child: String = map.create_object("marker", Transform3D(Basis(), Vector3(31, 1, 0)))
	map.parent_objects([child], parent)
	persist.begin_gesture("Link")
	persist.record("col.link", {"target": w, "members": ["obj:" + child]})
	persist.end_gesture()
	persist.refold()
	map.select_object(child)
	await _dup()
	var cc := String((map.selected_objects() as Array)[-1])
	_ok("D4 the child's copy keeps its parent", cc != child and map.object_parent(cc) == parent, map.object_parent(cc))
	_ok("D6 ... and is in the source's collection", _in("obj:" + cc) == [w], _in("obj:" + cc))
	_ok("D4 ... where the child is", (map.object_node(cc) as Node3D).global_position.is_equal_approx(
		(map.object_node(child) as Node3D).global_position))
	map.select_objects([parent, child], child)
	await _dup()
	var made: Array = map.selected_objects()
	var pc := ""                       # the parent's copy: top level
	var kc := ""                       # the child's copy
	for d: Variant in made:
		if map.object_parent(String(d)) == "":
			pc = String(d)
		else:
			kc = String(d)
	_ok("D5 both copied", made.size() == 2 and pc != "" and kc != "", made)
	_ok("D5 the child's copy hangs under the parent's copy", map.object_parent(kc) == pc, [map.object_parent(kc), pc])

	# ---- D8 / D9: a shape selected as its PIECES (user 2026-10-17: "shift d seems to just
	# move" — duplicating a staircase copied nothing and started no Move) ----
	var st8: String = persist.record("shape.create", {"type": "stairs", "gv": 1, "params": {},
		"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, Vector3(60, 0, 60))),
		"size": [3.0, 2.0, 1.5], "material_id": 2, "level_id": 0})
	persist.refold()
	var stairs_before := _count_type("stairs")
	map.select_brushes(level.get_group_brush_ids(st8))
	await _key(KEY_D, true)
	_ok("D8 Shift+D on a staircase's pieces: a new staircase", _count_type("stairs") == stairs_before + 1,
		[stairs_before, _count_type("stairs")])
	_ok("D8 ... and Move picks it up", stack.get("active") != null, stack.get("active"))
	stack.cancel()
	persist.refold()
	var plain := -1
	for b: int in (level.get_live_brush_ids() as PackedInt32Array):
		if map.shape_of_brush(b) == "":
			plain = b
			break
	var ids9 := PackedInt32Array(level.get_group_brush_ids(st8))
	ids9.append(plain)
	var n9: int = (level.get_live_brush_ids() as PackedInt32Array).size()
	var s9 := _count_type("stairs")
	map.select_brushes(ids9)
	await _key(KEY_D, true)
	stack.cancel()
	persist.refold()
	_ok("D9 stairs pieces + a plain brush: the staircase AND the brush copied",
		_count_type("stairs") == s9 + 1 and (level.get_live_brush_ids() as PackedInt32Array).size() == n9 + 1 + 11,
		[_count_type("stairs") - s9, (level.get_live_brush_ids() as PackedInt32Array).size() - n9])


func _count_type(t: String) -> int:
	var n := 0
	for d: String in (persist.live_shapes() as Dictionary):
		if String(persist.shape_state(d)["type"]) == t:
			n += 1
	return n


## A real key press (Input, not a call): the path the player's Shift+D takes.
func _key(k: Key, shift := false) -> void:
	var e := InputEventKey.new()
	e.keycode = k
	e.physical_keycode = k
	e.shift_pressed = shift
	e.pressed = true
	Input.parse_input_event(e)
	await get_tree().process_frame
	var u := e.duplicate()
	u.pressed = false
	Input.parse_input_event(u)
	await get_tree().process_frame