# Crescendo

A small arcade shmup, built as a testbed for the
[`game-audio`](https://gitlab.com/porky11/game-audio) `.track` **adaptive music**
system. The soundtrack is a song graph that reacts to what you are doing on both
axes: layers fade with the danger (vertical), and whole songs re-sequence at
musical boundaries through conditional `@jump` (horizontal).

Move with **WASD**, the **arrow keys**, the left stick, or the d-pad; shoot with
**Space**, **J**, the gamepad's south button, or the right trigger. Hold
**Shift**, **K** or the left trigger to *focus*: the ship slows to a third of
its speed, its true hitbox and graze radius appear, the shot spread narrows —
and the mix pulls in around you, a close, dry pulse rising under the song
(`focus`, the tenth parameter). **Escape**, **P** or **Start** pauses — the song freezes and a second player loops a
muffled holding pattern until you return; **−** and **+** set the volume, which
the pause screen shows. **F11** toggles fullscreen. Everything runs on the music: enemies enter on the beat and
follow the section the song announces (the intro and the victory fanfare give
room to breathe, the bridge only trickles, the verse fights), sentinels volley
and the boss attacks in time, and kills chain into a multiplier whose window is
measured in beats. **Kills also arm you**: thirty of them open a three-lane fan,
a hundred and fifty a five-lane spread, three hundred and fifty the full seven.
There is nothing to collect —
the gun is loaded by playing forward, it *stays* loaded, and a hit costs one
power level rather than all of it. A round spent grazing instead of killing stays lightly armed,
which is the price of the tightrope. Grazing — enemy bullets *and* enemy bodies — pays chain-multiplied score,
keeps the kill chain alive, and banks toward extra lives at rising cost: the
first takes 10 near misses, the next 20, then 30 (a score bonus once lives
cap at five); the ship flashes, a jittered click ticks, the spent bullet
dims, the `RISK` meter shows what the music hears. Score milestones, extra
lives and boss kills fire a **shockwave** that sweeps enemy bullets off the
field, each cancelled bullet paying a little score; every milestone costs more
than the one before it, so the wave stays an event instead of becoming a
metronome. Enemies shoot from the first wave on, one pattern per archetype:
drifters drop straight shots, weavers scissor diagonal pairs, sentinels
alternate aimed fans with short-lived homers (orange — they steer for a
second, then commit), and the boss adds homers in its middle phase. A boss can also be *outlasted*:
take no shot at it for twenty seconds and it loses interest and leaves the
stage — the act ends without a kill and without its score, which is what makes
a pacifist run possible at all. A hit costs one of
three lives and the chain, clears the screen, and dips the world and the music
into a shared slow-motion; on your last life a dissonant drone (`dread`) hangs
under every fight song, and losing it grinds the tape to a full stop — pitch
and world freeze together. Fire restarts from the intro, whose accelerando
doubles as the run-up. Every ~40 seconds of fighting a boss flies in; its
bullet patterns escalate through the same health thirds that swell the music's
rage layer, and it holds its fire through its own entrance: the boss *sings*
its taunt — a ghost-voice growl in phoneme notation over the first bars of its
song, with the English line burning red under the field, no speaker tag needed:
the ceremony runs long enough for the last syllable to ring out before anything
shoots. It is the same Warden across all three fights, wearing down from a
threat into an admission. **The third
act completes the run** — the victory fanfare plays under the tally screen, which
separates the two things a score can mean: the *phases* every run passes
through (the verse, the boss) and the *routes* the playstyle chose (rush,
wire, bridge). Only the route score names the run — RUSHER, WIREWALKER or
PACIFIST for a route that took the majority of it, BALANCED for a mix, and
METRONOME for a run that hardly ever left the verse.

The songs choreograph their own enemies: the verse's chord motifs (`wave_*`
placements on the Pulse layer) each send a wave the moment the chord sounds,
the rush's `surge` pickup sends a darter pair into each chord change, the
wire's `curtain` cascade drops a gap-riddled bullet wall from the top edge,
and the boss drops a minion exactly when its leitmotif plays. The harmonic
cues also report their chord root, and the kill stinger follows it — kills
ring in whatever chord the song is on. Four archetypes
keep the field moving: drifters fall, weavers cross lanes on a sine, darters
lock on and dash, sentinels park and fire aimed fans on the beat. Waves are
composed from a cost budget, so they are sometimes many weak enemies and
sometimes few strong ones.

The play field is a fixed 960×720 world that letterboxes into the window. Every
draw call multiplies by a pixels-per-world-unit factor rather than transforming
the canvas, so text is rasterized at its real size and stays sharp in fullscreen.

## The song graph

Fifteen embedded songs, chained by `@jump` at their ends, arranged as a
three-act album run. Each act is the same hub shape — verse, its own two
routes, its own boss — in its own tempo and its own palette:

```text
intro ──► ACT I ─┬─► ACT II ─┬─► ACT III ─┬─► RUN COMPLETE
   132 bpm       │  116 bpm  │   148 bpm  │
                 └─ cleared ─┴── cleared ─┘

   per act:   verseN ◄──► bridge     (fury < 0.1 — the pacifist route,
                │ ▲ ▲       │         the one song every act shares)
                │ │ └───────┼──► rushN   (fury > 0.6 — the killer route;
   boss > 0     │ └─────────┤    ◄──┘     stays while fury > 0.5)
                ▼           │ └─► wireN   (risk > 0.5 — the tightrope route;
              bossN ◄───────┘              stays while risk > 0.4)
                │ boss <= 0
                ▼
            victory ──► next act's verse (skip 8: re-enters mid-phrase)
```

Each act owns its songs, so a route sounds like the act it happens in: the
rush of act three is not the rush of act one. The `bridge` and the `victory`
fanfare are the exception — they are shared, and route back into whichever
act the run is in via two parameters: `boss` carries the act number of the
living boss (`@jump boss2.track if boss > 1.5`), and `act` picks the verse to
return to (`@jump verse2.track 8 if act > 1.5`).

What stays the same across all three acts is the core: the A-minor
progression and the `wave_*` motifs that choreograph its enemy waves, the
boss leitmotif family, and the layer names with their parameter bindings —
the adaptive contract the game plays against. What changes is everything
else. Act I runs at 132 bpm on `sounds.track`. Act II drops to a 116 bpm
half-time crawl on `sounds2.track` — saturated bass, pads with the air taken
out. Act III takes over the **boss songs' own 148 bpm** on `sounds3.track`:
the Warden has the clock now, and the final act runs at his tempo.

- `intro.track` accelerates 100 → 132 bpm into the verse (`@tempo` ramp).
- `verse.track` is the 16-beat A-minor main loop, ten layers deep. `Drums`,
  `Bass`, `Pad` always play; `Pulse`, `Lead` and `Drive` swell with `combat`,
  and four more stems each listen to their own playstyle parameter: `Shaker`
  to `swarm` (enemies on screen), `Sub` to `barrage` (bullets in the air),
  `Risk` to `risk` (recent grazes, decaying), `Fury` to `fury` (recent kills,
  decaying). The mix is literally a readout of how you are playing.
- The routes are chosen by how you play, decided at each song's end from the
  parameters your round built up: the rush is the killer route
  (`fury > 0.6`), the wire the tightrope route (`risk > 0.5` — bullet
  curtains, few enemies, sustained wire tones bending overhead), and the calm
  `bridge.track` rewards pacifist rounds (`fury < 0.1`). Each route spawns
  differently, and a narrator line introduces it — a different line in each
  act.
- Every route exists once per act, keeping its choreography motif and losing
  everything else. `rush2.track` drags a half-time weight behind each hit and
  turns the `surge` pickup upside down — it falls where act one's rose;
  `rush3.track` runs sixteenths and lets the pickup overshoot its own target.
  `wire2.track` hangs its wire tones a register lower and bends them down,
  its curtain falling in thirds; `wire3.track` bends them up past where they
  started. Same cues, same harmony, another world.
- `bridge.track` is a calmer half-time passage whose melody plays in a
  just-intonation minor pentatonic (`# Tunings` + `@tuning`/`@root`). It is
  the one song shared by all three acts — the quiet is the same quiet
  everywhere, which is the point of it.
- `verse2.track` (act two, "VERSE II") sinks the same progression low and
  slows it to a crawl. `verse3.track` (act three, "REPRISE") opens the
  voicings wide, weaves the first boss leitmotif into its lead, and runs at
  the boss tempo — the album closing over everything it taught.
- `boss.track` runs faster (148 bpm) around a boss leitmotif (`# Motifs`,
  placed with `motif … transpose`). Its `Sirens` layer is bound to the `rage`
  parameter, which rises as the boss loses health. `boss2.track` answers with
  a falling leitmotif over a stalking half-step bass, and `boss3.track`
  turns the first leitmotif upward — rising instead of falling — under twin
  sirens and sixteenth-burst drums.
- `victory.track` reprises the boss leitmotif lifted into the major (motif note
  overrides), ramps 148 → 132 bpm to glue the tempos, and jumps back into the
  verse at beat 8. Two of its layers mirror the run that earned it: a
  bitcrushed `Blaze` bound to `fury` and a sparkling `Glitter` bound to
  `risk` — a killer's fanfare sounds harder than a tightrope walker's.

The game drives the graph with ten parameters (`combat`, `boss`, `rage`,
`swarm`, `barrage`, `risk`, `fury`, `dread`, `act`, `focus`) and
reads back `current_song_beat` for beat-aligned spawns, `current_subtitle` for
the section label in the HUD (each song announces itself via `# Subtitles`),
and `layer_levels`/`master_levels` for the mix meters and the audio-reactive
field glow.

On top of the graph:

- **Stingers** — kills, hits, score milestones, the boss alarm and the boss
  kill fire motifs from `assets/stingers.track` over the running mix,
  quantized to the next beat (`play_stinger`).
- **Shot sounds** — every shot is a `sound_oneshot_varied` render of the
  bank's `shot` sound, whose `detune`/`vol-jitter` make mashed fire never
  sound like a loop.
- **Slow motion** — a hit dips `SongPlayer::set_speed` tape-style (pitch drops
  with it) and the game scales its world timestep by the same factor, so the
  action and the music recover together.

## Building

    cargo run --release                 # native
    sh build-web.sh                     # wasm-bindgen bundle in web/
    sh build-release.sh                 # linux tarball and windows zip in dist/

Every song, sound and the font live inside the binary, so a build is a single
file. The font is Inter Bold (`assets/font/`, SIL Open Font License — the
licence text sits next to it), tracked through Git LFS. The Linux binary still links against the system's ALSA and udev
libraries; the Windows build is cross-compiled with `x86_64-pc-windows-gnu` and
needs no toolchain on the target machine.

## Status

Playable score chase. Runs native (wgpu); the WASM path builds through
`gapp-winit` and has been played and measured in a browser — the same song
graph, one-shot sounds and beat-aligned waves as the desktop build, no errors in
the console, a median frame of 17 ms and no stall over 350 ms once the first
song has been rendered (game-audio 0.23.4 prepares each jump target in slices
while the current song plays). Starting a run still costs about a second while
that first song renders.

Two differences from the desktop build remain. `play_stinger` is native-only in
game-audio, so the browser hears a plain tone sequence where the desktop hears a
quantized stinger or a sung taunt. And live layer volume is a no-op on WASM, so
the parameter-driven fades — the mix swelling with the danger — land at the next
song boundary rather than continuously. The music still reacts to how you play;
it just reacts in steps.

Song transitions swap sinks only after the next song is rendered (game-audio
0.18.2), so the old loop keeps sounding through the switch; a residual timing
nudge of a few tens of milliseconds remains until game-audio pre-renders ahead
of the jump.
