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();
}
}