# Showcase: Nuclear reactor

An emergent chain reaction that *is* the simulation: neutron agents fly across the canvas, strike
fissile atoms, and on a fission roll spawn more neutrons; control rods, leakage, and finite
lifetime remove them. The visible neutron population is the reactor "power." It is regulated by
**real reactor physics** — not a hidden multiplier. This is the only showcase that uses the
engine's ECS and animation packages, so it's the one that teaches them.

<Aside type="note" title="Read Foundations first">
This guide builds on [Foundations](/guides/foundations) — the contract, the post-FX surface and
its frame shape, and the pure-module discipline.
</Aside>

## What the engine provides here

The richest footprint of all five: `GameLoop` + `CoroutineManager` (`@flare-engine/core`),
**`World` + `defineComponent` (`@flare-engine/ecs`)**, **`tween` + `updateTween`
(`@flare-engine/animation`)**, `ParticlePool` (`@flare-engine/particles`), `clamp`/`lerp`
(`@flare-engine/math`), and the post-FX finish (`@flare-engine/postfx`).

**The reactor physics itself is yours.** `reactor/physics.ts` is framework-free, zero-import,
bun-tested TypeScript — six-group delayed-neutron kinetics, a Doppler feedback term, control-rod
worth curves. The engine does not simulate reactors; it gives you the loop, the entities, the
tweens, the pool, and the glow.

## Modeling agents in the ECS

Atoms and neutrons are entities in an `@flare-engine/ecs` `World`. Define small components and
spawn entities with them:

```ts

const Neutron = defineComponent("Neutron", () => ({ life: 0 }));
const Atom    = defineComponent("Atom", () => ({ fissile: true }));

const world = new World();

function spawnNeutron(x: number, y: number, vx: number, vy: number): void {
  const e = world.create();
  world.add(e, Position, { x, y });
  world.add(e, Velocity, { x: vx, y: vy });
  world.add(e, Neutron, { life: 0 });
}
```

<Aside type="tip" title="Pre-create queries">
Create your ECS queries once in setup and reuse them; never call `world.query()` per frame — it
allocates on the hot path. See [Concepts → Architecture](/concepts/architecture).
</Aside>

## The loop: transport vs. choreography

Neutron transport — move, leak at the boundary, age out, and roll fission on contact — runs on the
deterministic **fixed** step. Display smoothing and the operating-cycle choreography ride the
**variable** step:

```ts
const loop = new GameLoop({ fixedDt: 1 / 60, maxDt: 0.1 });

loop.onFixedUpdate.add((dt) => transport(world, dt));  // emergent chain reaction
loop.onUpdate.add((dt) => { coro.update(dt); updateTween(rodTweens, dt); smooth(dt); });
```

`transport` is the whole reaction: for each neutron, integrate position, remove it on leakage /
absorption / lifetime, and on a fission hit spawn ν daughter neutrons (mean 2.43) and a spark
burst. Nothing is ever cleared; the population rises and falls emergently.

## Grounding it in real physics — your code, not the engine's

The number that decides how many neutrons survive each generation comes from `reactor/physics.ts`,
which models the genuine article:

- **Six delayed-neutron precursor groups** (Keepin U-235 λᵢ/βᵢ) emit into a reservoir — the slow
  feedback that makes a reactor controllable rather than a bomb.
- An inflated **"visual beta"** (~0.12 vs. the real 0.0065) widens the controllable
  prompt-critical margin so the animation reads on a human timescale — an honest piece of
  cartooning, called out as such.
- A lumped **fuel-temperature ODE** drives a **Doppler self-limit**: as it heats, reactivity
  drops, so the reaction self-regulates.
- **S-curve rod worth** shapes how much each control rod absorbs as it plunges.

```ts

// pure, tested helpers — no engine, no DOM
```

Because it's pure, it has a `physics.test.ts` next to it: the kinetics are verified with
`bun test`, independent of the renderer.

## Timer-free choreography

A `CoroutineManager` runs the operating cycle — `startup → supercritical → damping → peak → scram →
loop` — with no wall-clock timers. The dramatic beat is six control rods plunging in one by one in
random order, each plunge gated on the live neutron population:

```ts
function* operatingCycle() {
  yield* waitUntil(() => allRodsOut());
  yield* waitUntil(() => population() >= growthTrigger());   // let it go supercritical
  for (const rod of shuffledRods()) {
    plungeRod(rod);                                           // start a tween (below)
    yield* waitUntilOr(() => rodSeated(rod), () => population() < dampFloor());
  }
  yield* waitUntil(() => population() <= crashFloor());       // scram, then loop
}
```

## Animating control rods with tweens

Each rod's plunge and withdrawal is an `@flare-engine/animation` `tween`, advanced on the variable
step with `updateTween` — independent, eased motion without hand-rolled interpolation:

```ts

const rodTween = tween({ from: rod.y, to: seatedY, duration: 0.8, ease: "cubicInOut" });
// each frame: updateTween(rodTween, dt); rod.y = rodTween.value;
```

## Fission sparks

Each fission fires a short burst from an `@flare-engine/particles` `ParticlePool` — acquired,
updated, and read back for color/size/alpha, all pooled so there's no per-spark allocation:

```ts

const sparks = new ParticlePool(maxSparks);
function burst(x: number, y: number): void {
  for (let i = 0; i < 8; i++) sparks.acquire({ x, y, vx: rng.float(-1,1), vy: rng.float(-1,1) });
}
```

## Drawing: raw CanvasKit + the rod aberration trick

The scene is immediate-mode raw CanvasKit onto the shared offscreen surface with additive `Plus`
blending. Unlike Physarum and Spacetime, the reactor **hard-clears every frame** —
`fadeTrail(BG, 1)`, not a translucent fade — so the entities stay crisp and trail-free rather than
smearing into streaks: atoms, neutrons, and sparks are circles; rods are light-lines. The signature
touch is per-rod **chromatic aberration**: every neutron hit deposits a Gaussian bump along the
rod's length that decays over time, and the renderer reads that profile to split and jitter the
rod's light into cyan/magenta fringes. Then the engine's post-FX surface adds bloom and aberration
globally, driven by the reactor's `glow()`/`warmth()`:

```ts
this.surface.fadeTrail(BG, 1);                       // hard clear — crisp entities, no motion trails
drawAtoms(sc); drawNeutrons(sc); drawSparks(sc); drawAberratingRods(sc);
this.surface.composite(sim.glow());                 // engine bloom + aberration → present
```

## Parallax depth — the "Five Layers"

The scene isn't flat. Every atom, neutron, spark, and rod is dealt to one of up to five receding
depth planes (`layerCount`, default 5 — the "Five Layers"), and the renderer treats depth as pure
**aerial perspective**. As a layer recedes, its entities are drawn smaller (`layerScaleFar`),
dimmer (`layerDimFar`), pulled inward toward the frame centre / vanishing point (`layerConverge`),
desaturated toward grey (`layerGrey`), and softened with a cached per-layer Gaussian
depth-of-field blur (`layerBlur`, sharp in front, out-of-focus at the back):

```ts
const dp = sim.depthParams();   // { count, scaleFar, dimFar, converge, grey, blur }
sim.forEachAtom((x, y, r, fissile, layer) => {
  const t = layer / (dp.count - 1);          // 0 = front plane … 1 = farthest
  drawAtom(project(x, y, t), r * scale(t), dim(t), grey(t), blurFor(layer));
});
```

The key discipline: **depth is presentation only.** The chain reaction runs on true positions and
full-height rods; layers are applied at draw time (rods stay anchored to the top edge but render
shorter, thinner, and converged when farther), so physics stays global and frame-rate-independent
while the picture gains real 3D-ish depth. All six knobs are live-tunable in the dev panel; set
`layerCount` to 1 for the old flat look.

## Wiring the dev panel

Because the instance optionally implements `params()`/`getParams()`/`setParam()`/`stats()`, the
shared dev panel feature-detects it and gives you live tuning. Each `ParamDescriptor` declares
whether it applies `"live"` (mutated in place) or `"restart"` (needs a sim re-create); `stats()`
returns labeled `StatReadout`s (population, fuel temperature, reactivity) with thresholds the panel
renders in a warning state when exceeded.

## Recap

<Steps>

1. Atoms and neutrons as ECS entities; transport on `loop.onFixedUpdate`.
2. Real six-group kinetics + Doppler feedback in a pure, bun-tested `physics.ts`.
3. The operating cycle as a population-gated `CoroutineManager`.
4. Control-rod plunges as `@flare-engine/animation` tweens; fission sparks from a `ParticlePool`.
5. Raw-CanvasKit scene, hard-cleared for crisp entities, with the rod aberration trick and
   five-layer parallax depth (scale / dim / converge / grey / blur); engine bloom on top.

</Steps>

<Aside type="note">
See it live in the [hero](/) (pick "Reactor"). Next:
[Spacetime lattice](/guides/spacetime) — the heaviest-physics showcase.
</Aside>
