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);
}
}