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

/// <summary>Shared local material finish for both arenas; native decals, bounded and deterministic.</summary>
public sealed class ArenaSurfaceFinish : Component
{
    public int WetMarks {get;private set;}
    public ArenaSurfaceMaterials SurfaceMaterials {get;private set;}
    private GameObject overlay;
    private Texture roughness;
    private float ready;
    private bool building;
    private bool appliedOverdrive;
    protected override void OnUpdate()
    {
        if(ArenaSelection.Current(Scene)==ArenaSelection.Restaurant)return;
        bool overdrive=MultiplayerSession.Find(Scene)?.IsOverdrive==true;
        if(overlay.IsValid())overlay.Enabled=!overdrive;
        if(SurfaceMaterials is not null && appliedOverdrive!=overdrive)RefreshMaterials();
        if(overlay.IsValid() || building)return;
        ready+=Time.Delta;if(ready<1.5f || Scene.GetAllComponents<MapInstance>().Any(m=>m.Active && !m.IsLoaded))return;
        Build();
    }
    public void RefreshMaterials(){SurfaceMaterials?.Restore();SurfaceMaterials=new ArenaSurfaceMaterials();SurfaceMaterials.Apply(Scene);appliedOverdrive=MultiplayerSession.Find(Scene)?.IsOverdrive==true;}
    private void Build()
    {
        building=true;
        // All matched finish textures now ship under the portable project.
        // No extra cloud download is needed before the floor can be rendered.
        if(!this.IsValid())return;
        SurfaceMaterials=new ArenaSurfaceMaterials();
        SurfaceMaterials.Apply(Scene);
        appliedOverdrive=MultiplayerSession.Find(Scene)?.IsOverdrive==true;
        overlay=new GameObject(GameObject,true,"Fresh glossy arena paint"){NetworkMode=NetworkMode.Never};
        overlay.Enabled=!appliedOverdrive;
        using(var b=new Bitmap(1,1)){b.Clear(new Color(.19f,0,1,1));roughness=b.ToTexture();}
        var points=ArenaMap.PatrolPoints(Scene);
        if(points.Length==0)points=ArenaOpponent.CreatePatrolRoute();
        var random=new Random(9192026);
        float R(float a,float b)=>a+(float)random.NextDouble()*(b-a);
        var colors=new[]{new Color(.08f,.52f,.6f),new Color(.65f,.055f,.26f),new Color(.83f,.48f,.045f)};
        foreach(var p in points)
        {
            for(int i=0;i<3 && WetMarks<96;i++)
            {
                var from=p+new Vector3(R(-115,115),R(-115,115),50);
                var hit=Scene.Trace.Ray(from,from-Vector3.Up*110).WithoutTags("paintball_actor","paintball_debris","pop_target").Run();
                if(!hit.Hit || hit.Normal.z<.8f)continue;
                var go=new GameObject(overlay,true,"Recent paint pool")
                {LocalScale=Vector3.One,WorldPosition=hit.HitPosition+hit.Normal*.2f,WorldRotation=Rotation.LookAt(-hit.Normal)};
                var decal=go.Components.Create<Decal>();
                decal.Decals=[new(){ColorTexture=PaintImpactSystem.SplashTexture,RoughMetalOcclusionTexture=roughness,Width=1,Height=1,ColorMix=1}];
                float size=R(32,66);decal.Size=new Vector2(size,size*R(.75f,1.25f));
                decal.Depth=.7f;decal.Rotation=R(0,360);decal.ColorTint=colors[WetMarks++%colors.Length];
                decal.AttenuationAngle=.4f;decal.LifeTime=0;decal.Transient=false;
            }
        }
        Log.Info($"ARENA_SURFACE_FINISH arena={ArenaMap.DisplayTitle(Scene)} wet_marks={WetMarks} reflections=SSR");
    }
    protected override void OnDestroy(){SurfaceMaterials?.Restore();overlay?.Destroy();roughness?.Dispose();}
}