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

public sealed partial class PopRiotTarget
{
    private const string IndustrialPath="models/pop_riot/industrial/";
    private GameObject shell,launcher;
    private ModelRenderer skin;
    private SkinnedModelRenderer drone;
    private LineRenderer warningRing;
    private readonly List<ModelRenderer> liquid=new(),lamps=new(),glass=new(),shutters=new();
    private float shutterOpen=1;
    private bool wasCharging;
    private ModelRenderer Part(string name,GameObject parent,Vector3 position,float scale=1,Color? tint=null)
    {
        var go=new GameObject(parent,true,name){NetworkMode=NetworkMode.Never,LocalPosition=position,LocalScale=new Vector3(scale),LocalRotation=Rotation.FromYaw(-90)};
        var renderer=go.Components.Create<ModelRenderer>();renderer.Model=Model.Load(IndustrialPath+name+".vmdl");
        renderer.Tint=tint ?? Color.White;return renderer;
    }
    private void Capsule(GameObject parent,Vector3 position,float scale,int index,bool armored)
    {
        var assembly=new GameObject(parent,true,"Pressure chamber"){NetworkMode=NetworkMode.Never,LocalPosition=position,LocalScale=new Vector3(scale)};
        if(Kind==PopKind.Giant)assembly.LocalRotation=Rotation.FromYaw(index*120);
        Part("capsule_frame",assembly,Vector3.Zero);
        glass.Add(Part("capsule_glass",assembly,Vector3.Zero));
        liquid.Add(Part("capsule_paint",assembly,Vector3.Zero,1,PaintTint));
        lamps.Add(Part("capsule_light",assembly,Vector3.Zero,1,PaintTint));
        if(armored)shutters.Add(Part("shutter",assembly,Vector3.Zero));
        if(Kind==PopKind.Giant)
        {
            var hit=assembly.Components.Create<BoxCollider>();hit.Scale=new Vector3(25,25,34);hit.Static=false;
        }
    }
    private void BuildIndustrialVisuals()
    {
        shell=new GameObject(GameObject,true,"Industrial pressure target"){NetworkMode=NetworkMode.Never};
        if(Kind==PopKind.Glob)
        {
            skin=Sphere(shell,Vector3.Zero,Radius,PaintTint,false);
            Sphere(shell,Vector3.Forward*(Radius*.25f),Radius*.55f,PaintTint*1.35f,true);
            return;
        }
        if(Kind==PopKind.Giant)
        {
            Part("machine_base",shell,Vector3.Zero);
            for(int i=0;i<3;i++)Capsule(shell,SpotOffset(i),1.6f,i,true);
        }
        else if(Kind==PopKind.Blob)
        {
            var airframe=new GameObject(shell,true,"Rust quadcopter"){NetworkMode=NetworkMode.Never,LocalScale=new Vector3(.88f)};
            drone=airframe.Components.Create<SkinnedModelRenderer>();drone.Model=RiotDroneAssets.Drone;drone.UseAnimGraph=false;
            if(drone.Model.IsError)LoadAirframe();
            Capsule(shell,Vector3.Down*15,.65f,0,false);
            launcher=new GameObject(GameObject,true,"Drone paint launcher"){NetworkMode=NetworkMode.Never,LocalPosition=Vector3.Down*12};
            var barrel=Part("capsule_frame",launcher,Vector3.Forward*12,.48f);
            barrel.LocalRotation=Rotation.FromPitch(90)*Rotation.FromYaw(-90);
            var bore=Sphere(launcher,Vector3.Forward*20,4.8f,new Color(.018f,.02f,.022f),false);
            bore.GameObject.LocalScale=new Vector3(.012f,.15f,.15f);
            warningRing=ArenaGlow.Ring(GameObject,"Drone attack warning",43,1.4f,new Color(1,.38f,.055f),32);
            warningRing.LocalRotation=Rotation.FromPitch(90);warningRing.Enabled=false;
        }
        else
        {
            Capsule(shell,Vector3.Zero,Kind==PopKind.Chain ? .92f : .8f,0,false);
            if(Kind==PopKind.Chain)
            {
                // Twin cartridges distinguish chain reactors by shape as well as colour.
                Capsule(shell,new Vector3(0,-15,-2),.5f,1,false);
                Capsule(shell,new Vector3(0,15,-2),.5f,2,false);
            }
        }
    }
    private async void LoadAirframe()
    {
        var model=await RiotDroneAssets.LoadAsync();
        if(drone.IsValid() && model.IsValid() && !model.IsError)drone.Model=model;
    }
    protected override void OnPreRender()
    {
        if(!drone.IsValid())return;
        int i=0;
        foreach(var bone in drone.Model.Bones.AllBones)
        {
            if(!drone.TryGetBoneTransformAnimation(bone,out var pose))continue;
            var local=drone.WorldTransform.ToLocal(pose);
            local.Rotation*=Rotation.FromYaw(age*(Crashing ? 450 : 1800)*(i++%2==0 ? 1 : -1));
            drone.SetBoneTransform(bone,local);
        }
    }
    private void UpdateIndustrialVisuals()
    {
        if(!shell.IsValid())return;
        if(Charging && !wasCharging)DroneAudio.Play("charge",WorldPosition);
        wasCharging=Charging;
        if(Kind==PopKind.Glob)
        {
            shell.LocalRotation=Rotation.LookAt(GlobVelocity.Length>1 ? GlobVelocity.Normal : Vector3.Forward);
            shell.LocalScale=new Vector3(1.12f,.94f,.94f);return;
        }
        if(Kind==PopKind.Blob)
        {
            var facing=Charging ? AttackAim-WorldPosition : FlightVelocity;
            if(launcher.IsValid())
            {
                launcher.LocalRotation=Charging ? Rotation.LookAt(AttackAim-launcher.WorldPosition) : shell.LocalRotation;
                launcher.Enabled=!Crashing;
            }
            float yaw=facing.WithZ(0).Length>1 ? Rotation.LookAt(facing.WithZ(0)).Yaw() : shell.LocalRotation.Yaw();
            float damage=1-(float)Health/MaximumHealth;
            var bank=Rotation.FromRoll(MathF.Sin(age*1.6f+Seed)*4+MathF.Sin(age*9)*damage*13+MathF.Sin(age*31)*hitPulse*70);
            if(Crashing)bank=Rotation.FromRoll(18+CrashSeconds*280)*Rotation.FromPitch(CrashSeconds*110);
            shell.LocalRotation=Rotation.Lerp(shell.LocalRotation,Rotation.FromYaw(yaw)*bank,1-MathF.Exp(-6*Time.Delta));
            if(warningRing.IsValid())
            {
                warningRing.Enabled=!Crashing && (Charging || Entering);
                if(Scene.Camera.IsValid())warningRing.WorldRotation=Rotation.LookAt(Scene.Camera.WorldPosition-WorldPosition)*Rotation.FromPitch(90);
                warningRing.LocalScale=Vector3.One*(Charging ? .9f+.35f*ChargeRemaining/ChargeDuration : 1);
                var warning=(Entering ? new Color(.15f,.85f,.8f) : new Color(1,.25f,.04f))*1.5f;
                warningRing.Color=Gradient.FromColors(warning,warning);
            }
        }
        else if(Kind!=PopKind.Giant)shell.LocalRotation=Rotation.FromYaw(Seed*47+age*12)*Rotation.FromRoll(MathF.Sin(age*1.2f)*2);
        float goal=Kind==PopKind.Giant ? (WeakSpotsExposed ? 1 : 0) : (3-Health)*.38f;
        shutterOpen=MathX.Lerp(shutterOpen,goal,1-MathF.Exp(-12*Time.Delta));
        for(int i=0;i<liquid.Count;i++)
        {
            bool intact=Kind!=PopKind.Giant || ChamberHealth(i)>0;
            liquid[i].Enabled=glass[i].Enabled=intact;
            float pulse=Charging ? 1.7f+MathF.Sin(age*19)*.5f : 1;
            lamps[i].Tint=intact ? (Kind==PopKind.Blob && (Charging || Health<=2) ? new Color(1,.32f,.06f) : PaintTint)*pulse : new Color(.025f,.025f,.025f);
            liquid[i].Tint=Color.Lerp(PaintTint,Color.White,hitPulse*.9f);
            float fill=Kind==PopKind.Giant ? (float)ChamberHealth(i)/(MaximumHealth/3) : 1;
            liquid[i].GameObject.LocalScale=new Vector3(1,1,.5f+fill*.5f);
            liquid[i].GameObject.LocalPosition=Vector3.Down*(1-fill)*7;
        }
        foreach(var shutter in shutters)
        {
            shutter.GameObject.LocalRotation=Rotation.FromYaw(-90);
            shutter.GameObject.LocalPosition=Kind==PopKind.Giant ? Vector3.Down*(38*shutterOpen) : Vector3.Up*(5-22*shutterOpen);
        }
    }
}