EmittersShapes/ConchplexBoxEmitter.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace ConchplexEmitters;
/// <summary>
/// Emits particles within a box min/max bounds
/// </summary>
[Title("Conchplex Box Emitter"), Category("Particles"), Icon("border_outer")]
public sealed class ConchplexBoxEmitter : ConchplexEmitter
{
// Emitter shape
/// <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]
public Vector3 Size { get; set; } = 50f;
[Property]
[Range(0f, 1f), Step(0.01f)]
public float Thickness { get; set; } = 0f;
[Property, Group("Initial Velocity")]
public float VelocityFromSquare { get; set; } = 0f;
protected override void DrawGizmos()
{
if (Gizmo.IsSelected)
{
var point = Vector3.Zero;
Gizmo.Draw.Color = Color.White.WithAlpha(0.5f);
Gizmo.GizmoDraw draw = Gizmo.Draw;
// Draw max bounds
BBox box = BBox.FromPositionAndSize(point, Size);
draw.LineBBox(box);
// Draw min bounds
BBox boxMin = BBox.FromPositionAndSize(point, Size * Thickness);
draw.LineBBox(boxMin);
// Draw Distance Bias
Gizmo.Draw.Color = Color.Red.WithAlpha(0.5f);
float biasX = MathX.Lerp(Size.x, Size.x * Thickness, 1 - DistanceBias);
float biasY = MathX.Lerp(Size.y, Size.y * Thickness, 1 - DistanceBias);
float biasZ = MathX.Lerp(Size.z, Size.z * Thickness, 1 - DistanceBias);
BBox biasBox = BBox.FromPositionAndSize(point, new Vector3(biasX, biasY, biasZ) + point);
draw.LineBBox(biasBox);
if (PositionBias.IsNearlyZero()) return;
Gizmo.Draw.Color = Color.Red.WithAlpha(0.8f);
Vector3 biasClamped = Vector3.Clamp(PositionBias * 300f, new Vector3(-Size * 0.5f), new Vector3(Size * 0.5f));
draw.Arrow(point, biasClamped, biasClamped.Length * 0.1f, biasClamped.Length * 0.1f);
}
}
public override Particle Emit(ParticleEffect target)
{
Vector3 random = Random.Shared.VectorInCube(0.5f);
Vector3 sizeMin = Size * Thickness;
Vector3 sizeMax = Size;
Vector3 targetSize = Vector3.Lerp(sizeMin, sizeMax, DistanceBias);
Vector3 surfaceDirrection = Vector3.Zero;
// POSITION BIAS
var posBias = Vector3.Clamp(PositionBias, new Vector3(-1f), new Vector3(1f));
float biasStrength = posBias.Length;
if (biasStrength > 0f)
{
float randomFlipChance = Random.Shared.Float();
if (MathF.Abs(posBias.x) > randomFlipChance && MathF.Sign(posBias.x) != MathF.Sign(random.x))
random.x = -random.x;
if (MathF.Abs(posBias.y) > randomFlipChance && MathF.Sign(posBias.y) != MathF.Sign(random.y))
random.y = -random.y;
if (MathF.Abs(posBias.z) > randomFlipChance && MathF.Sign(posBias.z) != MathF.Sign(random.z))
random.z = -random.z;
random = Vector3.Lerp(random, random + posBias * 0.5f, biasStrength);
}
// Figure out Min / Max Distance bias
var maxEdgePos = RandomPositionOnBoxFace(random, Size, posBias, out surfaceDirrection);
var minEdgePos = maxEdgePos * Thickness;
// lol this is really stupid and nasty but it works!
var fuckedUpRandom = Random.Shared.Float();
var randomBetweenPos = Vector3.Lerp(maxEdgePos, minEdgePos, fuckedUpRandom * fuckedUpRandom * fuckedUpRandom);
// Apply Distance Bias
if (DistanceBias <= 0.5f)
{
float scaledBias = DistanceBias / 0.5f;
random = Vector3.Lerp(minEdgePos, randomBetweenPos, scaledBias);
}
else
{
float scaledBias = (DistanceBias - 0.5f) / 0.5f;
random = Vector3.Lerp(randomBetweenPos, maxEdgePos, scaledBias);
}
var clampedParticlePoint = ProcessClampPosition(random, surfaceDirrection, AxisClamp);
var pos = (clampedParticlePoint.Position * LocalScale * LocalRotation) + WorldPosition;
var velocity = clampedParticlePoint.Velocity * LocalRotation;
Particle particle = target.Emit(pos, base.Delta);
particle.Velocity += velocity * VelocityFromSquare;
return particle;
}
// this ensure particles are placed evenly on faces + corrects any overshooting
Vector3 RandomPositionOnBoxFace(Vector3 randomPos, Vector3 boxSize, Vector3 positionBias, out Vector3 surfaceDirrection)
{
// Scale the random position to the box size
randomPos = Vector3.Clamp(randomPos, new Vector3(-0.5f), new Vector3(0.5f));
var scaledPos = randomPos * boxSize;
// Determine valid faces for selection
List<(int axis, bool positiveSide)> validFaces = new List<(int, bool)>();
for (int i = 0; i < 3; i++) // Iterate over X, Y, Z axes
{
if (positionBias[i] >= 0) // Allow positive face if bias is >= 0
validFaces.Add((i, true));
if (positionBias[i] <= 0) // Allow negative face if bias is <= 0
validFaces.Add((i, false));
}
if (validFaces.Count == 0)
throw new InvalidOperationException("No valid faces available due to position bias.");
// Randomly select a valid face
var selectedFace = validFaces[Random.Shared.Next(validFaces.Count)];
int fixedAxis = selectedFace.axis;
bool positiveSide = selectedFace.positiveSide;
// Push the position to the selected face
scaledPos[fixedAxis] = positiveSide ? (boxSize[fixedAxis] * 0.5f) : -(boxSize[fixedAxis] * 0.5f);
surfaceDirrection = Vector3.Zero;
surfaceDirrection[fixedAxis] = positiveSide ? 1f : -1f;
return scaledPos;
}
}