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