{"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/// \u003Csummary\u003E\r\n/// Stage when a particle module executes\r\n/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// Context passed to particle modules during execution\r\n/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// Controls spawn rate over time\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Spawn Rate\u0022), Category(\u0022Spawn\u0022), Icon(\u0022speed\u0022)]\r\npublic partial class SpawnRateModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial particle stretch\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Particle Stretch\u0022), Category(\u0022Initialize\u0022), Icon(\u0022photo_size_select_small\u0022)]\r\npublic partial class ParticleStretchModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Controls spawn rate per unit\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Spawn Rate Over Distance\u0022), Category(\u0022Spawn\u0022), Icon(\u0022speed\u0022)]\r\npublic partial class SpawnRateOverDistanceModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Spawns particles in a burst\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Spawn Burst\u0022), Category(\u0022Spawn\u0022), Icon(\u0022auto_awesome\u0022)]\r\npublic partial class SpawnBurstModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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 \u0022spawn this many now\u0022 value, not a continuous per-tick one like\r\n    // SpawnRateModule\u0027s 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 \u0022Initialize-only, empty Execute\u0022 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/// \u003Csummary\u003E\r\n/// Sets initial position based on shape\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Position\u0022), Category(\u0022Initialize\u0022), Icon(\u0022place\u0022)]\r\npublic partial class InitializePositionModule : ParticleModule, IParticleComponentCreator\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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 =\u003E Shape == SpawnShape.Sphere || Shape == SpawnShape.Circle || Shape == SpawnShape.Cone;\r\n    [Hide] public bool ShowBoxSize =\u003E Shape == SpawnShape.Box;\r\n    [Hide] public bool ShowConeAngle =\u003E Shape == SpawnShape.Cone;\r\n    [Hide] public bool ShowLine =\u003E 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 =\u003E Vector3.Zero,\r\n\t\t\t    SpawnShape.Sphere =\u003E GetSpherePosition(),\r\n\t\t\t    SpawnShape.Box =\u003E GetBoxPosition(),\r\n\t\t\t    SpawnShape.Cone =\u003E GetConePosition(),\r\n\t\t\t    SpawnShape.Circle =\u003E GetCirclePosition(),\r\n\t\t\t    SpawnShape.Line =\u003E GetLinePosition(),\r\n\t\t\t    _ =\u003E 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 \u002B= pos;\r\n\t    }\r\n    }\r\n    public void CreateComponent(GameObject go)\r\n    {\r\n\t    var pointEmitter = go.AddComponent\u003CParticleSphereEmitter\u003E();\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 =\u003E new Vector3(-halfSize.x, MathX.Lerp(-halfSize.y, halfSize.y, u), MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                1 =\u003E new Vector3(halfSize.x, MathX.Lerp(-halfSize.y, halfSize.y, u), MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                2 =\u003E new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), -halfSize.y, MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                3 =\u003E new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), halfSize.y, MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                4 =\u003E new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), MathX.Lerp(-halfSize.y, halfSize.y, v), -halfSize.z),\r\n                _ =\u003E 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/// \u003Csummary\u003E\r\n/// Controls how strongly particles follow the emitter in local space\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Local Space\u0022), Category(\u0022Initialize\u0022), Icon(\u0022transform\u0022)]\r\npublic partial class InitializeLocalSpaceModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial velocity\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Velocity\u0022), Category(\u0022Initialize\u0022), Icon(\u0022air\u0022)]\r\npublic partial class InitializeVelocityModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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(\u0022InheritEmitterVelocity\u0022,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) \u002B 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/// \u003Csummary\u003E\r\n/// Sets initial color\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Collision\u0022), Category(\u0022Initialize\u0022), Icon(\u0022palette\u0022)]\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\u003CGameObject\u003E CollisionPrefabs { get; set; } = new List\u003CGameObject\u003E();\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 =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial velocity\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Rotation\u0022), Category(\u0022Initialize\u0022), Icon(\u0022air\u0022)]\r\npublic partial class InitializeRotationModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial lifetime\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Lifetime\u0022), Category(\u0022Initialize\u0022), Icon(\u0022schedule\u0022)]\r\npublic partial class InitializeLifetimeModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial size\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Size\u0022), Category(\u0022Initialize\u0022), Icon(\u0022photo_size_select_small\u0022)]\r\npublic partial class InitializeSizeModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial color\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Color\u0022), Category(\u0022Initialize\u0022), Icon(\u0022palette\u0022)]\r\npublic partial class InitializeColorModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial color\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Sprite Flipbook\u0022), Category(\u0022Initialize\u0022), Icon(\u0022palette\u0022)]\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 =\u003E 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/// \u003Csummary\u003E\r\n/// Randomly Kill a particle to spawn less\r\n/// \u003C/summary\u003E\r\n[Title(\u0022RandomKill\u0022), Category(\u0022Initialize\u0022), Icon(\u0022arrow_downward\u0022)]\r\npublic partial class RandomKill : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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 ) \u003C 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/// \u003Csummary\u003E\r\n/// Applies gravity force\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Gravity Force\u0022), Category(\u0022Update\u0022), Icon(\u0022arrow_downward\u0022)]\r\npublic partial class GravityForceModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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 \u002B= Force.GetValue(context.Particle, context.SystemComponent ) * Time.Delta;\r\n    }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Make a mesh follow it\u0027s velocity\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Follow Velocity\u0022), Category(\u0022Update\u0022), Icon(\u0022arrow_downward\u0022)]\r\npublic partial class FollowVelocity : ParticleModule, IParticleUpdater\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Applies drag/air resistance\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Drag Force\u0022), Category(\u0022Update\u0022), Icon(\u0022air\u0022)]\r\npublic partial class DragForceModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Makes particles rotate\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Rotation\u0022), Category(\u0022Update\u0022), Icon(\u0022rotate_right\u0022)]\r\npublic partial class RotationModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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 \u002B= 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 \u002B= 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/// \u003Csummary\u003E\r\n/// Attracts particles to a point. Full strength inside AttractorSize, falling off beyond it.\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Point Attractor\u0022), Category(\u0022Update\u0022), Icon(\u0022my_location\u0022)]\r\npublic partial class PointAttractorModule : ParticleModule, IParticleUpdater\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n\t/// How quickly strength falls off beyond AttractorSize.\r\n\t/// 1 = linear, 2 = inverse square, higher = sharper falloff.\r\n\t/// \u003C/summary\u003E\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 ) \u002B 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 \u003C 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 \u003C= 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 \u002B 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 \u002B= toAttractor.Normal * Strength.GetValue( context.SystemComponent ) * strengthMultiplier * Time.Delta;\r\n\t}\r\n}\r\n/// \u003Csummary\u003E\r\n/// Creates orbital motion\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Vortex Force\u0022), Category(\u0022Update\u0022), Icon(\u0022cyclone\u0022)]\r\npublic partial class VortexForceModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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 \u002B context.Emitter.WorldPosition);\r\n        var distance = toCenter.Length;\r\n        \r\n        \r\n        if (distance \u003E 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 \u002B= force * Time.Delta * 10000;\r\n        }\r\n    }\r\n}\r\n\r\n// ==================== RENDER MODULES ====================\r\n\r\n/// \u003Csummary\u003E\r\n/// Basic sprite renderer\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Sprite Renderer\u0022), Category(\u0022Render\u0022), Icon(\u0022image\u0022)]\r\npublic partial class SpriteRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n    [Hide] \r\n    public override ModuleStage Stage =\u003E 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\u003CParticleSpriteRenderer\u003E();\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/// \u003Csummary\u003E\r\n/// Basic light renderer\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Light Renderer\u0022), Category(\u0022Render\u0022), Icon(\u0022image\u0022)]\r\npublic partial class LightRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n\t[Hide] \r\n\tpublic override ModuleStage Stage =\u003E 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\u003CParticleLightRenderer\u003E();\r\n\t\t\r\n\t\tvar fxbox=go.GetComponentInParent\u003CFXBoxNativeParticleSystem\u003E(  );\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/// \u003Csummary\u003E\r\n/// Basic model renderer\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Model Renderer\u0022), Category(\u0022Render\u0022), Icon(\u0022image\u0022)]\r\npublic partial class ModelRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n\t[Hide] \r\n\tpublic override ModuleStage Stage =\u003E ModuleStage.Render;\r\n\r\n\t[Property] \r\n\tpublic List\u003CParticleModelRenderer.ModelEntry\u003E 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\u003CParticleModelRenderer\u003E();\r\n\r\n\t\trenderer.Choices = Models;\r\n\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Basic Trail Renderer\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Trail Renderer\u0022 ), Category( \u0022Render\u0022 ), Icon( \u0022image\u0022 )]\r\npublic partial class TrailRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n\t[Hide] public override ModuleStage Stage =\u003E 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( \u0022Opaque\u0022, 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\u003CParticleTrailRenderer\u003E();\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/// \u003Csummary\u003E\r\n/// Applies curl noise force for organic, swirling motion\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Curl Noise\u0022), Category(\u0022Update\u0022), Icon(\u0022air\u0022)]\r\npublic partial class CurlNoiseModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E ModuleStage.Update;\r\n    \r\n    [Property, Range(0f, 1000f)]\r\n    [Description(\u0022Strength of the curl noise effect\u0022)]\r\n    public FXParticleFloat Strength { get; set; } = 1.0f;\r\n    \r\n    [Property, Range(0.01f, 10f)]\r\n    [Description(\u0022Scale of the noise pattern - smaller values create tighter curls\u0022)]\r\n    public FXParticleFloat Scale { get; set; } = 1.0f;\r\n    \r\n    [Property, Range(0f, 10f)]\r\n    [Description(\u0022Speed at which the noise pattern evolves over time\u0022)]\r\n    public FXParticleFloat TimeScale { get; set; } = 1.0f;\r\n    \r\n    [Property]\r\n    [Description(\u0022Offset in the noise field\u0022)]\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 \u002B 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 \u002B= curl * Strength.GetValue( context.SystemComponent ) * delta * 10;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\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    /// \u003C/summary\u003E\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 \u002B new Vector3(0, epsilon, 0), time).z - \r\n             SamplePotential(pos - new Vector3(0, epsilon, 0), time).z) -\r\n            (SamplePotential(pos \u002B 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 \u002B new Vector3(0, 0, epsilon), time).x - \r\n             SamplePotential(pos - new Vector3(0, 0, epsilon), time).x) -\r\n            (SamplePotential(pos \u002B 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 \u002B new Vector3(epsilon, 0, 0), time).y - \r\n             SamplePotential(pos - new Vector3(epsilon, 0, 0), time).y) -\r\n            (SamplePotential(pos \u002B 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    /// \u003Csummary\u003E\r\n    /// Sample a 3D potential field using Perlin-like noise\r\n    /// \u003C/summary\u003E\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 \u002B new Vector3(0, 0, 0), time),\r\n            Noise3D(pos \u002B new Vector3(31.416f, -47.853f, 12.793f), time),\r\n            Noise3D(pos \u002B new Vector3(-17.737f, 86.214f, -59.482f), time)\r\n        );\r\n    }\r\n\r\n    /// \u003Csummary\u003E\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    /// \u003C/summary\u003E\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 \u002B new Vector3(time, time * 0.7f, time * 0.5f);\r\n        \r\n        float noise = 0;\r\n        noise \u002B= MathF.Sin(p.x * 1.0f \u002B p.y * 1.3f) * 0.5f;\r\n        noise \u002B= MathF.Sin(p.y * 1.7f \u002B p.z * 0.9f) * 0.3f;\r\n        noise \u002B= MathF.Sin(p.z * 2.1f \u002B p.x * 1.1f) * 0.2f;\r\n        noise \u002B= MathF.Sin(p.x * 3.7f \u002B p.y * 2.3f \u002B 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/// \u003Csummary\u003E\r\n/// Main particle system editor - ShaderGraph style with DockManager\r\n/// \u003C/summary\u003E\r\n[EditorForAssetType(\u0022fx\u0022)]\r\n[EditorApp(\u0022FXBox\u0022, \u0022auto_awesome\u0022, \u0022Create and edit particle systems\u0022)]\r\npublic class FXBoxEditor : DockWindow, IAssetEditor\r\n{\r\n    public bool CanOpenMultipleAssets =\u003E 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 = \u0022FXBox - Particle System Editor\u0022;\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()\u0027s \u0022if (Visible) RestoreLayout()\u0022).\r\n    // Setting it too early was part of why a layout you\u0027d closed with panels open never came\r\n    // back - restoring (or building the default layout) needs the window\u0027s 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 = \u0022FXBoxEditor_v6\u0022;\r\n    }\r\n\r\n    private void CreateToolBar()\r\n    {\r\n        var toolbar = new ToolBar(this, \u0022FXBoxToolbar\u0022);\r\n        AddToolBar(toolbar, ToolbarPosition.Top);\r\n\r\n        toolbar.AddOption(\u0022Save\u0022, \u0022save\u0022, Save).StatusTip = \u0022Save Particle System (Ctrl\u002BS)\u0022;\r\n        toolbar.AddSeparator();\r\n        toolbar.AddOption(\u0022Add Emitter\u0022, \u0022add_circle\u0022, AddEmitter).StatusTip = \u0022Add new emitter\u0022;\r\n        \r\n        // Add parameter menu\r\n        toolbar.AddSeparator();\r\n        toolbar.AddOption(\u0022Float Parameter\u0022, \u0022looks_one\u0022, AddFloatParameter);\r\n        toolbar.AddOption(\u0022Vector Parameter\u0022, \u00223d_rotation\u0022, AddVectorParameter);\r\n        toolbar.AddOption(\u0022Color Parameter\u0022, \u0022palette\u0022, AddColorParameter);\r\n        \r\n        toolbar.AddSeparator();\r\n        toolbar.AddOption(\u0022Play\u0022, \u0022play_arrow\u0022, () =\u003E _preview?.TogglePlayback()).StatusTip = \u0022Play/Pause\u0022;\r\n        toolbar.AddOption(\u0022Restart\u0022, \u0022replay\u0022, () =\u003E _preview?.Restart()).StatusTip = \u0022Restart\u0022;\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\u002BCreateAction 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\u0027d via ?.) to hand the\r\n    // resource to _emitterList/_preview/_properties, matching the exact \u0022not hooked up until\r\n    // you add a parameter or emitter\u0022 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\u0027s 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( \u0022Preview\u0022, \u0022visibility\u0022, _preview, DockArea.Center );\r\n        DockManager.AddDock( \u0022Emitters\u0022, \u0022list\u0022, _emitterList, DockArea.Left, relativeTo: preview );\r\n        DockManager.AddDock( \u0022Properties\u0022, \u0022tune\u0022, _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\u002Bplaced above by Title, it doesn\u0027t build a second set of widgets.\r\n    protected override void BuildDefaultLayout()\r\n    {\r\n        var preview = DockManager.OpenDock( \u0022Preview\u0022, DockArea.Center );\r\n        var emitters = DockManager.OpenDock( \u0022Emitters\u0022, DockArea.Left, preview );\r\n        var properties = DockManager.OpenDock( \u0022Properties\u0022, 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 = \u0022Emitters\u0022;\r\n        widget.WindowTitle = \u0022Emitters \u0026 Modules\u0022;\r\n        widget.SetWindowIcon(\u0022list\u0022);\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 = $\u0022{emitter.Name} (Copy)\u0022;\r\n\r\n\t    _workingCopy.Emitters.Insert(index \u002B 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 = \u0022Preview\u0022;\r\n        widget.WindowTitle = \u0022Preview\u0022;\r\n      \r\n        widget.SetWindowIcon(\u0022visibility\u0022);\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 = \u0022Properties\u0022;\r\n        widget.WindowTitle = \u0022Properties\u0022;\r\n        widget.SetWindowIcon(\u0022tune\u0022);\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(\u0022File\u0022);\r\n        file.AddOption(\u0022New\u0022, \u0022add\u0022, New, \u0022editor.new\u0022).StatusTip = \u0022New Particle System\u0022;\r\n        file.AddOption(\u0022Open\u0022, \u0022folder_open\u0022, Open, \u0022editor.open\u0022).StatusTip = \u0022Open Particle System\u0022;\r\n        file.AddOption(\u0022Save\u0022, \u0022save\u0022, Save, \u0022editor.save\u0022).StatusTip = \u0022Save Particle System\u0022;\r\n        file.AddSeparator();\r\n        file.AddOption(\u0022Close\u0022, null, () =\u003E Close(), \u0022editor.quit\u0022).StatusTip = \u0022Close Editor\u0022;\r\n\r\n        var edit = MenuBar.AddMenu(\u0022Edit\u0022);\r\n        edit.AddOption(\u0022Add Emitter\u0022, \u0022add_circle\u0022, AddEmitter);\r\n        edit.AddSeparator();\r\n        edit.AddOption(\u0022Add Float Parameter\u0022, \u0022looks_one\u0022, AddFloatParameter);\r\n        edit.AddOption(\u0022Add Vector Parameter\u0022, \u00223d_rotation\u0022, AddVectorParameter);\r\n        edit.AddOption(\u0022Add Color Parameter\u0022, \u0022palette\u0022, AddColorParameter);\r\n\r\n        var view = MenuBar.AddMenu(\u0022View\u0022);\r\n        view.AboutToShow \u002B= () =\u003E OnViewMenu(view);\r\n    }\r\n\r\n    private void OnViewMenu(Menu view)\r\n    {\r\n        view.Clear();\r\n        //view.AddOption(\u0022Restore To Default\u0022, \u0022settings_backup_restore\u0022, 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 \u002B= (b) =\u003E 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 = $\u0022FloatParam{_workingCopy.FloatParameters.Count \u002B 1}\u0022,\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 = $\u0022VectorParam{_workingCopy.VectorParameters.Count \u002B 1}\u0022,\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 = $\u0022ColorParam{_workingCopy.ColorParameters.Count \u002B 1}\u0022,\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\u003CParticleResource\u003E();\r\n\r\n        if (_resource == null)\r\n        {\r\n            _resource = new ParticleResource();\r\n            var defaultEmitter = new ParticleEmitter { Name = \u0022Emitter 1\u0022 };\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 = $\u0022FXBox - {asset.Name}\u0022;\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(\u0022editor.new\u0022, \u0022CTRL\u002BN\u0022, ShortcutType.Window)]\r\n    private void New()\r\n    {\r\n        PromptSave(() =\u003E 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 = \u0022Emitter 1\u0022 };\r\n        AddDefaultModules(defaultEmitter);\r\n        _resource.Emitters.Add(defaultEmitter);\r\n        \r\n        _workingCopy = DeepCopyResource(_resource);\r\n        \r\n        Title = \u0022FXBox - Untitled\u0022;\r\n        LoadResource();\r\n    }\r\n\r\n    [Shortcut(\u0022editor.open\u0022, \u0022CTRL\u002BO\u0022, ShortcutType.Window)]\r\n    private void Open()\r\n    {\r\n        var fd = new FileDialog(null)\r\n        {\r\n            Title = \u0022Open Particle System\u0022,\r\n            DefaultSuffix = \u0022.fx\u0022\r\n        };\r\n\r\n        fd.SetNameFilter(\u0022Particle System (*.fx)\u0022);\r\n\r\n        if (!fd.Execute())\r\n            return;\r\n\r\n        PromptSave(() =\u003E 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 = $\u0022Emitter {_workingCopy.Emitters.Count \u002B 1}\u0022\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 = \u0022Spawn Rate\u0022 });\r\n        emitter.InitializeModules.Add(new InitializePositionModule { Name = \u0022Initialize Position\u0022 });\r\n        emitter.InitializeModules.Add(new InitializeLocalSpaceModule { Name = \u0022Initialize Local Space\u0022 });\r\n        emitter.InitializeModules.Add(new InitializeVelocityModule { Name = \u0022Initialize Velocity\u0022 });\r\n        emitter.InitializeModules.Add(new InitializeLifetimeModule { Name = \u0022Initialize Lifetime\u0022 });\r\n        emitter.InitializeModules.Add(new InitializeSizeModule { Name = \u0022Initialize Size\u0022 });\r\n        emitter.InitializeModules.Add(new InitializeColorModule { Name = \u0022Initialize Color\u0022 });\r\n        emitter.UpdateModules.Add(new GravityForceModule { Name = \u0022Gravity\u0022 });\r\n        emitter.UpdateModules.Add(new DragForceModule { Name = \u0022Drag\u0022 });\r\n        emitter.RenderModules.Add(new SpriteRendererModule { Name = \u0022Sprite Renderer\u0022 });\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\u003CParticleModule\u003E()\r\n            .Where(t =\u003E !t.IsAbstract)\r\n            .Select(t =\u003E t.TargetType)\r\n            .Where(t =\u003E {\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 =\u003E t.Name))\r\n        {\r\n            var displayInfo = DisplayInfo.ForType(moduleType);\r\n            menu.AddOption(displayInfo.Name, displayInfo.Icon ?? \u0022extension\u0022, () =\u003E {\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 =\u003E emitter.SpawnModules,\r\n                        ModuleStage.Initialize =\u003E emitter.InitializeModules,\r\n                        ModuleStage.Update =\u003E emitter.UpdateModules,\r\n                        ModuleStage.Render =\u003E emitter.RenderModules,\r\n                        _ =\u003E 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 = $\u0022FXBox - {_asset?.Name ?? \u0022Untitled\u0022}*\u0022;\r\n        }\r\n    }\r\n\r\n    [Shortcut(\u0022editor.save\u0022, \u0022CTRL\u002BS\u0022, ShortcutType.Window)]\r\n    private void Save()\r\n    {\r\n        if (_asset != null \u0026\u0026 _workingCopy != null)\r\n        {\r\n            _workingCopy.IsDirty = false;\r\n            _workingCopy.Version\u002B\u002B;\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 = $\u0022FXBox - {_asset.Name}\u0022;\r\n            Log.Info($\u0022Saved: {_asset.Name}\u0022);\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 = \u0022Save Particle System\u0022,\r\n            DefaultSuffix = \u0022.fx\u0022\r\n        };\r\n\r\n        fd.SelectFile(\u0022untitled.fx\u0022);\r\n        fd.SetFindFile();\r\n        fd.SetModeSave();\r\n        fd.SetNameFilter(\u0022Particle System (*.fx)\u0022);\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\u002B\u002B;\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 = $\u0022FXBox - {_asset.Name}\u0022;\r\n        Log.Info($\u0022Saved: {_asset.Name}\u0022);\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      =\u003E emitter.SpawnModules,\r\n\t\t\t    ModuleStage.Initialize =\u003E emitter.InitializeModules,\r\n\t\t\t    ModuleStage.Update     =\u003E emitter.UpdateModules,\r\n\t\t\t    ModuleStage.Render     =\u003E emitter.RenderModules,\r\n\t\t\t    _                      =\u003E 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 \u002B 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      =\u003E resourceCopy.Emitters[emitterIndex].SpawnModules,\r\n\t\t\t    ModuleStage.Initialize =\u003E resourceCopy.Emitters[emitterIndex].InitializeModules,\r\n\t\t\t    ModuleStage.Update     =\u003E resourceCopy.Emitters[emitterIndex].UpdateModules,\r\n\t\t\t    ModuleStage.Render     =\u003E resourceCopy.Emitters[emitterIndex].RenderModules,\r\n\t\t\t    _                      =\u003E 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 = $\u0022{module.Name} (Copy)\u0022;\r\n\t\t    moduleList.Insert(index \u002B 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            \u0022Save Current Particle System\u0022, \r\n            \u0022The particle system has unsaved changes. Would you like to save now?\u0022, \r\n            \u0022Cancel\u0022,\r\n            new Dictionary\u003Cstring, System.Action\u003E()\r\n            {\r\n                { \u0022No\u0022, () =\u003E { _isDirty = false; action?.Invoke(); } },\r\n                { \u0022Yes\u0022, () =\u003E { 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(() =\u003E { _isDirty = false; Close(); });\r\n            return false;\r\n        }\r\n\r\n        return base.OnClose();\r\n    }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Left panel showing emitters and their modules in a tree structure\r\n/// \u003C/summary\u003E\r\npublic class EmitterList : Widget\r\n{\r\n    private TreeView _tree;\r\n    private ParticleResource _resource;\r\n\r\n    public System.Action\u003Cobject\u003E OnSelectionChanged;\r\n    public System.Action\u003CParticleEmitter\u003E OnEmitterDeleted;\r\n    public System.Action\u003CParticleModule\u003E OnModuleDeleted;\r\n    public System.Action\u003CParticleEmitter, ModuleStage\u003E OnAddModule;\r\n    public System.Action\u003CParticleEmitter\u003E OnEmitterDuplicated;\r\n    public System.Action\u003CParticleModule, ModuleStage\u003E 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(\u0022background-color: #1e1e1e;\u0022);\r\n\r\n        var label = new Label(\u0022Emitters \u0026 Modules\u0022, header);\r\n        label.SetStyles(\u0022font-weight: bold; font-size: 14px;\u0022);\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, \u0022Spawn\u0022, ModuleStage.Spawn, emitter.SpawnModules, emitter);\r\n            AddStageNode(emitterNode, \u0022Initialize\u0022, ModuleStage.Initialize, emitter.InitializeModules, emitter);\r\n            AddStageNode(emitterNode, \u0022Update\u0022, ModuleStage.Update, emitter.UpdateModules, emitter);\r\n            AddStageNode(emitterNode, \u0022Render\u0022, ModuleStage.Render, emitter.RenderModules, emitter);\r\n        }\r\n    }\r\n\r\n    private void AddStageNode(TreeNode parent, string name, ModuleStage stage, List\u003CParticleModule\u003E 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(\u0022Duplicate\u0022, \u0022content_copy\u0022, () =\u003E OnEmitterDuplicated?.Invoke(emitterNode.Emitter));\r\n\t        menu.AddSeparator();\r\n\t        menu.AddOption(\u0022Delete Emitter\u0022, \u0022delete\u0022, () =\u003E 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(\u0022Duplicate\u0022, \u0022content_copy\u0022, () =\u003E OnModuleDuplicated?.Invoke(module, moduleNode.Stage));\r\n\r\n\t        menu.AddOption(module.Enabled ? \u0022Disable\u0022 : \u0022Enable\u0022,\r\n\t\t        module.Enabled ? \u0022visibility_off\u0022 : \u0022visibility\u0022,\r\n\t\t        () =\u003E {\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(\u0022Delete Module\u0022, \u0022delete\u0022, () =\u003E OnModuleDeleted?.Invoke(module));\r\n        }\r\n        else if (selected is StageNode stageNode)\r\n        {\r\n            menu.AddOption(\u0022Add Module\u0022, \u0022add\u0022, () =\u003E 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, \u0022auto_awesome\u0022, 16, TextFlag.LeftCenter);\r\n            \r\n            rect.Left \u002B= 24;\r\n            Paint.DrawText(rect, \u0022Particle System\u0022, 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, \u0022\u25CF \u0022, TextFlag.LeftCenter);\r\n            \r\n            rect.Left \u002B= 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 =\u003E new Color(1f, 0.6f, 0.2f),\r\n                ModuleStage.Initialize =\u003E new Color(0.3f, 0.8f, 0.3f),\r\n                ModuleStage.Update =\u003E new Color(0.3f, 0.6f, 1f),\r\n                ModuleStage.Render =\u003E new Color(0.9f, 0.3f, 0.9f),\r\n                _ =\u003E Theme.Text\r\n            };\r\n            \r\n            Paint.SetPen(stageColor);\r\n            Paint.DrawText(rect, $\u0022{Name} ({Count})\u0022, 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, \u0022add\u0022, 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 =\u003E Emitter.SpawnModules,\r\n                    ModuleStage.Initialize =\u003E Emitter.InitializeModules,\r\n                    ModuleStage.Update =\u003E Emitter.UpdateModules,\r\n                    ModuleStage.Render =\u003E Emitter.RenderModules,\r\n                    _ =\u003E 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 \u002B= 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 =\u003E emitter.SpawnModules,\r\n                    ModuleStage.Initialize =\u003E emitter.InitializeModules,\r\n                    ModuleStage.Update =\u003E emitter.UpdateModules,\r\n                    ModuleStage.Render =\u003E emitter.RenderModules,\r\n                    _ =\u003E 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 \u003C 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 \u002B 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/// \u003Csummary\u003E\r\n/// Right panel showing properties - ShaderGraph style\r\n/// \u003C/summary\u003E\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(\u0022background-color: #1e1e1e;\u0022);\r\n\r\n        _titleLabel = new Label(\u0022Properties\u0022, header);\r\n        _titleLabel.SetStyles(\u0022font-weight: bold; font-size: 14px;\u0022);\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 = \u0022System Properties\u0022;\r\n        \r\n        RebuildContent(() =\u003E {\r\n            if (resource != null)\r\n            {\r\n                var so = resource.GetSerialized();\r\n                so.OnPropertyChanged \u002B= 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 = $\u0022Emitter: {emitter.Name}\u0022;\r\n        \r\n        RebuildContent(() =\u003E {\r\n            if (emitter != null)\r\n            {\r\n                var so = emitter.GetSerialized();\r\n                so.OnPropertyChanged \u002B= 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(() =\u003E {\r\n            if (module != null)\r\n            {\r\n                var so = module.GetSerialized();\r\n                so.OnPropertyChanged \u002B= 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 \u0026\u0026 _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\u003CSerializedObject\u003E 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/// \u003Csummary\u003E\r\n/// A named float parameter that can be used to control particle values\r\n/// \u003C/summary\u003E\r\npublic class FloatParameter\r\n{\r\n\t[Property] public string Name { get; set; } = \u0022Parameter\u0022;\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/// \u003Csummary\u003E\r\n/// A named vector parameter that can be used to control particle values\r\n/// \u003C/summary\u003E\r\npublic class VectorParameter\r\n{\r\n\t[Property] public string Name { get; set; } = \u0022VectorParameter\u0022;\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/// \u003Csummary\u003E\r\n/// A named color parameter that can be used to control particle values\r\n/// \u003C/summary\u003E\r\npublic class ColorParameter\r\n{\r\n\t[Property] public string Name { get; set; } = \u0022ColorParameter\u0022;\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/// \u003Csummary\u003E\r\n/// Particle system resource containing multiple emitters\r\n/// \u003C/summary\u003E\r\n[AssetType(Name = \u0022Particle System\u0022, Extension = \u0022fx\u0022, Category = \u0022FX\u0022, Flags = AssetTypeFlags.NoEmbedding)]\r\npublic class ParticleResource : GameResource\r\n{\r\n    public bool IsDirty { get; set; } = false;\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// All emitters in this particle system\r\n    /// \u003C/summary\u003E\r\n    public List\u003CParticleEmitter\u003E Emitters { get; set; } = new();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Named float parameters\r\n    /// \u003C/summary\u003E\r\n    [InlineEditor, DisplayName(\u0022FloatParameters\u0022)] public List\u003CFloatParameter\u003E FloatParameters { get; set; } = new();\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Named vector parameters\r\n    /// \u003C/summary\u003E\r\n    [InlineEditor] public List\u003CVectorParameter\u003E VectorParameters { get; set; } = new();\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Named color parameters\r\n    /// \u003C/summary\u003E\r\n    [InlineEditor] public List\u003CColorParameter\u003E ColorParameters { get; set; } = new();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Global system properties\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Preview settings for the editor\r\n    /// \u003C/summary\u003E\r\n    public ParticlePreviewSettings PreviewSettings { get; set; } = new();\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Get a float parameter\u0027s default value by name\r\n    /// \u003C/summary\u003E\r\n    public float GetParameterDefault(string name)\r\n    {\r\n        var param = FloatParameters.FirstOrDefault(p =\u003E p.Name == name);\r\n        return param?.DefaultValue ?? 0f;\r\n    }\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Get a vector parameter\u0027s default value by name\r\n    /// \u003C/summary\u003E\r\n    public Vector3 GetVectorParameterDefault(string name)\r\n    {\r\n        var param = VectorParameters.FirstOrDefault(p =\u003E p.Name == name);\r\n        return param?.DefaultValue ?? Vector3.Zero;\r\n    }\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Get a color parameter\u0027s default value by name\r\n    /// \u003C/summary\u003E\r\n    public ParticleGradient GetColorParameterDefault(string name)\r\n    {\r\n        var param = ColorParameters.FirstOrDefault(p =\u003E p.Name == name);\r\n        return param?.DefaultValue ?? Color.White;\r\n    }\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Add a new float parameter\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Add a new vector parameter\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Add a new color parameter\r\n    /// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// Preview settings for the particle editor\r\n/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// A single particle emitter with its own spawn and update logic\r\n/// \u003C/summary\u003E\r\npublic class ParticleEmitter\r\n{\r\n\tpublic string Name { get; set; } = \u0022Emitter\u0022;\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/// \u003Csummary\u003E\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\u0027s handled by the engine\u0027s own particle system rather\r\n\t/// than anything FXBox has to gate itself.\r\n\t/// \u003C/summary\u003E\r\n\tpublic float Delay { get; set; } = 0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Overrides the system\u0027s own Duration for deciding when THIS emitter is finished\r\n\t/// (see FXBoxParticleController.IsFinished) - 0 means \u0022use the particle system\u0027s own\r\n\t/// Duration instead\u0022, 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/// \u003C/summary\u003E\r\n\tpublic float Duration { get; set; } = 0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Modules that run when spawning particles\r\n\t/// \u003C/summary\u003E\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List\u003CParticleModule\u003E SpawnModules { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Modules that run once when a particle is created\r\n\t/// \u003C/summary\u003E\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List\u003CParticleModule\u003E InitializeModules { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Modules that run every frame for each particle\r\n\t/// \u003C/summary\u003E\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List\u003CParticleModule\u003E UpdateModules { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Modules that control how particles are rendered\r\n\t/// \u003C/summary\u003E\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List\u003CParticleModule\u003E RenderModules { get; set; } = new();\r\n}\r\n/// \u003Csummary\u003E\r\n/// Base class for all particle modules\r\n/// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// What stage this module belongs to\r\n    /// \u003C/summary\u003E\r\n    [JsonIgnore]\r\n    public abstract ModuleStage Stage { get; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Execute this module\r\n    /// \u003C/summary\u003E\r\n    public abstract void Execute(ParticleExecutionContext context);\r\n\r\n    public abstract void Initialize( ParticleExecutionContext context );\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// JSON converter for List of ParticleModule\r\n/// \u003C/summary\u003E\r\npublic class ParticleModuleListConverter : JsonConverter\u003CList\u003CParticleModule\u003E\u003E\r\n{\r\n    public override List\u003CParticleModule\u003E Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\r\n    {\r\n        var list = new List\u003CParticleModule\u003E();\r\n        \r\n        if (reader.TokenType != JsonTokenType.StartArray)\r\n            throw new JsonException(\u0022Expected start of array\u0022);\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(\u0022$type\u0022, out var typeProperty))\r\n                {\r\n                    Log.Warning(\u0022Missing $type property for ParticleModule\u0022);\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($\u0022Unknown module type: {typeName}\u0022);\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\u003CParticleModule\u003E 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(\u0022$type\u0022, 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/// \u003Csummary\u003E\r\n/// Component that integrates FXBox particle systems wit s\u0026box native particle components\r\n/// \u003C/summary\u003E\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\u003CGameObject\u003E 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    /// \u003Csummary\u003E\r\n    /// Instance-specific float parameter overrides\r\n    /// \u003C/summary\u003E\r\n    [Property, Group(\u0022Parameters\u0022), Title(\u0022Float Parameters\u0022)]\r\n    public Dictionary\u003Cstring, float\u003E ParameterOverrides { get; set; } = new();\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Instance-specific vector parameter overrides\r\n    /// \u003C/summary\u003E\r\n    [Property, Group(\u0022Parameters\u0022), Title(\u0022Vector Parameters\u0022)]\r\n    public Dictionary\u003Cstring, Vector3\u003E VectorParameterOverrides { get; set; } = new();\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Instance-specific color parameter overrides\r\n    /// \u003C/summary\u003E\r\n    [Property, Group(\u0022Parameters\u0022), Title(\u0022Color Parameters\u0022)]\r\n    public Dictionary\u003Cstring, ParticleGradient\u003E 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 \u0026\u0026 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\u0027s own Looping setting - same convention TracerEffect uses (\u0022Editor\r\n        // previews loop instead\u0022). This used to happen by accident: SpawnBurstModule.Execute\r\n        // re-firing every tick meant a \u0022finished\u0022 burst kept re-triggering itself forever in\r\n        // the editor. Now that it\u0027s 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 \u0022loop the particles after all of\r\n        // them are done emitting\u0022 - 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 \u0026\u0026 !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\u003CFXBoxParticleController\u003E()?.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\u003CSandbox.ParticleEmitter\u003E() )\r\n            {\r\n                particleEmitter.ResetEmitter();\r\n            }\r\n        }\r\n    }\r\n    \r\n    // ==================== FLOAT PARAMETERS ====================\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Set a float parameter value for this specific instance\r\n    /// \u003C/summary\u003E\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 =\u003E p.Name == name);\r\n        if (param == null)\r\n        {\r\n            //Log.Warning($\u0022Float parameter \u0027{name}\u0027 not found in particle system\u0022);\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\u003CSandbox.ParticleEmitter\u003E(  ) )\r\n\t\t    {\r\n\t\t\t    var target = particleEmitter.GetComponent\u003CParticleEffect\u003E();\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\u003CSandbox.ParticleEmitter\u003E(  ) )\r\n\t\t    {\r\n\t\t\t    var target = particleEmitter.GetComponent\u003CParticleEffect\u003E();\r\n\t\t\t    for(int i=0; i\u003C particleEmitter.Burst.Evaluate( Time.Delta, Time.Delta ); i\u002B\u002B)\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\u003CSandbox.ParticleEffect\u003E(  ) )\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 \u003C= 0.01f )\r\n\t\t\t\t    {\r\n\t\t\t\t\t\tparticle.Position \u002B= 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    /// \u003Csummary\u003E\r\n    /// Get a float parameter value (override or default)\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Reset a float parameter to its default value\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Set a vector parameter value for this specific instance\r\n    /// \u003C/summary\u003E\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 =\u003E p.Name == name);\r\n        if (param == null)\r\n        {\r\n            //Log.Warning($\u0022Vector parameter \u0027{name}\u0027 not found in particle system\u0022);\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\u003CParticleEffect\u003E();\r\n\t\t    particles \u002B= target.ParticleCount;\r\n\t    }\r\n\r\n\t    return particles;\r\n    }\r\n    /// \u003Csummary\u003E\r\n    /// Get a vector parameter value (override or default)\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Reset a vector parameter to its default value\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Set a color parameter value for this specific instance\r\n    /// \u003C/summary\u003E\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 =\u003E p.Name == name);\r\n        if (param == null)\r\n        {\r\n            //Log.Warning($\u0022Color parameter \u0027{name}\u0027 not found in particle system\u0022);\r\n            return;\r\n        }\r\n        \r\n        ColorParameterOverrides[name] = value;\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Get a color parameter value (override or default)\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Reset a color parameter to its default value\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Reset all parameters to their default values\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Helper buttons for the inspector\r\n    /// \u003C/summary\u003E\r\n    [Button(\u0022Reset All Parameters\u0022)]\r\n    [Group(\u0022Parameters\u0022)]\r\n    public void ResetAllParametersButton()\r\n    {\r\n        ResetAllParameters();\r\n    }\r\n    \r\n    [Button(\u0022Initialize All Parameters\u0022)]\r\n    [Group(\u0022Parameters\u0022)]\r\n    [Description(\u0022Copy all parameters from the resource as overrides\u0022)]\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    /// \u003Csummary\u003E\r\n    /// Update parameter values without rebuilding emitters\r\n    /// \u003C/summary\u003E\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\u003CFXBoxParticleController\u003E();\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(\u0022The scene has stopped\u0022);\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 \u0022actually finished\u0022 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\u0027s editor behavior).\r\n        if ( DestroyOnEnd \u0026\u0026 !Scene.IsEditor )\r\n        {\r\n            var temporaryEffect = GetOrAddComponent\u003CTemporaryEffect\u003E();\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( \u0022images/particlehover.vtex\u0022 ), 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\u003CParticleEffect\u003E();\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\u003CFXBoxParticleController\u003E();\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\u003CIParticleComponentCreator\u003E())\r\n        {\r\n            if (module is ParticleModule pm \u0026\u0026 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\u003CParticleEffect\u003E();\r\n        context.Emitter = go.GetComponent\u003CSandbox.ParticleEmitter\u003E();\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\u0027s own duration behavior matches what our controller thinks it does.\r\n        var effectiveDuration = emitterData.Duration \u003E 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\u003CSandbox.ParticleEmitter\u003E().IsValid())\r\n        {\r\n            var emitter = go.AddComponent\u003CParticleSphereEmitter\u003E();\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\u003CSandbox.ParticleEmitter\u003E();\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\u003CParticleEffect\u003E();\r\n        context.Emitter = go.GetComponent\u003CSandbox.ParticleEmitter\u003E();\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\u0027s actually\r\n    // still emitting/alive. True as soon as ANY emitter hasn\u0027t 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\u003CFXBoxParticleController\u003E();\r\n                if ( controller.IsValid() \u0026\u0026 !controller.IsFinished )\r\n                    return true;\r\n            }\r\n\r\n            return false;\r\n        }\r\n    }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Controller that executes particle update modules on each particle\r\n/// \u003C/summary\u003E\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\u003CParticleModule\u003E InitializeModules { get; set; } = new List\u003CParticleModule\u003E();\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\u0027s 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 =\u003E EmitterData != null \u0026\u0026 EmitterData.Duration \u003E 0f\r\n\t    ? EmitterData.Duration\r\n\t    : (ParticleSystemComponent?.ParticleSystem?.Duration ?? 0f);\r\n\r\n    public bool IsFinished =\u003E\r\n\t    _timeSinceCreated \u003E EffectiveDuration\r\n\t    \u0026\u0026 !(ParticleSystemComponent?.ParticleSystem?.Looping ?? false)\r\n\t    \u0026\u0026 ParticleEffect.Particles.Count \u003C= 0;\r\n\r\n    // Called by FXBoxNativeParticleSystem.RestartAllEmitters (editor-only looping of a\r\n    // non-looping system, once every emitter\u0027s 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() =\u003E _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\u003CSandbox.ParticleEmitter\u003E(),\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\u003CSandbox.ParticleEmitter\u003E(),\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\u003CIParticleUpdater\u003E())\r\n        {\r\n            if (module is ParticleModule pm \u0026\u0026 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\u003CParticleModule\u003E();\r\n        \r\n        var context = new ParticleExecutionContext\r\n        {\r\n            Particle = p,\r\n            Effect = ParticleEffect,\r\n            Emitter = ParticleEffect.GetComponent\u003CSandbox.ParticleEmitter\u003E(),\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/// \u003Csummary\u003E\r\n/// Interface for modules that can create native components\r\n/// \u003C/summary\u003E\r\npublic interface IParticleComponentCreator\r\n{\r\n    void CreateComponent(GameObject go);\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Interface for modules that update particles\r\n/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// Component that integrates FXBox particle systems wit s\u0026box native particle components\r\n/// \u003C/summary\u003E\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\u003CGameObject\u003E 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    /// \u003Csummary\u003E\r\n    /// Instance-specific float parameter overrides\r\n    /// \u003C/summary\u003E\r\n    [Property, Group(\u0022Parameters\u0022), Title(\u0022Float Parameters\u0022)]\r\n    public Dictionary\u003Cstring, float\u003E ParameterOverrides { get; set; } = new();\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Instance-specific vector parameter overrides\r\n    /// \u003C/summary\u003E\r\n    [Property, Group(\u0022Parameters\u0022), Title(\u0022Vector Parameters\u0022)]\r\n    public Dictionary\u003Cstring, Vector3\u003E VectorParameterOverrides { get; set; } = new();\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Instance-specific color parameter overrides\r\n    /// \u003C/summary\u003E\r\n    [Property, Group(\u0022Parameters\u0022), Title(\u0022Color Parameters\u0022)]\r\n    public Dictionary\u003Cstring, ParticleGradient\u003E 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 \u0026\u0026 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\u0027s own Looping setting - same convention TracerEffect uses (\u0022Editor\r\n        // previews loop instead\u0022). This used to happen by accident: SpawnBurstModule.Execute\r\n        // re-firing every tick meant a \u0022finished\u0022 burst kept re-triggering itself forever in\r\n        // the editor. Now that it\u0027s 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 \u0022loop the particles after all of\r\n        // them are done emitting\u0022 - 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 \u0026\u0026 !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\u003CFXBoxParticleController\u003E()?.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\u003CSandbox.ParticleEmitter\u003E() )\r\n            {\r\n                particleEmitter.ResetEmitter();\r\n            }\r\n        }\r\n    }\r\n    \r\n    // ==================== FLOAT PARAMETERS ====================\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Set a float parameter value for this specific instance\r\n    /// \u003C/summary\u003E\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 =\u003E p.Name == name);\r\n        if (param == null)\r\n        {\r\n            //Log.Warning($\u0022Float parameter \u0027{name}\u0027 not found in particle system\u0022);\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\u003CSandbox.ParticleEmitter\u003E(  ) )\r\n\t\t    {\r\n\t\t\t    var target = particleEmitter.GetComponent\u003CParticleEffect\u003E();\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\u003CSandbox.ParticleEmitter\u003E(  ) )\r\n\t\t    {\r\n\t\t\t    var target = particleEmitter.GetComponent\u003CParticleEffect\u003E();\r\n\t\t\t    for(int i=0; i\u003C particleEmitter.Burst.Evaluate( Time.Delta, Time.Delta ); i\u002B\u002B)\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\u003CSandbox.ParticleEffect\u003E(  ) )\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 \u003C= 0.01f )\r\n\t\t\t\t    {\r\n\t\t\t\t\t\tparticle.Position \u002B= 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    /// \u003Csummary\u003E\r\n    /// Get a float parameter value (override or default)\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Reset a float parameter to its default value\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Set a vector parameter value for this specific instance\r\n    /// \u003C/summary\u003E\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 =\u003E p.Name == name);\r\n        if (param == null)\r\n        {\r\n            //Log.Warning($\u0022Vector parameter \u0027{name}\u0027 not found in particle system\u0022);\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\u003CParticleEffect\u003E();\r\n\t\t    particles \u002B= target.ParticleCount;\r\n\t    }\r\n\r\n\t    return particles;\r\n    }\r\n    /// \u003Csummary\u003E\r\n    /// Get a vector parameter value (override or default)\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Reset a vector parameter to its default value\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Set a color parameter value for this specific instance\r\n    /// \u003C/summary\u003E\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 =\u003E p.Name == name);\r\n        if (param == null)\r\n        {\r\n            //Log.Warning($\u0022Color parameter \u0027{name}\u0027 not found in particle system\u0022);\r\n            return;\r\n        }\r\n        \r\n        ColorParameterOverrides[name] = value;\r\n        UpdateParameterValues();\r\n    }\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Get a color parameter value (override or default)\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Reset a color parameter to its default value\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Reset all parameters to their default values\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Helper buttons for the inspector\r\n    /// \u003C/summary\u003E\r\n    [Button(\u0022Reset All Parameters\u0022)]\r\n    [Group(\u0022Parameters\u0022)]\r\n    public void ResetAllParametersButton()\r\n    {\r\n        ResetAllParameters();\r\n    }\r\n    \r\n    [Button(\u0022Initialize All Parameters\u0022)]\r\n    [Group(\u0022Parameters\u0022)]\r\n    [Description(\u0022Copy all parameters from the resource as overrides\u0022)]\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    /// \u003Csummary\u003E\r\n    /// Update parameter values without rebuilding emitters\r\n    /// \u003C/summary\u003E\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\u003CFXBoxParticleController\u003E();\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(\u0022The scene has stopped\u0022);\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 \u0022actually finished\u0022 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\u0027s editor behavior).\r\n        if ( DestroyOnEnd \u0026\u0026 !Scene.IsEditor )\r\n        {\r\n            var temporaryEffect = GetOrAddComponent\u003CTemporaryEffect\u003E();\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( \u0022images/particlehover.vtex\u0022 ), 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\u003CParticleEffect\u003E();\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\u003CFXBoxParticleController\u003E();\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\u003CIParticleComponentCreator\u003E())\r\n        {\r\n            if (module is ParticleModule pm \u0026\u0026 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\u003CParticleEffect\u003E();\r\n        context.Emitter = go.GetComponent\u003CSandbox.ParticleEmitter\u003E();\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\u0027s own duration behavior matches what our controller thinks it does.\r\n        var effectiveDuration = emitterData.Duration \u003E 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\u003CSandbox.ParticleEmitter\u003E().IsValid())\r\n        {\r\n            var emitter = go.AddComponent\u003CParticleSphereEmitter\u003E();\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\u003CSandbox.ParticleEmitter\u003E();\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\u003CParticleEffect\u003E();\r\n        context.Emitter = go.GetComponent\u003CSandbox.ParticleEmitter\u003E();\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\u0027s actually\r\n    // still emitting/alive. True as soon as ANY emitter hasn\u0027t 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\u003CFXBoxParticleController\u003E();\r\n                if ( controller.IsValid() \u0026\u0026 !controller.IsFinished )\r\n                    return true;\r\n            }\r\n\r\n            return false;\r\n        }\r\n    }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Controller that executes particle update modules on each particle\r\n/// \u003C/summary\u003E\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\u003CParticleModule\u003E InitializeModules { get; set; } = new List\u003CParticleModule\u003E();\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\u0027s 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 =\u003E EmitterData != null \u0026\u0026 EmitterData.Duration \u003E 0f\r\n\t    ? EmitterData.Duration\r\n\t    : (ParticleSystemComponent?.ParticleSystem?.Duration ?? 0f);\r\n\r\n    public bool IsFinished =\u003E\r\n\t    _timeSinceCreated \u003E EffectiveDuration\r\n\t    \u0026\u0026 !(ParticleSystemComponent?.ParticleSystem?.Looping ?? false)\r\n\t    \u0026\u0026 ParticleEffect.Particles.Count \u003C= 0;\r\n\r\n    // Called by FXBoxNativeParticleSystem.RestartAllEmitters (editor-only looping of a\r\n    // non-looping system, once every emitter\u0027s 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() =\u003E _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\u003CSandbox.ParticleEmitter\u003E(),\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\u003CSandbox.ParticleEmitter\u003E(),\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\u003CIParticleUpdater\u003E())\r\n        {\r\n            if (module is ParticleModule pm \u0026\u0026 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\u003CParticleModule\u003E();\r\n        \r\n        var context = new ParticleExecutionContext\r\n        {\r\n            Particle = p,\r\n            Effect = ParticleEffect,\r\n            Emitter = ParticleEffect.GetComponent\u003CSandbox.ParticleEmitter\u003E(),\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/// \u003Csummary\u003E\r\n/// Interface for modules that can create native components\r\n/// \u003C/summary\u003E\r\npublic interface IParticleComponentCreator\r\n{\r\n    void CreateComponent(GameObject go);\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Interface for modules that update particles\r\n/// \u003C/summary\u003E\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(\u0022Select a parameter from the system\u0022)]\r\n    public string ParameterName { get; set; }\r\n    \r\n    [Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]\r\n    [Description(\u0022Multiplier applied to the parameter value\u0022)]\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 \u0026\u0026 !string.IsNullOrEmpty(ParameterName) \u0026\u0026 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 \u0026\u0026 !string.IsNullOrEmpty(ParameterName) \u0026\u0026 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(\u0022Division: a is null\u0022);\r\n        return null;\r\n    }\r\n    \r\n    if (b == 0 || MathF.Abs(b) \u003C 0.0001f)\r\n    {\r\n        Log.Warning($\u0022Division by zero or very small number ({b}) in FXParticleFloat\u0022);\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(\u0022Multiplication: a is null\u0022);\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\u0027t copy it at all since we\u0027re 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\u0027T 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\u0027T 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 \u002B offset;\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.Range:\r\n            result.ConstantA = pf.ConstantA \u002B offset;\r\n            result.ConstantB = pf.ConstantB \u002B 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\u0027T 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 =\u003E \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 =\u003E \r\n            new Curve.Frame(frame.Time, frame.Value \u002B 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(\u0022Select a vector parameter from the system\u0022)]\r\n\tpublic string ParameterName { get; set; }\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]\r\n\t[Description(\u0022Multiplier applied to the parameter value\u0022)]\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 \u0026\u0026 !string.IsNullOrEmpty(ParameterName) \u0026\u0026 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(\u0022Select a color parameter from the system\u0022)]\r\n\tpublic string ParameterName { get; set; }\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]\r\n\t[Description(\u0022Multiplier applied to the parameter value (affects RGB)\u0022)]\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 \u0026\u0026 !string.IsNullOrEmpty(ParameterName) \u0026\u0026 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/// \u003Csummary\u003E\r\n/// A named float parameter that can be used to control particle values\r\n/// \u003C/summary\u003E\r\npublic class FloatParameter\r\n{\r\n\t[Property] public string Name { get; set; } = \u0022Parameter\u0022;\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/// \u003Csummary\u003E\r\n/// A named vector parameter that can be used to control particle values\r\n/// \u003C/summary\u003E\r\npublic class VectorParameter\r\n{\r\n\t[Property] public string Name { get; set; } = \u0022VectorParameter\u0022;\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/// \u003Csummary\u003E\r\n/// A named color parameter that can be used to control particle values\r\n/// \u003C/summary\u003E\r\npublic class ColorParameter\r\n{\r\n\t[Property] public string Name { get; set; } = \u0022ColorParameter\u0022;\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/// \u003Csummary\u003E\r\n/// Widget that renders a real-time preview of the particle system using native components\r\n/// \u003C/summary\u003E\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 =\u003E _playbackSpeed;\r\n        set =\u003E _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, \u0022camera\u0022);\r\n            var camera = cameraGo.GetOrAddComponent\u003CCameraComponent\u003E();\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, \u0022sun\u0022);\r\n            var sun = sunGo.GetOrAddComponent\u003CDirectionalLight\u003E();\r\n            sun.WorldRotation = Rotation.FromPitch(50);\r\n            sun.LightColor = Color.White;\r\n\r\n            var ambientGo = new GameObject(true, \u0022ambient\u0022);\r\n            var ambient = ambientGo.GetOrAddComponent\u003CAmbientLight\u003E();\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, \u0022ParticleSystem\u0022);\r\n                _particleSystem = _particleSystemObject.AddComponent\u003CFXBoxNativeParticleSystem\u003E();\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($\u0022Loaded particle system with {_resource.Emitters?.Count ?? 0} emitters\u0022);\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\u003CParticleEffect\u003E();\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($\u0022Playback: {(_isPlaying ? \u0022Playing\u0022 : \u0022Paused\u0022)}\u0022);\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(\u0022Particle system restarted\u0022);\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 \u003E 0)\r\n        {\r\n            var emitter = _resource.Emitters[0];\r\n            var posModule = emitter.InitializeModules.OfType\u003CInitializePositionModule\u003E().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 \u003C points; i\u002B\u002B)\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 \u003C= 500; x \u002B= 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 \u003C= 500; y \u002B= 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 \u002B 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 \u002B= delta.x * 0.3f;\r\n            _angles.y \u002B= 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 =\u003E 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\u003CParticleEffect\u003E();\r\n                if (effect.IsValid())\r\n                {\r\n                    totalParticles \u002B= effect.Particles.Count;\r\n                }\r\n            }\r\n        }\r\n        \r\n        var text = $\u0022Particles: {totalParticles} / {totalMax}\u0022;\r\n        Paint.DrawText(new Rect(10, 10, 200, 30), text, TextFlag.LeftTop);\r\n        \r\n        var stateText = _isPlaying ? \u0022PLAYING\u0022 : \u0022PAUSED\u0022;\r\n        Paint.DrawText(new Rect(10, 40, 200, 30), stateText, TextFlag.LeftTop);\r\n        \r\n        // Draw emitter count\r\n        var emitterText = $\u0022Emitters: {_resource?.Emitters.Count ?? 0}\u0022;\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/// \u003Csummary\u003E\r\n/// Stage when a particle module executes\r\n/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// Context passed to particle modules during execution\r\n/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// Controls spawn rate over time\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Spawn Rate\u0022), Category(\u0022Spawn\u0022), Icon(\u0022speed\u0022)]\r\npublic partial class SpawnRateModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial particle stretch\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Particle Stretch\u0022), Category(\u0022Initialize\u0022), Icon(\u0022photo_size_select_small\u0022)]\r\npublic partial class ParticleStretchModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Controls spawn rate per unit\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Spawn Rate Over Distance\u0022), Category(\u0022Spawn\u0022), Icon(\u0022speed\u0022)]\r\npublic partial class SpawnRateOverDistanceModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Spawns particles in a burst\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Spawn Burst\u0022), Category(\u0022Spawn\u0022), Icon(\u0022auto_awesome\u0022)]\r\npublic partial class SpawnBurstModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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 \u0022spawn this many now\u0022 value, not a continuous per-tick one like\r\n    // SpawnRateModule\u0027s 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 \u0022Initialize-only, empty Execute\u0022 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/// \u003Csummary\u003E\r\n/// Sets initial position based on shape\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Position\u0022), Category(\u0022Initialize\u0022), Icon(\u0022place\u0022)]\r\npublic partial class InitializePositionModule : ParticleModule, IParticleComponentCreator\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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 =\u003E Shape == SpawnShape.Sphere || Shape == SpawnShape.Circle || Shape == SpawnShape.Cone;\r\n    [Hide] public bool ShowBoxSize =\u003E Shape == SpawnShape.Box;\r\n    [Hide] public bool ShowConeAngle =\u003E Shape == SpawnShape.Cone;\r\n    [Hide] public bool ShowLine =\u003E 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 =\u003E Vector3.Zero,\r\n\t\t\t    SpawnShape.Sphere =\u003E GetSpherePosition(),\r\n\t\t\t    SpawnShape.Box =\u003E GetBoxPosition(),\r\n\t\t\t    SpawnShape.Cone =\u003E GetConePosition(),\r\n\t\t\t    SpawnShape.Circle =\u003E GetCirclePosition(),\r\n\t\t\t    SpawnShape.Line =\u003E GetLinePosition(),\r\n\t\t\t    _ =\u003E 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 \u002B= pos;\r\n\t    }\r\n    }\r\n    public void CreateComponent(GameObject go)\r\n    {\r\n\t    var pointEmitter = go.AddComponent\u003CParticleSphereEmitter\u003E();\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 =\u003E new Vector3(-halfSize.x, MathX.Lerp(-halfSize.y, halfSize.y, u), MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                1 =\u003E new Vector3(halfSize.x, MathX.Lerp(-halfSize.y, halfSize.y, u), MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                2 =\u003E new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), -halfSize.y, MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                3 =\u003E new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), halfSize.y, MathX.Lerp(-halfSize.z, halfSize.z, v)),\r\n                4 =\u003E new Vector3(MathX.Lerp(-halfSize.x, halfSize.x, u), MathX.Lerp(-halfSize.y, halfSize.y, v), -halfSize.z),\r\n                _ =\u003E 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/// \u003Csummary\u003E\r\n/// Controls how strongly particles follow the emitter in local space\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Local Space\u0022), Category(\u0022Initialize\u0022), Icon(\u0022transform\u0022)]\r\npublic partial class InitializeLocalSpaceModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial velocity\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Velocity\u0022), Category(\u0022Initialize\u0022), Icon(\u0022air\u0022)]\r\npublic partial class InitializeVelocityModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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(\u0022InheritEmitterVelocity\u0022,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) \u002B 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/// \u003Csummary\u003E\r\n/// Sets initial color\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Collision\u0022), Category(\u0022Initialize\u0022), Icon(\u0022palette\u0022)]\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\u003CGameObject\u003E CollisionPrefabs { get; set; } = new List\u003CGameObject\u003E();\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 =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial velocity\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Rotation\u0022), Category(\u0022Initialize\u0022), Icon(\u0022air\u0022)]\r\npublic partial class InitializeRotationModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial lifetime\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Lifetime\u0022), Category(\u0022Initialize\u0022), Icon(\u0022schedule\u0022)]\r\npublic partial class InitializeLifetimeModule : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial size\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Size\u0022), Category(\u0022Initialize\u0022), Icon(\u0022photo_size_select_small\u0022)]\r\npublic partial class InitializeSizeModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial color\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Initialize Color\u0022), Category(\u0022Initialize\u0022), Icon(\u0022palette\u0022)]\r\npublic partial class InitializeColorModule : ParticleModule\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Sets initial color\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Sprite Flipbook\u0022), Category(\u0022Initialize\u0022), Icon(\u0022palette\u0022)]\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 =\u003E 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/// \u003Csummary\u003E\r\n/// Randomly Kill a particle to spawn less\r\n/// \u003C/summary\u003E\r\n[Title(\u0022RandomKill\u0022), Category(\u0022Initialize\u0022), Icon(\u0022arrow_downward\u0022)]\r\npublic partial class RandomKill : ParticleModule\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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 ) \u003C 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/// \u003Csummary\u003E\r\n/// Applies gravity force\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Gravity Force\u0022), Category(\u0022Update\u0022), Icon(\u0022arrow_downward\u0022)]\r\npublic partial class GravityForceModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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 \u002B= Force.GetValue(context.Particle, context.SystemComponent ) * Time.Delta;\r\n    }\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Make a mesh follow it\u0027s velocity\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Follow Velocity\u0022), Category(\u0022Update\u0022), Icon(\u0022arrow_downward\u0022)]\r\npublic partial class FollowVelocity : ParticleModule, IParticleUpdater\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Applies drag/air resistance\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Drag Force\u0022), Category(\u0022Update\u0022), Icon(\u0022air\u0022)]\r\npublic partial class DragForceModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n/// Makes particles rotate\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Rotation\u0022), Category(\u0022Update\u0022), Icon(\u0022rotate_right\u0022)]\r\npublic partial class RotationModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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 \u002B= 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 \u002B= 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/// \u003Csummary\u003E\r\n/// Attracts particles to a point. Full strength inside AttractorSize, falling off beyond it.\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Point Attractor\u0022), Category(\u0022Update\u0022), Icon(\u0022my_location\u0022)]\r\npublic partial class PointAttractorModule : ParticleModule, IParticleUpdater\r\n{\r\n\t[Hide]\r\n\tpublic override ModuleStage Stage =\u003E 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/// \u003Csummary\u003E\r\n\t/// How quickly strength falls off beyond AttractorSize.\r\n\t/// 1 = linear, 2 = inverse square, higher = sharper falloff.\r\n\t/// \u003C/summary\u003E\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 ) \u002B 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 \u003C 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 \u003C= 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 \u002B 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 \u002B= toAttractor.Normal * Strength.GetValue( context.SystemComponent ) * strengthMultiplier * Time.Delta;\r\n\t}\r\n}\r\n/// \u003Csummary\u003E\r\n/// Creates orbital motion\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Vortex Force\u0022), Category(\u0022Update\u0022), Icon(\u0022cyclone\u0022)]\r\npublic partial class VortexForceModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E 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 \u002B context.Emitter.WorldPosition);\r\n        var distance = toCenter.Length;\r\n        \r\n        \r\n        if (distance \u003E 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 \u002B= force * Time.Delta * 10000;\r\n        }\r\n    }\r\n}\r\n\r\n// ==================== RENDER MODULES ====================\r\n\r\n/// \u003Csummary\u003E\r\n/// Basic sprite renderer\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Sprite Renderer\u0022), Category(\u0022Render\u0022), Icon(\u0022image\u0022)]\r\npublic partial class SpriteRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n    [Hide] \r\n    public override ModuleStage Stage =\u003E 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\u003CParticleSpriteRenderer\u003E();\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/// \u003Csummary\u003E\r\n/// Basic light renderer\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Light Renderer\u0022), Category(\u0022Render\u0022), Icon(\u0022image\u0022)]\r\npublic partial class LightRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n\t[Hide] \r\n\tpublic override ModuleStage Stage =\u003E 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\u003CParticleLightRenderer\u003E();\r\n\t\t\r\n\t\tvar fxbox=go.GetComponentInParent\u003CFXBoxNativeParticleSystem\u003E(  );\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/// \u003Csummary\u003E\r\n/// Basic model renderer\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Model Renderer\u0022), Category(\u0022Render\u0022), Icon(\u0022image\u0022)]\r\npublic partial class ModelRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n\t[Hide] \r\n\tpublic override ModuleStage Stage =\u003E ModuleStage.Render;\r\n\r\n\t[Property] \r\n\tpublic List\u003CParticleModelRenderer.ModelEntry\u003E 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\u003CParticleModelRenderer\u003E();\r\n\r\n\t\trenderer.Choices = Models;\r\n\r\n\t}\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// Basic Trail Renderer\r\n/// \u003C/summary\u003E\r\n[Title( \u0022Trail Renderer\u0022 ), Category( \u0022Render\u0022 ), Icon( \u0022image\u0022 )]\r\npublic partial class TrailRendererModule : ParticleModule, IParticleComponentCreator\r\n{\r\n\t[Hide] public override ModuleStage Stage =\u003E 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( \u0022Opaque\u0022, 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\u003CParticleTrailRenderer\u003E();\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/// \u003Csummary\u003E\r\n/// Applies curl noise force for organic, swirling motion\r\n/// \u003C/summary\u003E\r\n[Title(\u0022Curl Noise\u0022), Category(\u0022Update\u0022), Icon(\u0022air\u0022)]\r\npublic partial class CurlNoiseModule : ParticleModule, IParticleUpdater\r\n{\r\n    [Hide]\r\n    public override ModuleStage Stage =\u003E ModuleStage.Update;\r\n    \r\n    [Property, Range(0f, 1000f)]\r\n    [Description(\u0022Strength of the curl noise effect\u0022)]\r\n    public FXParticleFloat Strength { get; set; } = 1.0f;\r\n    \r\n    [Property, Range(0.01f, 10f)]\r\n    [Description(\u0022Scale of the noise pattern - smaller values create tighter curls\u0022)]\r\n    public FXParticleFloat Scale { get; set; } = 1.0f;\r\n    \r\n    [Property, Range(0f, 10f)]\r\n    [Description(\u0022Speed at which the noise pattern evolves over time\u0022)]\r\n    public FXParticleFloat TimeScale { get; set; } = 1.0f;\r\n    \r\n    [Property]\r\n    [Description(\u0022Offset in the noise field\u0022)]\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 \u002B 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 \u002B= curl * Strength.GetValue( context.SystemComponent ) * delta * 10;\r\n    }\r\n\r\n    /// \u003Csummary\u003E\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    /// \u003C/summary\u003E\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 \u002B new Vector3(0, epsilon, 0), time).z - \r\n             SamplePotential(pos - new Vector3(0, epsilon, 0), time).z) -\r\n            (SamplePotential(pos \u002B 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 \u002B new Vector3(0, 0, epsilon), time).x - \r\n             SamplePotential(pos - new Vector3(0, 0, epsilon), time).x) -\r\n            (SamplePotential(pos \u002B 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 \u002B new Vector3(epsilon, 0, 0), time).y - \r\n             SamplePotential(pos - new Vector3(epsilon, 0, 0), time).y) -\r\n            (SamplePotential(pos \u002B 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    /// \u003Csummary\u003E\r\n    /// Sample a 3D potential field using Perlin-like noise\r\n    /// \u003C/summary\u003E\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 \u002B new Vector3(0, 0, 0), time),\r\n            Noise3D(pos \u002B new Vector3(31.416f, -47.853f, 12.793f), time),\r\n            Noise3D(pos \u002B new Vector3(-17.737f, 86.214f, -59.482f), time)\r\n        );\r\n    }\r\n\r\n    /// \u003Csummary\u003E\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    /// \u003C/summary\u003E\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 \u002B new Vector3(time, time * 0.7f, time * 0.5f);\r\n        \r\n        float noise = 0;\r\n        noise \u002B= MathF.Sin(p.x * 1.0f \u002B p.y * 1.3f) * 0.5f;\r\n        noise \u002B= MathF.Sin(p.y * 1.7f \u002B p.z * 0.9f) * 0.3f;\r\n        noise \u002B= MathF.Sin(p.z * 2.1f \u002B p.x * 1.1f) * 0.2f;\r\n        noise \u002B= MathF.Sin(p.x * 3.7f \u002B p.y * 2.3f \u002B 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, \u0022f\u0022, 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 = \u0022P\u0022;\r\n        ToggleButton.ToolTip = \u0022Toggle Parameter Mode\u0022;\r\n        ToggleButton.FixedWidth = Theme.RowHeight;\r\n        ToggleButton.Pressed = () =\u003E ToggleParameterMode();\r\n        ToggleButton.OnPaintOverride = PaintToggleButton;\r\n        Layout.Add(ToggleButton);\r\n\r\n        ModeSwitchButton = new Button();\r\n        ModeSwitchButton.Text = \u0022Mode\u0022;\r\n        ModeSwitchButton.OnPaintOverride = PaintButton;\r\n        ModeSwitchButton.Pressed = () =\u003E 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 \u002B= (p) =\u003E\r\n        {\r\n            if (p.Name == \u0022UseParameter\u0022)\r\n            {\r\n                Rebuild();\r\n                return;\r\n            }\r\n            \r\n            if (!Target.GetProperty(\u0022UseParameter\u0022).GetValue\u003Cbool\u003E())\r\n            {\r\n                // Only rebuild for Value changes when not using parameters\r\n                ValueProperty = Target.GetProperty(\u0022Value\u0022);\r\n                if (ValueProperty.TryGetAsObject(out ValueTarget))\r\n                {\r\n                    if (p.Name == \u0022Type\u0022 || p.Name == \u0022Evaluation\u0022)\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(\u0022UseParameter\u0022);\r\n        useParam.SetValue(!useParam.GetValue\u003Cbool\u003E());\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(\u0022UseParameter\u0022).GetValue\u003Cbool\u003E();\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, \u0022tune\u0022, 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(\u0022UseParameter\u0022).GetValue\u003Cbool\u003E())\r\n            return;\r\n\r\n        ValueProperty = Target.GetProperty(\u0022Value\u0022);\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(\u0022UseParameter\u0022).GetValue\u003Cbool\u003E();\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, \u0022block\u0022, 11, TextFlag.Center);\r\n            return true;\r\n        }\r\n\r\n        ValueProperty = Target.GetProperty(\u0022Value\u0022);\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(\u0022Type\u0022).GetValue\u003CParticleFloat.ValueType\u003E();\r\n        var eval = ValueTarget.GetProperty(\u0022Evaluation\u0022).GetValue\u003CParticleFloat.EvaluationType\u003E();\r\n\r\n        var icon = \u0022people\u0022;\r\n        float iconSize = 15;\r\n\r\n        if (type == ParticleFloat.ValueType.Constant)\r\n        {\r\n            icon = \u0022radio_button_unchecked\u0022;\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 = \u0022scatter_plot\u0022;\r\n            }\r\n\r\n            if (eval == ParticleFloat.EvaluationType.Life)\r\n            {\r\n                icon = \u0022play_arrow\u0022;\r\n            }\r\n\r\n            if (eval == ParticleFloat.EvaluationType.Frame)\r\n            {\r\n                icon = \u0022casino\u0022;\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(\u0022UseParameter\u0022).GetValue\u003Cbool\u003E();\r\n\r\n        if (useParam)\r\n        {\r\n            // Parameter mode - show parameter selector and multiplier\r\n            var paramName = Target.GetProperty(\u0022ParameterName\u0022);\r\n            var multiplier = Target.GetProperty(\u0022Multiplier\u0022);\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 = \u0022\u00D7\u0022\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(\u0022Value\u0022);\r\n            if (ValueProperty.TryGetAsObject(out ValueTarget))\r\n            {\r\n                var type = ValueTarget.GetProperty(\u0022Type\u0022).GetValue\u003CParticleFloat.ValueType\u003E();\r\n                var eval = ValueTarget.GetProperty(\u0022Evaluation\u0022).GetValue\u003CParticleFloat.EvaluationType\u003E();\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(\u0022ConstantValue\u0022))\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(\u0022ConstantA\u0022))\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(\u0022ConstantB\u0022))\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(\u0022CurveA\u0022))\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(\u0022CurveRange\u0022))\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 \u0022Constant value\u0022;\r\n            case ParticleFloat.ValueType.Curve:\r\n                {\r\n                    switch (eval)\r\n                    {\r\n                        case ParticleFloat.EvaluationType.Seed:\r\n                            return \u0022Random from curve per particle\u0022;\r\n                        case ParticleFloat.EvaluationType.Frame:\r\n                            return \u0022Random from curve\u0022;\r\n                        case ParticleFloat.EvaluationType.Life:\r\n                            return \u0022Curve over lifetime\u0022;\r\n                        default:\r\n                            return \u0022Unknown\u0022;\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 \u0022Random from range, per particle\u0022;\r\n                    case ParticleFloat.EvaluationType.Frame:\r\n                        return \u0022Random from range (per frame)\u0022;\r\n                    case ParticleFloat.EvaluationType.Life:\r\n                        return \u0022path between curve over lifetime, per particle\u0022;\r\n                    default:\r\n                        return \u0022Unknown\u0022;\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 \u0022Between range, per particle\u0022;\r\n                        case ParticleFloat.EvaluationType.Frame:\r\n                            return \u0022Random between range (per frame)\u0022;\r\n                        case ParticleFloat.EvaluationType.Life:\r\n                            return \u0022Lerp between range over lifetime\u0022;\r\n                        default:\r\n                            return \u0022Unknown\u0022;\r\n                    }\r\n                }\r\n        }\r\n\r\n        return \u0022Unknown Combo\u0022;\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(\u0022Type\u0022);\r\n        Eval = target.GetProperty(\u0022Evaluation\u0022);\r\n\r\n        AddQuickModes(Type.GetValue\u003CParticleFloat.ValueType\u003E(), Eval.GetValue\u003CParticleFloat.EvaluationType\u003E());\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(\u0022radio_button_unchecked\u0022, \u0022Constant\u0022, \u0022Value is constant. It stays the same. It doesn\u0027t change\u0022, ParticleFloat.ValueType.Constant, ParticleFloat.EvaluationType.Life));\r\n        grid.AddCell(1, 0, MakeQuickMode(\u0022casino\u0022, \u0022Random\u0022, \u0022Choose a value between two constants every frame\u0022, ParticleFloat.ValueType.Range, ParticleFloat.EvaluationType.Frame));\r\n\r\n        grid.AddCell(0, 1, MakeQuickMode(\u0022hdr_strong\u0022, \u0022Range\u0022, \u0022Choose a value between two constants\u0022, ParticleFloat.ValueType.Range, ParticleFloat.EvaluationType.Seed));\r\n        grid.AddCell(1, 1, MakeQuickMode(\u0022animation\u0022, \u0022Lerp\u0022, \u0022Lerp between two values over the lifetime of the particle\u0022, ParticleFloat.ValueType.Range, ParticleFloat.EvaluationType.Life));\r\n\r\n        grid.AddCell(0, 2, MakeQuickMode(\u0022show_chart\u0022, \u0022Curve\u0022, \u0022Get the value by querying a curve over the particle\u0027s lifetime\u0022, ParticleFloat.ValueType.Curve, ParticleFloat.EvaluationType.Life));\r\n        grid.AddCell(1, 2, MakeQuickMode(\u0022area_chart\u0022, \u0022Curve with Range\u0022, \u0022Choose a path between two curves - over the lifetime of the particle\u0022, 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(\u0022color: #ffffff;\u0022);\r\n\r\n        bool isCurrent = t == Type.GetValue\u003CParticleFloat.ValueType\u003E() \u0026\u0026 e == Eval.GetValue\u003CParticleFloat.EvaluationType\u003E();\r\n\r\n        b.MouseClick \u002B= () =\u003E\r\n        {\r\n            Type.SetValue(t);\r\n            Eval.SetValue(e);\r\n            Close();\r\n        };\r\n\r\n        b.OnPaintOverride = () =\u003E\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(\u0022font-size: 13px; font-family: Poppins; font-weight: bold;\u0022);\r\n        descLabel.SetStyles(\u0022font-size: 9px; font-family: Poppins;\u0022);\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 = \u0022P\u0022;\r\n        ToggleButton.ToolTip = \u0022Toggle Parameter Mode\u0022;\r\n        ToggleButton.FixedWidth = Theme.RowHeight;\r\n        ToggleButton.Pressed = () =\u003E 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 \u002B= (p) =\u003E\r\n        {\r\n            if (p.Name == \u0022UseParameter\u0022)\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(\u0022UseParameter\u0022);\r\n        useParam.SetValue(!useParam.GetValue\u003Cbool\u003E());\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(\u0022UseParameter\u0022).GetValue\u003Cbool\u003E();\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, \u0022tune\u0022, 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(\u0022UseParameter\u0022).GetValue\u003Cbool\u003E();\r\n\r\n        if (useParam)\r\n        {\r\n            // Parameter mode - show parameter selector and multiplier\r\n            var paramName = Target.GetProperty(\u0022ParameterName\u0022);\r\n            var multiplier = Target.GetProperty(\u0022Multiplier\u0022);\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 = \u0022\u00D7\u0022\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(\u0022Value\u0022);\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 = \u0022P\u0022;\r\n        ToggleButton.ToolTip = \u0022Toggle Parameter Mode\u0022;\r\n        ToggleButton.FixedWidth = Theme.RowHeight;\r\n        ToggleButton.Pressed = () =\u003E 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 \u002B= (p) =\u003E\r\n        {\r\n            if (p.Name == \u0022UseParameter\u0022)\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(\u0022UseParameter\u0022);\r\n        useParam.SetValue(!useParam.GetValue\u003Cbool\u003E());\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(\u0022UseParameter\u0022).GetValue\u003Cbool\u003E();\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, \u0022tune\u0022, 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(\u0022UseParameter\u0022).GetValue\u003Cbool\u003E();\r\n\r\n        if (useParam)\r\n        {\r\n            // Parameter mode - show parameter selector and multiplier\r\n            var paramName = Target.GetProperty(\u0022ParameterName\u0022);\r\n            var multiplier = Target.GetProperty(\u0022Multiplier\u0022);\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 = \u0022\u00D7\u0022\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(\u0022Value\u0022);\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/// \u003Csummary\u003E\r\n/// Particle system resource containing multiple emitters\r\n/// \u003C/summary\u003E\r\n[AssetType(Name = \u0022Particle System\u0022, Extension = \u0022fx\u0022, Category = \u0022FX\u0022, Flags = AssetTypeFlags.NoEmbedding)]\r\npublic class ParticleResource : GameResource\r\n{\r\n    public bool IsDirty { get; set; } = false;\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// All emitters in this particle system\r\n    /// \u003C/summary\u003E\r\n    public List\u003CParticleEmitter\u003E Emitters { get; set; } = new();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Named float parameters\r\n    /// \u003C/summary\u003E\r\n    [InlineEditor, DisplayName(\u0022FloatParameters\u0022)] public List\u003CFloatParameter\u003E FloatParameters { get; set; } = new();\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Named vector parameters\r\n    /// \u003C/summary\u003E\r\n    [InlineEditor] public List\u003CVectorParameter\u003E VectorParameters { get; set; } = new();\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Named color parameters\r\n    /// \u003C/summary\u003E\r\n    [InlineEditor] public List\u003CColorParameter\u003E ColorParameters { get; set; } = new();\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Global system properties\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Preview settings for the editor\r\n    /// \u003C/summary\u003E\r\n    public ParticlePreviewSettings PreviewSettings { get; set; } = new();\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Get a float parameter\u0027s default value by name\r\n    /// \u003C/summary\u003E\r\n    public float GetParameterDefault(string name)\r\n    {\r\n        var param = FloatParameters.FirstOrDefault(p =\u003E p.Name == name);\r\n        return param?.DefaultValue ?? 0f;\r\n    }\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Get a vector parameter\u0027s default value by name\r\n    /// \u003C/summary\u003E\r\n    public Vector3 GetVectorParameterDefault(string name)\r\n    {\r\n        var param = VectorParameters.FirstOrDefault(p =\u003E p.Name == name);\r\n        return param?.DefaultValue ?? Vector3.Zero;\r\n    }\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Get a color parameter\u0027s default value by name\r\n    /// \u003C/summary\u003E\r\n    public ParticleGradient GetColorParameterDefault(string name)\r\n    {\r\n        var param = ColorParameters.FirstOrDefault(p =\u003E p.Name == name);\r\n        return param?.DefaultValue ?? Color.White;\r\n    }\r\n    \r\n    /// \u003Csummary\u003E\r\n    /// Add a new float parameter\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Add a new vector parameter\r\n    /// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// Add a new color parameter\r\n    /// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// Preview settings for the particle editor\r\n/// \u003C/summary\u003E\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/// \u003Csummary\u003E\r\n/// A single particle emitter with its own spawn and update logic\r\n/// \u003C/summary\u003E\r\npublic class ParticleEmitter\r\n{\r\n\tpublic string Name { get; set; } = \u0022Emitter\u0022;\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/// \u003Csummary\u003E\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\u0027s handled by the engine\u0027s own particle system rather\r\n\t/// than anything FXBox has to gate itself.\r\n\t/// \u003C/summary\u003E\r\n\tpublic float Delay { get; set; } = 0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Overrides the system\u0027s own Duration for deciding when THIS emitter is finished\r\n\t/// (see FXBoxParticleController.IsFinished) - 0 means \u0022use the particle system\u0027s own\r\n\t/// Duration instead\u0022, 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/// \u003C/summary\u003E\r\n\tpublic float Duration { get; set; } = 0f;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Modules that run when spawning particles\r\n\t/// \u003C/summary\u003E\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List\u003CParticleModule\u003E SpawnModules { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Modules that run once when a particle is created\r\n\t/// \u003C/summary\u003E\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List\u003CParticleModule\u003E InitializeModules { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Modules that run every frame for each particle\r\n\t/// \u003C/summary\u003E\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List\u003CParticleModule\u003E UpdateModules { get; set; } = new();\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Modules that control how particles are rendered\r\n\t/// \u003C/summary\u003E\r\n\t[JsonConverter(typeof(ParticleModuleListConverter)), Hide]\r\n\tpublic List\u003CParticleModule\u003E RenderModules { get; set; } = new();\r\n}\r\n/// \u003Csummary\u003E\r\n/// Base class for all particle modules\r\n/// \u003C/summary\u003E\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    /// \u003Csummary\u003E\r\n    /// What stage this module belongs to\r\n    /// \u003C/summary\u003E\r\n    [JsonIgnore]\r\n    public abstract ModuleStage Stage { get; }\r\n\r\n    /// \u003Csummary\u003E\r\n    /// Execute this module\r\n    /// \u003C/summary\u003E\r\n    public abstract void Execute(ParticleExecutionContext context);\r\n\r\n    public abstract void Initialize( ParticleExecutionContext context );\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// JSON converter for List of ParticleModule\r\n/// \u003C/summary\u003E\r\npublic class ParticleModuleListConverter : JsonConverter\u003CList\u003CParticleModule\u003E\u003E\r\n{\r\n    public override List\u003CParticleModule\u003E Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)\r\n    {\r\n        var list = new List\u003CParticleModule\u003E();\r\n        \r\n        if (reader.TokenType != JsonTokenType.StartArray)\r\n            throw new JsonException(\u0022Expected start of array\u0022);\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(\u0022$type\u0022, out var typeProperty))\r\n                {\r\n                    Log.Warning(\u0022Missing $type property for ParticleModule\u0022);\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($\u0022Unknown module type: {typeName}\u0022);\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\u003CParticleModule\u003E 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(\u0022$type\u0022, 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( \u0022AddonTitle\u0022, \u0022FXBox\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022fxbox\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022stellawisps\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022stellawisps.fxbox\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002229\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-09-10T16:47:44.5430399Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.212.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.212.0\u0022)]"},{"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(\u0022Select a parameter from the system\u0022)]\r\n    public string ParameterName { get; set; }\r\n    \r\n    [Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]\r\n    [Description(\u0022Multiplier applied to the parameter value\u0022)]\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 \u0026\u0026 !string.IsNullOrEmpty(ParameterName) \u0026\u0026 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 \u0026\u0026 !string.IsNullOrEmpty(ParameterName) \u0026\u0026 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(\u0022Division: a is null\u0022);\r\n        return null;\r\n    }\r\n    \r\n    if (b == 0 || MathF.Abs(b) \u003C 0.0001f)\r\n    {\r\n        Log.Warning($\u0022Division by zero or very small number ({b}) in FXParticleFloat\u0022);\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(\u0022Multiplication: a is null\u0022);\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\u0027t copy it at all since we\u0027re 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\u0027T 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\u0027T 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 \u002B offset;\r\n            break;\r\n            \r\n        case ParticleFloat.ValueType.Range:\r\n            result.ConstantA = pf.ConstantA \u002B offset;\r\n            result.ConstantB = pf.ConstantB \u002B 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\u0027T 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 =\u003E \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 =\u003E \r\n            new Curve.Frame(frame.Time, frame.Value \u002B 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(\u0022Select a vector parameter from the system\u0022)]\r\n\tpublic string ParameterName { get; set; }\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]\r\n\t[Description(\u0022Multiplier applied to the parameter value\u0022)]\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 \u0026\u0026 !string.IsNullOrEmpty(ParameterName) \u0026\u0026 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(\u0022Select a color parameter from the system\u0022)]\r\n\tpublic string ParameterName { get; set; }\r\n    \r\n\t[Property, ShowIf(nameof(UseParameter), true), Range(0f, 10f)]\r\n\t[Description(\u0022Multiplier applied to the parameter value (affects RGB)\u0022)]\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 \u0026\u0026 !string.IsNullOrEmpty(ParameterName) \u0026\u0026 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/// \u003Csummary\u003E\r\n/// Custom control for parameter name selection with dropdown\r\n/// \u003C/summary\u003E\r\npublic class ParameterNameControlWidget : ControlWidget\r\n{\r\n    private SerializedProperty _property;\r\n    private ComboBox _dropdown;\r\n    private List\u003CFloatParameter\u003E _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\u003CFloatParameter\u003E();\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\u003Cstring\u003E();\r\n        if (!string.IsNullOrEmpty(currentValue))\r\n        {\r\n            SelectParameter(currentValue);\r\n        }\r\n\r\n        _dropdown.ItemChanged \u002B= OnValueChanged;\r\n        //_dropdown.On \u002B= 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(\u0022(No Parameters Available)\u0022);\r\n            _dropdown.Enabled = false;\r\n            return;\r\n        }\r\n\r\n        _dropdown.AddItem(\u0022(None)\u0022);\r\n        \r\n        foreach (var param in _availableParameters.OrderBy(p =\u003E p.Name))\r\n        {\r\n            _dropdown.AddItem($\u0022{param.Name} (default: {param.DefaultValue})\u0022);\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 =\u003E p.Name).ToList();\r\n        for (int i = 0; i \u003C orderedParams.Count; i\u002B\u002B)\r\n        {\r\n            if (orderedParams[i].Name == parameterName)\r\n            {\r\n                _dropdown.CurrentIndex = i \u002B 1; // \u002B1 because of \u0022(None)\u0022 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 \u003C= 0)\r\n        {\r\n            // \u0022(None)\u0022 selected\r\n            _property.SetValue(\u0022\u0022);\r\n            return;\r\n        }\r\n        \r\n        // Get the parameter name from the selected item\r\n        var orderedParams = _availableParameters.OrderBy(p =\u003E p.Name).ToList();\r\n        if (selectedIndex - 1 \u003C 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\u003CVectorParameter\u003E _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\u003CVectorParameter\u003E();\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\u003Cstring\u003E();\r\n        if (!string.IsNullOrEmpty(currentValue))\r\n        {\r\n            SelectParameter(currentValue);\r\n        }\r\n\r\n        _dropdown.ItemChanged \u002B= 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(\u0022(No Vector Parameters Available)\u0022);\r\n            _dropdown.Enabled = false;\r\n            return;\r\n        }\r\n\r\n        _dropdown.AddItem(\u0022(None)\u0022);\r\n        \r\n        foreach (var param in _availableParameters.OrderBy(p =\u003E p.Name))\r\n        {\r\n            _dropdown.AddItem($\u0022{param.Name} (default: {param.DefaultValue})\u0022);\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 =\u003E p.Name).ToList();\r\n        for (int i = 0; i \u003C orderedParams.Count; i\u002B\u002B)\r\n        {\r\n            if (orderedParams[i].Name == parameterName)\r\n            {\r\n                _dropdown.CurrentIndex = i \u002B 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 \u003C= 0)\r\n        {\r\n            _property.SetValue(\u0022\u0022);\r\n            return;\r\n        }\r\n        \r\n        var orderedParams = _availableParameters.OrderBy(p =\u003E p.Name).ToList();\r\n        if (selectedIndex - 1 \u003C 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\u003CColorParameter\u003E _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\u003CColorParameter\u003E();\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\u003Cstring\u003E();\r\n        if (!string.IsNullOrEmpty(currentValue))\r\n        {\r\n            SelectParameter(currentValue);\r\n        }\r\n\r\n        _dropdown.ItemChanged \u002B= 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(\u0022(No Color Parameters Available)\u0022);\r\n            _dropdown.Enabled = false;\r\n            return;\r\n        }\r\n\r\n        _dropdown.AddItem(\u0022(None)\u0022);\r\n        \r\n        foreach (var param in _availableParameters.OrderBy(p =\u003E p.Name))\r\n        {\r\n            _dropdown.AddItem($\u0022{param.Name}\u0022);\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 =\u003E p.Name).ToList();\r\n        for (int i = 0; i \u003C orderedParams.Count; i\u002B\u002B)\r\n        {\r\n            if (orderedParams[i].Name == parameterName)\r\n            {\r\n                _dropdown.CurrentIndex = i \u002B 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 \u003C= 0)\r\n        {\r\n            _property.SetValue(\u0022\u0022);\r\n            return;\r\n        }\r\n        \r\n        var orderedParams = _availableParameters.OrderBy(p =\u003E p.Name).ToList();\r\n        if (selectedIndex - 1 \u003C 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"}]}