# Relay Zero II: The Varnish — design notes

## The pitch

Relay Zero was a faithful room-by-room adventure in a sci-fi colony. The sequel keeps that simulation untouched and hangs the whole map in a gallery: the world is an Old-Master painting, and the enemy, the Varnish, is draining the colour out of it.

## Ideas combined

- **FIELDWORK / Island V3: Storm Signal** — repair that the world shows you. Every surface scene with machines in it starts as a brown grisaille underpainting (a single shared shader uniform in the 2D terrain shader and the 3D voxel shader). Break every machine in a scene and it is glazed back into colour with a light sweep, a bell chime and a burst of paint. Restored scenes are stored per save slot in `localStorage` (`relay-zero-v2-restored`); 114 scenes in all.
- **The Ashwicke Galleries** — exhibits as doorways. Dungeon entrances are gilded frames, and entering the first vault plays *Into the Frame*.
- Carried in from **Waymark**: the pixel-art kit (palette, character-map sprites, bitmap font built to canvas + WOFF2, 9-slice frames). From the **Horizon footer**: the easel oil-painting workflow.

## Why the 3D view wasn't fun, and what changed

The v2 3D build was a first-person camera at eye height 1.5 in a world whose tiles are 2 units wide. Rooms felt like corridors, machines were knee-high, and the NES fight rules (face the threat, strike in a short reach) were hard to read without seeing your own body.

- **Over-the-shoulder chase camera** (default): about 7 units back, about 40° down, north-up by default to match the 2D map, with drag or mouse-capture orbit. First person is still available in Settings.
- **A hero you can see**: `render/hero-rig.js` builds the wood-elf apprentice from voxels a quarter the size of the scenery's, on a jointed rig (hips, knees, shoulders, elbows, hands), with chibi proportions, big catch-light eyes, swept elven ears, a crimson scarf and the glowing Ion Cutter. A walk cycle is driven by real displacement, and a wind-up-and-whip strike with a light trail is driven by the source sword state.
- **Animation**: `render/hero-rig.js` layers locomotion, the strike and the idles, then runs a volume-preserving squash-and-stretch spring on a `body` group pivoted at the feet (y scale s, x and z scale 1/sqrt(s)). Crouches bend both knees and drop the hips by the leg length times (1 - cos a), so the feet stay planted. The strike has overlapping action: anticipation (85 ms), then hips, chest, shoulder and elbow each start a few milliseconds after the last, then a follow-through and settle (460 ms in all).
- **Idles**: `render/hero-idles.js` lists 17 idles (four need no item) and the inventory bytes that unlock them; unlocks are kept per save slot (`relay-zero-v2-idles`). After about 4.5 still seconds she turns three-quarters to the camera and plays one, never the same twice running. Tunes and effects are synthesized in `audio/hero-motifs.js`. Try them all in `character-labs/apprentice-rig.html`.
- **Machines as HD-2D billboards**: the exact pixel sprites stand in the world facing the camera, with a soft ground shadow.
- **See-through**: scenery between the camera and the hero is stippled away with a Bayer dither (`render/cutaway.js`).
- **Feel**: hit-stop (2–5 frames), camera shake, brass-gear bursts, footfall dust, and aim assist (a fresh cut swings at the nearest machine within about 70° in front).
- **Scale**: the camera sits closer and the actors are larger, so a room reads as a stage, not a corridor.

## Art

- Seven paintings on the claude-paint easel (Lua sources are alongside in the working repo): key art *Relay Tower*, a 4:1 seamless sky, *The Varnish Descends*, *The Ion Cutter*, *Into the Frame*, *Relay Restored*, *Fallen*, and a cast sheet cut out by difference matting (apprentice running and leaping, hermit, beetle, ornithopter, gargoyle).
- 3D oil-paint pass (`render/painterly-pass.js`): a 4-sector Kuwahara filter with a flow-field stroke nudge, impasto ridges, linen weave, an aged-varnish grade and a chiaroscuro vignette. It steps its radius down automatically if frames run slow, and it is off with Reduced effects.
- 2D: the atlas is repainted by `tools/repaint-atlas.py` from the v1 atlas (`source/runtime-atlas.v1.png`), with per-material earth palettes for terrain and redrawn characters with selective coloured outlines.

## Cut scenes

`ui/cutscenes.js` is data-driven: each scene is a painting, camera keyframes (a pan/zoom clamped so the painting always fills the frame), painted actors with arcs, spins and hits, effects (slash arcs, glows, light rays, a vortex, flicker, flashes, shake, gear and colour bursts), timed sound cues and typed captions. Scenes trigger from the source state: new save → *The Varnish Descends*; first cutter → *The Ion Cutter*; first vault → *Into the Frame*; relay core → *Relay Restored*; first fall each session → *Fallen*. The Gallery (Pause → Gallery) replays any scene that save has seen.

## Sound

25 effects and 9 room atmospheres come from `C:\Repos\SFX`. Every candidate was judged from rendered waveform and spectrogram images (see `source/sfx/SOURCES.md` and the waveform plates). Atmospheres are 45 s gapless loops at about -23 LUFS, chosen per room from what the room is made of (`audio/room-ambience.js`): meadow, forest, coast, highland, ruins, grotto, vault and deep vault, plus the title bed. They crossfade between rooms and duck under cut scenes.

## Loading and cache busting

The lesson from Waymark and Lodestar: never let a stale cached module start next to fresh ones, and always say exactly what broke.

- `release-manifest.json` lists every file with its size and SHA-256. Regenerate it after any change: `node tools/build-release.mjs`.
- The loader fetches the manifest with `no-store` plus a query cache-buster, then fetches each file as `path?v=<hash>`. It retries a mismatched or failed file with `no-store`, uses a fresh copy even if it differs from the manifest (flagged amber, with its integrity check dropped), and names any missing file and why.
- Buttons: **Try again**, **Reset game cache** (unregisters service workers in this folder, clears matching Cache Storage, reloads with `?refresh=`), and **Continue anyway**. Saves and settings in `localStorage` are never touched.
- Large optional media (atmospheres, cut-scene paintings, labs) is listed but loaded on demand, still hash-versioned.

## The full bestiary (v2.1)

- **Actor registry** (`game/actors/`): each monster family registers types with `defineActor(types,{init,update,metastate})`; `encounter.js` and `enemy-spawn.js` dispatch registered types before the v1 code. Families load in isolation (`actors/index.js`), so one broken family is reported instead of stopping the game. `actors/README.md` holds the contract: RAM-to-field mapping, helpers, ownership and the sprite-name table.
- **Families**: `overworld.js` + `shots.js`, `vault.js` + `vault-walker.js`, `vault-casters.js`, `bosses*.js` + `game/ending.js`, `world.js` (people, fairy, whirlwind, dock, flute secret), and the gear in `game/gear/`. All are translated from the disassembly (Z_01/Z_04/Z_07/Z_02); deliberate deviations are listed in each file's header and in `actors/REQUESTS.md`.
- **Drawing**: actors push sprite parts to `state.ext.actorDraw`; `render/actor-view.js` draws them in 2D and the 3D view turns them into billboards. Sprites are char maps in `tools/bestiary/*.txt`, built into the atlas by `tools/build-bestiary.py`.
- **Dark rooms**: `render/painterly-pass.js` has a `dark` uniform driven by the room's dark bit, the candle (`ext.gear.roomLit`) and the Varnisher's fade (`ext.bosses.fadeCycle`). The ending is drawn by `render/ending-view.js` from `state.ext.ending`.
- **Tests**: `node tools/sim-tests/run.mjs [family]` runs the headless suites (smoke, overworld, vault, casters, bosses, gear; about 500 checks).
