SWB/swb_base/particles/BulletEjectParticle.cs

A particle component for ejected bullet shells. Applies a random sideways offset and an extra push along the player's movement direction at spawn, then in OnUpdate destroys the shell if it stops moving with no ground underneath.

using SWB.Shared;

namespace SWB.Base.Particles;

[Group( "SWB Particles" )]
public class BulletEjectParticle : Component
{
	public IPlayerBase Owner { get; set; }
	public float MoveDirection { get; set; } = 1f;

	protected override void OnStart()
	{
		if ( Owner is null ) return;
		var vel = Owner.Velocity;
		var rot = Owner.EyeAngles.ToRotation();
		var moveDir = rot.Right * MoveDirection;
		var lateralSpeed = vel.Dot( moveDir );
		var effect = GetComponent<ParticleEffect>();
		var rndOffset = Game.Random.Float(-10, 10);
		effect.ForceDirection += effect.WorldRotation.Left * rndOffset;

		// Only push shells further in their ejection direction when the player moves that way.
		if ( lateralSpeed > 0f )
		{
			var lateralForce = moveDir * lateralSpeed * 0.2f;

			if ( effect.ForceSpace == ParticleEffect.SimulationSpace.Local )
				lateralForce = effect.WorldRotation.Inverse * lateralForce;

			effect.ForceDirection += lateralForce;
		}
	}

	// Horizon RP : une douille ne reste jamais en l'air. Elle se posait parfois sur un collider de
	// passage (un joueur, une zone) qui s'en allait ensuite, et flottait figée jusqu'au bout de ses
	// 10 secondes (Julien, 31/10/2026). Immobile sans sol dessous : elle disparaît.
	Vector3 lastPosition;
	RealTimeSince sinceStill;
	RealTimeSince sinceStart;

	protected override void OnUpdate()
	{
		var effect = GetComponent<ParticleEffect>();
		if ( effect is null || effect.ParticleCount == 0 || sinceStart < 0.25f )
			return;
		var p = effect.Particles.First().Position;
		if ( p.Distance( lastPosition ) > 0.05f )
			sinceStill = 0;
		lastPosition = p;
		if ( sinceStill < 0.15f )
			return;
		var ground = Scene.Trace.Ray( p + Vector3.Up * 1f, p + Vector3.Down * 4f ).WithoutTags( "particles", "player", "weapon", "trigger" ).Run();
		if ( !ground.Hit )
			GameObject.Destroy();
	}
}