Files
stickman/tests/test_phase6a_airborne_landing.gd

132 lines
7.0 KiB
GDScript

extends SceneTree
## Focused deterministic regression checks for the Phase 6a terrain controller.
## Runtime terrain/ramp integration remains covered by the existing stage suites.
const RIG_SCENE := preload("res://scenes/characters/stickman/stickman_rig.tscn")
const RIG := preload("res://scripts/characters/stickman/stickman_rig.gd")
const LOC := preload("res://scripts/characters/stickman/stickman_locomotion.gd")
var _checks: int = 0
var _failures: int = 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var rig := RIG_SCENE.instantiate() as Node2D
root.add_child(rig)
await physics_frame
var controller := rig as StickmanRig
_check(controller != null, "master rig exposes StickmanRig")
if controller != null:
_test_air_pose_and_momentum(controller)
_test_grounded_reset_and_ragdoll_cleanup(controller)
_test_landing_timeline_and_barrier(controller)
rig.queue_free()
await process_frame
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 _test_air_pose_and_momentum(rig: StickmanRig) -> void:
var locomotion: StickmanLocomotion = rig.get_locomotion()
var left_hand := rig._get_ik_marker("Left_Hand")
var right_hand := rig._get_ik_marker("Right_Hand")
var left_base := left_hand.position
var right_base := right_hand.position
locomotion._enter_airborne(300.0)
_check(locomotion.terrain_motion == LOC.TerrainMotion.AIRBORNE, "enter_airborne selects AIRBORNE")
_check(is_equal_approx(locomotion.terrain_air_velocity.x, 300.0), "takeoff keeps signed horizontal velocity")
locomotion.terrain_air_velocity.y = LOC.AIR_POSE_FULL_SPEED
locomotion._air_time = 0.0
locomotion._apply_air_pose(1.0)
_check(locomotion.air_pose_weight > 0.99 and locomotion.air_pose_weight <= 1.0, "air pose weight clamps at full fall speed")
_check(left_hand.position != left_base and right_hand.position != right_base, "air pose offsets both hands")
locomotion._restore_air_pose()
_check(left_hand.position.is_equal_approx(left_base) and right_hand.position.is_equal_approx(right_base), "air pose restores exact captured markers")
locomotion.terrain_motion = LOC.TerrainMotion.GROUNDED
locomotion._enter_airborne(0.0)
var forward_left_base: Vector2 = locomotion.air_pose_base["Left_Hand"]
var forward_right_base: Vector2 = locomotion.air_pose_base["Right_Hand"]
locomotion.terrain_air_velocity.y = LOC.AIR_HAND_FORWARD_START_SPEED + 20.0
locomotion._air_time = 0.0
locomotion._apply_air_pose(1.0)
_check(left_hand.position.x > forward_left_base.x and right_hand.position.x > forward_right_base.x, "hands reach forward before lift engages")
_check(is_equal_approx(left_hand.position.y, forward_left_base.y) and is_equal_approx(right_hand.position.y, forward_right_base.y), "early forward reach does not lift hands")
locomotion.terrain_air_velocity.y = LOC.AIR_POSE_FULL_SPEED
locomotion._apply_air_pose(1.0)
_check(left_hand.position.y < forward_left_base.y and right_hand.position.y < forward_right_base.y, "hands lift after forward reach")
locomotion._restore_air_pose()
locomotion.terrain_air_velocity = Vector2(300.0, 0.0)
locomotion.terrain_air_velocity.x *= exp(-LOC.AIR_DRAG_X * 0.1)
_check(locomotion.terrain_air_velocity.x > 0.0 and locomotion.terrain_air_velocity.x < 300.0, "air drag preserves forward momentum while decaying it")
func _test_grounded_reset_and_ragdoll_cleanup(rig: StickmanRig) -> void:
var locomotion: StickmanLocomotion = rig.get_locomotion()
locomotion.terrain_following_enabled = true
locomotion.terrain_motion = LOC.TerrainMotion.GROUNDED
locomotion._last_grounded_walk_velocity_x = 999.0
rig._physics_process(0.0)
_check(is_zero_approx(locomotion._last_grounded_walk_velocity_x), "grounded idle frame clears stale walk velocity")
locomotion._enter_airborne(-240.0)
locomotion.terrain_air_velocity.y = 640.0
locomotion._apply_air_pose(0.1)
rig.set_ragdoll(true)
_check(rig.state == RIG.RigState.RAGDOLL, "direct ragdoll enters from ordinary airborne motion")
_check(locomotion.terrain_motion == LOC.TerrainMotion.DISABLED and locomotion.air_pose_base.is_empty(), "direct ragdoll clears airborne terrain ownership")
_check(locomotion.terrain_air_velocity.is_zero_approx() and is_zero_approx(locomotion.air_pose_weight), "direct ragdoll clears air velocity and pose weight")
rig.snap_to_standing()
func _test_landing_timeline_and_barrier(rig: StickmanRig) -> void:
var locomotion: StickmanLocomotion = rig.get_locomotion()
var torso := rig._get_ik_marker("Torso")
var left_leg := rig._get_ik_marker("Left_Leg")
var right_leg := rig._get_ik_marker("Right_Leg")
locomotion._enter_airborne(0.0)
var base_torso := torso.position
var left_hand := rig._get_ik_marker("Left_Hand")
var right_hand := rig._get_ik_marker("Right_Hand")
var left_hand_base := left_hand.position
var right_hand_base := right_hand.position
var contacts := {
"Left_Leg": {"surface": left_leg.global_position + Vector2(0.0, 1200.0), "surface_valid": true},
"Right_Leg": {"surface": right_leg.global_position + Vector2(0.0, 1300.0), "surface_valid": true},
}
locomotion.air_fall_distance = LOC.HARD_LANDING_MIN_FALL_HEIGHT
locomotion._begin_landing(0.0, {"world_pos": torso.global_position}, contacts)
_check(is_equal_approx(locomotion.landing_compression, LOC.LANDING_COMPRESSION_MIN), "minimum hard fall height maps to minimum compression")
_check(locomotion.is_action_blocked(), "landing blocks primary queue")
locomotion._update_landing_impact(LOC.LANDING_COMPRESS_DURATION)
_check(is_equal_approx(torso.position.y, base_torso.y + LOC.LANDING_COMPRESSION_MIN), "impact reaches deepest compression at 0.08s")
_check(left_hand.position.y > left_hand_base.y and right_hand.position.y > right_hand_base.y, "hard landing sharply drives hands downward")
_check(left_leg.global_position.y < torso.global_position.y + 500.0 and right_leg.global_position.y < torso.global_position.y + 500.0, "landing ignores stale pre-motion solved contacts")
locomotion._update_landing_impact(LOC.LANDING_RECOVER_DURATION)
_check(locomotion.terrain_motion == LOC.TerrainMotion.GROUNDED, "landing returns to GROUNDED after 0.30s")
_check(torso.position.is_equal_approx(base_torso), "landing restores torso exactly")
_check(left_hand.position.is_equal_approx(left_hand_base) and right_hand.position.is_equal_approx(right_hand_base), "landing restores hand bases exactly")
_check(not locomotion.is_action_blocked(), "queue barrier clears after landing")
locomotion._enter_airborne(0.0)
locomotion.air_fall_distance = LOC.HARD_LANDING_MAX_FALL_HEIGHT
locomotion._begin_landing(0.0, {"world_pos": torso.global_position}, contacts)
_check(is_equal_approx(locomotion.landing_compression, LOC.LANDING_COMPRESSION_MAX), "maximum hard fall height maps to maximum compression")
locomotion._cancel_landing_impact(true)
locomotion._restore_air_pose()
func _check(condition: bool, message: String) -> void:
_checks += 1
if condition:
print("PASS: " + message)
else:
_failures += 1
print("FAIL: " + message)