> For the complete documentation index, see [llms.txt](https://docs.allout.game/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.allout.game/core-engine-concepts/particle-systems.md).

# Particle Systems

`Particle_System_Component` draws deterministic 2D particle effects. Particle motion is sampled analytically from a descriptor, seed, and time instead of advancing and storing a transform for every particle each frame.

## Quick start

Add the component and play one of the built-in presets:

```go
particles := entity.add_component(Particle_System_Component);
particles.play(Particle_System_Desc.preset_explosion());
```

`play` validates and copies a descriptor, resets the deterministic seed and start time, and enables the component. Call `restart()` to replay the active descriptor:

```go
particles.restart();
```

Call `stop()` to disable the effect. A bare `Particle_System_Component` with no descriptor is inert until `play()` is called, so adding one never starts an invisible default effect first.

Burst presets disable their component after the longest possible particle lifetime. Continuous presets remain enabled until you disable or remove the component.

## Presets

Every preset call returns a fresh descriptor that can be safely customized:

```go
desc := Particle_System_Desc.preset_smoke();
desc.color_over_lifetime = Particle_System_Desc.over_lifetime_v4(
    {0.4, 0.15, 0.7, 0.8},
    {0.05, 0.01, 0.1, 0}
);
desc.spawn_position_radius = 0.3;
particles.play(desc);
```

The component's active `desc` is read-only. `get_desc()` returns a value copy that can be safely customized and passed back to `play()`; running particles can therefore never observe a half-applied descriptor change.

The built-in presets are:

* `preset_default`
* `preset_shotgun`
* `preset_firework`
* `preset_explosion`
* `preset_sparks`
* `preset_smoke`
* `preset_dust`
* `preset_fountain`
* `preset_embers`
* `preset_snow`
* `preset_rain`
* `preset_confetti`
* `preset_magic_aura`

## Local and world simulation space

Set `simulation_space` according to how already-spawned particles should react when the emitter moves:

```go
desc.simulation_space = .LOCAL; // Existing particles follow the emitter.
desc.simulation_space = .WORLD; // Existing particles remain where they spawned.
```

Both modes use the same deterministic analytical sampler. They differ only in the runtime information needed to recover the particle origin:

* **Local space** keeps constant-size emitter state. A newly created local-space system allocates no per-particle simulation state.
* **World space** stores one small birth record per reusable particle slot: its emitter position at birth and life index. Position, velocity, rotation, size, and color are still computed analytically.

This makes local space O(1) state and world space O(`desc.get_particle_count()`) state. Switching an existing world-space component to local space clears the slot count, although the managed array may retain its previous capacity for reuse.

## Burst and continuous emission

Burst emission starts the requested number of particles at once:

```go
desc.emission = Particle_Emission.burst(32);
desc.prewarm = false;
desc.auto_disable_when_finished = true;
```

Continuous emission schedules the requested number of particles per second:

```go
desc.emission = Particle_Emission.continuous(20);
desc.prewarm = true;
desc.auto_disable_when_finished = false;
```

Treat `emission`, `prewarm`, and `auto_disable_when_finished` as one lifecycle configuration when changing a preset. Presets return complete descriptors, and assigning a new `emission` does not reset either of the other fields. In particular, ambient presets such as `preset_magic_aura()` already have `prewarm = true`; clear it explicitly before changing them to burst emission, or `play()` will reject the descriptor.

The engine derives the smallest safe reusable slot pool automatically:

```go
slot_count = ceil(emission.rate * lifetime.max)
```

Use `desc.get_particle_count()` to inspect the resulting burst count or continuous slot count. Continuous capacity is not an authoring control: emission rate and lifetime already determine the maximum number of simultaneously eligible particles.

`prewarm` analytically advances a continuous schedule by one complete pool cycle, giving ambient effects an already-running appearance without simulating earlier frames. A WORLD-space prewarm has no earlier emitter trajectory to inspect, so it deliberately assumes the emitter was stationary at its current position before activation.

## Common descriptor fields

Ranges and over-lifetime values use helper constructors:

```go
desc.speed = Particle_System_Desc.range_float(2, 5);
desc.lifetime = Particle_System_Desc.range_float(0.5, 1.2);
desc.size_over_lifetime = Particle_System_Desc.over_lifetime_v2(
    {0.2, 0.06},
    {0.04, 0.01}
);
```

`size_over_lifetime` contains full world-space width and height. Supplying both dimensions intentionally allows stretching for streaks and rectangular particles. To preserve a sprite's source aspect ratio, specify its visual height:

```go
desc.size_over_lifetime = Particle_System_Desc.size_from_sprite_height(
    desc.sprite,
    0.2,
    0.5
);
```

The main descriptor fields are:

* `spawn_offset`, `spawn_rect_size`, and `spawn_position_radius`: where particles originate relative to the emitter.
* `direction` and `direction_spread_degrees`: base travel direction and random angular spread. The sampler normalizes the direction.
* `speed`, `gravity`, and `friction`: analytical motion controls.
* `lifetime`, `fade_in_seconds`, and `fade_out_seconds`: lifetime and opacity envelope.
* `color_over_lifetime`: RGBA color interpolation. Fades multiply the interpolated alpha instead of replacing it.
* `initial_rotation_degrees` and `angular_velocity_degrees`: rotation range and angular velocity range.
* `face_direction`: points each particle along its current velocity instead of applying angular velocity.
* `size_over_lifetime`: independent full width/height over the particle lifetime.
* `sprite`: rendered texture.
* `seed_salt`: stable per-descriptor variation mixed into the activation seed.

`layer` and `z_offset` live on `Particle_System_Component` and control render ordering.

## Validation

`play()` and `restart()` assert on invalid descriptors so programmer errors fail at their source. Editors and other authoring tools can validate without starting the effect:

```go
error: string;
if !desc.try_validate(ref error) {
    // Present error to the author.
}
```

`validation_error()` returns the same error directly, or an empty string when the descriptor is valid. Invalid values are never silently clamped.

## Determinism and interpolation

Each sample is derived from the particle-system seed, slot index, life index, and current time. Drawing does not mutate simulation state. Current and previous analytical samples are passed to the interpolated quad renderer, including position, size, and rotation, so particles remain smooth between fixed simulation updates.

World-space birth positions are captured during the shared update path rather than during drawing. Drawing samples no later than the latest completed birth-position update, so startup and re-enable timing cannot expose a newly eligible slot before its origin exists. This keeps the effect compatible with prediction and resimulation while avoiding render-only state changes.
