OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / _probe_stairs_feet.gd

120 lines · gd ref:
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])