CitizenOpponentIntegrationReview.cs
using Sandbox;

namespace PaintballBaddies;

public sealed class CitizenOpponentIntegrationReview : Component
{
    [Property] public string CaptureId { get; set; } = "";
    [Property] public int CharacterIndex { get; set; }
    [Property] public bool CheckElimination { get; set; }
    [Property] public bool EliminateDuringReload { get; set; }
    [Property] public bool ReviewDropCamera { get; set; }
    [Property] public bool ReviewCarryCamera { get; set; }
    private float? actualEliminationAt;
    private float EliminationAt => actualEliminationAt ?? (EliminateDuringReload ? 2.5f : 5.5f);
    private float time;
    private int stage;
    private GameObject actor;
    private ArenaOpponent opponent;
    private CitizenOpponentPresentation presentation;
    private BoxCollider eliminationCollider;
    private bool consumed, reloadStarted;
    private int stepsBeforeFrozenClip;
    private PlayerController reviewPlayer;
    private bool previousCameraControl;
    private int contactSamples;
    private float maximumMarkerGap, totalMarkerGap;
    protected override void OnPreRender()
    {
        if(stage!=3 || !presentation.IsValid() || !opponent.IsValid())return;
        var body=presentation.FootstepRenderer;
        var marker=opponent.PresentationAnchor;
        if(!body.IsValid() || !marker.IsValid() || !body.TryGetBoneTransform("hold_R",out var hand))return;
        var expected=hand.Position+hand.Rotation*CitizenMarkerGrip.MarkerOffset;
        var gap=(marker.WorldPosition-expected).Length;
        contactSamples++;
        totalMarkerGap+=gap;
        maximumMarkerGap=System.MathF.Max(maximumMarkerGap,gap);
    }
    protected override void OnStart()
    {
        Scene.NavMesh.IsEnabled=true;
        Scene.NavMesh.IncludeStaticBodies=true;
        Scene.NavMesh.IncludeKeyframedBodies=false;
        Scene.NavMesh.AgentHeight=67;Scene.NavMesh.AgentRadius=17;
        Scene.NavMesh.SetDirty();
    }
    protected override void OnUpdate()
    {
        if(ReviewCarryCamera && actor.IsValid())
        {
            if(!reviewPlayer.IsValid())
            {
                reviewPlayer=Scene.GetAllComponents<PlayerController>().First();
                previousCameraControl=reviewPlayer.UseCameraControls;
            }
            reviewPlayer.UseCameraControls=false;
            WorldPosition=actor.WorldPosition+actor.WorldRotation*new Vector3(0,-100,45);
            WorldRotation=Rotation.LookAt(actor.WorldPosition+Vector3.Up*43-WorldPosition);
        }
        if(ReviewDropCamera && Scene.GetAllComponents<DroppedReloadPod>().FirstOrDefault() is {} dropped)
        {
            if(!reviewPlayer.IsValid())
            {
                reviewPlayer=Scene.GetAllComponents<PlayerController>().First();
                previousCameraControl=reviewPlayer.UseCameraControls;
            }
            reviewPlayer.UseCameraControls=false;
            WorldPosition=dropped.WorldPosition+new Vector3(28,-32,24);
            WorldRotation=Rotation.LookAt(dropped.WorldPosition+Vector3.Up*3-WorldPosition);
        }
        if(Scene.NavMesh.IsGenerating) return;
        time+=Time.Delta;
        if(stage==0 && time>.3f)
        {
            actor=new GameObject { Name="Citizen AI integration review" };
            actor.WorldPosition=new Vector3(-950,-500,2);
            // Exercise presentation startup before its AI dependency exists.
            presentation=actor.Components.Create<CitizenOpponentPresentation>();
            stage=1;
        }
        if(stage==1 && time>.6f)
        {
            opponent=actor.Components.Create<ArenaOpponent>();
            opponent.Character=RosterSelection.Names[CharacterIndex.Clamp(0,5)];opponent.CombatEnabled=true;stage=2;
        }
        if(stage==2 && time>1.3f && opponent.Body.IsValid() && presentation.IsReady)
        {
            // A stale hidden run clip must not suppress the visible Citizen's footsteps.
            opponent.Body.Sequence.Name="run";opponent.Body.PlaybackRate=0;
            stepsBeforeFrozenClip=opponent.Components.Get<MovementFootsteps>()?.StepsPlayed ?? 0;
            opponent.CombatEnabled=false;
            consumed=opponent.Magazine.TryFire();reloadStarted=opponent.Magazine.BeginReload();stage=3;
        }
        if(stage==3 && time>EliminationAt && (!EliminateDuringReload || presentation.PodHeld))
        {
            actualEliminationAt=time;
            Log.Info("CITIZEN_OPPONENT_INTEGRATION "+Json.Serialize(new {capture_id=CaptureId,
                phase="active",ready=presentation.IsReady,consumed,reload_started=reloadStarted,
                character=opponent.CharacterIndex,suit=presentation.EquippedSuit,
                ammo=opponent.Magazine.Ammo,reserve=opponent.Magazine.Reserve,reloads=opponent.Magazine.Reloads,
                pod_held=presentation.PodHeld,distance=opponent.DistanceTravelled,
                contact_samples=contactSamples,maximum_marker_gap=maximumMarkerGap,
                mean_marker_gap=contactSamples>0 ? totalMarkerGap/contactSamples : 0,
                animation_speed=presentation.AnimationSpeed,peak_animation_speed=presentation.PeakAnimationSpeed,
                footsteps=opponent.Components.Get<MovementFootsteps>()?.StepsPlayed ?? 0,
                native_contacts=opponent.Components.Get<MovementFootsteps>()?.NativeContactEvents ?? 0,
                native_steps=opponent.Components.Get<MovementFootsteps>()?.NativeStepsPlayed ?? 0,
                steps_after_frozen_clip=(opponent.Components.Get<MovementFootsteps>()?.StepsPlayed ?? 0)-stepsBeforeFrozenClip,
                anchor=opponent.PresentationAnchor.IsValid(),old_body_visible=opponent.Body.Enabled}));
            if(CheckElimination)
            {
                eliminationCollider=opponent.Components.Get<BoxCollider>();
                for(var hit=0;hit<3;hit++)opponent.Components.Get<PaintballCombatant>().RegisterHit(0);
                stage=6;
            }
            else { presentation.Destroy();stage=4; }
        }
        if(stage==6 && time>EliminationAt+.8f)
        {
            Log.Info("CITIZEN_OPPONENT_ELIMINATION "+Json.Serialize(new {capture_id=CaptureId,
                phase="signal",showing=presentation.ShowingOutSignal,weight=presentation.OutSignalWeight,
                hand_above_head=presentation.SignalHandAboveHead,
                dropped_pods=Scene.GetAllComponents<DroppedReloadPod>().Count(),
                pod_actor_filter=Scene.GetAllComponents<DroppedReloadPod>().All(x=>x.ActorFilterVerified),
                pod_shot_filter=Scene.GetAllComponents<DroppedReloadPod>().All(x=>x.ShotFilterVerified),
                pod_fall=Scene.GetAllComponents<DroppedReloadPod>().Select(x=>x.FallDistance).DefaultIfEmpty(0).Max(),
                collider_enabled=eliminationCollider.Enabled,shots=opponent.ShotsFired,
                pod_held=presentation.PodHeld,reload_props=presentation.ReloadPropsVisible,
                reload_remaining=opponent.Magazine.ReloadRemaining}));
            stage=7;
        }
        if(stage==7 && time>EliminationAt+1.8f)
        {
            Log.Info("CITIZEN_OPPONENT_ELIMINATION "+Json.Serialize(new {capture_id=CaptureId,
                phase="finished",showing=presentation.ShowingOutSignal,shots=opponent.ShotsFired}));
            presentation.Destroy();stage=4;
        }
        if(stage==4 && time>(EliminateDuringReload ? EliminationAt+5.4f : 6))
        {
            Log.Info("CITIZEN_OPPONENT_INTEGRATION "+Json.Serialize(new {capture_id=CaptureId,
                phase="removed",anchor=opponent.PresentationAnchor.IsValid(),body_restored=opponent.Body.Enabled,
                dropped_pods=Scene.GetAllComponents<DroppedReloadPod>().Count()}));stage=5;
        }
    }
    protected override void OnDestroy()
    {
        if(reviewPlayer.IsValid())reviewPlayer.UseCameraControls=previousCameraControl;
        if(actor.IsValid()) actor.Destroy();
    }
}