Back to Poly Dogfight
SOURCE / PINNED RELEASE

Made of little things.

Poly Dogfight

Release
b8511ee374e3…
Author-recorded commit
5579434b9d8b…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-44f63e5422b

Archive hash verified: bcadd2430d8ea2bc…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/audio.ts
export function createNoise(ctx: BaseAudioContext): AudioBuffer {
  const buffer = ctx.createBuffer(1, ctx.sampleRate, ctx.sampleRate);
  const samples = buffer.getChannelData(0);
  for (let i = 0; i < samples.length; i++) samples[i] = Math.random() * 2 - 1;
  return buffer;
}

/**
 * Shared gun channel: a fast compressor so overlapping rounds punch instead of
 * clipping, plus a short dark slapback that turns a held trigger into a rattle.
 */
export function createGunBus(ctx: BaseAudioContext, destination: AudioNode): AudioNode {
  const input = ctx.createGain();
  input.gain.value = 0.9;
  const comp = ctx.createDynamicsCompressor();
  comp.threshold.value = -20;
  comp.knee.value = 6;
  comp.ratio.value = 5;
  comp.attack.value = 0.002;
  comp.release.value = 0.08;
  input.connect(comp).connect(destination);

  const echo = ctx.createDelay(0.2);
  echo.delayTime.value = 0.07;
  const tone = ctx.createBiquadFilter();
  tone.type = 'lowpass';
  tone.frequency.value = 1600;
  const feedback = ctx.createGain();
  feedback.gain.value = 0.22;
  const wet = ctx.createGain();
  wet.gain.value = 0.3;
  input.connect(echo).connect(tone).connect(feedback).connect(echo);
  tone.connect(wet).connect(comp);
  return input;
}

/**
 * A single machine-gun round, three layers: a bright crack (the muzzle snap),
 * a mid blast whose filter slams shut (the bang) and a falling sine thump
 * (the weight). Small random variation keeps a burst from sounding looped.
 */
export function playShot(ctx: BaseAudioContext, bus: AudioNode, noise: AudioBuffer, when: number, pan = 0): void {
  const vary = 0.92 + Math.random() * 0.16;
  const out = ctx.createStereoPanner();
  out.pan.value = Math.max(-1, Math.min(1, pan));
  out.connect(bus);

  const crack = ctx.createBufferSource();
  crack.buffer = noise;
  crack.playbackRate.value = vary;
  const crackFilter = ctx.createBiquadFilter();
  crackFilter.type = 'highpass';
  crackFilter.frequency.value = 2400;
  const crackGain = ctx.createGain();
  crackGain.gain.setValueAtTime(0.55, when);
  crackGain.gain.exponentialRampToValueAtTime(0.001, when + 0.03);
  crack.connect(crackFilter).connect(crackGain).connect(out);
  crack.start(when, Math.random() * 0.8, 0.04);

  const blast = ctx.createBufferSource();
  blast.buffer = noise;
  blast.playbackRate.value = vary;
  const blastFilter = ctx.createBiquadFilter();
  blastFilter.type = 'lowpass';
  blastFilter.Q.value = 1.5;
  blastFilter.frequency.setValueAtTime(2200 * vary, when);
  blastFilter.frequency.exponentialRampToValueAtTime(260, when + 0.09);
  const blastGain = ctx.createGain();
  blastGain.gain.setValueAtTime(0.0001, when);
  blastGain.gain.linearRampToValueAtTime(0.9, when + 0.002);
  blastGain.gain.exponentialRampToValueAtTime(0.001, when + 0.12);
  blast.connect(blastFilter).connect(blastGain).connect(out);
  blast.start(when, Math.random() * 0.8, 0.13);

  const thump = ctx.createOscillator();
  thump.type = 'sine';
  thump.frequency.setValueAtTime(170 * vary, when);
  thump.frequency.exponentialRampToValueAtTime(48, when + 0.07);
  const thumpGain = ctx.createGain();
  thumpGain.gain.setValueAtTime(0.0001, when);
  thumpGain.gain.linearRampToValueAtTime(0.8, when + 0.003);
  thumpGain.gain.exponentialRampToValueAtTime(0.001, when + 0.1);
  thump.connect(thumpGain).connect(out);
  thump.start(when);
  thump.stop(when + 0.11);
}

/** Tiny synthesized engine, wind, gun and stall-horn sounds. Starts only after a user gesture. */
export function createAudio() {
  let ctx: AudioContext | null = null;
  let master: GainNode | null = null;
  let engineOsc: OscillatorNode | null = null;
  let engineOsc2: OscillatorNode | null = null;
  let engineFilter: BiquadFilterNode | null = null;
  let windGain: GainNode | null = null;
  let hornGain: GainNode | null = null;
  let muted = false;
  let gunNoise: AudioBuffer | null = null;
  let gunBus: AudioNode | null = null;
  const recentShots: number[] = [];

  function start() {
    if (ctx) {
      void ctx.resume();
      return;
    }
    try {
      ctx = new AudioContext();
    } catch {
      return;
    }
    master = ctx.createGain();
    master.gain.value = muted ? 0 : 0.5;
    master.connect(ctx.destination);

    engineFilter = ctx.createBiquadFilter();
    engineFilter.type = 'lowpass';
    engineFilter.frequency.value = 600;
    const engineGain = ctx.createGain();
    engineGain.gain.value = 0.08;
    engineOsc = ctx.createOscillator();
    engineOsc.type = 'sawtooth';
    engineOsc2 = ctx.createOscillator();
    engineOsc2.type = 'square';
    engineOsc.connect(engineFilter);
    engineOsc2.connect(engineFilter);
    engineFilter.connect(engineGain).connect(master);
    engineOsc.start();
    engineOsc2.start();

    // Wind: looping filtered noise.
    const buffer = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
    const noise = ctx.createBufferSource();
    noise.buffer = buffer;
    noise.loop = true;
    const windFilter = ctx.createBiquadFilter();
    windFilter.type = 'bandpass';
    windFilter.frequency.value = 900;
    windFilter.Q.value = 0.6;
    windGain = ctx.createGain();
    windGain.gain.value = 0;
    noise.connect(windFilter).connect(windGain).connect(master);
    noise.start();

    const horn = ctx.createOscillator();
    horn.type = 'triangle';
    horn.frequency.value = 880;
    hornGain = ctx.createGain();
    hornGain.gain.value = 0;
    horn.connect(hornGain).connect(master);
    horn.start();
    gunNoise = createNoise(ctx);
    gunBus = createGunBus(ctx, master);
  }

  return {
    start,
    get muted() {
      return muted;
    },
    setMuted(value: boolean) {
      muted = value;
      if (ctx && master) master.gain.setTargetAtTime(muted ? 0 : 0.5, ctx.currentTime, 0.05);
    },
    suspend() {
      void ctx?.suspend();
    },
    update(params: { throttle: number; airspeed: number; boosting: boolean; stall: number; alive: boolean; time: number }) {
      if (!ctx || !engineOsc || !engineOsc2 || !engineFilter || !windGain || !hornGain) return;
      const t = ctx.currentTime;
      const rpm = params.alive ? 0.25 + params.throttle * 0.75 + (params.boosting ? 0.35 : 0) : 0;
      engineOsc.frequency.setTargetAtTime(38 + rpm * 70, t, 0.1);
      engineOsc2.frequency.setTargetAtTime(19 + rpm * 35.5, t, 0.1);
      engineFilter.frequency.setTargetAtTime(params.alive ? 300 + rpm * 900 : 10, t, 0.1);
      windGain.gain.setTargetAtTime(params.alive ? Math.min(0.18, (params.airspeed / 100) ** 2 * 0.15) : 0, t, 0.2);
      const beep = params.stall > 0.2 && params.alive && Math.floor(params.time * 5) % 2 === 0 ? 0.07 : 0;
      hornGain.gain.setTargetAtTime(beep, t, 0.01);
    },
    /**
     * One gun round. `delay` places it on the audio clock (the sim step it was
     * fired in) so a held trigger keeps an even rhythm; `pan` is -1..1.
     */
    shot(delay = 0, pan = 0) {
      if (!ctx || !gunBus || !gunNoise) return;
      const when = ctx.currentTime + delay;
      // Several pilots firing together: cap overlapping voices so it can't turn to mush.
      while (recentShots.length && recentShots[0]! < when - 0.03) recentShots.shift();
      if (recentShots.length >= 3) return;
      recentShots.push(when);
      playShot(ctx, gunBus, gunNoise, when, pan);
    },
    /** A rocket leaving the rail: a hiss sweeping up through a band-pass, over a low thump. */
    rocketLaunch(delay = 0, pan = 0) {
      if (!ctx || !master || !gunNoise) return;
      const t = ctx.currentTime + delay;
      const out = ctx.createStereoPanner();
      out.pan.value = Math.max(-1, Math.min(1, pan));
      out.connect(master);
      const hiss = ctx.createBufferSource();
      hiss.buffer = gunNoise;
      const band = ctx.createBiquadFilter();
      band.type = 'bandpass';
      band.Q.value = 1.2;
      band.frequency.setValueAtTime(500, t);
      band.frequency.exponentialRampToValueAtTime(3200, t + 0.5);
      const hissGain = ctx.createGain();
      hissGain.gain.setValueAtTime(0.0001, t);
      hissGain.gain.linearRampToValueAtTime(0.5, t + 0.04);
      hissGain.gain.exponentialRampToValueAtTime(0.001, t + 0.9);
      hiss.connect(band).connect(hissGain).connect(out);
      hiss.start(t, Math.random() * 0.05, 0.95);
      const thump = ctx.createOscillator();
      thump.type = 'sine';
      thump.frequency.setValueAtTime(120, t);
      thump.frequency.exponentialRampToValueAtTime(40, t + 0.18);
      const thumpGain = ctx.createGain();
      thumpGain.gain.setValueAtTime(0.0001, t);
      thumpGain.gain.linearRampToValueAtTime(0.7, t + 0.005);
      thumpGain.gain.exponentialRampToValueAtTime(0.001, t + 0.2);
      thump.connect(thumpGain).connect(out);
      thump.start(t);
      thump.stop(t + 0.22);
    },
    /** A rocket going off: a short dark boom, heavier when it strikes a plane. */
    rocketBoom(delay = 0, heavy = false) {
      if (!ctx || !master || !gunNoise) return;
      const t = ctx.currentTime + delay;
      const src = ctx.createBufferSource();
      src.buffer = gunNoise;
      const filter = ctx.createBiquadFilter();
      filter.type = 'lowpass';
      filter.frequency.setValueAtTime(heavy ? 2400 : 1500, t);
      filter.frequency.exponentialRampToValueAtTime(90, t + 0.6);
      const gain = ctx.createGain();
      gain.gain.setValueAtTime(heavy ? 0.85 : 0.45, t);
      gain.gain.exponentialRampToValueAtTime(0.001, t + 0.65);
      src.connect(filter).connect(gain).connect(master);
      src.start(t, Math.random() * 0.2, 0.7);
      const thump = ctx.createOscillator();
      thump.type = 'sine';
      thump.frequency.setValueAtTime(90, t);
      thump.frequency.exponentialRampToValueAtTime(32, t + 0.35);
      const thumpGain = ctx.createGain();
      thumpGain.gain.setValueAtTime(heavy ? 0.8 : 0.4, t);
      thumpGain.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
      thump.connect(thumpGain).connect(master);
      thump.start(t);
      thump.stop(t + 0.42);
    },
    /** A round striking a plane: a bright metallic tick, lower when a part breaks. */
    hit(destroyed: boolean) {
      if (!ctx || !master) return;
      const t = ctx.currentTime;
      const osc = ctx.createOscillator();
      osc.type = 'square';
      osc.frequency.setValueAtTime(destroyed ? 220 : 1400 + Math.random() * 500, t);
      osc.frequency.exponentialRampToValueAtTime(destroyed ? 60 : 500, t + (destroyed ? 0.5 : 0.06));
      const gain = ctx.createGain();
      gain.gain.setValueAtTime(destroyed ? 0.25 : 0.08, t);
      gain.gain.exponentialRampToValueAtTime(0.001, t + (destroyed ? 0.5 : 0.07));
      osc.connect(gain).connect(master);
      osc.start(t);
      osc.stop(t + 0.6);
    },
    crash() {
      if (!ctx || !master) return;
      const t = ctx.currentTime;
      const buffer = ctx.createBuffer(1, ctx.sampleRate, ctx.sampleRate);
      const data = buffer.getChannelData(0);
      for (let i = 0; i < data.length; i++) data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / data.length, 2.5);
      const src = ctx.createBufferSource();
      src.buffer = buffer;
      const filter = ctx.createBiquadFilter();
      filter.type = 'lowpass';
      filter.frequency.setValueAtTime(1800, t);
      filter.frequency.exponentialRampToValueAtTime(120, t + 1);
      const gain = ctx.createGain();
      gain.gain.value = 0.9;
      src.connect(filter).connect(gain).connect(master);
      src.start();
    },
  };
}