# Visual effects: spells, impacts, auras, beams

`Vfx` is the effect engine, the way `sound` is the sound engine. An effect is plain data (an
`Effect`): a few layers played together. Each layer is particles, voxel cubes, a ring or ground
mark, a column, a dome or a beam, drawn by shaders, with no textures.

**Effects work like sounds.** Every game makes its own effects for its own moments, in its own
palette, in `vfx.ts`, the way it writes its own `sounds.ts`. The SDK's library (`VFX`: fire,
frost, lightning, holy, arcane, poison, hits, pickups) is there to learn from and copy. Don't use it
as a default: grabbing a key off a table is not a `frostNova`.

A flat `RingGeometry` mesh plus a few `Bits` sparks looks like placeholder art, and so does its
shinier cousin, a white shock ring on everything. A frost nova is ice creeping out to a jagged
crystal edge, voxel ice chunks flying, shards jutting up and cold mist: each moment has its own
silhouette.

## Contents
1. Playing effects
2. `vfx.ts`: your game's effects, and the rules of thumb
3. The look: chunky pixels and voxels, with real glow
4. The layers
5. Power: code any spell (your own pixel art, effects as code, nesting and flights, formations, `onDeath`, GLSL, `spawn`, themes)
6. Recipes
7. Why it's cheap, and stays cheap
8. Seeing effects: `vp gallery`
9. Pitfalls

---

## 1. Playing effects

```ts
import { Vfx } from '@voxelparty/sdk';
import { EFFECTS } from './vfx';

this.vfx = new Vfx(this.view);                     // a GameView (scene + camera) or just a scene
// every frame, with game time (a hitstop freezes effects too):
this.vfx.update(dt);
// in dispose():
this.vfx.dispose();

this.vfx.play(EFFECTS.keyGet, { at: key.position });            // fire and forget
this.vfx.play(EFFECTS.bombBlast, { at: pos, scale: 1.5 });       // bigger
this.vfx.play(EFFECTS.towerBuilt, { at: pos, color: team.color });  // recoloured
this.vfx.play(EFFECTS.teslaZap, { at: tower, to: creep });       // beams go from `at` to `to`
this.vfx.play(EFFECTS.thunder, { at: target, to: 'sky' });      // a strike from 10 units above
this.vfx.play(EFFECTS.cleave, { at: hero, dir: facing });        // `forward` layers follow `dir`
```

`play` returns a handle. Effects that loop (auras, projectiles, channels, shields) run until you
stop them:

```ts
const aura = this.vfx.play(EFFECTS.onFire, { follow: unit.root });  // follows an Object3D (or () => pos)
aura.stop();                                                     // it fades out; nothing new starts

const ball = this.vfx.play(EFFECTS.fireball, { at: from });      // a projectile with a trail
ball.move(p);                                                    // every frame: the trail fills in between
ball.stop(); this.vfx.play(EFFECTS.fireballHit, { at: p });      // on impact

const beam = this.vfx.play(EFFECTS.lifeDrain, { at: caster, to: victim });
beam.move(casterPos, victimPos);                                 // or followTo: victim.root
```

Loops that belong to game state (a status on a unit, a zone on the ground) are easiest with `keep`:
ask for them every frame, and `sweep()` stops the ones you didn't ask for, so a unit that dies or
loses the status loses its effect with no handles to track:

```ts
for (const u of units) if (u.burning) this.vfx.keep(`burn:${u.id}`, EFFECTS.onFire, { follow: u.root });
for (const z of zones) this.vfx.keep(`zone:${z.id}`, EFFECTS.blizzard, { at: z.pos, scale: z.radius / 3 });
this.vfx.sweep();                                                // once a frame, after the keeps
```

`PlayOptions`: `at`, `to`, `dir`, `color`, `scale`, `ground` (where cubes land; default `at`'s
height), `follow`, `followTo`. The handle has `move(at, to?)`, `aim(dir)`, `stop()` and `alive`.
`new Vfx(view, { scale: 1.5 })` scales every effect for a far or top-down camera.
`vfx.clear()` removes everything (a new round). `vfx.stats()` says how many things are alive and
how many draw calls they take.

Effects are only visual: play them on every client from the same events you already use for
sounds (a `HostSync` event, a `Lockstep` tick's outcome, your own `cues`). Nothing needs syncing.

## 2. `vfx.ts`: your game's effects, and the rules of thumb

Keep effects next to `sounds.ts`, as plain data from the headless core:

```ts
// vfx.ts
import { VFX, type Effect } from '@voxelparty/sdk/core';

export const EFFECTS = {
  /** The Frost Mage's Q: a cone of ice shards. */
  frostCone: {
    layers: [
      { kind: 'particles', shape: 'shard', count: 30, life: [0.4, 0.7], size: [0.25, 0.4],
        speed: [8, 12], dir: 'forward', spread: 25, drag: 2, spin: 5, color: ['#ffffff', '#8fe8ff', '#3fa0ff00'] },
      { kind: 'particles', shape: 'smoke', count: 8, life: 1, size: [0.8, 1.2], speed: [3, 5],
        dir: 'forward', spread: 30, drag: 3, color: '#dff6ffb0', alpha: [0.6, 0] },
    ],
  },
  /** The shrine's blessing: started from the library's heal, then made ours (gold, our pixel size, slower). */
  blessing: { ...VFX.heal, scale: 1.3 },   // a start: then change its colours, shapes and timing
} satisfies Record<string, Effect>;
```

**Rules of thumb:**

1. **Size the effect to the moment.** A pickup is a tiny glint of 0.2–0.4 s in the object's own
   colour. A hit is a quick flash and a spray. An ultimate is big and layered, with a ground mark
   that lingers. If everything is huge, nothing reads.
2. **One effect per meaningful game moment, named for the moment:** `keyGet`, `doorUnlock`,
   `towerBuilt`, `bossEnraged`. Name it for what happens, not for a spell school (`fireNova`).
   Two moments that feel different get two effects.
3. **Give each moment its own silhouette.** Ask what *this* thing looks like when it happens: a
   cannonball digs a `crater` and throws dirt; a slam `crack`s the ground; a claw leaves three
   `claw` marks; a splash throws drops and `ripple`s; a crit is a `burst` star; a heal rises in
   crosses. **Rings are seasoning, not the dish.** A `shock` ring belongs to a real wave (a nova, a
   stomp, a blast's shockwave), coloured for the moment and thin. A plain white shock ring on
   everything is the laziest effect there is: `vp check` warns when a game's effects lean on it
   (`checkEffectSet`), and when more than half of them carry a shock or soft ring at all.
4. **Write it for this game.** Picture the moment, then build it from layers, and reach past them
   when you need to (§5): draw the game's own particles as pixel art (its key, its coin, its rune),
   stamp its sigil on the ground, chain a charge into a flight into an impact, spell a word in
   sparks. A library effect or a `sketchEffect` is only a starting point to rewrite. Put the
   game's palette on the whole set with `themeEffects`. Audition every effect and change it until
   it belongs to this game.

- **Name the export `EFFECTS`.** `vp check` runs `checkEffect` on every effect `vfx.ts` exports:
  typos, bad colours and layers that make nothing fail the check, and floods are warned about.
  `vp gallery` finds them there too.
- Aim for an effect for every ability, hit, death, pickup and level-up, the way every event has a sound.
- Give your game its own look: a palette of 3–5 colours used by all its effects, and a shape
  vocabulary (`rune` and `bolt` read as magic, `shard` as ice and glass, cubes as anything physical).

## 3. The look: chunky pixels and voxels, with real glow

Make spells big and juicy, with glow, bloom and soft light,
but the particles, debris and patterns read as chunky pixels and voxels: pixel sparkles, voxel
chunks, and ground runes as crisp as the texels. That's the particle version of the 16×16 texture
rule. Soft is for light, smoke and mist: a flash, a glow pool on the ground, a cloud.

- Particles and patterns draw on the blocks' texel grid by default: `pixel` is texels a unit (16,
  the blocks' own; 8 is chunkier; `0` is smooth). Light draws smooth by default (glows, light pools,
  light shafts, lightning and laser cores) and so does smoke. Leave the rest pixel.
- Your own pixel art (§5) is always pixel-exact: its grid is the pixels.
- Debris is `cubes`: voxel chunks that tumble and land, in the colour of what broke.
- Big soft light belongs under the pixels, not instead of them: a `disc` ring as a glow pool, a
  `glow` dome, a short flash. The pixels carry the shape.
- Impacts leave the ground changed, not ringed: `crack` (dark for stone and earth; glowing with
  `blend: 'add'` for lava, lightning or holy light), `crater`, `scorch`, `splat`, `frost`, `claw`;
  and throw the material: `cubes` or `chunk` particles in the colour of what was hit.
- A wave you want to show goes coloured and patterned (`ripple` dashes, `runes`, `dash`) or thin
  (`shock` with `width` 0.1–0.25), never a fat white halo.

## 4. The layers

Every layer has `at` (a delay in s), `life` (s), `color` (a ramp over its life: `'#fff'` or
`['#fff', '#ffd23f', '#ff4b0000']`, `#rrggbbaa` for alpha), `alpha` (a curve, default
`[1, 1, 1, 0]`), `glow` (brightness; over 1 blooms, default 1.6 for glowing layers), `blend`
(`'add'` glows, `'alpha'` covers: smoke, dust, goo) and `tint` (how much it takes the play's
`color`, default 1; 0 keeps a white-hot core or black smoke as they are).

**Numbers:** a `VfxRange` is `n` or `[min, max]` (picked per particle). A `VfxCurve` is `n` or
keyframes spread over the life (`[0, 1, 1, 0]` pops in, holds, shrinks away).

| kind | what | its own fields |
|---|---|---|
| `particles` | sprites drawn in the shader. The `shape` is one of `glow` `dot` `pixel` `star` `flare` `ring` `smoke` `shard` `flame` `plus` `heart` `spark` `bubble` `leaf` `rune` `bolt` `skull` `note` `z` `swirl` `drop` `snow` `arrow` `chunk` (a knobbly bit of rock, wood or bone, lighter than a cube), or your own pixel art (§5) | `shape`, `spin`, `stretch` (streaks along the motion: sparks, rain), `glsl` (§5) |
| `cubes` | voxel chunks that tumble and land (debris, coins, splinters) | `spin`, `floor` (default true: they land on `ground`) |
| `ring` | flat rings and ground marks. The `style` is one of `shock` `soft` `disc` (a pool of light) `runes` (a magic circle) `dash` `spikes` `scorch` `frost` `splat` `target` `slash`; impact marks `crack` (branching cracks racing out), `burst` (a spiky POW star), `crater` (a bowl, a rim, thrown debris), `ripple` (rings of broken dashes: water, sound, a pulse), `claw` (three raking marks); or your own pixel `art` (§5) | `radius` (curve), `width` (a share of the radius), `face: 'forward'` (stands up), `art` |
| `effect` | another effect played as a part of this one: repeated, turned, offset, flown to `to` (§5) | `effect`, `count`, `every`, `offset`, `rotate`, `scale`, `jitter`, `follow`, `travel`, `arc`, `then` |
| `pillar` | columns. The `style` is one of `beam` `flame` `swirl` (tornado) `rays` `wall` | `radius`, `height` (curves), `taper` (0 a cone, 2 a funnel) |
| `dome` | spheres. The `style` is one of `shield` `blast` (a churning fireball) `bubble` `glow` | `radius` (curve), `squash` |
| `beam` | `at` → `to`. The `style` is one of `lightning` `laser` `chain` `drain` `rope` | `width` (curve), `jitter`, `arc` |

**Particles and cubes: how many and how they move.**

| field | meaning |
|---|---|
| `count` / `rate` + `dur` / `perMeter` | a burst; a stream per second (for `dur` s, or until stopped); a trail per unit moved |
| `from`, `radius`, `height`, `length` | where they're born: `point` `sphere` `ball` `ring` `disc` `box` `line` (to `to`, or `length` along `dir`), lifted by `height` |
| `speed`, `dir`, `spread`, `lift` | launch: `out` `in` `up` `down` `forward` `back` `random` `none`, opened into a cone of `spread`°, plus upward `lift`; or `to`: each flies to the play's `to` and lands on it exactly as it dies (`speed` ignored; drag and gravity bend the path, not where it lands). A tracer is one `dir: 'to'` spark with `stretch` |
| `gravity`, `drag`, `swirl`, `pull`, `turbulence` | fall (negative floats up); slow to a hang; circle the centre (rad/s); draw in to the centre by death (`pull: 1`); wander |
| `size`, `grow` | world size, times a curve over life |
| `local` | move with the effect after birth (an aura on a walking hero), instead of being left behind (a trail) |
| `thin` | `false`: never thinned for being far away or off screen, for what the player must read at any distance (a fire zone's extent, a bomb's ring). Graphics quality and full pools still thin it |
| `formation` | born on points you draw instead of `from`: a list, a function, or a `formation` helper (§5) |
| `onDeath`, `onDeathChance` | play an effect where each one dies (§5) |

**Rings, pillars, domes and beams** also have `count` + `every` (ripples), `hold` (plays the first
half of its life, holds the middle until `stop()`, then the rest: shields, auras, channels),
`y` (lift), `spin` (rad/s), `scroll` (pattern speed) and `local` (default true: they follow).

## 5. Power: code any spell

The layers above cover most spells. When you picture something they can't make, these tools can.
All of them keep the pixel-and-voxel look.

### Your own pixel art: particles and ground marks

A particle's `shape` can be your own pixel art, a text grid up to 32×32 with the top row first.
With no legend, `.` is empty and `#` `+` `:` `-` are 100/75/50/30% shades of the layer's colour.
With a `legend`, each character is its own colour, times the layer's colour (leave `color` white to
show it as drawn). `frames` animate over each particle's life. A ring's `art` stamps pixel art on
the ground, stretched over its diameter (`radius` grows it, `spin` turns it).

```ts
const KEY: PixelArt = { rows: ['.oo.....', 'oyyo....', 'oyyoooo.', 'oyyoyyyo', '.oo.oyo.', '.....o..'],
                        legend: { o: '#6a4a10', y: '#ffd23f' } };
const COIN: PixelArt = { frames: [['.###.', '#yyy#', '#y#y#', '#yyy#', '.###.'], ['..#..', '.#y#.', '.#y#.', '.#y#.', '..#..'],
                                  ['..#..', '..#..', '..#..', '..#..', '..#..'], ['..#..', '.#y#.', '.#y#.', '.#y#.', '..#..']],
                         legend: { '#': '#c08a10', y: '#ffe066' } };   // a coin that spins
keyGet: { layers: [
  { kind: 'particles', shape: KEY, count: 1, life: 0.7, size: 0.6, speed: 1.6, dir: 'up', gravity: 2.5, blend: 'alpha', glow: 1 },
  { kind: 'particles', shape: 'pixel', count: 8, life: [0.3, 0.6], size: [0.06, 0.1], speed: [1, 2], dir: 'up', spread: 40, color: ['#fff6c8', '#ffd23f'] },
] },
clanMark: { layers: [{ kind: 'ring', art: SIGIL_ROWS, radius: [0.5, 2.2, 2.2], spin: 0.4, life: 2.4, color: ['#fff', '#c58bff'], glow: 1.8 }] },
```

The art is packed into one small texture the first time it plays, and it costs the same as any
particle. `checkEffect` reports rows of the wrong length and characters missing from the legend.

### Effects as code

An effect can be a function `(rnd, call) => Effect`. It's called on every play, with a seeded
random source and the play's details (`call.at`, `call.to`, `call.dir`, `call.scale` and
`call.distance` from `at` to `to`). This is the sound engine's `(rnd) => Patch`, for effects. Use
it for casts that differ each time, a random rune, or an effect that fits the distance:

```ts
crit: ((rnd, call) => ({
  scale: 1 + Math.min(1, call.distance / 10),
  layers: [
    { kind: 'particles', shape: 'flare', count: 1, life: 0.15, size: 1.4 },
    { kind: 'particles', shape: 'star', count: 6 + Math.floor(rnd() * 10), life: [0.3, 0.6], speed: [3, 6], dir: 'out', drag: 3,
      color: ['#fff6c8', rnd() < 0.5 ? '#ffd23f' : '#ff7ab8'] },
  ],
})) satisfies EffectFn,
```

Checks and `vp gallery` call a function on stand-in plays (`sampleEffect`).

### Nesting and chaining: `kind: 'effect'`

A layer can be another effect. It plays at the effect's position plus `offset` (in the effect's
frame: x right, y up, z forward along `dir`), `count` times, `every` s apart, each one turned
`rotate`° further. `scale` scales it, `jitter` nudges each one randomly, and `follow: true` makes it
move with its parent. With `travel` it flies from `at` to the play's `to` over that many seconds,
bowing up by `arc`, and `then` plays where it lands. That's a whole spell in one effect:

```ts
fiveFold: { layers: [
  { kind: 'effect', effect: EFFECTS.sparkPop, count: 5, every: 0.1, offset: [2, 0, 0], rotate: 72 },   // five pops round a circle
] },
fireballSpell: { layers: [
  { kind: 'particles', shape: 'pixel', rate: 50, dur: 0.5, from: 'sphere', radius: 1.2, pull: 1, life: 0.4 },  // charge
  { kind: 'effect', at: 0.5, effect: EFFECTS.fireball, travel: 0.7, arc: 2, then: EFFECTS.fireballHit },  // fly, then burst
] },
// vfx.play(EFFECTS.fireballSpell, { at: hand, to: target })
```

### Formations: particles born in a shape you draw

`formation` replaces `from`. It's a list of `[x, y, z]` points in the effect's frame (particle i
is born on point i, so `count` = the list's length draws it once), or a function
`(i, n, rnd) => [x, y, z]`. The `formation` helpers make the common ones: `circle`, `arc`,
`spiral`, `helix`, `sphere`, `line`, `path`, `polygon`, `star`, `pentagram`, `grid` (a text grid
of pixels) and `text` (words in a 3×5 pixel font). `dir: 'out'` flies them away from the centre.

```ts
import { formation } from '@voxelparty/sdk/core';
{ kind: 'particles', shape: 'pixel', formation: formation.pentagram(2, 10), count: 50, life: 0.9, size: 0.09 },
{ kind: 'particles', shape: 'pixel', formation: formation.text('WIN', 0.28), count: 200, life: 1.6, size: 0.2, drag: 1 },
{ kind: 'particles', shape: 'pixel', count: 160, life: 2.5, swirl: 0.8,                    // a spiral galaxy
  formation: (i, n, rnd) => { const k = i / n, a = k * 7 + (i % 2) * Math.PI, r = 0.3 + k * 2.6;
                              return [Math.cos(a) * r, 0.4 + rnd() * 0.2, Math.sin(a) * r]; } },
```

Upright text reads from the front: aim the play's `dir` at the camera.

### Sub-effects on death: `onDeath`

Particles and cubes can play an effect where each one dies, in its direction of travel.
`onDeathChance` (0..1) sets off only some of them. Motion is closed-form, so the engine knows the
death point without simulating anything. Use it for fireworks, meteors that shatter, and sparks
that crackle:

```ts
firework: { layers: [{ kind: 'particles', shape: 'spark', count: 1, life: 0.7, speed: 8, dir: 'up', gravity: 9, stretch: 0.06,
  onDeath: { layers: [
    { kind: 'particles', shape: 'pixel', count: 40, life: [0.8, 1.2], speed: [3, 5], dir: 'random', drag: 1.5, gravity: 2,
      color: ['#ffffff', '#ff5a8a', '#7a3aff00'], onDeath: EFFECTS.crackle, onDeathChance: 0.3 },
  ] } }] },
```

Chains are capped (400 waiting, 48 started a frame), so a runaway chain thins out.

### Your own particle shape in GLSL (advanced)

`glsl` on a particle layer is the body of `vec2 shape(vec2 p, float k, float seed, float t)`.
`p` runs -1..1 across the particle, already on its pixel grid. `k` is its age 0..1, `seed` its own
0..1, and `t` the clock. It returns (coverage 0..1, brightness). `hash1`, `hash2`, `noise2`, `fbm2`
and `edge(d, w)` are in scope. Each distinct GLSL shape costs a draw call and a shader compile, so
use a few, not dozens:

```ts
{ kind: 'particles', count: 24, life: 1, size: 0.5, from: 'ring', radius: 1.2, speed: [1, 2], dir: 'up', color: ['#fff', '#4affe0'],
  glsl: 'float d = abs(p.x) + abs(p.y); float w = 0.25 + 0.15 * sin(t * 8.0 + seed * 6.0); return vec2(step(d, 0.95) * step(0.95 - w, d), 1.0);' }
```

### Particles from your own code: `vfx.spawn`

`vfx.spawn(layer, at, vel?, { color, scale, count, ground, dir })` makes particles of `layer` right
now, at `at`, with your velocity (world units a second). The layer supplies the look and the motion
(gravity, drag, colour over life). Use it for shell casings, sparks from your own simulation, or
anything your code places one by one.

### A game's identity: `restyle`, `themeEffects`, `vary`

- `themeEffects(EFFECTS, { palette, pixel, glow, voxel, speed, scale })` puts the game's look on
  all its effects:
  - **`palette`** moves every colour onto its nearest hue in your palette, keeping its lightness
    (whites and greys stay).
  - **`voxel: true`** turns falling pixels, shards, dots and leaves into cubes.
  - **`speed`** makes everything snappier or floatier without changing the distances.
- `restyle` does the same for one effect.
- `vary(effect, seed, 0.25)` makes a cousin: the second goblin's hit, related but not a copy.

### Tools

- `effectToCode(effect, 'name')` prints an effect as TypeScript, ready for `vfx.ts`.
- `sketchEffect({ verb, element, size }, seed)` rolls a rough starting point for a moment
  (`impact` `frost` `big`, `pickup` `gold` with the key's `color`), to copy and rewrite. It's a
  sketch, not a finished effect: write your effects yourself.

## 6. Recipes

**Projectile + impact.** Play a looping effect with a `perMeter` trail, `move()` it every frame,
and on impact `stop()` it and play the hit:

```ts
{ layers: [
  { kind: 'particles', shape: 'glow', rate: 30, life: 0.12, size: 1.2, color: '#b8e8ff', local: true },   // the head
  { kind: 'particles', shape: 'spark', perMeter: 8, life: [0.2, 0.4], size: 0.15, stretch: 0.05, color: ['#ffffff', '#5fb0ff00'] },
] }
```

**An aura that follows a unit:** `rate` layers with `local: true` and a `hold` ring, played with
`follow: unit.root`, stopped when the buff ends.

**A tower zapping a creep:** a `beam` with a short `life`, `{ at: towerTop, to: creep }`, plus a
small burst at the creep. For a continuous channel, give the beam `hold: true` and `followTo`.

**Charging up:** particles `from: 'sphere'` with `pull: 1` (they arrive at the centre as they
die), a growing `dome` `glow` with `hold`. On release, `stop()` it and play the blast.

**Team colours:** play with `{ color: team.color }`. Give the layers that should keep their own
colour `tint: 0` (smoke, a white core, gold coins).

**Big and small:** `scale` scales sizes, radii, speeds and gravity together, so a level-3 fireball
is `{ scale: 1.6 }`, not a new effect.

**Ground marks that linger:** a `ring` with `style: 'scorch'` (or `crack`, `crater`, `splat`, `frost`, `claw`), a `life` of 4–8 s
and `alpha: [1, 1, 1, 0]`.

## 7. Why it's cheap, and stays cheap

- **No CPU per particle.** A particle's whole life is a formula of the numbers it was born with
  and the clock: the GPU works out where it is. Spawning writes a few floats; after that it costs
  nothing on the CPU. A hitstop freezes effects for free.
- **A handful of draw calls.** Every effect shares one draw call per kind of thing (sprites
  that glow, sprites that cover, cubes, rings, pillars, domes, beams), and only while some are alive.
- **Thinning by itself.** Bursts and streams make fewer particles on lower graphics settings, as
  the pools fill, for effects far away or small on screen, and none off screen. The pools reuse
  their oldest slots when full; at most half a pool is born in one frame. Play what the moment
  deserves and let the engine budget it.
- **What still costs:** overdraw from many big sprites on screen. Prefer a few large ones plus a
  dome or ring over hundreds of big sprites. `checkEffect` warns past about 900 particles alive
  in one effect, and when many particles are over 5 units wide.

## 8. Seeing effects: `vp gallery`

**`vp gallery` (in your game).** Add your effects to `gallery.ts`:

```ts
import { EFFECTS } from './vfx';
g.effects('Effects', EFFECTS);                        // a group, each one added
g.effect('Fireball (red team)', EFFECTS.fireball, { color: '#e23b3b', colors: { blue: '#3b8ee2' } });
```

The overview shows each effect at its busy moment on a dark plate. `vp gallery --motion` shows its
film strip, with six frames bunched early (bursts are over in a blink). Looping effects run
2.5 s (`loop`) and are then stopped, so the strip shows them fade too. Trails circle so they
show. `--bg dark` suits glowing effects. Read the pages: an effect you haven't seen is an effect
you haven't made.

The library goes in the same way, to see what's there: `g.effects('Library', VFX)`.

## 9. Pitfalls

- **Forgetting `vfx.update(dt)`** leaves everything frozen at its first frame. Forgetting
  `stop()` on a looping effect leaves it running forever.
- **Playing the library for everything.** `VFX.*` effects are examples. A game whose key pickup
  is a frost nova looks lazy: make your own for each moment (§2, §5). `vp check` notes library effects
  played straight from game code.
- **Glow on bright ground.** Glowing layers keep their hue on a sunny meadow, but pale colours
  still read as white there. Use saturated colours and `glow` 1.2–2. Keep 2.5+ for tiny, bright cores.
- **Beams need `to`.** Without one they strike down from 10 units above `at`, with a warning;
  `to: 'sky'` says you meant it. Emitters with `dir: 'forward'`
  and upright rings need `dir`.
- **Cubes land on `ground`**, which defaults to `at`'s height. Play an explosion at chest height
  with `ground: 0`.
- **Effects are `transparent`** and don't write depth. Draw solid gameplay objects as meshes, not as effects.
