PaintImpactChecks.cs
using Sandbox;
using System.Linq;
namespace PaintballBaddies;
/// <summary>Opt-in native decal checks and close camera preview.</summary>
public sealed class PaintImpactChecks : Component
{
[Property] public bool RepeatPreview { get; set; }
private float nextPreview;
[Property] public bool CheckLifetime { get; set; }
[Property] public bool FloorView { get; set; }
private bool ran, dropletsExpired;
private bool fadeChecked,expiryChecked;
private bool reuseSurvived;
private Decal lifetimeMark;
private float elapsed;
private void Check(string name,bool pass,string detail) => Log.Info($"PAINT_DECAL {(pass ? "PASS" : "FAIL")} {name}: {detail}");
protected override void OnUpdate()
{
var player=Scene.GetAllComponents<PlayerController>().First();
player.UseCameraControls=player.UseInputControls=player.UseLookControls=false;
var camera=Components.Get<CameraComponent>();
camera.WorldPosition=FloorView ? new Vector3(-463,-28,35) : new Vector3(-485,-52,78);
camera.WorldRotation=Rotation.LookAt((FloorView ? new Vector3(-440,-5,0) : new Vector3(-420,0,47))-camera.WorldPosition);
camera.FieldOfView=55;
elapsed+=Time.Delta;
if(ran && RepeatPreview && elapsed>nextPreview)
{
nextPreview=elapsed+.4f;
var previewHit=Scene.Trace.Ray(new Vector3(-450,0,55),new Vector3(-410,0,55)).Run();
PaintImpactSystem.Find(Scene).Spawn(previewHit,new Color(1,.12f,.6f));
}
if(ran && !RepeatPreview && elapsed>1.6f && !dropletsExpired)
{
var active=PaintImpactSystem.Find(Scene);
Check("droplets expire",active.ActiveDroplets==0,$"active={active.ActiveDroplets}");
dropletsExpired=true;
}
if(ran && CheckLifetime)
{
var active=PaintImpactSystem.Find(Scene);
if(elapsed>66 && !fadeChecked)
{
var mark=lifetimeMark;
float alpha=mark?.ColorTint.ConstantValue.a ?? -1;
Log.Info($"PAINT_LIFETIME {(alpha>0 && alpha<1 ? "PASS" : "FAIL")} fades before expiry; alpha={alpha}");
fadeChecked=true;
var floorHit=Scene.Trace.Ray(new Vector3(-440,-5,15),new Vector3(-440,-5,-3)).Run();
while(active.ActiveCount<PaintImpactSystem.Capacity)active.Spawn(floorHit,Color.Cyan);
active.Spawn(floorHit,Color.Cyan);
Log.Info($"PAINT_LIFETIME {(mark.IsValid() && mark.ColorTint.ConstantValue.a>.99f && mark.GameObject.Parent==floorHit.GameObject ? "PASS" : "FAIL")} recycling restores full opacity on new surface");
}
if(elapsed>78 && !reuseSurvived)
{
Log.Info($"PAINT_LIFETIME {(lifetimeMark.IsValid() && lifetimeMark.ColorTint.ConstantValue.a>.99f && active.ActiveCount==91 ? "PASS" : "FAIL")} reused mark survives original expiry; queued={active.ActiveCount}");
reuseSurvived=true;
}
if(elapsed>144 && !expiryChecked)
{
int objects=Scene.GetAllComponents<Decal>().Count(d=>d.GameObject.Name=="Paint splash decal");
Log.Info($"PAINT_LIFETIME {(active.ActiveCount==0 && objects==0 ? "PASS" : "FAIL")} removes expired decals; queued={active.ActiveCount}, objects={objects}");
Log.Info("PAINT_LIFETIME COMPLETE");expiryChecked=true;
}
}
if(ran || elapsed<1)return;
ran=true;
var system=PaintImpactSystem.Find(Scene);
var hit=Scene.Trace.Ray(new Vector3(-450,0,55),new Vector3(-410,0,55)).Run();
Check("hits native target",hit.Hit && hit.GameObject.Components.Get<PaintballTarget>() is not null,$"hit={hit.Hit}");
system.Clear();system.Spawn(hit,new Color(1,.12f,.6f));
Check("impact emits six droplets",system.ActiveDroplets==6,$"active={system.ActiveDroplets}");
Check("spawns one decal",system.ActiveCount==1,$"active={system.ActiveCount}");
var decal=Scene.GetAllComponents<Decal>().First(d=>d.GameObject.Name=="Paint splash decal");
var texture=decal.Decals[0].ColorTexture;
Check("shared mask texture valid",texture.IsValid && !texture.IsError && texture.Width==512,$"size={texture.Size}");
Check("mask background transparent",texture.GetPixel(0,0).a<5,$"alpha={texture.GetPixel(0,0).a}");
Check("mask centre opaque",texture.GetPixel(256,256).a>240,$"alpha={texture.GetPixel(256,256).a}");
for(int i=0;i<PaintImpactSystem.Capacity-1;i++)system.Spawn(hit,Color.Cyan);
var recycledFloor=Scene.Trace.Ray(new Vector3(-440,-5,15),new Vector3(-440,-5,-3)).Run();
system.Spawn(recycledFloor,Color.Cyan);
Check("oldest native decal reused",decal.IsValid() && Scene.GetAllComponents<Decal>().Count(d=>d.GameObject.Name=="Paint splash decal")==PaintImpactSystem.Capacity,"original component remains alive at capacity");
Check("recycled decal moves to new surface",recycledFloor.Hit && decal.IsValid() && decal.GameObject.Parent==recycledFloor.GameObject
&& (decal.WorldPosition-(recycledFloor.HitPosition+recycledFloor.Normal*.35f)).Length<.01f,"target decal reparented to floor");
for(int i=0;i<14;i++)system.Spawn(hit,Color.Cyan);
Check("scene cap enforced",system.ActiveCount==PaintImpactSystem.Capacity,$"active={system.ActiveCount}");
Check("droplet pool bounded",system.ActiveDroplets==PaintImpactSystem.DropletCapacity,$"active={system.ActiveDroplets}");
system.Clear();Check("reset removes droplets",system.ActiveDroplets==0,$"active={system.ActiveDroplets}");
Check("reset removes marks",system.ActiveCount==0,$"active={system.ActiveCount}");
for(int i=0;i<5;i++)
{
var point=new Vector3(-420,(i-2)*5,48+(i%2)*13);
var sample=Scene.Trace.Ray(point+Vector3.Backward*30,point+Vector3.Forward*10).Run();
system.Spawn(sample,i%2==0 ? new Color(1,.12f,.6f) : new Color(.05f,.9f,.85f));
// Scene enumeration may still include objects queued for destruction
// by Clear this frame. Observe the actual live queue instead.
if(i==0)lifetimeMark=system.OldestDecal;
}
var floor=Scene.Trace.Ray(new Vector3(-440,-5,15),new Vector3(-440,-5,-3)).Run();
system.Spawn(floor,new Color(1,.12f,.6f));
Check("wall and floor preview marks",system.ActiveCount==6,$"active={system.ActiveCount}");
Log.Info("PAINT_DECAL COMPLETE");
}
}