RunMotionChecks.cs
using Sandbox;
using System;
namespace PaintballBaddies;

/// <summary>Opt-in full-body camera and actual-controller run review.</summary>
public sealed class RunMotionChecks : Component
{
    [Property] public bool ReviewFootsteps { get; set; }
    [Property] public int CharacterIndex { get; set; }
    private PlayerController player;
    private PaintballMarker weapon;
    private float elapsed,maxTime,maxLowerAnkle,minSpeed=10000;
    private int samples;
    private Vector3 start;
    private bool input,look,camera,accept,selected,runOnly=true;
    protected override void OnStart()
    {
        player=Scene.GetAllComponents<PlayerController>().First();
        weapon=player.Components.Get<PaintballMarker>();
        input=player.UseInputControls;look=player.UseLookControls;camera=player.UseCameraControls;accept=weapon.AcceptInput;
        player.UseInputControls=player.UseLookControls=player.UseCameraControls=false;weapon.AcceptInput=false;
        player.WorldPosition=new Vector3(-950,-600,2);player.EyeAngles=new Angles(0,90,0);start=player.WorldPosition;
    }
    protected override void OnUpdate()
    {
        // RosterSelection initializes in OnStart; choose the review character afterward.
        if(!selected){player.Components.Get<RosterSelection>().Select(CharacterIndex);selected=true;}
        if(player.Components.Get<MovementFootsteps>() is { } steps)steps.ReviewContacts=ReviewFootsteps;
        elapsed+=Time.Delta;
        player.WishVelocity=elapsed<3 ? Vector3.Left*180 : Vector3.Zero;
        WorldPosition=player.WorldPosition+new Vector3(170,-100,85);
        WorldRotation=Rotation.LookAt(player.WorldPosition+Vector3.Up*36-WorldPosition);
        if(elapsed>.8f && elapsed<2.9f)
        {
            minSpeed=MathF.Min(minSpeed,player.Velocity.WithZ(0).Length);
            runOnly &= player.Renderer.Sequence.Name=="run";
            maxTime=MathF.Max(maxTime,player.Renderer.Sequence.Time);
            if(player.Renderer.TryGetBoneTransform("LeftFoot",out var l) && player.Renderer.TryGetBoneTransform("RightFoot",out var r))
            {
                if(ReviewFootsteps)Log.Info($"FOOT_PHASE phase={player.Renderer.Sequence.TimeNormalized} left={l.Position.z-player.WorldPosition.z} right={r.Position.z-player.WorldPosition.z}");
                maxLowerAnkle=MathF.Max(maxLowerAnkle,MathF.Min(l.Position.z,r.Position.z)-player.WorldPosition.z);samples++;
            }
        }
        if(elapsed<4.5f)return;
        Check("forward travel",Vector3.Dot(player.WorldPosition-start,Vector3.Left)>400,$"travel={player.WorldPosition-start}");
        Check("sustained speed",minSpeed>150,$"minimum={minSpeed}");
        Check("cycle advances",runOnly && maxTime>.35f,$"run_only={runOnly},time={maxTime}");
        Check("selected character",player.Renderer.Model?.Name==RosterSelection.GetModelPath(CharacterIndex),player.Renderer.Model?.Name);
        Check("ankle samples",samples>60,$"samples={samples}");
        // Ankle origins sit above boot soles; this catches excessive whole-body flight,
        // but cannot establish sole contact or horizontal foot locking.
        Check("bounded lower ankle",maxLowerAnkle<12,$"maximum_inches={maxLowerAnkle}");
        Check("idle recovery",player.Renderer.Sequence.Name=="idle",player.Renderer.Sequence.Name);
        Log.Info("RUN_MOTION COMPLETE");Destroy();
    }
    private void Check(string name,bool pass,string detail)=>Log.Info($"RUN_MOTION {(pass ? "PASS" : "FAIL")} {name}: {detail}");
    protected override void OnDestroy()
    {
        if(player.IsValid()){player.WishVelocity=Vector3.Zero;player.UseInputControls=input;player.UseLookControls=look;player.UseCameraControls=camera;}
        if(weapon.IsValid())weapon.AcceptInput=accept;
    }
}