class_name ArrivalGround
extends RefCounted
## WHERE TO STAND when arriving at a point, and WHETHER that ground is real yet.
##
## Every way into a map — first spawn, teleport, R / fall respawn, a restored saved
## position — used to set the body's position and let gravity run. Measured
## 2026-10-06 (scenes/test/_probe_arrival.gd): spawn points 17 m (map_geiss) and 121 m
## (map_mountain) above the terrain, BURIED 11.8 m inside it (map_geiss_terraces), and
## on map_geiss_world, placed correctly, no collision under the point for 4.2 s — the
## player fell through terrain that had not built its collision yet.
##
## PHYSICS RAYS, not the field: the GPU-only generators (the Geiss maps, the mountain)
## answer every CPU density query with air, so the built collision is the only ground
## there is. That is also exactly what the player will stand on.
##
## resolve() returns {"ready": bool, "found": bool, "ground": Vector3, "why": String}:
## - the first surface BELOW (within MAX_DROP) is the ground when the arrival snaps
## down; an airborne saved position (snap_down = false) is kept where it is;
## - nothing below but an up-facing surface ABOVE, and the field at the point is
## SOLID: the point is inside rock -> the surface above is the ground. (Measured from ABOVE, front faces only: the terrain's
## trimesh is not hit from inside — a ray up out of map_geiss_terraces' rock
## found nothing.)
## - ground that is DCTerrain collision (a server body with no node) is ready only
## once the chunk under it is at the terrain's finest LOD: collision built from a
## coarse chunk is replaced by the fine one, and the surface moves by up to a
## coarse voxel when it is. "found" with "ready" false = a candidate to move the
## hold to, so the camera (and the LOD it drives) gets there.
const MAX_DROP := 400.0
const MAX_RISE := 300.0
const UP_FACE := 0.3
static func resolve(space: PhysicsDirectSpaceState3D, p: Vector3, snap_down: bool,
exclude: Array[RID], terrain: Node) -> Dictionary:
var downq := PhysicsRayQueryParameters3D.create(p + Vector3(0, 0.5, 0), p - Vector3(0, MAX_DROP, 0), 0xFFFFFFFF, exclude)
# FRONT faces only, and only ones that face UP: ray queries default to hitting
# back faces, and the player's body passes through those. Released onto one,
# map_mountain's player sank 3 m into an unchanged, collided LOD-0 chunk and fell.
downq.hit_back_faces = false
var down := space.intersect_ray(downq)
if not down.is_empty() and (down["normal"] as Vector3).y > UP_FACE:
if not snap_down:
return {"ready": true, "found": true, "ground": p, "why": "kept (not snapped)"}
return _ready_if_fine(down, terrain, "%.1f m below" % (p.y - (down["position"] as Vector3).y))
var aboveq := PhysicsRayQueryParameters3D.create(p + Vector3(0, MAX_RISE, 0), p + Vector3(0, 0.5, 0), 0xFFFFFFFF, exclude)
aboveq.hit_back_faces = false
var above := space.intersect_ray(aboveq)
if not above.is_empty() and (above["normal"] as Vector3).y > UP_FACE and _inside_solid(p, terrain):
return _ready_if_fine(above, terrain, "buried %.1f m — lifted to the surface above" % ((above["position"] as Vector3).y - p.y))
if not snap_down:
return {"ready": true, "found": false, "ground": p, "why": "kept (not snapped)"}
var pi: Dictionary = terrain.call("debug_chunk_info", p) if terrain != null and terrain.has_method("debug_chunk_info") else {}
return {"ready": false, "found": false, "ground": p, "why": "no ground within %.0f m below yet (above: %s; chunk at point: lod %s, %s tris)" % [
MAX_DROP, ("surface at %.1f, normal.y %.2f" % [(above["position"] as Vector3).y, (above["normal"] as Vector3).y]) if not above.is_empty() else "nothing",
str(pi.get("lod", "?")), str(pi.get("triangles", "?"))]}
## The FIELD at p is solid. The chunk's own density grid, filled on the GPU the way
## its mesh was (debug_chunk_density — one synchronous GPU round trip, so this runs
## only when nothing is below and a surface is above). Positive = solid, in metres:
## measured on map_geiss_terraces at (0, y, 16), surface 17.8 — y=6: +11.82,
## y=17: +0.82, y=18.6: -1.18, y=26: -8.18. (An "empty finest chunk" test failed
## there: the 16 m chunk holding the point also holds the surface, 2618 triangles.)
static func _inside_solid(p: Vector3, terrain: Node) -> bool:
if terrain == null or not terrain.has_method("debug_chunk_density"):
return false
var g: Dictionary = terrain.call("debug_chunk_density", p)
if g.is_empty():
return false
var v: float = g["voxel"]
var n: int = int(g["cells"]) + 1
var i := Vector3i((((p - (g["origin"] as Vector3)) / v).round()))
if i.x < 0 or i.y < 0 or i.z < 0 or i.x >= n or i.y >= n or i.z >= n:
return false
return (g["data"] as PackedFloat32Array)[(i.z * n + i.y) * n + i.x] > 0.0
static func _ready_if_fine(hit: Dictionary, terrain: Node, why: String) -> Dictionary:
var g: Vector3 = hit["position"]
# A node collider (a StaticBody, DCLevel, a heightmap body) is final as it stands.
if hit.get("collider") is Node or terrain == null or not terrain.has_method("debug_chunk_info"):
return {"ready": true, "found": true, "ground": g, "why": why}
var info: Dictionary = terrain.call("debug_chunk_info", g - Vector3(0, 0.05, 0))
var finest: int = int(terrain.call("get_terrain_min_lod")) if terrain.has_method("get_terrain_min_lod") else 0
var lod := int(info.get("lod", 99))
# ITS OWN collision: the ray may be standing on the PARENT's, kept alive only
# until this chunk's is built (dc-terrain holds it) — then it goes. Released
# onto it, map_mountain's player fell through and auto-respawned.
if lod <= finest and bool(info.get("has_collision", false)):
return {"ready": true, "found": true, "ground": g, "why": why}
return {"ready": false, "found": true, "ground": g, "why": "%s, terrain there LOD %d (finest %d), own collision %s" % [why, lod, finest, str(info.get("has_collision", false))]}
## The map's DCTerrain, or null.
static func find_terrain(from: Node) -> Node:
var found := from.get_tree().root.find_children("*", "DCTerrain", true, false)
return found[0] if not found.is_empty() else null