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