CitizenPlayerIntegrationReview.cs
using Sandbox;

namespace PaintballBaddies;

/// <summary>Opt-in lifecycle/weapon fixture; never saved in the playable scene.</summary>
public sealed class CitizenPlayerIntegrationReview : Component
{
    [Property] public string CaptureId { get; set; } = "";
    [Property] public int CharacterIndex { get; set; }
    [Property] public bool ExerciseVault { get; set; }
    [Property] public bool ExerciseHighCover { get; set; }
    [Property] public bool ExerciseSandbags { get; set; }
    [Property] public bool ExerciseSnake { get; set; }
    private SnakeBunker snake;
    private SandbagBunker sandbags;
    private float elapsed;
    private int stage;
    private PlayerController player;
    private PaintballMarker weapon;
    private CitizenPlayerPresentation presentation;
    private bool originalVisible, shot;
    private GameObject coverObject;
    private CoverController cover;
    private VaultController vault;
    private bool entered, started;
    private int contactSamples;
    private float maximumMarkerGap;
    private ArenaMatch match;
    private bool timedContest;
    protected override void OnPreRender()
    {
        if(!presentation.IsValid() || !weapon.IsValid() || !weapon.PresentationAnchor.IsValid())return;
        var body=presentation.FootstepRenderer;
        if(!body.IsValid() || !body.TryGetBoneTransform("hold_R",out var hand))return;
        var expected=hand.Position+hand.Rotation*CitizenMarkerGrip.MarkerOffset;
        maximumMarkerGap=System.MathF.Max(maximumMarkerGap,(weapon.PresentationAnchor.WorldPosition-expected).Length);
        contactSamples++;
    }
    protected override void OnUpdate()
    {
        elapsed+=Time.Delta;
        if(stage==0 && elapsed>.3f)
        {
            player=Scene.GetAllComponents<PlayerController>().First();
            match=player.Components.Get<ArenaMatch>();
            timedContest=match.TimedContest;
            match.TimedContest=false; // Exercise the retained reload animation without changing playable rules.
            player.Components.Get<RosterSelection>().Select(CharacterIndex.Clamp(0,5));
            weapon=player.Components.Get<PaintballMarker>();
            originalVisible=player.Renderer.Enabled;
            presentation=player.Components.Create<CitizenPlayerPresentation>();
            stage=1;
        }
        if(stage==1 && elapsed>1)
        {
            shot=weapon.FireAt(weapon.Muzzle+player.EyeAngles.Forward*1000);
            weapon.Reload();stage=2;
        }
        if(stage==2 && elapsed>5.5f)
        {
            Log.Info("CITIZEN_PLAYER_INTEGRATION "+Json.Serialize(new {capture_id=CaptureId,
                phase="active",ready=presentation.IsReady,shot,ammo=weapon.Ammo,reserve=weapon.Reserve,
                pod_held=presentation.PodHeld,anchor=weapon.PresentationAnchor.IsValid(),
                input=player.UseInputControls,look=player.UseLookControls,camera=player.UseCameraControls,
                old_body_visible=player.Renderer.Enabled}));
            if (ExerciseVault || ExerciseHighCover)
            {
                cover=player.Components.Get<CoverController>();
                vault=player.Components.Get<VaultController>();
                player.WorldPosition=new Vector3(-595,ExerciseHighCover ? 20 : 70,2);
                player.EyeAngles=new Angles(0,0,0);
                if(ExerciseSnake)
                {
                    snake=Components.Create<SnakeBunker>();snake.Position=new Vector3(-550,70,0);
                }
                else if(ExerciseSandbags)
                {
                    sandbags=Components.Create<SandbagBunker>();sandbags.Position=new Vector3(-550,70,0);
                }
                else
                {
                    coverObject=TrainingRange.Box(null,"Player integration cover",new Vector3(-550,70,ExerciseHighCover ? 45 : 24),new Vector3(20,180,ExerciseHighCover ? 90 : 48),Color.Gray,true);
                    coverObject.Components.Create<CoverSurface>();
                }
                cover.TestInput=true;stage=ExerciseHighCover ? 20 : 10;
            }
            else { presentation.Destroy();stage=3; }
        }
        if(stage==10 && elapsed>6.3f) { entered=cover.TryEnter();stage=11; }
        if(stage==20 && elapsed>6.3f) { entered=cover.TryEnter();stage=21; }
        if(stage==21 && elapsed>6.5f)
        {
            var protectedShot=weapon.FireAt(weapon.Muzzle+Vector3.Forward*1000);
            Log.Info("CITIZEN_PLAYER_INTEGRATION "+Json.Serialize(new {capture_id=CaptureId,
                phase="sheltered",entered,attached=cover.Attached,peeking=cover.Peeking,shot=protectedShot}));
            cover.TestMovement=Vector3.Right;stage=22;
        }
        if(stage==22 && elapsed>7.8f)
        {
            cover.TestMovement=Vector3.Zero;cover.TestAim=true;weapon.ReviewAimOverride=true;stage=23;
        }
        if(stage==23 && elapsed>9)
        {
            var before=weapon.ActivePaintballs;
            var peekShot=weapon.FireAt(weapon.Muzzle+Vector3.Forward*1000);
            Log.Info("CITIZEN_PLAYER_INTEGRATION "+Json.Serialize(new {capture_id=CaptureId,
                phase="peek",attached=cover.Attached,peeking=cover.Peeking,shot=peekShot,
                corner_available=cover.CornerAvailable,position=player.WorldPosition,
                projectile=weapon.ActivePaintballs>before,ready=presentation.IsReady,
                input=player.UseInputControls,look=player.UseLookControls,camera=player.UseCameraControls}));stage=24;
        }
        if(stage==24 && elapsed>10)
        {
            cover.TestAim=false;weapon.ReviewAimOverride=null;cover.Leave();cover.TestInput=false;
            if(coverObject.IsValid())coverObject.Destroy();
            if(sandbags.IsValid())sandbags.Destroy();
            if(snake.IsValid())snake.Destroy();
            presentation.Destroy();stage=3;
        }
        if(stage==11 && elapsed>7)
        {
            started=vault.TryBegin();stage=12;
        }
        if(stage==12 && elapsed>7.4f)
        {
            Log.Info("CITIZEN_PLAYER_INTEGRATION "+Json.Serialize(new {capture_id=CaptureId,
                phase="vault",entered,started,vaulting=vault.IsVaulting,progress=vault.Progress,
                ready=presentation.IsReady,anchor=weapon.PresentationAnchor.IsValid()}));stage=13;
        }
        if(stage==13 && elapsed>8.5f)
        {
            Log.Info("CITIZEN_PLAYER_INTEGRATION "+Json.Serialize(new {capture_id=CaptureId,
                phase="landed",vaulting=vault.IsVaulting,outcome=vault.Outcome,
                input=player.UseInputControls,look=player.UseLookControls,camera=player.UseCameraControls,
                ready=presentation.IsReady,pod_held=presentation.PodHeld}));
            cover.Leave();cover.TestInput=false;
            if(coverObject.IsValid())coverObject.Destroy();
            if(sandbags.IsValid())sandbags.Destroy();
            if(snake.IsValid())snake.Destroy();
            presentation.Destroy();stage=3;
        }
        if(stage==3 && elapsed>(ExerciseHighCover ? 10.5f : ExerciseVault ? 9 : 6))
        {
            Log.Info("CITIZEN_PLAYER_INTEGRATION "+Json.Serialize(new {capture_id=CaptureId,
                phase="removed",anchor=weapon.PresentationAnchor.IsValid(),
                character=CharacterIndex,contact_samples=contactSamples,maximum_marker_gap=maximumMarkerGap,
                body_restored=player.Renderer.Enabled==originalVisible,
                input=player.UseInputControls,look=player.UseLookControls,camera=player.UseCameraControls}));
            stage=4;
        }
    }
    protected override void OnDestroy()
    {
        if(match.IsValid())match.TimedContest=timedContest;
        if(presentation.IsValid()) presentation.Destroy();
        if(cover.IsValid()) { cover.Leave();cover.TestInput=false;cover.TestAim=false;cover.TestMovement=Vector3.Zero; }
        if(weapon.IsValid()) weapon.ReviewAimOverride=null;
        if(coverObject.IsValid()) coverObject.Destroy();
        if(sandbags.IsValid()) sandbags.Destroy();
        if(snake.IsValid()) snake.Destroy();
    }
}