using System;
using System.Collections.Generic;
using System.Numerics;
namespace AcDream.Core.Vfx;
///
/// Retail particle motion integrators from ParticleType in
/// acclient.h. Values are the retail dat values.
///
public enum ParticleType
{
Unknown = 0,
Still = 1,
LocalVelocity = 2,
ParabolicLVGA = 3,
ParabolicLVGAGR = 4,
Swarm = 5,
Explode = 6,
Implode = 7,
ParabolicLVLA = 8,
ParabolicLVLALR = 9,
ParabolicGVGA = 10,
ParabolicGVGAGR = 11,
GlobalVelocity = 12,
NumParticleType = 13,
}
///
/// Retail EmitterType from acclient.h.
///
public enum ParticleEmitterKind
{
Unknown = 0,
BirthratePerSec = 1,
BirthratePerMeter = 2,
}
///
/// Render stage for an active particle emitter.
///
public enum ParticleRenderPass
{
Scene = 0,
SkyPreScene = 1,
SkyPostScene = 2,
}
[Flags]
public enum EmitterFlags : uint
{
None = 0,
Additive = 0x01,
Billboard = 0x02,
FaceCamera = 0x04,
AttachLocal = 0x08,
}
///
/// Per-emitter configuration from the retail ParticleEmitterInfo
/// dat object.
///
public sealed class EmitterDesc
{
public uint DatId { get; init; }
public ParticleType Type { get; init; }
public ParticleEmitterKind EmitterKind { get; init; } = ParticleEmitterKind.BirthratePerSec;
public EmitterFlags Flags { get; init; }
public uint TextureSurfaceId { get; init; }
public uint GfxObjId { get; init; }
public uint HwGfxObjId { get; init; }
public uint SoundOnSpawn { get; init; }
// Emission behavior.
public float Birthrate { get; init; }
public float EmitRate { get; init; }
public int MaxParticles { get; init; }
public int InitialParticles { get; init; }
public int TotalParticles { get; init; }
public float LifetimeMin { get; init; }
public float LifetimeMax { get; init; }
public float Lifespan { get; init; }
public float LifespanRand { get; init; }
public float StartDelay { get; init; }
public float TotalDuration { get; init; }
// Spawn geometry.
public Vector3 OffsetDir { get; init; } = new(0, 0, 1);
public float MinOffset { get; init; }
public float MaxOffset { get; init; }
public float SpawnDiskRadius { get; init; }
// Kinematics. A/B/C are the retail vector coefficients.
public Vector3 InitialVelocity { get; init; }
public float VelocityJitter { get; init; }
public Vector3 Gravity { get; init; } = new(0, 0, -9.8f);
public Vector3 A { get; init; }
public float MinA { get; init; } = 1f;
public float MaxA { get; init; } = 1f;
public Vector3 B { get; init; }
public float MinB { get; init; } = 1f;
public float MaxB { get; init; } = 1f;
public Vector3 C { get; init; }
public float MinC { get; init; } = 1f;
public float MaxC { get; init; } = 1f;
// Appearance over lifetime.
public uint StartColorArgb { get; init; } = 0xFFFFFFFF;
public uint EndColorArgb { get; init; } = 0xFFFFFFFF;
public float StartAlpha { get; init; } = 1f;
public float EndAlpha { get; init; } = 0f;
public float StartSize { get; init; } = 0.5f;
public float EndSize { get; init; } = 0.5f;
public float ScaleRand { get; init; }
public float TransRand { get; init; }
public float StartRotation { get; init; }
public float EndRotation { get; init; }
}
///
/// A PhysicsScript (0x3Axxxxxx range in retail) is a list of hooks to
/// fire at specific start-times. Each hook creates an emitter or plays
/// a sound. Chaining hooks at different times gives "animation".
///
public sealed class PhysicsScript
{
public uint ScriptId { get; init; }
public IReadOnlyList Hooks { get; init; } = Array.Empty();
}
public sealed record PhysicsScriptHook(
float StartTime,
PhysicsScriptHookType Type,
uint RefDataId,
int PartIndex,
Vector3 Offset,
bool IsParentLocal);
public enum PhysicsScriptHookType
{
CreateParticle = 18,
DestroyParticle = 19,
PlaySound = 1,
AnimationDone = 2,
}
///
/// Individual runtime particle. Owned by the ParticleSystem.
///
public struct Particle
{
public Vector3 EmissionOrigin;
public Quaternion SpawnRotation;
public Vector3 Position;
public Vector3 Velocity;
public Vector3 Offset;
public Vector3 A;
public Vector3 B;
public Vector3 C;
public float SpawnedAt;
public float Lifetime;
public float Age;
public float StartSize;
public float EndSize;
public float StartAlpha;
public float EndAlpha;
public uint ColorArgb;
public float Size;
public float Rotation;
public bool Alive;
}
///
/// One active emitter instance. The ParticleSystem holds a pool
/// of these; each one maintains its own particle array.
///
public sealed class ParticleEmitter
{
public EmitterDesc Desc { get; init; } = null!;
public Vector3 AnchorPos { get; set; }
public Quaternion AnchorRot { get; set; } = Quaternion.Identity;
public uint AttachedObjectId { get; set; }
public int AttachedPartIndex { get; set; } = -1;
public Particle[] Particles { get; init; } = null!;
public ParticleRenderPass RenderPass { get; init; }
public int ActiveCount;
public float EmittedAccumulator;
public float StartedAt;
public float LastEmitTime;
public Vector3 LastEmitOffset;
public int TotalEmitted;
public bool Finished;
}
///
/// Top-level particle orchestrator. App-layer renderer batches these.
///
public interface IParticleSystem
{
/// Spawn an emitter attached to a world position or entity.
int SpawnEmitter(
EmitterDesc desc,
Vector3 anchor,
Quaternion? rot = null,
uint attachedObjectId = 0,
int attachedPartIndex = -1,
ParticleRenderPass renderPass = ParticleRenderPass.Scene);
/// Fire a full PhysicsScript at a target.
void PlayScript(uint scriptId, uint targetObjectId, float modifier = 1f);
/// Advance all active emitters by dt seconds.
void Tick(float dt);
/// Stop an emitter early.
void StopEmitter(int handle, bool fadeOut);
/// Current active particle count.
int ActiveParticleCount { get; }
int ActiveEmitterCount { get; }
}