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)