MarinePaintWear.cs
using Sandbox;
using System;
using System.Collections.Generic;
namespace PaintballBaddies;

// A local, deterministic overlay on the original workshop map. No map files
// are rewritten. Existing venue textures are shared with Neon Foundry.
public sealed class MarinePaintWear : Component
{
    private GameObject wear;
    private readonly List<Texture> masks=new();
    private Texture dry;
    public int Marks {get;private set;}
    public int RemovedPickups {get;private set;}
    protected override void OnStart()
    {
        var map=Scene.GetAllComponents<MapInstance>().FirstOrDefault();
        if(map.IsValid())
        {
            foreach(var child in map.GameObject.Children.ToArray())
            {
                var n=child.Name.ToLowerInvariant();
                if(n.StartsWith("ammo_") || n.StartsWith("item_") || n.StartsWith("random weapon")
                    || n.StartsWith("random grenade") || n=="editor_camera")
                { child.Destroy();RemovedPickups++; }
            }
        }
        wear=new GameObject(true,"Marine Base - dried paint overlay"){NetworkMode=NetworkMode.Never};
        for(int i=1;i<=4;i++)
        {
            using var source=Bitmap.CreateFromBytes(FileSystem.Mounted.ReadAllBytes($"textures/venue_wear/splash_{i}.png").ToArray());
            using var resized=source.Resize(512,512);
            var pixels=resized.GetPixels();
            for(int p=0;p<pixels.Length;p++)pixels[p]=Color.White.WithAlpha(pixels[p].a);
            resized.SetPixels(pixels);masks.Add(resized.ToTexture());
        }
        using(var bitmap=new Bitmap(1,1)){bitmap.Clear(new Color(.94f,0,1,1));dry=bitmap.ToTexture();}
        var random=new Random(9182026);
        float R(float a,float b)=>a+(float)random.NextDouble()*(b-a);
        foreach(var point in ArenaMap.PatrolPoints(Scene))
        {
            // Lanes and firing positions have clusters, clear paths keep quieter wear.
            for(int i=0;i<8;i++)
            {
                var origin=point+Vector3.Up*R(26,96);
                var direction=Rotation.FromYaw(R(0,360)).Forward;
                var hit=Scene.Trace.Ray(origin,origin+direction*620).WithoutTags("paintball_actor","paintball_debris").Run();
                if(!hit.Hit || MathF.Abs(hit.Normal.z)>.55f)continue;
                var tangent=hit.Normal.Cross(Vector3.Up).Normal;
                for(int m=0;m<2;m++)
                {
                    var offset=tangent*R(-20,20)+Vector3.Up*R(-14,14);
                    Mark(hit.HitPosition+hit.Normal*12+offset,hit.HitPosition-hit.Normal*7+offset,R(22,48),R(0,360));
                }
            }
            for(int i=0;i<7;i++)
            {
                var p=point+new Vector3(R(-160,160),R(-160,160),36);
                Mark(p,p-Vector3.Up*90,R(26,64),R(0,360));
            }
        }
        Log.Info($"MARINE_PAINT_WEAR marks={Marks} removed_pickups={RemovedPickups}");
    }
    private void Mark(Vector3 from,Vector3 to,float size,float rotation)
    {
        if(Marks>=650)return;
        var hit=Scene.Trace.Ray(from,to).WithoutTags("paintball_actor","paintball_debris").Run();
        if(!hit.Hit)return;
        var go=new GameObject(wear){Name="Old paint impact",WorldPosition=hit.HitPosition+hit.Normal*.12f,WorldRotation=Rotation.LookAt(-hit.Normal)};
        var decal=go.Components.Create<Decal>();
        decal.Decals=new List<DecalDefinition>{new(){ColorTexture=masks[Marks%4],RoughMetalOcclusionTexture=dry,Width=1,Height=1,ColorMix=1}};
        var palette=new[]{new Color(.94f,.13f,.4f,.76f),new Color(.08f,.72f,.74f,.8f),new Color(.95f,.71f,.07f,.83f),new Color(.55f,.22f,.78f,.8f),new Color(.3f,.5f,.33f,.5f)};
        decal.ColorTint=palette[Marks++%palette.Length];decal.Size=new Vector2(size,size*(Marks%3==0 ? 1.25f : 1));
        decal.Depth=.65f;decal.Rotation=rotation;decal.AttenuationAngle=.4f;decal.LifeTime=0;decal.Transient=false;
    }
    protected override void OnUpdate(){if(wear.IsValid())wear.Enabled=MultiplayerSession.Find(Scene)?.IsOverdrive!=true;}
    protected override void OnDestroy(){wear?.Destroy();masks.Clear();}
}