Back to super-platformer
SOURCE / PINNED RELEASE

Made of little things.

super-platformer

Release
b61593abc2c6…
Author-recorded commit
bbd35ca510f6…
License
LICENSE
Author’s source reference
nostr://npub16uawymqtmdg4j6zcl6snt5apzfp4ek2g8lpasnqa75tspdpglcqsgpatvc/wss%3A%2F%2Fgit.napplet.soy%2F/n-01288b1ab38

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

src/audio.ts
/**
 * Tiny Web Audio sound effects. No assets: every sound is a short synthesized
 * tone. The context is created lazily on the first user gesture (the Play
 * click), which satisfies browser autoplay rules.
 */

type Osc = OscillatorType;

class Sfx {
  private ctx: AudioContext | null = null;
  private master: GainNode | null = null;
  enabled = true;

  private ensure(): AudioContext | null {
    if (!this.enabled) return null;
    try {
      if (!this.ctx) {
        const Ctor: typeof AudioContext | undefined =
          window.AudioContext ??
          (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
        if (!Ctor) return null;
        this.ctx = new Ctor();
        this.master = this.ctx.createGain();
        this.master.gain.value = 0.5;
        this.master.connect(this.ctx.destination);
      }
      if (this.ctx.state === 'suspended') void this.ctx.resume();
      return this.ctx;
    } catch {
      return null;
    }
  }

  /** Call from a user gesture (e.g. the Play button) to unlock audio. */
  resume(): void {
    this.ensure();
  }

  setEnabled(value: boolean): void {
    this.enabled = value;
    if (value) this.ensure();
  }

  private blip(
    freq: number,
    dur: number,
    type: Osc,
    gain: number,
    delay = 0,
    slideTo?: number,
  ): void {
    const ctx = this.ensure();
    if (!ctx || !this.master) return;
    const t0 = ctx.currentTime + delay;
    const osc = ctx.createOscillator();
    const amp = ctx.createGain();
    osc.type = type;
    osc.frequency.setValueAtTime(freq, t0);
    if (slideTo) osc.frequency.exponentialRampToValueAtTime(slideTo, t0 + dur);
    amp.gain.setValueAtTime(0.0001, t0);
    amp.gain.exponentialRampToValueAtTime(gain, t0 + 0.01);
    amp.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
    osc.connect(amp);
    amp.connect(this.master);
    osc.start(t0);
    osc.stop(t0 + dur + 0.03);
  }

  jump(): void {
    this.blip(360, 0.14, 'square', 0.045, 0, 640);
  }

  coin(): void {
    this.blip(990, 0.07, 'sine', 0.05);
    this.blip(1480, 0.09, 'sine', 0.045, 0.06);
  }

  stomp(): void {
    this.blip(210, 0.12, 'sawtooth', 0.06, 0, 90);
  }

  hurt(): void {
    this.blip(420, 0.35, 'triangle', 0.07, 0, 70);
  }

  power(): void {
    [523, 659, 784, 1047].forEach((f, i) => this.blip(f, 0.1, 'square', 0.045, i * 0.07));
  }

  fire(): void {
    this.blip(900, 0.09, 'square', 0.04, 0, 320);
  }

  checkpoint(): void {
    this.blip(660, 0.1, 'sine', 0.055);
    this.blip(880, 0.13, 'sine', 0.05, 0.09);
  }

  bossHit(): void {
    this.blip(150, 0.18, 'sawtooth', 0.07, 0, 70);
  }

  bossDown(): void {
    [200, 150, 110, 80].forEach((f, i) => this.blip(f, 0.2, 'sawtooth', 0.07, i * 0.12));
  }

  complete(): void {
    [523, 659, 784, 1046].forEach((f, i) => this.blip(f, 0.14, 'triangle', 0.055, i * 0.12));
  }
}

export const sfx = new Sfx();