extends SceneTree
## GATE — every teleporter stands ON the ground once the map has streamed (user
## 2026-10-06: "our teleporters are not properly situated on the terrain").
##
## Measured 2026-10-06 (_probe_arrival): map_mountain's return pad 117 m in the air,
## map_geiss's 12.7 m, map_geiss_terraces' 11.8 m INSIDE the rock. A map loads with a
## camera at its first spawn point (as a player would arrive), streams SETTLE_S, then a
## ray down from each teleporter must find ground within STAND_TOL below its origin
## (the pad's base), and it must not be under a surface.
##
## ⚠ WINDOWED, one map per run:
## env -u CI HYPERTPS_DAEMON_TCP=127.0.0.1:1 DISPLAY=:0 "$HYPER_TPS_GODOT" --path . \
## --script res://scenes/test/gate_teleporter_ground.gd -- map_mountain
const SETTLE_S := 20.0
const STAND_TOL := 0.3
const ProbeWindow := preload("res://scenes/test/probe_window.gd")
const _Arrival := preload("res://scenes/player/arrival_ground.gd")
var _ws := ""
var _name := ""
var _map: Node
var _cam: Camera3D
var _start := 0
func _initialize() -> void:
var a := OS.get_cmdline_user_args()
_name = a[0] if a.size() > 0 else "map_mountain"
_ws = ProbeWindow.free_workspace()
_cam = Camera3D.new()
_cam.far = 20000.0
root.add_child(_cam)
_cam.current = true
_map = (load("res://scenes/maps/%s.tscn" % _name) as PackedScene).instantiate()
root.add_child(_map)
func _physics_process(_d: float) -> bool:
if not ProbeWindow.drawing(root):
ProbeWindow.move_to(_ws, Vector2i(1600, 900))
if _start == 0:
if Engine.get_physics_frames() < 2:
return false
var sp := get_nodes_in_group("spawn_points")
var at: Vector3 = (sp[0] as Node3D).global_position if not sp.is_empty() else Vector3.ZERO
_cam.look_at_from_position(at + Vector3(0, 1.7, 0), at + Vector3(10, 1.7, 0.01))
_start = Time.get_ticks_msec()
return false
if (Time.get_ticks_msec() - _start) / 1000.0 < SETTLE_S:
return false
var space := root.get_world_3d().direct_space_state
var bad := 0
var n := 0
for t in _map.find_children("*", "Area3D", true, false):
if not ("target_map" in t):
continue
n += 1
var p: Vector3 = (t as Node3D).global_position
var dq := PhysicsRayQueryParameters3D.create(p + Vector3(0, 0.5, 0), p - Vector3(0, 400, 0))
dq.hit_back_faces = false
var down := space.intersect_ray(dq)
var aq := PhysicsRayQueryParameters3D.create(p + Vector3(0, 300, 0), p + Vector3(0, 0.5, 0))
aq.hit_back_faces = false
var above := space.intersect_ray(aq)
var gap: float = p.y - (down["position"] as Vector3).y if not down.is_empty() else INF
var buried := down.is_empty() and not above.is_empty()
var ok := not buried and gap >= -0.05 and gap <= STAND_TOL
if not ok:
bad += 1
print(" %s %s at %s: %s" % ["ok " if ok else "FAIL", t.name, p.snapped(Vector3.ONE * 0.01),
"BURIED (surface above at %.1f)" % (above["position"] as Vector3).y if buried
else ("%.2f m above the ground" % gap if gap != INF else "NO GROUND within 400 m")])
var pass_ := n > 0 and bad == 0
print("GATE teleporter_ground %s: %s (%d teleporters, %d off the ground)" % [_name, "PASS" if pass_ else "FAIL", n, bad])
quit(0 if pass_ else 1)
return true