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

/// <summary>Measured run contact phases with distance fallback for unmeasured clips.</summary>
public sealed class MovementFootsteps : Component
{
    public bool ReviewContacts { get; set; }
    private Vector3 previous;
    private float distance,cooldown;
    private float previousPhase=-1;
    private string previousModel;
    public int StepsPlayed { get; private set; }
    public int NativeContactEvents { get; private set; }
    public int NativeStepsPlayed { get; private set; }
    private SkinnedModelRenderer contactSource;
    private bool pendingContact;
    private float contactAge=10;
    private void Contact(SceneModel.FootstepEvent e)
    {
        NativeContactEvents++;pendingContact=true;contactAge=0;
    }
    private void BindContacts(SkinnedModelRenderer renderer)
    {
        if(contactSource==renderer)return;
        if(contactSource.IsValid())contactSource.OnFootstepEvent-=Contact;
        contactSource=renderer;pendingContact=false;contactAge=10;
        if(contactSource.IsValid())contactSource.OnFootstepEvent+=Contact;
    }
    protected override void OnDestroy()=>BindContacts(null);
    protected override void OnStart()=>previous=WorldPosition;
    protected override void OnUpdate()
    {
        var citizenPlayer=Components.Get<CitizenPlayerPresentation>();
        var citizenOpponent=Components.Get<CitizenOpponentPresentation>();
        BindContacts(citizenPlayer?.IsReady==true ? citizenPlayer.FootstepRenderer :
            citizenOpponent?.IsReady==true ? citizenOpponent.FootstepRenderer : null);
        contactAge+=Time.Delta;
        var nativeContactPending=pendingContact;pendingContact=false;
        var travel=(WorldPosition-previous).WithZ(0).Length;
        previous=WorldPosition;
        cooldown=(cooldown-Time.Delta).Clamp(0,1);
        if(travel>32 || Components.Get<PaintballCombatant>()?.Eliminated==true ||
           Components.Get<VaultController>()?.IsVaulting==true)
        {distance=0;previousPhase=-1;return;}
        var speed=travel/MathF.Max(Time.Delta,.001f);
        if(speed<8){distance=0;previousPhase=-1;return;}
        var ground=Scene.Trace.Ray(WorldPosition+Vector3.Up*2,WorldPosition-Vector3.Up*5)
            .IgnoreGameObjectHierarchy(GameObject).Run();
        if(!ground.Hit || ground.Normal.z<.6f){distance=0;previousPhase=-1;return;}
        var player=Components.Get<PlayerController>();
        var crouched=player?.IsDucking==true || Components.Get<ArenaOpponent>()?.UsingCover==true;
        var stride=crouched ? 25f : speed>130 ? 48f : 32f;
        var animated=player?.Renderer ?? Components.Get<ArenaOpponent>()?.Body;
        var profile=animated?.Model?.Name switch
        {
            "models/characters/viper_protected_morph/viper_protected.vmdl" => (.0231f,.5529f),
            "models/characters/roxie_protected_morph/roxie_protected.vmdl" => (.0211f,.5570f),
            "models/characters/imani_protected_morph/imani_protected.vmdl" => (.0242f,.5221f),
            "models/characters/mei_protected_morph/mei_protected.vmdl" => (.0283f,.5590f),
            "models/characters/freya_protected_morph/freya_protected.vmdl" => (.0299f,.5635f),
            "models/characters/leilani_protected_morph/leilani_protected.vmdl" => (.0290f,.5548f),
            _ => (-1f,-1f)
        };
        if(previousModel!=animated?.Model?.Name){previousPhase=-1;previousModel=animated?.Model?.Name;}
        // Citizen uses an animation graph; the hidden legacy sequence is not a contact clock.
        bool citizen=Components.Get<CitizenPlayerPresentation>()?.IsReady==true ||
            Components.Get<CitizenOpponentPresentation>()?.IsReady==true;
        bool measuredRun=!citizen && animated.IsValid() && animated.Enabled && profile.Item1>=0 && animated.Sequence.Name=="run";
        bool nativeContact=contactSource.IsValid() && contactAge<.75f;
        if(nativeContact)
        {
            previousPhase=-1;distance=0;
            if(!nativeContactPending || cooldown>0)return;
        }
        else if(measuredRun)
        {
            var phase=animated.Sequence.TimeNormalized;
            bool crosses(float point)=>previousPhase>=0 && (phase>=previousPhase ? previousPhase<point && phase>=point : previousPhase<point || phase>=point);
            var advance=previousPhase<0 ? 0 : (phase-previousPhase+1)%1;
            bool contact=advance<=.1f && (crosses(profile.Item1)||crosses(profile.Item2));
            previousPhase=phase;distance=0;
            if(!contact || cooldown>0)return;
        }
        else
        {
            previousPhase=-1;distance+=travel;
            if(distance<stride || cooldown>0)return;
            distance%=stride;
        }
        cooldown=.16f;
        Sound.Play("sounds/paintball/footstep.sound",ground.HitPosition);
        StepsPlayed++;
        if(nativeContact)NativeStepsPlayed++;
        if(ReviewContacts && animated is { } body && body.TryGetBoneTransform("LeftFoot",out var left) && body.TryGetBoneTransform("RightFoot",out var right))
            Log.Info($"FOOTSTEP_CONTACT clip={body.Sequence.Name} phase={body.Sequence.TimeNormalized} left_z={left.Position.z-WorldPosition.z} right_z={right.Position.z-WorldPosition.z} speed={speed}");
    }
}