import { type ForceFieldConfig } from "../physics/forceVolume.js"; /** A 3D vector `[x, y, z]`. */ export type Vec3 = readonly [number, number, number]; /** A `[min, max]` range a spawned particle draws uniformly from. */ export interface Range { min: number; max: number; } /** A per-life start→end curve (linear interpolation from birth to death). */ export interface Curve { start: number; end: number; } /** Serializable spawn volume in emitter coordinates; cone widens from its base along +Y. */ export interface ParticleSpawnShape { kind: "disc" | "sphere" | "cone"; radius: number; height?: number; /** Spawn on the outer surface instead of filling the volume. */ surface?: boolean; } /** Bounded particle contact policy; a query adapter can replace the fallback horizontal plane. */ export interface ParticleCollisionConfig { planeY?: number; response?: "kill" | "bounce"; restitution?: number; maxQueries?: number; maxEvents?: number; } /** One contact from an injected scene query, in simulation coordinates. */ export interface ParticleCollisionHit { position: Vec3; normal: Vec3; } /** Presentation events never authorize gameplay damage or physical impulses. */ export interface ParticleEvent { type: "collision" | "death"; particleId: number; position: Vec3; velocity: Vec3; normal?: Vec3; } /** Optional geometry adapter; queries are capped independently of rendering. */ export interface ParticleSystemOptions { collision?: (from: Vec3, to: Vec3) => ParticleCollisionHit | null; /** External cosmetic acceleration; return `out` to reuse its allocation. */ acceleration?: (position: Vec3, velocity: Vec3, out: [number, number, number]) => Vec3; } /** * A particle emitter: how particles spawn and how each one evolves over its life. * Every field is data — no functions — so an emitter is fully serializable and an * editor/tunable can drive it. Genre-agnostic: smoke, sparks, rain, magic, dust. */ export interface EmitterConfig { /** Hard cap on live particles; the pool never grows past this. Default 512. */ max?: number; /** Continuous emission rate in particles/second. Default 0 (burst-only). */ rate?: number; /** Spawn origin. Default `[0, 0, 0]`. */ position?: Vec3; /** Half-extents of a box the spawn point is jittered within. Default `[0, 0, 0]`. */ spawnJitter?: Vec3; spawnShape?: ParticleSpawnShape; /** Particle lifetime in seconds. Default `{ min: 1, max: 1 }`. */ lifetime?: Range; /** Initial speed along the emit direction, in units/second. Default `{ min: 1, max: 1 }`. */ speed?: Range; /** Base emit direction (need not be normalized). Default `[0, 1, 0]` (up). */ direction?: Vec3; /** Cone half-angle in radians around `direction`; `Math.PI` emits in all directions. Default 0. */ spread?: number; /** Constant acceleration (e.g. gravity `[0, -9.8, 0]`). Default `[0, 0, 0]`. */ gravity?: Vec3; /** Linear velocity damping per second, `0` (none) to `1` (stop instantly). Default 0. */ drag?: number; /** Particle radius over life. Default `{ start: 1, end: 1 }`. */ size?: Curve; /** `0xRRGGBB` color at birth. Default `0xffffff`. */ colorStart?: number; /** `0xRRGGBB` color at death. Default = `colorStart`. */ colorEnd?: number; /** Opacity over life, each `0..1`. Default `{ start: 1, end: 0 }` (fade out). */ alpha?: Curve; /** Seed for deterministic births; retuning it resets future randomness. */ seed?: string | number; /** Shared physical field descriptors, sampled as cosmetic acceleration; at most eight per particle. */ forces?: readonly ForceFieldConfig[]; collision?: ParticleCollisionConfig; /** Enable bounded death events. Collision events use `collision.maxEvents`. */ deathEvents?: boolean; } /** * Read-only packed buffers of the live particles, laid out for a renderer to * upload directly (Structure-of-Arrays, no per-particle objects). Only the first * `count` entries are live; the arrays themselves are reused every frame. */ export interface ParticleBuffers { count: number; /** `count * 3` floats: x, y, z per particle. */ positions: Float32Array; /** `count` floats: current radius per particle. */ sizes: Float32Array; /** `count * 3` floats: r, g, b in `0..1` per particle. */ colors: Float32Array; /** `count` floats: opacity `0..1` per particle. */ alphas: Float32Array; /** Stable birth ids, retained through pool compaction. */ ids: Float64Array; /** Packed velocity and prior integration positions for streak/ribbon outputs. */ velocities: Float32Array; previousPositions: Float32Array; } /** Serializable simulation state for save/restore and deterministic replay. */ export interface ParticleSnapshot { seed: number; accumulator: number; count: number; data: number[]; running?: boolean; nextId?: number; ids?: number[]; previousPositions?: number[]; config?: EmitterConfig; } /** A live, dt-driven particle simulation. */ export interface ParticleSystem { /** Advance the sim by `dt` seconds: emit from `rate`, integrate, and reap dead particles. */ update(dt: number): void; /** Spawn `n` particles immediately (a one-shot puff/explosion), independent of `rate`. */ emit(n: number): void; /** Resume continuous emission without replacing the live pool. */ start(): void; /** Stop spawning while existing particles continue aging. */ stop(): void; /** Consume bounded presentation collision/death events exactly once. */ drainEvents(): ParticleEvent[]; /** Live particle count. */ count(): number; /** The packed buffers for rendering (valid until the next `update`/`emit`). */ buffers(): ParticleBuffers; /** Kill all particles and reset the emission accumulator (keeps the seed stream). */ clear(): void; /** Patch tuning in place; `replace` resets omitted fields to defaults while retaining live particles. */ configure(patch: Partial, replace?: boolean): void; subscribe(listener: () => void): () => void; snapshot(): ParticleSnapshot; restore(snapshot: ParticleSnapshot): void; } /** * A generic, allocation-aware particle system: one emitter, a fixed pool, and * Structure-of-Arrays buffers a renderer uploads straight to the GPU. It is * dt-driven (call `update(dt)` each frame) and deterministic — all randomness * flows from an injected `seed`, so the same seed and dt sequence reproduce the * same frames, and `snapshot`/`restore` round-trips the live pool. Nothing here * is combat- or genre-specific: configure it for smoke, sparks, rain, dust, * embers, magic, or confetti. Travel/gameplay stays elsewhere; this owns only * the spawn-integrate-fade lifecycle. * * @capability particle-system deterministic pooled particle emitter with spawn volumes, force fields, bounded collision/death events, live tuning, SoA render buffers, and serializable state */ export declare function createParticleSystem(config?: EmitterConfig, options?: ParticleSystemOptions): ParticleSystem; /** * Validate the serializable spawn/update descriptor before authoring or loading it. * @capability particle-emitter-validation validate finite spawn volumes, force fields, lifetimes, appearance curves, and bounded particle collision policies */ export declare function validateEmitterConfig(value: unknown): string[];