Code/ParticleModules.cs
using System;
using Sandbox;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace fxbox;

/// <summary>
/// Stage when a particle module executes
/// </summary>
public enum ModuleStage
{
    Spawn,      // Controls when/how particles spawn
    Initialize, // Runs once when particle is created
    Update,     // Runs every frame for each particle
    Render      // Controls how particles are rendered
}

/// <summary>
/// Context passed to particle modules during execution
/// </summary>
public class ParticleExecutionContext
{
	public Particle Particle;
	public ParticleEffect Effect;
	public Sandbox.ParticleEmitter Emitter;
	public FXBoxNativeParticleSystem SystemComponent; // Changed from Resource to SystemComponent
}

// ==================== SPAWN MODULES ====================

/// <summary>
/// Controls spawn rate over time
/// </summary>
[Title("Spawn Rate"), Category("Spawn"), Icon("speed")]
public partial class SpawnRateModule : ParticleModule
{
    [Hide]
    public override ModuleStage Stage => ModuleStage.Spawn;
    
    [Property, Range(0.1f, 1000f)] 
    public FXParticleFloat SpawnRate { get; set; } = 10.0f;
    
    public override void Execute(ParticleExecutionContext context)
    {
	    var rate = SpawnRate;
        context.Emitter.Rate = rate.GetValue( context.SystemComponent );
    }
    
    public override void Initialize( ParticleExecutionContext context )
    {
	    context.Emitter.Rate = SpawnRate.GetValue( context.SystemComponent );
    }
}
/// <summary>
/// Sets initial particle stretch
/// </summary>
[Title("Particle Stretch"), Category("Initialize"), Icon("photo_size_select_small")]
public partial class ParticleStretchModule : ParticleModule
{
	[Hide]
	public override ModuleStage Stage => ModuleStage.Initialize;
	
    
	[Property, Range(0.1f, 100f)]
	public FXParticleFloat Size { get; set; } = 1.0f;
	public override void Initialize( ParticleExecutionContext context )
	{
		context.Effect.ApplyShape = true;
		context.Effect.Stretch = Size.ToParticleFloat( context.SystemComponent );
	}
	public override void Execute(ParticleExecutionContext context)
	{
       
	}
}
/// <summary>
/// Controls spawn rate per unit
/// </summary>
[Title("Spawn Rate Over Distance"), Category("Spawn"), Icon("speed")]
public partial class SpawnRateOverDistanceModule : ParticleModule
{
	[Hide]
	public override ModuleStage Stage => ModuleStage.Spawn;
    
	[Property, Range(0.1f, 1000f)] 
	public FXParticleFloat SpawnRate { get; set; } = 10.0f;
    
	public override void Execute(ParticleExecutionContext context)
	{
		context.Emitter.RateOverDistance = SpawnRate.GetValue( context.SystemComponent );
	}
    
	public override void Initialize( ParticleExecutionContext context )
	{
		context.Emitter.RateOverDistance = SpawnRate.GetValue( context.SystemComponent );
	}
}
/// <summary>
/// Spawns particles in a burst
/// </summary>
[Title("Spawn Burst"), Category("Spawn"), Icon("auto_awesome")]
public partial class SpawnBurstModule : ParticleModule
{
    [Hide]
    public override ModuleStage Stage => ModuleStage.Spawn;
    
    [Property, Range(1, 1000)]
    public int ParticleCount { get; set; } = 50;
    
    public override void Initialize( ParticleExecutionContext context )
    {
	    context.Emitter.Burst = ParticleCount;
    }
    public override void Execute(ParticleExecutionContext context)
    {
	    context.Emitter.Burst = ParticleCount;
    }
}

// ==================== INITIALIZE MODULES ====================

/// <summary>
/// Sets initial position based on shape
/// </summary>
[Title("Initialize Position"), Category("Initialize"), Icon("place")]
public partial class InitializePositionModule : ParticleModule, IParticleComponentCreator
{
    [Hide]
    public override ModuleStage Stage => ModuleStage.Initialize;

    public override void Execute( ParticleExecutionContext context )
    {
	    
    }

    public enum SpawnShape { Point, Sphere, Box, Cone, Circle, Line }
    
    [Property]
    public FXCopyFlags CopyFlags { get; set; } = FXCopyFlags.Rotation | FXCopyFlags.Scale;
    
    [Property]
    public SpawnShape Shape { get; set; } = SpawnShape.Sphere;
    
    [Property, Range(0f, 1000f), ShowIf(nameof(ShowRadius), true)]
    public float Radius { get; set; } = 50.0f;
    
    [Property, ShowIf(nameof(ShowBoxSize), true)]
    public Vector3 BoxSize { get; set; } = new Vector3(100, 100, 100);
    
    [Property, Range(0f, 180f), ShowIf(nameof(ShowConeAngle), true)]
    public float ConeAngle { get; set; } = 45.0f;
    
    [Property]
    public bool EmitFromShell { get; set; } = false;
    
    [Property, ShowIf(nameof(ShowLine), true)]
    public Vector3 LineStart { get; set; } = Vector3.Zero;
    
    [Property, ShowIf(nameof(ShowLine), true)]
    public Vector3 LineEnd { get; set; } = Vector3.Up * 100;

    [Hide] public bool ShowRadius => Shape == SpawnShape.Sphere || Shape == SpawnShape.Circle || Shape == SpawnShape.Cone;
    [Hide] public bool ShowBoxSize => Shape == SpawnShape.Box;
    [Hide] public bool ShowConeAngle => Shape == SpawnShape.Cone;
    [Hide] public bool ShowLine => Shape == SpawnShape.Line;
    
    public override void Initialize( ParticleExecutionContext context )
    {
	    if ( context.Particle != null )
	    {
		    
		    var pos = Shape switch
		    {
			    SpawnShape.Point => Vector3.Zero,
			    SpawnShape.Sphere => GetSpherePosition(),
			    SpawnShape.Box => GetBoxPosition(),
			    SpawnShape.Cone => GetConePosition(),
			    SpawnShape.Circle => GetCirclePosition(),
			    SpawnShape.Line => GetLinePosition(),
			    _ => Vector3.Zero
		    };

		    if ( CopyFlags.HasFlag( FXCopyFlags.Scale ) )
		    {
			    pos = pos * context.Emitter.WorldScale;
		    }

		    if ( CopyFlags.HasFlag( FXCopyFlags.Rotation ) )
		    {
			    pos = pos.RotateAround( 0, context.SystemComponent.WorldRotation );
		    }

	
		    context.Particle.Position += pos;
	    }
    }
    public void CreateComponent(GameObject go)
    {
	    var pointEmitter = go.AddComponent<ParticleSphereEmitter>();
	    pointEmitter.Radius = 0;
	    pointEmitter.Velocity = 0;
	    pointEmitter.Burst = 0;
	    pointEmitter.Rate = 0;
    }

    private Vector3 GetSpherePosition()
    {
        var direction = Random.Shared.VectorInSphere().Normal;
        var radius = EmitFromShell ? Radius : Random.Shared.Float(0, Radius);
        
        return direction * radius;
    }

    private Vector3 GetBoxPosition()
    {
        if (EmitFromShell)
        {
            var face = Random.Shared.Int(0, 5);
            var u = Random.Shared.Float(0, 1);
            var v = Random.Shared.Float(0, 1);
            var halfSize = BoxSize / 2f;
            
            return face switch
            {
                0 => new Vector3(-halfSize.x, MathX.Lerp(-halfSize.y, halfSize.y, u), MathX.Lerp(-halfSize.z, halfSize.z, v)),
                1 => new Vector3(halfSize.x, MathX.Lerp(-halfSize.y, halfSize.y, u), MathX.Lerp(-halfSize.z, halfSize.z, v)),
                2 => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), -halfSize.y, MathX.Lerp(-halfSize.z, halfSize.z, v)),
                3 => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), halfSize.y, MathX.Lerp(-halfSize.z, halfSize.z, v)),
                4 => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), MathX.Lerp(-halfSize.y, halfSize.y, v), -halfSize.z),
                _ => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), MathX.Lerp(-halfSize.y, halfSize.y, v), halfSize.z)
            };
        }
        
        return new Vector3(
            Random.Shared.Float(-BoxSize.x / 2, BoxSize.x / 2),
            Random.Shared.Float(-BoxSize.y / 2, BoxSize.y / 2),
            Random.Shared.Float(-BoxSize.z / 2, BoxSize.z / 2)
        );
    }

    private Vector3 GetConePosition()
    {
        var angle = Random.Shared.Float(0, 360);
        var distance = Random.Shared.Float(0, Radius);
        var coneRadius = MathF.Tan(ConeAngle.DegreeToRadian()) * distance;
        var radius = EmitFromShell ? coneRadius : Random.Shared.Float(0, coneRadius);
        
        return new Vector3(
            MathF.Cos(angle.DegreeToRadian()) * radius,
            MathF.Sin(angle.DegreeToRadian()) * radius,
            distance
        );
    }

    private Vector3 GetCirclePosition()
    {
        var angle = Random.Shared.Float(0, 360);
        var radius = EmitFromShell ? Radius : Random.Shared.Float(0, Radius);
        return new Vector3(
            MathF.Cos(angle.DegreeToRadian()) * radius,
            MathF.Sin(angle.DegreeToRadian()) * radius,
            0
        );
    }

    private Vector3 GetLinePosition()
    {
        return Vector3.Lerp(LineStart, LineEnd, Random.Shared.Float(0, 1));
    }
}

/// <summary>
/// Sets initial velocity
/// </summary>
[Title("Initialize Velocity"), Category("Initialize"), Icon("air")]
public partial class InitializeVelocityModule : ParticleModule
{
    [Hide]
    public override ModuleStage Stage => ModuleStage.Initialize;
    
    [Property]
    public FXParticleVector Velocity { get; set; } = Vector3.Up * 100;

    [Property] public bool LocalSpace { get; set; } = false;
    
    [Property]
    public bool InheritEmitterVelocity { get; set; } = false;
    
    
    [Property,ShowIf("InheritEmitterVelocity",true)] public float EmitterVelocityScale { get; set; } = 1.0f;
    public override void Initialize( ParticleExecutionContext context )
    {
	    var startVelocity = Velocity;
	    if ( LocalSpace )
	    {
		    startVelocity = startVelocity.GetValue( context.Particle,context.SystemComponent ).RotateAround( 0,context.Emitter.WorldRotation );
	    }
	    context.Effect.InitialVelocity = startVelocity.GetValue( context.Particle,context.SystemComponent );
	    if ( InheritEmitterVelocity )
	    {
		    context.Effect.InitialVelocity = (context.SystemComponent.Velocity*EmitterVelocityScale) + startVelocity.GetValue( context.Particle,context.SystemComponent );
	    }
	    
    }
    public override void Execute(ParticleExecutionContext context)
    {

    }
}
/// <summary>
/// Sets initial color
/// </summary>
[Title("Collision"), Category("Initialize"), Icon("palette")]
public partial class ParticleCollisionModule : ParticleModule
{

	[Property] public TagSet CollisionIgnore { get; set; } = new TagSet();
	[Property] public List<GameObject> CollisionPrefabs { get; set; } = new List<GameObject>();
	[Property] public FXParticleFloat CollisionRadius { get; set; } = 5;
	[Property] public FXParticleFloat CollisionPrefabChance { get; set; } = 1;
	[Property] public FXParticleFloat CollisionPrefabRotation { get; set; } = 0;
	[Property] public FXParticleFloat DieOnCollisionChance { get; set; } = 0;
	[Property] public bool CollisionPrefabAlign { get; set; } = false;
	[Hide]
	public override ModuleStage Stage => ModuleStage.Initialize;

	public override void Execute(ParticleExecutionContext context)
	{
        
	}
    
	public override void Initialize( ParticleExecutionContext context )
	{
		context.Effect.Collision = true;
		context.Effect.CollisionIgnore = CollisionIgnore;
		context.Effect.CollisionRadius = CollisionRadius.GetValue( context.SystemComponent );
		context.Effect.CollisionPrefabChance = CollisionPrefabChance.GetValue( context.SystemComponent );
		context.Effect.CollisionPrefabRotation = CollisionPrefabRotation.ToParticleFloat( context.SystemComponent );
		if ( CollisionPrefabs.Any() )
		{
			context.Effect.UsePrefabFeature = true;
		}
		context.Effect.CollisionPrefab = CollisionPrefabs;
		context.Effect.DieOnCollisionChance = DieOnCollisionChance.GetValue( context.SystemComponent );
		context.Effect.CollisionPrefabAlign = CollisionPrefabAlign;
	}
}
/// <summary>
/// Sets initial velocity
/// </summary>
[Title("Initialize Rotation"), Category("Initialize"), Icon("air")]
public partial class InitializeRotationModule : ParticleModule
{
	[Hide]
	public override ModuleStage Stage => ModuleStage.Initialize;
    
	[Property]
	public ParticleVector3 InitialRotation { get; set; } = Vector3.Up;
	public override void Initialize( ParticleExecutionContext context )
	{
		if ( context.Particle != null )
		{
			context.Particle.Angles = new Angles( InitialRotation.Evaluate( Time.Delta,context.Particle.Rand(  ),context.Particle.Rand(  ),context.Particle.Rand(  ) ) );
		}
	    
	}
	public override void Execute(ParticleExecutionContext context)
	{

	}
}

/// <summary>
/// Sets initial lifetime
/// </summary>
[Title("Initialize Lifetime"), Category("Initialize"), Icon("schedule")]
public partial class InitializeLifetimeModule : ParticleModule
{
	[Hide]
	public override ModuleStage Stage => ModuleStage.Initialize;
    
	[Property, Range(0.1f, 100f)]
	public FXParticleFloat Lifetime { get; set; } = 2.0f;
    
	public override void Initialize(ParticleExecutionContext context)
	{
		context.Effect.Lifetime = Lifetime.ToParticleFloat( context.SystemComponent );
	}
    
	public override void Execute(ParticleExecutionContext context)
	{
		// Not used
	}
}

/// <summary>
/// Sets initial size
/// </summary>
[Title("Initialize Size"), Category("Initialize"), Icon("photo_size_select_small")]
public partial class InitializeSizeModule : ParticleModule
{
    [Hide]
    public override ModuleStage Stage => ModuleStage.Initialize;

    [Property] public bool InheritEmitterScale { get; set; } = true;
    
    [Property, Range(0.1f, 100f)]
    public FXParticleFloat Size { get; set; } = 10.0f;
    public override void Initialize( ParticleExecutionContext context )
    {
	    context.Effect.ApplyShape = true;
	    if ( InheritEmitterScale )
	    {
		    context.Effect.Scale = Size.ToParticleFloat( context.SystemComponent );
	    }
	    else
	    {
		     context.Effect.Scale = (Size / context.SystemComponent.WorldScale.x).ToParticleFloat( context.SystemComponent );
	    }
	    
	    
	  
    }
    public override void Execute(ParticleExecutionContext context)
    {
       
    }
}

/// <summary>
/// Sets initial color
/// </summary>
[Title("Initialize Color"), Category("Initialize"), Icon("palette")]
public partial class InitializeColorModule : ParticleModule
{
    [Hide]
    public override ModuleStage Stage => ModuleStage.Initialize;

    [Property] public FXParticleColor Color { get; set; } = global::Color.Red;

    public override void Execute(ParticleExecutionContext context)
    {
        
    }
    
    public override void Initialize( ParticleExecutionContext context )
    {
	    context.Effect.ApplyColor = true;
	    context.Effect.ApplyAlpha = true;
	    if ( Color != null )
	    {
		    context.Effect.Gradient = Color.GetValue( context.SystemComponent );
	    }
	    else
	    {
		    Color = new FXParticleColor( global::Color.Red );
	    }
	    
    }
}

/// <summary>
/// Sets initial color
/// </summary>
[Title("Sprite Flipbook"), Category("Initialize"), Icon("palette")]
public partial class SpriteFlipbookModule : ParticleModule
{
	[Property] public FXParticleFloat SequenceTime { get; set; } = 0;
	[Property] public FXParticleFloat SequenceSpeed { get; set; } = 1;
	[Property] public int SequenceId { get; set; } = 0;

	[Hide]
	public override ModuleStage Stage => ModuleStage.Initialize;

	public override void Execute(ParticleExecutionContext context)
	{
        
	}
    
	public override void Initialize( ParticleExecutionContext context )
	{
		context.Effect.SheetSequence = true;
		context.Effect.SequenceId = SequenceId;
		context.Effect.SequenceSpeed = SequenceSpeed.ToParticleFloat( context.SystemComponent );
		context.Effect.SequenceTime = SequenceTime.ToParticleFloat( context.SystemComponent );

	}
}

/// <summary>
/// Randomly Kill a particle to spawn less
/// </summary>
[Title("RandomKill"), Category("Initialize"), Icon("arrow_downward")]
public partial class RandomKill : ParticleModule
{
	[Hide]
	public override ModuleStage Stage => ModuleStage.Initialize;
	
	[Property]
	public float Chance { get; set; } = 0.5f;
	

	public override void Execute(ParticleExecutionContext context)
	{
        
	}
	public override void Initialize( ParticleExecutionContext context )
	{
		if ( context.Particle == null ) return;
		if ( Random.Shared.Float( 0, 1 ) < Chance )
		{
			context.Particle.Age = 100000;
		}
	}
	
}


// ==================== UPDATE MODULES ====================

/// <summary>
/// Applies gravity force
/// </summary>
[Title("Gravity Force"), Category("Update"), Icon("arrow_downward")]
public partial class GravityForceModule : ParticleModule, IParticleUpdater
{
    [Hide]
    public override ModuleStage Stage => ModuleStage.Update;
    
    [Property]
    public FXParticleVector Force { get; set; } = new Vector3(0, 0, -980);

    public override void Execute(ParticleExecutionContext context)
    {
        
    }
    public override void Initialize( ParticleExecutionContext context )
    {
	    
    }
    public void UpdateParticle(ParticleExecutionContext context, float delta)
    {
        context.Particle.Velocity += Force.GetValue(context.Particle, context.SystemComponent ) * Time.Delta;
    }
}

/// <summary>
/// Make a mesh follow it's velocity
/// </summary>
[Title("Follow Velocity"), Category("Update"), Icon("arrow_downward")]
public partial class FollowVelocity : ParticleModule, IParticleUpdater
{
	[Hide]
	public override ModuleStage Stage => ModuleStage.Update;
	

	public override void Execute(ParticleExecutionContext context)
	{
        
	}
	public override void Initialize( ParticleExecutionContext context )
	{
	    
	}
	public void UpdateParticle(ParticleExecutionContext context, float delta)
	{
		context.Particle.Angles = Rotation.LookAt( context.Particle.Velocity ).Angles();
	}
}
/// <summary>
/// Applies drag/air resistance
/// </summary>
[Title("Drag Force"), Category("Update"), Icon("air")]
public partial class DragForceModule : ParticleModule, IParticleUpdater
{
    [Hide]
    public override ModuleStage Stage => ModuleStage.Update;
    
    [Property, Range(0f, 10f)]
    public float Damping { get; set; } = 0.1f;

    public override void Execute(ParticleExecutionContext context)
    {
        
    }
    public override void Initialize( ParticleExecutionContext context )
    {
	    
    }
    public void UpdateParticle(ParticleExecutionContext context, float delta)
    {
        context.Particle.Velocity *= (1.0f - Damping * Time.Delta);
    }
}

/// <summary>
/// Makes particles rotate
/// </summary>
[Title("Rotation"), Category("Update"), Icon("rotate_right")]
public partial class RotationModule : ParticleModule, IParticleUpdater
{
    [Hide]
    public override ModuleStage Stage => ModuleStage.Update;
    
    [Property, Range(-360f, 360f)]
    public FXParticleFloat RotationSpeed { get; set; } = 90.0f;

    public override void Execute(ParticleExecutionContext context)
    {
        /*context.Particle.Rotation += RotationSpeed * context.DeltaTime;*/
    }
    public override void Initialize( ParticleExecutionContext context )
    {
	    
    }
    public void UpdateParticle(ParticleExecutionContext context, float delta)
    {
        context.Particle.Angles += RotationSpeed.GetValue( context.SystemComponent ) * Time.Delta;
    }
}

public enum PositionType
{
	Local,
	World
}

/// <summary>
/// Attracts particles to a point. Full strength inside AttractorSize, falling off beyond it.
/// </summary>
[Title("Point Attractor"), Category("Update"), Icon("my_location")]
public partial class PointAttractorModule : ParticleModule, IParticleUpdater
{
	[Hide]
	public override ModuleStage Stage => ModuleStage.Update;
	[Property] public PositionType PositionType { get; set; } = PositionType.Local;

	[Property]
	public FXParticleVector AttractorPosition { get; set; } = Vector3.Zero;

	[Property, Range(0f, 10000f)]
	public FXParticleFloat Strength { get; set; } = 500.0f;

	[Property, Range(0.01f, 10000f)]
	public float AttractorSize { get; set; } = 50.0f;

	[Property] public bool Invert { get; set; } = false;

	/// <summary>
	/// How quickly strength falls off beyond AttractorSize.
	/// 1 = linear, 2 = inverse square, higher = sharper falloff.
	/// </summary>
	[Property, Range(0.1f, 8f)]
	public float Falloff { get; set; } = 2.0f;

	public override void Execute(ParticleExecutionContext context) { }
	public override void Initialize(ParticleExecutionContext context) { }

	public void UpdateParticle(ParticleExecutionContext context, float delta)
	{
		var attractorPos = PositionType == PositionType.Local ? AttractorPosition.GetValue( context.Particle, context.SystemComponent ) + context.Emitter.WorldPosition : AttractorPosition.GetValue( context.Particle, context.SystemComponent );
		
		
		var toAttractor = attractorPos - context.Particle.Position;
		var distance = toAttractor.Length;

		if (distance < 0.01f) return;

		// Inside the attractor: full strength.
		// Outside: strength falls off based on normalised excess distance.
		float strengthMultiplier;
		if (distance <= AttractorSize)
		{
			strengthMultiplier = 1f;
		}
		else
		{
			// How many radii past the edge are we? 0 at the surface, grows outward.
			var excess = (distance - AttractorSize) / AttractorSize;
			strengthMultiplier = 1f / MathF.Pow(1f + excess, Falloff);
		}
		
		if ( Invert )
		{
			strengthMultiplier = 1 - strengthMultiplier;
		}

		context.Particle.Velocity += toAttractor.Normal * Strength.GetValue( context.SystemComponent ) * strengthMultiplier * Time.Delta;
	}
}
/// <summary>
/// Creates orbital motion
/// </summary>
[Title("Vortex Force"), Category("Update"), Icon("cyclone")]
public partial class VortexForceModule : ParticleModule, IParticleUpdater
{
    [Hide]
    public override ModuleStage Stage => ModuleStage.Update;
    
    [Property]
    public Vector3 Center { get; set; } = Vector3.Zero;
    
    [Property]
    public FXParticleVector Axis { get; set; } = Vector3.Up;
    
    [Property, Range(0f, 1000f)]
    public float Strength { get; set; } = 100.0f;

    public override void Execute(ParticleExecutionContext context)
    {
        
    }
    public override void Initialize( ParticleExecutionContext context )
    {
	    
    }
    public void UpdateParticle(ParticleExecutionContext context, float delta)
    {
        var toCenter = context.Particle.Position - (Center + context.Emitter.WorldPosition);
        var distance = toCenter.Length;
        
        
        if (distance > 0.01f)
        {
            var tangent = Vector3.Cross(Axis.GetValue( context.Particle,context.SystemComponent ).Normal, toCenter.Normal);
            var force = tangent * (Strength / distance);
            context.Particle.Velocity += force * Time.Delta * 10000;
        }
    }
}

// ==================== RENDER MODULES ====================

/// <summary>
/// Basic sprite renderer
/// </summary>
[Title("Sprite Renderer"), Category("Render"), Icon("image")]
public partial class SpriteRendererModule : ParticleModule, IParticleComponentCreator
{
    [Hide] 
    public override ModuleStage Stage => ModuleStage.Render;

    [Property] 
    public Sprite Sprite { get; set; }

    [Property]
    public ParticleSpriteRenderer.BillboardAlignment Alignment { get; set; } =
	    ParticleSpriteRenderer.BillboardAlignment.LookAtCamera;

    [Property] public bool FaceVelocity { get; set; } = false;

    [Property] public bool Additive { get; set; } = false;

    public override void Execute(ParticleExecutionContext context)
    {
    }
    public override void Initialize( ParticleExecutionContext context )
    {
	    
    }
    public void CreateComponent(GameObject go)
    {
        var renderer = go.AddComponent<ParticleSpriteRenderer>();

        renderer.Alignment = Alignment;
        renderer.FaceVelocity = FaceVelocity;
        renderer.Sprite = Sprite;
		renderer.Additive = Additive;
    }
}

/// <summary>
/// Basic light renderer
/// </summary>
[Title("Light Renderer"), Category("Render"), Icon("image")]
public partial class LightRendererModule : ParticleModule, IParticleComponentCreator
{
	[Hide] 
	public override ModuleStage Stage => ModuleStage.Render;
	
	[Property] public FXParticleColor LightColor { get; set; } = new FXParticleColor( Color.White );
	[Property] public FXParticleFloat Brightness { get; set; } = 10f;
	[Property] public FXParticleFloat MaxLights { get; set; } = 10f;
	[Property] public FXParticleFloat LightSize { get; set; } = 10f;
	[Property] public FXParticleFloat Attenuation { get; set; } = 1;
	[Property] public bool CastShadows { get; set; } = false;
	[Property] public FXParticleFloat Ratio { get; set; } = 1;
	public override void Execute(ParticleExecutionContext context)
	{
		// Rendering is handled externally, this just stores render properties
	}
	public override void Initialize( ParticleExecutionContext context )
	{
	    
	}
	public void CreateComponent(GameObject go)
	{
		var renderer = go.AddComponent<ParticleLightRenderer>();
		
		var fxbox=go.GetComponentInParent<FXBoxNativeParticleSystem>(  );
		renderer.LightColor = LightColor.GetValue( fxbox );
		renderer.Brightness = Brightness.GetValue( fxbox );
		
		renderer.MaximumLights = (int)MaxLights.GetValue( fxbox );
		renderer.Scale = LightSize.GetValue( fxbox );
		renderer.Attenuation = Attenuation.GetValue( fxbox );
		renderer.Ratio = Ratio.GetValue( go.GetComponentInParent<FXBoxNativeParticleSystem>() );
		renderer.CastShadows = CastShadows;
	}
}

/// <summary>
/// Basic model renderer
/// </summary>
[Title("Model Renderer"), Category("Render"), Icon("image")]
public partial class ModelRendererModule : ParticleModule, IParticleComponentCreator
{
	[Hide] 
	public override ModuleStage Stage => ModuleStage.Render;

	[Property] 
	public List<ParticleModelRenderer.ModelEntry> Models { get; set; }

	[Property] 
	public bool FaceCamera { get; set; } = true;

	public override void Execute(ParticleExecutionContext context)
	{
		// Rendering is handled externally, this just stores render properties
	}
	public override void Initialize( ParticleExecutionContext context )
	{
	    
	}
	public void CreateComponent(GameObject go)
	{
		var renderer = go.AddComponent<ParticleModelRenderer>();

		renderer.Choices = Models;

	}
}

/// <summary>
/// Basic Trail Renderer
/// </summary>
[Title( "Trail Renderer" ), Category( "Render" ), Icon( "image" )]
public partial class TrailRendererModule : ParticleModule, IParticleComponentCreator
{
	[Hide] public override ModuleStage Stage => ModuleStage.Render;

	[Property] public bool Game { get; set; } = true;
	[Property] public bool Overlay { get; set; } = false;
	[Property] public bool Bloom { get; set; } = false;
	[Property] public bool AfterUi { get; set; } = false;
	[Property] public Material Material { get; set; }
	[Property] public FXParticleFloat UnitsPerTexture { get; set; } = 10f;
	[Property] public FXParticleFloat Scroll { get; set; } = 0f;
	[Property] public FXParticleFloat Width { get; set; } = 1f;
	[Property] public bool Opaque { get; set; } = true;
	[Property, ShowIf( "Opaque", false )] public BlendMode BlendMode { get; set; } = BlendMode.Normal;
	[Property] public int MaxPoints { get; set; } = 32;
	[Property] public float PointDistance { get; set; } = 8;
	[Property] public float LifeTime { get; set; } = 2f;
	[Property] public FXParticleColor Color { get; set; } = new FXParticleColor(  );

public override void Execute(ParticleExecutionContext context)
	{
		// Rendering is handled externally, this just stores render properties
	}
	public override void Initialize( ParticleExecutionContext context )
	{
	    
	}
	public void CreateComponent(GameObject go)
	{
		var renderer = go.AddComponent<ParticleTrailRenderer>();

		var appearance = renderer.Texturing;
		appearance.Material = Material;
		appearance.UnitsPerTexture = UnitsPerTexture.GetValue(  );
		appearance.Scroll = Scroll.GetValue();

		var widthCurve = Width.ToParticleFloat();

		if ( widthCurve.Type == ParticleFloat.ValueType.Curve )
		{
			renderer.Width = widthCurve.CurveA;
		} else if ( widthCurve.Type == ParticleFloat.ValueType.Range )
		{
			var point1 = new Curve.Frame( 0, widthCurve.ConstantA );
			var point2 = new Curve.Frame( 1, widthCurve.ConstantB );
			renderer.Width = new Curve( point1, point2 );
		} else if ( widthCurve.Type == ParticleFloat.ValueType.Constant )
		{
			renderer.Width = widthCurve.ConstantA;
		}
		else
		{
			renderer.Width = widthCurve.CurveA;
		}
		
		renderer.Opaque = Opaque;
		renderer.BlendMode = BlendMode;
		renderer.MaxPoints = MaxPoints;
		renderer.PointDistance = PointDistance;
		renderer.LifeTime = LifeTime;
		var colorParam = Color.GetValue();
		if ( colorParam.Type == ParticleGradient.ValueType.Constant )
		{
			renderer.Color = colorParam.ConstantA;
		} else if ( colorParam.Type == ParticleGradient.ValueType.Range )
		{
			var point1 = new Gradient.ColorFrame( 0, colorParam.ConstantA );
			var point2 = new Gradient.ColorFrame( 1, colorParam.ConstantB );
			renderer.Color = new Gradient( point1, point2 );
		} else if ( colorParam.Type == ParticleGradient.ValueType.Gradient )
		{
			renderer.Color = colorParam.GradientA;
		}

		renderer.RenderOptions.Game = Game;
		renderer.RenderOptions.Overlay = Overlay;
		renderer.RenderOptions.Bloom = Bloom;
		renderer.RenderOptions.AfterUI = AfterUi;

		renderer.Texturing = appearance;

	}
}
/// <summary>
/// Applies curl noise force for organic, swirling motion
/// </summary>
[Title("Curl Noise"), Category("Update"), Icon("air")]
public partial class CurlNoiseModule : ParticleModule, IParticleUpdater
{
    [Hide]
    public override ModuleStage Stage => ModuleStage.Update;
    
    [Property, Range(0f, 1000f)]
    [Description("Strength of the curl noise effect")]
    public FXParticleFloat Strength { get; set; } = 1.0f;
    
    [Property, Range(0.01f, 10f)]
    [Description("Scale of the noise pattern - smaller values create tighter curls")]
    public FXParticleFloat Scale { get; set; } = 1.0f;
    
    [Property, Range(0f, 10f)]
    [Description("Speed at which the noise pattern evolves over time")]
    public FXParticleFloat TimeScale { get; set; } = 1.0f;
    
    [Property]
    [Description("Offset in the noise field")]
    public Vector3 Offset { get; set; } = Vector3.Zero;

    public override void Execute(ParticleExecutionContext context)
    {
        // Not used - handled in UpdateParticle
    }

    public override void Initialize(ParticleExecutionContext context)
    {
        // No initialization needed
    }

    public void UpdateParticle(ParticleExecutionContext context, float delta)
    {
        var particle = context.Particle;
        
        // Sample position in noise field
        var samplePos = (particle.Position + Offset) * Scale.GetValue( context.SystemComponent );
        var time = context.Particle.Age * TimeScale;
        
        // Calculate curl noise using the curl of a 3D noise field
        var curl = CalculateCurl(samplePos, time.GetValue( context.SystemComponent ));
        
        // Apply force
        particle.Velocity += curl * Strength.GetValue( context.SystemComponent ) * delta * 10;
    }

    /// <summary>
    /// Calculate curl noise by taking the curl of a potential field
    /// This creates divergence-free flow fields that look organic
    /// </summary>
    private Vector3 CalculateCurl(Vector3 pos, float time)
    {
        const float epsilon = 0.001f;
        
        // Sample the potential field at offset positions
        // We need 6 samples to calculate the curl (derivatives in all directions)
        
        // dPz/dy - dPy/dz
        float curlX = 
            (SamplePotential(pos + new Vector3(0, epsilon, 0), time).z - 
             SamplePotential(pos - new Vector3(0, epsilon, 0), time).z) -
            (SamplePotential(pos + new Vector3(0, 0, epsilon), time).y - 
             SamplePotential(pos - new Vector3(0, 0, epsilon), time).y);
        
        // dPx/dz - dPz/dx
        float curlY = 
            (SamplePotential(pos + new Vector3(0, 0, epsilon), time).x - 
             SamplePotential(pos - new Vector3(0, 0, epsilon), time).x) -
            (SamplePotential(pos + new Vector3(epsilon, 0, 0), time).z - 
             SamplePotential(pos - new Vector3(epsilon, 0, 0), time).z);
        
        // dPy/dx - dPx/dy
        float curlZ = 
            (SamplePotential(pos + new Vector3(epsilon, 0, 0), time).y - 
             SamplePotential(pos - new Vector3(epsilon, 0, 0), time).y) -
            (SamplePotential(pos + new Vector3(0, epsilon, 0), time).x - 
             SamplePotential(pos - new Vector3(0, epsilon, 0), time).x);
        
        return new Vector3(curlX, curlY, curlZ) / (2.0f * epsilon);
    }

    /// <summary>
    /// Sample a 3D potential field using Perlin-like noise
    /// </summary>
    private Vector3 SamplePotential(Vector3 pos, float time)
    {
        // Create three offset noise samples for each component
        // This creates a vector field from scalar noise functions
        return new Vector3(
            Noise3D(pos + new Vector3(0, 0, 0), time),
            Noise3D(pos + new Vector3(31.416f, -47.853f, 12.793f), time),
            Noise3D(pos + new Vector3(-17.737f, 86.214f, -59.482f), time)
        );
    }

    /// <summary>
    /// Simple 3D noise function using sine waves
    /// You could replace this with proper Perlin/Simplex noise for better results
    /// </summary>
    private float Noise3D(Vector3 pos, float time)
    {
        // Combine multiple sine waves at different frequencies for pseudo-noise
        var p = pos + new Vector3(time, time * 0.7f, time * 0.5f);
        
        float noise = 0;
        noise += MathF.Sin(p.x * 1.0f + p.y * 1.3f) * 0.5f;
        noise += MathF.Sin(p.y * 1.7f + p.z * 0.9f) * 0.3f;
        noise += MathF.Sin(p.z * 2.1f + p.x * 1.1f) * 0.2f;
        noise += MathF.Sin(p.x * 3.7f + p.y * 2.3f + p.z * 1.9f) * 0.15f;
        
        return noise;
    }
}