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

// Temporary private-match fixture. Never added to a saved scene.
public sealed class PopRiotBallisticsReview : Component
{
    public List<string> Checks {get;}=new();
    public bool Done {get;private set;}
    private MultiplayerSession session;
    private PaintballCombatant shooter;
    private PopRiotTarget target;
    private GameObject wall,floor;
    private PlayerController player;
    private Vector3 savedPosition;
    private Angles savedAngles;
    private bool savedInput;
    private int lifeBefore,paintBefore,shieldBefore;
    private Vector3 crashBefore;
    private int reactionBefore;
    private ArenaShield shield;
    private Vector3 point=new(0,5000,6200);
    private float clock;
    private int stage,before,combo;
    private void Check(bool value,string message)=>Checks.Add((value ? "PASS " : "FAIL ")+message);
    protected override void OnStart()
    {
        session=MultiplayerSession.Find(Scene);shooter=MultiplayerSession.LocalPlayer(Scene)?.Components.Get<PaintballCombatant>();
        if(!session.IsValid() || session.PublicLobby || !Networking.IsHost || !session.IsPopRiot || !session.InRound || !shooter.IsValid())
        {Check(false,"private cooperative host required");Done=true;return;}
        player=MultiplayerSession.LocalPlayer(Scene);savedPosition=player.WorldPosition;savedAngles=player.EyeAngles;savedInput=player.UseInputControls;
        floor=new GameObject(true,"Temporary drone physics platform"){WorldPosition=point+new Vector3(0,600,-210)};
        var collider=floor.Components.Create<BoxCollider>();collider.Scale=new Vector3(800,800,20);collider.Static=true;
        player.WorldPosition=floor.WorldPosition+Vector3.Up*12;player.EyeAngles=Angles.Zero;player.UseInputControls=false;
        target=session.SpawnRiotTarget(point,PopKind.Bubble);target.ReviewStill=true;
    }
    protected override void OnUpdate()
    {
        if(Done)return;
        // Keep the opt-in fixture stationary even if live keyboard input remains held.
        player.WorldPosition=floor.WorldPosition+Vector3.Up*12;player.EyeAngles=Angles.Zero;
        player.UseInputControls=false;if(player.Body.IsValid())player.Body.Velocity=Vector3.Zero;
        Scene.GetAllComponents<PaintballControls>().FirstOrDefault()?.ResumeGame();
        clock+=Time.Delta;if(clock<.65f)return;clock=0;
        if(stage==0 || stage==2)
        {
            before=session.TeamScore;combo=session.RiotCombo;
            var from=point-Vector3.Forward*300;
            NetworkEffects.SpawnBall(shooter.GameObject,shooter.Team,from,PaintballFlight.LaunchVelocity(from,point));
        }
        if(stage==1)
        {
            Check(target.Popped && session.TeamScore>before,"physical paintball bursts bubble and awards score");
            Check(session.RiotCombo==combo+1,"successful physical pop does not also count as a miss");
            target=session.SpawnRiotTarget(point,PopKind.Glob);target.ReviewStill=true;
        }
        if(stage==3)
        {
            Check(target.Popped && session.TeamScore>before,"physical paintball intercepts oversized enemy glob");
            wall=new GameObject(true,"Temporary glob cover collider"){WorldPosition=point};
            wall.Components.Create<BoxCollider>().Scale=new Vector3(30,160,160);
            target=session.SpawnRiotTarget(point-Vector3.Forward*150,PopKind.Glob);target.GlobVelocity=Vector3.Forward*320;
            before=session.TeamScore;
        }
        if(stage==4)
        {
            Check(!target.IsValid() && session.TeamScore==before,"solid cover stops hostile glob without awarding a pop");
            wall.Destroy();
            reactionBefore=shooter.Components.Get<PaintHitReaction>()?.Impulses ?? 0;
            int paintHits=shooter.PaintHits;int lives=session.RiotLives;shooter.ReceiveRiotGlob(shooter.WorldPosition+Vector3.Right*150);
            Check(shooter.HitMoveScale<1 && shooter.DirectionIndicatorRemaining>0 && shooter.PaintHits==paintHits && session.RiotLives==lives-1,"hostile splash removes one shared life, gives direction cue and no PvP penalty");
            shooter.ReceiveRiotGlob(shooter.WorldPosition+Vector3.Left*150);
            Check(session.RiotLives==lives-1,"brief hit grace prevents stacked same-frame life loss");
            var friend=Scene.GetAllComponents<PaintballCombatant>().First(a=>a!=shooter);
            var through=PaintballFlight.Trace(Scene,friend.EyePosition-Vector3.Forward*8,friend.EyePosition+Vector3.Forward*8,shooter.GameObject);
            Check(PaintballCombatant.Find(through.GameObject)!=friend,"cooperative paintballs pass through teammate collision");
        }
        if(stage==5)
        {
            Check(shooter.Components.Get<PaintHitReaction>()?.Impulses==reactionBefore+1,"confirmed owner hit plays one native camera impulse");
            Check(shooter.Components.Get<PaintHitReaction>()?.Weight>0 && Scene.TimeScale==1,"personal hit grading is active without slowing shared time");
        }
        if(stage==6)Check(shooter.HitMoveScale==1,"movement fully recovers after splash");
        if(stage==7 || stage==10 || stage==12)
        {
            lifeBefore=session.RiotLives;paintBefore=shooter.Components.Get<CharacterPaint>()?.Count ?? 0;
            var direction=stage==12 ? Vector3.Backward : Vector3.Forward;
            var destination=player.WorldPosition+Vector3.Up*45;
            target=session.SpawnRiotTarget(destination+direction*180,PopKind.Glob);
            target.GlobVelocity=-direction*420;
        }
        if(stage==8)
        {
            Check(session.RiotLives==lifeBefore-1,"physical enemy projectile takes one shared life from a human");
            Check(shooter.Components.Get<CharacterPaint>()?.Count>paintBefore,"physical enemy projectile paints the fitted suit");
        }
        if(stage==9)
        {
            shield=Scene.GetAllComponents<ArenaShield>().FirstOrDefault(s=>!s.Owner.IsValid() && s.RespawnDelay<=0);
            Check(shield.IsValid() && shield.Collect(shooter),"collect a shield for actual incoming-projectile test");
            shieldBefore=shield?.Remaining ?? 0;
        }
        if(stage==11)
        {
            Check(session.RiotLives==lifeBefore && shield?.Remaining==shieldBefore-1,"frontal shield hit damages shield without losing a team life");
            Check(shield?.PaintCount>0,"incoming drone paint sticks to shield");
        }
        if(stage==13)
        {
            Check(session.RiotLives==lifeBefore-1,"rear hit bypasses frontal shield and costs a life");
        }
        if(stage==14)
        {
            target=session.SpawnRiotTarget(point,PopKind.Blob);target.ReviewStill=true;
            for(int i=0;i<8;i++)target.Hit(target.WorldPosition,shooter);
            crashBefore=target.WorldPosition;before=session.TeamScore;
        }
        if(stage==15)
        {
            Check(target.IsValid() && target.Crashing && (target.WorldPosition-crashBefore).Length>20,"disabled drone spirals and falls instead of freezing");
            var aim=target.WorldPosition+target.CrashVelocity*.09f;
            var from=aim-Vector3.Forward*160;
            NetworkEffects.SpawnBall(shooter.GameObject,shooter.Team,from,PaintballFlight.LaunchVelocity(from,aim));
        }
        if(stage==16)
        {
            Check(!target.IsValid() && session.TeamScore==before,"physical extra shot clears falling wreck without duplicate score");
            lifeBefore=session.RiotLives;shooter.ReceiveRiotCrash(shooter.WorldPosition+Vector3.Right*50);
            Check(shooter.HitMoveScale<.6f && session.RiotLives==lifeBefore,"wreck contact gives personal slowdown without taking an extra team life");
        }
        if(stage==18)
        {
            Check(shooter.HitMoveScale==1 && shooter.Components.Get<PaintHitReaction>()?.Weight==0 && Scene.TimeScale==1,"hit presentation and movement fully restore");
        }
        if(stage is 19 or 21 or 23)
        {
            float speed=stage==19 ? 180 : stage==21 ? 360 : 480;
            target=session.SpawnRiotTarget(point,PopKind.Glob);target.GlobVelocity=Vector3.Forward*speed;
            var from=point-Vector3.Right*300;
            var aim=point+target.GlobVelocity*(300/PaintballFlight.Speed);
            NetworkEffects.SpawnBall(shooter.GameObject,shooter.Team,from,PaintballFlight.LaunchVelocity(from,aim));
        }
        if(stage is 20 or 22 or 24)
            Check(!target.IsValid() || target.Popped,$"physical crossing-shot interception at {(stage==20 ? 180 : stage==22 ? 360 : 480)} units/second");
        if(stage==24)Done=true;
        stage++;
    }
    protected override void OnDestroy()
    {
        if(player.IsValid()){player.WorldPosition=savedPosition;player.EyeAngles=savedAngles;player.UseInputControls=savedInput;if(player.Body.IsValid())player.Body.Velocity=Vector3.Zero;}
        if(floor.IsValid())floor.Destroy();
        if(wall.IsValid())wall.Destroy();
        if(target.IsValid())target.GameObject.Destroy();
    }
}