extends SceneTree ## Regression gate for speed-scaled traversal blends, authored stopping, and ## alternating mirrored airborne silhouettes in the Titanfall motion lab. const DEFAULT_MODEL_PATH := \ "res://assets/characters/local_reference/titanfall_motion/mannequin.glb" const SPEED := 13.0 var _failures: Array[String] = [] var _model: SkinnedPlayerModel var _body: CharacterBody3D var _model_path := DEFAULT_MODEL_PATH func _initialize() -> void: var args := OS.get_cmdline_user_args() if not args.is_empty(): _model_path = args[0] call_deferred("_run") func _run() -> void: _body = CharacterBody3D.new() root.add_child(_body) current_scene = _body _model = SkinnedPlayerModel.new() _model.model_path = _model_path _body.add_child(_model) for _frame in 60: await process_frame _expect(_model.loaded, "motion-lab mannequin loads") if not _model.loaded: _finish() return for clip in ["Stop", "JumpAlt", "FallAlt"]: _expect(_model.clip_names_debug().has(clip), "rebuilt motion pack contains " + clip) # The same transition is deliberately quicker at high speed, while both # retain enough time for the skeleton to interpolate rather than cut. _body.velocity = Vector3(0.0, -2.0, -4.0) await _drive("air", 4.0, 20) _model.set_wall_side(-1.0) await _drive("wall_run", 4.0, 1) var slow_wall_blend := _model._state_trans.xfade_time await _drive("ground", 0.0, 24) _body.velocity = Vector3(0.0, -2.0, -18.0) await _drive("air", 18.0, 20) _model.set_wall_side(1.0) await _drive("wall_run", 18.0, 1) var fast_wall_blend := _model._state_trans.xfade_time _expect(slow_wall_blend > 0.0 and fast_wall_blend > 0.0, "wall transitions never hard-cut") _expect(fast_wall_blend < slow_wall_blend, "wall transition shortens with speed (%.3f -> %.3f s)" % [ slow_wall_blend, fast_wall_blend]) # Stop owns the full body while physical velocity decays. It must not leave # the high-rate Sprint node active during the release glide. _model.set_locomotion(0.0, 1.0, 0.0) await _drive("ground", 11.0, 45) await _drive("stop", 11.0, 1) _expect(_model.current_clip_debug() == "Stop", "release selects the authored Stop performance") var stop_blend := _model._state_trans.xfade_time var stop_scale := float(_model._anim_tree.get( "parameters/loco_scale/scale")) _expect(stop_blend > 0.0 and stop_blend < 0.19, "stop enters through a quick nonzero transition (%.3f s)" % stop_blend) _expect(stop_scale > 1.0, "high-speed stop compresses source timing (%.2fx)" % stop_scale) var physical_speed := 11.0 var stop_distance := 0.0 var stop_held := true var deceleration := MovementParams.new().ground_deceleration while physical_speed > 0.6: physical_speed *= exp(-deceleration / 60.0) stop_distance += physical_speed / 60.0 await _drive("stop", physical_speed, 1) stop_held = stop_held and _model.current_clip_debug() == "Stop" _expect(stop_held, "Stop remains active during residual movement") await _drive("ground", 0.0, 14) _expect(stop_distance > 0.75 and stop_distance < 1.5, "physical stop carries a short distance (%.2f m)" % stop_distance) _expect(_model.current_clip_debug() == "Idle", "stop resolves into Idle instead of a fast run cycle") _expect(_model.locomotion_effective_speed_debug() < 0.25, "stale sprint playback speed is cleared after stopping") # Sample both clips at the same point after their short crossfade. The signed # knee lead must reverse, proving the alternate is a true mirrored source # performance rather than another phase of the same pose. await _drive("ground", 0.0, 30) _body.velocity = Vector3(0.0, 8.5, -8.0) await _drive("air", 8.0, 12) var normal_lead := _knee_lead() await _drive("ground", 0.0, 30) _body.velocity = Vector3(0.0, 8.5, -8.0) await _drive("air_alt", 8.0, 12) var alternate_lead := _knee_lead() _expect(absf(normal_lead) > 0.04 and absf(alternate_lead) > 0.04 \ and normal_lead * alternate_lead < 0.0, "successive airborne clips swap the leading knee (%.3f / %.3f m)" % [ normal_lead, alternate_lead]) # Controller event wiring alternates the presentation state on successive # ground jumps and leaves the physics movement state untouched. var controller := PlayerMovementController.new() controller._machine = MovementStateMachine.new() controller._on_movement_event("jump", {}) var first_air := controller._visual_animation_state("air", 8.0) controller._on_movement_event("jump", {}) var second_air := controller._visual_animation_state("air", 8.0) _expect(first_air != second_air and first_air in ["air", "air_alt"] \ and second_air in ["air", "air_alt"], "successive jump events alternate runtime airborne variants") controller._machine.free() controller.free() _body.queue_free() await process_frame _finish() func _drive(state: String, speed: float, frames: int) -> void: for _frame in frames: _model.update_state(state, speed, false) await process_frame func _knee_lead() -> float: var rises := {} for side in ["L", "R"]: var thigh := _model._role_bone("thigh." + side, ["thigh." + side, "Left thigh" if side == "L" else "Right thigh"]) var shin := _model._role_bone("shin." + side, ["shin." + side, "Left shin" if side == "L" else "Right shin"]) var segment := _model.skeleton.get_bone_global_pose(shin).origin \ - _model.skeleton.get_bone_global_pose(thigh).origin rises[side] = (_model.skeleton.global_transform.basis * segment).y return float(rises["R"]) - float(rises["L"]) func _expect(ok: bool, description: String) -> void: if ok: print(" OK: ", description) else: _failures.append(description) printerr(" FAIL: ", description) func _finish() -> void: print("\n=== MOTION VARIATION SUMMARY ===") print("Failures: %d" % _failures.size()) quit(1 if not _failures.is_empty() else 0)