s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (17 total matches found)
stellawisps.fxbox / Code/ParticleModules.cs
Game library
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;
    }

    // Burst is a one-shot "spawn this many now" value, not a continuous per-tick one like
    // SpawnRateModule's Rate - Execute() runs every single frame (see
    // FXBoxParticleController.OnUpdate), so reassigning Burst here too kept re-arming/
    // re-firing it every tick instead of once, spawning far more than ParticleCount actually
    // configured. Same "Initialize-only, empty Execute" shape ParticleStretchModule already
    // uses above for its own one-shot value.
    public override void Execute(ParticleExecutionContext context)
    {
    }
}

// ==================== 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>
/// Controls how strongly particles follow the emitter in local space
/// </summary>
[Title("Initialize Local Space"), Category("Initialize"), Icon("transform")]
public partial class InitializeLocalSpaceModule : ParticleModule
{
	[Hide]
	public override ModuleStage Stage => ModuleStage.Initialize;

	[Property, Range(0f, 1f)]
	public FXParticleFloat LocalSpace { get; set; } = 0f;

	public override void Initialize( ParticleExecutionContext context )
	{
		context.Effect.LocalSpace = LocalSpace.ToParticleFloat( context.SystemComponent );
	}

	public override void Execute( ParticleExecutionContext context )
	{
	}
}

/// <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 FXParticleFloat RandomVelocity { get; set; } = 0;

    [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.StartVelocity = RandomVelocity.ToParticleFloat();
	    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] public FXParticleVector RotationSpeed { get; set; } = Vector3.Zero;

    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)
    {
	    if ( context.Particle != null )
	    {
		    context.Particle.Angles += RotationSpeed.GetValue( context.Particle ) * 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 FXParticleFloat SpriteScale { get; set; } = 1f;

    [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;
		renderer.Scale = SpriteScale.GetValue();
    }
}

/// <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 bool UseParticleColor { get; set; } = true;
	[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( fxbox );
		renderer.CastShadows = CastShadows;
		renderer.UseParticleColor = UseParticleColor;
	}
}

/// <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;
    }
}
stellawisps.fxbox / UnitTests/UnitTest.cs
UnitTest library
global using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestInit
{
	[AssemblyInitialize]
	public static void ClassInitialize( TestContext context )
	{
		Sandbox.Application.InitUnitTest();
	}
}
stellawisps.fxbox / Editor/Graph/FXEditor.cs
Editor library
using Editor;
using Sandbox;
using System.Collections.Generic;
using System.Linq;

namespace fxbox.Graph;

/// <summary>
/// Main particle system editor - ShaderGraph style with DockManager
/// </summary>
[EditorForAssetType("fx")]
[EditorApp("FXBox", "auto_awesome", "Create and edit particle systems")]
public class FXBoxEditor : DockWindow, IAssetEditor
{
    public bool CanOpenMultipleAssets => true;
    public static ParticleResource CurrentEditingResource { get; private set; }
    
    private ParticleResource _resource; // Original resource from asset
    private ParticleResource _workingCopy; // Copy we actually edit
    private Asset _asset;
    private bool _isDirty = false;

    // UI Components
    private EmitterList _emitterList;
    private ParticlePreview _preview;
    private PropertiesWidget _properties;
    
    private string _defaultDockState;

    public FXBoxEditor()
    {
        DeleteOnClose = true;
        Title = "FXBox - Particle System Editor";
        Size = new Vector2(1800, 1000);

        CreateToolBar();
        CreateUI();
        Show();
    }

    // StateCookie is set here, AFTER base.Show(), rather than inside CreateUI() before the
    // window is even shown - same ordering as the working RectEditor reference
    // (HotspotEditorWindow.Show() / Window.CreateUI()'s "if (Visible) RestoreLayout()").
    // Setting it too early was part of why a layout you'd closed with panels open never came
    // back - restoring (or building the default layout) needs the window's dock widget
    // hierarchy to actually exist and be visible first.
    public override void Show()
    {
        base.Show();
        StateCookie = "FXBoxEditor_v6";
    }

    private void CreateToolBar()
    {
        var toolbar = new ToolBar(this, "FXBoxToolbar");
        AddToolBar(toolbar, ToolbarPosition.Top);

        toolbar.AddOption("Save", "save", Save).StatusTip = "Save Particle System (Ctrl+S)";
        toolbar.AddSeparator();
        toolbar.AddOption("Add Emitter", "add_circle", AddEmitter).StatusTip = "Add new emitter";
        
        // Add parameter menu
        toolbar.AddSeparator();
        toolbar.AddOption("Float Parameter", "looks_one", AddFloatParameter);
        toolbar.AddOption("Vector Parameter", "3d_rotation", AddVectorParameter);
        toolbar.AddOption("Color Parameter", "palette", AddColorParameter);
        
        toolbar.AddSeparator();
        toolbar.AddOption("Play", "play_arrow", () => _preview?.TogglePlayback()).StatusTip = "Play/Pause";
        toolbar.AddOption("Restart", "replay", () => _preview?.Restart()).StatusTip = "Restart";
    }

    private void CreateUI()
    {
        BuildMenuBar();
        BuildDock();

        DockManager.Update();

        _defaultDockState = DockManager.State;
    }

    // Builds the REAL widget instances and docks them - unlike the previous
    // RegisterDockType+CreateAction version, this runs eagerly every time CreateUI runs
    // (every open, every hotload), so _emitterList/_preview/_properties are never null by
    // the time anything else (AssetOpen, LoadResource, etc.) tries to push data into them.
    // The old lazy version only created a widget instance whenever the dock manager itself
    // got around to invoking CreateAction - which, on a restored/default layout, could
    // happen well after AssetOpen already tried (and silently no-op'd via ?.) to hand the
    // resource to _emitterList/_preview/_properties, matching the exact "not hooked up until
    // you add a parameter or emitter" symptom (that next SetResource/LoadParticleSystem call
    // was the first one to land after CreateAction had finally run).
    //
    // DockManager.AddDock(title, icon, widget, area, relativeTo) both registers the dock
    // TYPE under that title/icon (same as RegisterDockType did, for the View menu and
    // BuildDefaultLayout's OpenDock below to find it by name) AND places this exact instance
    // on screen - no separate CreateDockWidget/AddDock two-step needed.
    private void BuildDock()
    {
        _preview = CreatePreview();
        _emitterList = CreateEmitterList();
        _properties = CreateProperties();

        var preview = DockManager.AddDock( "Preview", "visibility", _preview, DockArea.Center );
        DockManager.AddDock( "Emitters", "list", _emitterList, DockArea.Left, relativeTo: preview );
        DockManager.AddDock( "Properties", "tune", _properties, DockArea.Right, relativeTo: preview );
    }

    // Called by the base class on a genuinely first-ever open (nothing saved under
    // StateCookie yet - see the Show() override above) to decide default POSITIONS/
    // proportions only. OpenDock resolves the SAME dock types BuildDock() already
    // registered+placed above by Title, it doesn't build a second set of widgets.
    protected override void BuildDefaultLayout()
    {
        var preview = DockManager.OpenDock( "Preview", DockArea.Center );
        var emitters = DockManager.OpenDock( "Emitters", DockArea.Left, preview );
        var properties = DockManager.OpenDock( "Properties", DockArea.Right, preview );

        DockManager.SetSplitterProportions( emitters, 0.20f, 0.80f );
        DockManager.SetSplitterProportions( properties, 0.80f, 0.20f );
    }

    private EmitterList CreateEmitterList()
    {
        var widget = new EmitterList(this);
        widget.Name = "Emitters";
        widget.WindowTitle = "Emitters & Modules";
        widget.SetWindowIcon("list");
        widget.MinimumWidth = 300;
        widget.OnSelectionChanged = OnSelectionChanged;
        widget.OnEmitterDeleted = OnEmitterDeleted;
        widget.OnModuleDeleted = OnModuleDeleted;
        widget.OnAddModule = OnAddModule;
        widget.OnSystemChanged = MarkDirty;
        widget.OnEmitterDuplicated = OnEmitterDuplicated;
        widget.OnModuleDuplicated = OnModuleDuplicated;

        return widget;
    }

    private void OnEmitterDuplicated(ParticleEmitter emitter)
    {
	    if (_workingCopy == null) return;

	    var index = _workingCopy.Emitters.IndexOf(emitter);
	    if (index == -1) return;

	    // Round-trip the entire resource through JSON, then pull out
	    // the emitter at the same index — gives us a full deep clone
	    // without needing Serialize/Deserialize on ParticleEmitter itself.
	    var resourceCopy = DeepCopyResource(_workingCopy);
	    var clone = resourceCopy.Emitters[index];
	    clone.Name = $"{emitter.Name} (Copy)";

	    _workingCopy.Emitters.Insert(index + 1, clone);

	    _emitterList?.SetResource(_workingCopy);
	    _preview?.LoadParticleSystem(_workingCopy);
	    MarkDirty();
    }

    private ParticlePreview CreatePreview()
    {
        var widget = new ParticlePreview(this);
        widget.Name = "Preview";
        widget.WindowTitle = "Preview";
      
        widget.SetWindowIcon("visibility");
        return widget;
    }

    private PropertiesWidget CreateProperties()
    {
        var widget = new PropertiesWidget(this);
        widget.Name = "Properties";
        widget.WindowTitle = "Properties";
        widget.SetWindowIcon("tune");
        widget.MinimumWidth = 300;
        widget.OnPropertyChanged = OnPropertyChanged;
        return widget;
    }

    private void BuildMenuBar()
    {
        var file = MenuBar.AddMenu("File");
        file.AddOption("New", "add", New, "editor.new").StatusTip = "New Particle System";
        file.AddOption("Open", "folder_open", Open, "editor.open").StatusTip = "Open Particle System";
        file.AddOption("Save", "save", Save, "editor.save").StatusTip = "Save Particle System";
        file.AddSeparator();
        file.AddOption("Close", null, () => Close(), "editor.quit").StatusTip = "Close Editor";

        var edit = MenuBar.AddMenu("Edit");
        edit.AddOption("Add Emitter", "add_circle", AddEmitter);
        edit.AddSeparator();
        edit.AddOption("Add Float Parameter", "looks_one", AddFloatParameter);
        edit.AddOption("Add Vector Parameter", "3d_rotation", AddVectorParameter);
        edit.AddOption("Add Color Parameter", "palette", AddColorParameter);

        var view = MenuBar.AddMenu("View");
        view.AboutToShow += () => OnViewMenu(view);
    }

    private void OnViewMenu(Menu view)
    {
        view.Clear();
        //view.AddOption("Restore To Default", "settings_backup_restore", RestoreDefaultDockLayout);
        view.AddSeparator();

        foreach (var dock in DockManager.DockTypes)
        {
            var o = view.AddOption(dock.Title, dock.Icon);
            o.Checkable = true;
            o.Checked = DockManager.IsDockOpen(dock.Title);
            o.Toggled += (b) => DockManager.SetDockState(dock.Title, b);
        }
    }

   // protected override void RestoreDefaultDockLayout()
   // {
        //DockManager.State = _defaultDockState;
       // SaveToStateCookie();
    //}

    private void AddFloatParameter()
    {
        if (_workingCopy == null) return;
        
        var param = new FloatParameter
        {
            Name = $"FloatParam{_workingCopy.FloatParameters.Count + 1}",
            DefaultValue = 1.0f
        };
        
        _workingCopy.FloatParameters.Add(param);
        _properties?.ShowSystemProperties(_workingCopy);
        MarkDirty();
    }

    private void AddVectorParameter()
    {
        if (_workingCopy == null) return;
        
        var param = new VectorParameter
        {
            Name = $"VectorParam{_workingCopy.VectorParameters.Count + 1}",
            DefaultValue = Vector3.One
        };
        
        _workingCopy.VectorParameters.Add(param);
        _properties?.ShowSystemProperties(_workingCopy);
        MarkDirty();
    }

    private void AddColorParameter()
    {
        if (_workingCopy == null) return;
        
        var param = new ColorParameter
        {
            Name = $"ColorParam{_workingCopy.ColorParameters.Count + 1}",
            DefaultValue = Color.White
        };
        
        _workingCopy.ColorParameters.Add(param);
        _properties?.ShowSystemProperties(_workingCopy);
        MarkDirty();
    }

    public void AssetOpen(Asset asset)
    {
        _asset = asset;
        _resource = asset.LoadResource<ParticleResource>();

        if (_resource == null)
        {
            _resource = new ParticleResource();
            var defaultEmitter = new ParticleEmitter { Name = "Emitter 1" };
            AddDefaultModules(defaultEmitter);
            _resource.Emitters.Add(defaultEmitter);
        }

        // Create a deep copy for editing
        _workingCopy = DeepCopyResource(_resource);

        Title = $"FXBox - {asset.Name}";
        LoadResource();
        Focus();
    }

    private ParticleResource DeepCopyResource(ParticleResource source)
    {
        if (source == null) return null;

        var json = source.Serialize().ToJsonString();
        var copy = new ParticleResource();
        copy.Deserialize(Json.ParseToJsonObject(json));
        copy.IsDirty = false;
        
        return copy;
    }

    private void LoadResource()
    {
        CurrentEditingResource = _workingCopy;
        
        _emitterList?.SetResource(_workingCopy);
        _preview?.LoadParticleSystem(_workingCopy);
        _properties?.ShowSystemProperties(_workingCopy);
        _isDirty = false;
        if (_asset != null)
            _asset.HasUnsavedChanges = false;
    }

    [Shortcut("editor.new", "CTRL+N", ShortcutType.Window)]
    private void New()
    {
        PromptSave(() => CreateNew());
    }

    private void CreateNew()
    {
        _asset = null;
        _resource = new ParticleResource();
        var defaultEmitter = new ParticleEmitter { Name = "Emitter 1" };
        AddDefaultModules(defaultEmitter);
        _resource.Emitters.Add(defaultEmitter);
        
        _workingCopy = DeepCopyResource(_resource);
        
        Title = "FXBox - Untitled";
        LoadResource();
    }

    [Shortcut("editor.open", "CTRL+O", ShortcutType.Window)]
    private void Open()
    {
        var fd = new FileDialog(null)
        {
            Title = "Open Particle System",
            DefaultSuffix = ".fx"
        };

        fd.SetNameFilter("Particle System (*.fx)");

        if (!fd.Execute())
            return;

        PromptSave(() => OpenFile(fd.SelectedFile));
    }

    private void OpenFile(string path)
    {
        var asset = AssetSystem.FindByPath(path);
        if (asset != null)
        {
            AssetOpen(asset);
        }
    }

    private void AddEmitter()
    {
        if (_workingCopy == null) return;
        
        var emitter = new ParticleEmitter 
        { 
            Name = $"Emitter {_workingCopy.Emitters.Count + 1}"
        };

        AddDefaultModules(emitter);
        _workingCopy.Emitters.Add(emitter);
        
        _emitterList?.SetResource(_workingCopy);
        _preview?.LoadParticleSystem(_workingCopy);
        MarkDirty();
    }

    private void AddDefaultModules(ParticleEmitter emitter)
    {
        emitter.SpawnModules.Add(new SpawnRateModule { Name = "Spawn Rate" });
        emitter.InitializeModules.Add(new InitializePositionModule { Name = "Initialize Position" });
        emitter.InitializeModules.Add(new InitializeLocalSpaceModule { Name = "Initialize Local Space" });
        emitter.InitializeModules.Add(new InitializeVelocityModule { Name = "Initialize Velocity" });
        emitter.InitializeModules.Add(new InitializeLifetimeModule { Name = "Initialize Lifetime" });
        emitter.InitializeModules.Add(new InitializeSizeModule { Name = "Initialize Size" });
        emitter.InitializeModules.Add(new InitializeColorModule { Name = "Initialize Color" });
        emitter.UpdateModules.Add(new GravityForceModule { Name = "Gravity" });
        emitter.UpdateModules.Add(new DragForceModule { Name = "Drag" });
        emitter.RenderModules.Add(new SpriteRendererModule { Name = "Sprite Renderer" });
    }

    private void OnAddModule(ParticleEmitter emitter, ModuleStage stage)
    {
        var menu = new Menu(this);

        var moduleTypes = EditorTypeLibrary.GetTypes<ParticleModule>()
            .Where(t => !t.IsAbstract)
            .Select(t => t.TargetType)
            .Where(t => {
                var instance = System.Activator.CreateInstance(t) as ParticleModule;
                return instance?.Stage == stage;
            });

        foreach (var moduleType in moduleTypes.OrderBy(t => t.Name))
        {
            var displayInfo = DisplayInfo.ForType(moduleType);
            menu.AddOption(displayInfo.Name, displayInfo.Icon ?? "extension", () => {
                var module = System.Activator.CreateInstance(moduleType) as ParticleModule;
                if (module != null)
                {
                    module.Name = displayInfo.Name;
                    
                    var targetList = stage switch
                    {
                        ModuleStage.Spawn => emitter.SpawnModules,
                        ModuleStage.Initialize => emitter.InitializeModules,
                        ModuleStage.Update => emitter.UpdateModules,
                        ModuleStage.Render => emitter.RenderModules,
                        _ => null
                    };

                    targetList?.Add(module);
                    _emitterList?.SetResource(_workingCopy);
                    _preview?.LoadParticleSystem(_workingCopy);
                    MarkDirty();
                }
            });
        }

        menu.OpenAtCursor();
    }

    private void OnSelectionChanged(object target)
    {
        if (target is ParticleResource resource)
        {
            _properties?.ShowSystemProperties(resource);
        }
        else if (target is ParticleEmitter emitter)
        {
            _properties?.ShowEmitterProperties(emitter);
        }
        else if (target is ParticleModule module)
        {
            _properties?.ShowModuleProperties(module);
        }
    }

    private void OnEmitterDeleted(ParticleEmitter emitter)
    {
        _workingCopy.Emitters.Remove(emitter);
        _emitterList?.SetResource(_workingCopy);
        _preview?.LoadParticleSystem(_workingCopy);
        _properties?.ShowSystemProperties(_workingCopy);
        MarkDirty();
    }

    private void OnModuleDeleted(ParticleModule module)
    {
        foreach (var emitter in _workingCopy.Emitters)
        {
            emitter.SpawnModules.Remove(module);
            emitter.InitializeModules.Remove(module);
            emitter.UpdateModules.Remove(module);
            emitter.RenderModules.Remove(module);
        }

        _emitterList?.SetResource(_workingCopy);
        _preview?.LoadParticleSystem(_workingCopy);
        MarkDirty();
    }

    private void OnPropertyChanged()
    {
        _preview?.LoadParticleSystem(_workingCopy);
        MarkDirty();
    }

    private void MarkDirty()
    {
        _preview?.LoadParticleSystem(_workingCopy);
        
        if (!_isDirty)
        {
            if (_workingCopy != null)
                _workingCopy.IsDirty = true;
            if (_resource != null)
                _resource.IsDirty = true;
                
            _isDirty = true;
            
            if (_asset != null)
                _asset.HasUnsavedChanges = true;
                
            Title = $"FXBox - {_asset?.Name ?? "Untitled"}*";
        }
    }

    [Shortcut("editor.save", "CTRL+S", ShortcutType.Window)]
    private void Save()
    {
        if (_asset != null && _workingCopy != null)
        {
            _workingCopy.IsDirty = false;
            _workingCopy.Version++;
            var json = _workingCopy.Serialize().ToJsonString();
            System.IO.File.WriteAllText(_asset.AbsolutePath, json);
            
            _resource = DeepCopyResource(_workingCopy);
            _resource.IsDirty = false;
            
            _isDirty = false;
            _asset.HasUnsavedChanges = false;
            
            Title = $"FXBox - {_asset.Name}";
            Log.Info($"Saved: {_asset.Name}");
        }
        else if (_workingCopy != null)
        {
            // No asset yet, prompt for save location
            SaveAs();
        }
    }

    private void SaveAs()
    {
        var fd = new FileDialog(null)
        {
            Title = "Save Particle System",
            DefaultSuffix = ".fx"
        };

        fd.SelectFile("untitled.fx");
        fd.SetFindFile();
        fd.SetModeSave();
        fd.SetNameFilter("Particle System (*.fx)");
        
        if (!fd.Execute())
            return;

        var savePath = fd.SelectedFile;
        
        _workingCopy.IsDirty = false;
        _workingCopy.Version++;
        var json = _workingCopy.Serialize().ToJsonString();
        System.IO.File.WriteAllText(savePath, json);
        
        _asset = AssetSystem.RegisterFile(savePath);
        _resource = DeepCopyResource(_workingCopy);
        _resource.IsDirty = false;
        
        _isDirty = false;
        if (_asset != null)
            _asset.HasUnsavedChanges = false;
        
        Title = $"FXBox - {_asset.Name}";
        Log.Info($"Saved: {_asset.Name}");
    }

    
    private void OnModuleDuplicated(ParticleModule module, ModuleStage stage)
    {
	    if (_workingCopy == null) return;

	    foreach (var emitter in _workingCopy.Emitters)
	    {
		    var moduleList = stage switch
		    {
			    ModuleStage.Spawn      => emitter.SpawnModules,
			    ModuleStage.Initialize => emitter.InitializeModules,
			    ModuleStage.Update     => emitter.UpdateModules,
			    ModuleStage.Render     => emitter.RenderModules,
			    _                      => null
		    };

		    if (moduleList == null) continue;

		    var index = moduleList.IndexOf(module);
		    if (index == -1) continue;

		    // Round-trip the whole resource; the module will be at the same
		    // emitter index + stage index in the cloned copy.
		    var emitterIndex = _workingCopy.Emitters.IndexOf(emitter);
		    var resourceCopy = DeepCopyResource(_workingCopy);

		    var clonedList = stage switch
		    {
			    ModuleStage.Spawn      => resourceCopy.Emitters[emitterIndex].SpawnModules,
			    ModuleStage.Initialize => resourceCopy.Emitters[emitterIndex].InitializeModules,
			    ModuleStage.Update     => resourceCopy.Emitters[emitterIndex].UpdateModules,
			    ModuleStage.Render     => resourceCopy.Emitters[emitterIndex].RenderModules,
			    _                      => null
		    };

		    if (clonedList == null) break;

		    var clone = clonedList[index];
		    clone.Name = $"{module.Name} (Copy)";
		    moduleList.Insert(index + 1, clone);

		    break; // modules are unique instances, no need to keep iterating
	    }

	    _emitterList?.SetResource(_workingCopy);
	    _preview?.LoadParticleSystem(_workingCopy);
	    MarkDirty();
    }

    private void PromptSave(System.Action action)
    {
        if (!_isDirty)
        {
            action?.Invoke();
            return;
        }

        var confirm = new PopupWindow(
            "Save Current Particle System", 
            "The particle system has unsaved changes. Would you like to save now?", 
            "Cancel",
            new Dictionary<string, System.Action>()
            {
                { "No", () => { _isDirty = false; action?.Invoke(); } },
                { "Yes", () => { Save(); if (!_isDirty) action?.Invoke(); } }
            }
        );

        confirm.Show();
    }

    public void SelectMember(string memberName) { }

    protected override bool OnClose()
    {
        CurrentEditingResource = null;
        
        if (_isDirty)
        {
            PromptSave(() => { _isDirty = false; Close(); });
            return false;
        }

        return base.OnClose();
    }
}

/// <summary>
/// Left panel showing emitters and their modules in a tree structure
/// </summary>
public class EmitterList : Widget
{
    private TreeView _tree;
    private ParticleResource _resource;

    public System.Action<object> OnSelectionChanged;
    public System.Action<ParticleEmitter> OnEmitterDeleted;
    public System.Action<ParticleModule> OnModuleDeleted;
    public System.Action<ParticleEmitter, ModuleStage> OnAddModule;
    public System.Action<ParticleEmitter> OnEmitterDuplicated;
    public System.Action<ParticleModule, ModuleStage> OnModuleDuplicated;
    public System.Action OnSystemChanged;
    
    public EmitterList(Widget parent) : base(parent)
    {
        Layout = Layout.Column();
        Layout.Spacing = 0;

        // Header
        var header = new Widget(this);
        header.Layout = Layout.Row();
        header.Layout.Spacing = 8;
        header.Layout.Margin = 8;
        header.MinimumHeight = 32;
        header.SetStyles("background-color: #1e1e1e;");

        var label = new Label("Emitters & Modules", header);
        label.SetStyles("font-weight: bold; font-size: 14px;");
        header.Layout.Add(label, 1);

        Layout.Add(header);

        _tree = new TreeView(this);
        _tree.AcceptDrops = true;
        _tree.ItemClicked = OnItemActivated;
        _tree.ItemContextMenu = OnItemContextMenu;
        _tree.ItemSelected = OnTreeMousePress;
        Layout.Add(_tree, 1);
    }

    private void OnTreeMousePress(object item)
    {
        TryHandleStageButtonClick(item);
    }

    private bool TryHandleStageButtonClick(object item)
    {
        if (item is not StageNode stageNode)
            return false;

        if (!_tree.TryGetItemRect(stageNode, out var itemRect))
            return false;

        var buttonRect = new Rect(itemRect.Right - 24, itemRect.Top, 24, itemRect.Height);
        
        if (itemRect.IsInside(buttonRect))
        {
            OnAddModule?.Invoke(stageNode.Emitter, stageNode.Stage);
            return true;
        }

        return false;
    }

    public void SetResource(ParticleResource resource)
    {
        _resource = resource;
        RebuildTree();
    }

    private void RebuildTree()
    {
        _tree.Clear();

        if (_resource == null) return;

        var systemNode = new SystemNode(_resource);
        _tree.AddItem(systemNode);
        _tree.Open(systemNode);

        foreach (var emitter in _resource.Emitters)
        {
            var emitterNode = new EmitterNode(emitter);
            systemNode.AddItem(emitterNode);
            _tree.Open(emitterNode);

            AddStageNode(emitterNode, "Spawn", ModuleStage.Spawn, emitter.SpawnModules, emitter);
            AddStageNode(emitterNode, "Initialize", ModuleStage.Initialize, emitter.InitializeModules, emitter);
            AddStageNode(emitterNode, "Update", ModuleStage.Update, emitter.UpdateModules, emitter);
            AddStageNode(emitterNode, "Render", ModuleStage.Render, emitter.RenderModules, emitter);
        }
    }

    private void AddStageNode(TreeNode parent, string name, ModuleStage stage, List<ParticleModule> modules, ParticleEmitter emitter)
    {
        var stageNode = new StageNode(name, stage, emitter, modules.Count);
        parent.AddItem(stageNode);
        _tree.Open(stageNode);

        foreach (var module in modules)
        {
            var moduleNode = new ModuleNode(module, stage);
            stageNode.AddItem(moduleNode);
        }
    }

    private void OnItemActivated(object item)
    {
        var selected = item;
        if (selected == null) return;
        
        if (selected is SystemNode systemNode)
            OnSelectionChanged?.Invoke(systemNode.Resource);
        else if (selected is EmitterNode emitterNode)
            OnSelectionChanged?.Invoke(emitterNode.Emitter);
        else if (selected is ModuleNode moduleNode)
            OnSelectionChanged?.Invoke(moduleNode.Module);
    }

    private void OnItemContextMenu(object item)
    {
        var selected = item;
        if (selected == null) return;

        var menu = new Menu(this);

        if (selected is EmitterNode emitterNode)
        {
	        menu.AddOption("Duplicate", "content_copy", () => OnEmitterDuplicated?.Invoke(emitterNode.Emitter));
	        menu.AddSeparator();
	        menu.AddOption("Delete Emitter", "delete", () => OnEmitterDeleted?.Invoke(emitterNode.Emitter));
        }
        else if (selected is ModuleNode moduleNode)
        {
	        var module = moduleNode.Module;

	        menu.AddOption("Duplicate", "content_copy", () => OnModuleDuplicated?.Invoke(module, moduleNode.Stage));

	        menu.AddOption(module.Enabled ? "Disable" : "Enable",
		        module.Enabled ? "visibility_off" : "visibility",
		        () => {
			        module.Enabled = !module.Enabled;
			        OnSystemChanged?.Invoke();
			        RebuildTree();
		        });

	        menu.AddSeparator();
	        menu.AddOption("Delete Module", "delete", () => OnModuleDeleted?.Invoke(module));
        }
        else if (selected is StageNode stageNode)
        {
            menu.AddOption("Add Module", "add", () => OnAddModule?.Invoke(stageNode.Emitter, stageNode.Stage));
        }

        menu.OpenAtCursor();
    }

    // TreeNode classes
    private class SystemNode : TreeNode
    {
        public ParticleResource Resource { get; }
        
        public SystemNode(ParticleResource resource)
        {
            Resource = resource;
        }

        public override void OnPaint(VirtualWidget item)
        {
            PaintSelection(item);
            
            var rect = item.Rect.Shrink(4, 2);
            Paint.SetDefaultFont();
            Paint.SetPen(Theme.Text);
            Paint.DrawIcon(rect, "auto_awesome", 16, TextFlag.LeftCenter);
            
            rect.Left += 24;
            Paint.DrawText(rect, "Particle System", TextFlag.LeftCenter);
        }
    }

    private class EmitterNode : TreeNode
    {
        public ParticleEmitter Emitter { get; }
        
        public EmitterNode(ParticleEmitter emitter)
        {
            Emitter = emitter;
        }

        public override void OnPaint(VirtualWidget item)
        {
            PaintSelection(item);
            
            var rect = item.Rect.Shrink(4, 2);
            Paint.SetDefaultFont();
            Paint.SetPen(Theme.Green);
            Paint.DrawText(rect, "● ", TextFlag.LeftCenter);
            
            rect.Left += 20;
            Paint.SetPen(Theme.Text);
            Paint.DrawText(rect, Emitter.Name, TextFlag.LeftCenter);
        }
    }

    private class StageNode : TreeNode
    {
        public new string Name { get; }
        public ModuleStage Stage { get; }
        public ParticleEmitter Emitter { get; }
        public int Count { get; }
        
        public StageNode(string name, ModuleStage stage, ParticleEmitter emitter, int count)
        {
            Name = name;
            Stage = stage;
            Emitter = emitter;
            Count = count;
        }

        public override void OnPaint(VirtualWidget item)
        {
            PaintSelection(item);
            
            var rect = item.Rect.Shrink(4, 2);
            Paint.SetDefaultFont();
            
            var stageColor = Stage switch
            {
                ModuleStage.Spawn => new Color(1f, 0.6f, 0.2f),
                ModuleStage.Initialize => new Color(0.3f, 0.8f, 0.3f),
                ModuleStage.Update => new Color(0.3f, 0.6f, 1f),
                ModuleStage.Render => new Color(0.9f, 0.3f, 0.9f),
                _ => Theme.Text
            };
            
            Paint.SetPen(stageColor);
            Paint.DrawText(rect, $"{Name} ({Count})", TextFlag.LeftCenter);
            
            var buttonRect = new Rect(rect.Right - 24, rect.Top, 24, rect.Height);
            
            if (buttonRect.IsInside(item.Rect))
            {
                Paint.ClearPen();
                Paint.SetBrush(Theme.ControlBackground.Lighten(0.2f));
                Paint.DrawRect(buttonRect.Shrink(2), 2);
            }
            
            Paint.SetPen(stageColor);
            Paint.DrawIcon(buttonRect, "add", 16, TextFlag.Center);
            
            if (item.Dropping)
            {
                Paint.ClearPen();
                Paint.SetBrush(Theme.Blue.WithAlpha(0.2f));
                Paint.DrawRect(item.Rect, 2);
            }
        }

        public override DropAction OnDragDrop(BaseItemWidget.ItemDragEvent e)
        {
            if (e.Data.Object is not ModuleNode draggedNode)
                return DropAction.Ignore;

            var draggedModule = draggedNode.Module;

            if (draggedNode.Stage != Stage)
                return DropAction.Ignore;

            if (e.IsDrop)
            {
                var targetList = Stage switch
                {
                    ModuleStage.Spawn => Emitter.SpawnModules,
                    ModuleStage.Initialize => Emitter.InitializeModules,
                    ModuleStage.Update => Emitter.UpdateModules,
                    ModuleStage.Render => Emitter.RenderModules,
                    _ => null
                };
                
                if (targetList == null) return DropAction.Ignore;

                targetList.Remove(draggedModule);
                targetList.Add(draggedModule);
                
                if (TreeView.Parent is EmitterList list)
                {
                    list.OnSystemChanged?.Invoke();
                    list.RebuildTree();
                }
            }

            return DropAction.Move;
        }
    }

    private class ModuleNode : TreeNode
    {
        public ParticleModule Module { get; }
        public ModuleStage Stage { get; }
        
        public ModuleNode(ParticleModule module, ModuleStage stage)
        {
            Module = module;
            Stage = stage;
        }

        public override void OnPaint(VirtualWidget item)
        {
            PaintSelection(item);
            
            var displayInfo = DisplayInfo.ForType(Module.GetType());
            var rect = item.Rect.Shrink(4, 2);
            
            Paint.SetDefaultFont();
            Paint.SetPen(Module.Enabled ? Theme.Text : Theme.Text.WithAlpha(0.5f));
            
            if (!string.IsNullOrEmpty(displayInfo.Icon))
            {
                Paint.DrawIcon(rect, displayInfo.Icon, 16, TextFlag.LeftCenter);
                rect.Left += 24;
            }
            
            Paint.DrawText(rect, Module.Name ?? displayInfo.Name, TextFlag.LeftCenter);
            
            if (item.Dropping)
            {
                Paint.ClearPen();
                Paint.SetBrush(Theme.Blue.WithAlpha(0.2f));
                
                if (TreeView.CurrentItemDragEvent.DropEdge.HasFlag(BaseItemWidget.ItemEdge.Top))
                {
                    var droprect = item.Rect;
                    droprect.Top -= 1;
                    droprect.Height = 2;
                    Paint.DrawRect(droprect, 2);
                }
                else if (TreeView.CurrentItemDragEvent.DropEdge.HasFlag(BaseItemWidget.ItemEdge.Bottom))
                {
                    var droprect = item.Rect;
                    droprect.Top = droprect.Bottom - 1;
                    droprect.Height = 2;
                    Paint.DrawRect(droprect, 2);
                }
                else
                {
                    Paint.DrawRect(item.Rect, 2);
                }
            }
        }

        public override bool OnDragStart()
        {
            var drag = new Drag(TreeView);
            drag.Data.Object = this;
            drag.Execute();
            return true;
        }

        public override DropAction OnDragDrop(BaseItemWidget.ItemDragEvent e)
        {
            if (e.Data.Object is not ModuleNode draggedNode)
                return DropAction.Ignore;

            var draggedModule = draggedNode.Module;
            var targetModule = Module;

            if (draggedNode.Stage != Stage)
                return DropAction.Ignore;

            var emitterList = TreeView.Parent as EmitterList;
            if (emitterList?._resource == null) return DropAction.Ignore;

            foreach (var emitter in emitterList._resource.Emitters)
            {
                var moduleList = Stage switch
                {
                    ModuleStage.Spawn => emitter.SpawnModules,
                    ModuleStage.Initialize => emitter.InitializeModules,
                    ModuleStage.Update => emitter.UpdateModules,
                    ModuleStage.Render => emitter.RenderModules,
                    _ => null
                };

                if (moduleList == null) continue;

                var draggedIndex = moduleList.IndexOf(draggedModule);
                var targetIndex = moduleList.IndexOf(targetModule);

                if (draggedIndex == -1 || targetIndex == -1) continue;

                if (e.IsDrop)
                {
                    moduleList.RemoveAt(draggedIndex);
                    
                    if (draggedIndex < targetIndex)
                        targetIndex--;
                    
                    if (e.DropEdge.HasFlag(BaseItemWidget.ItemEdge.Top))
                    {
                        moduleList.Insert(targetIndex, draggedModule);
                    }
                    else if (e.DropEdge.HasFlag(BaseItemWidget.ItemEdge.Bottom))
                    {
                        moduleList.Insert(targetIndex + 1, draggedModule);
                    }
                    else
                    {
                        moduleList.Insert(targetIndex, draggedModule);
                    }

                    emitterList.OnSystemChanged?.Invoke();
                    emitterList.RebuildTree();
                }
                
                return DropAction.Move;
            }

            return DropAction.Ignore;
        }
    }
}

/// <summary>
/// Right panel showing properties - ShaderGraph style
/// </summary>
public class PropertiesWidget : Widget
{
    private Label _titleLabel;
    private Layout _contentLayout;
    private object _currentTarget;
    private SerializedObject _currentSerializedObject;

    public System.Action OnPropertyChanged;

    public PropertiesWidget(Widget parent) : base(parent)
    {
        Layout = Layout.Column();
        Layout.Spacing = 0;

        var header = new Widget(this);
        header.Layout = Layout.Column();
        header.Layout.Margin = 8;
        header.MinimumHeight = 32;
        header.SetStyles("background-color: #1e1e1e;");

        _titleLabel = new Label("Properties", header);
        _titleLabel.SetStyles("font-weight: bold; font-size: 14px;");
        header.Layout.Add(_titleLabel);

        Layout.Add(header);

        _contentLayout = Layout.AddColumn(1);
    }

    public void ShowSystemProperties(ParticleResource resource)
    {
        _currentTarget = resource;
        _titleLabel.Text = "System Properties";
        
        RebuildContent(() => {
            if (resource != null)
            {
                var so = resource.GetSerialized();
                so.OnPropertyChanged += OnSerializedPropertyChanged;
                return so;
            }
            return null;
        });
    }

    public void ShowEmitterProperties(ParticleEmitter emitter)
    {
        _currentTarget = emitter;
        _titleLabel.Text = $"Emitter: {emitter.Name}";
        
        RebuildContent(() => {
            if (emitter != null)
            {
                var so = emitter.GetSerialized();
                so.OnPropertyChanged += OnSerializedPropertyChanged;
                return so;
            }
            return null;
        });
    }

    public void ShowModuleProperties(ParticleModule module)
    {
        _currentTarget = module;
        
        var displayInfo = DisplayInfo.ForType(module.GetType());
        _titleLabel.Text = displayInfo.Name;
        
        RebuildContent(() => {
            if (module != null)
            {
                var so = module.GetSerialized();
                so.OnPropertyChanged += OnSerializedPropertyChanged;
                return so;
            }
            return null;
        });
    }

    private void OnSerializedPropertyChanged(SerializedProperty property)
    {
        if (_currentSerializedObject != null && _currentTarget != null)
        {
            _currentSerializedObject.NoteFinishEdit(property);
        }
        
        OnPropertyChanged?.Invoke();
    }

    private void RebuildContent(System.Func<SerializedObject> getSerializedObject)
    {
        _contentLayout.Clear(true);

        if (_currentSerializedObject != null)
        {
            _currentSerializedObject.OnPropertyChanged -= OnSerializedPropertyChanged;
        }

        var scroll = new ScrollArea(this);
        scroll.Canvas = new Widget(scroll);
        scroll.Canvas.Layout = Layout.Column();
        scroll.Canvas.Layout.Margin = 8;
        scroll.Canvas.Layout.Spacing = 4;
        scroll.Canvas.VerticalSizeMode = SizeMode.CanGrow;
        scroll.Canvas.HorizontalSizeMode = SizeMode.Flexible;

        var so = getSerializedObject();
        if (so != null)
        {
            _currentSerializedObject = so;
            
            var sheet = new ControlSheet();
            sheet.AddObject(so);
            
            scroll.Canvas.Layout.Add(sheet);
            scroll.Canvas.Layout.AddStretchCell();
        }
        else
        {
            _currentSerializedObject = null;
        }

        _contentLayout.Add(scroll);
    }
}
stellawisps.fxbox / UnitTests/LibraryTest.cs
UnitTest library
using Sandbox;

[TestClass]
public partial class LibraryTests
{
	[TestMethod]
	public void SceneTest()
	{
		var scene = new Scene();
		using ( scene.Push() )
		{
			var go = new GameObject();

			Assert.AreEqual( 1, scene.Directory.GameObjectCount );
		}
	}

}
stellawisps.fxbox / FXParameter.cs
Game library
using System;
using Sandbox;
using System.Collections.Generic;
using System.Linq;

namespace fxbox;

/// <summary>
/// A named float parameter that can be used to control particle values
/// </summary>
public class FloatParameter
{
	[Property] public string Name { get; set; } = "Parameter";
	[Property] public float DefaultValue { get; set; } = 1.0f;
	[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();
}

/// <summary>
/// A named vector parameter that can be used to control particle values
/// </summary>
public class VectorParameter
{
	[Property] public string Name { get; set; } = "VectorParameter";
	[Property] public Vector3 DefaultValue { get; set; } = Vector3.One;
	[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();
}

/// <summary>
/// A named color parameter that can be used to control particle values
/// </summary>
public class ColorParameter
{
	[Property] public string Name { get; set; } = "ColorParameter";
	[Property] public ParticleGradient DefaultValue { get; set; } = Color.White;
	[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();
}
stellawisps.fxbox / ParticleResource.cs
Game library
using System;
using Sandbox;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace fxbox;

/// <summary>
/// Particle system resource containing multiple emitters
/// </summary>
[AssetType(Name = "Particle System", Extension = "fx", Category = "FX", Flags = AssetTypeFlags.NoEmbedding)]
public class ParticleResource : GameResource
{
    public bool IsDirty { get; set; } = false;
    
    /// <summary>
    /// All emitters in this particle system
    /// </summary>
    public List<ParticleEmitter> Emitters { get; set; } = new();

    /// <summary>
    /// Named float parameters
    /// </summary>
    [InlineEditor, DisplayName("FloatParameters")] public List<FloatParameter> FloatParameters { get; set; } = new();
    
    /// <summary>
    /// Named vector parameters
    /// </summary>
    [InlineEditor] public List<VectorParameter> VectorParameters { get; set; } = new();
    
    /// <summary>
    /// Named color parameters
    /// </summary>
    [InlineEditor] public List<ColorParameter> ColorParameters { get; set; } = new();

    /// <summary>
    /// Global system properties
    /// </summary>
    public float Duration { get; set; } = 5.0f;
    public bool Looping { get; set; } = true;

    public int Version { get; set; } = 0;
    
    /// <summary>
    /// Preview settings for the editor
    /// </summary>
    public ParticlePreviewSettings PreviewSettings { get; set; } = new();
    
    /// <summary>
    /// Get a float parameter's default value by name
    /// </summary>
    public float GetParameterDefault(string name)
    {
        var param = FloatParameters.FirstOrDefault(p => p.Name == name);
        return param?.DefaultValue ?? 0f;
    }
    
    /// <summary>
    /// Get a vector parameter's default value by name
    /// </summary>
    public Vector3 GetVectorParameterDefault(string name)
    {
        var param = VectorParameters.FirstOrDefault(p => p.Name == name);
        return param?.DefaultValue ?? Vector3.Zero;
    }
    
    /// <summary>
    /// Get a color parameter's default value by name
    /// </summary>
    public ParticleGradient GetColorParameterDefault(string name)
    {
        var param = ColorParameters.FirstOrDefault(p => p.Name == name);
        return param?.DefaultValue ?? Color.White;
    }
    
    /// <summary>
    /// Add a new float parameter
    /// </summary>
    public FloatParameter AddParameter(string name, float defaultValue = 1.0f)
    {
        var param = new FloatParameter
        {
            Name = name,
            DefaultValue = defaultValue
        };
        FloatParameters.Add(param);
        return param;
    }
    
    /// <summary>
    /// Add a new vector parameter
    /// </summary>
    public VectorParameter AddVectorParameter(string name, Vector3 defaultValue)
    {
        var param = new VectorParameter
        {
            Name = name,
            DefaultValue = defaultValue
        };
        VectorParameters.Add(param);
        return param;
    }
    
    /// <summary>
    /// Add a new color parameter
    /// </summary>
    public ColorParameter AddColorParameter(string name, Color defaultValue)
    {
        var param = new ColorParameter
        {
            Name = name,
            DefaultValue = defaultValue
        };
        ColorParameters.Add(param);
        return param;
    }
}

/// <summary>
/// Preview settings for the particle editor
/// </summary>
public class ParticlePreviewSettings
{
    public bool ShowGround { get; set; } = true;
    public bool ShowGrid { get; set; } = true;
    public Color BackgroundColor { get; set; } = new Color(0.1f, 0.1f, 0.15f);
    public float PlaybackSpeed { get; set; } = 1.0f;
}
/// <summary>
/// A single particle emitter with its own spawn and update logic
/// </summary>
public class ParticleEmitter
{
	public string Name { get; set; } = "Emitter";
	[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();
	public bool Enabled { get; set; } = true;
	public int MaxParticles { get; set; } = 1000;

	/// <summary>
	/// Seconds to wait before this emitter starts spawning - lets one emitter in the same
	/// system kick off a few seconds after another. Maps straight onto the native
	/// ParticleEffect.StartDelay, so it's handled by the engine's own particle system rather
	/// than anything FXBox has to gate itself.
	/// </summary>
	public float Delay { get; set; } = 0f;

	/// <summary>
	/// Overrides the system's own Duration for deciding when THIS emitter is finished
	/// (see FXBoxParticleController.IsFinished) - 0 means "use the particle system's own
	/// Duration instead", the same as before this existed. Lets one emitter in a system run
	/// longer or shorter than the rest without touching the system-wide Duration.
	/// </summary>
	public float Duration { get; set; } = 0f;

	/// <summary>
	/// Modules that run when spawning particles
	/// </summary>
	[JsonConverter(typeof(ParticleModuleListConverter)), Hide]
	public List<ParticleModule> SpawnModules { get; set; } = new();

	/// <summary>
	/// Modules that run once when a particle is created
	/// </summary>
	[JsonConverter(typeof(ParticleModuleListConverter)), Hide]
	public List<ParticleModule> InitializeModules { get; set; } = new();

	/// <summary>
	/// Modules that run every frame for each particle
	/// </summary>
	[JsonConverter(typeof(ParticleModuleListConverter)), Hide]
	public List<ParticleModule> UpdateModules { get; set; } = new();

	/// <summary>
	/// Modules that control how particles are rendered
	/// </summary>
	[JsonConverter(typeof(ParticleModuleListConverter)), Hide]
	public List<ParticleModule> RenderModules { get; set; } = new();
}
/// <summary>
/// Base class for all particle modules
/// </summary>
public abstract class ParticleModule
{
    [Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();
    [Hide] public string Name { get; set; }
    [Hide] public bool Enabled { get; set; } = true;

    /// <summary>
    /// What stage this module belongs to
    /// </summary>
    [JsonIgnore]
    public abstract ModuleStage Stage { get; }

    /// <summary>
    /// Execute this module
    /// </summary>
    public abstract void Execute(ParticleExecutionContext context);

    public abstract void Initialize( ParticleExecutionContext context );
}

/// <summary>
/// JSON converter for List of ParticleModule
/// </summary>
public class ParticleModuleListConverter : JsonConverter<List<ParticleModule>>
{
    public override List<ParticleModule> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var list = new List<ParticleModule>();
        
        if (reader.TokenType != JsonTokenType.StartArray)
            throw new JsonException("Expected start of array");
        
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndArray)
                break;
                
            using (var doc = JsonDocument.ParseValue(ref reader))
            {
                var root = doc.RootElement;
                
                // Get the type name
                if (!root.TryGetProperty("$type", out var typeProperty))
                {
                    Log.Warning("Missing $type property for ParticleModule");
                    continue;
                }
                
                var typeName = typeProperty.GetString();
                var type = TypeLibrary.GetType(typeName)?.TargetType;
                
                if (type == null)
                {
                    Log.Warning($"Unknown module type: {typeName}");
                    continue;
                }
                
                // Deserialize to the specific type
                var json = root.GetRawText();
                var module = (ParticleModule)JsonSerializer.Deserialize(json, type, options);
                if (module != null)
                {
                    list.Add(module);
                }
            }
        }
        
        return list;
    }

    public override void Write(Utf8JsonWriter writer, List<ParticleModule> value, JsonSerializerOptions options)
    {
        writer.WriteStartArray();
        
        foreach (var module in value)
        {
            if (module == null) continue;
            
            writer.WriteStartObject();
            
            // Write the type information
            writer.WriteString("$type", module.GetType().FullName);
            
            // Serialize the module
            var json = JsonSerializer.Serialize(module, module.GetType(), options);
            using (var doc = JsonDocument.Parse(json))
            {
                foreach (var property in doc.RootElement.EnumerateObject())
                {
                    property.WriteTo(writer);
                }
            }
            
            writer.WriteEndObject();
        }
        
        writer.WriteEndArray();
    }
}
stellawisps.fxbox / Code/FxboxParticleSystem.cs
Game library
using System;
using Sandbox;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace fxbox;

/// <summary>
/// Component that integrates FXBox particle systems wit s&box native particle components
/// </summary>
public class FXBoxNativeParticleSystem : Component, Component.ExecuteInEditor, ResourceLibrary.IEventListener, Component.ITemporaryEffect
{
    [Property] public ParticleResource ParticleSystem { get; set; }
    
    [Property, Hide] private List<GameObject> Emitters { get; set; } = new();
    
    [Property] public bool PlayOnStart { get; set; } = true;
    [Property] public bool DestroyOnEnd { get; set; }

    public Vector3 Velocity;
    
    /// <summary>
    /// Instance-specific float parameter overrides
    /// </summary>
    [Property, Group("Parameters"), Title("Float Parameters")]
    public Dictionary<string, float> ParameterOverrides { get; set; } = new();
    
    /// <summary>
    /// Instance-specific vector parameter overrides
    /// </summary>
    [Property, Group("Parameters"), Title("Vector Parameters")]
    public Dictionary<string, Vector3> VectorParameterOverrides { get; set; } = new();
    
    /// <summary>
    /// Instance-specific color parameter overrides
    /// </summary>
    [Property, Group("Parameters"), Title("Color Parameters")]
    public Dictionary<string, ParticleGradient> ColorParameterOverrides { get; set; } = new();
    private int _version = 0;

    private Vector3 _lastPosition;

    protected override void OnFixedUpdate()
    {
	    Velocity = (WorldPosition - _lastPosition);
	    _lastPosition = WorldPosition;
        if (ParticleSystem != null && ParticleSystem.Version != _version)
        {
            _version = ParticleSystem.Version;
            UpdateEmitters();
        }

        // Editor previews always loop once every emitter is done, regardless of the
        // resource's own Looping setting - same convention TracerEffect uses ("Editor
        // previews loop instead"). This used to happen by accident: SpawnBurstModule.Execute
        // re-firing every tick meant a "finished" burst kept re-triggering itself forever in
        // the editor. Now that it's fixed to only fire once (see SpawnBurstModule), a
        // finished one-shot system just stays finished with nothing to restart it - so that
        // restart needs to be explicit here instead. IsActive is only checked once ALL
        // emitters are done (not per-emitter), matching "loop the particles after all of
        // them are done emitting" - restarting one on its own the moment IT finishes would
        // desync multiple emitters from each other on every subsequent loop.
        if ( Scene.IsEditor && !IsActive )
        {
            RestartAllEmitters();
        }
    }

    private void RestartAllEmitters()
    {
        foreach ( var emitter in Emitters )
        {
            if ( !emitter.IsValid() ) continue;

            emitter.GetComponent<FXBoxParticleController>()?.ResetTimer();

            // Same mechanism FXBoxNativeParticleSystem.Burst() already uses to manually
            // re-fire an emitter - re-evaluates StartDelay/Burst/Rate from scratch.
            foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>() )
            {
                particleEmitter.ResetEmitter();
            }
        }
    }
    
    // ==================== FLOAT PARAMETERS ====================
    
    /// <summary>
    /// Set a float parameter value for this specific instance
    /// </summary>
    public void Set(string name, float value)
    {
        if (ParticleSystem == null) return;
        
        var param = ParticleSystem.FloatParameters?.FirstOrDefault(p => p.Name == name);
        if (param == null)
        {
            //Log.Warning($"Float parameter '{name}' not found in particle system");
            return;
        }
        
        ParameterOverrides[name] = value;
        UpdateParameterValues();
    }

    public void Burst()
    {
	    foreach ( var emitter in Emitters )
	    {
		    foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>(  ) )
		    {
			    var target = particleEmitter.GetComponent<ParticleEffect>();
			    particleEmitter.ResetEmitter();
		    }
	    }
    }

    public void Burst( Vector3 worldPosition )
    {
	    foreach ( var emitter in Emitters )
	    {
		    foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>(  ) )
		    {
			    var target = particleEmitter.GetComponent<ParticleEffect>();
			    for(int i=0; i< particleEmitter.Burst.Evaluate( Time.Delta, Time.Delta ); i++)
			    {
				    particleEmitter.Emit( target );
			    }
		    }
	    }
	    
	    foreach ( var emitter in Emitters )
	    {
		    foreach ( var particleEffect in emitter.GetComponentsInChildren<Sandbox.ParticleEffect>(  ) )
		    {
			    foreach ( var particle in particleEffect.Particles )
			    {
				    if ( particle.Age <= 0.01f )
				    {
						particle.Position += worldPosition - particleEffect.WorldTransform.Position;
				    }

			    }
		    }
	    }
    }
    
    /// <summary>
    /// Get a float parameter value (override or default)
    /// </summary>
    public float GetFloatParameter(string name)
    {
        if (ParameterOverrides.TryGetValue(name, out float overrideValue))
        {
            return overrideValue;
        }
        
        return ParticleSystem?.GetParameterDefault(name) ?? 0f;
    }
    
    /// <summary>
    /// Reset a float parameter to its default value
    /// </summary>
    public void ResetParameter(string name)
    {
        ParameterOverrides.Remove(name);
        UpdateParameterValues();
    }
    
    // ==================== VECTOR PARAMETERS ====================
    
    /// <summary>
    /// Set a vector parameter value for this specific instance
    /// </summary>
    public void Set(string name, Vector3 value)
    {
        if (ParticleSystem == null) return;
        
        var param = ParticleSystem.VectorParameters?.FirstOrDefault(p => p.Name == name);
        if (param == null)
        {
            //Log.Warning($"Vector parameter '{name}' not found in particle system");
            return;
        }
        
        VectorParameterOverrides[name] = value;
        UpdateParameterValues();
    }
    
    public int GetAliveParticles()
    {
	    var particles = 0;
	    foreach ( var emitter in Emitters )
	    {
		    var target = emitter.GetComponent<ParticleEffect>();
		    particles += target.ParticleCount;
	    }

	    return particles;
    }
    /// <summary>
    /// Get a vector parameter value (override or default)
    /// </summary>
    public Vector3 GetVectorParameter(string name)
    {
        if (VectorParameterOverrides.TryGetValue(name, out Vector3 overrideValue))
        {
            return overrideValue;
        }
        
        return ParticleSystem?.GetVectorParameterDefault(name) ?? Vector3.Zero;
    }
    
    /// <summary>
    /// Reset a vector parameter to its default value
    /// </summary>
    public void ResetVectorParameter(string name)
    {
        VectorParameterOverrides.Remove(name);
        UpdateParameterValues();
    }
    
    // ==================== COLOR PARAMETERS ====================
    
    /// <summary>
    /// Set a color parameter value for this specific instance
    /// </summary>
    public void Set(string name, Color value)
    {
        if (ParticleSystem == null) return;
        
        var param = ParticleSystem.ColorParameters?.FirstOrDefault(p => p.Name == name);
        if (param == null)
        {
            //Log.Warning($"Color parameter '{name}' not found in particle system");
            return;
        }
        
        ColorParameterOverrides[name] = value;
        UpdateParameterValues();
    }
    
    /// <summary>
    /// Get a color parameter value (override or default)
    /// </summary>
    public ParticleGradient GetColorParameter(string name)
    {
        if (ColorParameterOverrides.TryGetValue(name, out ParticleGradient overrideValue))
        {
            return overrideValue;
        }
        
        return ParticleSystem?.GetColorParameterDefault(name) ?? Color.White;
    }
    
    /// <summary>
    /// Reset a color parameter to its default value
    /// </summary>
    public void ResetColorParameter(string name)
    {
        ColorParameterOverrides.Remove(name);
        UpdateParameterValues();
    }
    
    // ==================== GENERAL ====================
    
    /// <summary>
    /// Reset all parameters to their default values
    /// </summary>
    public void ResetAllParameters()
    {
        ParameterOverrides.Clear();
        VectorParameterOverrides.Clear();
        ColorParameterOverrides.Clear();
        UpdateParameterValues();
    }
    
    /// <summary>
    /// Helper buttons for the inspector
    /// </summary>
    [Button("Reset All Parameters")]
    [Group("Parameters")]
    public void ResetAllParametersButton()
    {
        ResetAllParameters();
    }
    
    [Button("Initialize All Parameters")]
    [Group("Parameters")]
    [Description("Copy all parameters from the resource as overrides")]
    public void InitializeParametersFromResource()
    {
        if (ParticleSystem == null) return;
        
        ParameterOverrides.Clear();
        if (ParticleSystem.FloatParameters != null)
        {
            foreach (var param in ParticleSystem.FloatParameters)
            {
                ParameterOverrides[param.Name] = param.DefaultValue;
            }
        }
        
        VectorParameterOverrides.Clear();
        if (ParticleSystem.VectorParameters != null)
        {
            foreach (var param in ParticleSystem.VectorParameters)
            {
                VectorParameterOverrides[param.Name] = param.DefaultValue;
            }
        }
        
        ColorParameterOverrides.Clear();
        if (ParticleSystem.ColorParameters != null)
        {
            foreach (var param in ParticleSystem.ColorParameters)
            {
                ColorParameterOverrides[param.Name] = param.DefaultValue;
            }
        }
    }
    
    /// <summary>
    /// Update parameter values without rebuilding emitters
    /// </summary>
    private void UpdateParameterValues()
    {
        if (ParticleSystem?.Emitters == null) return;
        
        foreach (var emitterObject in Emitters)
        {
            if (!emitterObject.IsValid()) continue;
            
            var controller = emitterObject.GetComponent<FXBoxParticleController>();
            if (controller != null)
            {
                UpdateModulesWithParameters(emitterObject, controller.EmitterData);
            }
        }
    }
    
    public void OnSave(GameResource resource)
    {
        Log.Info("The scene has stopped");
    }
    
    protected override void OnStart()
    {
        UpdateEmitters();

        // TemporaryEffect is what actually destroys this GameObject once IsActive (below)
        // goes false - without one present, nothing ever would, since the per-emitter
        // controllers no longer destroy the root themselves (see FXBoxParticleController.
        // OnUpdate). DestroyAfterSeconds is 0 since the wait for "actually finished" is
        // already handled by WaitForChildEffects walking into our own IsActive below, not by
        // this timer - only DestroyOnEnd opts in at all, and editor previews never destroy
        // themselves (they loop, same as TracerEffect's editor behavior).
        if ( DestroyOnEnd && !Scene.IsEditor )
        {
            var temporaryEffect = GetOrAddComponent<TemporaryEffect>();
            temporaryEffect.DestroyAfterSeconds = 0f;
            temporaryEffect.WaitForChildEffects = true;
        }
    }

    protected override void OnEnabled()
    {
        UpdateEmitters();
        base.OnEnabled();
    }

    protected override void DrawGizmos()
    {
	    Gizmo.Hitbox.Sprite( 0, 50, false );
	    if ( Gizmo.IsHovered  || Gizmo.IsSelected)
	    {
		    Gizmo.Draw.Color = Color.White;
		    if ( Gizmo.IsSelected )
		    {
			    Gizmo.Draw.Color = Color.Yellow;
		    }
	    }
	    else
	    {
		    Gizmo.Draw.Color = Color.Gray;
	    }
	   
	    Gizmo.Draw.Sprite( 0, 50, Texture.Load( "images/particlehover.vtex" ), false );  
    }

    public void UpdateEmitters()
    {
        // Clean up existing emitters
        foreach (var emitter in Emitters)
        {
            emitter?.DestroyImmediate();
        }
        Emitters.Clear();

        if (ParticleSystem?.Emitters == null) return;

        // Create emitters from ParticleResource
        foreach (var emitterData in ParticleSystem.Emitters)
        {
            if (!emitterData.Enabled) continue;

            var emitterObject = new GameObject(GameObject);
            emitterObject.Flags = emitterObject.Flags.WithFlag( GameObjectFlags.Hidden, true );
            emitterObject.Name = emitterData.Name;
            Emitters.Add(emitterObject);

            // Add ParticleEffect component
            var particleEffect = emitterObject.GetOrAddComponent<ParticleEffect>();
            particleEffect.MaxParticles = emitterData.MaxParticles;
            particleEffect.StartDelay = emitterData.Delay;

            // Create native components from modules
            CreateModuleComponents(emitterObject, emitterData);

            // Add controller to handle particle updates
            var controller = emitterObject.GetOrAddComponent<FXBoxParticleController>();
            controller.EmitterData = emitterData;
            controller.ParticleEffect = particleEffect;
            controller.InitializeModules = emitterData.InitializeModules;
            controller.ParticleSystemComponent = this;
            controller.ParticleEffect.ResetEmitters();
            
        }
    }

    private void CreateModuleComponents(GameObject go, ParticleEmitter emitterData)
    {
        // Create components from all modules that implement IParticleComponentCreator
        var allModules = emitterData.SpawnModules
            .Concat(emitterData.InitializeModules)
            .Concat(emitterData.UpdateModules)
            .Concat(emitterData.RenderModules);

        var particleModules = allModules.ToList();
        foreach (var module in particleModules.OfType<IParticleComponentCreator>())
        {
            if (module is ParticleModule pm && pm.Enabled)
            {
                module.CreateComponent(go);
            }
        }

        var context = new ParticleExecutionContext();
        context.Effect = go.GetComponent<ParticleEffect>();
        context.Emitter = go.GetComponent<Sandbox.ParticleEmitter>();
        context.SystemComponent = this;

        foreach (var module in particleModules)
        {
            module.Initialize(context);
        }

        // emitterData.Duration (0 by default) overrides the system-wide Duration for THIS
        // emitter, same as FXBoxParticleController.EffectiveDuration - kept in sync so the
        // native emitter's own duration behavior matches what our controller thinks it does.
        var effectiveDuration = emitterData.Duration > 0f ? emitterData.Duration : ParticleSystem.Duration;

        // Ensure we have at least a basic emitter if none was created
        if (!go.GetComponent<Sandbox.ParticleEmitter>().IsValid())
        {
            var emitter = go.AddComponent<ParticleSphereEmitter>();
            emitter.Duration = effectiveDuration;
            emitter.Loop = ParticleSystem.Looping;
            emitter.DestroyOnEnd = DestroyOnEnd;
        }

        var emitters = go.GetComponentsInChildren<Sandbox.ParticleEmitter>();
        foreach ( var emit in emitters )
        {
	        emit.Duration = effectiveDuration;
	        emit.Loop = ParticleSystem.Looping;
	        emit.DestroyOnEnd = DestroyOnEnd;
        }
    }
    
    private void UpdateModulesWithParameters(GameObject go, ParticleEmitter emitterData)
    {
        var context = new ParticleExecutionContext();
        context.Effect = go.GetComponent<ParticleEffect>();
        context.Emitter = go.GetComponent<Sandbox.ParticleEmitter>();
        context.SystemComponent = this;

        var allModules = emitterData.SpawnModules
            .Concat(emitterData.InitializeModules)
            .Concat(emitterData.UpdateModules)
            .Concat(emitterData.RenderModules);

        foreach (var module in allModules)
        {
            module.Initialize(context);
        }
    }
    
    protected override void OnDisabled()
    {
	    // Clean up existing emitters
	    foreach (var emitter in Emitters)
	    {
		    emitter?.Destroy();
	    }
	    
	    Emitters.Clear();
	    base.OnDisabled();
    }

    // ITemporaryEffect.IsActive - computed live from the child emitters rather than a flag
    // someone has to remember to flip, so it can never go stale relative to what's actually
    // still emitting/alive. True as soon as ANY emitter hasn't finished yet (see
    // FXBoxParticleController.IsFinished); false only once every one of them has.
    public bool IsActive
    {
        get
        {
            foreach ( var emitter in Emitters )
            {
                if ( !emitter.IsValid() ) continue;

                var controller = emitter.GetComponent<FXBoxParticleController>();
                if ( controller.IsValid() && !controller.IsFinished )
                    return true;
            }

            return false;
        }
    }
}

/// <summary>
/// Controller that executes particle update modules on each particle
/// </summary>
public class FXBoxParticleController : ParticleController
{
    [Property, Hide] public ParticleEmitter EmitterData { get; set; }
    [Property, Hide] public new ParticleEffect ParticleEffect { get; set; }
    [Property, Hide] public FXBoxNativeParticleSystem ParticleSystemComponent { get; set; }
    private TimeSince _timeSinceCreated = 0;
    public List<ParticleModule> InitializeModules { get; set; } = new List<ParticleModule>();

    // This emitter is done - past its duration, not looping, and nothing left alive.
    // FXBoxNativeParticleSystem.IsActive (the ITemporaryEffect this whole system exposes)
    // is true as long as ANY emitter's controller reports false here; a TemporaryEffect on
    // the root is what actually destroys things once every one of them finally does, rather
    // than this controller destroying the root itself the moment ITS OWN emitter finishes -
    // that was the bug: a multi-emitter system got torn down as soon as the FIRST emitter
    // to finish was done, not once every emitter actually was.
    //
    // EmitterData.Duration (0 by default) overrides the system-wide Duration for THIS
    // emitter specifically - lets one emitter run longer/shorter than the rest of the
    // system without changing anything system-wide.
    private float EffectiveDuration => EmitterData != null && EmitterData.Duration > 0f
	    ? EmitterData.Duration
	    : (ParticleSystemComponent?.ParticleSystem?.Duration ?? 0f);

    public bool IsFinished =>
	    _timeSinceCreated > EffectiveDuration
	    && !(ParticleSystemComponent?.ParticleSystem?.Looping ?? false)
	    && ParticleEffect.Particles.Count <= 0;

    // Called by FXBoxNativeParticleSystem.RestartAllEmitters (editor-only looping of a
    // non-looping system, once every emitter's finished) alongside the native
    // ParticleEmitter.ResetEmitter() call - IsFinished depends on _timeSinceCreated, so
    // without also resetting this, it would immediately re-evaluate as finished again next
    // tick regardless of the native emitter actually having restarted.
    public void ResetTimer() => _timeSinceCreated = 0;

    protected override void OnUpdate()
    {
	    var context = new ParticleExecutionContext
	    {
		    Particle = null,
		    Effect = ParticleEffect,
		    Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),
		    SystemComponent = ParticleSystemComponent
	    };
	    foreach ( var init in EmitterData.InitializeModules )
	    {
		    init.Execute( context );
	    }
	    foreach ( var spawn in EmitterData.SpawnModules )
	    {
		    spawn.Execute( context );
	    }
    }

    protected override void OnParticleStep(Particle particle, float delta)
    {
        base.OnParticleStep(particle, delta);

        if (EmitterData == null) return;
        
        var context = new ParticleExecutionContext
        {
            Particle = particle,
            Effect = ParticleEffect,
            Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),
            SystemComponent = ParticleSystemComponent
        };
        
        // Execute all update modules that implement IParticleUpdater
        foreach (var module in EmitterData.UpdateModules.OfType<IParticleUpdater>())
        {
            if (module is ParticleModule pm && pm.Enabled)
            {
                module.UpdateParticle(context, delta);
            }
        }
    }
    
    protected override void OnParticleCreated(Particle p)
    {
        p.Position = ParticleEffect.WorldTransform.Position;
        InitializeModules ??= EmitterData?.InitializeModules ?? new List<ParticleModule>();
        
        var context = new ParticleExecutionContext
        {
            Particle = p,
            Effect = ParticleEffect,
            Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),
            SystemComponent = ParticleSystemComponent
        };
        
        foreach (var module in InitializeModules)
        {
            module.Initialize(context);
        }
    }
}

/// <summary>
/// Interface for modules that can create native components
/// </summary>
public interface IParticleComponentCreator
{
    void CreateComponent(GameObject go);
}

/// <summary>
/// Interface for modules that update particles
/// </summary>
public interface IParticleUpdater
{
    void UpdateParticle(ParticleExecutionContext particle, float delta);
}

[Flags]
public enum FXCopyFlags
{
	Rotation = 1,
	Scale = 2,
}
stellawisps.fxbox / FxboxParticleSystem.cs
Game library
using System;
using Sandbox;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace fxbox;

/// <summary>
/// Component that integrates FXBox particle systems wit s&box native particle components
/// </summary>
public class FXBoxNativeParticleSystem : Component, Component.ExecuteInEditor, ResourceLibrary.IEventListener, Component.ITemporaryEffect
{
    [Property] public ParticleResource ParticleSystem { get; set; }
    
    [Property, Hide] private List<GameObject> Emitters { get; set; } = new();
    
    [Property] public bool PlayOnStart { get; set; } = true;
    [Property] public bool DestroyOnEnd { get; set; }

    public Vector3 Velocity;
    
    /// <summary>
    /// Instance-specific float parameter overrides
    /// </summary>
    [Property, Group("Parameters"), Title("Float Parameters")]
    public Dictionary<string, float> ParameterOverrides { get; set; } = new();
    
    /// <summary>
    /// Instance-specific vector parameter overrides
    /// </summary>
    [Property, Group("Parameters"), Title("Vector Parameters")]
    public Dictionary<string, Vector3> VectorParameterOverrides { get; set; } = new();
    
    /// <summary>
    /// Instance-specific color parameter overrides
    /// </summary>
    [Property, Group("Parameters"), Title("Color Parameters")]
    public Dictionary<string, ParticleGradient> ColorParameterOverrides { get; set; } = new();
    private int _version = 0;

    private Vector3 _lastPosition;

    protected override void OnFixedUpdate()
    {
	    Velocity = (WorldPosition - _lastPosition);
	    _lastPosition = WorldPosition;
        if (ParticleSystem != null && ParticleSystem.Version != _version)
        {
            _version = ParticleSystem.Version;
            UpdateEmitters();
        }

        // Editor previews always loop once every emitter is done, regardless of the
        // resource's own Looping setting - same convention TracerEffect uses ("Editor
        // previews loop instead"). This used to happen by accident: SpawnBurstModule.Execute
        // re-firing every tick meant a "finished" burst kept re-triggering itself forever in
        // the editor. Now that it's fixed to only fire once (see SpawnBurstModule), a
        // finished one-shot system just stays finished with nothing to restart it - so that
        // restart needs to be explicit here instead. IsActive is only checked once ALL
        // emitters are done (not per-emitter), matching "loop the particles after all of
        // them are done emitting" - restarting one on its own the moment IT finishes would
        // desync multiple emitters from each other on every subsequent loop.
        if ( Scene.IsEditor && !IsActive )
        {
            RestartAllEmitters();
        }
    }

    private void RestartAllEmitters()
    {
        foreach ( var emitter in Emitters )
        {
            if ( !emitter.IsValid() ) continue;

            emitter.GetComponent<FXBoxParticleController>()?.ResetTimer();

            // Same mechanism FXBoxNativeParticleSystem.Burst() already uses to manually
            // re-fire an emitter - re-evaluates StartDelay/Burst/Rate from scratch.
            foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>() )
            {
                particleEmitter.ResetEmitter();
            }
        }
    }
    
    // ==================== FLOAT PARAMETERS ====================
    
    /// <summary>
    /// Set a float parameter value for this specific instance
    /// </summary>
    public void Set(string name, float value)
    {
        if (ParticleSystem == null) return;
        
        var param = ParticleSystem.FloatParameters?.FirstOrDefault(p => p.Name == name);
        if (param == null)
        {
            //Log.Warning($"Float parameter '{name}' not found in particle system");
            return;
        }
        
        ParameterOverrides[name] = value;
        UpdateParameterValues();
    }

    public void Burst()
    {
	    foreach ( var emitter in Emitters )
	    {
		    foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>(  ) )
		    {
			    var target = particleEmitter.GetComponent<ParticleEffect>();
			    particleEmitter.ResetEmitter();
		    }
	    }
    }

    public void Burst( Vector3 worldPosition )
    {
	    foreach ( var emitter in Emitters )
	    {
		    foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>(  ) )
		    {
			    var target = particleEmitter.GetComponent<ParticleEffect>();
			    for(int i=0; i< particleEmitter.Burst.Evaluate( Time.Delta, Time.Delta ); i++)
			    {
				    particleEmitter.Emit( target );
			    }
		    }
	    }
	    
	    foreach ( var emitter in Emitters )
	    {
		    foreach ( var particleEffect in emitter.GetComponentsInChildren<Sandbox.ParticleEffect>(  ) )
		    {
			    foreach ( var particle in particleEffect.Particles )
			    {
				    if ( particle.Age <= 0.01f )
				    {
						particle.Position += worldPosition - particleEffect.WorldTransform.Position;
				    }

			    }
		    }
	    }
    }
    
    /// <summary>
    /// Get a float parameter value (override or default)
    /// </summary>
    public float GetFloatParameter(string name)
    {
        if (ParameterOverrides.TryGetValue(name, out float overrideValue))
        {
            return overrideValue;
        }
        
        return ParticleSystem?.GetParameterDefault(name) ?? 0f;
    }
    
    /// <summary>
    /// Reset a float parameter to its default value
    /// </summary>
    public void ResetParameter(string name)
    {
        ParameterOverrides.Remove(name);
        UpdateParameterValues();
    }
    
    // ==================== VECTOR PARAMETERS ====================
    
    /// <summary>
    /// Set a vector parameter value for this specific instance
    /// </summary>
    public void Set(string name, Vector3 value)
    {
        if (ParticleSystem == null) return;
        
        var param = ParticleSystem.VectorParameters?.FirstOrDefault(p => p.Name == name);
        if (param == null)
        {
            //Log.Warning($"Vector parameter '{name}' not found in particle system");
            return;
        }
        
        VectorParameterOverrides[name] = value;
        UpdateParameterValues();
    }
    
    public int GetAliveParticles()
    {
	    var particles = 0;
	    foreach ( var emitter in Emitters )
	    {
		    var target = emitter.GetComponent<ParticleEffect>();
		    particles += target.ParticleCount;
	    }

	    return particles;
    }
    /// <summary>
    /// Get a vector parameter value (override or default)
    /// </summary>
    public Vector3 GetVectorParameter(string name)
    {
        if (VectorParameterOverrides.TryGetValue(name, out Vector3 overrideValue))
        {
            return overrideValue;
        }
        
        return ParticleSystem?.GetVectorParameterDefault(name) ?? Vector3.Zero;
    }
    
    /// <summary>
    /// Reset a vector parameter to its default value
    /// </summary>
    public void ResetVectorParameter(string name)
    {
        VectorParameterOverrides.Remove(name);
        UpdateParameterValues();
    }
    
    // ==================== COLOR PARAMETERS ====================
    
    /// <summary>
    /// Set a color parameter value for this specific instance
    /// </summary>
    public void Set(string name, Color value)
    {
        if (ParticleSystem == null) return;
        
        var param = ParticleSystem.ColorParameters?.FirstOrDefault(p => p.Name == name);
        if (param == null)
        {
            //Log.Warning($"Color parameter '{name}' not found in particle system");
            return;
        }
        
        ColorParameterOverrides[name] = value;
        UpdateParameterValues();
    }
    
    /// <summary>
    /// Get a color parameter value (override or default)
    /// </summary>
    public ParticleGradient GetColorParameter(string name)
    {
        if (ColorParameterOverrides.TryGetValue(name, out ParticleGradient overrideValue))
        {
            return overrideValue;
        }
        
        return ParticleSystem?.GetColorParameterDefault(name) ?? Color.White;
    }
    
    /// <summary>
    /// Reset a color parameter to its default value
    /// </summary>
    public void ResetColorParameter(string name)
    {
        ColorParameterOverrides.Remove(name);
        UpdateParameterValues();
    }
    
    // ==================== GENERAL ====================
    
    /// <summary>
    /// Reset all parameters to their default values
    /// </summary>
    public void ResetAllParameters()
    {
        ParameterOverrides.Clear();
        VectorParameterOverrides.Clear();
        ColorParameterOverrides.Clear();
        UpdateParameterValues();
    }
    
    /// <summary>
    /// Helper buttons for the inspector
    /// </summary>
    [Button("Reset All Parameters")]
    [Group("Parameters")]
    public void ResetAllParametersButton()
    {
        ResetAllParameters();
    }
    
    [Button("Initialize All Parameters")]
    [Group("Parameters")]
    [Description("Copy all parameters from the resource as overrides")]
    public void InitializeParametersFromResource()
    {
        if (ParticleSystem == null) return;
        
        ParameterOverrides.Clear();
        if (ParticleSystem.FloatParameters != null)
        {
            foreach (var param in ParticleSystem.FloatParameters)
            {
                ParameterOverrides[param.Name] = param.DefaultValue;
            }
        }
        
        VectorParameterOverrides.Clear();
        if (ParticleSystem.VectorParameters != null)
        {
            foreach (var param in ParticleSystem.VectorParameters)
            {
                VectorParameterOverrides[param.Name] = param.DefaultValue;
            }
        }
        
        ColorParameterOverrides.Clear();
        if (ParticleSystem.ColorParameters != null)
        {
            foreach (var param in ParticleSystem.ColorParameters)
            {
                ColorParameterOverrides[param.Name] = param.DefaultValue;
            }
        }
    }
    
    /// <summary>
    /// Update parameter values without rebuilding emitters
    /// </summary>
    private void UpdateParameterValues()
    {
        if (ParticleSystem?.Emitters == null) return;
        
        foreach (var emitterObject in Emitters)
        {
            if (!emitterObject.IsValid()) continue;
            
            var controller = emitterObject.GetComponent<FXBoxParticleController>();
            if (controller != null)
            {
                UpdateModulesWithParameters(emitterObject, controller.EmitterData);
            }
        }
    }
    
    public void OnSave(GameResource resource)
    {
        Log.Info("The scene has stopped");
    }
    
    protected override void OnStart()
    {
        UpdateEmitters();

        // TemporaryEffect is what actually destroys this GameObject once IsActive (below)
        // goes false - without one present, nothing ever would, since the per-emitter
        // controllers no longer destroy the root themselves (see FXBoxParticleController.
        // OnUpdate). DestroyAfterSeconds is 0 since the wait for "actually finished" is
        // already handled by WaitForChildEffects walking into our own IsActive below, not by
        // this timer - only DestroyOnEnd opts in at all, and editor previews never destroy
        // themselves (they loop, same as TracerEffect's editor behavior).
        if ( DestroyOnEnd && !Scene.IsEditor )
        {
            var temporaryEffect = GetOrAddComponent<TemporaryEffect>();
            temporaryEffect.DestroyAfterSeconds = 0f;
            temporaryEffect.WaitForChildEffects = true;
        }
    }

    protected override void OnEnabled()
    {
        UpdateEmitters();
        base.OnEnabled();
    }

    protected override void DrawGizmos()
    {
	    Gizmo.Hitbox.Sprite( 0, 50, false );
	    if ( Gizmo.IsHovered  || Gizmo.IsSelected)
	    {
		    Gizmo.Draw.Color = Color.White;
		    if ( Gizmo.IsSelected )
		    {
			    Gizmo.Draw.Color = Color.Yellow;
		    }
	    }
	    else
	    {
		    Gizmo.Draw.Color = Color.Gray;
	    }
	   
	    Gizmo.Draw.Sprite( 0, 50, Texture.Load( "images/particlehover.vtex" ), false );  
    }

    public void UpdateEmitters()
    {
        // Clean up existing emitters
        foreach (var emitter in Emitters)
        {
            emitter?.DestroyImmediate();
        }
        Emitters.Clear();

        if (ParticleSystem?.Emitters == null) return;

        // Create emitters from ParticleResource
        foreach (var emitterData in ParticleSystem.Emitters)
        {
            if (!emitterData.Enabled) continue;

            var emitterObject = new GameObject(GameObject);
            emitterObject.Flags = emitterObject.Flags.WithFlag( GameObjectFlags.Hidden, true );
            emitterObject.Name = emitterData.Name;
            Emitters.Add(emitterObject);

            // Add ParticleEffect component
            var particleEffect = emitterObject.GetOrAddComponent<ParticleEffect>();
            particleEffect.MaxParticles = emitterData.MaxParticles;
            particleEffect.StartDelay = emitterData.Delay;

            // Create native components from modules
            CreateModuleComponents(emitterObject, emitterData);

            // Add controller to handle particle updates
            var controller = emitterObject.GetOrAddComponent<FXBoxParticleController>();
            controller.EmitterData = emitterData;
            controller.ParticleEffect = particleEffect;
            controller.InitializeModules = emitterData.InitializeModules;
            controller.ParticleSystemComponent = this;
            controller.ParticleEffect.ResetEmitters();
            
        }
    }

    private void CreateModuleComponents(GameObject go, ParticleEmitter emitterData)
    {
        // Create components from all modules that implement IParticleComponentCreator
        var allModules = emitterData.SpawnModules
            .Concat(emitterData.InitializeModules)
            .Concat(emitterData.UpdateModules)
            .Concat(emitterData.RenderModules);

        var particleModules = allModules.ToList();
        foreach (var module in particleModules.OfType<IParticleComponentCreator>())
        {
            if (module is ParticleModule pm && pm.Enabled)
            {
                module.CreateComponent(go);
            }
        }

        var context = new ParticleExecutionContext();
        context.Effect = go.GetComponent<ParticleEffect>();
        context.Emitter = go.GetComponent<Sandbox.ParticleEmitter>();
        context.SystemComponent = this;

        foreach (var module in particleModules)
        {
            module.Initialize(context);
        }

        // emitterData.Duration (0 by default) overrides the system-wide Duration for THIS
        // emitter, same as FXBoxParticleController.EffectiveDuration - kept in sync so the
        // native emitter's own duration behavior matches what our controller thinks it does.
        var effectiveDuration = emitterData.Duration > 0f ? emitterData.Duration : ParticleSystem.Duration;

        // Ensure we have at least a basic emitter if none was created
        if (!go.GetComponent<Sandbox.ParticleEmitter>().IsValid())
        {
            var emitter = go.AddComponent<ParticleSphereEmitter>();
            emitter.Duration = effectiveDuration;
            emitter.Loop = ParticleSystem.Looping;
            emitter.DestroyOnEnd = DestroyOnEnd;
        }

        var emitters = go.GetComponentsInChildren<Sandbox.ParticleEmitter>();
        foreach ( var emit in emitters )
        {
	        emit.Duration = effectiveDuration;
	        emit.Loop = ParticleSystem.Looping;
	        emit.DestroyOnEnd = DestroyOnEnd;
        }
    }
    
    private void UpdateModulesWithParameters(GameObject go, ParticleEmitter emitterData)
    {
        var context = new ParticleExecutionContext();
        context.Effect = go.GetComponent<ParticleEffect>();
        context.Emitter = go.GetComponent<Sandbox.ParticleEmitter>();
        context.SystemComponent = this;

        var allModules = emitterData.SpawnModules
            .Concat(emitterData.InitializeModules)
            .Concat(emitterData.UpdateModules)
            .Concat(emitterData.RenderModules);

        foreach (var module in allModules)
        {
            module.Initialize(context);
        }
    }
    
    protected override void OnDisabled()
    {
	    // Clean up existing emitters
	    foreach (var emitter in Emitters)
	    {
		    emitter?.Destroy();
	    }
	    
	    Emitters.Clear();
	    base.OnDisabled();
    }

    // ITemporaryEffect.IsActive - computed live from the child emitters rather than a flag
    // someone has to remember to flip, so it can never go stale relative to what's actually
    // still emitting/alive. True as soon as ANY emitter hasn't finished yet (see
    // FXBoxParticleController.IsFinished); false only once every one of them has.
    public bool IsActive
    {
        get
        {
            foreach ( var emitter in Emitters )
            {
                if ( !emitter.IsValid() ) continue;

                var controller = emitter.GetComponent<FXBoxParticleController>();
                if ( controller.IsValid() && !controller.IsFinished )
                    return true;
            }

            return false;
        }
    }
}

/// <summary>
/// Controller that executes particle update modules on each particle
/// </summary>
public class FXBoxParticleController : ParticleController
{
    [Property, Hide] public ParticleEmitter EmitterData { get; set; }
    [Property, Hide] public new ParticleEffect ParticleEffect { get; set; }
    [Property, Hide] public FXBoxNativeParticleSystem ParticleSystemComponent { get; set; }
    private TimeSince _timeSinceCreated = 0;
    public List<ParticleModule> InitializeModules { get; set; } = new List<ParticleModule>();

    // This emitter is done - past its duration, not looping, and nothing left alive.
    // FXBoxNativeParticleSystem.IsActive (the ITemporaryEffect this whole system exposes)
    // is true as long as ANY emitter's controller reports false here; a TemporaryEffect on
    // the root is what actually destroys things once every one of them finally does, rather
    // than this controller destroying the root itself the moment ITS OWN emitter finishes -
    // that was the bug: a multi-emitter system got torn down as soon as the FIRST emitter
    // to finish was done, not once every emitter actually was.
    //
    // EmitterData.Duration (0 by default) overrides the system-wide Duration for THIS
    // emitter specifically - lets one emitter run longer/shorter than the rest of the
    // system without changing anything system-wide.
    private float EffectiveDuration => EmitterData != null && EmitterData.Duration > 0f
	    ? EmitterData.Duration
	    : (ParticleSystemComponent?.ParticleSystem?.Duration ?? 0f);

    public bool IsFinished =>
	    _timeSinceCreated > EffectiveDuration
	    && !(ParticleSystemComponent?.ParticleSystem?.Looping ?? false)
	    && ParticleEffect.Particles.Count <= 0;

    // Called by FXBoxNativeParticleSystem.RestartAllEmitters (editor-only looping of a
    // non-looping system, once every emitter's finished) alongside the native
    // ParticleEmitter.ResetEmitter() call - IsFinished depends on _timeSinceCreated, so
    // without also resetting this, it would immediately re-evaluate as finished again next
    // tick regardless of the native emitter actually having restarted.
    public void ResetTimer() => _timeSinceCreated = 0;

    protected override void OnUpdate()
    {
	    var context = new ParticleExecutionContext
	    {
		    Particle = null,
		    Effect = ParticleEffect,
		    Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),
		    SystemComponent = ParticleSystemComponent
	    };
	    foreach ( var init in EmitterData.InitializeModules )
	    {
		    init.Execute( context );
	    }
	    foreach ( var spawn in EmitterData.SpawnModules )
	    {
		    spawn.Execute( context );
	    }
    }

    protected override void OnParticleStep(Particle particle, float delta)
    {
        base.OnParticleStep(particle, delta);

        if (EmitterData == null) return;
        
        var context = new ParticleExecutionContext
        {
            Particle = particle,
            Effect = ParticleEffect,
            Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),
            SystemComponent = ParticleSystemComponent
        };
        
        // Execute all update modules that implement IParticleUpdater
        foreach (var module in EmitterData.UpdateModules.OfType<IParticleUpdater>())
        {
            if (module is ParticleModule pm && pm.Enabled)
            {
                module.UpdateParticle(context, delta);
            }
        }
    }
    
    protected override void OnParticleCreated(Particle p)
    {
        p.Position = ParticleEffect.WorldTransform.Position;
        InitializeModules ??= EmitterData?.InitializeModules ?? new List<ParticleModule>();
        
        var context = new ParticleExecutionContext
        {
            Particle = p,
            Effect = ParticleEffect,
            Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),
            SystemComponent = ParticleSystemComponent
        };
        
        foreach (var module in InitializeModules)
        {
            module.Initialize(context);
        }
    }
}

/// <summary>
/// Interface for modules that can create native components
/// </summary>
public interface IParticleComponentCreator
{
    void CreateComponent(GameObject go);
}

/// <summary>
/// Interface for modules that update particles
/// </summary>
public interface IParticleUpdater
{
    void UpdateParticle(ParticleExecutionContext particle, float delta);
}

[Flags]
public enum FXCopyFlags
{
	Rotation = 1,
	Scale = 2,
}
stellawisps.fxbox / ParticleValues.cs
Game library
using System;
using Sandbox;
using System.Linq;

namespace fxbox;
public class FXParticleFloat
{
    [Property] public bool UseParameter { get; set; } = false;
    
    [Property, ShowIf(nameof(UseParameter), false)]
    public ParticleFloat Value { get; set; }
    
    [Property, ShowIf(nameof(UseParameter), true)]
    [Description("Select a parameter from the system")]
    public string ParameterName { get; set; }
    
    [Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]
    [Description("Multiplier applied to the parameter value")]
    public float Multiplier { get; set; } = 1.0f;
    
    public float GetValue(FXBoxNativeParticleSystem systemComponent = null)
    {
	    if (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)
	    {
		    return systemComponent.GetFloatParameter(ParameterName) * Multiplier;
	    }
	    
	    var result = Value.Evaluate(Random.Shared.Float(), 3f);
	  
	    return result;
    }
    
    public ParticleFloat ToParticleFloat(FXBoxNativeParticleSystem systemComponent = null)
    {
        if (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)
        {
            // Get the instance-specific value (override or default)
            float value = systemComponent.GetFloatParameter(ParameterName) * Multiplier;
            
            return new ParticleFloat()
            {
                Type = ParticleFloat.ValueType.Constant,
                ConstantValue = value,
                Evaluation = ParticleFloat.EvaluationType.Seed
            };
        }
        
        return Value;
    }
    
    // ==================== OPERATORS ====================
    
    
    public static FXParticleFloat operator *(float a, FXParticleFloat b)
    {
        return b * a; // Commutative
    }
    
    // Division operators
    public static FXParticleFloat operator /(FXParticleFloat a, float b)
{
    if (a == null)
    {
        Log.Warning("Division: a is null");
        return null;
    }
    
    if (b == 0 || MathF.Abs(b) < 0.0001f)
    {
        Log.Warning($"Division by zero or very small number ({b}) in FXParticleFloat");
        return a;
    }
    
  
    var result = a * (1.0f / b);
   
    return result;
}

public static FXParticleFloat operator *(FXParticleFloat a, float b)
{
    if (a == null)
    {
        Log.Warning("Multiplication: a is null");
        return null;
    }
    
    var result = new FXParticleFloat();
    
    if (a.UseParameter)
    {
        result.UseParameter = true;
        result.ParameterName = a.ParameterName;
        result.Multiplier = a.Multiplier * b;
        result.Value = new ParticleFloat()
        {
            Type = ParticleFloat.ValueType.Constant,
            ConstantValue = 0f,
            Evaluation = ParticleFloat.EvaluationType.Seed
        };
    }
    else
    {
        result.UseParameter = false;
        result.Value = ScaleParticleFloat(a.Value, b);
    }
    
    return result;
}

private static ParticleFloat ScaleParticleFloat(ParticleFloat pf, float scale)
{
    
    var result = new ParticleFloat();
    result.Type = pf.Type;
    result.Evaluation = pf.Evaluation;
    
    // Copy Constants FIRST, before setting individual values
    // (or don't copy it at all since we're setting the values manually)
    // result.Constants = pf.Constants;
    
    switch (pf.Type)
    {
        case ParticleFloat.ValueType.Constant:
            result.ConstantValue = pf.ConstantValue * scale;
            break;
            
        case ParticleFloat.ValueType.Range:
            result.ConstantA = pf.ConstantA * scale;
            result.ConstantB = pf.ConstantB * scale;
            break;
            
        case ParticleFloat.ValueType.Curve:
            result.CurveA = ScaleCurve(pf.CurveA, scale);
            result.CurveB = ScaleCurve(pf.CurveB, scale);
            break;
            
        case ParticleFloat.ValueType.CurveRange:
            result.CurveRange = ScaleCurveRange(pf.CurveRange, scale);
            break;
    }
    
    // DON'T copy Constants here - it overwrites our values!
    // result.Constants = pf.Constants;
    
    return result;
}

private static ParticleFloat OffsetParticleFloat(ParticleFloat pf, float offset)
{
    var result = new ParticleFloat();
    result.Type = pf.Type;
    result.Evaluation = pf.Evaluation;
    
    // DON'T copy Constants - it will overwrite our values
    // result.Constants = pf.Constants;
    
    switch (pf.Type)
    {
        case ParticleFloat.ValueType.Constant:
            result.ConstantValue = pf.ConstantValue + offset;
            break;
            
        case ParticleFloat.ValueType.Range:
            result.ConstantA = pf.ConstantA + offset;
            result.ConstantB = pf.ConstantB + offset;
            break;
            
        case ParticleFloat.ValueType.Curve:
            result.CurveA = OffsetCurve(pf.CurveA, offset);
            result.CurveB = OffsetCurve(pf.CurveB, offset);
            break;
            
        case ParticleFloat.ValueType.CurveRange:
            result.CurveRange = OffsetCurveRange(pf.CurveRange, offset);
            break;
    }
    
    // DON'T copy Constants here!
    // result.Constants = pf.Constants;
    
    return result;
}
    
    private static Curve ScaleCurve(Curve curve, float scale)
    {
        var newFrames = curve.Frames.Select(frame => 
            new Curve.Frame(frame.Time, frame.Value * scale)).ToArray();
        
        return new Curve(newFrames);
    }
    
    private static Curve OffsetCurve(Curve curve, float offset)
    {
        var newFrames = curve.Frames.Select(frame => 
            new Curve.Frame(frame.Time, frame.Value + offset)).ToArray();
        
        return new Curve(newFrames);
    }
    
    private static CurveRange ScaleCurveRange(CurveRange range, float scale)
    {
        return new CurveRange
        (
           ScaleCurve(range.A, scale),
            ScaleCurve(range.B, scale)
        );
    }
    
    private static CurveRange OffsetCurveRange(CurveRange range, float offset)
    {
        return new CurveRange
        (
           OffsetCurve(range.A, offset),
            OffsetCurve(range.B, offset)
        );
    }
    
    // ==================== IMPLICIT CONVERSIONS ====================
    
    public static implicit operator FXParticleFloat(float v)
    {
        var fxParticle = new FXParticleFloat();
        fxParticle.Value = new ParticleFloat()
        {
            Type = ParticleFloat.ValueType.Constant,
            ConstantValue = v,
            Evaluation = ParticleFloat.EvaluationType.Seed
        };
        return fxParticle;
    }

    // ==================== CONSTRUCTORS ====================
    
    public FXParticleFloat()
    {
        var particleFloat = new ParticleFloat();
        particleFloat.Type = ParticleFloat.ValueType.Constant;
        particleFloat.ConstantValue = 0.0f;
        particleFloat.Evaluation = ParticleFloat.EvaluationType.Seed;
        particleFloat.CurveA = new Curve();
        particleFloat.CurveB = new Curve();
        particleFloat.Constants = new Vector4();
        this.Value = particleFloat;
    }

    public FXParticleFloat(float a, float b)
    {
        var particleFloat = new ParticleFloat();
        particleFloat.Type = ParticleFloat.ValueType.Range;
        particleFloat.ConstantA = a;
        particleFloat.ConstantB = b;
        particleFloat.Evaluation = ParticleFloat.EvaluationType.Seed;
        particleFloat.CurveA = new Curve();
        particleFloat.CurveB = new Curve();
        particleFloat.Constants = new Vector4();
        this.Value = particleFloat;
    }
}

public class FXParticleVector
{
	[Property] public bool UseParameter { get; set; } = false;
    
	[Property, ShowIf(nameof(UseParameter), false)]
	public ParticleVector3 Value { get; set; } = Vector3.Zero;
    
	[Property, ShowIf(nameof(UseParameter), true)]
	[Description("Select a vector parameter from the system")]
	public string ParameterName { get; set; }
    
	[Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]
	[Description("Multiplier applied to the parameter value")]
	public float Multiplier { get; set; } = 1.0f;
    
	public Vector3 GetValue(Particle particle,FXBoxNativeParticleSystem systemComponent = null)
	{
		if (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)
		{
			return systemComponent.GetVectorParameter(ParameterName) * Multiplier;
		}

		if ( particle == null )
		{
			return Value.Evaluate( Time.Delta, 0, 0, 0 );
		}
		return Value.Evaluate( Time.Delta,particle.Rand(1),particle.Rand(2),particle.Rand(3) );
	}
    
	public static implicit operator FXParticleVector(Vector3 v)
	{
		return new FXParticleVector { Value = v };
	}

	public FXParticleVector()
	{
		Value = Vector3.Zero;
	}

	public FXParticleVector(Vector3 value)
	{
		Value = value;
	}
    
	public FXParticleVector(float x, float y, float z)
	{
		Value = new Vector3(x, y, z);
	}
}

public class FXParticleColor
{
	[Property] public bool UseParameter { get; set; } = false;
    
	[Property, ShowIf(nameof(UseParameter), false)]
	public ParticleGradient Value { get; set; } = Color.White;
    
	[Property, ShowIf(nameof(UseParameter), true)]
	[Description("Select a color parameter from the system")]
	public string ParameterName { get; set; }
    
	[Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]
	[Description("Multiplier applied to the parameter value (affects RGB)")]
	public float Multiplier { get; set; } = 1.0f;
    
	public ParticleGradient GetValue(FXBoxNativeParticleSystem systemComponent = null)
	{
		if (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)
		{
			var color = systemComponent.GetColorParameter(ParameterName);
            
			// Apply multiplier to RGB components
			if (Multiplier != 1.0f)
			{
				return color;
			}
            
			return color;
		}
        
		return Value;
	}
    
	public static implicit operator FXParticleColor(Color c)
	{
		return new FXParticleColor { Value = c };
	}

	public FXParticleColor()
	{
		Value = Color.White;
	}

	public FXParticleColor(Color value)
	{
		Value = value;
	}
    
	public FXParticleColor(float r, float g, float b, float a = 1.0f)
	{
		Value = new Color(r, g, b, a);
	}
}
stellawisps.fxbox / Code/FXParameter.cs
Game library
using System;
using Sandbox;
using System.Collections.Generic;
using System.Linq;

namespace fxbox;

/// <summary>
/// A named float parameter that can be used to control particle values
/// </summary>
public class FloatParameter
{
	[Property] public string Name { get; set; } = "Parameter";
	[Property] public float DefaultValue { get; set; } = 1.0f;
	[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();
}

/// <summary>
/// A named vector parameter that can be used to control particle values
/// </summary>
public class VectorParameter
{
	[Property] public string Name { get; set; } = "VectorParameter";
	[Property] public Vector3 DefaultValue { get; set; } = Vector3.One;
	[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();
}

/// <summary>
/// A named color parameter that can be used to control particle values
/// </summary>
public class ColorParameter
{
	[Property] public string Name { get; set; } = "ColorParameter";
	[Property] public ParticleGradient DefaultValue { get; set; } = Color.White;
	[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();
}
stellawisps.fxbox / Editor/Graph/ParticlePreview.cs
Editor library
using System;
using Editor;
using Sandbox;
using System.Collections.Generic;
using System.Linq;

namespace fxbox.Graph;

/// <summary>
/// Widget that renders a real-time preview of the particle system using native components
/// </summary>
public class ParticlePreview : SceneRenderingWidget
{
    private ParticleResource _resource;
    private GameObject _particleSystemObject;
    private FXBoxNativeParticleSystem _particleSystem;
    private bool _isPlaying = true;
    private float _playbackSpeed = 1.0f;

    private Vector2 _lastCursorPos;
    private Vector2 _angles = new Vector2(45, 30);
    private float _distance = 500f;
    private float _actualDistance = 500f;
    private Vector3 _targetPosition = Vector3.Zero;
    private bool _isOrbiting = false;

    public float PlaybackSpeed
    {
        get => _playbackSpeed;
        set => _playbackSpeed = value;
    }

    public ParticlePreview(Widget parent) : base(parent)
    {
        MouseTracking = true;
        FocusMode = FocusMode.Click;

        Scene = Scene.CreateEditorScene();

        using (Scene.Push())
        {
            // Setup camera
            var cameraGo = new GameObject(true, "camera");
            var camera = cameraGo.GetOrAddComponent<CameraComponent>();
            camera.BackgroundColor = new Color(0.1f, 0.1f, 0.15f);
            camera.ZFar = 10000;
            camera.FieldOfView = 60;
            Camera = camera;

            // Add lighting
            var sunGo = new GameObject(true, "sun");
            var sun = sunGo.GetOrAddComponent<DirectionalLight>();
            sun.WorldRotation = Rotation.FromPitch(50);
            sun.LightColor = Color.White;

            var ambientGo = new GameObject(true, "ambient");
            var ambient = ambientGo.GetOrAddComponent<AmbientLight>();
            ambient.Color = Color.Gray * 0.3f;
        }

        UpdateCameraPosition();
    }

    public void LoadParticleSystem(ParticleResource resource)
    {
        _resource = resource;
        
        // Clean up old system
        if (_particleSystemObject.IsValid())
        {
            _particleSystemObject.Destroy();
        }

        if (_resource != null)
        {
            using (Scene.Push())
            {
                // Create new particle system object
                _particleSystemObject = new GameObject(true, "ParticleSystem");
                _particleSystem = _particleSystemObject.AddComponent<FXBoxNativeParticleSystem>();
                _particleSystem.ParticleSystem = _resource;
                _particleSystem.PlayOnStart = true;
                
                // Initialize the system
                _particleSystem.UpdateEmitters();
            }
            
            Log.Info($"Loaded particle system with {_resource.Emitters?.Count ?? 0} emitters");
        }
    }

    public void SetPlaying(bool playing)
    {
        _isPlaying = playing;
        
        if (_particleSystemObject.IsValid())
        {
            // Enable/disable all particle effects
            foreach (var effect in _particleSystemObject.Children)
            {
                var particleEffect = effect.GetComponent<ParticleEffect>();
                if (particleEffect.IsValid())
                {
                    particleEffect.Enabled = playing;
                }
            }
        }
    }

    public void TogglePlayback()
    {
        _isPlaying = !_isPlaying;
        SetPlaying(_isPlaying);
        Log.Info($"Playback: {(_isPlaying ? "Playing" : "Paused")}");
    }

    public void Restart()
    {
        if (_particleSystemObject.IsValid())
        {
            // Restart by rebuilding the entire system
            LoadParticleSystem(_resource);
            Log.Info("Particle system restarted");
        }
    }

    protected override void PreFrame()
    {
        Scene.EditorTick(RealTime.Now, RealTime.Delta);

        DrawGizmos();
        
        // Force continuous updates
        Update();
    }

    private void DrawGizmos()
    {
        if (_resource == null) return;

        // Draw spawn shape for first emitter
        if (_resource.Emitters.Count > 0)
        {
            var emitter = _resource.Emitters[0];
            var posModule = emitter.InitializeModules.OfType<InitializePositionModule>().FirstOrDefault();
            
            if (posModule != null)
            {
                Gizmo.Draw.Color = Color.Yellow.WithAlpha(0.3f);
                Gizmo.Draw.LineThickness = 2;
                
                switch (posModule.Shape)
                {
                    case InitializePositionModule.SpawnShape.Sphere:
                        Gizmo.Draw.LineSphere(new Sphere(Vector3.Zero, posModule.Radius));
                        break;
                    case InitializePositionModule.SpawnShape.Box:
                        Gizmo.Draw.LineBBox(BBox.FromPositionAndSize(Vector3.Zero, posModule.BoxSize));
                        break;
                    case InitializePositionModule.SpawnShape.Cone:
                        DrawConeGizmo(posModule);
                        break;
                    case InitializePositionModule.SpawnShape.Circle:
                        Gizmo.Draw.LineCircle(Vector3.Zero, Vector3.Forward, posModule.Radius);
                        break;
                    case InitializePositionModule.SpawnShape.Line:
                        Gizmo.Draw.Line(posModule.LineStart, posModule.LineEnd);
                        break;
                }
            }
        }

        DrawGrid();
    }

    private void DrawConeGizmo(InitializePositionModule module)
    {
        var height = module.Radius;
        var radius = MathF.Tan(module.ConeAngle.DegreeToRadian()) * height;
        
        // Draw cone base
        Gizmo.Draw.LineCircle(Vector3.Forward * height, Vector3.Forward, radius);
        
        // Draw cone lines
        var points = 8;
        for (int i = 0; i < points; i++)
        {
            var angle = (i / (float)points) * 360f;
            var dir = new Vector3(
                MathF.Cos(angle.DegreeToRadian()) * radius,
                MathF.Sin(angle.DegreeToRadian()) * radius,
                height
            );
            Gizmo.Draw.Line(Vector3.Zero, dir);
        }
    }

    private void DrawGrid()
    {
        if (_resource?.PreviewSettings?.ShowGrid ?? true)
        {
            Gizmo.Draw.Color = Color.White.WithAlpha(0.1f);
            Gizmo.Draw.LineThickness = 1;
            
            // Draw XY grid
            for (int x = -500; x <= 500; x += 100)
            {
                Gizmo.Draw.Line(new Vector3(x, -500, 0), new Vector3(x, 500, 0));
            }
            for (int y = -500; y <= 500; y += 100)
            {
                Gizmo.Draw.Line(new Vector3(-500, y, 0), new Vector3(500, y, 0));
            }
        }

        if (_resource?.PreviewSettings?.ShowGround ?? true)
        {
            Gizmo.Draw.Color = Color.Gray.WithAlpha(0.2f);
            Gizmo.Draw.LineBBox(new BBox(new Vector3(-500, -500, -1), new Vector3(500, 500, 0)));
        }
        UpdateCameraPosition();
    }

    private void UpdateCameraPosition()
    {
        if (!Camera.IsValid()) return;

        Camera.WorldRotation = new Angles(_angles.y, -_angles.x, 0);
        _actualDistance = _actualDistance.LerpTo( _distance, Time.Delta * 15f );
        Camera.WorldPosition = _targetPosition + Camera.WorldRotation.Backward * _actualDistance;
    }

    protected override void OnMousePress(MouseEvent e)
    {
        base.OnMousePress(e);

        if (e.LeftMouseButton)
        {
            _isOrbiting = true;
            _lastCursorPos = e.ScreenPosition;
        }
    }

    protected override void OnMouseReleased(MouseEvent e)
    {
        base.OnMouseReleased(e);

        if (e.LeftMouseButton)
        {
            _isOrbiting = false;
        }
    }

    protected override void OnMouseMove(MouseEvent e)
    {
        base.OnMouseMove(e);

        if (_isOrbiting)
        {
            var delta = e.ScreenPosition - _lastCursorPos;
            _angles.x += delta.x * 0.3f;
            _angles.y += delta.y * 0.3f;
            _angles.y = _angles.y.Clamp(-90, 90);
            
            //UpdateCameraPosition();
            _lastCursorPos = e.ScreenPosition;
        }
    }

    protected override void OnMouseWheel( WheelEvent e )
    {
	    base.OnMouseWheel(e);
	    _distance -= e.Delta * 1f;
	    _distance = _distance.Clamp(50, 5000);
    }
    
    protected override void OnPaint()
    {
        base.OnPaint();

        // Draw overlay info
        Paint.SetPen(Theme.Text);
        Paint.SetDefaultFont();
        // Count particles from all emitter objects
        int totalParticles = 0;
        int totalMax = _resource?.Emitters.Sum(e => e.MaxParticles) ?? 0;
        
        if (_particleSystemObject.IsValid())
        {
            foreach (var child in _particleSystemObject.Children)
            {
                var effect = child.GetComponent<ParticleEffect>();
                if (effect.IsValid())
                {
                    totalParticles += effect.Particles.Count;
                }
            }
        }
        
        var text = $"Particles: {totalParticles} / {totalMax}";
        Paint.DrawText(new Rect(10, 10, 200, 30), text, TextFlag.LeftTop);
        
        var stateText = _isPlaying ? "PLAYING" : "PAUSED";
        Paint.DrawText(new Rect(10, 40, 200, 30), stateText, TextFlag.LeftTop);
        
        // Draw emitter count
        var emitterText = $"Emitters: {_resource?.Emitters.Count ?? 0}";
        Paint.DrawText(new Rect(10, 70, 200, 30), emitterText, TextFlag.LeftTop);
    }
}
stellawisps.fxbox / ParticleModules.cs
Game library
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;
    }

    // Burst is a one-shot "spawn this many now" value, not a continuous per-tick one like
    // SpawnRateModule's Rate - Execute() runs every single frame (see
    // FXBoxParticleController.OnUpdate), so reassigning Burst here too kept re-arming/
    // re-firing it every tick instead of once, spawning far more than ParticleCount actually
    // configured. Same "Initialize-only, empty Execute" shape ParticleStretchModule already
    // uses above for its own one-shot value.
    public override void Execute(ParticleExecutionContext context)
    {
    }
}

// ==================== 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>
/// Controls how strongly particles follow the emitter in local space
/// </summary>
[Title("Initialize Local Space"), Category("Initialize"), Icon("transform")]
public partial class InitializeLocalSpaceModule : ParticleModule
{
	[Hide]
	public override ModuleStage Stage => ModuleStage.Initialize;

	[Property, Range(0f, 1f)]
	public FXParticleFloat LocalSpace { get; set; } = 0f;

	public override void Initialize( ParticleExecutionContext context )
	{
		context.Effect.LocalSpace = LocalSpace.ToParticleFloat( context.SystemComponent );
	}

	public override void Execute( ParticleExecutionContext context )
	{
	}
}

/// <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 FXParticleFloat RandomVelocity { get; set; } = 0;

    [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.StartVelocity = RandomVelocity.ToParticleFloat();
	    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] public FXParticleVector RotationSpeed { get; set; } = Vector3.Zero;

    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)
    {
	    if ( context.Particle != null )
	    {
		    context.Particle.Angles += RotationSpeed.GetValue( context.Particle ) * 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 FXParticleFloat SpriteScale { get; set; } = 1f;

    [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;
		renderer.Scale = SpriteScale.GetValue();
    }
}

/// <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 bool UseParticleColor { get; set; } = true;
	[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( fxbox );
		renderer.CastShadows = CastShadows;
		renderer.UseParticleColor = UseParticleColor;
	}
}

/// <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;
    }
}
stellawisps.fxbox / Editor/FXParticleControlWidget.cs
Editor library
using Editor;
using fxbox;
using Sandbox;

namespace Editor;

[CustomEditor(typeof(FXParticleFloat))]
public class FXParticleFloatControlWidget : ControlWidget
{
    public Color HighlightColor { get; set; }
    public string Label { get; set; }

    Layout ControlArea;
    SerializedObject Target;
    Button ModeSwitchButton;
    Button ToggleButton;
    
    SerializedProperty ValueProperty;
    SerializedObject ValueTarget;

    public FXParticleFloatControlWidget(SerializedProperty property) : this(property, "f", Theme.Green)
    {
    }

    public FXParticleFloatControlWidget(SerializedProperty property, string label, Color color) : base(property)
    {
        SetSizeMode(SizeMode.Ignore, SizeMode.Default);

        if (!property.TryGetAsObject(out Target))
            return;

        Label = label;
        HighlightColor = color;

        Layout = Layout.Row();
        Layout.Spacing = 3;

        Layout.AddStretchCell();

        // Add toggle button for UseParameter
        ToggleButton = new Button();
        ToggleButton.Text = "P";
        ToggleButton.ToolTip = "Toggle Parameter Mode";
        ToggleButton.FixedWidth = Theme.RowHeight;
        ToggleButton.Pressed = () => ToggleParameterMode();
        ToggleButton.OnPaintOverride = PaintToggleButton;
        Layout.Add(ToggleButton);

        ModeSwitchButton = new Button();
        ModeSwitchButton.Text = "Mode";
        ModeSwitchButton.OnPaintOverride = PaintButton;
        ModeSwitchButton.Pressed = () => OpenPopup(ModeSwitchButton.ScreenRect);
        ModeSwitchButton.FixedWidth = Theme.RowHeight;

        Layout.Add(ModeSwitchButton);

        ControlArea = Layout.AddRow(1);
        ControlArea.Spacing = 2;

        Target.OnPropertyChanged += (p) =>
        {
            if (p.Name == "UseParameter")
            {
                Rebuild();
                return;
            }
            
            if (!Target.GetProperty("UseParameter").GetValue<bool>())
            {
                // Only rebuild for Value changes when not using parameters
                ValueProperty = Target.GetProperty("Value");
                if (ValueProperty.TryGetAsObject(out ValueTarget))
                {
                    if (p.Name == "Type" || p.Name == "Evaluation")
                    {
                        Rebuild();
                    }
                }
            }
        };

        Rebuild();
    }

    private void ToggleParameterMode()
    {
        var useParam = Target.GetProperty("UseParameter");
        useParam.SetValue(!useParam.GetValue<bool>());
        Rebuild();
    }

    private bool PaintToggleButton()
    {
        Paint.Antialiasing = true;

        var useParam = Target.GetProperty("UseParameter").GetValue<bool>();

        if (Paint.HasPressed)
        {
            Paint.SetBrushAndPen(Theme.ControlBackground.Lighten(0.3f));
            Paint.DrawRect(Paint.LocalRect.Shrink(0.5f), Theme.ControlRadius);
            Paint.Pen = useParam ? Theme.Blue.Lighten(0.4f) : Theme.TextControl.Lighten(0.4f);
        }
        else if (Paint.HasMouseOver)
        {
            Paint.SetBrushAndPen(Theme.TextControl.Lighten(0.2f).WithAlpha(0.1f));
            Paint.DrawRect(Paint.LocalRect.Shrink(1), Theme.ControlRadius);
            Paint.Pen = useParam ? Theme.Blue.Lighten(0.5f) : Theme.TextControl.Lighten(0.5f);
        }
        else
        {
            Paint.Pen = useParam ? Theme.Blue : Theme.TextControl.WithAlpha(0.5f);
        }

        Paint.DrawIcon(Paint.LocalRect, "tune", 15, TextFlag.Center);

        return true;
    }

    private void OpenPopup(Rect parentRect)
    {
        // Only show popup if not in parameter mode
        if (Target.GetProperty("UseParameter").GetValue<bool>())
            return;

        ValueProperty = Target.GetProperty("Value");
        if (!ValueProperty.TryGetAsObject(out ValueTarget))
            return;

        var popup = new FXParticleFloatConfigPopup(ValueTarget, this);
        popup.Position = parentRect.BottomRight;
        popup.AdjustSize();
        popup.Position -= new Vector2(popup.Width, 0);

        popup.Show();
        popup.ConstrainToScreen();
    }

    bool PaintButton()
    {
        Paint.Antialiasing = true;

        var useParam = Target.GetProperty("UseParameter").GetValue<bool>();
        
        // Disable button appearance if in parameter mode
        if (useParam)
        {
            Paint.Pen = Theme.TextControl.WithAlpha(0.2f);
            Paint.DrawIcon(Paint.LocalRect, "block", 11, TextFlag.Center);
            return true;
        }

        ValueProperty = Target.GetProperty("Value");
        if (!ValueProperty.TryGetAsObject(out ValueTarget))
            return true;

        if (Paint.HasPressed)
        {
            Paint.SetBrushAndPen(Theme.ControlBackground.Lighten(0.3f));
            Paint.DrawRect(Paint.LocalRect.Shrink(0.5f), Theme.ControlRadius);
            Paint.Pen = Theme.TextControl.Lighten(0.4f);
        }
        else if (Paint.HasMouseOver)
        {
            Paint.SetBrushAndPen(Theme.TextControl.Lighten(0.2f).WithAlpha(0.1f));
            Paint.DrawRect(Paint.LocalRect.Shrink(1), Theme.ControlRadius);
            Paint.Pen = Theme.TextControl.Lighten(0.5f);
        }
        else
        {
            Paint.Pen = Theme.TextControl.WithAlpha(0.5f);
        }

        var type = ValueTarget.GetProperty("Type").GetValue<ParticleFloat.ValueType>();
        var eval = ValueTarget.GetProperty("Evaluation").GetValue<ParticleFloat.EvaluationType>();

        var icon = "people";
        float iconSize = 15;

        if (type == ParticleFloat.ValueType.Constant)
        {
            icon = "radio_button_unchecked";
            Paint.Pen = Paint.Pen.WithAlpha(0.3f);
            iconSize = 11;
        }
        else
        {
            if (eval == ParticleFloat.EvaluationType.Seed)
            {
                icon = "scatter_plot";
            }

            if (eval == ParticleFloat.EvaluationType.Life)
            {
                icon = "play_arrow";
            }

            if (eval == ParticleFloat.EvaluationType.Frame)
            {
                icon = "casino";
            }
        }

        Paint.DrawIcon(Paint.LocalRect, icon, iconSize, TextFlag.Center);

        return true;
    }

    void Rebuild()
    {
        ControlArea.Clear(true);

        var useParam = Target.GetProperty("UseParameter").GetValue<bool>();

        if (useParam)
        {
            // Parameter mode - show parameter selector and multiplier
            var paramName = Target.GetProperty("ParameterName");
            var multiplier = Target.GetProperty("Multiplier");

            var paramControl = new ParameterNameControlWidget(paramName);
            ControlArea.Add(paramControl, 1);

            var multControl = new FloatControlWidget(multiplier)
            {
                HighlightColor = Theme.Highlight,
                Label = "×"
            };
            ControlArea.Add(multControl);
        }
        else
        {
            // Normal mode - show particle float controls
            ValueProperty = Target.GetProperty("Value");
            if (ValueProperty.TryGetAsObject(out ValueTarget))
            {
                var type = ValueTarget.GetProperty("Type").GetValue<ParticleFloat.ValueType>();
                var eval = ValueTarget.GetProperty("Evaluation").GetValue<ParticleFloat.EvaluationType>();
                RebuildForType(type, eval);
            }
        }

        Update();
    }

    void RebuildForType(ParticleFloat.ValueType type, ParticleFloat.EvaluationType eval)
    {
        ModeSwitchButton.ToolTip = GetTypeName(type, eval);

        if (type == ParticleFloat.ValueType.Constant)
        {
            var control = new FloatControlWidget(ValueTarget.GetProperty("ConstantValue"))
            {
                HighlightColor = HighlightColor,
                Label = Label
            };
            ControlArea.Add(control);
        }

        if (type == ParticleFloat.ValueType.Range)
        {
            var controlA = new FloatControlWidget(ValueTarget.GetProperty("ConstantA"))
            {
                HighlightColor = HighlightColor,
                Label = Label
            };
            ControlArea.Add(controlA);

            var controlB = new FloatControlWidget(ValueTarget.GetProperty("ConstantB"))
            {
                HighlightColor = HighlightColor,
                Label = Label
            };
            ControlArea.Add(controlB);
        }

        if (type == ParticleFloat.ValueType.Curve)
        {
            var controlA = new CurveControlWidget(ValueTarget.GetProperty("CurveA"))
            {
                HighlightColor = HighlightColor
            };
            ControlArea.Add(controlA);
        }

        if (type == ParticleFloat.ValueType.CurveRange)
        {
            var controlA = new CurveRangeControlWidget(ValueTarget.GetProperty("CurveRange"))
            {
                HighlightColor = HighlightColor
            };
            ControlArea.Add(controlA);
        }
    }

    string GetTypeName(ParticleFloat.ValueType type, ParticleFloat.EvaluationType eval)
    {
        switch (type)
        {
            case ParticleFloat.ValueType.Constant:
                return "Constant value";
            case ParticleFloat.ValueType.Curve:
                {
                    switch (eval)
                    {
                        case ParticleFloat.EvaluationType.Seed:
                            return "Random from curve per particle";
                        case ParticleFloat.EvaluationType.Frame:
                            return "Random from curve";
                        case ParticleFloat.EvaluationType.Life:
                            return "Curve over lifetime";
                        default:
                            return "Unknown";
                    }
                }
            case ParticleFloat.ValueType.CurveRange:
                switch (eval)
                {
                    case ParticleFloat.EvaluationType.Seed:
                        return "Random from range, per particle";
                    case ParticleFloat.EvaluationType.Frame:
                        return "Random from range (per frame)";
                    case ParticleFloat.EvaluationType.Life:
                        return "path between curve over lifetime, per particle";
                    default:
                        return "Unknown";
                }
            case ParticleFloat.ValueType.Range:
                {
                    switch (eval)
                    {
                        case ParticleFloat.EvaluationType.Seed:
                            return "Between range, per particle";
                        case ParticleFloat.EvaluationType.Frame:
                            return "Random between range (per frame)";
                        case ParticleFloat.EvaluationType.Life:
                            return "Lerp between range over lifetime";
                        default:
                            return "Unknown";
                    }
                }
        }

        return "Unknown Combo";
    }

    protected override void OnPaint()
    {
    }
}

file class FXParticleFloatConfigPopup : PopupWidget
{
    SerializedObject SerializedObject;
    SerializedProperty Type;
    SerializedProperty Eval;

    public FXParticleFloatConfigPopup(SerializedObject target, Widget parent) : base(parent)
    {
        Layout = Layout.Column();
        Layout.Spacing = 8;
        Layout.Margin = 16;

        SerializedObject = target;
        Type = target.GetProperty("Type");
        Eval = target.GetProperty("Evaluation");

        AddQuickModes(Type.GetValue<ParticleFloat.ValueType>(), Eval.GetValue<ParticleFloat.EvaluationType>());
    }

    void AddQuickModes(ParticleFloat.ValueType type, ParticleFloat.EvaluationType eval)
    {
        var grid = new GridLayout();
        grid.Spacing = 8;

        grid.AddCell(0, 0, MakeQuickMode("radio_button_unchecked", "Constant", "Value is constant. It stays the same. It doesn't change", ParticleFloat.ValueType.Constant, ParticleFloat.EvaluationType.Life));
        grid.AddCell(1, 0, MakeQuickMode("casino", "Random", "Choose a value between two constants every frame", ParticleFloat.ValueType.Range, ParticleFloat.EvaluationType.Frame));

        grid.AddCell(0, 1, MakeQuickMode("hdr_strong", "Range", "Choose a value between two constants", ParticleFloat.ValueType.Range, ParticleFloat.EvaluationType.Seed));
        grid.AddCell(1, 1, MakeQuickMode("animation", "Lerp", "Lerp between two values over the lifetime of the particle", ParticleFloat.ValueType.Range, ParticleFloat.EvaluationType.Life));

        grid.AddCell(0, 2, MakeQuickMode("show_chart", "Curve", "Get the value by querying a curve over the particle's lifetime", ParticleFloat.ValueType.Curve, ParticleFloat.EvaluationType.Life));
        grid.AddCell(1, 2, MakeQuickMode("area_chart", "Curve with Range", "Choose a path between two curves - over the lifetime of the particle", ParticleFloat.ValueType.CurveRange, ParticleFloat.EvaluationType.Life));

        Layout.Add(grid);
    }

    private Widget MakeQuickMode(string icon, string label, string description, ParticleFloat.ValueType t, ParticleFloat.EvaluationType e)
    {
        var b = new Widget();

        b.Cursor = CursorShape.Finger;
        b.Layout = new IconTitleDescriptionLayout(icon, label, description);
        b.Layout.Margin = new Sandbox.UI.Margin(8, 4);
        b.SetStyles("color: #ffffff;");

        bool isCurrent = t == Type.GetValue<ParticleFloat.ValueType>() && e == Eval.GetValue<ParticleFloat.EvaluationType>();

        b.MouseClick += () =>
        {
            Type.SetValue(t);
            Eval.SetValue(e);
            Close();
        };

        b.OnPaintOverride = () =>
        {
            if (isCurrent || Paint.HasMouseOver)
            {
                Paint.SetBrushAndPen(Theme.Blue.Darken(0.5f).WithAlpha(0.5f));
                Paint.DrawRect(Paint.LocalRect, 4);
            }

            return true;
        };

        return b;
    }
}

file class IconTitleDescriptionLayout : GridLayout
{
    public IconTitleDescriptionLayout(string icon, string title, string description)
    {
        VerticalSpacing = 0;
        HorizontalSpacing = 8;

        var iconLabel = AddCell(0, 0, new IconButton(icon) { Background = Color.Transparent, IconSize = 33, FixedSize = 40, TransparentForMouseEvents = true }, ySpan: 2);
        var titleLabel = AddCell(1, 0, new Label(title));
        var descLabel = AddCell(1, 1, new Label(description) { WordWrap = true });

        titleLabel.SetStyles("font-size: 13px; font-family: Poppins; font-weight: bold;");
        descLabel.SetStyles("font-size: 9px; font-family: Poppins;");
        descLabel.SetEffectOpacity(0.5f);
    }
}

[CustomEditor(typeof(FXParticleVector))]
public class FXParticleVectorControlWidget : ControlWidget
{
    private SerializedObject Target;
    private Button ToggleButton;
    private Layout ControlArea;

    public FXParticleVectorControlWidget(SerializedProperty property) : base(property)
    {
        SetSizeMode(SizeMode.Ignore, SizeMode.Default);

        if (!property.TryGetAsObject(out Target))
            return;

        Layout = Layout.Row();
        Layout.Spacing = 3;

        Layout.AddStretchCell();

        // Add toggle button for UseParameter
        ToggleButton = new Button();
        ToggleButton.Text = "P";
        ToggleButton.ToolTip = "Toggle Parameter Mode";
        ToggleButton.FixedWidth = Theme.RowHeight;
        ToggleButton.Pressed = () => ToggleParameterMode();
        ToggleButton.OnPaintOverride = PaintToggleButton;
        Layout.Add(ToggleButton);

        ControlArea = Layout.AddRow(1);
        ControlArea.Spacing = 2;

        Target.OnPropertyChanged += (p) =>
        {
            if (p.Name == "UseParameter")
            {
                Rebuild();
            }
        };

        Rebuild();
    }

    private void ToggleParameterMode()
    {
        var useParam = Target.GetProperty("UseParameter");
        useParam.SetValue(!useParam.GetValue<bool>());
        Rebuild();
    }

    private bool PaintToggleButton()
    {
        Paint.Antialiasing = true;

        var useParam = Target.GetProperty("UseParameter").GetValue<bool>();

        if (Paint.HasPressed)
        {
            Paint.SetBrushAndPen(Theme.ControlBackground.Lighten(0.3f));
            Paint.DrawRect(Paint.LocalRect.Shrink(0.5f), Theme.ControlRadius);
            Paint.Pen = useParam ? Theme.Blue.Lighten(0.4f) : Theme.TextControl.Lighten(0.4f);
        }
        else if (Paint.HasMouseOver)
        {
            Paint.SetBrushAndPen(Theme.TextControl.Lighten(0.2f).WithAlpha(0.1f));
            Paint.DrawRect(Paint.LocalRect.Shrink(1), Theme.ControlRadius);
            Paint.Pen = useParam ? Theme.Blue.Lighten(0.5f) : Theme.TextControl.Lighten(0.5f);
        }
        else
        {
            Paint.Pen = useParam ? Theme.Blue : Theme.TextControl.WithAlpha(0.5f);
        }

        Paint.DrawIcon(Paint.LocalRect, "tune", 15, TextFlag.Center);

        return true;
    }

    void Rebuild()
    {
        ControlArea.Clear(true);

        var useParam = Target.GetProperty("UseParameter").GetValue<bool>();

        if (useParam)
        {
            // Parameter mode - show parameter selector and multiplier
            var paramName = Target.GetProperty("ParameterName");
            var multiplier = Target.GetProperty("Multiplier");

            var paramControl = new VectorParameterNameControlWidget(paramName);
            ControlArea.Add(paramControl, 1);

            var multControl = new FloatControlWidget(multiplier)
            {
                HighlightColor = Theme.Highlight,
                Label = "×"
            };
            ControlArea.Add(multControl);
        }
        else
        {
            // Normal mode - show vector control
            var valueProp = Target.GetProperty("Value");
            var control = ControlWidget.Create(valueProp);
            if (control != null)
            {
                ControlArea.Add(control, 1);
            }
        }

        Update();
    }

    protected override void OnPaint()
    {
    }
}

[CustomEditor(typeof(FXParticleColor))]
public class FXParticleColorControlWidget : ControlWidget
{
    private SerializedObject Target;
    private Button ToggleButton;
    private Layout ControlArea;

    public FXParticleColorControlWidget(SerializedProperty property) : base(property)
    {
        SetSizeMode(SizeMode.Ignore, SizeMode.Default);

        if (!property.TryGetAsObject(out Target))
            return;

        Layout = Layout.Row();
        Layout.Spacing = 3;

        Layout.AddStretchCell();

        // Add toggle button for UseParameter
        ToggleButton = new Button();
        ToggleButton.Text = "P";
        ToggleButton.ToolTip = "Toggle Parameter Mode";
        ToggleButton.FixedWidth = Theme.RowHeight;
        ToggleButton.Pressed = () => ToggleParameterMode();
        ToggleButton.OnPaintOverride = PaintToggleButton;
        Layout.Add(ToggleButton);

        ControlArea = Layout.AddRow(1);
        ControlArea.Spacing = 2;

        Target.OnPropertyChanged += (p) =>
        {
            if (p.Name == "UseParameter")
            {
                Rebuild();
            }
        };

        Rebuild();
    }

    private void ToggleParameterMode()
    {
        var useParam = Target.GetProperty("UseParameter");
        useParam.SetValue(!useParam.GetValue<bool>());
        Rebuild();
    }

    private bool PaintToggleButton()
    {
        Paint.Antialiasing = true;

        var useParam = Target.GetProperty("UseParameter").GetValue<bool>();

        if (Paint.HasPressed)
        {
            Paint.SetBrushAndPen(Theme.ControlBackground.Lighten(0.3f));
            Paint.DrawRect(Paint.LocalRect.Shrink(0.5f), Theme.ControlRadius);
            Paint.Pen = useParam ? Theme.Blue.Lighten(0.4f) : Theme.TextControl.Lighten(0.4f);
        }
        else if (Paint.HasMouseOver)
        {
            Paint.SetBrushAndPen(Theme.TextControl.Lighten(0.2f).WithAlpha(0.1f));
            Paint.DrawRect(Paint.LocalRect.Shrink(1), Theme.ControlRadius);
            Paint.Pen = useParam ? Theme.Blue.Lighten(0.5f) : Theme.TextControl.Lighten(0.5f);
        }
        else
        {
            Paint.Pen = useParam ? Theme.Blue : Theme.TextControl.WithAlpha(0.5f);
        }

        Paint.DrawIcon(Paint.LocalRect, "tune", 15, TextFlag.Center);

        return true;
    }

    void Rebuild()
    {
        ControlArea.Clear(true);

        var useParam = Target.GetProperty("UseParameter").GetValue<bool>();

        if (useParam)
        {
            // Parameter mode - show parameter selector and multiplier
            var paramName = Target.GetProperty("ParameterName");
            var multiplier = Target.GetProperty("Multiplier");

            var paramControl = new ColorParameterNameControlWidget(paramName);
            ControlArea.Add(paramControl, 1);

            var multControl = new FloatControlWidget(multiplier)
            {
                HighlightColor = Theme.Highlight,
                Label = "×"
            };
            ControlArea.Add(multControl);
        }
        else
        {
            // Normal mode - show color control
            var valueProp = Target.GetProperty("Value");
            var control = ControlWidget.Create(valueProp);
            if (control != null)
            {
                ControlArea.Add(control, 1);
            }
        }

        Update();
    }

    protected override void OnPaint()
    {
    }
}
stellawisps.fxbox / Code/ParticleResource.cs
Game library
using System;
using Sandbox;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace fxbox;

/// <summary>
/// Particle system resource containing multiple emitters
/// </summary>
[AssetType(Name = "Particle System", Extension = "fx", Category = "FX", Flags = AssetTypeFlags.NoEmbedding)]
public class ParticleResource : GameResource
{
    public bool IsDirty { get; set; } = false;
    
    /// <summary>
    /// All emitters in this particle system
    /// </summary>
    public List<ParticleEmitter> Emitters { get; set; } = new();

    /// <summary>
    /// Named float parameters
    /// </summary>
    [InlineEditor, DisplayName("FloatParameters")] public List<FloatParameter> FloatParameters { get; set; } = new();
    
    /// <summary>
    /// Named vector parameters
    /// </summary>
    [InlineEditor] public List<VectorParameter> VectorParameters { get; set; } = new();
    
    /// <summary>
    /// Named color parameters
    /// </summary>
    [InlineEditor] public List<ColorParameter> ColorParameters { get; set; } = new();

    /// <summary>
    /// Global system properties
    /// </summary>
    public float Duration { get; set; } = 5.0f;
    public bool Looping { get; set; } = true;

    public int Version { get; set; } = 0;
    
    /// <summary>
    /// Preview settings for the editor
    /// </summary>
    public ParticlePreviewSettings PreviewSettings { get; set; } = new();
    
    /// <summary>
    /// Get a float parameter's default value by name
    /// </summary>
    public float GetParameterDefault(string name)
    {
        var param = FloatParameters.FirstOrDefault(p => p.Name == name);
        return param?.DefaultValue ?? 0f;
    }
    
    /// <summary>
    /// Get a vector parameter's default value by name
    /// </summary>
    public Vector3 GetVectorParameterDefault(string name)
    {
        var param = VectorParameters.FirstOrDefault(p => p.Name == name);
        return param?.DefaultValue ?? Vector3.Zero;
    }
    
    /// <summary>
    /// Get a color parameter's default value by name
    /// </summary>
    public ParticleGradient GetColorParameterDefault(string name)
    {
        var param = ColorParameters.FirstOrDefault(p => p.Name == name);
        return param?.DefaultValue ?? Color.White;
    }
    
    /// <summary>
    /// Add a new float parameter
    /// </summary>
    public FloatParameter AddParameter(string name, float defaultValue = 1.0f)
    {
        var param = new FloatParameter
        {
            Name = name,
            DefaultValue = defaultValue
        };
        FloatParameters.Add(param);
        return param;
    }
    
    /// <summary>
    /// Add a new vector parameter
    /// </summary>
    public VectorParameter AddVectorParameter(string name, Vector3 defaultValue)
    {
        var param = new VectorParameter
        {
            Name = name,
            DefaultValue = defaultValue
        };
        VectorParameters.Add(param);
        return param;
    }
    
    /// <summary>
    /// Add a new color parameter
    /// </summary>
    public ColorParameter AddColorParameter(string name, Color defaultValue)
    {
        var param = new ColorParameter
        {
            Name = name,
            DefaultValue = defaultValue
        };
        ColorParameters.Add(param);
        return param;
    }
}

/// <summary>
/// Preview settings for the particle editor
/// </summary>
public class ParticlePreviewSettings
{
    public bool ShowGround { get; set; } = true;
    public bool ShowGrid { get; set; } = true;
    public Color BackgroundColor { get; set; } = new Color(0.1f, 0.1f, 0.15f);
    public float PlaybackSpeed { get; set; } = 1.0f;
}
/// <summary>
/// A single particle emitter with its own spawn and update logic
/// </summary>
public class ParticleEmitter
{
	public string Name { get; set; } = "Emitter";
	[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();
	public bool Enabled { get; set; } = true;
	public int MaxParticles { get; set; } = 1000;

	/// <summary>
	/// Seconds to wait before this emitter starts spawning - lets one emitter in the same
	/// system kick off a few seconds after another. Maps straight onto the native
	/// ParticleEffect.StartDelay, so it's handled by the engine's own particle system rather
	/// than anything FXBox has to gate itself.
	/// </summary>
	public float Delay { get; set; } = 0f;

	/// <summary>
	/// Overrides the system's own Duration for deciding when THIS emitter is finished
	/// (see FXBoxParticleController.IsFinished) - 0 means "use the particle system's own
	/// Duration instead", the same as before this existed. Lets one emitter in a system run
	/// longer or shorter than the rest without touching the system-wide Duration.
	/// </summary>
	public float Duration { get; set; } = 0f;

	/// <summary>
	/// Modules that run when spawning particles
	/// </summary>
	[JsonConverter(typeof(ParticleModuleListConverter)), Hide]
	public List<ParticleModule> SpawnModules { get; set; } = new();

	/// <summary>
	/// Modules that run once when a particle is created
	/// </summary>
	[JsonConverter(typeof(ParticleModuleListConverter)), Hide]
	public List<ParticleModule> InitializeModules { get; set; } = new();

	/// <summary>
	/// Modules that run every frame for each particle
	/// </summary>
	[JsonConverter(typeof(ParticleModuleListConverter)), Hide]
	public List<ParticleModule> UpdateModules { get; set; } = new();

	/// <summary>
	/// Modules that control how particles are rendered
	/// </summary>
	[JsonConverter(typeof(ParticleModuleListConverter)), Hide]
	public List<ParticleModule> RenderModules { get; set; } = new();
}
/// <summary>
/// Base class for all particle modules
/// </summary>
public abstract class ParticleModule
{
    [Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();
    [Hide] public string Name { get; set; }
    [Hide] public bool Enabled { get; set; } = true;

    /// <summary>
    /// What stage this module belongs to
    /// </summary>
    [JsonIgnore]
    public abstract ModuleStage Stage { get; }

    /// <summary>
    /// Execute this module
    /// </summary>
    public abstract void Execute(ParticleExecutionContext context);

    public abstract void Initialize( ParticleExecutionContext context );
}

/// <summary>
/// JSON converter for List of ParticleModule
/// </summary>
public class ParticleModuleListConverter : JsonConverter<List<ParticleModule>>
{
    public override List<ParticleModule> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var list = new List<ParticleModule>();
        
        if (reader.TokenType != JsonTokenType.StartArray)
            throw new JsonException("Expected start of array");
        
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndArray)
                break;
                
            using (var doc = JsonDocument.ParseValue(ref reader))
            {
                var root = doc.RootElement;
                
                // Get the type name
                if (!root.TryGetProperty("$type", out var typeProperty))
                {
                    Log.Warning("Missing $type property for ParticleModule");
                    continue;
                }
                
                var typeName = typeProperty.GetString();
                var type = TypeLibrary.GetType(typeName)?.TargetType;
                
                if (type == null)
                {
                    Log.Warning($"Unknown module type: {typeName}");
                    continue;
                }
                
                // Deserialize to the specific type
                var json = root.GetRawText();
                var module = (ParticleModule)JsonSerializer.Deserialize(json, type, options);
                if (module != null)
                {
                    list.Add(module);
                }
            }
        }
        
        return list;
    }

    public override void Write(Utf8JsonWriter writer, List<ParticleModule> value, JsonSerializerOptions options)
    {
        writer.WriteStartArray();
        
        foreach (var module in value)
        {
            if (module == null) continue;
            
            writer.WriteStartObject();
            
            // Write the type information
            writer.WriteString("$type", module.GetType().FullName);
            
            // Serialize the module
            var json = JsonSerializer.Serialize(module, module.GetType(), options);
            using (var doc = JsonDocument.Parse(json))
            {
                foreach (var property in doc.RootElement.EnumerateObject())
                {
                    property.WriteTo(writer);
                }
            }
            
            writer.WriteEndObject();
        }
        
        writer.WriteEndArray();
    }
}
stellawisps.fxbox / .obj/__compiler_extra.cs
Game library
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "FXBox" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "fxbox" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "stellawisps" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "stellawisps.fxbox" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "29" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-09-10T16:47:44.5430399Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.212.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.212.0")]
stellawisps.fxbox / Code/ParticleValues.cs
Game library
using System;
using Sandbox;
using System.Linq;

namespace fxbox;
public class FXParticleFloat
{
    [Property] public bool UseParameter { get; set; } = false;
    
    [Property, ShowIf(nameof(UseParameter), false)]
    public ParticleFloat Value { get; set; }
    
    [Property, ShowIf(nameof(UseParameter), true)]
    [Description("Select a parameter from the system")]
    public string ParameterName { get; set; }
    
    [Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]
    [Description("Multiplier applied to the parameter value")]
    public float Multiplier { get; set; } = 1.0f;
    
    public float GetValue(FXBoxNativeParticleSystem systemComponent = null)
    {
	    if (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)
	    {
		    return systemComponent.GetFloatParameter(ParameterName) * Multiplier;
	    }
	    
	    var result = Value.Evaluate(Random.Shared.Float(), 3f);
	  
	    return result;
    }
    
    public ParticleFloat ToParticleFloat(FXBoxNativeParticleSystem systemComponent = null)
    {
        if (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)
        {
            // Get the instance-specific value (override or default)
            float value = systemComponent.GetFloatParameter(ParameterName) * Multiplier;
            
            return new ParticleFloat()
            {
                Type = ParticleFloat.ValueType.Constant,
                ConstantValue = value,
                Evaluation = ParticleFloat.EvaluationType.Seed
            };
        }
        
        return Value;
    }
    
    // ==================== OPERATORS ====================
    
    
    public static FXParticleFloat operator *(float a, FXParticleFloat b)
    {
        return b * a; // Commutative
    }
    
    // Division operators
    public static FXParticleFloat operator /(FXParticleFloat a, float b)
{
    if (a == null)
    {
        Log.Warning("Division: a is null");
        return null;
    }
    
    if (b == 0 || MathF.Abs(b) < 0.0001f)
    {
        Log.Warning($"Division by zero or very small number ({b}) in FXParticleFloat");
        return a;
    }
    
  
    var result = a * (1.0f / b);
   
    return result;
}

public static FXParticleFloat operator *(FXParticleFloat a, float b)
{
    if (a == null)
    {
        Log.Warning("Multiplication: a is null");
        return null;
    }
    
    var result = new FXParticleFloat();
    
    if (a.UseParameter)
    {
        result.UseParameter = true;
        result.ParameterName = a.ParameterName;
        result.Multiplier = a.Multiplier * b;
        result.Value = new ParticleFloat()
        {
            Type = ParticleFloat.ValueType.Constant,
            ConstantValue = 0f,
            Evaluation = ParticleFloat.EvaluationType.Seed
        };
    }
    else
    {
        result.UseParameter = false;
        result.Value = ScaleParticleFloat(a.Value, b);
    }
    
    return result;
}

private static ParticleFloat ScaleParticleFloat(ParticleFloat pf, float scale)
{
    
    var result = new ParticleFloat();
    result.Type = pf.Type;
    result.Evaluation = pf.Evaluation;
    
    // Copy Constants FIRST, before setting individual values
    // (or don't copy it at all since we're setting the values manually)
    // result.Constants = pf.Constants;
    
    switch (pf.Type)
    {
        case ParticleFloat.ValueType.Constant:
            result.ConstantValue = pf.ConstantValue * scale;
            break;
            
        case ParticleFloat.ValueType.Range:
            result.ConstantA = pf.ConstantA * scale;
            result.ConstantB = pf.ConstantB * scale;
            break;
            
        case ParticleFloat.ValueType.Curve:
            result.CurveA = ScaleCurve(pf.CurveA, scale);
            result.CurveB = ScaleCurve(pf.CurveB, scale);
            break;
            
        case ParticleFloat.ValueType.CurveRange:
            result.CurveRange = ScaleCurveRange(pf.CurveRange, scale);
            break;
    }
    
    // DON'T copy Constants here - it overwrites our values!
    // result.Constants = pf.Constants;
    
    return result;
}

private static ParticleFloat OffsetParticleFloat(ParticleFloat pf, float offset)
{
    var result = new ParticleFloat();
    result.Type = pf.Type;
    result.Evaluation = pf.Evaluation;
    
    // DON'T copy Constants - it will overwrite our values
    // result.Constants = pf.Constants;
    
    switch (pf.Type)
    {
        case ParticleFloat.ValueType.Constant:
            result.ConstantValue = pf.ConstantValue + offset;
            break;
            
        case ParticleFloat.ValueType.Range:
            result.ConstantA = pf.ConstantA + offset;
            result.ConstantB = pf.ConstantB + offset;
            break;
            
        case ParticleFloat.ValueType.Curve:
            result.CurveA = OffsetCurve(pf.CurveA, offset);
            result.CurveB = OffsetCurve(pf.CurveB, offset);
            break;
            
        case ParticleFloat.ValueType.CurveRange:
            result.CurveRange = OffsetCurveRange(pf.CurveRange, offset);
            break;
    }
    
    // DON'T copy Constants here!
    // result.Constants = pf.Constants;
    
    return result;
}
    
    private static Curve ScaleCurve(Curve curve, float scale)
    {
        var newFrames = curve.Frames.Select(frame => 
            new Curve.Frame(frame.Time, frame.Value * scale)).ToArray();
        
        return new Curve(newFrames);
    }
    
    private static Curve OffsetCurve(Curve curve, float offset)
    {
        var newFrames = curve.Frames.Select(frame => 
            new Curve.Frame(frame.Time, frame.Value + offset)).ToArray();
        
        return new Curve(newFrames);
    }
    
    private static CurveRange ScaleCurveRange(CurveRange range, float scale)
    {
        return new CurveRange
        (
           ScaleCurve(range.A, scale),
            ScaleCurve(range.B, scale)
        );
    }
    
    private static CurveRange OffsetCurveRange(CurveRange range, float offset)
    {
        return new CurveRange
        (
           OffsetCurve(range.A, offset),
            OffsetCurve(range.B, offset)
        );
    }
    
    // ==================== IMPLICIT CONVERSIONS ====================
    
    public static implicit operator FXParticleFloat(float v)
    {
        var fxParticle = new FXParticleFloat();
        fxParticle.Value = new ParticleFloat()
        {
            Type = ParticleFloat.ValueType.Constant,
            ConstantValue = v,
            Evaluation = ParticleFloat.EvaluationType.Seed
        };
        return fxParticle;
    }

    // ==================== CONSTRUCTORS ====================
    
    public FXParticleFloat()
    {
        var particleFloat = new ParticleFloat();
        particleFloat.Type = ParticleFloat.ValueType.Constant;
        particleFloat.ConstantValue = 0.0f;
        particleFloat.Evaluation = ParticleFloat.EvaluationType.Seed;
        particleFloat.CurveA = new Curve();
        particleFloat.CurveB = new Curve();
        particleFloat.Constants = new Vector4();
        this.Value = particleFloat;
    }

    public FXParticleFloat(float a, float b)
    {
        var particleFloat = new ParticleFloat();
        particleFloat.Type = ParticleFloat.ValueType.Range;
        particleFloat.ConstantA = a;
        particleFloat.ConstantB = b;
        particleFloat.Evaluation = ParticleFloat.EvaluationType.Seed;
        particleFloat.CurveA = new Curve();
        particleFloat.CurveB = new Curve();
        particleFloat.Constants = new Vector4();
        this.Value = particleFloat;
    }
}

public class FXParticleVector
{
	[Property] public bool UseParameter { get; set; } = false;
    
	[Property, ShowIf(nameof(UseParameter), false)]
	public ParticleVector3 Value { get; set; } = Vector3.Zero;
    
	[Property, ShowIf(nameof(UseParameter), true)]
	[Description("Select a vector parameter from the system")]
	public string ParameterName { get; set; }
    
	[Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]
	[Description("Multiplier applied to the parameter value")]
	public float Multiplier { get; set; } = 1.0f;
    
	public Vector3 GetValue(Particle particle,FXBoxNativeParticleSystem systemComponent = null)
	{
		if (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)
		{
			return systemComponent.GetVectorParameter(ParameterName) * Multiplier;
		}

		if ( particle == null )
		{
			return Value.Evaluate( Time.Delta, 0, 0, 0 );
		}
		return Value.Evaluate( Time.Delta,particle.Rand(1),particle.Rand(2),particle.Rand(3) );
	}
    
	public static implicit operator FXParticleVector(Vector3 v)
	{
		return new FXParticleVector { Value = v };
	}

	public FXParticleVector()
	{
		Value = Vector3.Zero;
	}

	public FXParticleVector(Vector3 value)
	{
		Value = value;
	}
    
	public FXParticleVector(float x, float y, float z)
	{
		Value = new Vector3(x, y, z);
	}
}

public class FXParticleColor
{
	[Property] public bool UseParameter { get; set; } = false;
    
	[Property, ShowIf(nameof(UseParameter), false)]
	public ParticleGradient Value { get; set; } = Color.White;
    
	[Property, ShowIf(nameof(UseParameter), true)]
	[Description("Select a color parameter from the system")]
	public string ParameterName { get; set; }
    
	[Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]
	[Description("Multiplier applied to the parameter value (affects RGB)")]
	public float Multiplier { get; set; } = 1.0f;
    
	public ParticleGradient GetValue(FXBoxNativeParticleSystem systemComponent = null)
	{
		if (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)
		{
			var color = systemComponent.GetColorParameter(ParameterName);
            
			// Apply multiplier to RGB components
			if (Multiplier != 1.0f)
			{
				return color;
			}
            
			return color;
		}
        
		return Value;
	}
    
	public static implicit operator FXParticleColor(Color c)
	{
		return new FXParticleColor { Value = c };
	}

	public FXParticleColor()
	{
		Value = Color.White;
	}

	public FXParticleColor(Color value)
	{
		Value = value;
	}
    
	public FXParticleColor(float r, float g, float b, float a = 1.0f)
	{
		Value = new Color(r, g, b, a);
	}
}
stellawisps.fxbox / Editor/FXParticlePropertyControlWidget.cs
Editor library
using Editor;
using Sandbox;
using System.Collections.Generic;
using System.Linq;
using fxbox;
using fxbox.Graph;

namespace Editor;

/// <summary>
/// Custom control for parameter name selection with dropdown
/// </summary>
public class ParameterNameControlWidget : ControlWidget
{
    private SerializedProperty _property;
    private ComboBox _dropdown;
    private List<FloatParameter> _availableParameters;

    public ParameterNameControlWidget(SerializedProperty property) : base(property)
    {
        _property = property;
        
        SetSizeMode(SizeMode.Ignore, SizeMode.Default);

        Layout = Layout.Row();
        Layout.Spacing = 3;

        // Get available parameters from the currently editing resource
        _availableParameters = FXBoxEditor.CurrentEditingResource?.FloatParameters ?? new List<FloatParameter>();

        _dropdown = new ComboBox(this);
        _dropdown.MinimumWidth = 150;
        
        PopulateDropdown();
        
        // Set current value
        var currentValue = property.GetValue<string>();
        if (!string.IsNullOrEmpty(currentValue))
        {
            SelectParameter(currentValue);
        }

        _dropdown.ItemChanged += OnValueChanged;
        //_dropdown.On += OnValueChanged;

        Layout.Add(_dropdown, 1);
    }

    private void PopulateDropdown()
    {
        _dropdown.Clear();
        
        if (_availableParameters == null || _availableParameters.Count == 0)
        {
            _dropdown.AddItem("(No Parameters Available)");
            _dropdown.Enabled = false;
            return;
        }

        _dropdown.AddItem("(None)");
        
        foreach (var param in _availableParameters.OrderBy(p => p.Name))
        {
            _dropdown.AddItem($"{param.Name} (default: {param.DefaultValue})");
        }
        
        _dropdown.Enabled = true;
    }

    private void SelectParameter(string parameterName)
    {
        if (string.IsNullOrEmpty(parameterName))
        {
            _dropdown.CurrentIndex = 0;
            return;
        }

        // Find the index by matching parameter name
        var orderedParams = _availableParameters.OrderBy(p => p.Name).ToList();
        for (int i = 0; i < orderedParams.Count; i++)
        {
            if (orderedParams[i].Name == parameterName)
            {
                _dropdown.CurrentIndex = i + 1; // +1 because of "(None)" at index 0
                return;
            }
        }
    }

    private new void OnValueChanged()
    {
        var selectedIndex = _dropdown.CurrentIndex;
        
        if (selectedIndex <= 0)
        {
            // "(None)" selected
            _property.SetValue("");
            return;
        }
        
        // Get the parameter name from the selected item
        var orderedParams = _availableParameters.OrderBy(p => p.Name).ToList();
        if (selectedIndex - 1 < orderedParams.Count)
        {
            var selectedParam = orderedParams[selectedIndex - 1];
            _property.SetValue(selectedParam.Name);
        }
    }

    protected override void OnPaint()
    {
        // No custom painting
    }
}

public class VectorParameterNameControlWidget : ControlWidget
{
    private SerializedProperty _property;
    private ComboBox _dropdown;
    private List<VectorParameter> _availableParameters;

    public VectorParameterNameControlWidget(SerializedProperty property) : base(property)
    {
        _property = property;
        
        SetSizeMode(SizeMode.Ignore, SizeMode.Default);

        Layout = Layout.Row();
        Layout.Spacing = 3;

        _availableParameters = FXBoxEditor.CurrentEditingResource?.VectorParameters ?? new List<VectorParameter>();

        _dropdown = new ComboBox(this);
        _dropdown.MinimumWidth = 150;
        
        PopulateDropdown();
        
        var currentValue = property.GetValue<string>();
        if (!string.IsNullOrEmpty(currentValue))
        {
            SelectParameter(currentValue);
        }

        _dropdown.ItemChanged += OnValueChanged;

        Layout.Add(_dropdown, 1);
    }

    private void PopulateDropdown()
    {
        _dropdown.Clear();
        
        if (_availableParameters == null || _availableParameters.Count == 0)
        {
            _dropdown.AddItem("(No Vector Parameters Available)");
            _dropdown.Enabled = false;
            return;
        }

        _dropdown.AddItem("(None)");
        
        foreach (var param in _availableParameters.OrderBy(p => p.Name))
        {
            _dropdown.AddItem($"{param.Name} (default: {param.DefaultValue})");
        }
        
        _dropdown.Enabled = true;
    }

    private void SelectParameter(string parameterName)
    {
        if (string.IsNullOrEmpty(parameterName))
        {
            _dropdown.CurrentIndex = 0;
            return;
        }

        var orderedParams = _availableParameters.OrderBy(p => p.Name).ToList();
        for (int i = 0; i < orderedParams.Count; i++)
        {
            if (orderedParams[i].Name == parameterName)
            {
                _dropdown.CurrentIndex = i + 1;
                return;
            }
        }
    }

    private new void OnValueChanged()
    {
        var selectedIndex = _dropdown.CurrentIndex;
        
        if (selectedIndex <= 0)
        {
            _property.SetValue("");
            return;
        }
        
        var orderedParams = _availableParameters.OrderBy(p => p.Name).ToList();
        if (selectedIndex - 1 < orderedParams.Count)
        {
            var selectedParam = orderedParams[selectedIndex - 1];
            _property.SetValue(selectedParam.Name);
        }
    }

    protected override void OnPaint()
    {
    }
}

public class ColorParameterNameControlWidget : ControlWidget
{
    private SerializedProperty _property;
    private ComboBox _dropdown;
    private List<ColorParameter> _availableParameters;

    public ColorParameterNameControlWidget(SerializedProperty property) : base(property)
    {
        _property = property;
        
        SetSizeMode(SizeMode.Ignore, SizeMode.Default);

        Layout = Layout.Row();
        Layout.Spacing = 3;

        _availableParameters = FXBoxEditor.CurrentEditingResource?.ColorParameters ?? new List<ColorParameter>();

        _dropdown = new ComboBox(this);
        _dropdown.MinimumWidth = 150;
        
        PopulateDropdown();
        
        var currentValue = property.GetValue<string>();
        if (!string.IsNullOrEmpty(currentValue))
        {
            SelectParameter(currentValue);
        }

        _dropdown.ItemChanged += OnValueChanged;

        Layout.Add(_dropdown, 1);
    }

    private void PopulateDropdown()
    {
        _dropdown.Clear();
        
        if (_availableParameters == null || _availableParameters.Count == 0)
        {
            _dropdown.AddItem("(No Color Parameters Available)");
            _dropdown.Enabled = false;
            return;
        }

        _dropdown.AddItem("(None)");
        
        foreach (var param in _availableParameters.OrderBy(p => p.Name))
        {
            _dropdown.AddItem($"{param.Name}");
        }
        
        _dropdown.Enabled = true;
    }

    private void SelectParameter(string parameterName)
    {
        if (string.IsNullOrEmpty(parameterName))
        {
            _dropdown.CurrentIndex = 0;
            return;
        }

        var orderedParams = _availableParameters.OrderBy(p => p.Name).ToList();
        for (int i = 0; i < orderedParams.Count; i++)
        {
            if (orderedParams[i].Name == parameterName)
            {
                _dropdown.CurrentIndex = i + 1;
                return;
            }
        }
    }

    private new void OnValueChanged()
    {
        var selectedIndex = _dropdown.CurrentIndex;
        
        if (selectedIndex <= 0)
        {
            _property.SetValue("");
            return;
        }
        
        var orderedParams = _availableParameters.OrderBy(p => p.Name).ToList();
        if (selectedIndex - 1 < orderedParams.Count)
        {
            var selectedParam = orderedParams[selectedIndex - 1];
            _property.SetValue(selectedParam.Name);
        }
    }

    protected override void OnPaint()
    {
    }
}
Debug: View Raw JSON Response
{
    "TotalCount": 17,
    "Files": [
        {
            "Ident": "stellawisps.fxbox",
            "Path": "Code/ParticleModules.cs",
            "FileName": "ParticleModules.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 379493,
            "Code": "using System;\r\nusing Sandbox;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Text.Json;\r\nusing System.Text.Json.Serialization;\r\n\r\nnamespace fxbox;\r\n\r\n/// <summary>\r\n/// Stage when a particle module executes\r\n/// </summary>\r\npublic enum ModuleStage\r\n{\r\n    Spawn,      // Controls when/how particles spawn\r\n    Initialize, // Runs once when particle is created\r\n    Update,     // Runs every frame for each particle\r\n    Render      // Controls how particles are rendered\r\n}\r\n\r\n/// <summary>\r\n/// Context passed to particle modules during execution\r\n/// </summary>\r\npublic class ParticleExecutionContext\r\n{\r\n\tpublic Particle Particle;\r\n\tpublic ParticleEffect Effect;\r\n\tpublic Sandbox.ParticleEmitter Emitter;\r\n\tpublic FXBoxNativeParticleSystem SystemComponent; // Changed from Resource to SystemComponent\r\n}\r\n\r\n// ==================== SPAWN MODULES ====================\r\n\r\n/// <summary>\r\n/// Controls spawn rate over time\r\n/// </summary>\r\n[Title(\"Spawn Rate\"), Category(\"Spawn\"), Icon(\"speed\")]\r\npublic partial class SpawnRateModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Spawn;\r\n    \r\n    [Property, Range(0.1f, 1000f)] \r\n    public FXParticleFloat SpawnRate { get; set; } = 10.0f;\r\n    \r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n\t    var rate = SpawnRate;\r\n        context.Emitter.Rate = rate.GetValue( context.SystemComponent );\r\n    }\r\n    \r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    context.Emitter.Rate = SpawnRate.GetValue( context.SystemComponent );\r\n    }\r\n}\r\n/// <summary>\r\n/// Sets initial particle stretch\r\n/// </summary>\r\n[Title(\"Particle Stretch\"), Category(\"Initialize\"), Icon(\"photo_size_select_small\")]\r\npublic partial class ParticleStretchModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n\t\r\n    \r\n\t[Property, Range(0.1f, 100f)]\r\n\tpublic FXParticleFloat Size { get; set; } = 1.0f;\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tcontext.Effect.ApplyShape = true;\r\n\t\tcontext.Effect.Stretch = Size.ToParticleFloat( context.SystemComponent );\r\n\t}\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n       \r\n\t}\r\n}\r\n/// <summary>\r\n/// Controls spawn rate per unit\r\n/// </summary>\r\n[Title(\"Spawn Rate Over Distance\"), Category(\"Spawn\"), Icon(\"speed\")]\r\npublic partial class SpawnRateOverDistanceModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Spawn;\r\n    \r\n\t[Property, Range(0.1f, 1000f)] \r\n\tpublic FXParticleFloat SpawnRate { get; set; } = 10.0f;\r\n    \r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n\t\tcontext.Emitter.RateOverDistance = SpawnRate.GetValue( context.SystemComponent );\r\n\t}\r\n    \r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tcontext.Emitter.RateOverDistance = SpawnRate.GetValue( context.SystemComponent );\r\n\t}\r\n}\r\n/// <summary>\r\n/// Spawns particles in a burst\r\n/// </summary>\r\n[Title(\"Spawn Burst\"), Category(\"Spawn\"), Icon(\"auto_awesome\")]\r\npublic partial class SpawnBurstModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Spawn;\r\n    \r\n    [Property, Range(1, 1000)]\r\n    public int ParticleCount { get; set; } = 50;\r\n    \r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    context.Emitter.Burst = ParticleCount;\r\n    }\r\n\r\n    // Burst is a one-shot \"spawn this many now\" value, not a continuous per-tick one like\r\n    // SpawnRateModule's Rate - Execute() runs every single frame (see\r\n    // FXBoxParticleController.OnUpdate), so reassigning Burst here too kept re-arming/\r\n    // re-firing it every tick instead of once, spawning far more than ParticleCount actually\r\n    // configured. Same \"Initialize-only, empty Execute\" shape ParticleStretchModule already\r\n    // uses above for its own one-shot value.\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n    }\r\n}\r\n\r\n// ==================== INITIALIZE MODULES ====================\r\n\r\n/// <summary>\r\n/// Sets initial position based on shape\r\n/// </summary>\r\n[Title(\"Initialize Position\"), Category(\"Initialize\"), Icon(\"place\")]\r\npublic partial class InitializePositionModule : ParticleModule, IParticleComponentCreator\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Initialize;\r\n\r\n    public override void Execute( ParticleExecutionContext context )\r\n    {\r\n\t    \r\n    }\r\n\r\n    public enum SpawnShape { Point, Sphere, Box, Cone, Circle, Line }\r\n    \r\n    [Property]\r\n    public FXCopyFlags CopyFlags { get; set; } = FXCopyFlags.Rotation | FXCopyFlags.Scale;\r\n    \r\n    [Property]\r\n    public SpawnShape Shape { get; set; } = SpawnShape.Sphere;\r\n    \r\n    [Property, Range(0f, 1000f), ShowIf(nameof(ShowRadius), true)]\r\n    public float Radius { get; set; } = 50.0f;\r\n    \r\n    [Property, ShowIf(nameof(ShowBoxSize), true)]\r\n    public Vector3 BoxSize { get; set; } = new Vector3(100, 100, 100);\r\n    \r\n    [Property, Range(0f, 180f), ShowIf(nameof(ShowConeAngle), true)]\r\n    public float ConeAngle { get; set; } = 45.0f;\r\n    \r\n    [Property]\r\n    public bool EmitFromShell { get; set; } = false;\r\n    \r\n    [Property, ShowIf(nameof(ShowLine), true)]\r\n    public Vector3 LineStart { get; set; } = Vector3.Zero;\r\n    \r\n    [Property, ShowIf(nameof(ShowLine), true)]\r\n    public Vector3 LineEnd { get; set; } = Vector3.Up * 100;\r\n\r\n    [Hide] public bool ShowRadius => Shape == SpawnShape.Sphere || Shape == SpawnShape.Circle || Shape == SpawnShape.Cone;\r\n    [Hide] public bool ShowBoxSize => Shape == SpawnShape.Box;\r\n    [Hide] public bool ShowConeAngle => Shape == SpawnShape.Cone;\r\n    [Hide] public bool ShowLine => Shape == SpawnShape.Line;\r\n    \r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    if ( context.Particle != null )\r\n\t    {\r\n\t\t    \r\n\t\t    var pos = Shape switch\r\n\t\t    {\r\n\t\t\t    SpawnShape.Point => Vector3.Zero,\r\n\t\t\t    SpawnShape.Sphere => GetSpherePosition(),\r\n\t\t\t    SpawnShape.Box => GetBoxPosition(),\r\n\t\t\t    SpawnShape.Cone => GetConePosition(),\r\n\t\t\t    SpawnShape.Circle => GetCirclePosition(),\r\n\t\t\t    SpawnShape.Line => GetLinePosition(),\r\n\t\t\t    _ => Vector3.Zero\r\n\t\t    };\r\n\r\n\t\t    if ( CopyFlags.HasFlag( FXCopyFlags.Scale ) )\r\n\t\t    {\r\n\t\t\t    pos = pos * context.Emitter.WorldScale;\r\n\t\t    }\r\n\r\n\t\t    if ( CopyFlags.HasFlag( FXCopyFlags.Rotation ) )\r\n\t\t    {\r\n\t\t\t    pos = pos.RotateAround( 0, context.SystemComponent.WorldRotation );\r\n\t\t    }\r\n\r\n\t\r\n\t\t    context.Particle.Position += pos;\r\n\t    }\r\n    }\r\n    public void CreateComponent(GameObject go)\r\n    {\r\n\t    var pointEmitter = go.AddComponent<ParticleSphereEmitter>();\r\n\t    pointEmitter.Radius = 0;\r\n\t    pointEmitter.Velocity = 0;\r\n\t    pointEmitter.Burst = 0;\r\n\t    pointEmitter.Rate = 0;\r\n    }\r\n\r\n    private Vector3 GetSpherePosition()\r\n    {\r\n        var direction = Random.Shared.VectorInSphere().Normal;\r\n        var radius = EmitFromShell ? Radius : Random.Shared.Float(0, Radius);\r\n        \r\n        return direction * radius;\r\n    }\r\n\r\n    private Vector3 GetBoxPosition()\r\n    {\r\n        if (EmitFromShell)\r\n        {\r\n            var face = Random.Shared.Int(0, 5);\r\n            var u = Random.Shared.Float(0, 1);\r\n            var v = Random.Shared.Float(0, 1);\r\n            var halfSize = BoxSize / 2f;\r\n            \r\n            return face switch\r\n            {\r\n                0 => new Vector3(-halfSize.x, MathX.Lerp(-halfSize.y, halfSize.y, u), MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                1 => new Vector3(halfSize.x, MathX.Lerp(-halfSize.y, halfSize.y, u), MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                2 => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), -halfSize.y, MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                3 => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), halfSize.y, MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                4 => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), MathX.Lerp(-halfSize.y, halfSize.y, v), -halfSize.z),\r\n                _ => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), MathX.Lerp(-halfSize.y, halfSize.y, v), halfSize.z)\r\n            };\r\n        }\r\n        \r\n        return new Vector3(\r\n            Random.Shared.Float(-BoxSize.x / 2, BoxSize.x / 2),\r\n            Random.Shared.Float(-BoxSize.y / 2, BoxSize.y / 2),\r\n            Random.Shared.Float(-BoxSize.z / 2, BoxSize.z / 2)\r\n        );\r\n    }\r\n\r\n    private Vector3 GetConePosition()\r\n    {\r\n        var angle = Random.Shared.Float(0, 360);\r\n        var distance = Random.Shared.Float(0, Radius);\r\n        var coneRadius = MathF.Tan(ConeAngle.DegreeToRadian()) * distance;\r\n        var radius = EmitFromShell ? coneRadius : Random.Shared.Float(0, coneRadius);\r\n        \r\n        return new Vector3(\r\n            MathF.Cos(angle.DegreeToRadian()) * radius,\r\n            MathF.Sin(angle.DegreeToRadian()) * radius,\r\n            distance\r\n        );\r\n    }\r\n\r\n    private Vector3 GetCirclePosition()\r\n    {\r\n        var angle = Random.Shared.Float(0, 360);\r\n        var radius = EmitFromShell ? Radius : Random.Shared.Float(0, Radius);\r\n        return new Vector3(\r\n            MathF.Cos(angle.DegreeToRadian()) * radius,\r\n            MathF.Sin(angle.DegreeToRadian()) * radius,\r\n            0\r\n        );\r\n    }\r\n\r\n    private Vector3 GetLinePosition()\r\n    {\r\n        return Vector3.Lerp(LineStart, LineEnd, Random.Shared.Float(0, 1));\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Controls how strongly particles follow the emitter in local space\r\n/// </summary>\r\n[Title(\"Initialize Local Space\"), Category(\"Initialize\"), Icon(\"transform\")]\r\npublic partial class InitializeLocalSpaceModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n\r\n\t[Property, Range(0f, 1f)]\r\n\tpublic FXParticleFloat LocalSpace { get; set; } = 0f;\r\n\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tcontext.Effect.LocalSpace = LocalSpace.ToParticleFloat( context.SystemComponent );\r\n\t}\r\n\r\n\tpublic override void Execute( ParticleExecutionContext context )\r\n\t{\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Sets initial velocity\r\n/// </summary>\r\n[Title(\"Initialize Velocity\"), Category(\"Initialize\"), Icon(\"air\")]\r\npublic partial class InitializeVelocityModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Initialize;\r\n    \r\n    [Property]\r\n    public FXParticleVector Velocity { get; set; } = Vector3.Up * 100;\r\n\r\n    [Property] public FXParticleFloat RandomVelocity { get; set; } = 0;\r\n\r\n    [Property] public bool LocalSpace { get; set; } = false;\r\n    \r\n    [Property]\r\n    public bool InheritEmitterVelocity { get; set; } = false;\r\n    \r\n    \r\n    [Property,ShowIf(\"InheritEmitterVelocity\",true)] public float EmitterVelocityScale { get; set; } = 1.0f;\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    var startVelocity = Velocity;\r\n\t    if ( LocalSpace )\r\n\t    {\r\n\t\t    startVelocity = startVelocity.GetValue( context.Particle,context.SystemComponent ).RotateAround( 0,context.Emitter.WorldRotation );\r\n\t    }\r\n\r\n\t    context.Effect.StartVelocity = RandomVelocity.ToParticleFloat();\r\n\t    context.Effect.InitialVelocity = startVelocity.GetValue( context.Particle,context.SystemComponent );\r\n\t    if ( InheritEmitterVelocity )\r\n\t    {\r\n\t\t    context.Effect.InitialVelocity = (context.SystemComponent.Velocity*EmitterVelocityScale) + startVelocity.GetValue( context.Particle,context.SystemComponent );\r\n\t    }\r\n\t    \r\n    }\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n\r\n    }\r\n}\r\n/// <summary>\r\n/// Sets initial color\r\n/// </summary>\r\n[Title(\"Collision\"), Category(\"Initialize\"), Icon(\"palette\")]\r\npublic partial class ParticleCollisionModule : ParticleModule\r\n{\r\n\r\n\t[Property] public TagSet CollisionIgnore { get; set; } = new TagSet();\r\n\t[Property] public List<GameObject> CollisionPrefabs { get; set; } = new List<GameObject>();\r\n\t[Property] public FXParticleFloat CollisionRadius { get; set; } = 5;\r\n\t[Property] public FXParticleFloat CollisionPrefabChance { get; set; } = 1;\r\n\t[Property] public FXParticleFloat CollisionPrefabRotation { get; set; } = 0;\r\n\t[Property] public FXParticleFloat DieOnCollisionChance { get; set; } = 0;\r\n\t[Property] public bool CollisionPrefabAlign { get; set; } = false;\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n        \r\n\t}\r\n    \r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tcontext.Effect.Collision = true;\r\n\t\tcontext.Effect.CollisionIgnore = CollisionIgnore;\r\n\t\tcontext.Effect.CollisionRadius = CollisionRadius.GetValue( context.SystemComponent );\r\n\t\tcontext.Effect.CollisionPrefabChance = CollisionPrefabChance.GetValue( context.SystemComponent );\r\n\t\tcontext.Effect.CollisionPrefabRotation = CollisionPrefabRotation.ToParticleFloat( context.SystemComponent );\r\n\t\tif ( CollisionPrefabs.Any() )\r\n\t\t{\r\n\t\t\tcontext.Effect.UsePrefabFeature = true;\r\n\t\t}\r\n\t\tcontext.Effect.CollisionPrefab = CollisionPrefabs;\r\n\t\tcontext.Effect.DieOnCollisionChance = DieOnCollisionChance.GetValue( context.SystemComponent );\r\n\t\tcontext.Effect.CollisionPrefabAlign = CollisionPrefabAlign;\r\n\t}\r\n}\r\n/// <summary>\r\n/// Sets initial velocity\r\n/// </summary>\r\n[Title(\"Initialize Rotation\"), Category(\"Initialize\"), Icon(\"air\")]\r\npublic partial class InitializeRotationModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n    \r\n\t[Property]\r\n\tpublic ParticleVector3 InitialRotation { get; set; } = Vector3.Up;\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tif ( context.Particle != null )\r\n\t\t{\r\n\t\t\tcontext.Particle.Angles = new Angles( InitialRotation.Evaluate( Time.Delta,context.Particle.Rand(  ),context.Particle.Rand(  ),context.Particle.Rand(  ) ) );\r\n\t\t}\r\n\t    \r\n\t}\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Sets initial lifetime\r\n/// </summary>\r\n[Title(\"Initialize Lifetime\"), Category(\"Initialize\"), Icon(\"schedule\")]\r\npublic partial class InitializeLifetimeModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n    \r\n\t[Property, Range(0.1f, 100f)]\r\n\tpublic FXParticleFloat Lifetime { get; set; } = 2.0f;\r\n    \r\n\tpublic override void Initialize(ParticleExecutionContext context)\r\n\t{\r\n\t\tcontext.Effect.Lifetime = Lifetime.ToParticleFloat( context.SystemComponent );\r\n\t}\r\n    \r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n\t\t// Not used\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Sets initial size\r\n/// </summary>\r\n[Title(\"Initialize Size\"), Category(\"Initialize\"), Icon(\"photo_size_select_small\")]\r\npublic partial class InitializeSizeModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Initialize;\r\n\r\n    [Property] public bool InheritEmitterScale { get; set; } = true;\r\n    \r\n    [Property, Range(0.1f, 100f)]\r\n    public FXParticleFloat Size { get; set; } = 10.0f;\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    context.Effect.ApplyShape = true;\r\n\t    if ( InheritEmitterScale )\r\n\t    {\r\n\t\t    context.Effect.Scale = Size.ToParticleFloat( context.SystemComponent );\r\n\t    }\r\n\t    else\r\n\t    {\r\n\t\t     context.Effect.Scale = (Size / context.SystemComponent.WorldScale.x).ToParticleFloat( context.SystemComponent );\r\n\t    }\r\n\t    \r\n\t    \r\n\t  \r\n    }\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n       \r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Sets initial color\r\n/// </summary>\r\n[Title(\"Initialize Color\"), Category(\"Initialize\"), Icon(\"palette\")]\r\npublic partial class InitializeColorModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Initialize;\r\n\r\n    [Property] public FXParticleColor Color { get; set; } = global::Color.Red;\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n        \r\n    }\r\n    \r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    context.Effect.ApplyColor = true;\r\n\t    context.Effect.ApplyAlpha = true;\r\n\t    if ( Color != null )\r\n\t    {\r\n\t\t    context.Effect.Gradient = Color.GetValue( context.SystemComponent );\r\n\t    }\r\n\t    else\r\n\t    {\r\n\t\t    Color = new FXParticleColor( global::Color.Red );\r\n\t    }\r\n\t    \r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Sets initial color\r\n/// </summary>\r\n[Title(\"Sprite Flipbook\"), Category(\"Initialize\"), Icon(\"palette\")]\r\npublic partial class SpriteFlipbookModule : ParticleModule\r\n{\r\n\t[Property] public FXParticleFloat SequenceTime { get; set; } = 0;\r\n\t[Property] public FXParticleFloat SequenceSpeed { get; set; } = 1;\r\n\t[Property] public int SequenceId { get; set; } = 0;\r\n\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n        \r\n\t}\r\n    \r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tcontext.Effect.SheetSequence = true;\r\n\t\tcontext.Effect.SequenceId = SequenceId;\r\n\t\tcontext.Effect.SequenceSpeed = SequenceSpeed.ToParticleFloat( context.SystemComponent );\r\n\t\tcontext.Effect.SequenceTime = SequenceTime.ToParticleFloat( context.SystemComponent );\r\n\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Randomly Kill a particle to spawn less\r\n/// </summary>\r\n[Title(\"RandomKill\"), Category(\"Initialize\"), Icon(\"arrow_downward\")]\r\npublic partial class RandomKill : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n\t\r\n\t[Property]\r\n\tpublic float Chance { get; set; } = 0.5f;\r\n\t\r\n\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n        \r\n\t}\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tif ( context.Particle == null ) return;\r\n\t\tif ( Random.Shared.Float( 0, 1 ) < Chance )\r\n\t\t{\r\n\t\t\tcontext.Particle.Age = 100000;\r\n\t\t}\r\n\t}\r\n\t\r\n}\r\n\r\n\r\n// ==================== UPDATE MODULES ====================\r\n\r\n/// <summary>\r\n/// Applies gravity force\r\n/// </summary>\r\n[Title(\"Gravity Force\"), Category(\"Update\"), Icon(\"arrow_downward\")]\r\npublic partial class GravityForceModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Update;\r\n    \r\n    [Property]\r\n    public FXParticleVector Force { get; set; } = new Vector3(0, 0, -980);\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n        \r\n    }\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    \r\n    }\r\n    public void UpdateParticle(ParticleExecutionContext context, float delta)\r\n    {\r\n        context.Particle.Velocity += Force.GetValue(context.Particle, context.SystemComponent ) * Time.Delta;\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Make a mesh follow it's velocity\r\n/// </summary>\r\n[Title(\"Follow Velocity\"), Category(\"Update\"), Icon(\"arrow_downward\")]\r\npublic partial class FollowVelocity : ParticleModule, IParticleUpdater\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Update;\r\n\t\r\n\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n        \r\n\t}\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t    \r\n\t}\r\n\tpublic void UpdateParticle(ParticleExecutionContext context, float delta)\r\n\t{\r\n\t\tcontext.Particle.Angles = Rotation.LookAt( context.Particle.Velocity ).Angles();\r\n\t}\r\n}\r\n/// <summary>\r\n/// Applies drag/air resistance\r\n/// </summary>\r\n[Title(\"Drag Force\"), Category(\"Update\"), Icon(\"air\")]\r\npublic partial class DragForceModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Update;\r\n    \r\n    [Property, Range(0f, 10f)]\r\n    public float Damping { get; set; } = 0.1f;\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n        \r\n    }\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    \r\n    }\r\n    public void UpdateParticle(ParticleExecutionContext context, float delta)\r\n    {\r\n        context.Particle.Velocity *= (1.0f - Damping * Time.Delta);\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Makes particles rotate\r\n/// </summary>\r\n[Title(\"Rotation\"), Category(\"Update\"), Icon(\"rotate_right\")]\r\npublic partial class RotationModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Update;\r\n\r\n    [Property] public FXParticleVector RotationSpeed { get; set; } = Vector3.Zero;\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n        /*context.Particle.Rotation += RotationSpeed * context.DeltaTime;*/\r\n    }\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    \r\n    }\r\n    public void UpdateParticle(ParticleExecutionContext context, float delta)\r\n    {\r\n\t    if ( context.Particle != null )\r\n\t    {\r\n\t\t    context.Particle.Angles += RotationSpeed.GetValue( context.Particle ) * Time.Delta;\r\n\t    }\r\n        \r\n    }\r\n}\r\n\r\npublic enum PositionType\r\n{\r\n\tLocal,\r\n\tWorld\r\n}\r\n\r\n/// <summary>\r\n/// Attracts particles to a point. Full strength inside AttractorSize, falling off beyond it.\r\n/// </summary>\r\n[Title(\"Point Attractor\"), Category(\"Update\"), Icon(\"my_location\")]\r\npublic partial class PointAttractorModule : ParticleModule, IParticleUpdater\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Update;\r\n\t[Property] public PositionType PositionType { get; set; } = PositionType.Local;\r\n\r\n\t[Property]\r\n\tpublic FXParticleVector AttractorPosition { get; set; } = Vector3.Zero;\r\n\r\n\t[Property, Range(0f, 10000f)]\r\n\tpublic FXParticleFloat Strength { get; set; } = 500.0f;\r\n\r\n\t[Property, Range(0.01f, 10000f)]\r\n\tpublic float AttractorSize { get; set; } = 50.0f;\r\n\r\n\t[Property] public bool Invert { get; set; } = false;\r\n\r\n\t/// <summary>\r\n\t/// How quickly strength falls off beyond AttractorSize.\r\n\t/// 1 = linear, 2 = inverse square, higher = sharper falloff.\r\n\t/// </summary>\r\n\t[Property, Range(0.1f, 8f)]\r\n\tpublic float Falloff { get; set; } = 2.0f;\r\n\r\n\tpublic override void Execute(ParticleExecutionContext context) { }\r\n\tpublic override void Initialize(ParticleExecutionContext context) { }\r\n\r\n\tpublic void UpdateParticle(ParticleExecutionContext context, float delta)\r\n\t{\r\n\t\tvar attractorPos = PositionType == PositionType.Local ? AttractorPosition.GetValue( context.Particle, context.SystemComponent ) + context.Emitter.WorldPosition : AttractorPosition.GetValue( context.Particle, context.SystemComponent );\r\n\t\t\r\n\t\t\r\n\t\tvar toAttractor = attractorPos - context.Particle.Position;\r\n\t\tvar distance = toAttractor.Length;\r\n\r\n\t\tif (distance < 0.01f) return;\r\n\r\n\t\t// Inside the attractor: full strength.\r\n\t\t// Outside: strength falls off based on normalised excess distance.\r\n\t\tfloat strengthMultiplier;\r\n\t\tif (distance <= AttractorSize)\r\n\t\t{\r\n\t\t\tstrengthMultiplier = 1f;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t// How many radii past the edge are we? 0 at the surface, grows outward.\r\n\t\t\tvar excess = (distance - AttractorSize) / AttractorSize;\r\n\t\t\tstrengthMultiplier = 1f / MathF.Pow(1f + excess, Falloff);\r\n\t\t}\r\n\t\t\r\n\t\tif ( Invert )\r\n\t\t{\r\n\t\t\tstrengthMultiplier = 1 - strengthMultiplier;\r\n\t\t}\r\n\r\n\t\tcontext.Particle.Velocity += toAttractor.Normal * Strength.GetValue( context.SystemComponent ) * strengthMultiplier * Time.Delta;\r\n\t}\r\n}\r\n/// <summary>\r\n/// Creates orbital motion\r\n/// </summary>\r\n[Title(\"Vortex Force\"), Category(\"Update\"), Icon(\"cyclone\")]\r\npublic partial class VortexForceModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Update;\r\n    \r\n    [Property]\r\n    public Vector3 Center { get; set; } = Vector3.Zero;\r\n    \r\n    [Property]\r\n    public FXParticleVector Axis { get; set; } = Vector3.Up;\r\n    \r\n    [Property, Range(0f, 1000f)]\r\n    public float Strength { get; set; } = 100.0f;\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n        \r\n    }\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    \r\n    }\r\n    public void UpdateParticle(ParticleExecutionContext context, float delta)\r\n    {\r\n        var toCenter = context.Particle.Position - (Center + context.Emitter.WorldPosition);\r\n        var distance = toCenter.Length;\r\n        \r\n        \r\n        if (distance > 0.01f)\r\n        {\r\n            var tangent = Vector3.Cross(Axis.GetValue( context.Particle,context.SystemComponent ).Normal, toCenter.Normal);\r\n            var force = tangent * (Strength / distance);\r\n            context.Particle.Velocity += force * Time.Delta * 10000;\r\n        }\r\n    }\r\n}\r\n\r\n// ==================== RENDER MODULES ====================\r\n\r\n/// <summary>\r\n/// Basic sprite renderer\r\n/// </summary>\r\n[Title(\"Sprite Renderer\"), Category(\"Render\"), Icon(\"image\")]\r\npublic partial class SpriteRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n    [Hide] \r\n    public override ModuleStage Stage => ModuleStage.Render;\r\n\r\n    [Property] \r\n    public Sprite Sprite { get; set; }\r\n\r\n    [Property] public FXParticleFloat SpriteScale { get; set; } = 1f;\r\n\r\n    [Property]\r\n    public ParticleSpriteRenderer.BillboardAlignment Alignment { get; set; } =\r\n\t    ParticleSpriteRenderer.BillboardAlignment.LookAtCamera;\r\n\r\n    [Property] public bool FaceVelocity { get; set; } = false;\r\n\r\n    [Property] public bool Additive { get; set; } = false;\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n    }\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    \r\n    }\r\n    public void CreateComponent(GameObject go)\r\n    {\r\n        var renderer = go.AddComponent<ParticleSpriteRenderer>();\r\n\r\n        renderer.Alignment = Alignment;\r\n        renderer.FaceVelocity = FaceVelocity;\r\n        renderer.Sprite = Sprite;\r\n\t\trenderer.Additive = Additive;\r\n\t\trenderer.Scale = SpriteScale.GetValue();\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Basic light renderer\r\n/// </summary>\r\n[Title(\"Light Renderer\"), Category(\"Render\"), Icon(\"image\")]\r\npublic partial class LightRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n\t[Hide] \r\n\tpublic override ModuleStage Stage => ModuleStage.Render;\r\n\t\r\n\t[Property] public FXParticleColor LightColor { get; set; } = new FXParticleColor( Color.White );\r\n\t[Property] public FXParticleFloat Brightness { get; set; } = 10f;\r\n\t[Property] public FXParticleFloat MaxLights { get; set; } = 10f;\r\n\t[Property] public FXParticleFloat LightSize { get; set; } = 10f;\r\n\t[Property] public FXParticleFloat Attenuation { get; set; } = 1;\r\n\t[Property] public bool CastShadows { get; set; } = false;\r\n\t[Property] public bool UseParticleColor { get; set; } = true;\r\n\t[Property] public FXParticleFloat Ratio { get; set; } = 1;\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n\t\t// Rendering is handled externally, this just stores render properties\r\n\t}\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t    \r\n\t}\r\n\tpublic void CreateComponent(GameObject go)\r\n\t{\r\n\t\tvar renderer = go.AddComponent<ParticleLightRenderer>();\r\n\t\t\r\n\t\tvar fxbox=go.GetComponentInParent<FXBoxNativeParticleSystem>(  );\r\n\t\trenderer.LightColor = LightColor.GetValue( fxbox );\r\n\t\trenderer.Brightness = Brightness.GetValue( fxbox );\r\n\t\t\r\n\t\trenderer.MaximumLights = (int)MaxLights.GetValue( fxbox );\r\n\t\trenderer.Scale = LightSize.GetValue( fxbox );\r\n\t\trenderer.Attenuation = Attenuation.GetValue( fxbox );\r\n\t\trenderer.Ratio = Ratio.GetValue( fxbox );\r\n\t\trenderer.CastShadows = CastShadows;\r\n\t\trenderer.UseParticleColor = UseParticleColor;\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Basic model renderer\r\n/// </summary>\r\n[Title(\"Model Renderer\"), Category(\"Render\"), Icon(\"image\")]\r\npublic partial class ModelRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n\t[Hide] \r\n\tpublic override ModuleStage Stage => ModuleStage.Render;\r\n\r\n\t[Property] \r\n\tpublic List<ParticleModelRenderer.ModelEntry> Models { get; set; }\r\n\r\n\t[Property] \r\n\tpublic bool FaceCamera { get; set; } = true;\r\n\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n\t\t// Rendering is handled externally, this just stores render properties\r\n\t}\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t    \r\n\t}\r\n\tpublic void CreateComponent(GameObject go)\r\n\t{\r\n\t\tvar renderer = go.AddComponent<ParticleModelRenderer>();\r\n\r\n\t\trenderer.Choices = Models;\r\n\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Basic Trail Renderer\r\n/// </summary>\r\n[Title( \"Trail Renderer\" ), Category( \"Render\" ), Icon( \"image\" )]\r\npublic partial class TrailRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n\t[Hide] public override ModuleStage Stage => ModuleStage.Render;\r\n\r\n\t[Property] public bool Game { get; set; } = true;\r\n\t[Property] public bool Overlay { get; set; } = false;\r\n\t[Property] public bool Bloom { get; set; } = false;\r\n\t[Property] public bool AfterUi { get; set; } = false;\r\n\t[Property] public Material Material { get; set; }\r\n\t[Property] public FXParticleFloat UnitsPerTexture { get; set; } = 10f;\r\n\t[Property] public FXParticleFloat Scroll { get; set; } = 0f;\r\n\t[Property] public FXParticleFloat Width { get; set; } = 1f;\r\n\t[Property] public bool Opaque { get; set; } = true;\r\n\t[Property, ShowIf( \"Opaque\", false )] public BlendMode BlendMode { get; set; } = BlendMode.Normal;\r\n\t[Property] public int MaxPoints { get; set; } = 32;\r\n\t[Property] public float PointDistance { get; set; } = 8;\r\n\t[Property] public float LifeTime { get; set; } = 2f;\r\n\t[Property] public FXParticleColor Color { get; set; } = new FXParticleColor(  );\r\n\r\npublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n\t\t// Rendering is handled externally, this just stores render properties\r\n\t}\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t    \r\n\t}\r\n\tpublic void CreateComponent(GameObject go)\r\n\t{\r\n\t\tvar renderer = go.AddComponent<ParticleTrailRenderer>();\r\n\r\n\t\tvar appearance = renderer.Texturing;\r\n\t\tappearance.Material = Material;\r\n\t\tappearance.UnitsPerTexture = UnitsPerTexture.GetValue(  );\r\n\t\tappearance.Scroll = Scroll.GetValue();\r\n\r\n\t\tvar widthCurve = Width.ToParticleFloat();\r\n\r\n\t\tif ( widthCurve.Type == ParticleFloat.ValueType.Curve )\r\n\t\t{\r\n\t\t\trenderer.Width = widthCurve.CurveA;\r\n\t\t} else if ( widthCurve.Type == ParticleFloat.ValueType.Range )\r\n\t\t{\r\n\t\t\tvar point1 = new Curve.Frame( 0, widthCurve.ConstantA );\r\n\t\t\tvar point2 = new Curve.Frame( 1, widthCurve.ConstantB );\r\n\t\t\trenderer.Width = new Curve( point1, point2 );\r\n\t\t} else if ( widthCurve.Type == ParticleFloat.ValueType.Constant )\r\n\t\t{\r\n\t\t\trenderer.Width = widthCurve.ConstantA;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\trenderer.Width = widthCurve.CurveA;\r\n\t\t}\r\n\t\t\r\n\t\trenderer.Opaque = Opaque;\r\n\t\trenderer.BlendMode = BlendMode;\r\n\t\trenderer.MaxPoints = MaxPoints;\r\n\t\trenderer.PointDistance = PointDistance;\r\n\t\trenderer.LifeTime = LifeTime;\r\n\t\tvar colorParam = Color.GetValue();\r\n\t\tif ( colorParam.Type == ParticleGradient.ValueType.Constant )\r\n\t\t{\r\n\t\t\trenderer.Color = colorParam.ConstantA;\r\n\t\t} else if ( colorParam.Type == ParticleGradient.ValueType.Range )\r\n\t\t{\r\n\t\t\tvar point1 = new Gradient.ColorFrame( 0, colorParam.ConstantA );\r\n\t\t\tvar point2 = new Gradient.ColorFrame( 1, colorParam.ConstantB );\r\n\t\t\trenderer.Color = new Gradient( point1, point2 );\r\n\t\t} else if ( colorParam.Type == ParticleGradient.ValueType.Gradient )\r\n\t\t{\r\n\t\t\trenderer.Color = colorParam.GradientA;\r\n\t\t}\r\n\r\n\t\trenderer.RenderOptions.Game = Game;\r\n\t\trenderer.RenderOptions.Overlay = Overlay;\r\n\t\trenderer.RenderOptions.Bloom = Bloom;\r\n\t\trenderer.RenderOptions.AfterUI = AfterUi;\r\n\r\n\t\trenderer.Texturing = appearance;\r\n\r\n\t}\r\n}\r\n/// <summary>\r\n/// Applies curl noise force for organic, swirling motion\r\n/// </summary>\r\n[Title(\"Curl Noise\"), Category(\"Update\"), Icon(\"air\")]\r\npublic partial class CurlNoiseModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Update;\r\n    \r\n    [Property, Range(0f, 1000f)]\r\n    [Description(\"Strength of the curl noise effect\")]\r\n    public FXParticleFloat Strength { get; set; } = 1.0f;\r\n    \r\n    [Property, Range(0.01f, 10f)]\r\n    [Description(\"Scale of the noise pattern - smaller values create tighter curls\")]\r\n    public FXParticleFloat Scale { get; set; } = 1.0f;\r\n    \r\n    [Property, Range(0f, 10f)]\r\n    [Description(\"Speed at which the noise pattern evolves over time\")]\r\n    public FXParticleFloat TimeScale { get; set; } = 1.0f;\r\n    \r\n    [Property]\r\n    [Description(\"Offset in the noise field\")]\r\n    public Vector3 Offset { get; set; } = Vector3.Zero;\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n        // Not used - handled in UpdateParticle\r\n    }\r\n\r\n    public override void Initialize(ParticleExecutionContext context)\r\n    {\r\n        // No initialization needed\r\n    }\r\n\r\n    public void UpdateParticle(ParticleExecutionContext context, float delta)\r\n    {\r\n        var particle = context.Particle;\r\n        \r\n        // Sample position in noise field\r\n        var samplePos = (particle.Position + Offset) * Scale.GetValue( context.SystemComponent );\r\n        var time = context.Particle.Age * TimeScale;\r\n        \r\n        // Calculate curl noise using the curl of a 3D noise field\r\n        var curl = CalculateCurl(samplePos, time.GetValue( context.SystemComponent ));\r\n        \r\n        // Apply force\r\n        particle.Velocity += curl * Strength.GetValue( context.SystemComponent ) * delta * 10;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Calculate curl noise by taking the curl of a potential field\r\n    /// This creates divergence-free flow fields that look organic\r\n    /// </summary>\r\n    private Vector3 CalculateCurl(Vector3 pos, float time)\r\n    {\r\n        const float epsilon = 0.001f;\r\n        \r\n        // Sample the potential field at offset positions\r\n        // We need 6 samples to calculate the curl (derivatives in all directions)\r\n        \r\n        // dPz/dy - dPy/dz\r\n        float curlX = \r\n            (SamplePotential(pos + new Vector3(0, epsilon, 0), time).z - \r\n             SamplePotential(pos - new Vector3(0, epsilon, 0), time).z) -\r\n            (SamplePotential(pos + new Vector3(0, 0, epsilon), time).y - \r\n             SamplePotential(pos - new Vector3(0, 0, epsilon), time).y);\r\n        \r\n        // dPx/dz - dPz/dx\r\n        float curlY = \r\n            (SamplePotential(pos + new Vector3(0, 0, epsilon), time).x - \r\n             SamplePotential(pos - new Vector3(0, 0, epsilon), time).x) -\r\n            (SamplePotential(pos + new Vector3(epsilon, 0, 0), time).z - \r\n             SamplePotential(pos - new Vector3(epsilon, 0, 0), time).z);\r\n        \r\n        // dPy/dx - dPx/dy\r\n        float curlZ = \r\n            (SamplePotential(pos + new Vector3(epsilon, 0, 0), time).y - \r\n             SamplePotential(pos - new Vector3(epsilon, 0, 0), time).y) -\r\n            (SamplePotential(pos + new Vector3(0, epsilon, 0), time).x - \r\n             SamplePotential(pos - new Vector3(0, epsilon, 0), time).x);\r\n        \r\n        return new Vector3(curlX, curlY, curlZ) / (2.0f * epsilon);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Sample a 3D potential field using Perlin-like noise\r\n    /// </summary>\r\n    private Vector3 SamplePotential(Vector3 pos, float time)\r\n    {\r\n        // Create three offset noise samples for each component\r\n        // This creates a vector field from scalar noise functions\r\n        return new Vector3(\r\n            Noise3D(pos + new Vector3(0, 0, 0), time),\r\n            Noise3D(pos + new Vector3(31.416f, -47.853f, 12.793f), time),\r\n            Noise3D(pos + new Vector3(-17.737f, 86.214f, -59.482f), time)\r\n        );\r\n    }\r\n\r\n    /// <summary>\r\n    /// Simple 3D noise function using sine waves\r\n    /// You could replace this with proper Perlin/Simplex noise for better results\r\n    /// </summary>\r\n    private float Noise3D(Vector3 pos, float time)\r\n    {\r\n        // Combine multiple sine waves at different frequencies for pseudo-noise\r\n        var p = pos + new Vector3(time, time * 0.7f, time * 0.5f);\r\n        \r\n        float noise = 0;\r\n        noise += MathF.Sin(p.x * 1.0f + p.y * 1.3f) * 0.5f;\r\n        noise += MathF.Sin(p.y * 1.7f + p.z * 0.9f) * 0.3f;\r\n        noise += MathF.Sin(p.z * 2.1f + p.x * 1.1f) * 0.2f;\r\n        noise += MathF.Sin(p.x * 3.7f + p.y * 2.3f + p.z * 1.9f) * 0.15f;\r\n        \r\n        return noise;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "UnitTests/UnitTest.cs",
            "FileName": "UnitTest.cs",
            "PackageType": "library",
            "CodeKind": "UnitTest",
            "AssetVersionId": 379493,
            "Code": "global using Microsoft.VisualStudio.TestTools.UnitTesting;\r\n\r\n[TestClass]\r\npublic class TestInit\r\n{\r\n\t[AssemblyInitialize]\r\n\tpublic static void ClassInitialize( TestContext context )\r\n\t{\r\n\t\tSandbox.Application.InitUnitTest();\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "Editor/Graph/FXEditor.cs",
            "FileName": "FXEditor.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 379493,
            "Code": "using Editor;\r\nusing Sandbox;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\nnamespace fxbox.Graph;\r\n\r\n/// <summary>\r\n/// Main particle system editor - ShaderGraph style with DockManager\r\n/// </summary>\r\n[EditorForAssetType(\"fx\")]\r\n[EditorApp(\"FXBox\", \"auto_awesome\", \"Create and edit particle systems\")]\r\npublic class FXBoxEditor : DockWindow, IAssetEditor\r\n{\r\n    public bool CanOpenMultipleAssets => true;\r\n    public static ParticleResource CurrentEditingResource { get; private set; }\r\n    \r\n    private ParticleResource _resource; // Original resource from asset\r\n    private ParticleResource _workingCopy; // Copy we actually edit\r\n    private Asset _asset;\r\n    private bool _isDirty = false;\r\n\r\n    // UI Components\r\n    private EmitterList _emitterList;\r\n    private ParticlePreview _preview;\r\n    private PropertiesWidget _properties;\r\n    \r\n    private string _defaultDockState;\r\n\r\n    public FXBoxEditor()\r\n    {\r\n        DeleteOnClose = true;\r\n        Title = \"FXBox - Particle System Editor\";\r\n        Size = new Vector2(1800, 1000);\r\n\r\n        CreateToolBar();\r\n        CreateUI();\r\n        Show();\r\n    }\r\n\r\n    // StateCookie is set here, AFTER base.Show(), rather than inside CreateUI() before the\r\n    // window is even shown - same ordering as the working RectEditor reference\r\n    // (HotspotEditorWindow.Show() / Window.CreateUI()'s \"if (Visible) RestoreLayout()\").\r\n    // Setting it too early was part of why a layout you'd closed with panels open never came\r\n    // back - restoring (or building the default layout) needs the window's dock widget\r\n    // hierarchy to actually exist and be visible first.\r\n    public override void Show()\r\n    {\r\n        base.Show();\r\n        StateCookie = \"FXBoxEditor_v6\";\r\n    }\r\n\r\n    private void CreateToolBar()\r\n    {\r\n        var toolbar = new ToolBar(this, \"FXBoxToolbar\");\r\n        AddToolBar(toolbar, ToolbarPosition.Top);\r\n\r\n        toolbar.AddOption(\"Save\", \"save\", Save).StatusTip = \"Save Particle System (Ctrl+S)\";\r\n        toolbar.AddSeparator();\r\n        toolbar.AddOption(\"Add Emitter\", \"add_circle\", AddEmitter).StatusTip = \"Add new emitter\";\r\n        \r\n        // Add parameter menu\r\n        toolbar.AddSeparator();\r\n        toolbar.AddOption(\"Float Parameter\", \"looks_one\", AddFloatParameter);\r\n        toolbar.AddOption(\"Vector Parameter\", \"3d_rotation\", AddVectorParameter);\r\n        toolbar.AddOption(\"Color Parameter\", \"palette\", AddColorParameter);\r\n        \r\n        toolbar.AddSeparator();\r\n        toolbar.AddOption(\"Play\", \"play_arrow\", () => _preview?.TogglePlayback()).StatusTip = \"Play/Pause\";\r\n        toolbar.AddOption(\"Restart\", \"replay\", () => _preview?.Restart()).StatusTip = \"Restart\";\r\n    }\r\n\r\n    private void CreateUI()\r\n    {\r\n        BuildMenuBar();\r\n        BuildDock();\r\n\r\n        DockManager.Update();\r\n\r\n        _defaultDockState = DockManager.State;\r\n    }\r\n\r\n    // Builds the REAL widget instances and docks them - unlike the previous\r\n    // RegisterDockType+CreateAction version, this runs eagerly every time CreateUI runs\r\n    // (every open, every hotload), so _emitterList/_preview/_properties are never null by\r\n    // the time anything else (AssetOpen, LoadResource, etc.) tries to push data into them.\r\n    // The old lazy version only created a widget instance whenever the dock manager itself\r\n    // got around to invoking CreateAction - which, on a restored/default layout, could\r\n    // happen well after AssetOpen already tried (and silently no-op'd via ?.) to hand the\r\n    // resource to _emitterList/_preview/_properties, matching the exact \"not hooked up until\r\n    // you add a parameter or emitter\" symptom (that next SetResource/LoadParticleSystem call\r\n    // was the first one to land after CreateAction had finally run).\r\n    //\r\n    // DockManager.AddDock(title, icon, widget, area, relativeTo) both registers the dock\r\n    // TYPE under that title/icon (same as RegisterDockType did, for the View menu and\r\n    // BuildDefaultLayout's OpenDock below to find it by name) AND places this exact instance\r\n    // on screen - no separate CreateDockWidget/AddDock two-step needed.\r\n    private void BuildDock()\r\n    {\r\n        _preview = CreatePreview();\r\n        _emitterList = CreateEmitterList();\r\n        _properties = CreateProperties();\r\n\r\n        var preview = DockManager.AddDock( \"Preview\", \"visibility\", _preview, DockArea.Center );\r\n        DockManager.AddDock( \"Emitters\", \"list\", _emitterList, DockArea.Left, relativeTo: preview );\r\n        DockManager.AddDock( \"Properties\", \"tune\", _properties, DockArea.Right, relativeTo: preview );\r\n    }\r\n\r\n    // Called by the base class on a genuinely first-ever open (nothing saved under\r\n    // StateCookie yet - see the Show() override above) to decide default POSITIONS/\r\n    // proportions only. OpenDock resolves the SAME dock types BuildDock() already\r\n    // registered+placed above by Title, it doesn't build a second set of widgets.\r\n    protected override void BuildDefaultLayout()\r\n    {\r\n        var preview = DockManager.OpenDock( \"Preview\", DockArea.Center );\r\n        var emitters = DockManager.OpenDock( \"Emitters\", DockArea.Left, preview );\r\n        var properties = DockManager.OpenDock( \"Properties\", DockArea.Right, preview );\r\n\r\n        DockManager.SetSplitterProportions( emitters, 0.20f, 0.80f );\r\n        DockManager.SetSplitterProportions( properties, 0.80f, 0.20f );\r\n    }\r\n\r\n    private EmitterList CreateEmitterList()\r\n    {\r\n        var widget = new EmitterList(this);\r\n        widget.Name = \"Emitters\";\r\n        widget.WindowTitle = \"Emitters & Modules\";\r\n        widget.SetWindowIcon(\"list\");\r\n        widget.MinimumWidth = 300;\r\n        widget.OnSelectionChanged = OnSelectionChanged;\r\n        widget.OnEmitterDeleted = OnEmitterDeleted;\r\n        widget.OnModuleDeleted = OnModuleDeleted;\r\n        widget.OnAddModule = OnAddModule;\r\n        widget.OnSystemChanged = MarkDirty;\r\n        widget.OnEmitterDuplicated = OnEmitterDuplicated;\r\n        widget.OnModuleDuplicated = OnModuleDuplicated;\r\n\r\n        return widget;\r\n    }\r\n\r\n    private void OnEmitterDuplicated(ParticleEmitter emitter)\r\n    {\r\n\t    if (_workingCopy == null) return;\r\n\r\n\t    var index = _workingCopy.Emitters.IndexOf(emitter);\r\n\t    if (index == -1) return;\r\n\r\n\t    // Round-trip the entire resource through JSON, then pull out\r\n\t    // the emitter at the same index \u2014 gives us a full deep clone\r\n\t    // without needing Serialize/Deserialize on ParticleEmitter itself.\r\n\t    var resourceCopy = DeepCopyResource(_workingCopy);\r\n\t    var clone = resourceCopy.Emitters[index];\r\n\t    clone.Name = $\"{emitter.Name} (Copy)\";\r\n\r\n\t    _workingCopy.Emitters.Insert(index + 1, clone);\r\n\r\n\t    _emitterList?.SetResource(_workingCopy);\r\n\t    _preview?.LoadParticleSystem(_workingCopy);\r\n\t    MarkDirty();\r\n    }\r\n\r\n    private ParticlePreview CreatePreview()\r\n    {\r\n        var widget = new ParticlePreview(this);\r\n        widget.Name = \"Preview\";\r\n        widget.WindowTitle = \"Preview\";\r\n      \r\n        widget.SetWindowIcon(\"visibility\");\r\n        return widget;\r\n    }\r\n\r\n    private PropertiesWidget CreateProperties()\r\n    {\r\n        var widget = new PropertiesWidget(this);\r\n        widget.Name = \"Properties\";\r\n        widget.WindowTitle = \"Properties\";\r\n        widget.SetWindowIcon(\"tune\");\r\n        widget.MinimumWidth = 300;\r\n        widget.OnPropertyChanged = OnPropertyChanged;\r\n        return widget;\r\n    }\r\n\r\n    private void BuildMenuBar()\r\n    {\r\n        var file = MenuBar.AddMenu(\"File\");\r\n        file.AddOption(\"New\", \"add\", New, \"editor.new\").StatusTip = \"New Particle System\";\r\n        file.AddOption(\"Open\", \"folder_open\", Open, \"editor.open\").StatusTip = \"Open Particle System\";\r\n        file.AddOption(\"Save\", \"save\", Save, \"editor.save\").StatusTip = \"Save Particle System\";\r\n        file.AddSeparator();\r\n        file.AddOption(\"Close\", null, () => Close(), \"editor.quit\").StatusTip = \"Close Editor\";\r\n\r\n        var edit = MenuBar.AddMenu(\"Edit\");\r\n        edit.AddOption(\"Add Emitter\", \"add_circle\", AddEmitter);\r\n        edit.AddSeparator();\r\n        edit.AddOption(\"Add Float Parameter\", \"looks_one\", AddFloatParameter);\r\n        edit.AddOption(\"Add Vector Parameter\", \"3d_rotation\", AddVectorParameter);\r\n        edit.AddOption(\"Add Color Parameter\", \"palette\", AddColorParameter);\r\n\r\n        var view = MenuBar.AddMenu(\"View\");\r\n        view.AboutToShow += () => OnViewMenu(view);\r\n    }\r\n\r\n    private void OnViewMenu(Menu view)\r\n    {\r\n        view.Clear();\r\n        //view.AddOption(\"Restore To Default\", \"settings_backup_restore\", RestoreDefaultDockLayout);\r\n        view.AddSeparator();\r\n\r\n        foreach (var dock in DockManager.DockTypes)\r\n        {\r\n            var o = view.AddOption(dock.Title, dock.Icon);\r\n            o.Checkable = true;\r\n            o.Checked = DockManager.IsDockOpen(dock.Title);\r\n            o.Toggled += (b) => DockManager.SetDockState(dock.Title, b);\r\n        }\r\n    }\r\n\r\n   // protected override void RestoreDefaultDockLayout()\r\n   // {\r\n        //DockManager.State = _defaultDockState;\r\n       // SaveToStateCookie();\r\n    //}\r\n\r\n    private void AddFloatParameter()\r\n    {\r\n        if (_workingCopy == null) return;\r\n        \r\n        var param = new FloatParameter\r\n        {\r\n            Name = $\"FloatParam{_workingCopy.FloatParameters.Count + 1}\",\r\n            DefaultValue = 1.0f\r\n        };\r\n        \r\n        _workingCopy.FloatParameters.Add(param);\r\n        _properties?.ShowSystemProperties(_workingCopy);\r\n        MarkDirty();\r\n    }\r\n\r\n    private void AddVectorParameter()\r\n    {\r\n        if (_workingCopy == null) return;\r\n        \r\n        var param = new VectorParameter\r\n        {\r\n            Name = $\"VectorParam{_workingCopy.VectorParameters.Count + 1}\",\r\n            DefaultValue = Vector3.One\r\n        };\r\n        \r\n        _workingCopy.VectorParameters.Add(param);\r\n        _properties?.ShowSystemProperties(_workingCopy);\r\n        MarkDirty();\r\n    }\r\n\r\n    private void AddColorParameter()\r\n    {\r\n        if (_workingCopy == null) return;\r\n        \r\n        var param = new ColorParameter\r\n        {\r\n            Name = $\"ColorParam{_workingCopy.ColorParameters.Count + 1}\",\r\n            DefaultValue = Color.White\r\n        };\r\n        \r\n        _workingCopy.ColorParameters.Add(param);\r\n        _properties?.ShowSystemProperties(_workingCopy);\r\n        MarkDirty();\r\n    }\r\n\r\n    public void AssetOpen(Asset asset)\r\n    {\r\n        _asset = asset;\r\n        _resource = asset.LoadResource<ParticleResource>();\r\n\r\n        if (_resource == null)\r\n        {\r\n            _resource = new ParticleResource();\r\n            var defaultEmitter = new ParticleEmitter { Name = \"Emitter 1\" };\r\n            AddDefaultModules(defaultEmitter);\r\n            _resource.Emitters.Add(defaultEmitter);\r\n        }\r\n\r\n        // Create a deep copy for editing\r\n        _workingCopy = DeepCopyResource(_resource);\r\n\r\n        Title = $\"FXBox - {asset.Name}\";\r\n        LoadResource();\r\n        Focus();\r\n    }\r\n\r\n    private ParticleResource DeepCopyResource(ParticleResource source)\r\n    {\r\n        if (source == null) return null;\r\n\r\n        var json = source.Serialize().ToJsonString();\r\n        var copy = new ParticleResource();\r\n        copy.Deserialize(Json.ParseToJsonObject(json));\r\n        copy.IsDirty = false;\r\n        \r\n        return copy;\r\n    }\r\n\r\n    private void LoadResource()\r\n    {\r\n        CurrentEditingResource = _workingCopy;\r\n        \r\n        _emitterList?.SetResource(_workingCopy);\r\n        _preview?.LoadParticleSystem(_workingCopy);\r\n        _properties?.ShowSystemProperties(_workingCopy);\r\n        _isDirty = false;\r\n        if (_asset != null)\r\n            _asset.HasUnsavedChanges = false;\r\n    }\r\n\r\n    [Shortcut(\"editor.new\", \"CTRL+N\", ShortcutType.Window)]\r\n    private void New()\r\n    {\r\n        PromptSave(() => CreateNew());\r\n    }\r\n\r\n    private void CreateNew()\r\n    {\r\n        _asset = null;\r\n        _resource = new ParticleResource();\r\n        var defaultEmitter = new ParticleEmitter { Name = \"Emitter 1\" };\r\n        AddDefaultModules(defaultEmitter);\r\n        _resource.Emitters.Add(defaultEmitter);\r\n        \r\n        _workingCopy = DeepCopyResource(_resource);\r\n        \r\n        Title = \"FXBox - Untitled\";\r\n        LoadResource();\r\n    }\r\n\r\n    [Shortcut(\"editor.open\", \"CTRL+O\", ShortcutType.Window)]\r\n    private void Open()\r\n    {\r\n        var fd = new FileDialog(null)\r\n        {\r\n            Title = \"Open Particle System\",\r\n            DefaultSuffix = \".fx\"\r\n        };\r\n\r\n        fd.SetNameFilter(\"Particle System (*.fx)\");\r\n\r\n        if (!fd.Execute())\r\n            return;\r\n\r\n        PromptSave(() => OpenFile(fd.SelectedFile));\r\n    }\r\n\r\n    private void OpenFile(string path)\r\n    {\r\n        var asset = AssetSystem.FindByPath(path);\r\n        if (asset != null)\r\n        {\r\n            AssetOpen(asset);\r\n        }\r\n    }\r\n\r\n    private void AddEmitter()\r\n    {\r\n        if (_workingCopy == null) return;\r\n        \r\n        var emitter = new ParticleEmitter \r\n        { \r\n            Name = $\"Emitter {_workingCopy.Emitters.Count + 1}\"\r\n        };\r\n\r\n        AddDefaultModules(emitter);\r\n        _workingCopy.Emitters.Add(emitter);\r\n        \r\n        _emitterList?.SetResource(_workingCopy);\r\n        _preview?.LoadParticleSystem(_workingCopy);\r\n        MarkDirty();\r\n    }\r\n\r\n    private void AddDefaultModules(ParticleEmitter emitter)\r\n    {\r\n        emitter.SpawnModules.Add(new SpawnRateModule { Name = \"Spawn Rate\" });\r\n        emitter.InitializeModules.Add(new InitializePositionModule { Name = \"Initialize Position\" });\r\n        emitter.InitializeModules.Add(new InitializeLocalSpaceModule { Name = \"Initialize Local Space\" });\r\n        emitter.InitializeModules.Add(new InitializeVelocityModule { Name = \"Initialize Velocity\" });\r\n        emitter.InitializeModules.Add(new InitializeLifetimeModule { Name = \"Initialize Lifetime\" });\r\n        emitter.InitializeModules.Add(new InitializeSizeModule { Name = \"Initialize Size\" });\r\n        emitter.InitializeModules.Add(new InitializeColorModule { Name = \"Initialize Color\" });\r\n        emitter.UpdateModules.Add(new GravityForceModule { Name = \"Gravity\" });\r\n        emitter.UpdateModules.Add(new DragForceModule { Name = \"Drag\" });\r\n        emitter.RenderModules.Add(new SpriteRendererModule { Name = \"Sprite Renderer\" });\r\n    }\r\n\r\n    private void OnAddModule(ParticleEmitter emitter, ModuleStage stage)\r\n    {\r\n        var menu = new Menu(this);\r\n\r\n        var moduleTypes = EditorTypeLibrary.GetTypes<ParticleModule>()\r\n            .Where(t => !t.IsAbstract)\r\n            .Select(t => t.TargetType)\r\n            .Where(t => {\r\n                var instance = System.Activator.CreateInstance(t) as ParticleModule;\r\n                return instance?.Stage == stage;\r\n            });\r\n\r\n        foreach (var moduleType in moduleTypes.OrderBy(t => t.Name))\r\n        {\r\n            var displayInfo = DisplayInfo.ForType(moduleType);\r\n            menu.AddOption(displayInfo.Name, displayInfo.Icon ?? \"extension\", () => {\r\n                var module = System.Activator.CreateInstance(moduleType) as ParticleModule;\r\n                if (module != null)\r\n                {\r\n                    module.Name = displayInfo.Name;\r\n                    \r\n                    var targetList = stage switch\r\n                    {\r\n                        ModuleStage.Spawn => emitter.SpawnModules,\r\n                        ModuleStage.Initialize => emitter.InitializeModules,\r\n                        ModuleStage.Update => emitter.UpdateModules,\r\n                        ModuleStage.Render => emitter.RenderModules,\r\n                        _ => null\r\n                    };\r\n\r\n                    targetList?.Add(module);\r\n                    _emitterList?.SetResource(_workingCopy);\r\n                    _preview?.LoadParticleSystem(_workingCopy);\r\n                    MarkDirty();\r\n                }\r\n            });\r\n        }\r\n\r\n        menu.OpenAtCursor();\r\n    }\r\n\r\n    private void OnSelectionChanged(object target)\r\n    {\r\n        if (target is ParticleResource resource)\r\n        {\r\n            _properties?.ShowSystemProperties(resource);\r\n        }\r\n        else if (target is ParticleEmitter emitter)\r\n        {\r\n            _properties?.ShowEmitterProperties(emitter);\r\n        }\r\n        else if (target is ParticleModule module)\r\n        {\r\n            _properties?.ShowModuleProperties(module);\r\n        }\r\n    }\r\n\r\n    private void OnEmitterDeleted(ParticleEmitter emitter)\r\n    {\r\n        _workingCopy.Emitters.Remove(emitter);\r\n        _emitterList?.SetResource(_workingCopy);\r\n        _preview?.LoadParticleSystem(_workingCopy);\r\n        _properties?.ShowSystemProperties(_workingCopy);\r\n        MarkDirty();\r\n    }\r\n\r\n    private void OnModuleDeleted(ParticleModule module)\r\n    {\r\n        foreach (var emitter in _workingCopy.Emitters)\r\n        {\r\n            emitter.SpawnModules.Remove(module);\r\n            emitter.InitializeModules.Remove(module);\r\n            emitter.UpdateModules.Remove(module);\r\n            emitter.RenderModules.Remove(module);\r\n        }\r\n\r\n        _emitterList?.SetResource(_workingCopy);\r\n        _preview?.LoadParticleSystem(_workingCopy);\r\n        MarkDirty();\r\n    }\r\n\r\n    private void OnPropertyChanged()\r\n    {\r\n        _preview?.LoadParticleSystem(_workingCopy);\r\n        MarkDirty();\r\n    }\r\n\r\n    private void MarkDirty()\r\n    {\r\n        _preview?.LoadParticleSystem(_workingCopy);\r\n        \r\n        if (!_isDirty)\r\n        {\r\n            if (_workingCopy != null)\r\n                _workingCopy.IsDirty = true;\r\n            if (_resource != null)\r\n                _resource.IsDirty = true;\r\n                \r\n            _isDirty = true;\r\n            \r\n            if (_asset != null)\r\n                _asset.HasUnsavedChanges = true;\r\n                \r\n            Title = $\"FXBox - {_asset?.Name ?? \"Untitled\"}*\";\r\n        }\r\n    }\r\n\r\n    [Shortcut(\"editor.save\", \"CTRL+S\", ShortcutType.Window)]\r\n    private void Save()\r\n    {\r\n        if (_asset != null && _workingCopy != null)\r\n        {\r\n            _workingCopy.IsDirty = false;\r\n            _workingCopy.Version++;\r\n            var json = _workingCopy.Serialize().ToJsonString();\r\n            System.IO.File.WriteAllText(_asset.AbsolutePath, json);\r\n            \r\n            _resource = DeepCopyResource(_workingCopy);\r\n            _resource.IsDirty = false;\r\n            \r\n            _isDirty = false;\r\n            _asset.HasUnsavedChanges = false;\r\n            \r\n            Title = $\"FXBox - {_asset.Name}\";\r\n            Log.Info($\"Saved: {_asset.Name}\");\r\n        }\r\n        else if (_workingCopy != null)\r\n        {\r\n            // No asset yet, prompt for save location\r\n            SaveAs();\r\n        }\r\n    }\r\n\r\n    private void SaveAs()\r\n    {\r\n        var fd = new FileDialog(null)\r\n        {\r\n            Title = \"Save Particle System\",\r\n            DefaultSuffix = \".fx\"\r\n        };\r\n\r\n        fd.SelectFile(\"untitled.fx\");\r\n        fd.SetFindFile();\r\n        fd.SetModeSave();\r\n        fd.SetNameFilter(\"Particle System (*.fx)\");\r\n        \r\n        if (!fd.Execute())\r\n            return;\r\n\r\n        var savePath = fd.SelectedFile;\r\n        \r\n        _workingCopy.IsDirty = false;\r\n        _workingCopy.Version++;\r\n        var json = _workingCopy.Serialize().ToJsonString();\r\n        System.IO.File.WriteAllText(savePath, json);\r\n        \r\n        _asset = AssetSystem.RegisterFile(savePath);\r\n        _resource = DeepCopyResource(_workingCopy);\r\n        _resource.IsDirty = false;\r\n        \r\n        _isDirty = false;\r\n        if (_asset != null)\r\n            _asset.HasUnsavedChanges = false;\r\n        \r\n        Title = $\"FXBox - {_asset.Name}\";\r\n        Log.Info($\"Saved: {_asset.Name}\");\r\n    }\r\n\r\n    \r\n    private void OnModuleDuplicated(ParticleModule module, ModuleStage stage)\r\n    {\r\n\t    if (_workingCopy == null) return;\r\n\r\n\t    foreach (var emitter in _workingCopy.Emitters)\r\n\t    {\r\n\t\t    var moduleList = stage switch\r\n\t\t    {\r\n\t\t\t    ModuleStage.Spawn      => emitter.SpawnModules,\r\n\t\t\t    ModuleStage.Initialize => emitter.InitializeModules,\r\n\t\t\t    ModuleStage.Update     => emitter.UpdateModules,\r\n\t\t\t    ModuleStage.Render     => emitter.RenderModules,\r\n\t\t\t    _                      => null\r\n\t\t    };\r\n\r\n\t\t    if (moduleList == null) continue;\r\n\r\n\t\t    var index = moduleList.IndexOf(module);\r\n\t\t    if (index == -1) continue;\r\n\r\n\t\t    // Round-trip the whole resource; the module will be at the same\r\n\t\t    // emitter index + stage index in the cloned copy.\r\n\t\t    var emitterIndex = _workingCopy.Emitters.IndexOf(emitter);\r\n\t\t    var resourceCopy = DeepCopyResource(_workingCopy);\r\n\r\n\t\t    var clonedList = stage switch\r\n\t\t    {\r\n\t\t\t    ModuleStage.Spawn      => resourceCopy.Emitters[emitterIndex].SpawnModules,\r\n\t\t\t    ModuleStage.Initialize => resourceCopy.Emitters[emitterIndex].InitializeModules,\r\n\t\t\t    ModuleStage.Update     => resourceCopy.Emitters[emitterIndex].UpdateModules,\r\n\t\t\t    ModuleStage.Render     => resourceCopy.Emitters[emitterIndex].RenderModules,\r\n\t\t\t    _                      => null\r\n\t\t    };\r\n\r\n\t\t    if (clonedList == null) break;\r\n\r\n\t\t    var clone = clonedList[index];\r\n\t\t    clone.Name = $\"{module.Name} (Copy)\";\r\n\t\t    moduleList.Insert(index + 1, clone);\r\n\r\n\t\t    break; // modules are unique instances, no need to keep iterating\r\n\t    }\r\n\r\n\t    _emitterList?.SetResource(_workingCopy);\r\n\t    _preview?.LoadParticleSystem(_workingCopy);\r\n\t    MarkDirty();\r\n    }\r\n\r\n    private void PromptSave(System.Action action)\r\n    {\r\n        if (!_isDirty)\r\n        {\r\n            action?.Invoke();\r\n            return;\r\n        }\r\n\r\n        var confirm = new PopupWindow(\r\n            \"Save Current Particle System\", \r\n            \"The particle system has unsaved changes. Would you like to save now?\", \r\n            \"Cancel\",\r\n            new Dictionary<string, System.Action>()\r\n            {\r\n                { \"No\", () => { _isDirty = false; action?.Invoke(); } },\r\n                { \"Yes\", () => { Save(); if (!_isDirty) action?.Invoke(); } }\r\n            }\r\n        );\r\n\r\n        confirm.Show();\r\n    }\r\n\r\n    public void SelectMember(string memberName) { }\r\n\r\n    protected override bool OnClose()\r\n    {\r\n        CurrentEditingResource = null;\r\n        \r\n        if (_isDirty)\r\n        {\r\n            PromptSave(() => { _isDirty = false; Close(); });\r\n            return false;\r\n        }\r\n\r\n        return base.OnClose();\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Left panel showing emitters and their modules in a tree structure\r\n/// </summary>\r\npublic class EmitterList : Widget\r\n{\r\n    private TreeView _tree;\r\n    private ParticleResource _resource;\r\n\r\n    public System.Action<object> OnSelectionChanged;\r\n    public System.Action<ParticleEmitter> OnEmitterDeleted;\r\n    public System.Action<ParticleModule> OnModuleDeleted;\r\n    public System.Action<ParticleEmitter, ModuleStage> OnAddModule;\r\n    public System.Action<ParticleEmitter> OnEmitterDuplicated;\r\n    public System.Action<ParticleModule, ModuleStage> OnModuleDuplicated;\r\n    public System.Action OnSystemChanged;\r\n    \r\n    public EmitterList(Widget parent) : base(parent)\r\n    {\r\n        Layout = Layout.Column();\r\n        Layout.Spacing = 0;\r\n\r\n        // Header\r\n        var header = new Widget(this);\r\n        header.Layout = Layout.Row();\r\n        header.Layout.Spacing = 8;\r\n        header.Layout.Margin = 8;\r\n        header.MinimumHeight = 32;\r\n        header.SetStyles(\"background-color: #1e1e1e;\");\r\n\r\n        var label = new Label(\"Emitters & Modules\", header);\r\n        label.SetStyles(\"font-weight: bold; font-size: 14px;\");\r\n        header.Layout.Add(label, 1);\r\n\r\n        Layout.Add(header);\r\n\r\n        _tree = new TreeView(this);\r\n        _tree.AcceptDrops = true;\r\n        _tree.ItemClicked = OnItemActivated;\r\n        _tree.ItemContextMenu = OnItemContextMenu;\r\n        _tree.ItemSelected = OnTreeMousePress;\r\n        Layout.Add(_tree, 1);\r\n    }\r\n\r\n    private void OnTreeMousePress(object item)\r\n    {\r\n        TryHandleStageButtonClick(item);\r\n    }\r\n\r\n    private bool TryHandleStageButtonClick(object item)\r\n    {\r\n        if (item is not StageNode stageNode)\r\n            return false;\r\n\r\n        if (!_tree.TryGetItemRect(stageNode, out var itemRect))\r\n            return false;\r\n\r\n        var buttonRect = new Rect(itemRect.Right - 24, itemRect.Top, 24, itemRect.Height);\r\n        \r\n        if (itemRect.IsInside(buttonRect))\r\n        {\r\n            OnAddModule?.Invoke(stageNode.Emitter, stageNode.Stage);\r\n            return true;\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    public void SetResource(ParticleResource resource)\r\n    {\r\n        _resource = resource;\r\n        RebuildTree();\r\n    }\r\n\r\n    private void RebuildTree()\r\n    {\r\n        _tree.Clear();\r\n\r\n        if (_resource == null) return;\r\n\r\n        var systemNode = new SystemNode(_resource);\r\n        _tree.AddItem(systemNode);\r\n        _tree.Open(systemNode);\r\n\r\n        foreach (var emitter in _resource.Emitters)\r\n        {\r\n            var emitterNode = new EmitterNode(emitter);\r\n            systemNode.AddItem(emitterNode);\r\n            _tree.Open(emitterNode);\r\n\r\n            AddStageNode(emitterNode, \"Spawn\", ModuleStage.Spawn, emitter.SpawnModules, emitter);\r\n            AddStageNode(emitterNode, \"Initialize\", ModuleStage.Initialize, emitter.InitializeModules, emitter);\r\n            AddStageNode(emitterNode, \"Update\", ModuleStage.Update, emitter.UpdateModules, emitter);\r\n            AddStageNode(emitterNode, \"Render\", ModuleStage.Render, emitter.RenderModules, emitter);\r\n        }\r\n    }\r\n\r\n    private void AddStageNode(TreeNode parent, string name, ModuleStage stage, List<ParticleModule> modules, ParticleEmitter emitter)\r\n    {\r\n        var stageNode = new StageNode(name, stage, emitter, modules.Count);\r\n        parent.AddItem(stageNode);\r\n        _tree.Open(stageNode);\r\n\r\n        foreach (var module in modules)\r\n        {\r\n            var moduleNode = new ModuleNode(module, stage);\r\n            stageNode.AddItem(moduleNode);\r\n        }\r\n    }\r\n\r\n    private void OnItemActivated(object item)\r\n    {\r\n        var selected = item;\r\n        if (selected == null) return;\r\n        \r\n        if (selected is SystemNode systemNode)\r\n            OnSelectionChanged?.Invoke(systemNode.Resource);\r\n        else if (selected is EmitterNode emitterNode)\r\n            OnSelectionChanged?.Invoke(emitterNode.Emitter);\r\n        else if (selected is ModuleNode moduleNode)\r\n            OnSelectionChanged?.Invoke(moduleNode.Module);\r\n    }\r\n\r\n    private void OnItemContextMenu(object item)\r\n    {\r\n        var selected = item;\r\n        if (selected == null) return;\r\n\r\n        var menu = new Menu(this);\r\n\r\n        if (selected is EmitterNode emitterNode)\r\n        {\r\n\t        menu.AddOption(\"Duplicate\", \"content_copy\", () => OnEmitterDuplicated?.Invoke(emitterNode.Emitter));\r\n\t        menu.AddSeparator();\r\n\t        menu.AddOption(\"Delete Emitter\", \"delete\", () => OnEmitterDeleted?.Invoke(emitterNode.Emitter));\r\n        }\r\n        else if (selected is ModuleNode moduleNode)\r\n        {\r\n\t        var module = moduleNode.Module;\r\n\r\n\t        menu.AddOption(\"Duplicate\", \"content_copy\", () => OnModuleDuplicated?.Invoke(module, moduleNode.Stage));\r\n\r\n\t        menu.AddOption(module.Enabled ? \"Disable\" : \"Enable\",\r\n\t\t        module.Enabled ? \"visibility_off\" : \"visibility\",\r\n\t\t        () => {\r\n\t\t\t        module.Enabled = !module.Enabled;\r\n\t\t\t        OnSystemChanged?.Invoke();\r\n\t\t\t        RebuildTree();\r\n\t\t        });\r\n\r\n\t        menu.AddSeparator();\r\n\t        menu.AddOption(\"Delete Module\", \"delete\", () => OnModuleDeleted?.Invoke(module));\r\n        }\r\n        else if (selected is StageNode stageNode)\r\n        {\r\n            menu.AddOption(\"Add Module\", \"add\", () => OnAddModule?.Invoke(stageNode.Emitter, stageNode.Stage));\r\n        }\r\n\r\n        menu.OpenAtCursor();\r\n    }\r\n\r\n    // TreeNode classes\r\n    private class SystemNode : TreeNode\r\n    {\r\n        public ParticleResource Resource { get; }\r\n        \r\n        public SystemNode(ParticleResource resource)\r\n        {\r\n            Resource = resource;\r\n        }\r\n\r\n        public override void OnPaint(VirtualWidget item)\r\n        {\r\n            PaintSelection(item);\r\n            \r\n            var rect = item.Rect.Shrink(4, 2);\r\n            Paint.SetDefaultFont();\r\n            Paint.SetPen(Theme.Text);\r\n            Paint.DrawIcon(rect, \"auto_awesome\", 16, TextFlag.LeftCenter);\r\n            \r\n            rect.Left += 24;\r\n            Paint.DrawText(rect, \"Particle System\", TextFlag.LeftCenter);\r\n        }\r\n    }\r\n\r\n    private class EmitterNode : TreeNode\r\n    {\r\n        public ParticleEmitter Emitter { get; }\r\n        \r\n        public EmitterNode(ParticleEmitter emitter)\r\n        {\r\n            Emitter = emitter;\r\n        }\r\n\r\n        public override void OnPaint(VirtualWidget item)\r\n        {\r\n            PaintSelection(item);\r\n            \r\n            var rect = item.Rect.Shrink(4, 2);\r\n            Paint.SetDefaultFont();\r\n            Paint.SetPen(Theme.Green);\r\n            Paint.DrawText(rect, \"\u25cf \", TextFlag.LeftCenter);\r\n            \r\n            rect.Left += 20;\r\n            Paint.SetPen(Theme.Text);\r\n            Paint.DrawText(rect, Emitter.Name, TextFlag.LeftCenter);\r\n        }\r\n    }\r\n\r\n    private class StageNode : TreeNode\r\n    {\r\n        public new string Name { get; }\r\n        public ModuleStage Stage { get; }\r\n        public ParticleEmitter Emitter { get; }\r\n        public int Count { get; }\r\n        \r\n        public StageNode(string name, ModuleStage stage, ParticleEmitter emitter, int count)\r\n        {\r\n            Name = name;\r\n            Stage = stage;\r\n            Emitter = emitter;\r\n            Count = count;\r\n        }\r\n\r\n        public override void OnPaint(VirtualWidget item)\r\n        {\r\n            PaintSelection(item);\r\n            \r\n            var rect = item.Rect.Shrink(4, 2);\r\n            Paint.SetDefaultFont();\r\n            \r\n            var stageColor = Stage switch\r\n            {\r\n                ModuleStage.Spawn => new Color(1f, 0.6f, 0.2f),\r\n                ModuleStage.Initialize => new Color(0.3f, 0.8f, 0.3f),\r\n                ModuleStage.Update => new Color(0.3f, 0.6f, 1f),\r\n                ModuleStage.Render => new Color(0.9f, 0.3f, 0.9f),\r\n                _ => Theme.Text\r\n            };\r\n            \r\n            Paint.SetPen(stageColor);\r\n            Paint.DrawText(rect, $\"{Name} ({Count})\", TextFlag.LeftCenter);\r\n            \r\n            var buttonRect = new Rect(rect.Right - 24, rect.Top, 24, rect.Height);\r\n            \r\n            if (buttonRect.IsInside(item.Rect))\r\n            {\r\n                Paint.ClearPen();\r\n                Paint.SetBrush(Theme.ControlBackground.Lighten(0.2f));\r\n                Paint.DrawRect(buttonRect.Shrink(2), 2);\r\n            }\r\n            \r\n            Paint.SetPen(stageColor);\r\n            Paint.DrawIcon(buttonRect, \"add\", 16, TextFlag.Center);\r\n            \r\n            if (item.Dropping)\r\n            {\r\n                Paint.ClearPen();\r\n                Paint.SetBrush(Theme.Blue.WithAlpha(0.2f));\r\n                Paint.DrawRect(item.Rect, 2);\r\n            }\r\n        }\r\n\r\n        public override DropAction OnDragDrop(BaseItemWidget.ItemDragEvent e)\r\n        {\r\n            if (e.Data.Object is not ModuleNode draggedNode)\r\n                return DropAction.Ignore;\r\n\r\n            var draggedModule = draggedNode.Module;\r\n\r\n            if (draggedNode.Stage != Stage)\r\n                return DropAction.Ignore;\r\n\r\n            if (e.IsDrop)\r\n            {\r\n                var targetList = Stage switch\r\n                {\r\n                    ModuleStage.Spawn => Emitter.SpawnModules,\r\n                    ModuleStage.Initialize => Emitter.InitializeModules,\r\n                    ModuleStage.Update => Emitter.UpdateModules,\r\n                    ModuleStage.Render => Emitter.RenderModules,\r\n                    _ => null\r\n                };\r\n                \r\n                if (targetList == null) return DropAction.Ignore;\r\n\r\n                targetList.Remove(draggedModule);\r\n                targetList.Add(draggedModule);\r\n                \r\n                if (TreeView.Parent is EmitterList list)\r\n                {\r\n                    list.OnSystemChanged?.Invoke();\r\n                    list.RebuildTree();\r\n                }\r\n            }\r\n\r\n            return DropAction.Move;\r\n        }\r\n    }\r\n\r\n    private class ModuleNode : TreeNode\r\n    {\r\n        public ParticleModule Module { get; }\r\n        public ModuleStage Stage { get; }\r\n        \r\n        public ModuleNode(ParticleModule module, ModuleStage stage)\r\n        {\r\n            Module = module;\r\n            Stage = stage;\r\n        }\r\n\r\n        public override void OnPaint(VirtualWidget item)\r\n        {\r\n            PaintSelection(item);\r\n            \r\n            var displayInfo = DisplayInfo.ForType(Module.GetType());\r\n            var rect = item.Rect.Shrink(4, 2);\r\n            \r\n            Paint.SetDefaultFont();\r\n            Paint.SetPen(Module.Enabled ? Theme.Text : Theme.Text.WithAlpha(0.5f));\r\n            \r\n            if (!string.IsNullOrEmpty(displayInfo.Icon))\r\n            {\r\n                Paint.DrawIcon(rect, displayInfo.Icon, 16, TextFlag.LeftCenter);\r\n                rect.Left += 24;\r\n            }\r\n            \r\n            Paint.DrawText(rect, Module.Name ?? displayInfo.Name, TextFlag.LeftCenter);\r\n            \r\n            if (item.Dropping)\r\n            {\r\n                Paint.ClearPen();\r\n                Paint.SetBrush(Theme.Blue.WithAlpha(0.2f));\r\n                \r\n                if (TreeView.CurrentItemDragEvent.DropEdge.HasFlag(BaseItemWidget.ItemEdge.Top))\r\n                {\r\n                    var droprect = item.Rect;\r\n                    droprect.Top -= 1;\r\n                    droprect.Height = 2;\r\n                    Paint.DrawRect(droprect, 2);\r\n                }\r\n                else if (TreeView.CurrentItemDragEvent.DropEdge.HasFlag(BaseItemWidget.ItemEdge.Bottom))\r\n                {\r\n                    var droprect = item.Rect;\r\n                    droprect.Top = droprect.Bottom - 1;\r\n                    droprect.Height = 2;\r\n                    Paint.DrawRect(droprect, 2);\r\n                }\r\n                else\r\n                {\r\n                    Paint.DrawRect(item.Rect, 2);\r\n                }\r\n            }\r\n        }\r\n\r\n        public override bool OnDragStart()\r\n        {\r\n            var drag = new Drag(TreeView);\r\n            drag.Data.Object = this;\r\n            drag.Execute();\r\n            return true;\r\n        }\r\n\r\n        public override DropAction OnDragDrop(BaseItemWidget.ItemDragEvent e)\r\n        {\r\n            if (e.Data.Object is not ModuleNode draggedNode)\r\n                return DropAction.Ignore;\r\n\r\n            var draggedModule = draggedNode.Module;\r\n            var targetModule = Module;\r\n\r\n            if (draggedNode.Stage != Stage)\r\n                return DropAction.Ignore;\r\n\r\n            var emitterList = TreeView.Parent as EmitterList;\r\n            if (emitterList?._resource == null) return DropAction.Ignore;\r\n\r\n            foreach (var emitter in emitterList._resource.Emitters)\r\n            {\r\n                var moduleList = Stage switch\r\n                {\r\n                    ModuleStage.Spawn => emitter.SpawnModules,\r\n                    ModuleStage.Initialize => emitter.InitializeModules,\r\n                    ModuleStage.Update => emitter.UpdateModules,\r\n                    ModuleStage.Render => emitter.RenderModules,\r\n                    _ => null\r\n                };\r\n\r\n                if (moduleList == null) continue;\r\n\r\n                var draggedIndex = moduleList.IndexOf(draggedModule);\r\n                var targetIndex = moduleList.IndexOf(targetModule);\r\n\r\n                if (draggedIndex == -1 || targetIndex == -1) continue;\r\n\r\n                if (e.IsDrop)\r\n                {\r\n                    moduleList.RemoveAt(draggedIndex);\r\n                    \r\n                    if (draggedIndex < targetIndex)\r\n                        targetIndex--;\r\n                    \r\n                    if (e.DropEdge.HasFlag(BaseItemWidget.ItemEdge.Top))\r\n                    {\r\n                        moduleList.Insert(targetIndex, draggedModule);\r\n                    }\r\n                    else if (e.DropEdge.HasFlag(BaseItemWidget.ItemEdge.Bottom))\r\n                    {\r\n                        moduleList.Insert(targetIndex + 1, draggedModule);\r\n                    }\r\n                    else\r\n                    {\r\n                        moduleList.Insert(targetIndex, draggedModule);\r\n                    }\r\n\r\n                    emitterList.OnSystemChanged?.Invoke();\r\n                    emitterList.RebuildTree();\r\n                }\r\n                \r\n                return DropAction.Move;\r\n            }\r\n\r\n            return DropAction.Ignore;\r\n        }\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Right panel showing properties - ShaderGraph style\r\n/// </summary>\r\npublic class PropertiesWidget : Widget\r\n{\r\n    private Label _titleLabel;\r\n    private Layout _contentLayout;\r\n    private object _currentTarget;\r\n    private SerializedObject _currentSerializedObject;\r\n\r\n    public System.Action OnPropertyChanged;\r\n\r\n    public PropertiesWidget(Widget parent) : base(parent)\r\n    {\r\n        Layout = Layout.Column();\r\n        Layout.Spacing = 0;\r\n\r\n        var header = new Widget(this);\r\n        header.Layout = Layout.Column();\r\n        header.Layout.Margin = 8;\r\n        header.MinimumHeight = 32;\r\n        header.SetStyles(\"background-color: #1e1e1e;\");\r\n\r\n        _titleLabel = new Label(\"Properties\", header);\r\n        _titleLabel.SetStyles(\"font-weight: bold; font-size: 14px;\");\r\n        header.Layout.Add(_titleLabel);\r\n\r\n        Layout.Add(header);\r\n\r\n        _contentLayout = Layout.AddColumn(1);\r\n    }\r\n\r\n    public void ShowSystemProperties(ParticleResource resource)\r\n    {\r\n        _currentTarget = resource;\r\n        _titleLabel.Text = \"System Properties\";\r\n        \r\n        RebuildContent(() => {\r\n            if (resource != null)\r\n            {\r\n                var so = resource.GetSerialized();\r\n                so.OnPropertyChanged += OnSerializedPropertyChanged;\r\n                return so;\r\n            }\r\n            return null;\r\n        });\r\n    }\r\n\r\n    public void ShowEmitterProperties(ParticleEmitter emitter)\r\n    {\r\n        _currentTarget = emitter;\r\n        _titleLabel.Text = $\"Emitter: {emitter.Name}\";\r\n        \r\n        RebuildContent(() => {\r\n            if (emitter != null)\r\n            {\r\n                var so = emitter.GetSerialized();\r\n                so.OnPropertyChanged += OnSerializedPropertyChanged;\r\n                return so;\r\n            }\r\n            return null;\r\n        });\r\n    }\r\n\r\n    public void ShowModuleProperties(ParticleModule module)\r\n    {\r\n        _currentTarget = module;\r\n        \r\n        var displayInfo = DisplayInfo.ForType(module.GetType());\r\n        _titleLabel.Text = displayInfo.Name;\r\n        \r\n        RebuildContent(() => {\r\n            if (module != null)\r\n            {\r\n                var so = module.GetSerialized();\r\n                so.OnPropertyChanged += OnSerializedPropertyChanged;\r\n                return so;\r\n            }\r\n            return null;\r\n        });\r\n    }\r\n\r\n    private void OnSerializedPropertyChanged(SerializedProperty property)\r\n    {\r\n        if (_currentSerializedObject != null && _currentTarget != null)\r\n        {\r\n            _currentSerializedObject.NoteFinishEdit(property);\r\n        }\r\n        \r\n        OnPropertyChanged?.Invoke();\r\n    }\r\n\r\n    private void RebuildContent(System.Func<SerializedObject> getSerializedObject)\r\n    {\r\n        _contentLayout.Clear(true);\r\n\r\n        if (_currentSerializedObject != null)\r\n        {\r\n            _currentSerializedObject.OnPropertyChanged -= OnSerializedPropertyChanged;\r\n        }\r\n\r\n        var scroll = new ScrollArea(this);\r\n        scroll.Canvas = new Widget(scroll);\r\n        scroll.Canvas.Layout = Layout.Column();\r\n        scroll.Canvas.Layout.Margin = 8;\r\n        scroll.Canvas.Layout.Spacing = 4;\r\n        scroll.Canvas.VerticalSizeMode = SizeMode.CanGrow;\r\n        scroll.Canvas.HorizontalSizeMode = SizeMode.Flexible;\r\n\r\n        var so = getSerializedObject();\r\n        if (so != null)\r\n        {\r\n            _currentSerializedObject = so;\r\n            \r\n            var sheet = new ControlSheet();\r\n            sheet.AddObject(so);\r\n            \r\n            scroll.Canvas.Layout.Add(sheet);\r\n            scroll.Canvas.Layout.AddStretchCell();\r\n        }\r\n        else\r\n        {\r\n            _currentSerializedObject = null;\r\n        }\r\n\r\n        _contentLayout.Add(scroll);\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "UnitTests/LibraryTest.cs",
            "FileName": "LibraryTest.cs",
            "PackageType": "library",
            "CodeKind": "UnitTest",
            "AssetVersionId": 379493,
            "Code": "using Sandbox;\r\n\r\n[TestClass]\r\npublic partial class LibraryTests\r\n{\r\n\t[TestMethod]\r\n\tpublic void SceneTest()\r\n\t{\r\n\t\tvar scene = new Scene();\r\n\t\tusing ( scene.Push() )\r\n\t\t{\r\n\t\t\tvar go = new GameObject();\r\n\r\n\t\t\tAssert.AreEqual( 1, scene.Directory.GameObjectCount );\r\n\t\t}\r\n\t}\r\n\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "FXParameter.cs",
            "FileName": "FXParameter.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 379493,
            "Code": "using System;\r\nusing Sandbox;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\nnamespace fxbox;\r\n\r\n/// <summary>\r\n/// A named float parameter that can be used to control particle values\r\n/// </summary>\r\npublic class FloatParameter\r\n{\r\n\t[Property] public string Name { get; set; } = \"Parameter\";\r\n\t[Property] public float DefaultValue { get; set; } = 1.0f;\r\n\t[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();\r\n}\r\n\r\n/// <summary>\r\n/// A named vector parameter that can be used to control particle values\r\n/// </summary>\r\npublic class VectorParameter\r\n{\r\n\t[Property] public string Name { get; set; } = \"VectorParameter\";\r\n\t[Property] public Vector3 DefaultValue { get; set; } = Vector3.One;\r\n\t[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();\r\n}\r\n\r\n/// <summary>\r\n/// A named color parameter that can be used to control particle values\r\n/// </summary>\r\npublic class ColorParameter\r\n{\r\n\t[Property] public string Name { get; set; } = \"ColorParameter\";\r\n\t[Property] public ParticleGradient DefaultValue { get; set; } = Color.White;\r\n\t[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "ParticleResource.cs",
            "FileName": "ParticleResource.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 379493,
            "Code": "using System;\r\nusing Sandbox;\r\nusing System.Collections.Generic;\r\nusing System.ComponentModel;\r\nusing System.Linq;\r\nusing System.Text.Json;\r\nusing System.Text.Json.Serialization;\r\n\r\nnamespace fxbox;\r\n\r\n/// <summary>\r\n/// Particle system resource containing multiple emitters\r\n/// </summary>\r\n[AssetType(Name = \"Particle System\", Extension = \"fx\", Category = \"FX\", Flags = AssetTypeFlags.NoEmbedding)]\r\npublic class ParticleResource : GameResource\r\n{\r\n    public bool IsDirty { get; set; } = false;\r\n    \r\n    /// <summary>\r\n    /// All emitters in this particle system\r\n    /// </summary>\r\n    public List<ParticleEmitter> Emitters { get; set; } = new();\r\n\r\n    /// <summary>\r\n    /// Named float parameters\r\n    /// </summary>\r\n    [InlineEditor, DisplayName(\"FloatParameters\")] public List<FloatParameter> FloatParameters { get; set; } = new();\r\n    \r\n    /// <summary>\r\n    /// Named vector parameters\r\n    /// </summary>\r\n    [InlineEditor] public List<VectorParameter> VectorParameters { get; set; } = new();\r\n    \r\n    /// <summary>\r\n    /// Named color parameters\r\n    /// </summary>\r\n    [InlineEditor] public List<ColorParameter> ColorParameters { get; set; } = new();\r\n\r\n    /// <summary>\r\n    /// Global system properties\r\n    /// </summary>\r\n    public float Duration { get; set; } = 5.0f;\r\n    public bool Looping { get; set; } = true;\r\n\r\n    public int Version { get; set; } = 0;\r\n    \r\n    /// <summary>\r\n    /// Preview settings for the editor\r\n    /// </summary>\r\n    public ParticlePreviewSettings PreviewSettings { get; set; } = new();\r\n    \r\n    /// <summary>\r\n    /// Get a float parameter's default value by name\r\n    /// </summary>\r\n    public float GetParameterDefault(string name)\r\n    {\r\n        var param = FloatParameters.FirstOrDefault(p => p.Name == name);\r\n        return param?.DefaultValue ?? 0f;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Get a vector parameter's default value by name\r\n    /// </summary>\r\n    public Vector3 GetVectorParameterDefault(string name)\r\n    {\r\n        var param = VectorParameters.FirstOrDefault(p => p.Name == name);\r\n        return param?.DefaultValue ?? Vector3.Zero;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Get a color parameter's default value by name\r\n    /// </summary>\r\n    public ParticleGradient GetColorParameterDefault(string name)\r\n    {\r\n        var param = ColorParameters.FirstOrDefault(p => p.Name == name);\r\n        return param?.DefaultValue ?? Color.White;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Add a new float parameter\r\n    /// </summary>\r\n    public FloatParameter AddParameter(string name, float defaultValue = 1.0f)\r\n    {\r\n        var param = new FloatParameter\r\n        {\r\n            Name = name,\r\n            DefaultValue = defaultValue\r\n        };\r\n        FloatParameters.Add(param);\r\n        return param;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Add a new vector parameter\r\n    /// </summary>\r\n    public VectorParameter AddVectorParameter(string name, Vector3 defaultValue)\r\n    {\r\n        var param = new VectorParameter\r\n        {\r\n            Name = name,\r\n            DefaultValue = defaultValue\r\n        };\r\n        VectorParameters.Add(param);\r\n        return param;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Add a new color parameter\r\n    /// </summary>\r\n    public ColorParameter AddColorParameter(string name, Color defaultValue)\r\n    {\r\n        var param = new ColorParameter\r\n        {\r\n            Name = name,\r\n            DefaultValue = defaultValue\r\n        };\r\n        ColorParameters.Add(param);\r\n        return param;\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Preview settings for the particle editor\r\n/// </summary>\r\npublic class ParticlePreviewSettings\r\n{\r\n    public bool ShowGround { get; set; } = true;\r\n    public bool ShowGrid { get; set; } = true;\r\n    public Color BackgroundColor { get; set; } = new Color(0.1f, 0.1f, 0.15f);\r\n    public float PlaybackSpeed { get; set; } = 1.0f;\r\n}\r\n/// <summary>\r\n/// A single particle emitter with its own spawn and update logic\r\n/// </summary>\r\npublic class ParticleEmitter\r\n{\r\n\tpublic string Name { get; set; } = \"Emitter\";\r\n\t[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();\r\n\tpublic bool Enabled { get; set; } = true;\r\n\tpublic int MaxParticles { get; set; } = 1000;\r\n\r\n\t/// <summary>\r\n\t/// Seconds to wait before this emitter starts spawning - lets one emitter in the same\r\n\t/// system kick off a few seconds after another. Maps straight onto the native\r\n\t/// ParticleEffect.StartDelay, so it's handled by the engine's own particle system rather\r\n\t/// than anything FXBox has to gate itself.\r\n\t/// </summary>\r\n\tpublic float Delay { get; set; } = 0f;\r\n\r\n\t/// <summary>\r\n\t/// Overrides the system's own Duration for deciding when THIS emitter is finished\r\n\t/// (see FXBoxParticleController.IsFinished) - 0 means \"use the particle system's own\r\n\t/// Duration instead\", the same as before this existed. Lets one emitter in a system run\r\n\t/// longer or shorter than the rest without touching the system-wide Duration.\r\n\t/// </summary>\r\n\tpublic float Duration { get; set; } = 0f;\r\n\r\n\t/// <summary>\r\n\t/// Modules that run when spawning particles\r\n\t/// </summary>\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List<ParticleModule> SpawnModules { get; set; } = new();\r\n\r\n\t/// <summary>\r\n\t/// Modules that run once when a particle is created\r\n\t/// </summary>\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List<ParticleModule> InitializeModules { get; set; } = new();\r\n\r\n\t/// <summary>\r\n\t/// Modules that run every frame for each particle\r\n\t/// </summary>\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List<ParticleModule> UpdateModules { get; set; } = new();\r\n\r\n\t/// <summary>\r\n\t/// Modules that control how particles are rendered\r\n\t/// </summary>\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List<ParticleModule> RenderModules { get; set; } = new();\r\n}\r\n/// <summary>\r\n/// Base class for all particle modules\r\n/// </summary>\r\npublic abstract class ParticleModule\r\n{\r\n    [Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();\r\n    [Hide] public string Name { get; set; }\r\n    [Hide] public bool Enabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// What stage this module belongs to\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public abstract ModuleStage Stage { get; }\r\n\r\n    /// <summary>\r\n    /// Execute this module\r\n    /// </summary>\r\n    public abstract void Execute(ParticleExecutionContext context);\r\n\r\n    public abstract void Initialize( ParticleExecutionContext context );\r\n}\r\n\r\n/// <summary>\r\n/// JSON converter for List of ParticleModule\r\n/// </summary>\r\npublic class ParticleModuleListConverter : JsonConverter<List<ParticleModule>>\r\n{\r\n    public override List<ParticleModule> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\r\n    {\r\n        var list = new List<ParticleModule>();\r\n        \r\n        if (reader.TokenType != JsonTokenType.StartArray)\r\n            throw new JsonException(\"Expected start of array\");\r\n        \r\n        while (reader.Read())\r\n        {\r\n            if (reader.TokenType == JsonTokenType.EndArray)\r\n                break;\r\n                \r\n            using (var doc = JsonDocument.ParseValue(ref reader))\r\n            {\r\n                var root = doc.RootElement;\r\n                \r\n                // Get the type name\r\n                if (!root.TryGetProperty(\"$type\", out var typeProperty))\r\n                {\r\n                    Log.Warning(\"Missing $type property for ParticleModule\");\r\n                    continue;\r\n                }\r\n                \r\n                var typeName = typeProperty.GetString();\r\n                var type = TypeLibrary.GetType(typeName)?.TargetType;\r\n                \r\n                if (type == null)\r\n                {\r\n                    Log.Warning($\"Unknown module type: {typeName}\");\r\n                    continue;\r\n                }\r\n                \r\n                // Deserialize to the specific type\r\n                var json = root.GetRawText();\r\n                var module = (ParticleModule)JsonSerializer.Deserialize(json, type, options);\r\n                if (module != null)\r\n                {\r\n                    list.Add(module);\r\n                }\r\n            }\r\n        }\r\n        \r\n        return list;\r\n    }\r\n\r\n    public override void Write(Utf8JsonWriter writer, List<ParticleModule> value, JsonSerializerOptions options)\r\n    {\r\n        writer.WriteStartArray();\r\n        \r\n        foreach (var module in value)\r\n        {\r\n            if (module == null) continue;\r\n            \r\n            writer.WriteStartObject();\r\n            \r\n            // Write the type information\r\n            writer.WriteString(\"$type\", module.GetType().FullName);\r\n            \r\n            // Serialize the module\r\n            var json = JsonSerializer.Serialize(module, module.GetType(), options);\r\n            using (var doc = JsonDocument.Parse(json))\r\n            {\r\n                foreach (var property in doc.RootElement.EnumerateObject())\r\n                {\r\n                    property.WriteTo(writer);\r\n                }\r\n            }\r\n            \r\n            writer.WriteEndObject();\r\n        }\r\n        \r\n        writer.WriteEndArray();\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "Code/FxboxParticleSystem.cs",
            "FileName": "FxboxParticleSystem.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 379493,
            "Code": "using System;\r\nusing Sandbox;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Runtime.CompilerServices;\r\n\r\nnamespace fxbox;\r\n\r\n/// <summary>\r\n/// Component that integrates FXBox particle systems wit s&box native particle components\r\n/// </summary>\r\npublic class FXBoxNativeParticleSystem : Component, Component.ExecuteInEditor, ResourceLibrary.IEventListener, Component.ITemporaryEffect\r\n{\r\n    [Property] public ParticleResource ParticleSystem { get; set; }\r\n    \r\n    [Property, Hide] private List<GameObject> Emitters { get; set; } = new();\r\n    \r\n    [Property] public bool PlayOnStart { get; set; } = true;\r\n    [Property] public bool DestroyOnEnd { get; set; }\r\n\r\n    public Vector3 Velocity;\r\n    \r\n    /// <summary>\r\n    /// Instance-specific float parameter overrides\r\n    /// </summary>\r\n    [Property, Group(\"Parameters\"), Title(\"Float Parameters\")]\r\n    public Dictionary<string, float> ParameterOverrides { get; set; } = new();\r\n    \r\n    /// <summary>\r\n    /// Instance-specific vector parameter overrides\r\n    /// </summary>\r\n    [Property, Group(\"Parameters\"), Title(\"Vector Parameters\")]\r\n    public Dictionary<string, Vector3> VectorParameterOverrides { get; set; } = new();\r\n    \r\n    /// <summary>\r\n    /// Instance-specific color parameter overrides\r\n    /// </summary>\r\n    [Property, Group(\"Parameters\"), Title(\"Color Parameters\")]\r\n    public Dictionary<string, ParticleGradient> ColorParameterOverrides { get; set; } = new();\r\n    private int _version = 0;\r\n\r\n    private Vector3 _lastPosition;\r\n\r\n    protected override void OnFixedUpdate()\r\n    {\r\n\t    Velocity = (WorldPosition - _lastPosition);\r\n\t    _lastPosition = WorldPosition;\r\n        if (ParticleSystem != null && ParticleSystem.Version != _version)\r\n        {\r\n            _version = ParticleSystem.Version;\r\n            UpdateEmitters();\r\n        }\r\n\r\n        // Editor previews always loop once every emitter is done, regardless of the\r\n        // resource's own Looping setting - same convention TracerEffect uses (\"Editor\r\n        // previews loop instead\"). This used to happen by accident: SpawnBurstModule.Execute\r\n        // re-firing every tick meant a \"finished\" burst kept re-triggering itself forever in\r\n        // the editor. Now that it's fixed to only fire once (see SpawnBurstModule), a\r\n        // finished one-shot system just stays finished with nothing to restart it - so that\r\n        // restart needs to be explicit here instead. IsActive is only checked once ALL\r\n        // emitters are done (not per-emitter), matching \"loop the particles after all of\r\n        // them are done emitting\" - restarting one on its own the moment IT finishes would\r\n        // desync multiple emitters from each other on every subsequent loop.\r\n        if ( Scene.IsEditor && !IsActive )\r\n        {\r\n            RestartAllEmitters();\r\n        }\r\n    }\r\n\r\n    private void RestartAllEmitters()\r\n    {\r\n        foreach ( var emitter in Emitters )\r\n        {\r\n            if ( !emitter.IsValid() ) continue;\r\n\r\n            emitter.GetComponent<FXBoxParticleController>()?.ResetTimer();\r\n\r\n            // Same mechanism FXBoxNativeParticleSystem.Burst() already uses to manually\r\n            // re-fire an emitter - re-evaluates StartDelay/Burst/Rate from scratch.\r\n            foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>() )\r\n            {\r\n                particleEmitter.ResetEmitter();\r\n            }\r\n        }\r\n    }\r\n    \r\n    // ==================== FLOAT PARAMETERS ====================\r\n    \r\n    /// <summary>\r\n    /// Set a float parameter value for this specific instance\r\n    /// </summary>\r\n    public void Set(string name, float value)\r\n    {\r\n        if (ParticleSystem == null) return;\r\n        \r\n        var param = ParticleSystem.FloatParameters?.FirstOrDefault(p => p.Name == name);\r\n        if (param == null)\r\n        {\r\n            //Log.Warning($\"Float parameter '{name}' not found in particle system\");\r\n            return;\r\n        }\r\n        \r\n        ParameterOverrides[name] = value;\r\n        UpdateParameterValues();\r\n    }\r\n\r\n    public void Burst()\r\n    {\r\n\t    foreach ( var emitter in Emitters )\r\n\t    {\r\n\t\t    foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>(  ) )\r\n\t\t    {\r\n\t\t\t    var target = particleEmitter.GetComponent<ParticleEffect>();\r\n\t\t\t    particleEmitter.ResetEmitter();\r\n\t\t    }\r\n\t    }\r\n    }\r\n\r\n    public void Burst( Vector3 worldPosition )\r\n    {\r\n\t    foreach ( var emitter in Emitters )\r\n\t    {\r\n\t\t    foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>(  ) )\r\n\t\t    {\r\n\t\t\t    var target = particleEmitter.GetComponent<ParticleEffect>();\r\n\t\t\t    for(int i=0; i< particleEmitter.Burst.Evaluate( Time.Delta, Time.Delta ); i++)\r\n\t\t\t    {\r\n\t\t\t\t    particleEmitter.Emit( target );\r\n\t\t\t    }\r\n\t\t    }\r\n\t    }\r\n\t    \r\n\t    foreach ( var emitter in Emitters )\r\n\t    {\r\n\t\t    foreach ( var particleEffect in emitter.GetComponentsInChildren<Sandbox.ParticleEffect>(  ) )\r\n\t\t    {\r\n\t\t\t    foreach ( var particle in particleEffect.Particles )\r\n\t\t\t    {\r\n\t\t\t\t    if ( particle.Age <= 0.01f )\r\n\t\t\t\t    {\r\n\t\t\t\t\t\tparticle.Position += worldPosition - particleEffect.WorldTransform.Position;\r\n\t\t\t\t    }\r\n\r\n\t\t\t    }\r\n\t\t    }\r\n\t    }\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Get a float parameter value (override or default)\r\n    /// </summary>\r\n    public float GetFloatParameter(string name)\r\n    {\r\n        if (ParameterOverrides.TryGetValue(name, out float overrideValue))\r\n        {\r\n            return overrideValue;\r\n        }\r\n        \r\n        return ParticleSystem?.GetParameterDefault(name) ?? 0f;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Reset a float parameter to its default value\r\n    /// </summary>\r\n    public void ResetParameter(string name)\r\n    {\r\n        ParameterOverrides.Remove(name);\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    // ==================== VECTOR PARAMETERS ====================\r\n    \r\n    /// <summary>\r\n    /// Set a vector parameter value for this specific instance\r\n    /// </summary>\r\n    public void Set(string name, Vector3 value)\r\n    {\r\n        if (ParticleSystem == null) return;\r\n        \r\n        var param = ParticleSystem.VectorParameters?.FirstOrDefault(p => p.Name == name);\r\n        if (param == null)\r\n        {\r\n            //Log.Warning($\"Vector parameter '{name}' not found in particle system\");\r\n            return;\r\n        }\r\n        \r\n        VectorParameterOverrides[name] = value;\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    public int GetAliveParticles()\r\n    {\r\n\t    var particles = 0;\r\n\t    foreach ( var emitter in Emitters )\r\n\t    {\r\n\t\t    var target = emitter.GetComponent<ParticleEffect>();\r\n\t\t    particles += target.ParticleCount;\r\n\t    }\r\n\r\n\t    return particles;\r\n    }\r\n    /// <summary>\r\n    /// Get a vector parameter value (override or default)\r\n    /// </summary>\r\n    public Vector3 GetVectorParameter(string name)\r\n    {\r\n        if (VectorParameterOverrides.TryGetValue(name, out Vector3 overrideValue))\r\n        {\r\n            return overrideValue;\r\n        }\r\n        \r\n        return ParticleSystem?.GetVectorParameterDefault(name) ?? Vector3.Zero;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Reset a vector parameter to its default value\r\n    /// </summary>\r\n    public void ResetVectorParameter(string name)\r\n    {\r\n        VectorParameterOverrides.Remove(name);\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    // ==================== COLOR PARAMETERS ====================\r\n    \r\n    /// <summary>\r\n    /// Set a color parameter value for this specific instance\r\n    /// </summary>\r\n    public void Set(string name, Color value)\r\n    {\r\n        if (ParticleSystem == null) return;\r\n        \r\n        var param = ParticleSystem.ColorParameters?.FirstOrDefault(p => p.Name == name);\r\n        if (param == null)\r\n        {\r\n            //Log.Warning($\"Color parameter '{name}' not found in particle system\");\r\n            return;\r\n        }\r\n        \r\n        ColorParameterOverrides[name] = value;\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Get a color parameter value (override or default)\r\n    /// </summary>\r\n    public ParticleGradient GetColorParameter(string name)\r\n    {\r\n        if (ColorParameterOverrides.TryGetValue(name, out ParticleGradient overrideValue))\r\n        {\r\n            return overrideValue;\r\n        }\r\n        \r\n        return ParticleSystem?.GetColorParameterDefault(name) ?? Color.White;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Reset a color parameter to its default value\r\n    /// </summary>\r\n    public void ResetColorParameter(string name)\r\n    {\r\n        ColorParameterOverrides.Remove(name);\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    // ==================== GENERAL ====================\r\n    \r\n    /// <summary>\r\n    /// Reset all parameters to their default values\r\n    /// </summary>\r\n    public void ResetAllParameters()\r\n    {\r\n        ParameterOverrides.Clear();\r\n        VectorParameterOverrides.Clear();\r\n        ColorParameterOverrides.Clear();\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Helper buttons for the inspector\r\n    /// </summary>\r\n    [Button(\"Reset All Parameters\")]\r\n    [Group(\"Parameters\")]\r\n    public void ResetAllParametersButton()\r\n    {\r\n        ResetAllParameters();\r\n    }\r\n    \r\n    [Button(\"Initialize All Parameters\")]\r\n    [Group(\"Parameters\")]\r\n    [Description(\"Copy all parameters from the resource as overrides\")]\r\n    public void InitializeParametersFromResource()\r\n    {\r\n        if (ParticleSystem == null) return;\r\n        \r\n        ParameterOverrides.Clear();\r\n        if (ParticleSystem.FloatParameters != null)\r\n        {\r\n            foreach (var param in ParticleSystem.FloatParameters)\r\n            {\r\n                ParameterOverrides[param.Name] = param.DefaultValue;\r\n            }\r\n        }\r\n        \r\n        VectorParameterOverrides.Clear();\r\n        if (ParticleSystem.VectorParameters != null)\r\n        {\r\n            foreach (var param in ParticleSystem.VectorParameters)\r\n            {\r\n                VectorParameterOverrides[param.Name] = param.DefaultValue;\r\n            }\r\n        }\r\n        \r\n        ColorParameterOverrides.Clear();\r\n        if (ParticleSystem.ColorParameters != null)\r\n        {\r\n            foreach (var param in ParticleSystem.ColorParameters)\r\n            {\r\n                ColorParameterOverrides[param.Name] = param.DefaultValue;\r\n            }\r\n        }\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Update parameter values without rebuilding emitters\r\n    /// </summary>\r\n    private void UpdateParameterValues()\r\n    {\r\n        if (ParticleSystem?.Emitters == null) return;\r\n        \r\n        foreach (var emitterObject in Emitters)\r\n        {\r\n            if (!emitterObject.IsValid()) continue;\r\n            \r\n            var controller = emitterObject.GetComponent<FXBoxParticleController>();\r\n            if (controller != null)\r\n            {\r\n                UpdateModulesWithParameters(emitterObject, controller.EmitterData);\r\n            }\r\n        }\r\n    }\r\n    \r\n    public void OnSave(GameResource resource)\r\n    {\r\n        Log.Info(\"The scene has stopped\");\r\n    }\r\n    \r\n    protected override void OnStart()\r\n    {\r\n        UpdateEmitters();\r\n\r\n        // TemporaryEffect is what actually destroys this GameObject once IsActive (below)\r\n        // goes false - without one present, nothing ever would, since the per-emitter\r\n        // controllers no longer destroy the root themselves (see FXBoxParticleController.\r\n        // OnUpdate). DestroyAfterSeconds is 0 since the wait for \"actually finished\" is\r\n        // already handled by WaitForChildEffects walking into our own IsActive below, not by\r\n        // this timer - only DestroyOnEnd opts in at all, and editor previews never destroy\r\n        // themselves (they loop, same as TracerEffect's editor behavior).\r\n        if ( DestroyOnEnd && !Scene.IsEditor )\r\n        {\r\n            var temporaryEffect = GetOrAddComponent<TemporaryEffect>();\r\n            temporaryEffect.DestroyAfterSeconds = 0f;\r\n            temporaryEffect.WaitForChildEffects = true;\r\n        }\r\n    }\r\n\r\n    protected override void OnEnabled()\r\n    {\r\n        UpdateEmitters();\r\n        base.OnEnabled();\r\n    }\r\n\r\n    protected override void DrawGizmos()\r\n    {\r\n\t    Gizmo.Hitbox.Sprite( 0, 50, false );\r\n\t    if ( Gizmo.IsHovered  || Gizmo.IsSelected)\r\n\t    {\r\n\t\t    Gizmo.Draw.Color = Color.White;\r\n\t\t    if ( Gizmo.IsSelected )\r\n\t\t    {\r\n\t\t\t    Gizmo.Draw.Color = Color.Yellow;\r\n\t\t    }\r\n\t    }\r\n\t    else\r\n\t    {\r\n\t\t    Gizmo.Draw.Color = Color.Gray;\r\n\t    }\r\n\t   \r\n\t    Gizmo.Draw.Sprite( 0, 50, Texture.Load( \"images/particlehover.vtex\" ), false );  \r\n    }\r\n\r\n    public void UpdateEmitters()\r\n    {\r\n        // Clean up existing emitters\r\n        foreach (var emitter in Emitters)\r\n        {\r\n            emitter?.DestroyImmediate();\r\n        }\r\n        Emitters.Clear();\r\n\r\n        if (ParticleSystem?.Emitters == null) return;\r\n\r\n        // Create emitters from ParticleResource\r\n        foreach (var emitterData in ParticleSystem.Emitters)\r\n        {\r\n            if (!emitterData.Enabled) continue;\r\n\r\n            var emitterObject = new GameObject(GameObject);\r\n            emitterObject.Flags = emitterObject.Flags.WithFlag( GameObjectFlags.Hidden, true );\r\n            emitterObject.Name = emitterData.Name;\r\n            Emitters.Add(emitterObject);\r\n\r\n            // Add ParticleEffect component\r\n            var particleEffect = emitterObject.GetOrAddComponent<ParticleEffect>();\r\n            particleEffect.MaxParticles = emitterData.MaxParticles;\r\n            particleEffect.StartDelay = emitterData.Delay;\r\n\r\n            // Create native components from modules\r\n            CreateModuleComponents(emitterObject, emitterData);\r\n\r\n            // Add controller to handle particle updates\r\n            var controller = emitterObject.GetOrAddComponent<FXBoxParticleController>();\r\n            controller.EmitterData = emitterData;\r\n            controller.ParticleEffect = particleEffect;\r\n            controller.InitializeModules = emitterData.InitializeModules;\r\n            controller.ParticleSystemComponent = this;\r\n            controller.ParticleEffect.ResetEmitters();\r\n            \r\n        }\r\n    }\r\n\r\n    private void CreateModuleComponents(GameObject go, ParticleEmitter emitterData)\r\n    {\r\n        // Create components from all modules that implement IParticleComponentCreator\r\n        var allModules = emitterData.SpawnModules\r\n            .Concat(emitterData.InitializeModules)\r\n            .Concat(emitterData.UpdateModules)\r\n            .Concat(emitterData.RenderModules);\r\n\r\n        var particleModules = allModules.ToList();\r\n        foreach (var module in particleModules.OfType<IParticleComponentCreator>())\r\n        {\r\n            if (module is ParticleModule pm && pm.Enabled)\r\n            {\r\n                module.CreateComponent(go);\r\n            }\r\n        }\r\n\r\n        var context = new ParticleExecutionContext();\r\n        context.Effect = go.GetComponent<ParticleEffect>();\r\n        context.Emitter = go.GetComponent<Sandbox.ParticleEmitter>();\r\n        context.SystemComponent = this;\r\n\r\n        foreach (var module in particleModules)\r\n        {\r\n            module.Initialize(context);\r\n        }\r\n\r\n        // emitterData.Duration (0 by default) overrides the system-wide Duration for THIS\r\n        // emitter, same as FXBoxParticleController.EffectiveDuration - kept in sync so the\r\n        // native emitter's own duration behavior matches what our controller thinks it does.\r\n        var effectiveDuration = emitterData.Duration > 0f ? emitterData.Duration : ParticleSystem.Duration;\r\n\r\n        // Ensure we have at least a basic emitter if none was created\r\n        if (!go.GetComponent<Sandbox.ParticleEmitter>().IsValid())\r\n        {\r\n            var emitter = go.AddComponent<ParticleSphereEmitter>();\r\n            emitter.Duration = effectiveDuration;\r\n            emitter.Loop = ParticleSystem.Looping;\r\n            emitter.DestroyOnEnd = DestroyOnEnd;\r\n        }\r\n\r\n        var emitters = go.GetComponentsInChildren<Sandbox.ParticleEmitter>();\r\n        foreach ( var emit in emitters )\r\n        {\r\n\t        emit.Duration = effectiveDuration;\r\n\t        emit.Loop = ParticleSystem.Looping;\r\n\t        emit.DestroyOnEnd = DestroyOnEnd;\r\n        }\r\n    }\r\n    \r\n    private void UpdateModulesWithParameters(GameObject go, ParticleEmitter emitterData)\r\n    {\r\n        var context = new ParticleExecutionContext();\r\n        context.Effect = go.GetComponent<ParticleEffect>();\r\n        context.Emitter = go.GetComponent<Sandbox.ParticleEmitter>();\r\n        context.SystemComponent = this;\r\n\r\n        var allModules = emitterData.SpawnModules\r\n            .Concat(emitterData.InitializeModules)\r\n            .Concat(emitterData.UpdateModules)\r\n            .Concat(emitterData.RenderModules);\r\n\r\n        foreach (var module in allModules)\r\n        {\r\n            module.Initialize(context);\r\n        }\r\n    }\r\n    \r\n    protected override void OnDisabled()\r\n    {\r\n\t    // Clean up existing emitters\r\n\t    foreach (var emitter in Emitters)\r\n\t    {\r\n\t\t    emitter?.Destroy();\r\n\t    }\r\n\t    \r\n\t    Emitters.Clear();\r\n\t    base.OnDisabled();\r\n    }\r\n\r\n    // ITemporaryEffect.IsActive - computed live from the child emitters rather than a flag\r\n    // someone has to remember to flip, so it can never go stale relative to what's actually\r\n    // still emitting/alive. True as soon as ANY emitter hasn't finished yet (see\r\n    // FXBoxParticleController.IsFinished); false only once every one of them has.\r\n    public bool IsActive\r\n    {\r\n        get\r\n        {\r\n            foreach ( var emitter in Emitters )\r\n            {\r\n                if ( !emitter.IsValid() ) continue;\r\n\r\n                var controller = emitter.GetComponent<FXBoxParticleController>();\r\n                if ( controller.IsValid() && !controller.IsFinished )\r\n                    return true;\r\n            }\r\n\r\n            return false;\r\n        }\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Controller that executes particle update modules on each particle\r\n/// </summary>\r\npublic class FXBoxParticleController : ParticleController\r\n{\r\n    [Property, Hide] public ParticleEmitter EmitterData { get; set; }\r\n    [Property, Hide] public new ParticleEffect ParticleEffect { get; set; }\r\n    [Property, Hide] public FXBoxNativeParticleSystem ParticleSystemComponent { get; set; }\r\n    private TimeSince _timeSinceCreated = 0;\r\n    public List<ParticleModule> InitializeModules { get; set; } = new List<ParticleModule>();\r\n\r\n    // This emitter is done - past its duration, not looping, and nothing left alive.\r\n    // FXBoxNativeParticleSystem.IsActive (the ITemporaryEffect this whole system exposes)\r\n    // is true as long as ANY emitter's controller reports false here; a TemporaryEffect on\r\n    // the root is what actually destroys things once every one of them finally does, rather\r\n    // than this controller destroying the root itself the moment ITS OWN emitter finishes -\r\n    // that was the bug: a multi-emitter system got torn down as soon as the FIRST emitter\r\n    // to finish was done, not once every emitter actually was.\r\n    //\r\n    // EmitterData.Duration (0 by default) overrides the system-wide Duration for THIS\r\n    // emitter specifically - lets one emitter run longer/shorter than the rest of the\r\n    // system without changing anything system-wide.\r\n    private float EffectiveDuration => EmitterData != null && EmitterData.Duration > 0f\r\n\t    ? EmitterData.Duration\r\n\t    : (ParticleSystemComponent?.ParticleSystem?.Duration ?? 0f);\r\n\r\n    public bool IsFinished =>\r\n\t    _timeSinceCreated > EffectiveDuration\r\n\t    && !(ParticleSystemComponent?.ParticleSystem?.Looping ?? false)\r\n\t    && ParticleEffect.Particles.Count <= 0;\r\n\r\n    // Called by FXBoxNativeParticleSystem.RestartAllEmitters (editor-only looping of a\r\n    // non-looping system, once every emitter's finished) alongside the native\r\n    // ParticleEmitter.ResetEmitter() call - IsFinished depends on _timeSinceCreated, so\r\n    // without also resetting this, it would immediately re-evaluate as finished again next\r\n    // tick regardless of the native emitter actually having restarted.\r\n    public void ResetTimer() => _timeSinceCreated = 0;\r\n\r\n    protected override void OnUpdate()\r\n    {\r\n\t    var context = new ParticleExecutionContext\r\n\t    {\r\n\t\t    Particle = null,\r\n\t\t    Effect = ParticleEffect,\r\n\t\t    Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),\r\n\t\t    SystemComponent = ParticleSystemComponent\r\n\t    };\r\n\t    foreach ( var init in EmitterData.InitializeModules )\r\n\t    {\r\n\t\t    init.Execute( context );\r\n\t    }\r\n\t    foreach ( var spawn in EmitterData.SpawnModules )\r\n\t    {\r\n\t\t    spawn.Execute( context );\r\n\t    }\r\n    }\r\n\r\n    protected override void OnParticleStep(Particle particle, float delta)\r\n    {\r\n        base.OnParticleStep(particle, delta);\r\n\r\n        if (EmitterData == null) return;\r\n        \r\n        var context = new ParticleExecutionContext\r\n        {\r\n            Particle = particle,\r\n            Effect = ParticleEffect,\r\n            Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),\r\n            SystemComponent = ParticleSystemComponent\r\n        };\r\n        \r\n        // Execute all update modules that implement IParticleUpdater\r\n        foreach (var module in EmitterData.UpdateModules.OfType<IParticleUpdater>())\r\n        {\r\n            if (module is ParticleModule pm && pm.Enabled)\r\n            {\r\n                module.UpdateParticle(context, delta);\r\n            }\r\n        }\r\n    }\r\n    \r\n    protected override void OnParticleCreated(Particle p)\r\n    {\r\n        p.Position = ParticleEffect.WorldTransform.Position;\r\n        InitializeModules ??= EmitterData?.InitializeModules ?? new List<ParticleModule>();\r\n        \r\n        var context = new ParticleExecutionContext\r\n        {\r\n            Particle = p,\r\n            Effect = ParticleEffect,\r\n            Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),\r\n            SystemComponent = ParticleSystemComponent\r\n        };\r\n        \r\n        foreach (var module in InitializeModules)\r\n        {\r\n            module.Initialize(context);\r\n        }\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Interface for modules that can create native components\r\n/// </summary>\r\npublic interface IParticleComponentCreator\r\n{\r\n    void CreateComponent(GameObject go);\r\n}\r\n\r\n/// <summary>\r\n/// Interface for modules that update particles\r\n/// </summary>\r\npublic interface IParticleUpdater\r\n{\r\n    void UpdateParticle(ParticleExecutionContext particle, float delta);\r\n}\r\n\r\n[Flags]\r\npublic enum FXCopyFlags\r\n{\r\n\tRotation = 1,\r\n\tScale = 2,\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "FxboxParticleSystem.cs",
            "FileName": "FxboxParticleSystem.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 379493,
            "Code": "using System;\r\nusing Sandbox;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Runtime.CompilerServices;\r\n\r\nnamespace fxbox;\r\n\r\n/// <summary>\r\n/// Component that integrates FXBox particle systems wit s&box native particle components\r\n/// </summary>\r\npublic class FXBoxNativeParticleSystem : Component, Component.ExecuteInEditor, ResourceLibrary.IEventListener, Component.ITemporaryEffect\r\n{\r\n    [Property] public ParticleResource ParticleSystem { get; set; }\r\n    \r\n    [Property, Hide] private List<GameObject> Emitters { get; set; } = new();\r\n    \r\n    [Property] public bool PlayOnStart { get; set; } = true;\r\n    [Property] public bool DestroyOnEnd { get; set; }\r\n\r\n    public Vector3 Velocity;\r\n    \r\n    /// <summary>\r\n    /// Instance-specific float parameter overrides\r\n    /// </summary>\r\n    [Property, Group(\"Parameters\"), Title(\"Float Parameters\")]\r\n    public Dictionary<string, float> ParameterOverrides { get; set; } = new();\r\n    \r\n    /// <summary>\r\n    /// Instance-specific vector parameter overrides\r\n    /// </summary>\r\n    [Property, Group(\"Parameters\"), Title(\"Vector Parameters\")]\r\n    public Dictionary<string, Vector3> VectorParameterOverrides { get; set; } = new();\r\n    \r\n    /// <summary>\r\n    /// Instance-specific color parameter overrides\r\n    /// </summary>\r\n    [Property, Group(\"Parameters\"), Title(\"Color Parameters\")]\r\n    public Dictionary<string, ParticleGradient> ColorParameterOverrides { get; set; } = new();\r\n    private int _version = 0;\r\n\r\n    private Vector3 _lastPosition;\r\n\r\n    protected override void OnFixedUpdate()\r\n    {\r\n\t    Velocity = (WorldPosition - _lastPosition);\r\n\t    _lastPosition = WorldPosition;\r\n        if (ParticleSystem != null && ParticleSystem.Version != _version)\r\n        {\r\n            _version = ParticleSystem.Version;\r\n            UpdateEmitters();\r\n        }\r\n\r\n        // Editor previews always loop once every emitter is done, regardless of the\r\n        // resource's own Looping setting - same convention TracerEffect uses (\"Editor\r\n        // previews loop instead\"). This used to happen by accident: SpawnBurstModule.Execute\r\n        // re-firing every tick meant a \"finished\" burst kept re-triggering itself forever in\r\n        // the editor. Now that it's fixed to only fire once (see SpawnBurstModule), a\r\n        // finished one-shot system just stays finished with nothing to restart it - so that\r\n        // restart needs to be explicit here instead. IsActive is only checked once ALL\r\n        // emitters are done (not per-emitter), matching \"loop the particles after all of\r\n        // them are done emitting\" - restarting one on its own the moment IT finishes would\r\n        // desync multiple emitters from each other on every subsequent loop.\r\n        if ( Scene.IsEditor && !IsActive )\r\n        {\r\n            RestartAllEmitters();\r\n        }\r\n    }\r\n\r\n    private void RestartAllEmitters()\r\n    {\r\n        foreach ( var emitter in Emitters )\r\n        {\r\n            if ( !emitter.IsValid() ) continue;\r\n\r\n            emitter.GetComponent<FXBoxParticleController>()?.ResetTimer();\r\n\r\n            // Same mechanism FXBoxNativeParticleSystem.Burst() already uses to manually\r\n            // re-fire an emitter - re-evaluates StartDelay/Burst/Rate from scratch.\r\n            foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>() )\r\n            {\r\n                particleEmitter.ResetEmitter();\r\n            }\r\n        }\r\n    }\r\n    \r\n    // ==================== FLOAT PARAMETERS ====================\r\n    \r\n    /// <summary>\r\n    /// Set a float parameter value for this specific instance\r\n    /// </summary>\r\n    public void Set(string name, float value)\r\n    {\r\n        if (ParticleSystem == null) return;\r\n        \r\n        var param = ParticleSystem.FloatParameters?.FirstOrDefault(p => p.Name == name);\r\n        if (param == null)\r\n        {\r\n            //Log.Warning($\"Float parameter '{name}' not found in particle system\");\r\n            return;\r\n        }\r\n        \r\n        ParameterOverrides[name] = value;\r\n        UpdateParameterValues();\r\n    }\r\n\r\n    public void Burst()\r\n    {\r\n\t    foreach ( var emitter in Emitters )\r\n\t    {\r\n\t\t    foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>(  ) )\r\n\t\t    {\r\n\t\t\t    var target = particleEmitter.GetComponent<ParticleEffect>();\r\n\t\t\t    particleEmitter.ResetEmitter();\r\n\t\t    }\r\n\t    }\r\n    }\r\n\r\n    public void Burst( Vector3 worldPosition )\r\n    {\r\n\t    foreach ( var emitter in Emitters )\r\n\t    {\r\n\t\t    foreach ( var particleEmitter in emitter.GetComponentsInChildren<Sandbox.ParticleEmitter>(  ) )\r\n\t\t    {\r\n\t\t\t    var target = particleEmitter.GetComponent<ParticleEffect>();\r\n\t\t\t    for(int i=0; i< particleEmitter.Burst.Evaluate( Time.Delta, Time.Delta ); i++)\r\n\t\t\t    {\r\n\t\t\t\t    particleEmitter.Emit( target );\r\n\t\t\t    }\r\n\t\t    }\r\n\t    }\r\n\t    \r\n\t    foreach ( var emitter in Emitters )\r\n\t    {\r\n\t\t    foreach ( var particleEffect in emitter.GetComponentsInChildren<Sandbox.ParticleEffect>(  ) )\r\n\t\t    {\r\n\t\t\t    foreach ( var particle in particleEffect.Particles )\r\n\t\t\t    {\r\n\t\t\t\t    if ( particle.Age <= 0.01f )\r\n\t\t\t\t    {\r\n\t\t\t\t\t\tparticle.Position += worldPosition - particleEffect.WorldTransform.Position;\r\n\t\t\t\t    }\r\n\r\n\t\t\t    }\r\n\t\t    }\r\n\t    }\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Get a float parameter value (override or default)\r\n    /// </summary>\r\n    public float GetFloatParameter(string name)\r\n    {\r\n        if (ParameterOverrides.TryGetValue(name, out float overrideValue))\r\n        {\r\n            return overrideValue;\r\n        }\r\n        \r\n        return ParticleSystem?.GetParameterDefault(name) ?? 0f;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Reset a float parameter to its default value\r\n    /// </summary>\r\n    public void ResetParameter(string name)\r\n    {\r\n        ParameterOverrides.Remove(name);\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    // ==================== VECTOR PARAMETERS ====================\r\n    \r\n    /// <summary>\r\n    /// Set a vector parameter value for this specific instance\r\n    /// </summary>\r\n    public void Set(string name, Vector3 value)\r\n    {\r\n        if (ParticleSystem == null) return;\r\n        \r\n        var param = ParticleSystem.VectorParameters?.FirstOrDefault(p => p.Name == name);\r\n        if (param == null)\r\n        {\r\n            //Log.Warning($\"Vector parameter '{name}' not found in particle system\");\r\n            return;\r\n        }\r\n        \r\n        VectorParameterOverrides[name] = value;\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    public int GetAliveParticles()\r\n    {\r\n\t    var particles = 0;\r\n\t    foreach ( var emitter in Emitters )\r\n\t    {\r\n\t\t    var target = emitter.GetComponent<ParticleEffect>();\r\n\t\t    particles += target.ParticleCount;\r\n\t    }\r\n\r\n\t    return particles;\r\n    }\r\n    /// <summary>\r\n    /// Get a vector parameter value (override or default)\r\n    /// </summary>\r\n    public Vector3 GetVectorParameter(string name)\r\n    {\r\n        if (VectorParameterOverrides.TryGetValue(name, out Vector3 overrideValue))\r\n        {\r\n            return overrideValue;\r\n        }\r\n        \r\n        return ParticleSystem?.GetVectorParameterDefault(name) ?? Vector3.Zero;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Reset a vector parameter to its default value\r\n    /// </summary>\r\n    public void ResetVectorParameter(string name)\r\n    {\r\n        VectorParameterOverrides.Remove(name);\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    // ==================== COLOR PARAMETERS ====================\r\n    \r\n    /// <summary>\r\n    /// Set a color parameter value for this specific instance\r\n    /// </summary>\r\n    public void Set(string name, Color value)\r\n    {\r\n        if (ParticleSystem == null) return;\r\n        \r\n        var param = ParticleSystem.ColorParameters?.FirstOrDefault(p => p.Name == name);\r\n        if (param == null)\r\n        {\r\n            //Log.Warning($\"Color parameter '{name}' not found in particle system\");\r\n            return;\r\n        }\r\n        \r\n        ColorParameterOverrides[name] = value;\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Get a color parameter value (override or default)\r\n    /// </summary>\r\n    public ParticleGradient GetColorParameter(string name)\r\n    {\r\n        if (ColorParameterOverrides.TryGetValue(name, out ParticleGradient overrideValue))\r\n        {\r\n            return overrideValue;\r\n        }\r\n        \r\n        return ParticleSystem?.GetColorParameterDefault(name) ?? Color.White;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Reset a color parameter to its default value\r\n    /// </summary>\r\n    public void ResetColorParameter(string name)\r\n    {\r\n        ColorParameterOverrides.Remove(name);\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    // ==================== GENERAL ====================\r\n    \r\n    /// <summary>\r\n    /// Reset all parameters to their default values\r\n    /// </summary>\r\n    public void ResetAllParameters()\r\n    {\r\n        ParameterOverrides.Clear();\r\n        VectorParameterOverrides.Clear();\r\n        ColorParameterOverrides.Clear();\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Helper buttons for the inspector\r\n    /// </summary>\r\n    [Button(\"Reset All Parameters\")]\r\n    [Group(\"Parameters\")]\r\n    public void ResetAllParametersButton()\r\n    {\r\n        ResetAllParameters();\r\n    }\r\n    \r\n    [Button(\"Initialize All Parameters\")]\r\n    [Group(\"Parameters\")]\r\n    [Description(\"Copy all parameters from the resource as overrides\")]\r\n    public void InitializeParametersFromResource()\r\n    {\r\n        if (ParticleSystem == null) return;\r\n        \r\n        ParameterOverrides.Clear();\r\n        if (ParticleSystem.FloatParameters != null)\r\n        {\r\n            foreach (var param in ParticleSystem.FloatParameters)\r\n            {\r\n                ParameterOverrides[param.Name] = param.DefaultValue;\r\n            }\r\n        }\r\n        \r\n        VectorParameterOverrides.Clear();\r\n        if (ParticleSystem.VectorParameters != null)\r\n        {\r\n            foreach (var param in ParticleSystem.VectorParameters)\r\n            {\r\n                VectorParameterOverrides[param.Name] = param.DefaultValue;\r\n            }\r\n        }\r\n        \r\n        ColorParameterOverrides.Clear();\r\n        if (ParticleSystem.ColorParameters != null)\r\n        {\r\n            foreach (var param in ParticleSystem.ColorParameters)\r\n            {\r\n                ColorParameterOverrides[param.Name] = param.DefaultValue;\r\n            }\r\n        }\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Update parameter values without rebuilding emitters\r\n    /// </summary>\r\n    private void UpdateParameterValues()\r\n    {\r\n        if (ParticleSystem?.Emitters == null) return;\r\n        \r\n        foreach (var emitterObject in Emitters)\r\n        {\r\n            if (!emitterObject.IsValid()) continue;\r\n            \r\n            var controller = emitterObject.GetComponent<FXBoxParticleController>();\r\n            if (controller != null)\r\n            {\r\n                UpdateModulesWithParameters(emitterObject, controller.EmitterData);\r\n            }\r\n        }\r\n    }\r\n    \r\n    public void OnSave(GameResource resource)\r\n    {\r\n        Log.Info(\"The scene has stopped\");\r\n    }\r\n    \r\n    protected override void OnStart()\r\n    {\r\n        UpdateEmitters();\r\n\r\n        // TemporaryEffect is what actually destroys this GameObject once IsActive (below)\r\n        // goes false - without one present, nothing ever would, since the per-emitter\r\n        // controllers no longer destroy the root themselves (see FXBoxParticleController.\r\n        // OnUpdate). DestroyAfterSeconds is 0 since the wait for \"actually finished\" is\r\n        // already handled by WaitForChildEffects walking into our own IsActive below, not by\r\n        // this timer - only DestroyOnEnd opts in at all, and editor previews never destroy\r\n        // themselves (they loop, same as TracerEffect's editor behavior).\r\n        if ( DestroyOnEnd && !Scene.IsEditor )\r\n        {\r\n            var temporaryEffect = GetOrAddComponent<TemporaryEffect>();\r\n            temporaryEffect.DestroyAfterSeconds = 0f;\r\n            temporaryEffect.WaitForChildEffects = true;\r\n        }\r\n    }\r\n\r\n    protected override void OnEnabled()\r\n    {\r\n        UpdateEmitters();\r\n        base.OnEnabled();\r\n    }\r\n\r\n    protected override void DrawGizmos()\r\n    {\r\n\t    Gizmo.Hitbox.Sprite( 0, 50, false );\r\n\t    if ( Gizmo.IsHovered  || Gizmo.IsSelected)\r\n\t    {\r\n\t\t    Gizmo.Draw.Color = Color.White;\r\n\t\t    if ( Gizmo.IsSelected )\r\n\t\t    {\r\n\t\t\t    Gizmo.Draw.Color = Color.Yellow;\r\n\t\t    }\r\n\t    }\r\n\t    else\r\n\t    {\r\n\t\t    Gizmo.Draw.Color = Color.Gray;\r\n\t    }\r\n\t   \r\n\t    Gizmo.Draw.Sprite( 0, 50, Texture.Load( \"images/particlehover.vtex\" ), false );  \r\n    }\r\n\r\n    public void UpdateEmitters()\r\n    {\r\n        // Clean up existing emitters\r\n        foreach (var emitter in Emitters)\r\n        {\r\n            emitter?.DestroyImmediate();\r\n        }\r\n        Emitters.Clear();\r\n\r\n        if (ParticleSystem?.Emitters == null) return;\r\n\r\n        // Create emitters from ParticleResource\r\n        foreach (var emitterData in ParticleSystem.Emitters)\r\n        {\r\n            if (!emitterData.Enabled) continue;\r\n\r\n            var emitterObject = new GameObject(GameObject);\r\n            emitterObject.Flags = emitterObject.Flags.WithFlag( GameObjectFlags.Hidden, true );\r\n            emitterObject.Name = emitterData.Name;\r\n            Emitters.Add(emitterObject);\r\n\r\n            // Add ParticleEffect component\r\n            var particleEffect = emitterObject.GetOrAddComponent<ParticleEffect>();\r\n            particleEffect.MaxParticles = emitterData.MaxParticles;\r\n            particleEffect.StartDelay = emitterData.Delay;\r\n\r\n            // Create native components from modules\r\n            CreateModuleComponents(emitterObject, emitterData);\r\n\r\n            // Add controller to handle particle updates\r\n            var controller = emitterObject.GetOrAddComponent<FXBoxParticleController>();\r\n            controller.EmitterData = emitterData;\r\n            controller.ParticleEffect = particleEffect;\r\n            controller.InitializeModules = emitterData.InitializeModules;\r\n            controller.ParticleSystemComponent = this;\r\n            controller.ParticleEffect.ResetEmitters();\r\n            \r\n        }\r\n    }\r\n\r\n    private void CreateModuleComponents(GameObject go, ParticleEmitter emitterData)\r\n    {\r\n        // Create components from all modules that implement IParticleComponentCreator\r\n        var allModules = emitterData.SpawnModules\r\n            .Concat(emitterData.InitializeModules)\r\n            .Concat(emitterData.UpdateModules)\r\n            .Concat(emitterData.RenderModules);\r\n\r\n        var particleModules = allModules.ToList();\r\n        foreach (var module in particleModules.OfType<IParticleComponentCreator>())\r\n        {\r\n            if (module is ParticleModule pm && pm.Enabled)\r\n            {\r\n                module.CreateComponent(go);\r\n            }\r\n        }\r\n\r\n        var context = new ParticleExecutionContext();\r\n        context.Effect = go.GetComponent<ParticleEffect>();\r\n        context.Emitter = go.GetComponent<Sandbox.ParticleEmitter>();\r\n        context.SystemComponent = this;\r\n\r\n        foreach (var module in particleModules)\r\n        {\r\n            module.Initialize(context);\r\n        }\r\n\r\n        // emitterData.Duration (0 by default) overrides the system-wide Duration for THIS\r\n        // emitter, same as FXBoxParticleController.EffectiveDuration - kept in sync so the\r\n        // native emitter's own duration behavior matches what our controller thinks it does.\r\n        var effectiveDuration = emitterData.Duration > 0f ? emitterData.Duration : ParticleSystem.Duration;\r\n\r\n        // Ensure we have at least a basic emitter if none was created\r\n        if (!go.GetComponent<Sandbox.ParticleEmitter>().IsValid())\r\n        {\r\n            var emitter = go.AddComponent<ParticleSphereEmitter>();\r\n            emitter.Duration = effectiveDuration;\r\n            emitter.Loop = ParticleSystem.Looping;\r\n            emitter.DestroyOnEnd = DestroyOnEnd;\r\n        }\r\n\r\n        var emitters = go.GetComponentsInChildren<Sandbox.ParticleEmitter>();\r\n        foreach ( var emit in emitters )\r\n        {\r\n\t        emit.Duration = effectiveDuration;\r\n\t        emit.Loop = ParticleSystem.Looping;\r\n\t        emit.DestroyOnEnd = DestroyOnEnd;\r\n        }\r\n    }\r\n    \r\n    private void UpdateModulesWithParameters(GameObject go, ParticleEmitter emitterData)\r\n    {\r\n        var context = new ParticleExecutionContext();\r\n        context.Effect = go.GetComponent<ParticleEffect>();\r\n        context.Emitter = go.GetComponent<Sandbox.ParticleEmitter>();\r\n        context.SystemComponent = this;\r\n\r\n        var allModules = emitterData.SpawnModules\r\n            .Concat(emitterData.InitializeModules)\r\n            .Concat(emitterData.UpdateModules)\r\n            .Concat(emitterData.RenderModules);\r\n\r\n        foreach (var module in allModules)\r\n        {\r\n            module.Initialize(context);\r\n        }\r\n    }\r\n    \r\n    protected override void OnDisabled()\r\n    {\r\n\t    // Clean up existing emitters\r\n\t    foreach (var emitter in Emitters)\r\n\t    {\r\n\t\t    emitter?.Destroy();\r\n\t    }\r\n\t    \r\n\t    Emitters.Clear();\r\n\t    base.OnDisabled();\r\n    }\r\n\r\n    // ITemporaryEffect.IsActive - computed live from the child emitters rather than a flag\r\n    // someone has to remember to flip, so it can never go stale relative to what's actually\r\n    // still emitting/alive. True as soon as ANY emitter hasn't finished yet (see\r\n    // FXBoxParticleController.IsFinished); false only once every one of them has.\r\n    public bool IsActive\r\n    {\r\n        get\r\n        {\r\n            foreach ( var emitter in Emitters )\r\n            {\r\n                if ( !emitter.IsValid() ) continue;\r\n\r\n                var controller = emitter.GetComponent<FXBoxParticleController>();\r\n                if ( controller.IsValid() && !controller.IsFinished )\r\n                    return true;\r\n            }\r\n\r\n            return false;\r\n        }\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Controller that executes particle update modules on each particle\r\n/// </summary>\r\npublic class FXBoxParticleController : ParticleController\r\n{\r\n    [Property, Hide] public ParticleEmitter EmitterData { get; set; }\r\n    [Property, Hide] public new ParticleEffect ParticleEffect { get; set; }\r\n    [Property, Hide] public FXBoxNativeParticleSystem ParticleSystemComponent { get; set; }\r\n    private TimeSince _timeSinceCreated = 0;\r\n    public List<ParticleModule> InitializeModules { get; set; } = new List<ParticleModule>();\r\n\r\n    // This emitter is done - past its duration, not looping, and nothing left alive.\r\n    // FXBoxNativeParticleSystem.IsActive (the ITemporaryEffect this whole system exposes)\r\n    // is true as long as ANY emitter's controller reports false here; a TemporaryEffect on\r\n    // the root is what actually destroys things once every one of them finally does, rather\r\n    // than this controller destroying the root itself the moment ITS OWN emitter finishes -\r\n    // that was the bug: a multi-emitter system got torn down as soon as the FIRST emitter\r\n    // to finish was done, not once every emitter actually was.\r\n    //\r\n    // EmitterData.Duration (0 by default) overrides the system-wide Duration for THIS\r\n    // emitter specifically - lets one emitter run longer/shorter than the rest of the\r\n    // system without changing anything system-wide.\r\n    private float EffectiveDuration => EmitterData != null && EmitterData.Duration > 0f\r\n\t    ? EmitterData.Duration\r\n\t    : (ParticleSystemComponent?.ParticleSystem?.Duration ?? 0f);\r\n\r\n    public bool IsFinished =>\r\n\t    _timeSinceCreated > EffectiveDuration\r\n\t    && !(ParticleSystemComponent?.ParticleSystem?.Looping ?? false)\r\n\t    && ParticleEffect.Particles.Count <= 0;\r\n\r\n    // Called by FXBoxNativeParticleSystem.RestartAllEmitters (editor-only looping of a\r\n    // non-looping system, once every emitter's finished) alongside the native\r\n    // ParticleEmitter.ResetEmitter() call - IsFinished depends on _timeSinceCreated, so\r\n    // without also resetting this, it would immediately re-evaluate as finished again next\r\n    // tick regardless of the native emitter actually having restarted.\r\n    public void ResetTimer() => _timeSinceCreated = 0;\r\n\r\n    protected override void OnUpdate()\r\n    {\r\n\t    var context = new ParticleExecutionContext\r\n\t    {\r\n\t\t    Particle = null,\r\n\t\t    Effect = ParticleEffect,\r\n\t\t    Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),\r\n\t\t    SystemComponent = ParticleSystemComponent\r\n\t    };\r\n\t    foreach ( var init in EmitterData.InitializeModules )\r\n\t    {\r\n\t\t    init.Execute( context );\r\n\t    }\r\n\t    foreach ( var spawn in EmitterData.SpawnModules )\r\n\t    {\r\n\t\t    spawn.Execute( context );\r\n\t    }\r\n    }\r\n\r\n    protected override void OnParticleStep(Particle particle, float delta)\r\n    {\r\n        base.OnParticleStep(particle, delta);\r\n\r\n        if (EmitterData == null) return;\r\n        \r\n        var context = new ParticleExecutionContext\r\n        {\r\n            Particle = particle,\r\n            Effect = ParticleEffect,\r\n            Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),\r\n            SystemComponent = ParticleSystemComponent\r\n        };\r\n        \r\n        // Execute all update modules that implement IParticleUpdater\r\n        foreach (var module in EmitterData.UpdateModules.OfType<IParticleUpdater>())\r\n        {\r\n            if (module is ParticleModule pm && pm.Enabled)\r\n            {\r\n                module.UpdateParticle(context, delta);\r\n            }\r\n        }\r\n    }\r\n    \r\n    protected override void OnParticleCreated(Particle p)\r\n    {\r\n        p.Position = ParticleEffect.WorldTransform.Position;\r\n        InitializeModules ??= EmitterData?.InitializeModules ?? new List<ParticleModule>();\r\n        \r\n        var context = new ParticleExecutionContext\r\n        {\r\n            Particle = p,\r\n            Effect = ParticleEffect,\r\n            Emitter = ParticleEffect.GetComponent<Sandbox.ParticleEmitter>(),\r\n            SystemComponent = ParticleSystemComponent\r\n        };\r\n        \r\n        foreach (var module in InitializeModules)\r\n        {\r\n            module.Initialize(context);\r\n        }\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Interface for modules that can create native components\r\n/// </summary>\r\npublic interface IParticleComponentCreator\r\n{\r\n    void CreateComponent(GameObject go);\r\n}\r\n\r\n/// <summary>\r\n/// Interface for modules that update particles\r\n/// </summary>\r\npublic interface IParticleUpdater\r\n{\r\n    void UpdateParticle(ParticleExecutionContext particle, float delta);\r\n}\r\n\r\n[Flags]\r\npublic enum FXCopyFlags\r\n{\r\n\tRotation = 1,\r\n\tScale = 2,\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "ParticleValues.cs",
            "FileName": "ParticleValues.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 379493,
            "Code": "using System;\r\nusing Sandbox;\r\nusing System.Linq;\r\n\r\nnamespace fxbox;\r\npublic class FXParticleFloat\r\n{\r\n    [Property] public bool UseParameter { get; set; } = false;\r\n    \r\n    [Property, ShowIf(nameof(UseParameter), false)]\r\n    public ParticleFloat Value { get; set; }\r\n    \r\n    [Property, ShowIf(nameof(UseParameter), true)]\r\n    [Description(\"Select a parameter from the system\")]\r\n    public string ParameterName { get; set; }\r\n    \r\n    [Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]\r\n    [Description(\"Multiplier applied to the parameter value\")]\r\n    public float Multiplier { get; set; } = 1.0f;\r\n    \r\n    public float GetValue(FXBoxNativeParticleSystem systemComponent = null)\r\n    {\r\n\t    if (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)\r\n\t    {\r\n\t\t    return systemComponent.GetFloatParameter(ParameterName) * Multiplier;\r\n\t    }\r\n\t    \r\n\t    var result = Value.Evaluate(Random.Shared.Float(), 3f);\r\n\t  \r\n\t    return result;\r\n    }\r\n    \r\n    public ParticleFloat ToParticleFloat(FXBoxNativeParticleSystem systemComponent = null)\r\n    {\r\n        if (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)\r\n        {\r\n            // Get the instance-specific value (override or default)\r\n            float value = systemComponent.GetFloatParameter(ParameterName) * Multiplier;\r\n            \r\n            return new ParticleFloat()\r\n            {\r\n                Type = ParticleFloat.ValueType.Constant,\r\n                ConstantValue = value,\r\n                Evaluation = ParticleFloat.EvaluationType.Seed\r\n            };\r\n        }\r\n        \r\n        return Value;\r\n    }\r\n    \r\n    // ==================== OPERATORS ====================\r\n    \r\n    \r\n    public static FXParticleFloat operator *(float a, FXParticleFloat b)\r\n    {\r\n        return b * a; // Commutative\r\n    }\r\n    \r\n    // Division operators\r\n    public static FXParticleFloat operator /(FXParticleFloat a, float b)\r\n{\r\n    if (a == null)\r\n    {\r\n        Log.Warning(\"Division: a is null\");\r\n        return null;\r\n    }\r\n    \r\n    if (b == 0 || MathF.Abs(b) < 0.0001f)\r\n    {\r\n        Log.Warning($\"Division by zero or very small number ({b}) in FXParticleFloat\");\r\n        return a;\r\n    }\r\n    \r\n  \r\n    var result = a * (1.0f / b);\r\n   \r\n    return result;\r\n}\r\n\r\npublic static FXParticleFloat operator *(FXParticleFloat a, float b)\r\n{\r\n    if (a == null)\r\n    {\r\n        Log.Warning(\"Multiplication: a is null\");\r\n        return null;\r\n    }\r\n    \r\n    var result = new FXParticleFloat();\r\n    \r\n    if (a.UseParameter)\r\n    {\r\n        result.UseParameter = true;\r\n        result.ParameterName = a.ParameterName;\r\n        result.Multiplier = a.Multiplier * b;\r\n        result.Value = new ParticleFloat()\r\n        {\r\n            Type = ParticleFloat.ValueType.Constant,\r\n            ConstantValue = 0f,\r\n            Evaluation = ParticleFloat.EvaluationType.Seed\r\n        };\r\n    }\r\n    else\r\n    {\r\n        result.UseParameter = false;\r\n        result.Value = ScaleParticleFloat(a.Value, b);\r\n    }\r\n    \r\n    return result;\r\n}\r\n\r\nprivate static ParticleFloat ScaleParticleFloat(ParticleFloat pf, float scale)\r\n{\r\n    \r\n    var result = new ParticleFloat();\r\n    result.Type = pf.Type;\r\n    result.Evaluation = pf.Evaluation;\r\n    \r\n    // Copy Constants FIRST, before setting individual values\r\n    // (or don't copy it at all since we're setting the values manually)\r\n    // result.Constants = pf.Constants;\r\n    \r\n    switch (pf.Type)\r\n    {\r\n        case ParticleFloat.ValueType.Constant:\r\n            result.ConstantValue = pf.ConstantValue * scale;\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.Range:\r\n            result.ConstantA = pf.ConstantA * scale;\r\n            result.ConstantB = pf.ConstantB * scale;\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.Curve:\r\n            result.CurveA = ScaleCurve(pf.CurveA, scale);\r\n            result.CurveB = ScaleCurve(pf.CurveB, scale);\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.CurveRange:\r\n            result.CurveRange = ScaleCurveRange(pf.CurveRange, scale);\r\n            break;\r\n    }\r\n    \r\n    // DON'T copy Constants here - it overwrites our values!\r\n    // result.Constants = pf.Constants;\r\n    \r\n    return result;\r\n}\r\n\r\nprivate static ParticleFloat OffsetParticleFloat(ParticleFloat pf, float offset)\r\n{\r\n    var result = new ParticleFloat();\r\n    result.Type = pf.Type;\r\n    result.Evaluation = pf.Evaluation;\r\n    \r\n    // DON'T copy Constants - it will overwrite our values\r\n    // result.Constants = pf.Constants;\r\n    \r\n    switch (pf.Type)\r\n    {\r\n        case ParticleFloat.ValueType.Constant:\r\n            result.ConstantValue = pf.ConstantValue + offset;\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.Range:\r\n            result.ConstantA = pf.ConstantA + offset;\r\n            result.ConstantB = pf.ConstantB + offset;\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.Curve:\r\n            result.CurveA = OffsetCurve(pf.CurveA, offset);\r\n            result.CurveB = OffsetCurve(pf.CurveB, offset);\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.CurveRange:\r\n            result.CurveRange = OffsetCurveRange(pf.CurveRange, offset);\r\n            break;\r\n    }\r\n    \r\n    // DON'T copy Constants here!\r\n    // result.Constants = pf.Constants;\r\n    \r\n    return result;\r\n}\r\n    \r\n    private static Curve ScaleCurve(Curve curve, float scale)\r\n    {\r\n        var newFrames = curve.Frames.Select(frame => \r\n            new Curve.Frame(frame.Time, frame.Value * scale)).ToArray();\r\n        \r\n        return new Curve(newFrames);\r\n    }\r\n    \r\n    private static Curve OffsetCurve(Curve curve, float offset)\r\n    {\r\n        var newFrames = curve.Frames.Select(frame => \r\n            new Curve.Frame(frame.Time, frame.Value + offset)).ToArray();\r\n        \r\n        return new Curve(newFrames);\r\n    }\r\n    \r\n    private static CurveRange ScaleCurveRange(CurveRange range, float scale)\r\n    {\r\n        return new CurveRange\r\n        (\r\n           ScaleCurve(range.A, scale),\r\n            ScaleCurve(range.B, scale)\r\n        );\r\n    }\r\n    \r\n    private static CurveRange OffsetCurveRange(CurveRange range, float offset)\r\n    {\r\n        return new CurveRange\r\n        (\r\n           OffsetCurve(range.A, offset),\r\n            OffsetCurve(range.B, offset)\r\n        );\r\n    }\r\n    \r\n    // ==================== IMPLICIT CONVERSIONS ====================\r\n    \r\n    public static implicit operator FXParticleFloat(float v)\r\n    {\r\n        var fxParticle = new FXParticleFloat();\r\n        fxParticle.Value = new ParticleFloat()\r\n        {\r\n            Type = ParticleFloat.ValueType.Constant,\r\n            ConstantValue = v,\r\n            Evaluation = ParticleFloat.EvaluationType.Seed\r\n        };\r\n        return fxParticle;\r\n    }\r\n\r\n    // ==================== CONSTRUCTORS ====================\r\n    \r\n    public FXParticleFloat()\r\n    {\r\n        var particleFloat = new ParticleFloat();\r\n        particleFloat.Type = ParticleFloat.ValueType.Constant;\r\n        particleFloat.ConstantValue = 0.0f;\r\n        particleFloat.Evaluation = ParticleFloat.EvaluationType.Seed;\r\n        particleFloat.CurveA = new Curve();\r\n        particleFloat.CurveB = new Curve();\r\n        particleFloat.Constants = new Vector4();\r\n        this.Value = particleFloat;\r\n    }\r\n\r\n    public FXParticleFloat(float a, float b)\r\n    {\r\n        var particleFloat = new ParticleFloat();\r\n        particleFloat.Type = ParticleFloat.ValueType.Range;\r\n        particleFloat.ConstantA = a;\r\n        particleFloat.ConstantB = b;\r\n        particleFloat.Evaluation = ParticleFloat.EvaluationType.Seed;\r\n        particleFloat.CurveA = new Curve();\r\n        particleFloat.CurveB = new Curve();\r\n        particleFloat.Constants = new Vector4();\r\n        this.Value = particleFloat;\r\n    }\r\n}\r\n\r\npublic class FXParticleVector\r\n{\r\n\t[Property] public bool UseParameter { get; set; } = false;\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), false)]\r\n\tpublic ParticleVector3 Value { get; set; } = Vector3.Zero;\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), true)]\r\n\t[Description(\"Select a vector parameter from the system\")]\r\n\tpublic string ParameterName { get; set; }\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]\r\n\t[Description(\"Multiplier applied to the parameter value\")]\r\n\tpublic float Multiplier { get; set; } = 1.0f;\r\n    \r\n\tpublic Vector3 GetValue(Particle particle,FXBoxNativeParticleSystem systemComponent = null)\r\n\t{\r\n\t\tif (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)\r\n\t\t{\r\n\t\t\treturn systemComponent.GetVectorParameter(ParameterName) * Multiplier;\r\n\t\t}\r\n\r\n\t\tif ( particle == null )\r\n\t\t{\r\n\t\t\treturn Value.Evaluate( Time.Delta, 0, 0, 0 );\r\n\t\t}\r\n\t\treturn Value.Evaluate( Time.Delta,particle.Rand(1),particle.Rand(2),particle.Rand(3) );\r\n\t}\r\n    \r\n\tpublic static implicit operator FXParticleVector(Vector3 v)\r\n\t{\r\n\t\treturn new FXParticleVector { Value = v };\r\n\t}\r\n\r\n\tpublic FXParticleVector()\r\n\t{\r\n\t\tValue = Vector3.Zero;\r\n\t}\r\n\r\n\tpublic FXParticleVector(Vector3 value)\r\n\t{\r\n\t\tValue = value;\r\n\t}\r\n    \r\n\tpublic FXParticleVector(float x, float y, float z)\r\n\t{\r\n\t\tValue = new Vector3(x, y, z);\r\n\t}\r\n}\r\n\r\npublic class FXParticleColor\r\n{\r\n\t[Property] public bool UseParameter { get; set; } = false;\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), false)]\r\n\tpublic ParticleGradient Value { get; set; } = Color.White;\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), true)]\r\n\t[Description(\"Select a color parameter from the system\")]\r\n\tpublic string ParameterName { get; set; }\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]\r\n\t[Description(\"Multiplier applied to the parameter value (affects RGB)\")]\r\n\tpublic float Multiplier { get; set; } = 1.0f;\r\n    \r\n\tpublic ParticleGradient GetValue(FXBoxNativeParticleSystem systemComponent = null)\r\n\t{\r\n\t\tif (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)\r\n\t\t{\r\n\t\t\tvar color = systemComponent.GetColorParameter(ParameterName);\r\n            \r\n\t\t\t// Apply multiplier to RGB components\r\n\t\t\tif (Multiplier != 1.0f)\r\n\t\t\t{\r\n\t\t\t\treturn color;\r\n\t\t\t}\r\n            \r\n\t\t\treturn color;\r\n\t\t}\r\n        \r\n\t\treturn Value;\r\n\t}\r\n    \r\n\tpublic static implicit operator FXParticleColor(Color c)\r\n\t{\r\n\t\treturn new FXParticleColor { Value = c };\r\n\t}\r\n\r\n\tpublic FXParticleColor()\r\n\t{\r\n\t\tValue = Color.White;\r\n\t}\r\n\r\n\tpublic FXParticleColor(Color value)\r\n\t{\r\n\t\tValue = value;\r\n\t}\r\n    \r\n\tpublic FXParticleColor(float r, float g, float b, float a = 1.0f)\r\n\t{\r\n\t\tValue = new Color(r, g, b, a);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "Code/FXParameter.cs",
            "FileName": "FXParameter.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 379493,
            "Code": "using System;\r\nusing Sandbox;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\nnamespace fxbox;\r\n\r\n/// <summary>\r\n/// A named float parameter that can be used to control particle values\r\n/// </summary>\r\npublic class FloatParameter\r\n{\r\n\t[Property] public string Name { get; set; } = \"Parameter\";\r\n\t[Property] public float DefaultValue { get; set; } = 1.0f;\r\n\t[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();\r\n}\r\n\r\n/// <summary>\r\n/// A named vector parameter that can be used to control particle values\r\n/// </summary>\r\npublic class VectorParameter\r\n{\r\n\t[Property] public string Name { get; set; } = \"VectorParameter\";\r\n\t[Property] public Vector3 DefaultValue { get; set; } = Vector3.One;\r\n\t[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();\r\n}\r\n\r\n/// <summary>\r\n/// A named color parameter that can be used to control particle values\r\n/// </summary>\r\npublic class ColorParameter\r\n{\r\n\t[Property] public string Name { get; set; } = \"ColorParameter\";\r\n\t[Property] public ParticleGradient DefaultValue { get; set; } = Color.White;\r\n\t[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "Editor/Graph/ParticlePreview.cs",
            "FileName": "ParticlePreview.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 379493,
            "Code": "using System;\r\nusing Editor;\r\nusing Sandbox;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\nnamespace fxbox.Graph;\r\n\r\n/// <summary>\r\n/// Widget that renders a real-time preview of the particle system using native components\r\n/// </summary>\r\npublic class ParticlePreview : SceneRenderingWidget\r\n{\r\n    private ParticleResource _resource;\r\n    private GameObject _particleSystemObject;\r\n    private FXBoxNativeParticleSystem _particleSystem;\r\n    private bool _isPlaying = true;\r\n    private float _playbackSpeed = 1.0f;\r\n\r\n    private Vector2 _lastCursorPos;\r\n    private Vector2 _angles = new Vector2(45, 30);\r\n    private float _distance = 500f;\r\n    private float _actualDistance = 500f;\r\n    private Vector3 _targetPosition = Vector3.Zero;\r\n    private bool _isOrbiting = false;\r\n\r\n    public float PlaybackSpeed\r\n    {\r\n        get => _playbackSpeed;\r\n        set => _playbackSpeed = value;\r\n    }\r\n\r\n    public ParticlePreview(Widget parent) : base(parent)\r\n    {\r\n        MouseTracking = true;\r\n        FocusMode = FocusMode.Click;\r\n\r\n        Scene = Scene.CreateEditorScene();\r\n\r\n        using (Scene.Push())\r\n        {\r\n            // Setup camera\r\n            var cameraGo = new GameObject(true, \"camera\");\r\n            var camera = cameraGo.GetOrAddComponent<CameraComponent>();\r\n            camera.BackgroundColor = new Color(0.1f, 0.1f, 0.15f);\r\n            camera.ZFar = 10000;\r\n            camera.FieldOfView = 60;\r\n            Camera = camera;\r\n\r\n            // Add lighting\r\n            var sunGo = new GameObject(true, \"sun\");\r\n            var sun = sunGo.GetOrAddComponent<DirectionalLight>();\r\n            sun.WorldRotation = Rotation.FromPitch(50);\r\n            sun.LightColor = Color.White;\r\n\r\n            var ambientGo = new GameObject(true, \"ambient\");\r\n            var ambient = ambientGo.GetOrAddComponent<AmbientLight>();\r\n            ambient.Color = Color.Gray * 0.3f;\r\n        }\r\n\r\n        UpdateCameraPosition();\r\n    }\r\n\r\n    public void LoadParticleSystem(ParticleResource resource)\r\n    {\r\n        _resource = resource;\r\n        \r\n        // Clean up old system\r\n        if (_particleSystemObject.IsValid())\r\n        {\r\n            _particleSystemObject.Destroy();\r\n        }\r\n\r\n        if (_resource != null)\r\n        {\r\n            using (Scene.Push())\r\n            {\r\n                // Create new particle system object\r\n                _particleSystemObject = new GameObject(true, \"ParticleSystem\");\r\n                _particleSystem = _particleSystemObject.AddComponent<FXBoxNativeParticleSystem>();\r\n                _particleSystem.ParticleSystem = _resource;\r\n                _particleSystem.PlayOnStart = true;\r\n                \r\n                // Initialize the system\r\n                _particleSystem.UpdateEmitters();\r\n            }\r\n            \r\n            Log.Info($\"Loaded particle system with {_resource.Emitters?.Count ?? 0} emitters\");\r\n        }\r\n    }\r\n\r\n    public void SetPlaying(bool playing)\r\n    {\r\n        _isPlaying = playing;\r\n        \r\n        if (_particleSystemObject.IsValid())\r\n        {\r\n            // Enable/disable all particle effects\r\n            foreach (var effect in _particleSystemObject.Children)\r\n            {\r\n                var particleEffect = effect.GetComponent<ParticleEffect>();\r\n                if (particleEffect.IsValid())\r\n                {\r\n                    particleEffect.Enabled = playing;\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    public void TogglePlayback()\r\n    {\r\n        _isPlaying = !_isPlaying;\r\n        SetPlaying(_isPlaying);\r\n        Log.Info($\"Playback: {(_isPlaying ? \"Playing\" : \"Paused\")}\");\r\n    }\r\n\r\n    public void Restart()\r\n    {\r\n        if (_particleSystemObject.IsValid())\r\n        {\r\n            // Restart by rebuilding the entire system\r\n            LoadParticleSystem(_resource);\r\n            Log.Info(\"Particle system restarted\");\r\n        }\r\n    }\r\n\r\n    protected override void PreFrame()\r\n    {\r\n        Scene.EditorTick(RealTime.Now, RealTime.Delta);\r\n\r\n        DrawGizmos();\r\n        \r\n        // Force continuous updates\r\n        Update();\r\n    }\r\n\r\n    private void DrawGizmos()\r\n    {\r\n        if (_resource == null) return;\r\n\r\n        // Draw spawn shape for first emitter\r\n        if (_resource.Emitters.Count > 0)\r\n        {\r\n            var emitter = _resource.Emitters[0];\r\n            var posModule = emitter.InitializeModules.OfType<InitializePositionModule>().FirstOrDefault();\r\n            \r\n            if (posModule != null)\r\n            {\r\n                Gizmo.Draw.Color = Color.Yellow.WithAlpha(0.3f);\r\n                Gizmo.Draw.LineThickness = 2;\r\n                \r\n                switch (posModule.Shape)\r\n                {\r\n                    case InitializePositionModule.SpawnShape.Sphere:\r\n                        Gizmo.Draw.LineSphere(new Sphere(Vector3.Zero, posModule.Radius));\r\n                        break;\r\n                    case InitializePositionModule.SpawnShape.Box:\r\n                        Gizmo.Draw.LineBBox(BBox.FromPositionAndSize(Vector3.Zero, posModule.BoxSize));\r\n                        break;\r\n                    case InitializePositionModule.SpawnShape.Cone:\r\n                        DrawConeGizmo(posModule);\r\n                        break;\r\n                    case InitializePositionModule.SpawnShape.Circle:\r\n                        Gizmo.Draw.LineCircle(Vector3.Zero, Vector3.Forward, posModule.Radius);\r\n                        break;\r\n                    case InitializePositionModule.SpawnShape.Line:\r\n                        Gizmo.Draw.Line(posModule.LineStart, posModule.LineEnd);\r\n                        break;\r\n                }\r\n            }\r\n        }\r\n\r\n        DrawGrid();\r\n    }\r\n\r\n    private void DrawConeGizmo(InitializePositionModule module)\r\n    {\r\n        var height = module.Radius;\r\n        var radius = MathF.Tan(module.ConeAngle.DegreeToRadian()) * height;\r\n        \r\n        // Draw cone base\r\n        Gizmo.Draw.LineCircle(Vector3.Forward * height, Vector3.Forward, radius);\r\n        \r\n        // Draw cone lines\r\n        var points = 8;\r\n        for (int i = 0; i < points; i++)\r\n        {\r\n            var angle = (i / (float)points) * 360f;\r\n            var dir = new Vector3(\r\n                MathF.Cos(angle.DegreeToRadian()) * radius,\r\n                MathF.Sin(angle.DegreeToRadian()) * radius,\r\n                height\r\n            );\r\n            Gizmo.Draw.Line(Vector3.Zero, dir);\r\n        }\r\n    }\r\n\r\n    private void DrawGrid()\r\n    {\r\n        if (_resource?.PreviewSettings?.ShowGrid ?? true)\r\n        {\r\n            Gizmo.Draw.Color = Color.White.WithAlpha(0.1f);\r\n            Gizmo.Draw.LineThickness = 1;\r\n            \r\n            // Draw XY grid\r\n            for (int x = -500; x <= 500; x += 100)\r\n            {\r\n                Gizmo.Draw.Line(new Vector3(x, -500, 0), new Vector3(x, 500, 0));\r\n            }\r\n            for (int y = -500; y <= 500; y += 100)\r\n            {\r\n                Gizmo.Draw.Line(new Vector3(-500, y, 0), new Vector3(500, y, 0));\r\n            }\r\n        }\r\n\r\n        if (_resource?.PreviewSettings?.ShowGround ?? true)\r\n        {\r\n            Gizmo.Draw.Color = Color.Gray.WithAlpha(0.2f);\r\n            Gizmo.Draw.LineBBox(new BBox(new Vector3(-500, -500, -1), new Vector3(500, 500, 0)));\r\n        }\r\n        UpdateCameraPosition();\r\n    }\r\n\r\n    private void UpdateCameraPosition()\r\n    {\r\n        if (!Camera.IsValid()) return;\r\n\r\n        Camera.WorldRotation = new Angles(_angles.y, -_angles.x, 0);\r\n        _actualDistance = _actualDistance.LerpTo( _distance, Time.Delta * 15f );\r\n        Camera.WorldPosition = _targetPosition + Camera.WorldRotation.Backward * _actualDistance;\r\n    }\r\n\r\n    protected override void OnMousePress(MouseEvent e)\r\n    {\r\n        base.OnMousePress(e);\r\n\r\n        if (e.LeftMouseButton)\r\n        {\r\n            _isOrbiting = true;\r\n            _lastCursorPos = e.ScreenPosition;\r\n        }\r\n    }\r\n\r\n    protected override void OnMouseReleased(MouseEvent e)\r\n    {\r\n        base.OnMouseReleased(e);\r\n\r\n        if (e.LeftMouseButton)\r\n        {\r\n            _isOrbiting = false;\r\n        }\r\n    }\r\n\r\n    protected override void OnMouseMove(MouseEvent e)\r\n    {\r\n        base.OnMouseMove(e);\r\n\r\n        if (_isOrbiting)\r\n        {\r\n            var delta = e.ScreenPosition - _lastCursorPos;\r\n            _angles.x += delta.x * 0.3f;\r\n            _angles.y += delta.y * 0.3f;\r\n            _angles.y = _angles.y.Clamp(-90, 90);\r\n            \r\n            //UpdateCameraPosition();\r\n            _lastCursorPos = e.ScreenPosition;\r\n        }\r\n    }\r\n\r\n    protected override void OnMouseWheel( WheelEvent e )\r\n    {\r\n\t    base.OnMouseWheel(e);\r\n\t    _distance -= e.Delta * 1f;\r\n\t    _distance = _distance.Clamp(50, 5000);\r\n    }\r\n    \r\n    protected override void OnPaint()\r\n    {\r\n        base.OnPaint();\r\n\r\n        // Draw overlay info\r\n        Paint.SetPen(Theme.Text);\r\n        Paint.SetDefaultFont();\r\n        // Count particles from all emitter objects\r\n        int totalParticles = 0;\r\n        int totalMax = _resource?.Emitters.Sum(e => e.MaxParticles) ?? 0;\r\n        \r\n        if (_particleSystemObject.IsValid())\r\n        {\r\n            foreach (var child in _particleSystemObject.Children)\r\n            {\r\n                var effect = child.GetComponent<ParticleEffect>();\r\n                if (effect.IsValid())\r\n                {\r\n                    totalParticles += effect.Particles.Count;\r\n                }\r\n            }\r\n        }\r\n        \r\n        var text = $\"Particles: {totalParticles} / {totalMax}\";\r\n        Paint.DrawText(new Rect(10, 10, 200, 30), text, TextFlag.LeftTop);\r\n        \r\n        var stateText = _isPlaying ? \"PLAYING\" : \"PAUSED\";\r\n        Paint.DrawText(new Rect(10, 40, 200, 30), stateText, TextFlag.LeftTop);\r\n        \r\n        // Draw emitter count\r\n        var emitterText = $\"Emitters: {_resource?.Emitters.Count ?? 0}\";\r\n        Paint.DrawText(new Rect(10, 70, 200, 30), emitterText, TextFlag.LeftTop);\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "ParticleModules.cs",
            "FileName": "ParticleModules.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 379493,
            "Code": "using System;\r\nusing Sandbox;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Text.Json;\r\nusing System.Text.Json.Serialization;\r\n\r\nnamespace fxbox;\r\n\r\n/// <summary>\r\n/// Stage when a particle module executes\r\n/// </summary>\r\npublic enum ModuleStage\r\n{\r\n    Spawn,      // Controls when/how particles spawn\r\n    Initialize, // Runs once when particle is created\r\n    Update,     // Runs every frame for each particle\r\n    Render      // Controls how particles are rendered\r\n}\r\n\r\n/// <summary>\r\n/// Context passed to particle modules during execution\r\n/// </summary>\r\npublic class ParticleExecutionContext\r\n{\r\n\tpublic Particle Particle;\r\n\tpublic ParticleEffect Effect;\r\n\tpublic Sandbox.ParticleEmitter Emitter;\r\n\tpublic FXBoxNativeParticleSystem SystemComponent; // Changed from Resource to SystemComponent\r\n}\r\n\r\n// ==================== SPAWN MODULES ====================\r\n\r\n/// <summary>\r\n/// Controls spawn rate over time\r\n/// </summary>\r\n[Title(\"Spawn Rate\"), Category(\"Spawn\"), Icon(\"speed\")]\r\npublic partial class SpawnRateModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Spawn;\r\n    \r\n    [Property, Range(0.1f, 1000f)] \r\n    public FXParticleFloat SpawnRate { get; set; } = 10.0f;\r\n    \r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n\t    var rate = SpawnRate;\r\n        context.Emitter.Rate = rate.GetValue( context.SystemComponent );\r\n    }\r\n    \r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    context.Emitter.Rate = SpawnRate.GetValue( context.SystemComponent );\r\n    }\r\n}\r\n/// <summary>\r\n/// Sets initial particle stretch\r\n/// </summary>\r\n[Title(\"Particle Stretch\"), Category(\"Initialize\"), Icon(\"photo_size_select_small\")]\r\npublic partial class ParticleStretchModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n\t\r\n    \r\n\t[Property, Range(0.1f, 100f)]\r\n\tpublic FXParticleFloat Size { get; set; } = 1.0f;\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tcontext.Effect.ApplyShape = true;\r\n\t\tcontext.Effect.Stretch = Size.ToParticleFloat( context.SystemComponent );\r\n\t}\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n       \r\n\t}\r\n}\r\n/// <summary>\r\n/// Controls spawn rate per unit\r\n/// </summary>\r\n[Title(\"Spawn Rate Over Distance\"), Category(\"Spawn\"), Icon(\"speed\")]\r\npublic partial class SpawnRateOverDistanceModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Spawn;\r\n    \r\n\t[Property, Range(0.1f, 1000f)] \r\n\tpublic FXParticleFloat SpawnRate { get; set; } = 10.0f;\r\n    \r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n\t\tcontext.Emitter.RateOverDistance = SpawnRate.GetValue( context.SystemComponent );\r\n\t}\r\n    \r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tcontext.Emitter.RateOverDistance = SpawnRate.GetValue( context.SystemComponent );\r\n\t}\r\n}\r\n/// <summary>\r\n/// Spawns particles in a burst\r\n/// </summary>\r\n[Title(\"Spawn Burst\"), Category(\"Spawn\"), Icon(\"auto_awesome\")]\r\npublic partial class SpawnBurstModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Spawn;\r\n    \r\n    [Property, Range(1, 1000)]\r\n    public int ParticleCount { get; set; } = 50;\r\n    \r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    context.Emitter.Burst = ParticleCount;\r\n    }\r\n\r\n    // Burst is a one-shot \"spawn this many now\" value, not a continuous per-tick one like\r\n    // SpawnRateModule's Rate - Execute() runs every single frame (see\r\n    // FXBoxParticleController.OnUpdate), so reassigning Burst here too kept re-arming/\r\n    // re-firing it every tick instead of once, spawning far more than ParticleCount actually\r\n    // configured. Same \"Initialize-only, empty Execute\" shape ParticleStretchModule already\r\n    // uses above for its own one-shot value.\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n    }\r\n}\r\n\r\n// ==================== INITIALIZE MODULES ====================\r\n\r\n/// <summary>\r\n/// Sets initial position based on shape\r\n/// </summary>\r\n[Title(\"Initialize Position\"), Category(\"Initialize\"), Icon(\"place\")]\r\npublic partial class InitializePositionModule : ParticleModule, IParticleComponentCreator\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Initialize;\r\n\r\n    public override void Execute( ParticleExecutionContext context )\r\n    {\r\n\t    \r\n    }\r\n\r\n    public enum SpawnShape { Point, Sphere, Box, Cone, Circle, Line }\r\n    \r\n    [Property]\r\n    public FXCopyFlags CopyFlags { get; set; } = FXCopyFlags.Rotation | FXCopyFlags.Scale;\r\n    \r\n    [Property]\r\n    public SpawnShape Shape { get; set; } = SpawnShape.Sphere;\r\n    \r\n    [Property, Range(0f, 1000f), ShowIf(nameof(ShowRadius), true)]\r\n    public float Radius { get; set; } = 50.0f;\r\n    \r\n    [Property, ShowIf(nameof(ShowBoxSize), true)]\r\n    public Vector3 BoxSize { get; set; } = new Vector3(100, 100, 100);\r\n    \r\n    [Property, Range(0f, 180f), ShowIf(nameof(ShowConeAngle), true)]\r\n    public float ConeAngle { get; set; } = 45.0f;\r\n    \r\n    [Property]\r\n    public bool EmitFromShell { get; set; } = false;\r\n    \r\n    [Property, ShowIf(nameof(ShowLine), true)]\r\n    public Vector3 LineStart { get; set; } = Vector3.Zero;\r\n    \r\n    [Property, ShowIf(nameof(ShowLine), true)]\r\n    public Vector3 LineEnd { get; set; } = Vector3.Up * 100;\r\n\r\n    [Hide] public bool ShowRadius => Shape == SpawnShape.Sphere || Shape == SpawnShape.Circle || Shape == SpawnShape.Cone;\r\n    [Hide] public bool ShowBoxSize => Shape == SpawnShape.Box;\r\n    [Hide] public bool ShowConeAngle => Shape == SpawnShape.Cone;\r\n    [Hide] public bool ShowLine => Shape == SpawnShape.Line;\r\n    \r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    if ( context.Particle != null )\r\n\t    {\r\n\t\t    \r\n\t\t    var pos = Shape switch\r\n\t\t    {\r\n\t\t\t    SpawnShape.Point => Vector3.Zero,\r\n\t\t\t    SpawnShape.Sphere => GetSpherePosition(),\r\n\t\t\t    SpawnShape.Box => GetBoxPosition(),\r\n\t\t\t    SpawnShape.Cone => GetConePosition(),\r\n\t\t\t    SpawnShape.Circle => GetCirclePosition(),\r\n\t\t\t    SpawnShape.Line => GetLinePosition(),\r\n\t\t\t    _ => Vector3.Zero\r\n\t\t    };\r\n\r\n\t\t    if ( CopyFlags.HasFlag( FXCopyFlags.Scale ) )\r\n\t\t    {\r\n\t\t\t    pos = pos * context.Emitter.WorldScale;\r\n\t\t    }\r\n\r\n\t\t    if ( CopyFlags.HasFlag( FXCopyFlags.Rotation ) )\r\n\t\t    {\r\n\t\t\t    pos = pos.RotateAround( 0, context.SystemComponent.WorldRotation );\r\n\t\t    }\r\n\r\n\t\r\n\t\t    context.Particle.Position += pos;\r\n\t    }\r\n    }\r\n    public void CreateComponent(GameObject go)\r\n    {\r\n\t    var pointEmitter = go.AddComponent<ParticleSphereEmitter>();\r\n\t    pointEmitter.Radius = 0;\r\n\t    pointEmitter.Velocity = 0;\r\n\t    pointEmitter.Burst = 0;\r\n\t    pointEmitter.Rate = 0;\r\n    }\r\n\r\n    private Vector3 GetSpherePosition()\r\n    {\r\n        var direction = Random.Shared.VectorInSphere().Normal;\r\n        var radius = EmitFromShell ? Radius : Random.Shared.Float(0, Radius);\r\n        \r\n        return direction * radius;\r\n    }\r\n\r\n    private Vector3 GetBoxPosition()\r\n    {\r\n        if (EmitFromShell)\r\n        {\r\n            var face = Random.Shared.Int(0, 5);\r\n            var u = Random.Shared.Float(0, 1);\r\n            var v = Random.Shared.Float(0, 1);\r\n            var halfSize = BoxSize / 2f;\r\n            \r\n            return face switch\r\n            {\r\n                0 => new Vector3(-halfSize.x, MathX.Lerp(-halfSize.y, halfSize.y, u), MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                1 => new Vector3(halfSize.x, MathX.Lerp(-halfSize.y, halfSize.y, u), MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                2 => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), -halfSize.y, MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                3 => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), halfSize.y, MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                4 => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), MathX.Lerp(-halfSize.y, halfSize.y, v), -halfSize.z),\r\n                _ => new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), MathX.Lerp(-halfSize.y, halfSize.y, v), halfSize.z)\r\n            };\r\n        }\r\n        \r\n        return new Vector3(\r\n            Random.Shared.Float(-BoxSize.x / 2, BoxSize.x / 2),\r\n            Random.Shared.Float(-BoxSize.y / 2, BoxSize.y / 2),\r\n            Random.Shared.Float(-BoxSize.z / 2, BoxSize.z / 2)\r\n        );\r\n    }\r\n\r\n    private Vector3 GetConePosition()\r\n    {\r\n        var angle = Random.Shared.Float(0, 360);\r\n        var distance = Random.Shared.Float(0, Radius);\r\n        var coneRadius = MathF.Tan(ConeAngle.DegreeToRadian()) * distance;\r\n        var radius = EmitFromShell ? coneRadius : Random.Shared.Float(0, coneRadius);\r\n        \r\n        return new Vector3(\r\n            MathF.Cos(angle.DegreeToRadian()) * radius,\r\n            MathF.Sin(angle.DegreeToRadian()) * radius,\r\n            distance\r\n        );\r\n    }\r\n\r\n    private Vector3 GetCirclePosition()\r\n    {\r\n        var angle = Random.Shared.Float(0, 360);\r\n        var radius = EmitFromShell ? Radius : Random.Shared.Float(0, Radius);\r\n        return new Vector3(\r\n            MathF.Cos(angle.DegreeToRadian()) * radius,\r\n            MathF.Sin(angle.DegreeToRadian()) * radius,\r\n            0\r\n        );\r\n    }\r\n\r\n    private Vector3 GetLinePosition()\r\n    {\r\n        return Vector3.Lerp(LineStart, LineEnd, Random.Shared.Float(0, 1));\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Controls how strongly particles follow the emitter in local space\r\n/// </summary>\r\n[Title(\"Initialize Local Space\"), Category(\"Initialize\"), Icon(\"transform\")]\r\npublic partial class InitializeLocalSpaceModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n\r\n\t[Property, Range(0f, 1f)]\r\n\tpublic FXParticleFloat LocalSpace { get; set; } = 0f;\r\n\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tcontext.Effect.LocalSpace = LocalSpace.ToParticleFloat( context.SystemComponent );\r\n\t}\r\n\r\n\tpublic override void Execute( ParticleExecutionContext context )\r\n\t{\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Sets initial velocity\r\n/// </summary>\r\n[Title(\"Initialize Velocity\"), Category(\"Initialize\"), Icon(\"air\")]\r\npublic partial class InitializeVelocityModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Initialize;\r\n    \r\n    [Property]\r\n    public FXParticleVector Velocity { get; set; } = Vector3.Up * 100;\r\n\r\n    [Property] public FXParticleFloat RandomVelocity { get; set; } = 0;\r\n\r\n    [Property] public bool LocalSpace { get; set; } = false;\r\n    \r\n    [Property]\r\n    public bool InheritEmitterVelocity { get; set; } = false;\r\n    \r\n    \r\n    [Property,ShowIf(\"InheritEmitterVelocity\",true)] public float EmitterVelocityScale { get; set; } = 1.0f;\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    var startVelocity = Velocity;\r\n\t    if ( LocalSpace )\r\n\t    {\r\n\t\t    startVelocity = startVelocity.GetValue( context.Particle,context.SystemComponent ).RotateAround( 0,context.Emitter.WorldRotation );\r\n\t    }\r\n\r\n\t    context.Effect.StartVelocity = RandomVelocity.ToParticleFloat();\r\n\t    context.Effect.InitialVelocity = startVelocity.GetValue( context.Particle,context.SystemComponent );\r\n\t    if ( InheritEmitterVelocity )\r\n\t    {\r\n\t\t    context.Effect.InitialVelocity = (context.SystemComponent.Velocity*EmitterVelocityScale) + startVelocity.GetValue( context.Particle,context.SystemComponent );\r\n\t    }\r\n\t    \r\n    }\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n\r\n    }\r\n}\r\n/// <summary>\r\n/// Sets initial color\r\n/// </summary>\r\n[Title(\"Collision\"), Category(\"Initialize\"), Icon(\"palette\")]\r\npublic partial class ParticleCollisionModule : ParticleModule\r\n{\r\n\r\n\t[Property] public TagSet CollisionIgnore { get; set; } = new TagSet();\r\n\t[Property] public List<GameObject> CollisionPrefabs { get; set; } = new List<GameObject>();\r\n\t[Property] public FXParticleFloat CollisionRadius { get; set; } = 5;\r\n\t[Property] public FXParticleFloat CollisionPrefabChance { get; set; } = 1;\r\n\t[Property] public FXParticleFloat CollisionPrefabRotation { get; set; } = 0;\r\n\t[Property] public FXParticleFloat DieOnCollisionChance { get; set; } = 0;\r\n\t[Property] public bool CollisionPrefabAlign { get; set; } = false;\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n        \r\n\t}\r\n    \r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tcontext.Effect.Collision = true;\r\n\t\tcontext.Effect.CollisionIgnore = CollisionIgnore;\r\n\t\tcontext.Effect.CollisionRadius = CollisionRadius.GetValue( context.SystemComponent );\r\n\t\tcontext.Effect.CollisionPrefabChance = CollisionPrefabChance.GetValue( context.SystemComponent );\r\n\t\tcontext.Effect.CollisionPrefabRotation = CollisionPrefabRotation.ToParticleFloat( context.SystemComponent );\r\n\t\tif ( CollisionPrefabs.Any() )\r\n\t\t{\r\n\t\t\tcontext.Effect.UsePrefabFeature = true;\r\n\t\t}\r\n\t\tcontext.Effect.CollisionPrefab = CollisionPrefabs;\r\n\t\tcontext.Effect.DieOnCollisionChance = DieOnCollisionChance.GetValue( context.SystemComponent );\r\n\t\tcontext.Effect.CollisionPrefabAlign = CollisionPrefabAlign;\r\n\t}\r\n}\r\n/// <summary>\r\n/// Sets initial velocity\r\n/// </summary>\r\n[Title(\"Initialize Rotation\"), Category(\"Initialize\"), Icon(\"air\")]\r\npublic partial class InitializeRotationModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n    \r\n\t[Property]\r\n\tpublic ParticleVector3 InitialRotation { get; set; } = Vector3.Up;\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tif ( context.Particle != null )\r\n\t\t{\r\n\t\t\tcontext.Particle.Angles = new Angles( InitialRotation.Evaluate( Time.Delta,context.Particle.Rand(  ),context.Particle.Rand(  ),context.Particle.Rand(  ) ) );\r\n\t\t}\r\n\t    \r\n\t}\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Sets initial lifetime\r\n/// </summary>\r\n[Title(\"Initialize Lifetime\"), Category(\"Initialize\"), Icon(\"schedule\")]\r\npublic partial class InitializeLifetimeModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n    \r\n\t[Property, Range(0.1f, 100f)]\r\n\tpublic FXParticleFloat Lifetime { get; set; } = 2.0f;\r\n    \r\n\tpublic override void Initialize(ParticleExecutionContext context)\r\n\t{\r\n\t\tcontext.Effect.Lifetime = Lifetime.ToParticleFloat( context.SystemComponent );\r\n\t}\r\n    \r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n\t\t// Not used\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Sets initial size\r\n/// </summary>\r\n[Title(\"Initialize Size\"), Category(\"Initialize\"), Icon(\"photo_size_select_small\")]\r\npublic partial class InitializeSizeModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Initialize;\r\n\r\n    [Property] public bool InheritEmitterScale { get; set; } = true;\r\n    \r\n    [Property, Range(0.1f, 100f)]\r\n    public FXParticleFloat Size { get; set; } = 10.0f;\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    context.Effect.ApplyShape = true;\r\n\t    if ( InheritEmitterScale )\r\n\t    {\r\n\t\t    context.Effect.Scale = Size.ToParticleFloat( context.SystemComponent );\r\n\t    }\r\n\t    else\r\n\t    {\r\n\t\t     context.Effect.Scale = (Size / context.SystemComponent.WorldScale.x).ToParticleFloat( context.SystemComponent );\r\n\t    }\r\n\t    \r\n\t    \r\n\t  \r\n    }\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n       \r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Sets initial color\r\n/// </summary>\r\n[Title(\"Initialize Color\"), Category(\"Initialize\"), Icon(\"palette\")]\r\npublic partial class InitializeColorModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Initialize;\r\n\r\n    [Property] public FXParticleColor Color { get; set; } = global::Color.Red;\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n        \r\n    }\r\n    \r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    context.Effect.ApplyColor = true;\r\n\t    context.Effect.ApplyAlpha = true;\r\n\t    if ( Color != null )\r\n\t    {\r\n\t\t    context.Effect.Gradient = Color.GetValue( context.SystemComponent );\r\n\t    }\r\n\t    else\r\n\t    {\r\n\t\t    Color = new FXParticleColor( global::Color.Red );\r\n\t    }\r\n\t    \r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Sets initial color\r\n/// </summary>\r\n[Title(\"Sprite Flipbook\"), Category(\"Initialize\"), Icon(\"palette\")]\r\npublic partial class SpriteFlipbookModule : ParticleModule\r\n{\r\n\t[Property] public FXParticleFloat SequenceTime { get; set; } = 0;\r\n\t[Property] public FXParticleFloat SequenceSpeed { get; set; } = 1;\r\n\t[Property] public int SequenceId { get; set; } = 0;\r\n\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n        \r\n\t}\r\n    \r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tcontext.Effect.SheetSequence = true;\r\n\t\tcontext.Effect.SequenceId = SequenceId;\r\n\t\tcontext.Effect.SequenceSpeed = SequenceSpeed.ToParticleFloat( context.SystemComponent );\r\n\t\tcontext.Effect.SequenceTime = SequenceTime.ToParticleFloat( context.SystemComponent );\r\n\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Randomly Kill a particle to spawn less\r\n/// </summary>\r\n[Title(\"RandomKill\"), Category(\"Initialize\"), Icon(\"arrow_downward\")]\r\npublic partial class RandomKill : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Initialize;\r\n\t\r\n\t[Property]\r\n\tpublic float Chance { get; set; } = 0.5f;\r\n\t\r\n\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n        \r\n\t}\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t\tif ( context.Particle == null ) return;\r\n\t\tif ( Random.Shared.Float( 0, 1 ) < Chance )\r\n\t\t{\r\n\t\t\tcontext.Particle.Age = 100000;\r\n\t\t}\r\n\t}\r\n\t\r\n}\r\n\r\n\r\n// ==================== UPDATE MODULES ====================\r\n\r\n/// <summary>\r\n/// Applies gravity force\r\n/// </summary>\r\n[Title(\"Gravity Force\"), Category(\"Update\"), Icon(\"arrow_downward\")]\r\npublic partial class GravityForceModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Update;\r\n    \r\n    [Property]\r\n    public FXParticleVector Force { get; set; } = new Vector3(0, 0, -980);\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n        \r\n    }\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    \r\n    }\r\n    public void UpdateParticle(ParticleExecutionContext context, float delta)\r\n    {\r\n        context.Particle.Velocity += Force.GetValue(context.Particle, context.SystemComponent ) * Time.Delta;\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Make a mesh follow it's velocity\r\n/// </summary>\r\n[Title(\"Follow Velocity\"), Category(\"Update\"), Icon(\"arrow_downward\")]\r\npublic partial class FollowVelocity : ParticleModule, IParticleUpdater\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Update;\r\n\t\r\n\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n        \r\n\t}\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t    \r\n\t}\r\n\tpublic void UpdateParticle(ParticleExecutionContext context, float delta)\r\n\t{\r\n\t\tcontext.Particle.Angles = Rotation.LookAt( context.Particle.Velocity ).Angles();\r\n\t}\r\n}\r\n/// <summary>\r\n/// Applies drag/air resistance\r\n/// </summary>\r\n[Title(\"Drag Force\"), Category(\"Update\"), Icon(\"air\")]\r\npublic partial class DragForceModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Update;\r\n    \r\n    [Property, Range(0f, 10f)]\r\n    public float Damping { get; set; } = 0.1f;\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n        \r\n    }\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    \r\n    }\r\n    public void UpdateParticle(ParticleExecutionContext context, float delta)\r\n    {\r\n        context.Particle.Velocity *= (1.0f - Damping * Time.Delta);\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Makes particles rotate\r\n/// </summary>\r\n[Title(\"Rotation\"), Category(\"Update\"), Icon(\"rotate_right\")]\r\npublic partial class RotationModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Update;\r\n\r\n    [Property] public FXParticleVector RotationSpeed { get; set; } = Vector3.Zero;\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n        /*context.Particle.Rotation += RotationSpeed * context.DeltaTime;*/\r\n    }\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    \r\n    }\r\n    public void UpdateParticle(ParticleExecutionContext context, float delta)\r\n    {\r\n\t    if ( context.Particle != null )\r\n\t    {\r\n\t\t    context.Particle.Angles += RotationSpeed.GetValue( context.Particle ) * Time.Delta;\r\n\t    }\r\n        \r\n    }\r\n}\r\n\r\npublic enum PositionType\r\n{\r\n\tLocal,\r\n\tWorld\r\n}\r\n\r\n/// <summary>\r\n/// Attracts particles to a point. Full strength inside AttractorSize, falling off beyond it.\r\n/// </summary>\r\n[Title(\"Point Attractor\"), Category(\"Update\"), Icon(\"my_location\")]\r\npublic partial class PointAttractorModule : ParticleModule, IParticleUpdater\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage => ModuleStage.Update;\r\n\t[Property] public PositionType PositionType { get; set; } = PositionType.Local;\r\n\r\n\t[Property]\r\n\tpublic FXParticleVector AttractorPosition { get; set; } = Vector3.Zero;\r\n\r\n\t[Property, Range(0f, 10000f)]\r\n\tpublic FXParticleFloat Strength { get; set; } = 500.0f;\r\n\r\n\t[Property, Range(0.01f, 10000f)]\r\n\tpublic float AttractorSize { get; set; } = 50.0f;\r\n\r\n\t[Property] public bool Invert { get; set; } = false;\r\n\r\n\t/// <summary>\r\n\t/// How quickly strength falls off beyond AttractorSize.\r\n\t/// 1 = linear, 2 = inverse square, higher = sharper falloff.\r\n\t/// </summary>\r\n\t[Property, Range(0.1f, 8f)]\r\n\tpublic float Falloff { get; set; } = 2.0f;\r\n\r\n\tpublic override void Execute(ParticleExecutionContext context) { }\r\n\tpublic override void Initialize(ParticleExecutionContext context) { }\r\n\r\n\tpublic void UpdateParticle(ParticleExecutionContext context, float delta)\r\n\t{\r\n\t\tvar attractorPos = PositionType == PositionType.Local ? AttractorPosition.GetValue( context.Particle, context.SystemComponent ) + context.Emitter.WorldPosition : AttractorPosition.GetValue( context.Particle, context.SystemComponent );\r\n\t\t\r\n\t\t\r\n\t\tvar toAttractor = attractorPos - context.Particle.Position;\r\n\t\tvar distance = toAttractor.Length;\r\n\r\n\t\tif (distance < 0.01f) return;\r\n\r\n\t\t// Inside the attractor: full strength.\r\n\t\t// Outside: strength falls off based on normalised excess distance.\r\n\t\tfloat strengthMultiplier;\r\n\t\tif (distance <= AttractorSize)\r\n\t\t{\r\n\t\t\tstrengthMultiplier = 1f;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\t// How many radii past the edge are we? 0 at the surface, grows outward.\r\n\t\t\tvar excess = (distance - AttractorSize) / AttractorSize;\r\n\t\t\tstrengthMultiplier = 1f / MathF.Pow(1f + excess, Falloff);\r\n\t\t}\r\n\t\t\r\n\t\tif ( Invert )\r\n\t\t{\r\n\t\t\tstrengthMultiplier = 1 - strengthMultiplier;\r\n\t\t}\r\n\r\n\t\tcontext.Particle.Velocity += toAttractor.Normal * Strength.GetValue( context.SystemComponent ) * strengthMultiplier * Time.Delta;\r\n\t}\r\n}\r\n/// <summary>\r\n/// Creates orbital motion\r\n/// </summary>\r\n[Title(\"Vortex Force\"), Category(\"Update\"), Icon(\"cyclone\")]\r\npublic partial class VortexForceModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Update;\r\n    \r\n    [Property]\r\n    public Vector3 Center { get; set; } = Vector3.Zero;\r\n    \r\n    [Property]\r\n    public FXParticleVector Axis { get; set; } = Vector3.Up;\r\n    \r\n    [Property, Range(0f, 1000f)]\r\n    public float Strength { get; set; } = 100.0f;\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n        \r\n    }\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    \r\n    }\r\n    public void UpdateParticle(ParticleExecutionContext context, float delta)\r\n    {\r\n        var toCenter = context.Particle.Position - (Center + context.Emitter.WorldPosition);\r\n        var distance = toCenter.Length;\r\n        \r\n        \r\n        if (distance > 0.01f)\r\n        {\r\n            var tangent = Vector3.Cross(Axis.GetValue( context.Particle,context.SystemComponent ).Normal, toCenter.Normal);\r\n            var force = tangent * (Strength / distance);\r\n            context.Particle.Velocity += force * Time.Delta * 10000;\r\n        }\r\n    }\r\n}\r\n\r\n// ==================== RENDER MODULES ====================\r\n\r\n/// <summary>\r\n/// Basic sprite renderer\r\n/// </summary>\r\n[Title(\"Sprite Renderer\"), Category(\"Render\"), Icon(\"image\")]\r\npublic partial class SpriteRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n    [Hide] \r\n    public override ModuleStage Stage => ModuleStage.Render;\r\n\r\n    [Property] \r\n    public Sprite Sprite { get; set; }\r\n\r\n    [Property] public FXParticleFloat SpriteScale { get; set; } = 1f;\r\n\r\n    [Property]\r\n    public ParticleSpriteRenderer.BillboardAlignment Alignment { get; set; } =\r\n\t    ParticleSpriteRenderer.BillboardAlignment.LookAtCamera;\r\n\r\n    [Property] public bool FaceVelocity { get; set; } = false;\r\n\r\n    [Property] public bool Additive { get; set; } = false;\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n    }\r\n    public override void Initialize( ParticleExecutionContext context )\r\n    {\r\n\t    \r\n    }\r\n    public void CreateComponent(GameObject go)\r\n    {\r\n        var renderer = go.AddComponent<ParticleSpriteRenderer>();\r\n\r\n        renderer.Alignment = Alignment;\r\n        renderer.FaceVelocity = FaceVelocity;\r\n        renderer.Sprite = Sprite;\r\n\t\trenderer.Additive = Additive;\r\n\t\trenderer.Scale = SpriteScale.GetValue();\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Basic light renderer\r\n/// </summary>\r\n[Title(\"Light Renderer\"), Category(\"Render\"), Icon(\"image\")]\r\npublic partial class LightRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n\t[Hide] \r\n\tpublic override ModuleStage Stage => ModuleStage.Render;\r\n\t\r\n\t[Property] public FXParticleColor LightColor { get; set; } = new FXParticleColor( Color.White );\r\n\t[Property] public FXParticleFloat Brightness { get; set; } = 10f;\r\n\t[Property] public FXParticleFloat MaxLights { get; set; } = 10f;\r\n\t[Property] public FXParticleFloat LightSize { get; set; } = 10f;\r\n\t[Property] public FXParticleFloat Attenuation { get; set; } = 1;\r\n\t[Property] public bool CastShadows { get; set; } = false;\r\n\t[Property] public bool UseParticleColor { get; set; } = true;\r\n\t[Property] public FXParticleFloat Ratio { get; set; } = 1;\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n\t\t// Rendering is handled externally, this just stores render properties\r\n\t}\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t    \r\n\t}\r\n\tpublic void CreateComponent(GameObject go)\r\n\t{\r\n\t\tvar renderer = go.AddComponent<ParticleLightRenderer>();\r\n\t\t\r\n\t\tvar fxbox=go.GetComponentInParent<FXBoxNativeParticleSystem>(  );\r\n\t\trenderer.LightColor = LightColor.GetValue( fxbox );\r\n\t\trenderer.Brightness = Brightness.GetValue( fxbox );\r\n\t\t\r\n\t\trenderer.MaximumLights = (int)MaxLights.GetValue( fxbox );\r\n\t\trenderer.Scale = LightSize.GetValue( fxbox );\r\n\t\trenderer.Attenuation = Attenuation.GetValue( fxbox );\r\n\t\trenderer.Ratio = Ratio.GetValue( fxbox );\r\n\t\trenderer.CastShadows = CastShadows;\r\n\t\trenderer.UseParticleColor = UseParticleColor;\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Basic model renderer\r\n/// </summary>\r\n[Title(\"Model Renderer\"), Category(\"Render\"), Icon(\"image\")]\r\npublic partial class ModelRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n\t[Hide] \r\n\tpublic override ModuleStage Stage => ModuleStage.Render;\r\n\r\n\t[Property] \r\n\tpublic List<ParticleModelRenderer.ModelEntry> Models { get; set; }\r\n\r\n\t[Property] \r\n\tpublic bool FaceCamera { get; set; } = true;\r\n\r\n\tpublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n\t\t// Rendering is handled externally, this just stores render properties\r\n\t}\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t    \r\n\t}\r\n\tpublic void CreateComponent(GameObject go)\r\n\t{\r\n\t\tvar renderer = go.AddComponent<ParticleModelRenderer>();\r\n\r\n\t\trenderer.Choices = Models;\r\n\r\n\t}\r\n}\r\n\r\n/// <summary>\r\n/// Basic Trail Renderer\r\n/// </summary>\r\n[Title( \"Trail Renderer\" ), Category( \"Render\" ), Icon( \"image\" )]\r\npublic partial class TrailRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n\t[Hide] public override ModuleStage Stage => ModuleStage.Render;\r\n\r\n\t[Property] public bool Game { get; set; } = true;\r\n\t[Property] public bool Overlay { get; set; } = false;\r\n\t[Property] public bool Bloom { get; set; } = false;\r\n\t[Property] public bool AfterUi { get; set; } = false;\r\n\t[Property] public Material Material { get; set; }\r\n\t[Property] public FXParticleFloat UnitsPerTexture { get; set; } = 10f;\r\n\t[Property] public FXParticleFloat Scroll { get; set; } = 0f;\r\n\t[Property] public FXParticleFloat Width { get; set; } = 1f;\r\n\t[Property] public bool Opaque { get; set; } = true;\r\n\t[Property, ShowIf( \"Opaque\", false )] public BlendMode BlendMode { get; set; } = BlendMode.Normal;\r\n\t[Property] public int MaxPoints { get; set; } = 32;\r\n\t[Property] public float PointDistance { get; set; } = 8;\r\n\t[Property] public float LifeTime { get; set; } = 2f;\r\n\t[Property] public FXParticleColor Color { get; set; } = new FXParticleColor(  );\r\n\r\npublic override void Execute(ParticleExecutionContext context)\r\n\t{\r\n\t\t// Rendering is handled externally, this just stores render properties\r\n\t}\r\n\tpublic override void Initialize( ParticleExecutionContext context )\r\n\t{\r\n\t    \r\n\t}\r\n\tpublic void CreateComponent(GameObject go)\r\n\t{\r\n\t\tvar renderer = go.AddComponent<ParticleTrailRenderer>();\r\n\r\n\t\tvar appearance = renderer.Texturing;\r\n\t\tappearance.Material = Material;\r\n\t\tappearance.UnitsPerTexture = UnitsPerTexture.GetValue(  );\r\n\t\tappearance.Scroll = Scroll.GetValue();\r\n\r\n\t\tvar widthCurve = Width.ToParticleFloat();\r\n\r\n\t\tif ( widthCurve.Type == ParticleFloat.ValueType.Curve )\r\n\t\t{\r\n\t\t\trenderer.Width = widthCurve.CurveA;\r\n\t\t} else if ( widthCurve.Type == ParticleFloat.ValueType.Range )\r\n\t\t{\r\n\t\t\tvar point1 = new Curve.Frame( 0, widthCurve.ConstantA );\r\n\t\t\tvar point2 = new Curve.Frame( 1, widthCurve.ConstantB );\r\n\t\t\trenderer.Width = new Curve( point1, point2 );\r\n\t\t} else if ( widthCurve.Type == ParticleFloat.ValueType.Constant )\r\n\t\t{\r\n\t\t\trenderer.Width = widthCurve.ConstantA;\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\trenderer.Width = widthCurve.CurveA;\r\n\t\t}\r\n\t\t\r\n\t\trenderer.Opaque = Opaque;\r\n\t\trenderer.BlendMode = BlendMode;\r\n\t\trenderer.MaxPoints = MaxPoints;\r\n\t\trenderer.PointDistance = PointDistance;\r\n\t\trenderer.LifeTime = LifeTime;\r\n\t\tvar colorParam = Color.GetValue();\r\n\t\tif ( colorParam.Type == ParticleGradient.ValueType.Constant )\r\n\t\t{\r\n\t\t\trenderer.Color = colorParam.ConstantA;\r\n\t\t} else if ( colorParam.Type == ParticleGradient.ValueType.Range )\r\n\t\t{\r\n\t\t\tvar point1 = new Gradient.ColorFrame( 0, colorParam.ConstantA );\r\n\t\t\tvar point2 = new Gradient.ColorFrame( 1, colorParam.ConstantB );\r\n\t\t\trenderer.Color = new Gradient( point1, point2 );\r\n\t\t} else if ( colorParam.Type == ParticleGradient.ValueType.Gradient )\r\n\t\t{\r\n\t\t\trenderer.Color = colorParam.GradientA;\r\n\t\t}\r\n\r\n\t\trenderer.RenderOptions.Game = Game;\r\n\t\trenderer.RenderOptions.Overlay = Overlay;\r\n\t\trenderer.RenderOptions.Bloom = Bloom;\r\n\t\trenderer.RenderOptions.AfterUI = AfterUi;\r\n\r\n\t\trenderer.Texturing = appearance;\r\n\r\n\t}\r\n}\r\n/// <summary>\r\n/// Applies curl noise force for organic, swirling motion\r\n/// </summary>\r\n[Title(\"Curl Noise\"), Category(\"Update\"), Icon(\"air\")]\r\npublic partial class CurlNoiseModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage => ModuleStage.Update;\r\n    \r\n    [Property, Range(0f, 1000f)]\r\n    [Description(\"Strength of the curl noise effect\")]\r\n    public FXParticleFloat Strength { get; set; } = 1.0f;\r\n    \r\n    [Property, Range(0.01f, 10f)]\r\n    [Description(\"Scale of the noise pattern - smaller values create tighter curls\")]\r\n    public FXParticleFloat Scale { get; set; } = 1.0f;\r\n    \r\n    [Property, Range(0f, 10f)]\r\n    [Description(\"Speed at which the noise pattern evolves over time\")]\r\n    public FXParticleFloat TimeScale { get; set; } = 1.0f;\r\n    \r\n    [Property]\r\n    [Description(\"Offset in the noise field\")]\r\n    public Vector3 Offset { get; set; } = Vector3.Zero;\r\n\r\n    public override void Execute(ParticleExecutionContext context)\r\n    {\r\n        // Not used - handled in UpdateParticle\r\n    }\r\n\r\n    public override void Initialize(ParticleExecutionContext context)\r\n    {\r\n        // No initialization needed\r\n    }\r\n\r\n    public void UpdateParticle(ParticleExecutionContext context, float delta)\r\n    {\r\n        var particle = context.Particle;\r\n        \r\n        // Sample position in noise field\r\n        var samplePos = (particle.Position + Offset) * Scale.GetValue( context.SystemComponent );\r\n        var time = context.Particle.Age * TimeScale;\r\n        \r\n        // Calculate curl noise using the curl of a 3D noise field\r\n        var curl = CalculateCurl(samplePos, time.GetValue( context.SystemComponent ));\r\n        \r\n        // Apply force\r\n        particle.Velocity += curl * Strength.GetValue( context.SystemComponent ) * delta * 10;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Calculate curl noise by taking the curl of a potential field\r\n    /// This creates divergence-free flow fields that look organic\r\n    /// </summary>\r\n    private Vector3 CalculateCurl(Vector3 pos, float time)\r\n    {\r\n        const float epsilon = 0.001f;\r\n        \r\n        // Sample the potential field at offset positions\r\n        // We need 6 samples to calculate the curl (derivatives in all directions)\r\n        \r\n        // dPz/dy - dPy/dz\r\n        float curlX = \r\n            (SamplePotential(pos + new Vector3(0, epsilon, 0), time).z - \r\n             SamplePotential(pos - new Vector3(0, epsilon, 0), time).z) -\r\n            (SamplePotential(pos + new Vector3(0, 0, epsilon), time).y - \r\n             SamplePotential(pos - new Vector3(0, 0, epsilon), time).y);\r\n        \r\n        // dPx/dz - dPz/dx\r\n        float curlY = \r\n            (SamplePotential(pos + new Vector3(0, 0, epsilon), time).x - \r\n             SamplePotential(pos - new Vector3(0, 0, epsilon), time).x) -\r\n            (SamplePotential(pos + new Vector3(epsilon, 0, 0), time).z - \r\n             SamplePotential(pos - new Vector3(epsilon, 0, 0), time).z);\r\n        \r\n        // dPy/dx - dPx/dy\r\n        float curlZ = \r\n            (SamplePotential(pos + new Vector3(epsilon, 0, 0), time).y - \r\n             SamplePotential(pos - new Vector3(epsilon, 0, 0), time).y) -\r\n            (SamplePotential(pos + new Vector3(0, epsilon, 0), time).x - \r\n             SamplePotential(pos - new Vector3(0, epsilon, 0), time).x);\r\n        \r\n        return new Vector3(curlX, curlY, curlZ) / (2.0f * epsilon);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Sample a 3D potential field using Perlin-like noise\r\n    /// </summary>\r\n    private Vector3 SamplePotential(Vector3 pos, float time)\r\n    {\r\n        // Create three offset noise samples for each component\r\n        // This creates a vector field from scalar noise functions\r\n        return new Vector3(\r\n            Noise3D(pos + new Vector3(0, 0, 0), time),\r\n            Noise3D(pos + new Vector3(31.416f, -47.853f, 12.793f), time),\r\n            Noise3D(pos + new Vector3(-17.737f, 86.214f, -59.482f), time)\r\n        );\r\n    }\r\n\r\n    /// <summary>\r\n    /// Simple 3D noise function using sine waves\r\n    /// You could replace this with proper Perlin/Simplex noise for better results\r\n    /// </summary>\r\n    private float Noise3D(Vector3 pos, float time)\r\n    {\r\n        // Combine multiple sine waves at different frequencies for pseudo-noise\r\n        var p = pos + new Vector3(time, time * 0.7f, time * 0.5f);\r\n        \r\n        float noise = 0;\r\n        noise += MathF.Sin(p.x * 1.0f + p.y * 1.3f) * 0.5f;\r\n        noise += MathF.Sin(p.y * 1.7f + p.z * 0.9f) * 0.3f;\r\n        noise += MathF.Sin(p.z * 2.1f + p.x * 1.1f) * 0.2f;\r\n        noise += MathF.Sin(p.x * 3.7f + p.y * 2.3f + p.z * 1.9f) * 0.15f;\r\n        \r\n        return noise;\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "Editor/FXParticleControlWidget.cs",
            "FileName": "FXParticleControlWidget.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 379493,
            "Code": "using Editor;\r\nusing fxbox;\r\nusing Sandbox;\r\n\r\nnamespace Editor;\r\n\r\n[CustomEditor(typeof(FXParticleFloat))]\r\npublic class FXParticleFloatControlWidget : ControlWidget\r\n{\r\n    public Color HighlightColor { get; set; }\r\n    public string Label { get; set; }\r\n\r\n    Layout ControlArea;\r\n    SerializedObject Target;\r\n    Button ModeSwitchButton;\r\n    Button ToggleButton;\r\n    \r\n    SerializedProperty ValueProperty;\r\n    SerializedObject ValueTarget;\r\n\r\n    public FXParticleFloatControlWidget(SerializedProperty property) : this(property, \"f\", Theme.Green)\r\n    {\r\n    }\r\n\r\n    public FXParticleFloatControlWidget(SerializedProperty property, string label, Color color) : base(property)\r\n    {\r\n        SetSizeMode(SizeMode.Ignore, SizeMode.Default);\r\n\r\n        if (!property.TryGetAsObject(out Target))\r\n            return;\r\n\r\n        Label = label;\r\n        HighlightColor = color;\r\n\r\n        Layout = Layout.Row();\r\n        Layout.Spacing = 3;\r\n\r\n        Layout.AddStretchCell();\r\n\r\n        // Add toggle button for UseParameter\r\n        ToggleButton = new Button();\r\n        ToggleButton.Text = \"P\";\r\n        ToggleButton.ToolTip = \"Toggle Parameter Mode\";\r\n        ToggleButton.FixedWidth = Theme.RowHeight;\r\n        ToggleButton.Pressed = () => ToggleParameterMode();\r\n        ToggleButton.OnPaintOverride = PaintToggleButton;\r\n        Layout.Add(ToggleButton);\r\n\r\n        ModeSwitchButton = new Button();\r\n        ModeSwitchButton.Text = \"Mode\";\r\n        ModeSwitchButton.OnPaintOverride = PaintButton;\r\n        ModeSwitchButton.Pressed = () => OpenPopup(ModeSwitchButton.ScreenRect);\r\n        ModeSwitchButton.FixedWidth = Theme.RowHeight;\r\n\r\n        Layout.Add(ModeSwitchButton);\r\n\r\n        ControlArea = Layout.AddRow(1);\r\n        ControlArea.Spacing = 2;\r\n\r\n        Target.OnPropertyChanged += (p) =>\r\n        {\r\n            if (p.Name == \"UseParameter\")\r\n            {\r\n                Rebuild();\r\n                return;\r\n            }\r\n            \r\n            if (!Target.GetProperty(\"UseParameter\").GetValue<bool>())\r\n            {\r\n                // Only rebuild for Value changes when not using parameters\r\n                ValueProperty = Target.GetProperty(\"Value\");\r\n                if (ValueProperty.TryGetAsObject(out ValueTarget))\r\n                {\r\n                    if (p.Name == \"Type\" || p.Name == \"Evaluation\")\r\n                    {\r\n                        Rebuild();\r\n                    }\r\n                }\r\n            }\r\n        };\r\n\r\n        Rebuild();\r\n    }\r\n\r\n    private void ToggleParameterMode()\r\n    {\r\n        var useParam = Target.GetProperty(\"UseParameter\");\r\n        useParam.SetValue(!useParam.GetValue<bool>());\r\n        Rebuild();\r\n    }\r\n\r\n    private bool PaintToggleButton()\r\n    {\r\n        Paint.Antialiasing = true;\r\n\r\n        var useParam = Target.GetProperty(\"UseParameter\").GetValue<bool>();\r\n\r\n        if (Paint.HasPressed)\r\n        {\r\n            Paint.SetBrushAndPen(Theme.ControlBackground.Lighten(0.3f));\r\n            Paint.DrawRect(Paint.LocalRect.Shrink(0.5f), Theme.ControlRadius);\r\n            Paint.Pen = useParam ? Theme.Blue.Lighten(0.4f) : Theme.TextControl.Lighten(0.4f);\r\n        }\r\n        else if (Paint.HasMouseOver)\r\n        {\r\n            Paint.SetBrushAndPen(Theme.TextControl.Lighten(0.2f).WithAlpha(0.1f));\r\n            Paint.DrawRect(Paint.LocalRect.Shrink(1), Theme.ControlRadius);\r\n            Paint.Pen = useParam ? Theme.Blue.Lighten(0.5f) : Theme.TextControl.Lighten(0.5f);\r\n        }\r\n        else\r\n        {\r\n            Paint.Pen = useParam ? Theme.Blue : Theme.TextControl.WithAlpha(0.5f);\r\n        }\r\n\r\n        Paint.DrawIcon(Paint.LocalRect, \"tune\", 15, TextFlag.Center);\r\n\r\n        return true;\r\n    }\r\n\r\n    private void OpenPopup(Rect parentRect)\r\n    {\r\n        // Only show popup if not in parameter mode\r\n        if (Target.GetProperty(\"UseParameter\").GetValue<bool>())\r\n            return;\r\n\r\n        ValueProperty = Target.GetProperty(\"Value\");\r\n        if (!ValueProperty.TryGetAsObject(out ValueTarget))\r\n            return;\r\n\r\n        var popup = new FXParticleFloatConfigPopup(ValueTarget, this);\r\n        popup.Position = parentRect.BottomRight;\r\n        popup.AdjustSize();\r\n        popup.Position -= new Vector2(popup.Width, 0);\r\n\r\n        popup.Show();\r\n        popup.ConstrainToScreen();\r\n    }\r\n\r\n    bool PaintButton()\r\n    {\r\n        Paint.Antialiasing = true;\r\n\r\n        var useParam = Target.GetProperty(\"UseParameter\").GetValue<bool>();\r\n        \r\n        // Disable button appearance if in parameter mode\r\n        if (useParam)\r\n        {\r\n            Paint.Pen = Theme.TextControl.WithAlpha(0.2f);\r\n            Paint.DrawIcon(Paint.LocalRect, \"block\", 11, TextFlag.Center);\r\n            return true;\r\n        }\r\n\r\n        ValueProperty = Target.GetProperty(\"Value\");\r\n        if (!ValueProperty.TryGetAsObject(out ValueTarget))\r\n            return true;\r\n\r\n        if (Paint.HasPressed)\r\n        {\r\n            Paint.SetBrushAndPen(Theme.ControlBackground.Lighten(0.3f));\r\n            Paint.DrawRect(Paint.LocalRect.Shrink(0.5f), Theme.ControlRadius);\r\n            Paint.Pen = Theme.TextControl.Lighten(0.4f);\r\n        }\r\n        else if (Paint.HasMouseOver)\r\n        {\r\n            Paint.SetBrushAndPen(Theme.TextControl.Lighten(0.2f).WithAlpha(0.1f));\r\n            Paint.DrawRect(Paint.LocalRect.Shrink(1), Theme.ControlRadius);\r\n            Paint.Pen = Theme.TextControl.Lighten(0.5f);\r\n        }\r\n        else\r\n        {\r\n            Paint.Pen = Theme.TextControl.WithAlpha(0.5f);\r\n        }\r\n\r\n        var type = ValueTarget.GetProperty(\"Type\").GetValue<ParticleFloat.ValueType>();\r\n        var eval = ValueTarget.GetProperty(\"Evaluation\").GetValue<ParticleFloat.EvaluationType>();\r\n\r\n        var icon = \"people\";\r\n        float iconSize = 15;\r\n\r\n        if (type == ParticleFloat.ValueType.Constant)\r\n        {\r\n            icon = \"radio_button_unchecked\";\r\n            Paint.Pen = Paint.Pen.WithAlpha(0.3f);\r\n            iconSize = 11;\r\n        }\r\n        else\r\n        {\r\n            if (eval == ParticleFloat.EvaluationType.Seed)\r\n            {\r\n                icon = \"scatter_plot\";\r\n            }\r\n\r\n            if (eval == ParticleFloat.EvaluationType.Life)\r\n            {\r\n                icon = \"play_arrow\";\r\n            }\r\n\r\n            if (eval == ParticleFloat.EvaluationType.Frame)\r\n            {\r\n                icon = \"casino\";\r\n            }\r\n        }\r\n\r\n        Paint.DrawIcon(Paint.LocalRect, icon, iconSize, TextFlag.Center);\r\n\r\n        return true;\r\n    }\r\n\r\n    void Rebuild()\r\n    {\r\n        ControlArea.Clear(true);\r\n\r\n        var useParam = Target.GetProperty(\"UseParameter\").GetValue<bool>();\r\n\r\n        if (useParam)\r\n        {\r\n            // Parameter mode - show parameter selector and multiplier\r\n            var paramName = Target.GetProperty(\"ParameterName\");\r\n            var multiplier = Target.GetProperty(\"Multiplier\");\r\n\r\n            var paramControl = new ParameterNameControlWidget(paramName);\r\n            ControlArea.Add(paramControl, 1);\r\n\r\n            var multControl = new FloatControlWidget(multiplier)\r\n            {\r\n                HighlightColor = Theme.Highlight,\r\n                Label = \"\u00d7\"\r\n            };\r\n            ControlArea.Add(multControl);\r\n        }\r\n        else\r\n        {\r\n            // Normal mode - show particle float controls\r\n            ValueProperty = Target.GetProperty(\"Value\");\r\n            if (ValueProperty.TryGetAsObject(out ValueTarget))\r\n            {\r\n                var type = ValueTarget.GetProperty(\"Type\").GetValue<ParticleFloat.ValueType>();\r\n                var eval = ValueTarget.GetProperty(\"Evaluation\").GetValue<ParticleFloat.EvaluationType>();\r\n                RebuildForType(type, eval);\r\n            }\r\n        }\r\n\r\n        Update();\r\n    }\r\n\r\n    void RebuildForType(ParticleFloat.ValueType type, ParticleFloat.EvaluationType eval)\r\n    {\r\n        ModeSwitchButton.ToolTip = GetTypeName(type, eval);\r\n\r\n        if (type == ParticleFloat.ValueType.Constant)\r\n        {\r\n            var control = new FloatControlWidget(ValueTarget.GetProperty(\"ConstantValue\"))\r\n            {\r\n                HighlightColor = HighlightColor,\r\n                Label = Label\r\n            };\r\n            ControlArea.Add(control);\r\n        }\r\n\r\n        if (type == ParticleFloat.ValueType.Range)\r\n        {\r\n            var controlA = new FloatControlWidget(ValueTarget.GetProperty(\"ConstantA\"))\r\n            {\r\n                HighlightColor = HighlightColor,\r\n                Label = Label\r\n            };\r\n            ControlArea.Add(controlA);\r\n\r\n            var controlB = new FloatControlWidget(ValueTarget.GetProperty(\"ConstantB\"))\r\n            {\r\n                HighlightColor = HighlightColor,\r\n                Label = Label\r\n            };\r\n            ControlArea.Add(controlB);\r\n        }\r\n\r\n        if (type == ParticleFloat.ValueType.Curve)\r\n        {\r\n            var controlA = new CurveControlWidget(ValueTarget.GetProperty(\"CurveA\"))\r\n            {\r\n                HighlightColor = HighlightColor\r\n            };\r\n            ControlArea.Add(controlA);\r\n        }\r\n\r\n        if (type == ParticleFloat.ValueType.CurveRange)\r\n        {\r\n            var controlA = new CurveRangeControlWidget(ValueTarget.GetProperty(\"CurveRange\"))\r\n            {\r\n                HighlightColor = HighlightColor\r\n            };\r\n            ControlArea.Add(controlA);\r\n        }\r\n    }\r\n\r\n    string GetTypeName(ParticleFloat.ValueType type, ParticleFloat.EvaluationType eval)\r\n    {\r\n        switch (type)\r\n        {\r\n            case ParticleFloat.ValueType.Constant:\r\n                return \"Constant value\";\r\n            case ParticleFloat.ValueType.Curve:\r\n                {\r\n                    switch (eval)\r\n                    {\r\n                        case ParticleFloat.EvaluationType.Seed:\r\n                            return \"Random from curve per particle\";\r\n                        case ParticleFloat.EvaluationType.Frame:\r\n                            return \"Random from curve\";\r\n                        case ParticleFloat.EvaluationType.Life:\r\n                            return \"Curve over lifetime\";\r\n                        default:\r\n                            return \"Unknown\";\r\n                    }\r\n                }\r\n            case ParticleFloat.ValueType.CurveRange:\r\n                switch (eval)\r\n                {\r\n                    case ParticleFloat.EvaluationType.Seed:\r\n                        return \"Random from range, per particle\";\r\n                    case ParticleFloat.EvaluationType.Frame:\r\n                        return \"Random from range (per frame)\";\r\n                    case ParticleFloat.EvaluationType.Life:\r\n                        return \"path between curve over lifetime, per particle\";\r\n                    default:\r\n                        return \"Unknown\";\r\n                }\r\n            case ParticleFloat.ValueType.Range:\r\n                {\r\n                    switch (eval)\r\n                    {\r\n                        case ParticleFloat.EvaluationType.Seed:\r\n                            return \"Between range, per particle\";\r\n                        case ParticleFloat.EvaluationType.Frame:\r\n                            return \"Random between range (per frame)\";\r\n                        case ParticleFloat.EvaluationType.Life:\r\n                            return \"Lerp between range over lifetime\";\r\n                        default:\r\n                            return \"Unknown\";\r\n                    }\r\n                }\r\n        }\r\n\r\n        return \"Unknown Combo\";\r\n    }\r\n\r\n    protected override void OnPaint()\r\n    {\r\n    }\r\n}\r\n\r\nfile class FXParticleFloatConfigPopup : PopupWidget\r\n{\r\n    SerializedObject SerializedObject;\r\n    SerializedProperty Type;\r\n    SerializedProperty Eval;\r\n\r\n    public FXParticleFloatConfigPopup(SerializedObject target, Widget parent) : base(parent)\r\n    {\r\n        Layout = Layout.Column();\r\n        Layout.Spacing = 8;\r\n        Layout.Margin = 16;\r\n\r\n        SerializedObject = target;\r\n        Type = target.GetProperty(\"Type\");\r\n        Eval = target.GetProperty(\"Evaluation\");\r\n\r\n        AddQuickModes(Type.GetValue<ParticleFloat.ValueType>(), Eval.GetValue<ParticleFloat.EvaluationType>());\r\n    }\r\n\r\n    void AddQuickModes(ParticleFloat.ValueType type, ParticleFloat.EvaluationType eval)\r\n    {\r\n        var grid = new GridLayout();\r\n        grid.Spacing = 8;\r\n\r\n        grid.AddCell(0, 0, MakeQuickMode(\"radio_button_unchecked\", \"Constant\", \"Value is constant. It stays the same. It doesn't change\", ParticleFloat.ValueType.Constant, ParticleFloat.EvaluationType.Life));\r\n        grid.AddCell(1, 0, MakeQuickMode(\"casino\", \"Random\", \"Choose a value between two constants every frame\", ParticleFloat.ValueType.Range, ParticleFloat.EvaluationType.Frame));\r\n\r\n        grid.AddCell(0, 1, MakeQuickMode(\"hdr_strong\", \"Range\", \"Choose a value between two constants\", ParticleFloat.ValueType.Range, ParticleFloat.EvaluationType.Seed));\r\n        grid.AddCell(1, 1, MakeQuickMode(\"animation\", \"Lerp\", \"Lerp between two values over the lifetime of the particle\", ParticleFloat.ValueType.Range, ParticleFloat.EvaluationType.Life));\r\n\r\n        grid.AddCell(0, 2, MakeQuickMode(\"show_chart\", \"Curve\", \"Get the value by querying a curve over the particle's lifetime\", ParticleFloat.ValueType.Curve, ParticleFloat.EvaluationType.Life));\r\n        grid.AddCell(1, 2, MakeQuickMode(\"area_chart\", \"Curve with Range\", \"Choose a path between two curves - over the lifetime of the particle\", ParticleFloat.ValueType.CurveRange, ParticleFloat.EvaluationType.Life));\r\n\r\n        Layout.Add(grid);\r\n    }\r\n\r\n    private Widget MakeQuickMode(string icon, string label, string description, ParticleFloat.ValueType t, ParticleFloat.EvaluationType e)\r\n    {\r\n        var b = new Widget();\r\n\r\n        b.Cursor = CursorShape.Finger;\r\n        b.Layout = new IconTitleDescriptionLayout(icon, label, description);\r\n        b.Layout.Margin = new Sandbox.UI.Margin(8, 4);\r\n        b.SetStyles(\"color: #ffffff;\");\r\n\r\n        bool isCurrent = t == Type.GetValue<ParticleFloat.ValueType>() && e == Eval.GetValue<ParticleFloat.EvaluationType>();\r\n\r\n        b.MouseClick += () =>\r\n        {\r\n            Type.SetValue(t);\r\n            Eval.SetValue(e);\r\n            Close();\r\n        };\r\n\r\n        b.OnPaintOverride = () =>\r\n        {\r\n            if (isCurrent || Paint.HasMouseOver)\r\n            {\r\n                Paint.SetBrushAndPen(Theme.Blue.Darken(0.5f).WithAlpha(0.5f));\r\n                Paint.DrawRect(Paint.LocalRect, 4);\r\n            }\r\n\r\n            return true;\r\n        };\r\n\r\n        return b;\r\n    }\r\n}\r\n\r\nfile class IconTitleDescriptionLayout : GridLayout\r\n{\r\n    public IconTitleDescriptionLayout(string icon, string title, string description)\r\n    {\r\n        VerticalSpacing = 0;\r\n        HorizontalSpacing = 8;\r\n\r\n        var iconLabel = AddCell(0, 0, new IconButton(icon) { Background = Color.Transparent, IconSize = 33, FixedSize = 40, TransparentForMouseEvents = true }, ySpan: 2);\r\n        var titleLabel = AddCell(1, 0, new Label(title));\r\n        var descLabel = AddCell(1, 1, new Label(description) { WordWrap = true });\r\n\r\n        titleLabel.SetStyles(\"font-size: 13px; font-family: Poppins; font-weight: bold;\");\r\n        descLabel.SetStyles(\"font-size: 9px; font-family: Poppins;\");\r\n        descLabel.SetEffectOpacity(0.5f);\r\n    }\r\n}\r\n\r\n[CustomEditor(typeof(FXParticleVector))]\r\npublic class FXParticleVectorControlWidget : ControlWidget\r\n{\r\n    private SerializedObject Target;\r\n    private Button ToggleButton;\r\n    private Layout ControlArea;\r\n\r\n    public FXParticleVectorControlWidget(SerializedProperty property) : base(property)\r\n    {\r\n        SetSizeMode(SizeMode.Ignore, SizeMode.Default);\r\n\r\n        if (!property.TryGetAsObject(out Target))\r\n            return;\r\n\r\n        Layout = Layout.Row();\r\n        Layout.Spacing = 3;\r\n\r\n        Layout.AddStretchCell();\r\n\r\n        // Add toggle button for UseParameter\r\n        ToggleButton = new Button();\r\n        ToggleButton.Text = \"P\";\r\n        ToggleButton.ToolTip = \"Toggle Parameter Mode\";\r\n        ToggleButton.FixedWidth = Theme.RowHeight;\r\n        ToggleButton.Pressed = () => ToggleParameterMode();\r\n        ToggleButton.OnPaintOverride = PaintToggleButton;\r\n        Layout.Add(ToggleButton);\r\n\r\n        ControlArea = Layout.AddRow(1);\r\n        ControlArea.Spacing = 2;\r\n\r\n        Target.OnPropertyChanged += (p) =>\r\n        {\r\n            if (p.Name == \"UseParameter\")\r\n            {\r\n                Rebuild();\r\n            }\r\n        };\r\n\r\n        Rebuild();\r\n    }\r\n\r\n    private void ToggleParameterMode()\r\n    {\r\n        var useParam = Target.GetProperty(\"UseParameter\");\r\n        useParam.SetValue(!useParam.GetValue<bool>());\r\n        Rebuild();\r\n    }\r\n\r\n    private bool PaintToggleButton()\r\n    {\r\n        Paint.Antialiasing = true;\r\n\r\n        var useParam = Target.GetProperty(\"UseParameter\").GetValue<bool>();\r\n\r\n        if (Paint.HasPressed)\r\n        {\r\n            Paint.SetBrushAndPen(Theme.ControlBackground.Lighten(0.3f));\r\n            Paint.DrawRect(Paint.LocalRect.Shrink(0.5f), Theme.ControlRadius);\r\n            Paint.Pen = useParam ? Theme.Blue.Lighten(0.4f) : Theme.TextControl.Lighten(0.4f);\r\n        }\r\n        else if (Paint.HasMouseOver)\r\n        {\r\n            Paint.SetBrushAndPen(Theme.TextControl.Lighten(0.2f).WithAlpha(0.1f));\r\n            Paint.DrawRect(Paint.LocalRect.Shrink(1), Theme.ControlRadius);\r\n            Paint.Pen = useParam ? Theme.Blue.Lighten(0.5f) : Theme.TextControl.Lighten(0.5f);\r\n        }\r\n        else\r\n        {\r\n            Paint.Pen = useParam ? Theme.Blue : Theme.TextControl.WithAlpha(0.5f);\r\n        }\r\n\r\n        Paint.DrawIcon(Paint.LocalRect, \"tune\", 15, TextFlag.Center);\r\n\r\n        return true;\r\n    }\r\n\r\n    void Rebuild()\r\n    {\r\n        ControlArea.Clear(true);\r\n\r\n        var useParam = Target.GetProperty(\"UseParameter\").GetValue<bool>();\r\n\r\n        if (useParam)\r\n        {\r\n            // Parameter mode - show parameter selector and multiplier\r\n            var paramName = Target.GetProperty(\"ParameterName\");\r\n            var multiplier = Target.GetProperty(\"Multiplier\");\r\n\r\n            var paramControl = new VectorParameterNameControlWidget(paramName);\r\n            ControlArea.Add(paramControl, 1);\r\n\r\n            var multControl = new FloatControlWidget(multiplier)\r\n            {\r\n                HighlightColor = Theme.Highlight,\r\n                Label = \"\u00d7\"\r\n            };\r\n            ControlArea.Add(multControl);\r\n        }\r\n        else\r\n        {\r\n            // Normal mode - show vector control\r\n            var valueProp = Target.GetProperty(\"Value\");\r\n            var control = ControlWidget.Create(valueProp);\r\n            if (control != null)\r\n            {\r\n                ControlArea.Add(control, 1);\r\n            }\r\n        }\r\n\r\n        Update();\r\n    }\r\n\r\n    protected override void OnPaint()\r\n    {\r\n    }\r\n}\r\n\r\n[CustomEditor(typeof(FXParticleColor))]\r\npublic class FXParticleColorControlWidget : ControlWidget\r\n{\r\n    private SerializedObject Target;\r\n    private Button ToggleButton;\r\n    private Layout ControlArea;\r\n\r\n    public FXParticleColorControlWidget(SerializedProperty property) : base(property)\r\n    {\r\n        SetSizeMode(SizeMode.Ignore, SizeMode.Default);\r\n\r\n        if (!property.TryGetAsObject(out Target))\r\n            return;\r\n\r\n        Layout = Layout.Row();\r\n        Layout.Spacing = 3;\r\n\r\n        Layout.AddStretchCell();\r\n\r\n        // Add toggle button for UseParameter\r\n        ToggleButton = new Button();\r\n        ToggleButton.Text = \"P\";\r\n        ToggleButton.ToolTip = \"Toggle Parameter Mode\";\r\n        ToggleButton.FixedWidth = Theme.RowHeight;\r\n        ToggleButton.Pressed = () => ToggleParameterMode();\r\n        ToggleButton.OnPaintOverride = PaintToggleButton;\r\n        Layout.Add(ToggleButton);\r\n\r\n        ControlArea = Layout.AddRow(1);\r\n        ControlArea.Spacing = 2;\r\n\r\n        Target.OnPropertyChanged += (p) =>\r\n        {\r\n            if (p.Name == \"UseParameter\")\r\n            {\r\n                Rebuild();\r\n            }\r\n        };\r\n\r\n        Rebuild();\r\n    }\r\n\r\n    private void ToggleParameterMode()\r\n    {\r\n        var useParam = Target.GetProperty(\"UseParameter\");\r\n        useParam.SetValue(!useParam.GetValue<bool>());\r\n        Rebuild();\r\n    }\r\n\r\n    private bool PaintToggleButton()\r\n    {\r\n        Paint.Antialiasing = true;\r\n\r\n        var useParam = Target.GetProperty(\"UseParameter\").GetValue<bool>();\r\n\r\n        if (Paint.HasPressed)\r\n        {\r\n            Paint.SetBrushAndPen(Theme.ControlBackground.Lighten(0.3f));\r\n            Paint.DrawRect(Paint.LocalRect.Shrink(0.5f), Theme.ControlRadius);\r\n            Paint.Pen = useParam ? Theme.Blue.Lighten(0.4f) : Theme.TextControl.Lighten(0.4f);\r\n        }\r\n        else if (Paint.HasMouseOver)\r\n        {\r\n            Paint.SetBrushAndPen(Theme.TextControl.Lighten(0.2f).WithAlpha(0.1f));\r\n            Paint.DrawRect(Paint.LocalRect.Shrink(1), Theme.ControlRadius);\r\n            Paint.Pen = useParam ? Theme.Blue.Lighten(0.5f) : Theme.TextControl.Lighten(0.5f);\r\n        }\r\n        else\r\n        {\r\n            Paint.Pen = useParam ? Theme.Blue : Theme.TextControl.WithAlpha(0.5f);\r\n        }\r\n\r\n        Paint.DrawIcon(Paint.LocalRect, \"tune\", 15, TextFlag.Center);\r\n\r\n        return true;\r\n    }\r\n\r\n    void Rebuild()\r\n    {\r\n        ControlArea.Clear(true);\r\n\r\n        var useParam = Target.GetProperty(\"UseParameter\").GetValue<bool>();\r\n\r\n        if (useParam)\r\n        {\r\n            // Parameter mode - show parameter selector and multiplier\r\n            var paramName = Target.GetProperty(\"ParameterName\");\r\n            var multiplier = Target.GetProperty(\"Multiplier\");\r\n\r\n            var paramControl = new ColorParameterNameControlWidget(paramName);\r\n            ControlArea.Add(paramControl, 1);\r\n\r\n            var multControl = new FloatControlWidget(multiplier)\r\n            {\r\n                HighlightColor = Theme.Highlight,\r\n                Label = \"\u00d7\"\r\n            };\r\n            ControlArea.Add(multControl);\r\n        }\r\n        else\r\n        {\r\n            // Normal mode - show color control\r\n            var valueProp = Target.GetProperty(\"Value\");\r\n            var control = ControlWidget.Create(valueProp);\r\n            if (control != null)\r\n            {\r\n                ControlArea.Add(control, 1);\r\n            }\r\n        }\r\n\r\n        Update();\r\n    }\r\n\r\n    protected override void OnPaint()\r\n    {\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "Code/ParticleResource.cs",
            "FileName": "ParticleResource.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 379493,
            "Code": "using System;\r\nusing Sandbox;\r\nusing System.Collections.Generic;\r\nusing System.ComponentModel;\r\nusing System.Linq;\r\nusing System.Text.Json;\r\nusing System.Text.Json.Serialization;\r\n\r\nnamespace fxbox;\r\n\r\n/// <summary>\r\n/// Particle system resource containing multiple emitters\r\n/// </summary>\r\n[AssetType(Name = \"Particle System\", Extension = \"fx\", Category = \"FX\", Flags = AssetTypeFlags.NoEmbedding)]\r\npublic class ParticleResource : GameResource\r\n{\r\n    public bool IsDirty { get; set; } = false;\r\n    \r\n    /// <summary>\r\n    /// All emitters in this particle system\r\n    /// </summary>\r\n    public List<ParticleEmitter> Emitters { get; set; } = new();\r\n\r\n    /// <summary>\r\n    /// Named float parameters\r\n    /// </summary>\r\n    [InlineEditor, DisplayName(\"FloatParameters\")] public List<FloatParameter> FloatParameters { get; set; } = new();\r\n    \r\n    /// <summary>\r\n    /// Named vector parameters\r\n    /// </summary>\r\n    [InlineEditor] public List<VectorParameter> VectorParameters { get; set; } = new();\r\n    \r\n    /// <summary>\r\n    /// Named color parameters\r\n    /// </summary>\r\n    [InlineEditor] public List<ColorParameter> ColorParameters { get; set; } = new();\r\n\r\n    /// <summary>\r\n    /// Global system properties\r\n    /// </summary>\r\n    public float Duration { get; set; } = 5.0f;\r\n    public bool Looping { get; set; } = true;\r\n\r\n    public int Version { get; set; } = 0;\r\n    \r\n    /// <summary>\r\n    /// Preview settings for the editor\r\n    /// </summary>\r\n    public ParticlePreviewSettings PreviewSettings { get; set; } = new();\r\n    \r\n    /// <summary>\r\n    /// Get a float parameter's default value by name\r\n    /// </summary>\r\n    public float GetParameterDefault(string name)\r\n    {\r\n        var param = FloatParameters.FirstOrDefault(p => p.Name == name);\r\n        return param?.DefaultValue ?? 0f;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Get a vector parameter's default value by name\r\n    /// </summary>\r\n    public Vector3 GetVectorParameterDefault(string name)\r\n    {\r\n        var param = VectorParameters.FirstOrDefault(p => p.Name == name);\r\n        return param?.DefaultValue ?? Vector3.Zero;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Get a color parameter's default value by name\r\n    /// </summary>\r\n    public ParticleGradient GetColorParameterDefault(string name)\r\n    {\r\n        var param = ColorParameters.FirstOrDefault(p => p.Name == name);\r\n        return param?.DefaultValue ?? Color.White;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Add a new float parameter\r\n    /// </summary>\r\n    public FloatParameter AddParameter(string name, float defaultValue = 1.0f)\r\n    {\r\n        var param = new FloatParameter\r\n        {\r\n            Name = name,\r\n            DefaultValue = defaultValue\r\n        };\r\n        FloatParameters.Add(param);\r\n        return param;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Add a new vector parameter\r\n    /// </summary>\r\n    public VectorParameter AddVectorParameter(string name, Vector3 defaultValue)\r\n    {\r\n        var param = new VectorParameter\r\n        {\r\n            Name = name,\r\n            DefaultValue = defaultValue\r\n        };\r\n        VectorParameters.Add(param);\r\n        return param;\r\n    }\r\n    \r\n    /// <summary>\r\n    /// Add a new color parameter\r\n    /// </summary>\r\n    public ColorParameter AddColorParameter(string name, Color defaultValue)\r\n    {\r\n        var param = new ColorParameter\r\n        {\r\n            Name = name,\r\n            DefaultValue = defaultValue\r\n        };\r\n        ColorParameters.Add(param);\r\n        return param;\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// Preview settings for the particle editor\r\n/// </summary>\r\npublic class ParticlePreviewSettings\r\n{\r\n    public bool ShowGround { get; set; } = true;\r\n    public bool ShowGrid { get; set; } = true;\r\n    public Color BackgroundColor { get; set; } = new Color(0.1f, 0.1f, 0.15f);\r\n    public float PlaybackSpeed { get; set; } = 1.0f;\r\n}\r\n/// <summary>\r\n/// A single particle emitter with its own spawn and update logic\r\n/// </summary>\r\npublic class ParticleEmitter\r\n{\r\n\tpublic string Name { get; set; } = \"Emitter\";\r\n\t[Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();\r\n\tpublic bool Enabled { get; set; } = true;\r\n\tpublic int MaxParticles { get; set; } = 1000;\r\n\r\n\t/// <summary>\r\n\t/// Seconds to wait before this emitter starts spawning - lets one emitter in the same\r\n\t/// system kick off a few seconds after another. Maps straight onto the native\r\n\t/// ParticleEffect.StartDelay, so it's handled by the engine's own particle system rather\r\n\t/// than anything FXBox has to gate itself.\r\n\t/// </summary>\r\n\tpublic float Delay { get; set; } = 0f;\r\n\r\n\t/// <summary>\r\n\t/// Overrides the system's own Duration for deciding when THIS emitter is finished\r\n\t/// (see FXBoxParticleController.IsFinished) - 0 means \"use the particle system's own\r\n\t/// Duration instead\", the same as before this existed. Lets one emitter in a system run\r\n\t/// longer or shorter than the rest without touching the system-wide Duration.\r\n\t/// </summary>\r\n\tpublic float Duration { get; set; } = 0f;\r\n\r\n\t/// <summary>\r\n\t/// Modules that run when spawning particles\r\n\t/// </summary>\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List<ParticleModule> SpawnModules { get; set; } = new();\r\n\r\n\t/// <summary>\r\n\t/// Modules that run once when a particle is created\r\n\t/// </summary>\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List<ParticleModule> InitializeModules { get; set; } = new();\r\n\r\n\t/// <summary>\r\n\t/// Modules that run every frame for each particle\r\n\t/// </summary>\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List<ParticleModule> UpdateModules { get; set; } = new();\r\n\r\n\t/// <summary>\r\n\t/// Modules that control how particles are rendered\r\n\t/// </summary>\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List<ParticleModule> RenderModules { get; set; } = new();\r\n}\r\n/// <summary>\r\n/// Base class for all particle modules\r\n/// </summary>\r\npublic abstract class ParticleModule\r\n{\r\n    [Hide] public string Identifier { get; set; } = Guid.NewGuid().ToString();\r\n    [Hide] public string Name { get; set; }\r\n    [Hide] public bool Enabled { get; set; } = true;\r\n\r\n    /// <summary>\r\n    /// What stage this module belongs to\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public abstract ModuleStage Stage { get; }\r\n\r\n    /// <summary>\r\n    /// Execute this module\r\n    /// </summary>\r\n    public abstract void Execute(ParticleExecutionContext context);\r\n\r\n    public abstract void Initialize( ParticleExecutionContext context );\r\n}\r\n\r\n/// <summary>\r\n/// JSON converter for List of ParticleModule\r\n/// </summary>\r\npublic class ParticleModuleListConverter : JsonConverter<List<ParticleModule>>\r\n{\r\n    public override List<ParticleModule> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\r\n    {\r\n        var list = new List<ParticleModule>();\r\n        \r\n        if (reader.TokenType != JsonTokenType.StartArray)\r\n            throw new JsonException(\"Expected start of array\");\r\n        \r\n        while (reader.Read())\r\n        {\r\n            if (reader.TokenType == JsonTokenType.EndArray)\r\n                break;\r\n                \r\n            using (var doc = JsonDocument.ParseValue(ref reader))\r\n            {\r\n                var root = doc.RootElement;\r\n                \r\n                // Get the type name\r\n                if (!root.TryGetProperty(\"$type\", out var typeProperty))\r\n                {\r\n                    Log.Warning(\"Missing $type property for ParticleModule\");\r\n                    continue;\r\n                }\r\n                \r\n                var typeName = typeProperty.GetString();\r\n                var type = TypeLibrary.GetType(typeName)?.TargetType;\r\n                \r\n                if (type == null)\r\n                {\r\n                    Log.Warning($\"Unknown module type: {typeName}\");\r\n                    continue;\r\n                }\r\n                \r\n                // Deserialize to the specific type\r\n                var json = root.GetRawText();\r\n                var module = (ParticleModule)JsonSerializer.Deserialize(json, type, options);\r\n                if (module != null)\r\n                {\r\n                    list.Add(module);\r\n                }\r\n            }\r\n        }\r\n        \r\n        return list;\r\n    }\r\n\r\n    public override void Write(Utf8JsonWriter writer, List<ParticleModule> value, JsonSerializerOptions options)\r\n    {\r\n        writer.WriteStartArray();\r\n        \r\n        foreach (var module in value)\r\n        {\r\n            if (module == null) continue;\r\n            \r\n            writer.WriteStartObject();\r\n            \r\n            // Write the type information\r\n            writer.WriteString(\"$type\", module.GetType().FullName);\r\n            \r\n            // Serialize the module\r\n            var json = JsonSerializer.Serialize(module, module.GetType(), options);\r\n            using (var doc = JsonDocument.Parse(json))\r\n            {\r\n                foreach (var property in doc.RootElement.EnumerateObject())\r\n                {\r\n                    property.WriteTo(writer);\r\n                }\r\n            }\r\n            \r\n            writer.WriteEndObject();\r\n        }\r\n        \r\n        writer.WriteEndArray();\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": ".obj/__compiler_extra.cs",
            "FileName": "__compiler_extra.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 379493,
            "Code": "global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonTitle\", \"FXBox\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"AddonIdent\", \"fxbox\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"OrgIdent\", \"stellawisps\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"Ident\", \"stellawisps.fxbox\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineVersion\", \"29\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"EngineMinorVersion\", \"1\" )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \".NETCoreApp,Version=v9.0\", FrameworkDisplayName = \".NET 9.0\" )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \"CompileTime\", \"2026-09-10T16:47:44.5430399Z\" )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\"0.0.212.0\")]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\"0.0.212.0\")]"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "Code/ParticleValues.cs",
            "FileName": "ParticleValues.cs",
            "PackageType": "library",
            "CodeKind": "Game",
            "AssetVersionId": 379493,
            "Code": "using System;\r\nusing Sandbox;\r\nusing System.Linq;\r\n\r\nnamespace fxbox;\r\npublic class FXParticleFloat\r\n{\r\n    [Property] public bool UseParameter { get; set; } = false;\r\n    \r\n    [Property, ShowIf(nameof(UseParameter), false)]\r\n    public ParticleFloat Value { get; set; }\r\n    \r\n    [Property, ShowIf(nameof(UseParameter), true)]\r\n    [Description(\"Select a parameter from the system\")]\r\n    public string ParameterName { get; set; }\r\n    \r\n    [Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]\r\n    [Description(\"Multiplier applied to the parameter value\")]\r\n    public float Multiplier { get; set; } = 1.0f;\r\n    \r\n    public float GetValue(FXBoxNativeParticleSystem systemComponent = null)\r\n    {\r\n\t    if (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)\r\n\t    {\r\n\t\t    return systemComponent.GetFloatParameter(ParameterName) * Multiplier;\r\n\t    }\r\n\t    \r\n\t    var result = Value.Evaluate(Random.Shared.Float(), 3f);\r\n\t  \r\n\t    return result;\r\n    }\r\n    \r\n    public ParticleFloat ToParticleFloat(FXBoxNativeParticleSystem systemComponent = null)\r\n    {\r\n        if (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)\r\n        {\r\n            // Get the instance-specific value (override or default)\r\n            float value = systemComponent.GetFloatParameter(ParameterName) * Multiplier;\r\n            \r\n            return new ParticleFloat()\r\n            {\r\n                Type = ParticleFloat.ValueType.Constant,\r\n                ConstantValue = value,\r\n                Evaluation = ParticleFloat.EvaluationType.Seed\r\n            };\r\n        }\r\n        \r\n        return Value;\r\n    }\r\n    \r\n    // ==================== OPERATORS ====================\r\n    \r\n    \r\n    public static FXParticleFloat operator *(float a, FXParticleFloat b)\r\n    {\r\n        return b * a; // Commutative\r\n    }\r\n    \r\n    // Division operators\r\n    public static FXParticleFloat operator /(FXParticleFloat a, float b)\r\n{\r\n    if (a == null)\r\n    {\r\n        Log.Warning(\"Division: a is null\");\r\n        return null;\r\n    }\r\n    \r\n    if (b == 0 || MathF.Abs(b) < 0.0001f)\r\n    {\r\n        Log.Warning($\"Division by zero or very small number ({b}) in FXParticleFloat\");\r\n        return a;\r\n    }\r\n    \r\n  \r\n    var result = a * (1.0f / b);\r\n   \r\n    return result;\r\n}\r\n\r\npublic static FXParticleFloat operator *(FXParticleFloat a, float b)\r\n{\r\n    if (a == null)\r\n    {\r\n        Log.Warning(\"Multiplication: a is null\");\r\n        return null;\r\n    }\r\n    \r\n    var result = new FXParticleFloat();\r\n    \r\n    if (a.UseParameter)\r\n    {\r\n        result.UseParameter = true;\r\n        result.ParameterName = a.ParameterName;\r\n        result.Multiplier = a.Multiplier * b;\r\n        result.Value = new ParticleFloat()\r\n        {\r\n            Type = ParticleFloat.ValueType.Constant,\r\n            ConstantValue = 0f,\r\n            Evaluation = ParticleFloat.EvaluationType.Seed\r\n        };\r\n    }\r\n    else\r\n    {\r\n        result.UseParameter = false;\r\n        result.Value = ScaleParticleFloat(a.Value, b);\r\n    }\r\n    \r\n    return result;\r\n}\r\n\r\nprivate static ParticleFloat ScaleParticleFloat(ParticleFloat pf, float scale)\r\n{\r\n    \r\n    var result = new ParticleFloat();\r\n    result.Type = pf.Type;\r\n    result.Evaluation = pf.Evaluation;\r\n    \r\n    // Copy Constants FIRST, before setting individual values\r\n    // (or don't copy it at all since we're setting the values manually)\r\n    // result.Constants = pf.Constants;\r\n    \r\n    switch (pf.Type)\r\n    {\r\n        case ParticleFloat.ValueType.Constant:\r\n            result.ConstantValue = pf.ConstantValue * scale;\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.Range:\r\n            result.ConstantA = pf.ConstantA * scale;\r\n            result.ConstantB = pf.ConstantB * scale;\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.Curve:\r\n            result.CurveA = ScaleCurve(pf.CurveA, scale);\r\n            result.CurveB = ScaleCurve(pf.CurveB, scale);\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.CurveRange:\r\n            result.CurveRange = ScaleCurveRange(pf.CurveRange, scale);\r\n            break;\r\n    }\r\n    \r\n    // DON'T copy Constants here - it overwrites our values!\r\n    // result.Constants = pf.Constants;\r\n    \r\n    return result;\r\n}\r\n\r\nprivate static ParticleFloat OffsetParticleFloat(ParticleFloat pf, float offset)\r\n{\r\n    var result = new ParticleFloat();\r\n    result.Type = pf.Type;\r\n    result.Evaluation = pf.Evaluation;\r\n    \r\n    // DON'T copy Constants - it will overwrite our values\r\n    // result.Constants = pf.Constants;\r\n    \r\n    switch (pf.Type)\r\n    {\r\n        case ParticleFloat.ValueType.Constant:\r\n            result.ConstantValue = pf.ConstantValue + offset;\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.Range:\r\n            result.ConstantA = pf.ConstantA + offset;\r\n            result.ConstantB = pf.ConstantB + offset;\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.Curve:\r\n            result.CurveA = OffsetCurve(pf.CurveA, offset);\r\n            result.CurveB = OffsetCurve(pf.CurveB, offset);\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.CurveRange:\r\n            result.CurveRange = OffsetCurveRange(pf.CurveRange, offset);\r\n            break;\r\n    }\r\n    \r\n    // DON'T copy Constants here!\r\n    // result.Constants = pf.Constants;\r\n    \r\n    return result;\r\n}\r\n    \r\n    private static Curve ScaleCurve(Curve curve, float scale)\r\n    {\r\n        var newFrames = curve.Frames.Select(frame => \r\n            new Curve.Frame(frame.Time, frame.Value * scale)).ToArray();\r\n        \r\n        return new Curve(newFrames);\r\n    }\r\n    \r\n    private static Curve OffsetCurve(Curve curve, float offset)\r\n    {\r\n        var newFrames = curve.Frames.Select(frame => \r\n            new Curve.Frame(frame.Time, frame.Value + offset)).ToArray();\r\n        \r\n        return new Curve(newFrames);\r\n    }\r\n    \r\n    private static CurveRange ScaleCurveRange(CurveRange range, float scale)\r\n    {\r\n        return new CurveRange\r\n        (\r\n           ScaleCurve(range.A, scale),\r\n            ScaleCurve(range.B, scale)\r\n        );\r\n    }\r\n    \r\n    private static CurveRange OffsetCurveRange(CurveRange range, float offset)\r\n    {\r\n        return new CurveRange\r\n        (\r\n           OffsetCurve(range.A, offset),\r\n            OffsetCurve(range.B, offset)\r\n        );\r\n    }\r\n    \r\n    // ==================== IMPLICIT CONVERSIONS ====================\r\n    \r\n    public static implicit operator FXParticleFloat(float v)\r\n    {\r\n        var fxParticle = new FXParticleFloat();\r\n        fxParticle.Value = new ParticleFloat()\r\n        {\r\n            Type = ParticleFloat.ValueType.Constant,\r\n            ConstantValue = v,\r\n            Evaluation = ParticleFloat.EvaluationType.Seed\r\n        };\r\n        return fxParticle;\r\n    }\r\n\r\n    // ==================== CONSTRUCTORS ====================\r\n    \r\n    public FXParticleFloat()\r\n    {\r\n        var particleFloat = new ParticleFloat();\r\n        particleFloat.Type = ParticleFloat.ValueType.Constant;\r\n        particleFloat.ConstantValue = 0.0f;\r\n        particleFloat.Evaluation = ParticleFloat.EvaluationType.Seed;\r\n        particleFloat.CurveA = new Curve();\r\n        particleFloat.CurveB = new Curve();\r\n        particleFloat.Constants = new Vector4();\r\n        this.Value = particleFloat;\r\n    }\r\n\r\n    public FXParticleFloat(float a, float b)\r\n    {\r\n        var particleFloat = new ParticleFloat();\r\n        particleFloat.Type = ParticleFloat.ValueType.Range;\r\n        particleFloat.ConstantA = a;\r\n        particleFloat.ConstantB = b;\r\n        particleFloat.Evaluation = ParticleFloat.EvaluationType.Seed;\r\n        particleFloat.CurveA = new Curve();\r\n        particleFloat.CurveB = new Curve();\r\n        particleFloat.Constants = new Vector4();\r\n        this.Value = particleFloat;\r\n    }\r\n}\r\n\r\npublic class FXParticleVector\r\n{\r\n\t[Property] public bool UseParameter { get; set; } = false;\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), false)]\r\n\tpublic ParticleVector3 Value { get; set; } = Vector3.Zero;\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), true)]\r\n\t[Description(\"Select a vector parameter from the system\")]\r\n\tpublic string ParameterName { get; set; }\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]\r\n\t[Description(\"Multiplier applied to the parameter value\")]\r\n\tpublic float Multiplier { get; set; } = 1.0f;\r\n    \r\n\tpublic Vector3 GetValue(Particle particle,FXBoxNativeParticleSystem systemComponent = null)\r\n\t{\r\n\t\tif (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)\r\n\t\t{\r\n\t\t\treturn systemComponent.GetVectorParameter(ParameterName) * Multiplier;\r\n\t\t}\r\n\r\n\t\tif ( particle == null )\r\n\t\t{\r\n\t\t\treturn Value.Evaluate( Time.Delta, 0, 0, 0 );\r\n\t\t}\r\n\t\treturn Value.Evaluate( Time.Delta,particle.Rand(1),particle.Rand(2),particle.Rand(3) );\r\n\t}\r\n    \r\n\tpublic static implicit operator FXParticleVector(Vector3 v)\r\n\t{\r\n\t\treturn new FXParticleVector { Value = v };\r\n\t}\r\n\r\n\tpublic FXParticleVector()\r\n\t{\r\n\t\tValue = Vector3.Zero;\r\n\t}\r\n\r\n\tpublic FXParticleVector(Vector3 value)\r\n\t{\r\n\t\tValue = value;\r\n\t}\r\n    \r\n\tpublic FXParticleVector(float x, float y, float z)\r\n\t{\r\n\t\tValue = new Vector3(x, y, z);\r\n\t}\r\n}\r\n\r\npublic class FXParticleColor\r\n{\r\n\t[Property] public bool UseParameter { get; set; } = false;\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), false)]\r\n\tpublic ParticleGradient Value { get; set; } = Color.White;\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), true)]\r\n\t[Description(\"Select a color parameter from the system\")]\r\n\tpublic string ParameterName { get; set; }\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]\r\n\t[Description(\"Multiplier applied to the parameter value (affects RGB)\")]\r\n\tpublic float Multiplier { get; set; } = 1.0f;\r\n    \r\n\tpublic ParticleGradient GetValue(FXBoxNativeParticleSystem systemComponent = null)\r\n\t{\r\n\t\tif (UseParameter && !string.IsNullOrEmpty(ParameterName) && systemComponent != null)\r\n\t\t{\r\n\t\t\tvar color = systemComponent.GetColorParameter(ParameterName);\r\n            \r\n\t\t\t// Apply multiplier to RGB components\r\n\t\t\tif (Multiplier != 1.0f)\r\n\t\t\t{\r\n\t\t\t\treturn color;\r\n\t\t\t}\r\n            \r\n\t\t\treturn color;\r\n\t\t}\r\n        \r\n\t\treturn Value;\r\n\t}\r\n    \r\n\tpublic static implicit operator FXParticleColor(Color c)\r\n\t{\r\n\t\treturn new FXParticleColor { Value = c };\r\n\t}\r\n\r\n\tpublic FXParticleColor()\r\n\t{\r\n\t\tValue = Color.White;\r\n\t}\r\n\r\n\tpublic FXParticleColor(Color value)\r\n\t{\r\n\t\tValue = value;\r\n\t}\r\n    \r\n\tpublic FXParticleColor(float r, float g, float b, float a = 1.0f)\r\n\t{\r\n\t\tValue = new Color(r, g, b, a);\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "stellawisps.fxbox",
            "Path": "Editor/FXParticlePropertyControlWidget.cs",
            "FileName": "FXParticlePropertyControlWidget.cs",
            "PackageType": "library",
            "CodeKind": "Editor",
            "AssetVersionId": 379493,
            "Code": "using Editor;\r\nusing Sandbox;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing fxbox;\r\nusing fxbox.Graph;\r\n\r\nnamespace Editor;\r\n\r\n/// <summary>\r\n/// Custom control for parameter name selection with dropdown\r\n/// </summary>\r\npublic class ParameterNameControlWidget : ControlWidget\r\n{\r\n    private SerializedProperty _property;\r\n    private ComboBox _dropdown;\r\n    private List<FloatParameter> _availableParameters;\r\n\r\n    public ParameterNameControlWidget(SerializedProperty property) : base(property)\r\n    {\r\n        _property = property;\r\n        \r\n        SetSizeMode(SizeMode.Ignore, SizeMode.Default);\r\n\r\n        Layout = Layout.Row();\r\n        Layout.Spacing = 3;\r\n\r\n        // Get available parameters from the currently editing resource\r\n        _availableParameters = FXBoxEditor.CurrentEditingResource?.FloatParameters ?? new List<FloatParameter>();\r\n\r\n        _dropdown = new ComboBox(this);\r\n        _dropdown.MinimumWidth = 150;\r\n        \r\n        PopulateDropdown();\r\n        \r\n        // Set current value\r\n        var currentValue = property.GetValue<string>();\r\n        if (!string.IsNullOrEmpty(currentValue))\r\n        {\r\n            SelectParameter(currentValue);\r\n        }\r\n\r\n        _dropdown.ItemChanged += OnValueChanged;\r\n        //_dropdown.On += OnValueChanged;\r\n\r\n        Layout.Add(_dropdown, 1);\r\n    }\r\n\r\n    private void PopulateDropdown()\r\n    {\r\n        _dropdown.Clear();\r\n        \r\n        if (_availableParameters == null || _availableParameters.Count == 0)\r\n        {\r\n            _dropdown.AddItem(\"(No Parameters Available)\");\r\n            _dropdown.Enabled = false;\r\n            return;\r\n        }\r\n\r\n        _dropdown.AddItem(\"(None)\");\r\n        \r\n        foreach (var param in _availableParameters.OrderBy(p => p.Name))\r\n        {\r\n            _dropdown.AddItem($\"{param.Name} (default: {param.DefaultValue})\");\r\n        }\r\n        \r\n        _dropdown.Enabled = true;\r\n    }\r\n\r\n    private void SelectParameter(string parameterName)\r\n    {\r\n        if (string.IsNullOrEmpty(parameterName))\r\n        {\r\n            _dropdown.CurrentIndex = 0;\r\n            return;\r\n        }\r\n\r\n        // Find the index by matching parameter name\r\n        var orderedParams = _availableParameters.OrderBy(p => p.Name).ToList();\r\n        for (int i = 0; i < orderedParams.Count; i++)\r\n        {\r\n            if (orderedParams[i].Name == parameterName)\r\n            {\r\n                _dropdown.CurrentIndex = i + 1; // +1 because of \"(None)\" at index 0\r\n                return;\r\n            }\r\n        }\r\n    }\r\n\r\n    private new void OnValueChanged()\r\n    {\r\n        var selectedIndex = _dropdown.CurrentIndex;\r\n        \r\n        if (selectedIndex <= 0)\r\n        {\r\n            // \"(None)\" selected\r\n            _property.SetValue(\"\");\r\n            return;\r\n        }\r\n        \r\n        // Get the parameter name from the selected item\r\n        var orderedParams = _availableParameters.OrderBy(p => p.Name).ToList();\r\n        if (selectedIndex - 1 < orderedParams.Count)\r\n        {\r\n            var selectedParam = orderedParams[selectedIndex - 1];\r\n            _property.SetValue(selectedParam.Name);\r\n        }\r\n    }\r\n\r\n    protected override void OnPaint()\r\n    {\r\n        // No custom painting\r\n    }\r\n}\r\n\r\npublic class VectorParameterNameControlWidget : ControlWidget\r\n{\r\n    private SerializedProperty _property;\r\n    private ComboBox _dropdown;\r\n    private List<VectorParameter> _availableParameters;\r\n\r\n    public VectorParameterNameControlWidget(SerializedProperty property) : base(property)\r\n    {\r\n        _property = property;\r\n        \r\n        SetSizeMode(SizeMode.Ignore, SizeMode.Default);\r\n\r\n        Layout = Layout.Row();\r\n        Layout.Spacing = 3;\r\n\r\n        _availableParameters = FXBoxEditor.CurrentEditingResource?.VectorParameters ?? new List<VectorParameter>();\r\n\r\n        _dropdown = new ComboBox(this);\r\n        _dropdown.MinimumWidth = 150;\r\n        \r\n        PopulateDropdown();\r\n        \r\n        var currentValue = property.GetValue<string>();\r\n        if (!string.IsNullOrEmpty(currentValue))\r\n        {\r\n            SelectParameter(currentValue);\r\n        }\r\n\r\n        _dropdown.ItemChanged += OnValueChanged;\r\n\r\n        Layout.Add(_dropdown, 1);\r\n    }\r\n\r\n    private void PopulateDropdown()\r\n    {\r\n        _dropdown.Clear();\r\n        \r\n        if (_availableParameters == null || _availableParameters.Count == 0)\r\n        {\r\n            _dropdown.AddItem(\"(No Vector Parameters Available)\");\r\n            _dropdown.Enabled = false;\r\n            return;\r\n        }\r\n\r\n        _dropdown.AddItem(\"(None)\");\r\n        \r\n        foreach (var param in _availableParameters.OrderBy(p => p.Name))\r\n        {\r\n            _dropdown.AddItem($\"{param.Name} (default: {param.DefaultValue})\");\r\n        }\r\n        \r\n        _dropdown.Enabled = true;\r\n    }\r\n\r\n    private void SelectParameter(string parameterName)\r\n    {\r\n        if (string.IsNullOrEmpty(parameterName))\r\n        {\r\n            _dropdown.CurrentIndex = 0;\r\n            return;\r\n        }\r\n\r\n        var orderedParams = _availableParameters.OrderBy(p => p.Name).ToList();\r\n        for (int i = 0; i < orderedParams.Count; i++)\r\n        {\r\n            if (orderedParams[i].Name == parameterName)\r\n            {\r\n                _dropdown.CurrentIndex = i + 1;\r\n                return;\r\n            }\r\n        }\r\n    }\r\n\r\n    private new void OnValueChanged()\r\n    {\r\n        var selectedIndex = _dropdown.CurrentIndex;\r\n        \r\n        if (selectedIndex <= 0)\r\n        {\r\n            _property.SetValue(\"\");\r\n            return;\r\n        }\r\n        \r\n        var orderedParams = _availableParameters.OrderBy(p => p.Name).ToList();\r\n        if (selectedIndex - 1 < orderedParams.Count)\r\n        {\r\n            var selectedParam = orderedParams[selectedIndex - 1];\r\n            _property.SetValue(selectedParam.Name);\r\n        }\r\n    }\r\n\r\n    protected override void OnPaint()\r\n    {\r\n    }\r\n}\r\n\r\npublic class ColorParameterNameControlWidget : ControlWidget\r\n{\r\n    private SerializedProperty _property;\r\n    private ComboBox _dropdown;\r\n    private List<ColorParameter> _availableParameters;\r\n\r\n    public ColorParameterNameControlWidget(SerializedProperty property) : base(property)\r\n    {\r\n        _property = property;\r\n        \r\n        SetSizeMode(SizeMode.Ignore, SizeMode.Default);\r\n\r\n        Layout = Layout.Row();\r\n        Layout.Spacing = 3;\r\n\r\n        _availableParameters = FXBoxEditor.CurrentEditingResource?.ColorParameters ?? new List<ColorParameter>();\r\n\r\n        _dropdown = new ComboBox(this);\r\n        _dropdown.MinimumWidth = 150;\r\n        \r\n        PopulateDropdown();\r\n        \r\n        var currentValue = property.GetValue<string>();\r\n        if (!string.IsNullOrEmpty(currentValue))\r\n        {\r\n            SelectParameter(currentValue);\r\n        }\r\n\r\n        _dropdown.ItemChanged += OnValueChanged;\r\n\r\n        Layout.Add(_dropdown, 1);\r\n    }\r\n\r\n    private void PopulateDropdown()\r\n    {\r\n        _dropdown.Clear();\r\n        \r\n        if (_availableParameters == null || _availableParameters.Count == 0)\r\n        {\r\n            _dropdown.AddItem(\"(No Color Parameters Available)\");\r\n            _dropdown.Enabled = false;\r\n            return;\r\n        }\r\n\r\n        _dropdown.AddItem(\"(None)\");\r\n        \r\n        foreach (var param in _availableParameters.OrderBy(p => p.Name))\r\n        {\r\n            _dropdown.AddItem($\"{param.Name}\");\r\n        }\r\n        \r\n        _dropdown.Enabled = true;\r\n    }\r\n\r\n    private void SelectParameter(string parameterName)\r\n    {\r\n        if (string.IsNullOrEmpty(parameterName))\r\n        {\r\n            _dropdown.CurrentIndex = 0;\r\n            return;\r\n        }\r\n\r\n        var orderedParams = _availableParameters.OrderBy(p => p.Name).ToList();\r\n        for (int i = 0; i < orderedParams.Count; i++)\r\n        {\r\n            if (orderedParams[i].Name == parameterName)\r\n            {\r\n                _dropdown.CurrentIndex = i + 1;\r\n                return;\r\n            }\r\n        }\r\n    }\r\n\r\n    private new void OnValueChanged()\r\n    {\r\n        var selectedIndex = _dropdown.CurrentIndex;\r\n        \r\n        if (selectedIndex <= 0)\r\n        {\r\n            _property.SetValue(\"\");\r\n            return;\r\n        }\r\n        \r\n        var orderedParams = _availableParameters.OrderBy(p => p.Name).ToList();\r\n        if (selectedIndex - 1 < orderedParams.Count)\r\n        {\r\n            var selectedParam = orderedParams[selectedIndex - 1];\r\n            _property.SetValue(selectedParam.Name);\r\n        }\r\n    }\r\n\r\n    protected override void OnPaint()\r\n    {\r\n    }\r\n}\r\n"
        }
    ]
}