extends SceneTree
## GATE — arriving in a map puts the player ON the ground: no long fall, no fall
## through terrain that has not built its collision yet, no spawning inside rock
## (user 2026-10-06: "our teleporters are not properly situated on the terrain and
## even if they are, the player will fall through").
##
## The real player (player_procedural.tscn, its own camera driving the terrain LOD, as
## in the game) arrives at the map's first spawn point through player.arrive() — the
## one entry every spawn / teleport / respawn path uses. For ARRIVE_SECS it records the
## fastest downward speed; then a ray finds the ground. Passes when the player
## stands on it (feet within STAND_TOL), never fell faster than MAX_FALL_SPEED (a
## free fall of ~3 m), and is not under a surface (buried).
##
## Measured 2026-10-06 (_probe_arrival, collision under the spawn / height above it):
## map_geiss +17.1 m above the terrain
## map_geiss_terraces BURIED 11.8 m inside the rock
## map_mountain +121 m above the terrain
## map_geiss_world placed right, but no collision under it for 4.2 s (1.8 s after
## the dc-terrain dispatch-key fix) — the player falls meanwhile
##
## ⚠ WINDOWED. One map per run (user arg; default map_geiss):
## env -u CI HYPERTPS_DAEMON_TCP=127.0.0.1:1 DISPLAY=:0 "$HYPER_TPS_GODOT" --path . \
## --script res://scenes/test/gate_arrival.gd -- map_geiss_terraces
const ARRIVE_SECS := 20.0
const STAND_TOL := 0.6
## 3 m of free fall at 9.8 m/s² is ~7.7 m/s.
const MAX_FALL_SPEED := 8.0
const MIN_LOOP_FPS := 15
const ProbeWindow := preload("res://scenes/test/probe_window.gd")
var _ws := ""
var _name := ""
var _player: CharacterBody3D
var _spawn := Vector3.ZERO
var _start := 0
var _worst_vy := 0.0
var _worst_vy_t := 0.0
var _last := 0
var _last_frames := 0
var _loop_min := 1 << 30
var _released_t := -1.0
var _released_at := Vector3.ZERO
var _was_arriving := false
func _initialize() -> void:
var a := OS.get_cmdline_user_args()
_name = a[0] if a.size() > 0 else "map_geiss"
_ws = ProbeWindow.free_workspace()
# The PLAYER first, so its camera exists when the map's _ready looks for one (the
# Geiss maps make a fly camera only when there is none).
_player = (load("res://scenes/player/player_procedural.tscn") as PackedScene).instantiate()
root.add_child(_player)
for c in _player.find_children("*", "Camera3D", true, false):
(c as Camera3D).current = true
var m: Node = (load("res://scenes/maps/%s.tscn" % _name) as PackedScene).instantiate()
root.add_child(m)
## Spawn points register a frame after the map enters the tree (scene objects) —
## game.gd's change_map waits one process frame for the same reason.
func _arrive() -> void:
var sp := get_nodes_in_group("spawn_points")
if sp.is_empty():
print("FAIL %s has no spawn point" % _name)
quit(1)
return
_spawn = (sp[0] as Node3D).global_position
if _player.has_method("arrive"):
_player.call("arrive", _spawn)
else:
_player.global_position = _spawn # what every spawn path does today
_player.velocity = Vector3.ZERO
_start = Time.get_ticks_msec()
print(" %s: arriving at %s (arrive() %s)" % [_name, _spawn, "present" if _player.has_method("arrive") else "ABSENT — raw position set"])
func _physics_process(_d: float) -> bool:
if _start == 0:
if Engine.get_physics_frames() >= 2:
_arrive()
return false
if not ProbeWindow.drawing(root):
ProbeWindow.move_to(_ws, Vector2i(1600, 900))
var now := Time.get_ticks_msec()
if _last > 0 and now - _last >= 500:
var frames := Engine.get_process_frames()
_loop_min = mini(_loop_min, int((frames - _last_frames) * 1000.0 / (now - _last)))
_last_frames = frames
_last = now
elif _last == 0:
_last = now
_last_frames = Engine.get_process_frames()
var el := (now - _start) / 1000.0
var arriving: bool = _player.has_method("is_arriving") and bool(_player.call("is_arriving"))
if _was_arriving and not arriving:
_released_t = el
_released_at = _player.global_position
print(" released at %s after %.1f s" % [_released_at.snapped(Vector3.ONE * 0.01), el])
var tr := root.find_children("*", "DCTerrain", true, false)
if not tr.is_empty():
print(" chunk under it then: %s" % str(tr[0].debug_chunk_info(_released_at - Vector3(0, 0.05, 0))))
print(" column then: %s" % _column(_released_at))
_was_arriving = arriving
if _released_t >= 0.0 and _player.velocity.y < -MAX_FALL_SPEED and _worst_vy >= -MAX_FALL_SPEED:
print(" FALLING at t=%.1f from %s (%.1f s after release)" % [el, _player.global_position.snapped(Vector3.ONE * 0.01), el - _released_t])
var ts := root.find_children("*", "DCTerrain", true, false)
if not ts.is_empty():
print(" chunk under the release point now: %s" % str(ts[0].debug_chunk_info(_released_at - Vector3(0, 0.05, 0))))
print(" column now: %s" % _column(_released_at))
if _player.velocity.y < _worst_vy:
_worst_vy = _player.velocity.y
_worst_vy_t = el
if el < ARRIVE_SECS:
return false
_report()
return true
## Every surface in the column under p, top down: "y@rid(nx,ny)" — each hit body is
## excluded in turn, so stacked bodies (a chunk and its still-held parent) all show.
func _column(p: Vector3) -> String:
var space := root.get_world_3d().direct_space_state
var excl: Array[RID] = [_player.get_rid()]
var out := []
for _i in 6:
var q := PhysicsRayQueryParameters3D.create(p + Vector3(0, 30, 0), p - Vector3(0, 60, 0), 0xFFFFFFFF, excl)
q.hit_back_faces = false
var h := space.intersect_ray(q)
if h.is_empty():
break
var rid: RID = h["rid"]
out.append("%.2f@%d(n.y %.2f)" % [(h["position"] as Vector3).y, rid.get_id(), (h["normal"] as Vector3).y])
excl.append(rid)
return ", ".join(out) if not out.is_empty() else "NOTHING"
func _report() -> void:
var space := root.get_world_3d().direct_space_state
var p := _player.global_position
var down := space.intersect_ray(PhysicsRayQueryParameters3D.create(p + Vector3(0, 1.0, 0), p - Vector3(0, 400, 0), 0xFFFFFFFF, [_player.get_rid()]))
var gap := INF
if not down.is_empty():
gap = p.y - (down["position"] as Vector3).y
var upq := PhysicsRayQueryParameters3D.create(p + Vector3(0, 0.2, 0), p + Vector3(0, 300, 0), 0xFFFFFFFF, [_player.get_rid()])
upq.hit_back_faces = true
var up := space.intersect_ray(upq)
var buried := not up.is_empty() and (up["normal"] as Vector3).y > 0.3
print(" ended at %s: %s above the ground below, buried=%s; fastest fall %.1f m/s at t=%.1f s; min loop fps %d" % [
p.snapped(Vector3.ONE * 0.01), ("%.2f m" % gap) if gap != INF else "NO GROUND", buried, -_worst_vy, _worst_vy_t, _loop_min])
var ok := true
if _loop_min < MIN_LOOP_FPS:
print("FAIL main loop fell to %d fps — window hidden? proves nothing" % _loop_min)
ok = false
if buried:
print("FAIL the player is INSIDE the terrain (a surface faces up above them)")
ok = false
if gap == INF or gap > STAND_TOL or gap < -0.05:
print("FAIL not standing on the ground (gap %s, tolerance %.1f m)" % [str(gap), STAND_TOL])
ok = false
if -_worst_vy > MAX_FALL_SPEED:
print("FAIL fell at %.1f m/s (max %.1f) — arrived in the air or before the ground had collision" % [-_worst_vy, MAX_FALL_SPEED])
ok = false
print("PASS" if ok else "FAIL")
quit(0 if ok else 1)