Effects/ParticleLineEmitter.cs
/// <summary>
/// Emits particles along a line between two points.
/// Points can be defined as world positions or tracked GameObjects.
/// </summary>
[Title( "Line Emitter" )]
[Category( "Particles" )]
[Icon( "show_chart" )]
public sealed class ParticleLineEmitter : ParticleEmitter
{
	public enum LineVelocityMode
	{
		/// <summary>No extra velocity applied.</summary>
		None,
		/// <summary>Velocity shoots outward from the line (radial).</summary>
		Radial,
		/// <summary>Velocity along the line direction (start → end).</summary>
		Along,
		/// <summary>Velocity along the line normal (perpendicular in the XY plane).</summary>
		Normal,
	}

	[Header( "End Points" )]
	/// <summary>If set, overrides StartPosition each frame.</summary>
	[Property] public GameObject StartObject { get; set; }
	/// <summary>If set, overrides EndPosition each frame.</summary>
	[Property] public GameObject EndObject { get; set; }
	[Property] public Vector3 StartPosition { get; set; } = Vector3.Zero;
	[Property] public Vector3 EndPosition { get; set; } = new Vector3( 100, 0, 0 );

	[Header( "Emission" )]
	/// <summary>Spread radius around each emitted point perpendicular to the line.</summary>
	[Property, Range( 0, 200 )] public float Radius { get; set; } = 0f;

	[Header( "Velocity" )]
	[Property] public LineVelocityMode VelocityMode { get; set; } = LineVelocityMode.None;
	[Property, Range( -2000, 2000 )] public float Velocity { get; set; } = 100f;

	protected override void DrawGizmos()
	{
		if ( !Gizmo.IsSelected ) return;

		var start = StartObject.IsValid() ? StartObject.WorldPosition : WorldTransform.PointToWorld( StartPosition );
		var end = EndObject.IsValid() ? EndObject.WorldPosition : WorldTransform.PointToWorld( EndPosition );

		Gizmo.Draw.Color = Color.Yellow.WithAlpha( 0.8f );
		Gizmo.Draw.Line( WorldTransform.PointToLocal( start ), WorldTransform.PointToLocal( end ) );

		if ( Radius > 0f )
		{
			Gizmo.Draw.Color = Color.Yellow.WithAlpha( 0.15f );
			var mid = WorldTransform.PointToLocal( Vector3.Lerp( start, end, 0.5f ) );
			Gizmo.Draw.LineSphere( mid, Radius );
		}
	}

	public override bool Emit( ParticleEffect target )
	{
		var start = StartObject.IsValid() ? StartObject.WorldPosition : WorldTransform.PointToWorld( StartPosition );
		var end = EndObject.IsValid() ? EndObject.WorldPosition : WorldTransform.PointToWorld( EndPosition );

		var t = System.Random.Shared.Float( 0f, 1f );
		var pos = Vector3.Lerp( start, end, t );

		// Perpendicular spread
		if ( Radius > 0f )
		{
			var lineDir = (end - start).Normal;
			var perp = lineDir.Cross( Vector3.Up );
			if ( perp.LengthSquared < 0.001f )
				perp = lineDir.Cross( Vector3.Forward );
			perp = perp.Normal;

			var spreadAngle = System.Random.Shared.Float( 0f, MathF.PI * 2f );
			var spreadDist = System.Random.Shared.Float( 0f, Radius );
			var biNormal = lineDir.Cross( perp ).Normal;
			pos += (perp * MathF.Cos( spreadAngle ) + biNormal * MathF.Sin( spreadAngle )) * spreadDist;
		}

		var p = target.Emit( pos, Delta );

		if ( VelocityMode != LineVelocityMode.None && Velocity != 0f )
		{
			var lineDir = (end - start).Normal;

			Vector3 velDir = VelocityMode switch
			{
				LineVelocityMode.Radial => (pos - Vector3.Lerp( start, end, t )).Normal,
				LineVelocityMode.Along => lineDir,
				LineVelocityMode.Normal => lineDir.Cross( Vector3.Up ).Normal,
				_ => Vector3.Zero,
			};

			p.Velocity += velDir * Velocity;
		}

		return true;
	}
}