Files
stickman/tests/test_phase4b_walk.gd
T

127 lines
4.7 KiB
GDScript

# test_phase4b_walk.gd
# Headless regression checks for the Phase 4b walk-waypoint arrival-jitter fix
# (spec §3.4 / §5.9):
# 1. On-mesh waypoint: steering mode latches "nav" once, exactly one `arrived`
# fires, and the rig position is unchanged for 60+ physics frames after.
# 2. Off-mesh waypoint: mode latches "direct" once, one arrive, stable.
# 3. No vertical jitter: the root's final resting Y equals the snapped target.
#
# Run with:
# & "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --script res://tests/test_phase4b_walk.gd --path .
#
# Prints PASS/FAIL per assertion and exits 0 on all PASS, 1 on any FAIL.
extends SceneTree
const STAGE_SCENE := preload("res://scenes/stage/sandbox_stage.tscn")
const RIG := preload("res://scripts/characters/stickman/stickman_rig.gd")
const LOCOMOTION := preload("res://scripts/characters/stickman/stickman_locomotion.gd")
const SETTLE_FRAMES := 60
const MAX_FRAMES := 300
var _checks := 0
var _failures := 0
var _arrived_count := 0
func _initialize() -> void:
call_deferred("_run")
func _on_arrived(_target: Vector2) -> void:
_arrived_count += 1
func _run() -> void:
print("")
print("========================================================")
print(" PHASE 4b WALK JITTER / LATCH TEST (headless)")
print("========================================================")
# On-mesh waypoint (x=50 inside the 200-wide ground slab).
await _walk_case("on-mesh", Vector2(50.0, 0.0), "nav")
# Off-mesh waypoint (x=200 outside the slab edge at x=100).
await _walk_case("off-mesh", Vector2(200.0, 0.0), "direct")
print("--------------------------------------------------------")
if _failures == 0:
print("RESULT: ALL PASSED (%d assertions, 0 failures)" % _checks)
quit(0)
else:
print("RESULT: %d FAILURE(S) out of %d assertions" % [_failures, _checks])
quit(1)
func _walk_case(label: String, target_feet: Vector2, expected_mode: String) -> void:
print("")
print("--- %s waypoint (%s) ---" % [label, target_feet])
var stage: Node2D = STAGE_SCENE.instantiate()
root.add_child(stage)
var ground = stage._spawner.spawn("ground", Vector2(0, 0))
stage._rebake_navigation()
var rig = stage._spawner.spawn("stickman", Vector2(0, 0))
_check(rig != null, "%s: stickman spawns" % label)
if rig == null:
stage.queue_free()
await process_frame
return
_arrived_count = 0
rig.arrived.connect(_on_arrived)
rig.walk_to(target_feet, 200.0)
var locomotion: StickmanLocomotion = rig.get_locomotion()
_check(locomotion != null, "%s: locomotion component is available" % label)
if locomotion == null:
stage.queue_free()
await process_frame
return
_check(not locomotion._walk_mode_latched, "%s: mode not latched before map sync" % label)
var frames := 0
while not locomotion._walk_done and frames < MAX_FRAMES:
await physics_frame
frames += 1
_check(locomotion._walk_done, "%s: walk finished within %d frames (used %d)" % [label, MAX_FRAMES, frames])
_check(_arrived_count == 1, "%s: exactly one arrived signal (got %d)" % [label, _arrived_count])
_check(locomotion._walk_mode_latched, "%s: mode is latched" % label)
_check(locomotion._walk_mode == expected_mode,
"%s: mode latched to '%s' (got '%s')" % [label, expected_mode, locomotion._walk_mode])
# Final position equals the snapped root target (feet target + FOOT_OFFSET).
var final_root: Vector2 = target_feet + LOCOMOTION.FOOT_OFFSET
_check(rig.global_position.distance_to(final_root) <= 0.01,
"%s: position snapped to final root (dist=%.3f)" % [label, rig.global_position.distance_to(final_root)])
# Completion freezes the solved final animation frame; it must not be reset
# or drift while the root is at rest.
var torso = rig.get_node_or_null(NodePath("IK_Targets/Torso"))
var frozen_torso_position: Vector2 = Vector2.ZERO
if torso != null:
frozen_torso_position = (torso as Node2D).position
_check(frozen_torso_position.is_finite(), "%s: Torso final animation marker is finite" % label)
else:
_check(false, "%s: IK_Targets/Torso missing" % label)
# Post-arrival stability for 60 physics frames.
var p0: Vector2 = rig.global_position
var settled := true
for i in SETTLE_FRAMES:
await physics_frame
if rig.global_position != p0:
settled = false
break
_check(settled and rig.global_position == p0,
"%s: position unchanged for %d frames after arrival" % [label, SETTLE_FRAMES])
if torso != null:
_check((torso as Node2D).position.distance_to(frozen_torso_position) <= 0.01,
"%s: Torso final animation marker remains frozen" % label)
_check(not rig.is_walking(), "%s: is_walking false after arrival" % label)
stage.queue_free()
await process_frame
func _check(condition: bool, message: String) -> void:
_checks += 1
if condition:
print("PASS: " + message)
else:
_failures += 1
print("FAIL: " + message)