CoverChecks.cs
using Sandbox;

namespace PaintballBaddies;

/// <summary>Opt-in physical cover checks; exercises the same public controller used by input.</summary>
public sealed class CoverChecks : Component
{
    [Property] public string CandidateModelPath { get; set; } = "";
    private PlayerController player;
    private CoverController cover;
    private PaintballMarker weapon;
    private GameObject fixture;
    private GameObject obstruction;
    private float elapsed;
    private int phase;
    private Vector3 start;
    private void Check( string label, bool pass, string detail ) => Log.Info( $"PB_COVER {(pass ? "PASS" : "FAIL")} {label}: {detail}" );
    protected override void OnStart()
    {
        player = Components.Get<PlayerController>(); cover = Components.Get<CoverController>(); weapon = Components.Get<PaintballMarker>();
        player.UseInputControls = false; player.UseLookControls = false; weapon.AcceptInput = false;
        cover.TestInput = true;
        Components.Get<RosterSelection>().Select( 2 );
        if(!string.IsNullOrEmpty(CandidateModelPath))
        {
            Components.Get<RosterSelection>().Select(CoveredRosterAssets.CharacterIndex(CandidateModelPath));
            player.Renderer.Model=Model.Load(CandidateModelPath);
        }
        WorldPosition = new Vector3( -595, 70, 2 ); player.EyeAngles = new Angles( 0, 0, 0 );
        fixture = TrainingRange.Box( null, "Cover check fixture", new Vector3( -550, 70, 24 ), new Vector3( 20, 180, 48 ), Color.Gray, true );
        fixture.Components.Create<CoverSurface>();
    }
    protected override void OnFixedUpdate()
    {
        elapsed += Time.Delta;
        if ( phase == 0 && elapsed > 1 )
        {
            Check( "entry", cover.TryEnter(), $"position={WorldPosition}" ); phase++;
        }
        else if ( phase == 1 && elapsed > 2.5f )
        {
            Check( "physical attachment", cover.Attached && WorldPosition.x < -576 && WorldPosition.x > -585, $"position={WorldPosition}" );
            Check( "low cover posture", player.IsDucking && player.CurrentHeight < 45, $"height={player.CurrentHeight}" );
            Check( "protected fire blocked", !weapon.FireAt( new Vector3( 100, 70, 55 ) ), $"ammo={weapon.Ammo}" );
            start = WorldPosition; cover.TestMovement = Vector3.Right; phase++;
        }
        else if ( phase == 2 && elapsed > 3.2f )
        {
            Check( "slide along face", (WorldPosition-start).WithZ( 0 ).Length > 15 && cover.Attached, $"start={start} position={WorldPosition}" );
            Check( "directional cover animation", player.Renderer.Sequence.Name == "crouch_right", player.Renderer.Sequence.Name );
            cover.TestMovement = Vector3.Zero; cover.TestAim = true; phase++;
        }
        else if ( phase == 3 && elapsed > 4 )
        {
            Check( "low cover peek exposes body", cover.Peeking && !player.IsDucking && player.CurrentHeight > 60, $"height={player.CurrentHeight}" );
            cover.TestAim = false; phase++;
        }
        else if ( phase == 4 && elapsed > 4.7f )
        {
            Check( "release returns to protection", !cover.Peeking && player.IsDucking, $"height={player.CurrentHeight}" );
            cover.TestMovement = cover.Normal; phase++;
        }
        else if ( phase == 5 && elapsed > 5.3f )
        {
            Check( "movement cancels cover", !cover.Attached && player.DuckedHeight == 46, $"attached={cover.Attached} duckHeight={player.DuckedHeight}" );
            player.EyeAngles = new Angles( 0, 180, 0 );
            Check( "reject empty space", !cover.TryEnter(), $"attached={cover.Attached}" );
            fixture.Destroy();
            fixture = TrainingRange.Box( null, "High cover check fixture", new Vector3( -550, 70, 45 ), new Vector3( 20, 180, 90 ), Color.Gray, true );
            fixture.Components.Create<CoverSurface>();
            WorldPosition = new Vector3( -595, 70, 2 ); player.Body.Velocity = Vector3.Zero;
            player.EyeAngles = new Angles( 0, 0, 0 ); cover.TestMovement = Vector3.Zero; phase++;
        }
        else if ( phase == 6 && elapsed > 6 )
        {
            Check( "high cover entry", cover.TryEnter() && !cover.LowCover, $"position={WorldPosition}" );
            cover.TestMovement = Vector3.Right; phase++;
        }
        else if ( phase == 7 && elapsed > 8.5f )
        {
            Check( "edge stops protected slide", cover.Attached && WorldPosition.y > -5 && WorldPosition.y < 8 && cover.CornerAvailable, $"position={WorldPosition} corner={cover.CornerAvailable}" );
            start = WorldPosition; cover.TestMovement = Vector3.Zero; cover.TestAim = true; phase++;
        }
        else if ( phase == 8 && elapsed > 9.5f )
        {
            Check( "corner physically exposes body", cover.Peeking && WorldPosition.y < start.y - 38, $"start={start} position={WorldPosition}" );
            cover.TestAim = false; phase++;
        }
        else if ( phase == 9 && elapsed > 10.5f )
        {
            Check( "corner release returns behind cover", cover.Attached && !cover.Peeking && (WorldPosition-start).WithZ(0).Length < 2, $"position={WorldPosition}" );
            obstruction = TrainingRange.Box( null, "Blocked corner fixture", new Vector3( -580, -35, 35 ), new Vector3( 28, 25, 70 ), Color.Gray, true );
            phase++;
        }
        else if ( phase == 10 && elapsed > 11 )
        {
            cover.TestAim = true; phase++;
        }
        else if ( phase == 11 && elapsed > 12 )
        {
            Check( "blocked corner rejects peek", !cover.Peeking && !cover.CornerAvailable && WorldPosition.y > -5, $"position={WorldPosition} corner={cover.CornerAvailable}" );
            Check( "blocked corner cannot fire", !weapon.FireAt( new Vector3( 100, 0, 55 ) ), $"ammo={weapon.Ammo}" );
            obstruction.Destroy();
            cover.Leave(); fixture.Destroy(); cover.TestInput = false; player.UseInputControls = true; player.UseLookControls = true; weapon.AcceptInput = true;
            Components.Get<RosterSelection>().Select( 0 );
            Log.Info( "PB_COVER COMPLETE" ); Destroy();
        }
    }
}