Code/EmittersShapes/ConchplexDonutEmitter.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConchplexEmitters;

public sealed class ConchplexDonutEmitter : 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;

	/// <summary>
	/// Bias partcles inwards or outwards on a each axis
	/// </summary>
	[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]
	public float MainRadius { get; set; } = 20f;

	[Property]
	public float RingRadius { get; set; } = 7.5f;

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


	[Property, Group("Initial Velocity")]
	public ParticleFloat VelocityFromDonut { get; set; } = 0f;

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

		Gizmo.Draw.Color = Color.White.WithAlpha(0.5f);
		Gizmo.GizmoDraw draw = Gizmo.Draw;

		// draw shape
		draw.LineCircle(Vector3.Zero, Vector3.Up, MainRadius + RingRadius, 16);
		draw.LineCircle(Vector3.Zero, Vector3.Up, MainRadius - RingRadius, 16);
		draw.LineCircle(Vector3.Zero.WithZ(RingRadius), Vector3.Up, MainRadius, 16);
		draw.LineCircle(Vector3.Zero.WithZ(-RingRadius), Vector3.Up, MainRadius, 16);

		draw.LineCircle(Vector3.Zero.WithX(MainRadius), Vector3.Left, RingRadius, 8);
		draw.LineCircle(Vector3.Zero.WithX(-MainRadius), Vector3.Left, RingRadius, 8);
		draw.LineCircle(Vector3.Zero.WithY(MainRadius), Vector3.Forward, RingRadius, 8);
		draw.LineCircle(Vector3.Zero.WithY(-MainRadius), Vector3.Forward, RingRadius, 8);

		// draw thickness
		if (Thickness > 0f && Thickness < 1f)
		{
			draw.LineCircle(Vector3.Zero, Vector3.Up, MainRadius + (RingRadius * Thickness), 16);
			draw.LineCircle(Vector3.Zero, Vector3.Up, MainRadius - (RingRadius * Thickness), 16);
			draw.LineCircle(Vector3.Zero.WithZ(RingRadius), Vector3.Up, MainRadius, 16);
			draw.LineCircle(Vector3.Zero.WithZ(-RingRadius * Thickness), Vector3.Up, MainRadius, 16);

			draw.LineCircle(Vector3.Zero.WithX(MainRadius), Vector3.Left, RingRadius * Thickness, 8);
			draw.LineCircle(Vector3.Zero.WithX(-MainRadius), Vector3.Left, RingRadius * Thickness, 8);
			draw.LineCircle(Vector3.Zero.WithY(MainRadius), Vector3.Forward, RingRadius * Thickness, 8);
			draw.LineCircle(Vector3.Zero.WithY(-MainRadius), Vector3.Forward, RingRadius * Thickness, 8);
		}

		// draw Distance Bias
		if (DistanceBias > 0f && DistanceBias < 1f)
		{
			Gizmo.Draw.Color = Color.Red.WithAlpha(0.4f);
			var biasRadius = MathX.Lerp(RingRadius * Thickness, RingRadius, DistanceBias);

			draw.LineCircle(Vector3.Zero, Vector3.Up, MainRadius + biasRadius, 16);
			draw.LineCircle(Vector3.Zero, Vector3.Up, MainRadius - biasRadius, 16);
			draw.LineCircle(Vector3.Zero.WithZ(biasRadius), Vector3.Up, MainRadius, 16);
			draw.LineCircle(Vector3.Zero.WithZ(-biasRadius), Vector3.Up, MainRadius, 16);

			draw.LineCircle(Vector3.Zero.WithX(MainRadius), Vector3.Left, biasRadius, 8);
			draw.LineCircle(Vector3.Zero.WithX(-MainRadius), Vector3.Left, biasRadius, 8);
			draw.LineCircle(Vector3.Zero.WithY(MainRadius), Vector3.Forward, biasRadius, 8);
			draw.LineCircle(Vector3.Zero.WithY(-MainRadius), Vector3.Forward, biasRadius, 8);
		}

		if (PositionBias.IsNearlyZero()) return;
		Gizmo.Draw.Color = Color.Red.WithAlpha(0.8f);
		Vector3 biasClamped = Vector3.Clamp(PositionBias, new Vector3(-1f), new Vector3(1f)) * (MainRadius + RingRadius);
		draw.Arrow(Vector3.Zero, biasClamped, biasClamped.Length * 0.1f, biasClamped.Length * 0.1f);
	}

	public override Particle Emit(ParticleEffect target)
	{
		// radius point.
		var mainRadius = Random.Shared.VectorInCircle();

		// radius pos bias
		mainRadius += new Vector2(PositionBias.x, PositionBias.y);
		mainRadius = mainRadius.Normal;

		

		// Random point around the edge of a unit circle
		Vector3 random = new Vector3(mainRadius.x, mainRadius.y, 0f);
		random *= MainRadius;

		// get random point on the ring 
		var randomRing = Random.Shared.VectorInCircle();

		

		var biasedRing = randomRing;
		biasedRing.y += PositionBias.z;
		var ringPosBias = new Vector2(PositionBias.x, PositionBias.y).Length;
		biasedRing.x += ringPosBias;

		var outterRing = biasedRing.Normal * RingRadius;
		var innerRing = outterRing * Thickness;
		var betweenRandomRing = Vector2.Lerp(innerRing, outterRing, randomRing.Length);

		var ringAdjustment = Vector2.Zero;
		// apply distance bias
		if (DistanceBias <= 0.5f)
		{
			float scaledBias = DistanceBias / 0.5f;
			ringAdjustment = Vector2.Lerp(innerRing, betweenRandomRing, scaledBias);
		}
		else
		{
			float scaledBias = (DistanceBias - 0.5f) / 0.5f;
			ringAdjustment = Vector2.Lerp(betweenRandomRing, outterRing, scaledBias);
		}

		var randomXY = new Vector2(random.x, random.y);

		random.x += randomXY.Normal.x * ringAdjustment.x;
		random.y += randomXY.Normal.y * ringAdjustment.x;
		random.z += ringAdjustment.y;

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

		// TODO - we should clamp position and velocity before calculating world position/rotation position/velocity
		var donutVelocityDir = new Vector3(randomXY.Normal.x * ringAdjustment.x, randomXY.Normal.y * ringAdjustment.x, ringAdjustment.y).Normal;

		var clampedParticlePoint = ProcessClampPosition(random, donutVelocityDir, AxisClamp);
		var finalPos = (clampedParticlePoint.Position * LocalScale * LocalRotation) + WorldPosition;
		var finalParticleVel = clampedParticlePoint.Velocity * LocalRotation;

		Particle particle = target.Emit(finalPos, base.Delta);
		particle.Velocity += finalParticleVel * VelocityFromDonut.Evaluate(Delta,Random.Shared.Float());

		return particle;
	}
}