OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_curve_member.gd

241 lines · gd ref:
extends Node
## GATE — A CURVE IS A COLLECTION MEMBER (#120 §16.2, step 1): "c:<curve dot>", like an
## object or a brush object.
##   M1  select_curve holds the curve and drops every other selection; selecting a shape
##       drops the curve
##   M2  Ctrl+G with a curve selected: the new collection holds "c:<dot>", one gesture
##   M3  Shift+M links it into a second collection, M moves it, Ctrl+Alt+G takes it out
##   M4  Delete Hierarchy deletes a FREE curve only in the subtree; a curve a cable outside
##       still uses survives (§16.7 Q2); one Ctrl+Z brings the free one back
##   M5  ... a cable and its curve both in the subtree both go (no curve left behind)
##   M7  Save as asset: a member curve is in the blob (flagged a member), a placement folds
##       it moved by the frame, Make Real links the new curve into the new collection
##   M8  ... a curve in a SUB-collection keeps its place there through Make Real
##   M6  a flatten keeps the link: the member names a live curve with the same name
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_curve_member.tscn

const CollectionFold := preload("res://scenes/maps/collection_fold.gd")
const AssetDoc := preload("res://scenes/maps/asset_doc.gd")

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


func _tree() -> Dictionary:
	return persist.collection_tree()["cols"]


func _members(col: String) -> Array:
	var t := _tree()
	return t[col]["members"] if t.has(col) else []


func _curves() -> Dictionary:
	return map.curve_states()


func _curve(at: Vector3, name: String) -> String:
	return map.create_curve([CurveMath.point(at), CurveMath.point(at + Vector3(4.37, 0, 0)),
		CurveMath.point(at + Vector3(4.37, 0, 3.11))], false, name)


func _box(at: Vector3) -> String:
	persist.begin_gesture("box")
	var d: String = persist.record("shape.create", {"type": "box", "gv": 1, "params": {},
		"frame": SceneObjects.xform_to(Transform3D(Basis.IDENTITY, at)), "size": [1, 1, 1],
		"material_id": 2, "level_id": 0})
	persist.end_gesture()
	persist.refold()
	return d


func _run() -> void:
	persist = map.get("_persist")
	_ok("setup: journal", persist != null and map.has_method("create_curve"))
	if persist == null:
		return
	var O0 := Vector3(300.137, 1.31, 299.873)
	var f := _curve(O0, "Free")
	var shape := _box(O0 + Vector3(0, 0, 10))
	_ok("setup: a free curve", _curves().has(f), f)

	# ---- M1 ----
	map.select_shape(shape)
	_ok("M1 the map can select a curve", map.has_method("select_curve"))
	if not map.has_method("select_curve"):
		return
	map.select_curve(f)
	_ok("M1 select_curve holds it", map.selected_curve() == f, map.selected_curve())
	_ok("M1 ... and drops the shape and its brushes", map.selected_shape() == ""
		and (map.get("_sel_brushes") as Array).is_empty() and (map.selected_objects() as Array).is_empty())
	map.select_shape(shape)
	_ok("M1 selecting a shape drops the curve", map.selected_curve() == "" and map.selected_shape() == shape)

	# ---- M2 ----
	map.select_curve(f)
	var g0: int = (persist.gestures() as Array).size()
	var a: String = CollectionOps.create_from_selection(map, "A")
	persist.refold()
	_ok("M2 Ctrl+G: the collection holds the curve", _members(a) == ["c:" + f], _members(a))
	_ok("M2 ... as one gesture", (persist.gestures() as Array).size() == g0 + 1)

	# ---- M3 ----
	var b: String = CollectionOps.create_empty(map, "B")
	map.select_curve(f)
	CollectionOps.link_selection(map, b)
	persist.refold()
	_ok("M3 Shift+M: in A and in B", _members(a).has("c:" + f) and _members(b).has("c:" + f))
	var c: String = CollectionOps.create_empty(map, "C")
	map.select_curve(f)
	CollectionOps.move_selection(map, c)
	persist.refold()
	_ok("M3 M: only in C", _members(c) == ["c:" + f] and not _members(a).has("c:" + f)
		and not _members(b).has("c:" + f), [_members(a), _members(b), _members(c)])
	map.select_curve(f)
	CollectionOps.unlink_selection(map, c)
	persist.refold()
	_ok("M3 Ctrl+Alt+G: in none", CollectionFold.collections_of(persist.collection_tree(), "c:" + f).is_empty())

	# ---- M4 ----
	var cable: String = map.add_cable([CurveMath.point(O0 + Vector3(0, 0, -8)), CurveMath.point(O0 + Vector3(6.21, 0, -8))],
		false, {"radius": 0.05})
	var u := String(persist.shape_state(cable)["params"]["curve"])
	persist.begin_gesture("D")
	var d: String = persist.record("col.create", {"name": "D", "parent": ""})
	persist.record("col.link", {"target": d, "members": ["c:" + f, "c:" + u]})
	persist.end_gesture()
	persist.refold()
	CollectionOps.remove(map, d, true)
	persist.refold()
	_ok("M4 Delete Hierarchy: the free curve is gone", not _curves().has(f))
	_ok("M4 ... the cable's curve (cable outside) survives", _curves().has(u) and (persist.live_shapes() as Dictionary).has(cable))
	persist.undo_last()
	persist.refold()
	_ok("M4 one Ctrl+Z: the free curve and D are back", _curves().has(f) and _tree().has(d))

	# ---- M5 ----
	persist.begin_gesture("E")
	var e: String = persist.record("col.create", {"name": "E", "parent": ""})
	persist.record("col.link", {"target": e, "members": ["g:" + cable, "c:" + u]})
	persist.end_gesture()
	persist.refold()
	CollectionOps.remove(map, e, true)
	persist.refold()
	_ok("M5 the cable and its curve both go", not _curves().has(u) and not (persist.live_shapes() as Dictionary).has(cable))
	persist.undo_last()
	persist.refold()
	_ok("M5 ... and come back on Ctrl+Z", _curves().has(u) and (persist.live_shapes() as Dictionary).has(cable))

	# ---- M7 / M8 ----
	var f3 := _curve(O0 + Vector3(0, 3, 0), "Sub curve")
	var kbox := _box(O0 + Vector3(2, 0, 2))
	persist.begin_gesture("Kit")
	var kit: String = persist.record("col.create", {"name": "Kit", "parent": ""})
	var sub: String = persist.record("col.create", {"name": "Sub", "parent": kit})
	persist.record("col.link", {"target": kit, "members": ["c:" + f, "g:" + kbox]})
	persist.record("col.link", {"target": sub, "members": ["c:" + f3]})
	persist.end_gesture()
	persist.refold()
	var w: Dictionary = AssetDoc.write(persist, kit)
	var doc: Dictionary = w["doc"]
	var crecs: Array = (doc.get("records", []) as Array).filter(func(r: Dictionary) -> bool: return String(r["kind"]) == "curve.create")
	var members_flag: Array = crecs.filter(func(r: Dictionary) -> bool: return bool(r.get("member", false)))
	_ok("M7 the blob carries both member curves", crecs.size() == 2, crecs.size())
	_ok("M7 ... the root's one flagged a member, the sub's not", members_flag.size() == 1
		and String(members_flag[0]["name"]) == "Free", members_flag.map(func(r: Dictionary) -> String: return String(r["name"])))
	# The free curve reaches x + 4.37 and z + 3.11; the only box sits in [2, 3] — so only a
	# bounds that counts member curves reaches past x + 3.
	var bhi: Vector3 = (doc.get("bounds", [Vector3.ZERO, Vector3.ZERO]) as Array)[1]
	_ok("M7 ... the bounds take in the member curves", bhi.x >= O0.x + 4.37 - 1.0e-3, bhi - O0)
	var subrec: Array = (doc.get("records", []) as Array).filter(func(r: Dictionary) -> bool:
		return String(r["kind"]) == "col.create" and String(r["name"]) == "Sub")
	_ok("M8 ... the sub-collection lists its curve as c:<local>", subrec.size() == 1
		and (subrec[0]["members"] as Array).any(func(m: String) -> bool: return m.begins_with("c:L:")),
		subrec[0]["members"] if subrec.size() == 1 else subrec)
	var lib: Node = map.asset_library()
	lib.save_collection(kit, "CurveKit")
	var off := Vector3(50, 0, 0)
	var pivot: Vector3 = doc["pivot"]
	var pl: String = lib.place("CurveKit", Transform3D(Basis.IDENTITY, pivot + off))
	persist.refold()
	# A placement's curves are FOLD-INTERNAL (refold hands them to ShapeFold, never to
	# curve_states()), so its synthetic records are read from AssetFold.payloads — the one
	# builder the fold and Make Real share.
	var pst: Dictionary = (persist.placements() as Dictionary).get(pl, {})
	var pc: Array = []
	if not pst.is_empty():
		pc = (LevelPersistence.AssetFold.payloads(pst, doc, func(l: String) -> String: return "X" + l) as Array) \
			.filter(func(e: Dictionary) -> bool: return String(e["payload"]["kind"]) == "curve.create")
	var free_p: Array = pc.filter(func(e: Dictionary) -> bool: return String(e["payload"].get("name", "")) == "Free")
	_ok("M7 a placement folds both curves", pc.size() == 2, pc.size())
	_ok("M7 ... moved by the frame", free_p.size() == 1
		and Vector3(free_p[0]["payload"]["points"][0]["p"][0], free_p[0]["payload"]["points"][0]["p"][1],
			free_p[0]["payload"]["points"][0]["p"][2]).is_equal_approx(O0 + off))
	_ok("M7 ... with the blob's member flag left out of the record", pc.all(func(e: Dictionary) -> bool:
		return not (e["payload"] as Dictionary).has("member")))
	map.make_real_asset(pl)
	persist.refold()
	var real_col := ""
	for cd2: String in _tree():
		if String(_tree()[cd2]["name"]) == "CurveKit":
			real_col = cd2
	var real_c: Array = _members(real_col).filter(func(m: String) -> bool: return m.begins_with("c:"))
	_ok("M7 Make Real: the new collection holds one curve", real_c.size() == 1, _members(real_col))
	_ok("M7 ... a live curve named Free", real_c.size() == 1 and _curves().has(String(real_c[0]).substr(2))
		and String(_curves()[String(real_c[0]).substr(2)]["name"]) == "Free")
	var real_sub := ""
	for cd3: String in _tree():
		if String(_tree()[cd3]["name"]) == "Sub" and String(_tree()[cd3]["parent"]) == real_col and real_col != "":
			real_sub = cd3
	var sub_c: Array = _members(real_sub).filter(func(m: String) -> bool: return m.begins_with("c:"))
	_ok("M8 Make Real: the copied Sub holds the sub curve", sub_c.size() == 1 and _curves().has(String(sub_c[0]).substr(2))
		and String(_curves()[String(sub_c[0]).substr(2)]["name"]) == "Sub curve", _members(real_sub))

	# ---- M6 (last: a flatten re-mints every dot) ----
	persist.begin_gesture("F")
	var fc: String = persist.record("col.create", {"name": "Flat", "parent": ""})
	persist.record("col.link", {"target": fc, "members": ["c:" + f]})
	persist.end_gesture()
	persist.refold()
	var res: Dictionary = persist.flatten_map()
	persist.refold()
	_ok("M6 flatten ran", not res.has("error"), res.get("error", ""))
	var flat := ""
	for cd4: String in _tree():
		if String(_tree()[cd4]["name"]) == "Flat":
			flat = cd4
	var fm: Array = _members(flat).filter(func(m: String) -> bool: return m.begins_with("c:"))
	_ok("M6 the flattened collection names a LIVE curve", fm.size() == 1 and _curves().has(String(fm[0]).substr(2)), _members(flat))
	_ok("M6 ... the same one (its name)", fm.size() == 1 and _curves().has(String(fm[0]).substr(2))
		and String(_curves()[String(fm[0]).substr(2)]["name"]) == "Free")