OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_map_flat.gd

113 lines · gd ref:
extends SceneTree
## Gate: map_flat.tscn declares exactly the nodes map_flat.gd reaches for.
## instantiate() does NOT run _ready (that needs the tree), so this validates the
## SCENE CONTRACT without needing autoloads.

func _initialize() -> void:
	var ok := true
	var ps := load("res://scenes/maps/map_flat.tscn") as PackedScene
	if ps == null:
		print("FAIL: map_flat.tscn did not load"); quit(1); return
	var m := ps.instantiate()

	var surf := m.get_node_or_null(^"Ground/Surface")
	var coll := m.get_node_or_null(^"Ground/Collision")
	var ground := m.get_node_or_null(^"Ground")

	if surf == null or not (surf is MeshInstance3D) or not (surf.mesh is PlaneMesh):
		print("FAIL: Ground/Surface missing or not a MeshInstance3D+PlaneMesh"); ok = false
	else:
		print("ok   Ground/Surface PlaneMesh size=%s" % str((surf.mesh as PlaneMesh).size))
	if coll == null or not (coll is CollisionShape3D) or not (coll.shape is BoxShape3D):
		print("FAIL: Ground/Collision missing or not a CollisionShape3D+BoxShape3D"); ok = false
	else:
		print("ok   Ground/Collision BoxShape3D size=%s" % str((coll.shape as BoxShape3D).size))
	if ground == null or not (ground is StaticBody3D):
		print("FAIL: Ground missing or not StaticBody3D"); ok = false
	else:
		print("ok   Ground collision_layer=%d (expect bit1+bit2 = 3)" % ground.collision_layer)

	var spawns := 0
	for c in m.get_children():
		if c is Marker3D and c.is_in_group("spawn_points"):
			spawns += 1
	if spawns < 2:
		print("FAIL: need >=2 spawn_points, found %d" % spawns); ok = false
	else:
		print("ok   spawn_points=%d" % spawns)


	# _apply_extent() is the only logic in map_flat.gd. Call it directly: it
	# touches nodes only, so it needs no autoloads and no _ready.
	m.extent = 512.0
	m.slab_thickness = 4.0
	m._apply_extent()
	var ps2 := (surf.mesh as PlaneMesh).size
	var bs2 := (coll.shape as BoxShape3D).size
	if ps2 != Vector2(512.0, 512.0):
		print("FAIL: extent did not resize mesh, got %s" % str(ps2)); ok = false
	else:
		print("ok   _apply_extent mesh -> %s" % str(ps2))
	if bs2 != Vector3(512.0, 4.0, 512.0):
		print("FAIL: extent did not resize slab, got %s" % str(bs2)); ok = false
	else:
		print("ok   _apply_extent slab -> %s" % str(bs2))
	# Surface must end up flush with y=0, i.e. slab hangs entirely below it.
	if not is_equal_approx(coll.position.y, -2.0):
		print("FAIL: slab top not flush with y=0, collision y=%f" % coll.position.y); ok = false
	else:
		print("ok   slab top flush with y=0 (collision y=%.1f)" % coll.position.y)

	# The link from the main map. A teleporter whose target_map does not resolve
	# is a dead pad that fails silently at walk-through time, which is the worst
	# place to find it.
	var tps := load("res://scenes/maps/map_test.tscn") as PackedScene
	if tps == null:
		print("FAIL: map_test.tscn did not load"); ok = false
	else:
		var mt := tps.instantiate()
		var tp := mt.get_node_or_null(^"TeleporterToFlat")
		if tp == null:
			print("FAIL: map_test has no TeleporterToFlat"); ok = false
		else:
			if not ResourceLoader.exists(tp.target_map):
				print("FAIL: TeleporterToFlat target_map does not exist: %s" % tp.target_map); ok = false
			else:
				print("ok   TeleporterToFlat -> %s" % tp.target_map)
			# Must arrive ABOVE the plane surface (y=0), not inside the slab.
			if tp.destination.y <= 0.0:
				print("FAIL: destination y=%f is not above the surface" % tp.destination.y); ok = false
			else:
				print("ok   TeleporterToFlat destination=%s" % str(tp.destination))
		mt.free()

	# ⛔ THE MAP MUST *NOT* OWN A WorldEnvironment -- that detaches game.gd's Sky3D
	# (game.gd:637) and the Atmosphere sliders, bound to it once at setup, then
	# drive a node that is out of the tree. Verified from a real session backtrace.
	for c in m.get_children():
		if c is WorldEnvironment:
			print("FAIL: map owns a WorldEnvironment -- this kills the sky sliders on this map"); ok = false
	print("ok   no WorldEnvironment (sliders keep driving game.gd's attached Sky3D)")

	# Return pad, and it must not sit on the arrival point or it re-fires instantly.
	var home := m.get_node_or_null(^"TeleporterToHome")
	if home == null:
		print("FAIL: map_flat has no TeleporterToHome"); ok = false
	else:
		if not ResourceLoader.exists(home.target_map):
			print("FAIL: TeleporterToHome target missing: %s" % home.target_map); ok = false
		else:
			print("ok   TeleporterToHome -> %s" % home.target_map)
		# `position`, NOT `global_position`: out of tree the global transform is not
		# resolved and reads (0,0,0), which made this gate report 2.0m for a pad
		# authored at (10,0,0). The instrument was wrong, not the scene.
		var arrival := Vector3(0, 2, 0)   # where TeleporterToFlat lands us
		var d: float = home.position.distance_to(arrival)
		if d < 5.0:
			print("FAIL: return pad %.1fm from arrival -- will re-fire on arrival" % d); ok = false
		else:
			print("ok   return pad %.1fm clear of arrival (pos=%s)" % [d, str(home.position)])

	print("GATE map_flat: %s" % ("PASS" if ok else "FAIL"))
	quit(0 if ok else 1)