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"))