EmittersShapes/ConchplexSphereEmitter.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConchplexEmitters;


/// <summary>
/// Emits particles within a ring
/// </summary>
[Title( "Conchplex Sphere Emitter" ), Category( "Particles" ), Icon( "panorama_photosphere" )]
public sealed class ConchplexSphereEmitter : ConchplexEmitter
{
	/// <summary>
	/// 1 = spawns on edge, 0 = spawns in centre
	/// </summary>
	[Property, Group("Spawn Bias"), Order(0)]
	[Range(0f, 1f), Step(0.01f)]
	public float DistanceBias { get; set; } = 0.5f;

	/// <summary>
	/// Direction + intensity to bias particles to spawn towards
	/// </summary>
	[Property, Group("Spawn Bias"), Order(0)]
	[Range(-1, 1)]
	public Vector3 PositionBias { get; set; } = 0f;

	[Property, Group("Spawn Bias"), Order(0)]
	[Range(-1, 1)]
	public Vector3 AxisBias { get; set; } = 0f;
	/// <summary>
	/// Particles only spawn on the axes marked
	/// </summary>
	[Property, Group("Spawn Bias"), Order(0)]
	public AxisClampFlags AxisClamp { get; set; }

	[Property]
	[Range( 0f, 100f), Step(0.01f)]
	public float Radius { get; set; } = 25f;

	[Property]
	[Range(0f, 1f), Step(0.01f)]
	public float Thickness { get; set; } = 0f;

	protected override void DrawGizmos()
	{
		if (Gizmo.IsSelected)
		{
			Gizmo.Draw.Color = Color.White.WithAlpha(0.5f);
			Gizmo.GizmoDraw draw = Gizmo.Draw;

			// Draw max bounds
			Vector3 point = Vector3.Zero;
			float radius = Radius;
			int rings = 8;
			draw.LineSphere(in point, in radius, in rings);

			// Draw min bounds
			Gizmo.Draw.Color = Color.White.WithAlpha(0.6f);
			float minRadius = Radius * Thickness;
			draw.LineSphere(point, minRadius, rings);

			// Draw Distance Bias
			Gizmo.Draw.Color = Color.Red.WithAlpha(0.5f);
			float biasRadius = MathX.Lerp(minRadius, radius, DistanceBias);
			draw.LineSphere(point, biasRadius, rings);

			// draw Position Bias
			if (PositionBias.IsNearlyZero()) return;
			Gizmo.Draw.Color = Color.Red.WithAlpha(0.8f);
			Vector3 biasClamped = Vector3.Clamp(PositionBias, new Vector3(-1f), new Vector3(1f)) * radius * 1.1f;
			draw.Arrow(point, biasClamped, biasClamped.Length*0.1f, biasClamped.Length * 0.1f);
		}
	}

	public override Particle Emit(ParticleEffect target )
	{
		Vector3 random = Vector3.Random;
		Vector3 radius = Radius;
		Vector3 radiusMin = Radius * Thickness;


		// POSITION BIAS
		var posBias = PositionBias.ClampLength(1);
		var randomFlipChance = Random.Shared.Float();
		random = new Vector3(
			MathF.Abs(posBias.x) > randomFlipChance & Vector3.Dot(random, posBias) < 0f ? -random.x : random.x,
			MathF.Abs(posBias.y) > randomFlipChance & Vector3.Dot(random, posBias) < 0f ? -random.y : random.y,
			MathF.Abs(posBias.z) > randomFlipChance & Vector3.Dot(random, posBias) < 0f ? -random.z : random.z
		);
		random = Vector3.Lerp( random, posBias + random, posBias.Length);

		// AXIS BIAS
		random = ApplyAxisBias(random, AxisBias);

		// MIN / MAX / TRUE RANDOM
		var targetRandomLenth = MathX.Lerp( Radius * Thickness, Radius, random.Length );
		random = random.Normal * targetRandomLenth;
		var maxEdgePos = random.Normal * radius;
		var minEdgePos = random.Normal * radiusMin;

		// DISTANCE BIAS
		if ( DistanceBias <= 0.5f )
		{
			float scaledBias = DistanceBias / 0.5f;
			random = Vector3.Lerp( minEdgePos, random, scaledBias );
		}
		else
		{
			float scaledBias = (DistanceBias - 0.5f) / 0.5f;
			random = Vector3.Lerp( random, maxEdgePos, scaledBias );
		}

		// APPLY SCALE / POSITION / ROTATION
		var finalPos = ProcessClampPosition( random, AxisClamp);
		finalPos = (finalPos * LocalScale * LocalRotation) + WorldPosition;
		return target.Emit(finalPos, base.Delta );
	}
}