239 lines
8.8 KiB
GDScript
239 lines
8.8 KiB
GDScript
extends SceneTree
|
|
|
|
## Regression gate for jet-propelled wall traversal and physical grapple posing.
|
|
##
|
|
## Wall traversal must select the authored side-specific pilot performances,
|
|
## keep their source timing independent of gameplay speed, and face along the
|
|
## live wall tangent. Jets still own propulsion; the animation owns silhouette.
|
|
## Grapple must keep its authored airborne base while
|
|
## the live arm constraint lands the palm on the cable ray and body alignment
|
|
## responds to pendulum energy.
|
|
|
|
const SETTLE_FRAMES := 90
|
|
const SAMPLE_FRAMES := 30
|
|
const WALL_GLIDE_SPEED := 13.0
|
|
|
|
var _failures := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
|
|
func _run() -> void:
|
|
await process_frame
|
|
var args := OS.get_cmdline_user_args()
|
|
var path: String = args[0] if not args.is_empty() \
|
|
else "res://assets/characters/skins/taila.glb"
|
|
|
|
var scene := Node3D.new()
|
|
root.add_child(scene)
|
|
current_scene = scene
|
|
var model := SkinnedPlayerModel.new()
|
|
model.model_path = path
|
|
scene.add_child(model)
|
|
|
|
for _frame in 60:
|
|
await process_frame
|
|
if not model.loaded or model.skeleton == null:
|
|
_expect(false, "the authored character loads")
|
|
_done()
|
|
return
|
|
|
|
var foot := model._role_bone("foot.L", ["foot.L", "LeftFoot", "Left ankle"])
|
|
var hips := model._role_bone("hips", ["hips", "Hips", "pelvis"])
|
|
var left_hand := model._role_bone("hand.L", ["hand.L", "LeftHand", "hand_l"])
|
|
var right_hand := model._role_bone("hand.R", ["hand.R", "RightHand", "hand_r"])
|
|
_expect(foot >= 0, "a foot bone resolves")
|
|
_expect(hips >= 0 and left_hand >= 0 and right_hand >= 0,
|
|
"hips and both hands resolve")
|
|
|
|
model.set_wall_side(-1.0)
|
|
model.set_wall_glide_motion(Vector3(13.0, 0.0, 0.0))
|
|
await _settle(model, "wall_run", WALL_GLIDE_SPEED)
|
|
_expect(model.current_clip_debug() == "WallRunLeft",
|
|
"left wall traversal selects its authored pilot performance")
|
|
var wall_scale := float(model._anim_tree.get("parameters/loco_scale/scale"))
|
|
_expect(absf(wall_scale - 1.0) < 0.02,
|
|
"wall traversal keeps the authored source timing (%.3fx)"
|
|
% wall_scale)
|
|
var wall_foot_motion := await _sample_foot_motion(
|
|
model, foot, "wall_run", WALL_GLIDE_SPEED)
|
|
_expect(wall_foot_motion > 0.12,
|
|
"wall traversal retains its authored propulsion cycle (%.3f rad motion)"
|
|
% wall_foot_motion)
|
|
var wall_forward := model.wall_glide_forward_debug()
|
|
_expect(wall_forward.dot(Vector3.RIGHT) > 0.97,
|
|
"wall-glide model faces along wall-tangent velocity (dot %.3f)"
|
|
% wall_forward.dot(Vector3.RIGHT))
|
|
model.set_wall_side(1.0)
|
|
await _settle(model, "wall_run", WALL_GLIDE_SPEED)
|
|
_expect(model.current_clip_debug() == "WallRunRight",
|
|
"right wall traversal selects its authored pilot performance")
|
|
_expect(not model.clip_names_debug().has("WallRun"),
|
|
"the relabelled ground-run WallRun clip is absent")
|
|
|
|
# The imported performance must own the unarmed silhouette. A target rig's
|
|
# crossed/T-pose rest arms used to survive every clip because only relative
|
|
# bone rotations were copied.
|
|
model.set_weapon("")
|
|
model.set_locomotion(0.0, 0.0, 0.0)
|
|
await _settle(model, "idle", 0.0)
|
|
var left_shoulder := model._role_bone(
|
|
"shoulder.L", ["shoulder.L", "LeftShoulder", "clavicle_l"])
|
|
var right_shoulder := model._role_bone(
|
|
"shoulder.R", ["shoulder.R", "RightShoulder", "clavicle_r"])
|
|
var idle_drop := minf(
|
|
model.skeleton.get_bone_global_pose(left_shoulder).origin.y
|
|
- model.skeleton.get_bone_global_pose(left_hand).origin.y,
|
|
model.skeleton.get_bone_global_pose(right_shoulder).origin.y
|
|
- model.skeleton.get_bone_global_pose(right_hand).origin.y,
|
|
)
|
|
_expect(idle_drop > 0.22,
|
|
"unarmed idle hands hang below the shoulders (drop %.3f m)"
|
|
% idle_drop)
|
|
|
|
# A real backward capture turns/leans the whole body. The old fabricated
|
|
# version rotated only hips and legs by 180 degrees, leaving the first
|
|
# spine joint facing forward and creating an abdominal corkscrew.
|
|
model.set_locomotion(0.0, -1.0, 0.0)
|
|
await _settle(model, "ground", 8.25)
|
|
var spine_names: Array = model._rig_info.get("roles", {}).get("spine", [])
|
|
var first_spine := RigRoles.find_imported_bone(
|
|
model.skeleton, String(spine_names[0])) \
|
|
if not spine_names.is_empty() else -1
|
|
_expect(first_spine >= 0, "the first spine bone resolves")
|
|
if first_spine >= 0:
|
|
var hips_pose := model.skeleton.get_bone_global_pose(hips) \
|
|
.basis.get_rotation_quaternion()
|
|
var hips_rest := model.skeleton.get_bone_global_rest(hips) \
|
|
.basis.get_rotation_quaternion()
|
|
var spine_pose := model.skeleton.get_bone_global_pose(first_spine) \
|
|
.basis.get_rotation_quaternion()
|
|
var spine_rest := model.skeleton.get_bone_global_rest(first_spine) \
|
|
.basis.get_rotation_quaternion()
|
|
var hips_delta := hips_pose * hips_rest.inverse()
|
|
var spine_delta := spine_pose * spine_rest.inverse()
|
|
var abdomen_twist := hips_delta.angle_to(spine_delta)
|
|
# The Titanfall combat-backpedal deliberately keeps the weapon-facing
|
|
# chest counter-rotated over the travelling hips. The old broken capture
|
|
# was a full 180-degree lower-body reversal; preserve the authored turn
|
|
# while still rejecting that corkscrew.
|
|
_expect(abdomen_twist < 2.25,
|
|
"backward capture keeps the authored torso counter-turn (%.3f rad)"
|
|
% abdomen_twist)
|
|
|
|
var anchor := model.global_position + Vector3(3.0, 7.0, -6.0)
|
|
await _settle_grapple(model, anchor, Vector3(1.0, 0.0, 0.0))
|
|
_expect(model.current_clip_debug() == "Grapple",
|
|
"grapple selects the authored Grapple clip")
|
|
var grapple_scale := float(model._anim_tree.get("parameters/loco_scale/scale"))
|
|
_expect(absf(grapple_scale - 1.0) < 0.001,
|
|
"grapple holds its authored pose at 1.0x")
|
|
var grapple_foot_motion := await _sample_grapple_foot_motion(
|
|
model, foot, anchor, Vector3(1.0, 0.0, 0.0))
|
|
_expect(grapple_foot_motion < 0.06,
|
|
"grapple legs stay held instead of swimming (%.3f rad)"
|
|
% grapple_foot_motion)
|
|
|
|
var low: Dictionary = model.grapple_debug()
|
|
print(" grapple slow debug: ", low)
|
|
_expect(float(low.get("line_error", 1.0)) < 0.035,
|
|
"slow grapple palm lies on the cable ray (%.1f mm error)"
|
|
% (float(low.get("line_error", 1.0)) * 1000.0))
|
|
_expect(float(low.get("alignment", 1.0)) < 0.20,
|
|
"slow grapple hangs under gravity (alignment %.3f)"
|
|
% float(low.get("alignment", 1.0)))
|
|
var low_body: Quaternion = low.get(
|
|
"body_rotation", Quaternion.IDENTITY)
|
|
|
|
await _settle_grapple(model, anchor, Vector3(18.0, 2.0, 4.0))
|
|
var medium: Dictionary = model.grapple_debug()
|
|
print(" grapple medium debug: ", medium)
|
|
_expect(float(medium.get("alignment", 1.0)) < 0.60,
|
|
"medium grapple still reads as hanging (alignment %.3f)"
|
|
% float(medium.get("alignment", 1.0)))
|
|
|
|
await _settle_grapple(model, anchor, Vector3(32.0, 4.0, 8.0))
|
|
var fast: Dictionary = model.grapple_debug()
|
|
print(" grapple high-speed debug: ", fast)
|
|
_expect(float(fast.get("line_error", 1.0)) < 0.035,
|
|
"high-speed grapple palm stays on the cable ray (%.1f mm error)"
|
|
% (float(fast.get("line_error", 1.0)) * 1000.0))
|
|
_expect(float(fast.get("alignment", 0.0)) > 0.85,
|
|
"only high-speed grapple aligns the body (alignment %.3f)"
|
|
% float(fast.get("alignment", 0.0)))
|
|
var fast_body: Quaternion = fast.get(
|
|
"body_rotation", Quaternion.IDENTITY)
|
|
_expect(low_body.angle_to(fast_body) > 0.20,
|
|
"body orientation physically changes with swing energy (%.3f rad)"
|
|
% low_body.angle_to(fast_body))
|
|
|
|
model.queue_free()
|
|
_done()
|
|
|
|
|
|
func _settle(model: SkinnedPlayerModel, state: String, speed: float) -> void:
|
|
for _frame in SETTLE_FRAMES:
|
|
model.update_state(state, speed, false)
|
|
await process_frame
|
|
|
|
|
|
func _sample_foot_motion(
|
|
model: SkinnedPlayerModel,
|
|
foot: int,
|
|
state: String,
|
|
speed: float,
|
|
) -> float:
|
|
if foot < 0:
|
|
return 0.0
|
|
var start := model.skeleton.get_bone_pose_rotation(foot)
|
|
var greatest := 0.0
|
|
for _frame in SAMPLE_FRAMES:
|
|
model.update_state(state, speed, false)
|
|
await process_frame
|
|
greatest = maxf(greatest, start.angle_to(
|
|
model.skeleton.get_bone_pose_rotation(foot)))
|
|
return greatest
|
|
|
|
|
|
func _settle_grapple(model: SkinnedPlayerModel, anchor: Vector3,
|
|
velocity: Vector3) -> void:
|
|
for _frame in SETTLE_FRAMES:
|
|
model.update_state("grapple", velocity.length(), false)
|
|
model.set_grapple_target(anchor, velocity)
|
|
await process_frame
|
|
|
|
|
|
func _sample_grapple_foot_motion(
|
|
model: SkinnedPlayerModel,
|
|
foot: int,
|
|
anchor: Vector3,
|
|
velocity: Vector3,
|
|
) -> float:
|
|
if foot < 0:
|
|
return 0.0
|
|
var start := model.skeleton.get_bone_pose_rotation(foot)
|
|
var greatest := 0.0
|
|
for _frame in SAMPLE_FRAMES:
|
|
model.update_state("grapple", velocity.length(), false)
|
|
model.set_grapple_target(anchor, velocity)
|
|
await process_frame
|
|
greatest = maxf(greatest, start.angle_to(
|
|
model.skeleton.get_bone_pose_rotation(foot)))
|
|
return greatest
|
|
|
|
|
|
func _expect(ok: bool, description: String) -> void:
|
|
if ok:
|
|
print(" OK: ", description)
|
|
else:
|
|
printerr(" FAIL: ", description)
|
|
_failures += 1
|
|
|
|
|
|
func _done() -> void:
|
|
print("\n=== AUTHORED TRAVERSAL SUMMARY ===")
|
|
print("Failures: %d" % _failures)
|
|
quit(1 if _failures > 0 else 0)
|