feat: Implement Phase 6a Airborne Mechanics and Landing Absorption
- Added new scene file `test_fall.scene` for testing airborne dynamics. - Created architecture proposal in `PHASE_6a_AIRBORNE_LANDING.md` detailing airborne momentum, procedural air pose, and landing impact absorption mechanics. - Developed specifications in `phase_6a_airborne_landing.md` outlining implementation decisions, targets, and static contracts for the airborne system. - Introduced focused regression tests in `test_phase6a_airborne_landing.gd` to validate airborne pose, momentum, landing timeline, and queue behavior. - Ensured all tests pass with 100% assertion success.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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://master_rig.tscn")
|
||||
const RIG := preload("res://scripts/stickman_rig.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 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
|
||||
rig._enter_airborne(300.0)
|
||||
_check(rig._terrain_motion == RIG.TerrainMotion.AIRBORNE, "enter_airborne selects AIRBORNE")
|
||||
_check(is_equal_approx(rig._terrain_air_velocity.x, 300.0), "takeoff keeps signed horizontal velocity")
|
||||
rig._terrain_air_velocity.y = RIG.AIR_POSE_FULL_SPEED
|
||||
rig._air_time = 0.0
|
||||
rig._apply_air_pose(1.0)
|
||||
_check(rig._air_pose_weight > 0.99 and rig._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")
|
||||
rig._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")
|
||||
rig._terrain_motion = RIG.TerrainMotion.GROUNDED
|
||||
rig._enter_airborne(0.0)
|
||||
var forward_left_base: Vector2 = rig._air_pose_base["Left_Hand"]
|
||||
var forward_right_base: Vector2 = rig._air_pose_base["Right_Hand"]
|
||||
rig._terrain_air_velocity.y = RIG.AIR_HAND_FORWARD_START_SPEED + 20.0
|
||||
rig._air_time = 0.0
|
||||
rig._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")
|
||||
rig._terrain_air_velocity.y = RIG.AIR_POSE_FULL_SPEED
|
||||
rig._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")
|
||||
rig._restore_air_pose()
|
||||
rig._terrain_air_velocity = Vector2(300.0, 0.0)
|
||||
rig._terrain_air_velocity.x *= exp(-RIG.AIR_DRAG_X * 0.1)
|
||||
_check(rig._terrain_air_velocity.x > 0.0 and rig._terrain_air_velocity.x < 300.0, "air drag preserves forward momentum while decaying it")
|
||||
|
||||
|
||||
func _test_grounded_reset_and_ragdoll_cleanup(rig: StickmanRig) -> void:
|
||||
rig._terrain_following_enabled = true
|
||||
rig._terrain_motion = RIG.TerrainMotion.GROUNDED
|
||||
rig._last_grounded_walk_velocity_x = 999.0
|
||||
rig._physics_process(0.0)
|
||||
_check(is_zero_approx(rig._last_grounded_walk_velocity_x), "grounded idle frame clears stale walk velocity")
|
||||
rig._enter_airborne(-240.0)
|
||||
rig._terrain_air_velocity.y = 640.0
|
||||
rig._apply_air_pose(0.1)
|
||||
rig.set_ragdoll(true)
|
||||
_check(rig.state == RIG.RigState.RAGDOLL, "direct ragdoll enters from ordinary airborne motion")
|
||||
_check(rig._terrain_motion == RIG.TerrainMotion.DISABLED and rig._air_pose_base.is_empty(), "direct ragdoll clears airborne terrain ownership")
|
||||
_check(rig._terrain_air_velocity.is_zero_approx() and is_zero_approx(rig._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 torso := rig._get_ik_marker("Torso")
|
||||
var left_leg := rig._get_ik_marker("Left_Leg")
|
||||
var right_leg := rig._get_ik_marker("Right_Leg")
|
||||
rig._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},
|
||||
}
|
||||
rig._air_fall_distance = RIG.HARD_LANDING_MIN_FALL_HEIGHT
|
||||
rig._begin_landing(0.0, {"world_pos": torso.global_position}, contacts)
|
||||
_check(is_equal_approx(rig._landing_compression, RIG.LANDING_COMPRESSION_MIN), "minimum hard fall height maps to minimum compression")
|
||||
_check(rig._is_action_queue_blocked(), "landing blocks primary queue")
|
||||
rig._update_landing_impact(RIG.LANDING_COMPRESS_DURATION)
|
||||
_check(is_equal_approx(torso.position.y, base_torso.y + RIG.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")
|
||||
rig._update_landing_impact(RIG.LANDING_RECOVER_DURATION)
|
||||
_check(rig._terrain_motion == RIG.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 rig._is_action_queue_blocked(), "queue barrier clears after landing")
|
||||
rig._enter_airborne(0.0)
|
||||
rig._air_fall_distance = RIG.HARD_LANDING_MAX_FALL_HEIGHT
|
||||
rig._begin_landing(0.0, {"world_pos": torso.global_position}, contacts)
|
||||
_check(is_equal_approx(rig._landing_compression, RIG.LANDING_COMPRESSION_MAX), "maximum hard fall height maps to maximum compression")
|
||||
rig._cancel_landing_impact(true)
|
||||
rig._restore_air_pose()
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print("PASS: " + message)
|
||||
else:
|
||||
_failures += 1
|
||||
print("FAIL: " + message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2w7tlb4cke6t
|
||||
Reference in New Issue
Block a user