# Margin Aces — design notes

A WWII arcade shooter drawn as blue ballpoint on college-ruled paper. Twelve pages, twelve aircraft, a page ace at the end of each, and real history in the margins.

## The one rule

Every enemy attack draws a red warning before it fires: a ring for an aircraft windup, a line for a ground gun, a circle on the ground for flak or a broadside. Leave the line, then punish the recovery.

## Polish pass, October 2026

### Difficulty curve (`src/game/stageRosters.js`)

Before: every page used three-plane formations every five seconds, and the QA pilot took almost no hits on any page. Page 1 and page 12 played the same.

Now `PAGE_CURVE` sets, per page: aircraft per formation (and on every third "heavy" wave), seconds between formations, a cooldown multiplier on every attack, how often a red-ink veteran appears, the patrol attack budget, the budget once the ace is up, the claim cap, how often the ace calls escorts, and an ace health multiplier.

| Page | Formation | Spacing | Tempo | Veteran | Budget (patrol/ace) | Escorts | Targets |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | 3 | 5.0 s | 1.00 | — | 4 / 3 | — | 36 |
| 4 | 4 | 4.7 s | 0.90 | 1 in 6 | 6 / 4 | 10 s | 45 |
| 8 | 4 (5) | 4.3 s | 0.82 | 1 in 4 | 6 / 4 | 8 s | 55 |
| 12 | 5 (6) | 3.9 s | 0.72 | 1 in 3 | 8 / 5 | 6 s | 73 |

The attack-pressure budget still decides who may fire, so a crowded page is busier to shoot at but never an unreadable wall of shots. The encounter contract's `maxObservedPressureCost` for each page now matches the curve.

Other rule changes in `simulation.js`:

- **Veterans**: 1.8× health, 250 points, fire sooner, re-arm 15% faster, drawn in red ink.
- **Escorts**: pairs of fighters during the ace fight (page 3+), at most four alive. When the ace goes down, its escorts go down with it.
- **Combo**: now built from a kill *chain* (every 3 kills within 3 s of each other raises it, up to ×8). Taking damage resets it. Before, the multiplier came from total kills and was effectively always maxed.
- **Graze**: an enemy shot that passes within about 40 units of the airframe scores 25 and is counted.
- **Clean page**: finishing with no damage adds 2,500.
- **Pickups**: one extra drop allowed from page 6 and page 12.
- Events now carry positions (`x`, `y`), `enemyType`, `veteran`, `points` and `combo` for the effects and sound layers.

Measured with `node gamelab/tools/verify-marginaces-encounters.mjs --metrics` (12 aircraft × 2 strategies per page): the pilot still wins all 288 flights, peak aircraft on screen rise from 6 on page 1 to about 20 on page 12, and average hits taken rise from 0 to about 0.6. The pilot sees every bullet; people will take more.

### Ink effects (`src/render/inkFx.js`, `src/render/fxDirector.js`)

Ten Prism Forge choreographies (impact, shatter, detonation, nova, parry, mend, sky lance, summon, aegis, blink) were re-shot from above with no glow or halos and baked into `assets/2d/ink-fx.png`: 217 frames at 128 px, red = coverage, green = hot core. `tools/bake-ink-fx.cjs` rebuilds it from the pack.

The shader draws coverage as ballpoint hatching in screen space (lines measured in device pixels), cross-hatches dense areas, goes solid at the strongest strokes and leaves paper showing in the hot cores. A highlighter mode draws flat translucent colour for pickups.

Two small companion layers: pen strokes (debris, graze sparks, two contrails behind the player) and pencil-scribble smoke (wounded aircraft below half health, the player on the last airframe).

Enemy sprites gained a per-instance ink tone: a highlighter flash when hit and a red-pencil re-ink for veterans (escorts at half strength).

DOM layer: handwritten score notes (`src/ui/inkPops.js`), page moods on hits, bombs and ace kills, red pulsing margin rules on the last airframe, and a rubber-stamp cue for "ACE INBOUND" / "ACE DEFEATED". The HUD shows the combo and graze count; the results screen shows clean page, best combo, grazes and accuracy.

Settings: **Reduced effects** turns the ink layer and score notes off. **Reduced motion** keeps them but stops page shake and stamp animation.

### Sound (`assets/audio`, `src/audio`)

Thirteen new samples from the local SFX library, trimmed and encoded mono MP3 (about 150 KB together): ace siren, ace demolition, heavy debris blast, kill burst, airframe sheet-metal wobble, graze whoosh, escort fly-by, pickup ding, combo chime (pitched up per combo step), clean-page bell, last-airframe heartbeat, victory snare roll + cymbal, failure trombone. Every new cue keeps a synth fallback.

### Music (`assets/audio/music-*.mp3`, `src/audio/soundscape.js`)

One ElevenLabs Music generation (90 s, 140 BPM, instrumental) is cut on bar lines into loops by `_trailer-delivery/marginaces-source/build_game_music.py`: **music-flight-a** (bars 8–23) for odd pages, **music-flight-b** (bars 24–31 then 16–23) for even pages and **music-ace** (bars 32–47) once the ace is up. Each file carries one bar of pre-roll and post-roll so the sound bank's `loopStart`/`loopEnd` sit on bar lines. **music-radio** is PowerPillGo's instrumental "Block Drop Parade", retimed from ~143.6 to 140 BPM and run through a band-limited, saturated "pocket radio" with crackle; it plays in the menus and hangar.

The soundscape now allows three beds (engine, theatre, music). Music has its own setting (on by default). The decoded-audio budget went from 24 MB to 48 MB to fit the four loops.

### Trailer

A 2-minute hype trailer (`gamelab/assets/marginaces/trailer.mp4`, captions in `trailer.vtt`) was cut from the same score, re-ordered to ~2 minutes, with an ElevenLabs narrator (one take, split on silences) and in-game SFX on the cuts. Gameplay is real footage captured frame by frame from the QA build.

### Loader (`index.html`, `gamelab/tools/build-marginaces-boot.mjs`)

Follows JustDig's: a generated file list with a build id hashed from every file's contents, versioned import map and stylesheets, four-lane fetch with retries, named failures, Try again / Reset app cache / Continue anyway, and a 25-second start watchdog. Visually: ruled paper, a pencil plane flying a dashed route home and a handwritten five-item checklist. Run `npm run gamelab:marginaces:boot` after changing any file.

## Verify

```
npm run gamelab:marginaces:verify
node gamelab/tools/verify-marginaces-encounters.mjs --metrics
```

## Known limits

- The art is about 40 MB of PNG. WebP would cut that a lot but would need the art contract and labs updated.
- The ink effects were checked in headless Chromium (SwiftShader) captures, not yet on a phone GPU.
