extends Node3D
## Does the SWING foot clear ground that is rising to meet it?
##
## foot_slope_gate holds a STANDING character, where swing is zero -- which is why
## standing is provably unchanged by the gait work and equally why that gate
## cannot see the feature at all. This one walks.
##
## WHAT IT MEASURES, over a real walk cycle on a real slope:
## - PEAK CLEARANCE: how high above the surface the swing foot gets. On a slope
## it must be HIGHER than on the flat, because the ground is coming up to meet
## it -- that is the whole of the gait change.
## - WORST PENETRATION: how far the lowest foot goes BELOW the surface. Ground
## adaptation that clears the swing at the cost of sinking the stance foot has
## traded one artifact for another.
## - Whether the cycle ran at all, since none of it means anything at a standstill.
##
## Run:
## "$HYPER_TPS_GODOT" --headless res://scenes/test/foot_walk_gate.tscn
const SLOPES := [0.0, 20.0, 35.0]
const SETTLE := 40 ## frames to land and reach walking speed
const WALK := 150 ## frames of walking to sample
var _plan: Array = []
var _idx := 0
var _frames := 0
var _player: Node = null
var _ground: StaticBody3D = null
var _skel: Node = null
var _peak := [-1e9, -1e9]
var _worst := [1e9, 1e9]
var _speed_sum := 0.0
var _speed_n := 0
var _phase_seen := 0.0
var _rows: Array = []
var _done := false
func _ready() -> void:
process_priority = 100
for s in SLOPES:
_plan.append(s)
_build()
func _build() -> void:
var deg: float = float(_plan[_idx])
var s: float = tan(deg_to_rad(deg))
_ground = StaticBody3D.new()
var cs := CollisionShape3D.new()
var f := PackedVector3Array()
var i := 0
while i < 80:
var j := 0
while j < 80:
var x0 := float(i) * 1.5 - 60.0
var z0 := float(j) * 1.5 - 60.0
var x1 := x0 + 1.5
var z1 := z0 + 1.5
var y0 := x0 * s
var y1 := x1 * s
# CW so the normal points UP. [[feedback_godot-cw-winding]]
f.push_back(Vector3(x0, y0, z0)); f.push_back(Vector3(x1, y1, z1)); f.push_back(Vector3(x0, y0, z1))
f.push_back(Vector3(x0, y0, z0)); f.push_back(Vector3(x1, y1, z0)); f.push_back(Vector3(x1, y1, z1))
j += 1
i += 1
var tri := ConcavePolygonShape3D.new()
tri.set_faces(f)
cs.shape = tri
_ground.add_child(cs)
add_child(_ground)
var scene: PackedScene = load("res://scenes/player/player_procedural.tscn")
_player = scene.instantiate()
add_child(_player)
_player.global_position = Vector3(0.0, 0.6, 0.0)
# Walk ACROSS the slope: the traverse holds a steady speed without climbing,
# so the cycle runs at a constant rate and the samples are comparable between
# slopes. Climbing would confound clearance with a changing speed.
_player.camera_head.global_transform = Transform3D(Basis.IDENTITY, Vector3.ZERO)
_player.input_sync.input_direction = Vector2(0.0, -1.0)
_skel = null
_frames = 0
_peak = [-1e9, -1e9]
_worst = [1e9, 1e9]
_speed_sum = 0.0
_speed_n = 0
_phase_seen = 0.0
func _physics_process(_d: float) -> void:
if _done or _player == null:
return
if _skel == null:
_skel = _find_skel(_player)
if _skel != null and _skel.has_signal("skeleton_updated"):
_skel.skeleton_updated.connect(_on_skeleton_updated)
# Hold the input: the controller reads it every frame.
_player.input_sync.input_direction = Vector2(0.0, -1.0)
_frames += 1
func _on_skeleton_updated() -> void:
if _done or _skel == null or _frames < SETTLE:
return
var deg: float = float(_plan[_idx])
var s: float = tan(deg_to_rad(deg))
var c: float = cos(deg_to_rad(deg))
var names := ["DEF-foot.L", "DEF-foot.R"]
for i in range(2):
var bi: int = _skel.find_bone(names[i])
if bi < 0:
continue
var p: Vector3 = (_skel.global_transform * _skel.get_bone_global_pose(bi)).origin
# Perpendicular height above the slope plane y = s*x.
var h: float = (p.y - s * p.x) * c
if h > _peak[i]:
_peak[i] = h
if h < _worst[i]:
_worst[i] = h
var anim: Node = _find(_player, "ProceduralAnimator")
if anim != null:
_speed_sum += float(anim.get("horizontal_speed"))
_speed_n += 1
var ph: float = float(anim.get("_cycle_phase"))
if ph > _phase_seen:
_phase_seen = ph
func _process(_d: float) -> void:
if _done or _player == null or _frames < SETTLE + WALK:
return
var deg: float = float(_plan[_idx])
var spd: float = (_speed_sum / float(_speed_n)) if _speed_n > 0 else 0.0
_rows.append({"slope": deg, "peak": maxf(_peak[0], _peak[1]),
"worst": minf(_worst[0], _worst[1]), "speed": spd,
"phase": _phase_seen, "samples": _speed_n})
_player.queue_free(); _player = null
_ground.queue_free(); _ground = null
_idx += 1
if _idx >= _plan.size():
_report()
_done = true
get_tree().quit(0)
return
_build()
func _report() -> void:
print("")
print("slope avg speed cycle ran peak clearance worst penetration")
print("---------------------------------------------------------------------")
var flat_peak := 0.0
for r in _rows:
if r["slope"] == 0.0:
flat_peak = r["peak"]
for r in _rows:
var ran: String = "yes" if r["phase"] > 0.01 else "NO -- standing"
print("%5.0f %7.2f m/s %-13s %8.4f m %+9.4f m"
% [r["slope"], r["speed"], ran, r["peak"], r["worst"]])
print("")
print("Peak clearance is measured against the FLAT row (%.4f m): on a slope the" % flat_peak)
print("swing foot must get HIGHER, because the ground is rising to meet it.")
for r in _rows:
if r["slope"] > 0.0:
print(" %2.0f deg: %+.4f m vs flat" % [r["slope"], r["peak"] - flat_peak])
var any_standing := false
for r in _rows:
if r["phase"] <= 0.01:
any_standing = true
if any_standing:
print("")
print("RIG FAILED: the walk cycle never advanced in at least one row, so those")
print("rows measure a standing character and say nothing about gait.")
func _find(n: Node, nm: String) -> Node:
if n.name == nm:
return n
for c in n.get_children():
var f: Node = _find(c, nm)
if f != null:
return f
return null
func _find_skel(n: Node) -> Node:
if n.get_class() == "Skeleton3D":
return n
for c in n.get_children():
var f: Node = _find_skel(c)
if f != null:
return f
return null