extends Node3D
## PROBE (not a gate): what the FEET and the drawn body do walking up stairs (user
## 2026-10-05: "player stair step is all wrong ... this is really just normal walk or
## run but with ik offsets"). Per frame, each foot's height above the ground under it
## (a ray down from above the foot), on the flat run-up and on the stairs, plus the
## drawn-body offset (_step_smooth) and the camera head's height over the body.
##
## Run: "$HYPER_TPS_GODOT" --headless --path . res://scenes/test/_probe_stairs_feet.tscn
const RISE := 0.224
const TREAD := 0.30
var _player: Node = null
var _skel: Skeleton3D = null
var _frames := 0
var _rows := {"flat": [], "stairs": []}
var _smooth_max := 0.0
var _cam_dev := 0.0
var _spd := {"flat": [], "stairs": []}
var _lag_sum := 0.0
var _lag_n := 0
func _box(lo: Vector3, hi: Vector3) -> void:
var b := StaticBody3D.new()
var cs := CollisionShape3D.new()
var sh := BoxShape3D.new()
sh.size = hi - lo
cs.shape = sh
b.position = (lo + hi) * 0.5
b.add_child(cs)
add_child(b)
func _ready() -> void:
process_priority = 100
_box(Vector3(-2, -1, -40), Vector3(2, 0, 6))
for i in 12:
_box(Vector3(-2, 0, -3 - TREAD * (i + 1)), Vector3(2, RISE * (i + 1), -3 - TREAD * i))
_box(Vector3(-2, 0, -40), Vector3(2, RISE * 12, -3 - TREAD * 12))
_player = (load("res://scenes/player/player_procedural.tscn") as PackedScene).instantiate()
add_child(_player)
_player.global_position = Vector3(0, 0.02, 3.0)
_player.camera_head.global_transform = Transform3D(Basis.IDENTITY, _player.camera_head.global_position)
if OS.get_environment("PROBE_WALK") == "1":
_player.set("walking", true)
func _physics_process(_d: float) -> void:
if _player == null:
return
_player.input_sync.input_direction = Vector2(0.0, -1.0)
_frames += 1
if _skel == null:
for n: Node in _player.find_children("*", "Skeleton3D", true, false):
_skel = n
if _skel != null:
_skel.skeleton_updated.connect(_sample)
_smooth_max = maxf(_smooth_max, absf(float(_player.get("_step_smooth"))))
_cam_dev = maxf(_cam_dev, absf(_player.camera_head.position.y - 1.5))
var hs := Vector2(_player.velocity.x, _player.velocity.z).length()
if _frames > 30 and _player.global_position.z > -2.5:
(_spd["flat"] as Array).append(hs)
elif _player.global_position.z < -3.5 and _player.global_position.z > -3 - TREAD * 11:
(_spd["stairs"] as Array).append(hs)
if _player.global_position.z < -3.5 and _player.global_position.z > -3 - TREAD * 11:
_lag_sum += absf(float(_player.get("_step_smooth")))
_lag_n += 1
if _frames > 260:
_report()
_player = null
get_tree().quit(0)
func _sample() -> void:
if _frames < 30 or _player == null:
return
var z: float = _player.global_position.z
var where := "flat" if z > -2.5 else ("stairs" if z > -3 - TREAD * 11 else "")
if where == "":
return
var space := get_world_3d().direct_space_state
for nm: String in ["DEF-foot.L", "DEF-foot.R"]:
var bi := _skel.find_bone(nm)
if bi < 0:
continue
var p: Vector3 = (_skel.global_transform * _skel.get_bone_global_pose(bi)).origin
var q := PhysicsRayQueryParameters3D.create(p + Vector3(0, 0.6, 0), p - Vector3(0, 1.0, 0))
q.exclude = [_player.get_rid()]
var hit: Dictionary = space.intersect_ray(q)
if hit.is_empty():
continue
(_rows[where] as Array).append(p.y - (hit["position"] as Vector3).y)
func _report() -> void:
for where: String in ["flat", "stairs"]:
var a: Array = _rows[where]
if a.is_empty():
print("PROBE %s: no samples" % where)
continue
a.sort()
# The ankle bone rests ~0.12 m over the ground when planted on the flat; below
# 0.06 the foot is INSIDE the surface under it.
var below := a.filter(func(v: float) -> bool: return v < 0.06).size()
print("PROBE %-6s feet above ground: min %.3f p10 %.3f median %.3f p90 %.3f max %.3f | %d of %d samples with the foot inside the ground (ankle < 0.06)" % [
where, a[0], a[int(a.size() * 0.1)], a[a.size() / 2], a[int(a.size() * 0.9)], a[-1], below, a.size()])
for w: String in ["flat", "stairs"]:
var a: Array = _spd[w]
if not a.is_empty():
var tot := 0.0
var slow := 0
for v: float in a:
tot += v
if v < 2.0:
slow += 1
print("PROBE %-6s horizontal speed: mean %.2f m/s, min %.2f, %d of %d frames under 2 m/s" % [w, tot / a.size(), a.min(), slow, a.size()])
print("PROBE ON THE FLIGHT: drawn body below the real one by %.3f m on average (%d frames)" % [_lag_sum / maxf(1, _lag_n), _lag_n])
print("PROBE drawn-body offset max %.3f m, camera head off its rest by up to %.3f m, reached y %.2f of %.2f" % [
_smooth_max, _cam_dev, _player.global_position.y, RISE * 12])