# Particle Effects / Atmosphere Panel

**Date:** 2026-04-10  
**Status:** Approved

## Summary

Replace the single fog toggle button in the campaign header with an atmosphere panel that supports four effects: fog (CSS, existing), fire, rain, and embers (tsParticles). Effects stack independently. Each effect has an on/off toggle and an intensity slider (0–100).

## Library

`@tsparticles/react` + `@tsparticles/slim`

- Slim engine includes all shapes and movers needed for fire, rain, embers
- Intensity maps directly onto particle config params (count, speed)
- FogOverlay stays as CSS — no library change for fog

## Components

### `AtmospherePanel.tsx` (new)

Button trigger in the campaign header, replacing the existing fog `<button>`. When one or more effects are active the button shows their icons (e.g. `🌫🔥`). Clicking opens a popover panel anchored below the button. Clicking outside closes it.

Panel contains one row per effect (Fog, Fire, Rain, Embers):
- Label with emoji
- Toggle switch (on/off)
- Intensity slider (0–100), dimmed when effect is off

On any toggle or slider change, the component calls an `onAtmosphereChange(newAtmosphere)` callback. CampaignView owns the socket; its handler calls `setAtmosphere(next)` and `socket.emit("atmosphere:update", { campaignId, ...next })`. AtmospherePanel receives only `atmosphere` state and `onAtmosphereChange` as props — no direct socket access.

Fog intensity slider is rendered but has no mechanical effect in v1 (CSS fog doesn't have an intensity param). It will not be wired until the fog overlay supports it.

### `ParticleOverlay.tsx` (new)

Full-screen fixed canvas, `pointer-events: none`, z-index 9997 (just below FogOverlay at 9998). Receives the full `atmosphere` state. For each of fire, rain, embers: if `active === true`, renders a tsParticles instance with a config generated from `getXxxConfig(intensity)`. Each effect is a separate tsParticles instance so they layer independently.

### `particleConfigs.ts` (new)

Pure functions — no side effects, no imports from React. Each takes `intensity: number` (0–100) and returns a tsParticles `ISourceOptions` config.

| Effect | Count range | Speed range | Particle style |
|--------|-------------|-------------|----------------|
| Fire   | 20–200      | 2–8 upward  | Small circles, orange/red/yellow gradient, fade as they rise, emitter along bottom edge |
| Rain   | 50–500      | 5–20 downward | Thin elongated lines, ~70° angle, slight opacity, full-width emitter at top |
| Embers | 5–80        | 0.2–1.5 drift | Small dots, warm amber, slow random drift, gentle fade-out |

### `FogOverlay.tsx` (unchanged)

Props unchanged: `active: boolean`. CampaignView passes `atmosphere.fog.active`.

## State

In `CampaignView.tsx`, replace `fogActive: boolean` with:

```typescript
const [atmosphere, setAtmosphere] = useState({
  fog:    { active: false, intensity: 50 },
  fire:   { active: false, intensity: 50 },
  rain:   { active: false, intensity: 50 },
  embers: { active: false, intensity: 50 },
});
```

## Socket

### Payload change

`atmosphere:update` payload changes from `{ fog: boolean }` to:

```typescript
{
  fog:    { active: boolean; intensity: number };
  fire:   { active: boolean; intensity: number };
  rain:   { active: boolean; intensity: number };
  embers: { active: boolean; intensity: number };
}
```

### Server (`server/src/socket.ts`)

Update the TypeScript type for the `atmosphere:update` handler to accept and rebroadcast the new payload shape. No persistence — server is a pure relay.

### Client (`CampaignView.tsx`)

The `atmosphere:update` listener replaces the entire `atmosphere` state with the received payload, keeping all clients in sync including intensity values.

## File Checklist

| File | Action |
|------|--------|
| `client/src/components/AtmospherePanel.tsx` | Create |
| `client/src/components/AtmospherePanel.module.css` | Create |
| `client/src/components/ParticleOverlay.tsx` | Create |
| `client/src/lib/particleConfigs.ts` | Create |
| `client/src/components/FogOverlay.tsx` | No change |
| `client/src/components/FogOverlay.module.css` | No change |
| `client/src/pages/CampaignView.tsx` | Update state, socket listener, JSX |
| `server/src/socket.ts` | Update TypeScript type |
| `client/package.json` | Add @tsparticles/react, @tsparticles/slim |

## Out of Scope

- Fog intensity wiring (slider exists, does nothing in v1)
- Moving atmosphere control to App-level header (deferred to DM view redesign)
- Snow or other effects beyond fog/fire/rain/embers
- Per-effect configuration beyond intensity (e.g. wind direction for rain)
