Fire that looks like fire, a rack you can leave, and a boss you can read

The band work from f352612 rides along; everything else is this round's list.

- proj returns a NEGATIVE scale behind the camera, so four ellipse call sites
  could be handed negative radii — ctx.ellipse throws on those, and the throw
  killed the rest of the frame (no HUD, no rack, no band). All four now skip
  points behind the camera.
- a burst tin is no longer a commitment: tapping another slot aborts it, spends
  it and hands back the serve loop. Lives in selectWeapon, so the rack tap and
  the 1-0 keys both get it. A horde still locks the rack — that is the one place
  your finger is already spoken for. Spending splices the rack, so the tap
  resolves to a weapon id and re-finds it rather than trusting the index.
- beacon 1.6 -> 2.4 rounds/s: a lob cannon you could not leave AND could not
  feel working was two problems.
- FIRE. Flamer throws fat embers instead of a one-pixel thread; burning bodies
  get nine tongues over a heat bloom plus smoke; the fire ball gets a halo and a
  real tail; an ash death gets twice the motes at twice the life over a ground
  bloom and a column. The flamer had no sfx case at all and was using the gun's
  dry tick — it has its own roar now.
- chapter bosses carry an HP bar over their head. The no-HP-bar rule stands for
  everything else and the comment says why a boss is the exception.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5 committed 2026-08-09 13:21:57 +02:00
1 parent f352612de3
commit 655b86d27f
7 files changed
+184 -31

No files matched your search

+30
View File
@@ -182,6 +182,36 @@ export function swish(vol = 0.3, f0 = 1600, f1 = 3200, dur = 0.16) {
}
export const whoosh = (vol = 0.34) => swish(vol, 4200, 700, 0.3); // a panel flying in and settling
// THE FLAMETHROWER. It was borrowing pock(0.25, 300) — a short dry bandpassed tick — on every other
// round, so the loudest-looking weapon in the game sounded like someone tapping a table. A flame is
// broadband ROAR with a low guttural body under it, and it has to survive being retriggered ~15
// times a second without turning into a buzz, so each call is a soft-attacked overlapping puff that
// sums into a continuous sheet rather than a train of clicks.
export function flame(vol = 0.5) {
try {
const ac = audio(), t = ac.currentTime, dur = 0.26;
const src = ac.createBufferSource(); src.buffer = noiseBuf(ac);
// the roar: wide noise under a lowpass that opens then closes, which is the breath of it
const lp = ac.createBiquadFilter(); lp.type = 'lowpass'; lp.Q.value = 3;
lp.frequency.setValueAtTime(700, t);
lp.frequency.exponentialRampToValueAtTime(2600, t + dur * 0.25);
lp.frequency.exponentialRampToValueAtTime(500, t + dur);
const g = ac.createGain(); // soft attack: a hard one is a click, and 15/s of clicks is a buzz
g.gain.setValueAtTime(0.001, t);
g.gain.exponentialRampToValueAtTime(vol, t + 0.03);
g.gain.exponentialRampToValueAtTime(0.001, t + dur);
src.connect(lp).connect(g).connect(ac.destination); src.start(t, nOff(dur), dur);
const o = ac.createOscillator(), og = ac.createGain(); // the guttural body under the hiss
o.type = 'sawtooth';
o.frequency.setValueAtTime(64 + Math.random() * 22, t);
o.frequency.exponentialRampToValueAtTime(38, t + dur);
og.gain.setValueAtTime(0.001, t);
og.gain.exponentialRampToValueAtTime(vol * 0.35, t + 0.04);
og.gain.exponentialRampToValueAtTime(0.001, t + dur);
o.connect(og).connect(ac.destination); o.start(t); o.stop(t + dur + 0.02);
} catch (e) {}
}
export function pock(vol = 0.8, freq = 900) { // racket contact / bounce thock
try {
const ac = audio(), t = ac.currentTime;