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

// Enum values stay stable for the existing score and network effect paths.
public enum PopKind { Bubble, Blob, Chain, Giant, Glob }

public sealed partial class PopRiotTarget : Component
{
    [Sync(SyncFlags.FromHost)] public PopKind Kind {get;set;}
    [Sync(SyncFlags.FromHost)] public int Health {get;set;}
    [Sync(SyncFlags.FromHost)] public int Round {get;set;}
    [Sync(SyncFlags.FromHost)] public int Seed {get;set;}
    [Sync(SyncFlags.FromHost)] public bool Popped {get;private set;}
    [Sync(SyncFlags.FromHost)] public Vector3 Anchor {get;set;}
    [Sync(SyncFlags.FromHost)] public int ChamberA {get;private set;}
    [Sync(SyncFlags.FromHost)] public int ChamberB {get;private set;}
    [Sync(SyncFlags.FromHost)] public int ChamberC {get;private set;}
    public static float SizeFor(PopKind kind)=>kind switch {PopKind.Glob=>4.2f,PopKind.Blob=>38,PopKind.Giant=>84,PopKind.Chain=>25,_=>22};
    public float Radius=>SizeFor(Kind);
    public int MaximumHealth=>Kind switch {PopKind.Blob=>8,PopKind.Giant=>MultiplayerSession.Find(Scene)?.ActiveMinutes==1 ? 30 : 60,_=>1};
    public Color PaintTint=>Kind switch {PopKind.Chain=>new Color(1,.68f,.08f),PopKind.Giant=>new Color(.94f,.58f,.08f),PopKind.Blob=>new Color(.85f,.12f,.49f),_=>(Seed%3) switch {0=>new Color(.96f,.23f,.55f),1=>new Color(.08f,.7f,.87f),_=>new Color(.62f,.9f,.16f)}};
    public int ActiveChambers=>Kind==PopKind.Giant ? Enumerable.Range(0,3).Count(i=>ChamberHealth(i)>0) : 1;
    public bool CanBeDamaged=>Kind!=PopKind.Giant || WeakSpotsExposed;
    private float age,hitPulse;
    private int shownHealth=-1;
    internal bool ReviewStill {get;set;}
    public static PopRiotTarget Find(GameObject go)
    {
        while(go.IsValid()){var t=go.Components.Get<PopRiotTarget>();if(t.IsValid())return t;go=go.Parent;}return null;
    }
    internal void InitializeHealth()
    {
        Health=MaximumHealth;
        if(Kind==PopKind.Giant)ChamberA=ChamberB=ChamberC=Health/3;
    }
    public int ChamberHealth(int index)=>index switch {0=>ChamberA,1=>ChamberB,_=>ChamberC};
    private void DamageChamber(int index)
    {
        if(index==0)ChamberA=Math.Max(0,ChamberA-1);
        else if(index==1)ChamberB=Math.Max(0,ChamberB-1);
        else ChamberC=Math.Max(0,ChamberC-1);
        Health=ChamberA+ChamberB+ChamberC;
    }
    protected override void OnStart()
    {
        GameObject.Tags.Add("pop_target");
        if(Kind==PopKind.Giant)
        {
            var hull=Components.Create<SphereCollider>();hull.Radius=28;hull.Static=false;
            var baseHull=Components.Create<BoxCollider>();baseHull.Center=Vector3.Down*72;baseHull.Scale=new Vector3(118,118,20);baseHull.Static=false;
        }
        else {var hull=Components.Create<SphereCollider>();hull.Radius=Kind==PopKind.Glob ? GlobInterceptRadius : Radius;hull.Static=false;}
        BuildIndustrialVisuals();shownHealth=Health;SetupThreat();
    }
    internal static ModelRenderer Sphere(GameObject parent,Vector3 position,float radius,Color tint,bool glow)
    {
        var go=new GameObject(parent,true,"Paint liquid"){NetworkMode=NetworkMode.Never,LocalPosition=position,LocalScale=new Vector3(radius/32)};
        var model=go.Components.Create<ModelRenderer>();model.Model=Model.Load("models/dev/sphere.vmdl");
        model.MaterialOverride=Material.Load(glow ? "materials/dev/primary_white_emissive.vmat" : "models/pop_riot/industrial/paint.vmat");
        model.Tint=tint;return model;
    }
    private Vector3 SpotOffset(int i)=>Rotation.FromYaw(i*120).Forward*50+Vector3.Up*5;
    public Vector3 ChamberPoint(int i)=>WorldTransform.PointToWorld(SpotOffset(i));
    public Vector3 AimPoint(Vector3 from)=>Kind!=PopKind.Giant ? WorldPosition : Enumerable.Range(0,3)
        .Where(i=>ChamberHealth(i)>0).Select(ChamberPoint).OrderBy(p=>(p-from).Length).DefaultIfEmpty(WorldPosition).First();
    protected override void OnUpdate()
    {
        age+=Time.Delta;hitPulse=MathF.Max(0,hitPulse-Time.Delta);
        if(Health!=shownHealth){shownHealth=Health;hitPulse=.2f;}
        UpdateIndustrialVisuals();
        if(Crashing){UpdateCrash();return;}
        if(!MultiplayerSession.Authority || ReviewStill || Popped)return;
        var session=MultiplayerSession.Find(Scene);
        if(!session.IsValid() || !session.IsCooperative || !session.InRound || session.RoundId!=Round){GameObject.Destroy();return;}
        if(Kind==PopKind.Glob){AdvanceGlob(session);return;}
        AdvanceThreat(session);
        if(Kind==PopKind.Giant || session.RiotReviewPaused)return;
        AdvanceHover(session);
    }
    public bool Hit(Vector3 impact,PaintballCombatant shooter)
    {
        var session=MultiplayerSession.Find(Scene);
        if(!MultiplayerSession.Authority || (Popped && !Crashing) || !session.IsValid() || !session.InRound || !session.IsCooperative || session.RoundId!=Round || !shooter.IsValid() || !shooter.AcceptHits)return false;
        if(Crashing){FinishCrash(WorldPosition);return true;}
        if(Kind==PopKind.Giant)
        {
            if(!WeakSpotsExposed)return false;
            int index=Enumerable.Range(0,3).Where(i=>ChamberHealth(i)>0).FirstOrDefault(i=>
            {
                var delta=WorldTransform.PointToLocal(impact)-SpotOffset(i);
                return delta.WithZ(0).Length<23 && MathF.Abs(delta.z)<30;
            },-1);
            if(index<0)return false;
            DamageChamber(index);
            if(ChamberHealth(index)==0)
                NetworkEffects.RiotBurst(ChamberPoint(index),30,PaintTint,false,false,Round);
        }
        else Health=Math.Max(0,Health-1);
        session.RiotHit(shooter);
        if(Health==0)Pop(shooter,false);
        else
        {
            NetworkEffects.RiotCue("collect");
            if(Kind==PopKind.Blob)NetworkEffects.RiotDroneFlare(WorldPosition,PaintTint,false,Round,Seed);
        }
        return true;
    }
    internal void Pop(PaintballCombatant shooter,bool chained)
    {
        if(!MultiplayerSession.Authority || Popped)return;
        var session=MultiplayerSession.Find(Scene);
        if(!session.IsValid() || !session.InRound || !session.IsCooperative || Round!=session.RoundId)return;
        Popped=true;Health=0;
        session.RiotPop(this,shooter,chained);
        if(Kind==PopKind.Blob)BeginCrash();
        else GameObject.Destroy();
    }
}