OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_scene_objects_base.gd

123 lines · gd ref:
extends Node
## GATE — a level map's TELEPORTERS and SPAWN POINTS are editable objects (2026-10-05:
## "include teleporters and spawns too").
##
## They were plain scene nodes: no journal identity, so nothing could select, move,
## edit or delete them. Now SceneObjects ADOPTS them as BASE objects ("scene:<Name>"):
## present without an object.create, reset to their authored transform and props on
## every fold, and targetable by object.transform / set_prop / remove like any object.
## A removed base object is DETACHED (not freed), so Ctrl+Z puts the same node back.
##
##   1. every teleporter and spawn point is an object "scene:<Name>"; none left as a
##      direct child of the map
##   2. a teleporter keeps its AUTHORED label (the scene's, not the catalogue default)
##   3. object.transform moves a spawn point; Ctrl+Z puts it back where the scene had it
##   4. set_prop relabels a teleporter; Ctrl+Z returns the AUTHORED label
##   5. Delete detaches a teleporter (out of the tree) and a spawn point (out of the
##      spawn_points group); Ctrl+Z brings both back
##   6. a spawn_point prefab exists, so new ones can be placed
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_scene_objects_base.tscn

var fails := 0
var checks := 0


func _ok(what: String, cond: bool, note: String = "") -> void:
	checks += 1
	if not cond:
		fails += 1
	print("  %-64s %s  %s" % [what, "OK" if cond else "**FAIL**", 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"
	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
	_run(map)
	if checks < 14:
		print("  **FAIL** only %d checks ran -- setup died before the assertions" % checks)
		fails += 1
	print("\nGATE scene_objects_base: %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 objs: Node = map.get_node_or_null("SceneObjects")
	var persist: Node = map.get("_persist")
	_ok("map_test has SceneObjects and a journal", objs != null and persist != null)
	if objs == null or persist == null:
		return
	var dots: Array = (objs.entries() as Array).map(func(e: Dictionary) -> String: return String(e["dot"]))
	var tele: Array = dots.filter(func(d: String) -> bool: return d.begins_with("scene:Teleporter"))
	var spawn: Array = dots.filter(func(d: String) -> bool: return d.begins_with("scene:SpawnPoint"))
	# Counted from a fresh, unadopted instance of the scene — not assumed.
	var src: Node = (load("res://scenes/maps/map_dev_test.tscn") as PackedScene).instantiate()
	var want_t := 0
	var want_s := 0
	for c in src.get_children():
		if c is Node3D and "target_map" in c:
			want_t += 1
		elif c.is_in_group("spawn_points"):
			want_s += 1
	src.free()
	_ok("every teleporter is an object scene:Teleporter* (%d)" % want_t, want_t > 0 and tele.size() == want_t, "%d" % tele.size())
	_ok("every spawn point is an object scene:SpawnPoint* (%d)" % want_s, want_s > 0 and spawn.size() == want_s, "%d" % spawn.size())
	var left: Array = []
	for c in map.get_children():
		if (c is Node3D and "target_map" in c) or c.is_in_group("spawn_points"):
			left.append(c.name)
	_ok("none left as direct children of the map", left.is_empty(), str(left))

	# 2. the authored label (map_dev_test.tscn sets label_text on TeleporterToFlat)
	var tf := "scene:TeleporterToFlat"
	var tnode: Node = objs.node_for(tf)
	var authored := String(tnode.get("label_text")) if tnode != null else ""
	_ok("a teleporter keeps its AUTHORED label", authored != "" and String(objs.props(tf).get("label", "")) == authored,
		"'%s'" % authored)

	# 3. move a spawn point, then undo
	var sp := "scene:SpawnPoint1"
	var snode := objs.node_for(sp) as Node3D
	var base_pos: Vector3 = snode.global_position if snode != null else Vector3.INF
	map.move_object(sp, Transform3D(Basis.IDENTITY, base_pos + Vector3(0, 0, 7)))
	persist.refold()
	snode = objs.node_for(sp) as Node3D
	_ok("object.transform moves a spawn point", snode != null and snode.global_position.distance_to(base_pos + Vector3(0, 0, 7)) < 0.01,
		str(snode.global_position if snode != null else "?"))
	persist.undo_last()
	persist.refold()
	snode = objs.node_for(sp) as Node3D
	_ok("Ctrl+Z puts it back where the scene had it", snode != null and snode.global_position.distance_to(base_pos) < 0.01)

	# 4. relabel, then undo -> the AUTHORED label, not the catalogue's ""
	map.set_object_props(tf, {"label": "Gate Label"})
	persist.refold()
	_ok("set_prop relabels the teleporter", String(objs.node_for(tf).get("label_text")) == "Gate Label")
	persist.undo_last()
	persist.refold()
	_ok("Ctrl+Z returns the authored label", String(objs.node_for(tf).get("label_text")) == authored,
		"'%s'" % String(objs.node_for(tf).get("label_text")))

	# 5. delete a teleporter and a spawn point (one gesture), then undo
	var tref: Node = objs.node_for(tf)
	var sref: Node = objs.node_for("scene:SpawnPoint2")
	map.delete_objects([tf, "scene:SpawnPoint2"])
	persist.refold()
	_ok("Delete detaches the teleporter (out of the tree)", tref != null and is_instance_valid(tref) and not tref.is_inside_tree())
	_ok("and the spawn point leaves the spawn_points group", sref != null and not get_tree().get_nodes_in_group("spawn_points").has(sref))
	_ok("neither is an object any more", objs.node_for(tf) == null and objs.node_for("scene:SpawnPoint2") == null)
	persist.undo_last()
	persist.refold()
	_ok("Ctrl+Z brings back the SAME teleporter node", objs.node_for(tf) == tref and tref.is_inside_tree())
	_ok("and the spawn point is a spawn point again", get_tree().get_nodes_in_group("spawn_points").has(sref))

	# 6. new spawn points can be placed
	_ok("a spawn_point prefab exists", PrefabCatalogue.PREFABS.has("spawn_point"))