EmittersShapes/ConchplexEmitterShape.cs
using Sandbox;
using Sandbox.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using static Sandbox.PhysicsContact;
using static System.Runtime.InteropServices.JavaScript.JSType;

namespace ConchplexEmitters;



/// <summary>
/// Defines the shape of an emitter, optionally you can emit dirrectly from this shape
/// </summary>
public abstract class ConchplexEmitter : Sandbox.Component, Sandbox.Component.ExecuteInEditor, Sandbox.Component.ITemporaryEffect
{
	public float time;
	public int emitted;
	private int loops;
	private bool suspended;

	[Property, Group("Initial Velocity"), Order(0)]
	public ParticleVector3 InitialVelocity { get; set; } = Vector3.Zero;

	[Property, Group("Initial Velocity"), Order(0)]
	public float VelocityFromCentre { get; set; } = 0f;

	// If we are emitting what effect should we target
	//[Property, Feature("Emitter")]
	public ParticleEffect target;

	[Property, Feature("Emitter"), Order(2)]
	public bool Loop { get; set; } = false;

	/// <summary>
	/// How many times to loop - 0 = Infinite
	/// </summary>
	[Property, Feature("Emitter"), Order(2), ShowIf(nameof(Loop), true)]
	public int LoopAmmount { get; set; } = 0;

	/// <summary>
	/// How long should the emitter go for
	/// </summary>
	[Property, Feature("Emitter"), Order(2)]
	public float Duration { get; set; } = 10f;

	[Property, Feature("Emitter"), Order(2)]
	public float Delay { get; set; }

	[Property, Feature("Emitter"), Order(2)]
	public bool DestroyOnEnd { get; set; }


	// EMISSIONS

	[Property, Feature("Emitter"), Category("Emissions"), Order(2)]
	public ParticleFloat RateOverTime { get; set; }

	[Property, Feature("Emitter"), Category("Emissions"), Order(2)]
	public int RateOverDistance { get; set; }
	[Hide] private Vector3? lastPos;
	[Hide] private float distanceTravelled;

	[Property, Feature("Emitter"), Category("Emissions"), WideMode, Order(2)]
	public List<ConchplexBurstEmission> Bursts { get; set; } = new List<ConchplexBurstEmission>();



	public float Delta { get; private set; }
	public float EmitRandom { get; private set; }
	public bool IsStarted => time - Delay >= 0f;
	private bool IsFinished => time > Duration + Delay;

	bool ITemporaryEffect.IsActive
	{
		get
		{
			if (suspended)
			{
				return false;
			}

			if (Loop)
			{
				return true;
			}

			foreach (var burst in Bursts)
			{
				if (burst.IsActive()) return true;
			}

			if (!IsStarted)
			{
				return true;
			}

			if (IsFinished)
			{
				return false;
			}

			target.IsValid();
			return false;
		}
	}

	protected override void OnEnabled()
	{
		suspended = false;
		ResetEmitter();
		loops = 0;
		// If there is no overrided target set, then try get target from the object
		if ( target == null) target = base.Components.GetInAncestorsOrSelf<ParticleEffect>();
		target = base.Components.GetInAncestorsOrSelf<ParticleEffect>();
		if (target != null)
		{
			ParticleEffect particleEffect = target;
			particleEffect.OnPreStep = (Action<float>)Delegate.Combine(particleEffect.OnPreStep, new Action<float>(OnParticleStep));
		}
		else
		{
			GlobalSystemNamespace.Log.Warning($"No particle effect found for {this}");
		}

		foreach (var burst in Bursts) burst.ResetEmission();
		lastPos = null;
	}

	protected override void OnDisabled()
	{
		base.OnDisabled();
		if (target != null)
		{
			ParticleEffect particleEffect = target;
			particleEffect.OnPreStep = (Action<float>)Delegate.Remove(particleEffect.OnPreStep, new Action<float>(OnParticleStep));
		}

		target = null;

	}

	private void DestroyEmitter()
	{
		// Try destroy the effect, if its no longer needed
		var allEmitters = GameObject.GetComponentsInChildren<ParticleEffect>(includeSelf: false);
		bool stillHasChildEffects = allEmitters.Count() > 0;

		// there are still child effects in progress / this effect is still running, so return
		if (stillHasChildEffects || target.Particles.Count != 0) return;

		bool isSoloEmitter = GameObject.Components.GetAll<ConchplexEmitter>().Count() == 1;

		if (isSoloEmitter) GameObject.Destroy();
		else Destroy();

	}
	public void ResetEmitter()
	{
		loops++;
		emitted = 0;
		time = 0f;
		EmitRandom = Random.Shared.Float(0f, 1f);
		foreach (var burst in Bursts) burst.ResetEmission();
	}

	private void OnParticleStep(float delta)
	{
		if (!target.IsValid() || !target.Active || suspended )
		{
			return;
		}
		// Set current time we have been emitting since we started
		time += delta;
		// Get time minus dely, so its only positive after the delay is over
		float postDelayTime = time - Delay;

		Delta = 0f;
		if (!IsStarted)
		{
			return;
		}

		// Emitter has reached final duration
		if (IsFinished)
		{
			if (Loop)
			{
				if (LoopAmmount == 0 || loops <= LoopAmmount)
					ResetEmitter();
				else
					return;
			}
			else
			{
				if (base.Scene.IsEditor && !base.GameObject.Flags.HasFlag(GameObjectFlags.NotSaved))
				{
					ResetEmitter();
				}

				if (DestroyOnEnd && !base.Scene.IsEditor)
				{
					DestroyEmitter();
				}

			}
			return;
		}


		Delta = time.Remap(Delay, Duration + Delay);

		while (!target.IsFull && emitted < GetRateCount() * (postDelayTime))
		{
			emitted += 1;
			EmitParticle(target);
		}

		if (RateOverDistance > 0) DistanceEmit(target, RateOverDistance);

		foreach (var burst in Bursts)
		{
			burst.TryEmit(target ,this, postDelayTime);
		}
		
	}
	void ITemporaryEffect.DisableLooping()
	{
		suspended = true;
	}

	protected virtual int GetRateCount()
	{
		var rate = RateOverTime;
		float delta = Delta;
		float randomFixed = 1f;
		return (int)rate.Evaluate(delta, in randomFixed);
	}

	public abstract Particle Emit(ParticleEffect target);

	public void DistanceEmit(ParticleEffect target, int distance)
	{
		// Set last world position
		Vector3 worldPosition = WorldPosition;
		if (!lastPos.HasValue)
		{
			lastPos = worldPosition;
			return;
		}

		float length = (lastPos.Value - worldPosition).Length;
		float num = 100f / distance;
		lastPos = worldPosition;
		distanceTravelled += length;
		while (distanceTravelled > num)
		{
			distanceTravelled -= num;
			if (!target.IsFull)
			{
				EmitParticle(target);
			}
		}
	}

	public void EmitParticle(ParticleEffect target)
	{
		var particle = Emit(target);
		var randomSeed = Random.Shared.Float(0f, 1f);
		particle.Velocity += (InitialVelocity.Evaluate(Delta, randomSeed, randomSeed, randomSeed) * WorldRotation);
		var particlePos = particle.Position - WorldPosition;
		particle.Velocity += particlePos.Normal * VelocityFromCentre;
	}

	public Vector3 ApplyAxisBias(Vector3 random, Vector3 axisBias)
	{
		Vector3 result = random;

		// Helper to apply bias to a Vector2 slice
		Vector2 ApplyBias(Vector2 vec2, float bias)
		{
			if (bias == 0f) return vec2; // no change

			Vector2 min = Vector2.Zero;
			Vector2 max = vec2.Normal;

			return bias < 0f
				? Vector2.Lerp(vec2, min, -bias) // pull inward
				: Vector2.Lerp(vec2, max, bias); // push outward
		}

		// X bias affects YZ
		Vector2 yz = new Vector2(result.y, result.z);
		yz = ApplyBias(yz, axisBias.x);
		result.y = yz.x;
		result.z = yz.y;

		// Y bias affects XZ
		Vector2 xz = new Vector2(result.x, result.z);
		xz = ApplyBias(xz, axisBias.y);
		result.x = xz.x;
		result.z = xz.y;

		// Z bias affects XY
		Vector2 xy = new Vector2(result.x, result.y);
		xy = ApplyBias(xy, axisBias.z);
		result.x = xy.x;
		result.y = xy.y;

		return result;
	}

	/// <summary>
	/// Which axis to clamp to / in between
	/// </summary>
	[Flags]
	public enum AxisClampFlags
	{
		None = 0,
		XPositive = 1 << 0,
		XNegative = 1 << 1,
		YPositive = 1 << 2,
		YNegative = 1 << 3,
		ZPositive = 1 << 4,
		ZNegative = 1 << 5
	}

	public Vector3 ProcessClampPosition(Vector3 randomPosition, AxisClampFlags flags)
	{
		Vector3 resultPos = randomPosition;

		// Process X axis
		resultPos.x = ProcessAxis(
			randomPosition.x,
			(flags & AxisClampFlags.XPositive) != 0,
			(flags & AxisClampFlags.XNegative) != 0);

		// Process Y axis
		resultPos.y = ProcessAxis(
			randomPosition.y,
			(flags & AxisClampFlags.YPositive) != 0,
			(flags & AxisClampFlags.YNegative) != 0);

		// Process Z axis
		resultPos.z = ProcessAxis(
			randomPosition.z,
			(flags & AxisClampFlags.ZPositive) != 0,
			(flags & AxisClampFlags.ZNegative) != 0);

		return resultPos;
	}

	public (Vector3 Position, Vector3 Velocity) ProcessClampPosition(Vector3 randomPosition, Vector3 velocity, AxisClampFlags flags)
	{
		Vector3 resultPos = randomPosition;
		Vector3 resultVelocity = velocity;

		// Process X axis
		(resultPos.x, resultVelocity.x) = ProcessAxis(
			resultPos.x, resultVelocity.x,
			(flags & AxisClampFlags.XPositive) != 0,
			(flags & AxisClampFlags.XNegative) != 0);

		// Process Y axis
		(resultPos.y, resultVelocity.y) = ProcessAxis(
			resultPos.y, resultVelocity.y,
			(flags & AxisClampFlags.YPositive) != 0,
			(flags & AxisClampFlags.YNegative) != 0);

		// Process Z axis
		(resultPos.z, resultVelocity.z) = ProcessAxis(
			resultPos.z, resultVelocity.z,
			(flags & AxisClampFlags.ZPositive) != 0,
			(flags & AxisClampFlags.ZNegative) != 0);

		return (resultPos, resultVelocity);
	}

	private (float pos, float vel) ProcessAxis(float pos, float vel, bool positive, bool negative)
	{
		float originalPos = pos;

		if (positive && negative)
		{
			pos = 0;
			vel = 0;
		}
		else if (positive)
		{
			pos = MathF.Abs(pos); // Flip negative to positive
		}
		else if (negative)
		{
			pos = -MathF.Abs(pos); // Flip positive to negative
		}

		// If position was changed, flip velocity
		if (pos != originalPos)
		{
			vel *= -1;
		}

		return (pos, vel);
	}

	private float ProcessAxis(float pos, bool positive, bool negative)
	{
		if (positive && negative)
		{
			pos = 0;
		}
		else if (positive)
		{
			pos = MathF.Abs(pos); // Flip negative to positive
		}
		else if (negative)
		{
			pos = -MathF.Abs(pos); // Flip positive to negative
		}

		return (pos);
	}
}