PopRiotReview.cs
using Sandbox;
using Sandbox.UI;
using System;
namespace PaintballBaddies;

public static class PopRiotReview
{
    public static object Snapshot(Scene scene)
    {
        var s=MultiplayerSession.Find(scene);
        return new{Mode=s?.Mode.ToString(),ActiveMode=s?.ActiveMode.ToString(),s?.InRound,s?.TeamScore,s?.RiotWave,
            s?.RiotCombo,s?.RiotMultiplier,s?.RiotAlive,s?.RiotBreather,s?.RiotLives,s?.RiotWaveTotal,s?.RiotWaveCleared,s?.RiotResult,
            Targets=scene.GetAllComponents<PopRiotTarget>().Select(t=>new{t.Seed,Kind=t.Kind.ToString(),t.Health,t.Round,t.Popped,
                t.FlightWaypoints,t.WaypointIndex,t.Entering,t.Charging,t.ChargeRemaining,t.VolleyShots,t.Crashing,t.CrashSeconds,Target=t.AttackTarget?.GameObject.Name,
                Velocity=t.FlightVelocity.ToString(),ProjectileVelocity=t.GlobVelocity.ToString(),AttackAim=t.AttackAim.ToString(),Position=t.WorldPosition.ToString(),Anchor=t.Anchor.ToString(),Aim=t.AimPoint(Vector3.Zero).ToString()}).ToArray(),
            Actors=scene.GetAllComponents<PaintballCombatant>().Select(a=>new{a.GameObject.Name,Seat=a.Components.Get<NetworkPawn>()?.Seat ?? a.Components.Get<NetworkBot>()?.Seat,
                a.PopHits,a.PopScore,a.TargetsPopped,a.PaintHits,a.KnockdownsReceived,Task=a.Components.Get<ArenaOpponent>()?.PopRiotTask,
                Shots=a.Components.Get<ArenaOpponent>()?.ShotsFired,Travel=a.Components.Get<ArenaOpponent>()?.DistanceTravelled}).ToArray(),
            Labels=scene.GetAllComponents<PopRiotHud>().SelectMany(h=>h.Panel?.Descendants.OfType<Label>().Select(l=>l.Text) ?? Array.Empty<string>()).ToArray(),
            Bursts=scene.GetAllComponents<PopRiotBurst>().Count(),PopRiotBurst.SoundsPlayed};
    }
    public static string[] Rules(Scene scene)
    {
        var s=MultiplayerSession.Find(scene);
        if(!s.IsValid() || s.PublicLobby || !s.InRound || !s.IsPopRiot)return ["FAIL private Pop Riot round required"];
        var checks=new List<string>();void Check(bool ok,string msg)=>checks.Add((ok ? "PASS " : "FAIL ")+msg);
        var actors=scene.GetAllComponents<PaintballCombatant>().ToArray();var shooter=actors[0];var friend=actors.First(a=>a!=shooter);
        Check(!friend.RegisterHit(shooter.Team,attacker:shooter),"friendly body hits rejected");
        Check(!friend.RegisterKnockdown(shooter),"friendly knockdown score rejected");
        Check(shooter.Components.Get<CloseCombat>()?.CanContact(friend)!=true,"friendly contextual melee rejected");
        var point=new Vector3(0,5000,6200);
        var bubble=s.SpawnRiotTarget(point,PopKind.Bubble);int before=s.TeamScore;
        Check(bubble.IsValid() && bubble.Hit(point,shooter) && s.TeamScore>before,"one hit bursts a bubble and scores");
        int once=s.TeamScore;bubble.Pop(shooter,true);Check(s.TeamScore==once,"duplicate pop cannot award twice");
        var blob=s.SpawnRiotTarget(point+Vector3.Right*600,PopKind.Blob);
        Check(blob.Hit(blob.WorldPosition,shooter) && blob.Health==7,"first drone hit damages the airframe without popping");
        int small=s.RiotTargets.Count(t=>t.Kind==PopKind.Bubble);
        for(int i=0;i<6;i++)blob.Hit(blob.WorldPosition,shooter);
        Check(blob.Health==1 && !blob.Popped,"seven drone hits leave a damaged but flying target");
        Check(blob.Hit(blob.WorldPosition,shooter) && blob.Popped && blob.Crashing,"eighth drone hit starts loss-of-control crash");
        int crashScore=s.TeamScore;
        Check(blob.Hit(blob.WorldPosition,shooter) && s.TeamScore==crashScore,"one extra hit destroys the falling wreck without awarding duplicate points");
        Check(s.RiotTargets.Count(t=>t.Kind==PopKind.Bubble)==small,"disabled drone does not create child blobs");
        for(int i=0;i<6;i++)
        {
            var t=s.SpawnRiotTarget(point+Vector3.Forward*(1200+i*80),PopKind.Bubble);
            t.Hit(t.WorldPosition,shooter);
        }
        Check(s.RiotMultiplier>=2,"consecutive accurate hits build the team multiplier");
        Check(s.TeamScore==scene.GetAllComponents<PaintballCombatant>().Sum(a=>a.PopScore),"team score equals credited contributions");
        foreach(var t in s.RiotTargets.Where(t=>t.WorldPosition.z>5000).ToArray())t.GameObject.Destroy();
        Check(s.RiotLives==MultiplayerSession.RiotMaximumLives,"new round starts with five shared lives");
        s.LoseRiotLife(shooter);s.LoseRiotLife(friend);
        Check(s.RiotLives==3,"both humans and AI lose a shared life on an enemy hit");
        s.CompleteRiotWave();Check(s.RiotLives==4,"wave clear restores exactly one life");
        s.CompleteRiotWave();Check(s.RiotLives==4,"duplicate wave completion cannot heal repeatedly");
        for(int i=0;i<4;i++)s.LoseRiotLife(shooter);
        Check(s.RiotLives==0 && s.Remaining==0 && s.RiotResult.Contains("OUT OF LIVES"),"fifth unprotected team hit ends the round");
        Check(!s.LoseRiotLife(friend) && s.RiotLives==0,"ended round rejects further damage and clamps lives at zero");
        return checks.ToArray();
    }
}