Back to Wizard Duel
SOURCE / PINNED RELEASE

Made of little things.

Wizard Duel

Release
48b4ed933a32…
Author-recorded commit
3ed8fcbbf62c…
License
LICENSE
Author’s source reference
nostr://npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2/wss%3A%2F%2Fgit.napplet.soy%2F/n-86005d75eb4

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

src/game/sound.ts
// Little synthesized sound effects made with Web Audio (no sound files needed).
// Audio only starts after the players click or press a key, as browsers require.

let audio: AudioContext | null = null;
let enabled = true;

export function setSoundEnabled(value: boolean): void {
  enabled = value;
}

export function unlockAudio(): void {
  try {
    if (!audio) audio = new AudioContext();
    if (audio.state === 'suspended') void audio.resume();
  } catch {
    audio = null;
  }
}

function ready(): AudioContext | null {
  if (!enabled || !audio || audio.state !== 'running') return null;
  return audio;
}

interface ToneOptions {
  type?: OscillatorType;
  to?: number;
  volume?: number;
  delay?: number;
  vibrato?: number;
}

function tone(freq: number, duration: number, options: ToneOptions = {}): void {
  const ctx = ready();
  if (!ctx) return;
  const start = ctx.currentTime + (options.delay ?? 0);
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  osc.type = options.type ?? 'square';
  osc.frequency.setValueAtTime(freq, start);
  if (options.to) osc.frequency.exponentialRampToValueAtTime(options.to, start + duration);
  if (options.vibrato) {
    const lfo = ctx.createOscillator();
    const lfoGain = ctx.createGain();
    lfo.frequency.value = 9;
    lfoGain.gain.value = options.vibrato;
    lfo.connect(lfoGain).connect(osc.frequency);
    lfo.start(start);
    lfo.stop(start + duration);
  }
  const volume = options.volume ?? 0.08;
  gain.gain.setValueAtTime(volume, start);
  gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
  osc.connect(gain).connect(ctx.destination);
  osc.start(start);
  osc.stop(start + duration + 0.02);
}

function noise(duration: number, volume = 0.1, filterFreq = 1200, delay = 0): void {
  const ctx = ready();
  if (!ctx) return;
  const start = ctx.currentTime + delay;
  const length = Math.max(1, Math.floor(ctx.sampleRate * duration));
  const buffer = ctx.createBuffer(1, length, ctx.sampleRate);
  const data = buffer.getChannelData(0);
  for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1;
  const source = ctx.createBufferSource();
  source.buffer = buffer;
  const filter = ctx.createBiquadFilter();
  filter.type = 'lowpass';
  filter.frequency.value = filterFreq;
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(volume, start);
  gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
  source.connect(filter).connect(gain).connect(ctx.destination);
  source.start(start);
}

const RUNE_PITCH = { up: 660, right: 587, down: 494, left: 523 } as const;

export const sfx = {
  rune(dir: keyof typeof RUNE_PITCH, step: number) {
    tone(RUNE_PITCH[dir] * (1 + step * 0.06), 0.07, { type: 'square', volume: 0.05 });
  },
  cast() {
    tone(300, 0.25, { type: 'sawtooth', to: 900, volume: 0.06 });
  },
  fizzle() {
    tone(400, 0.3, { type: 'square', to: 80, volume: 0.05 });
    noise(0.2, 0.04, 800);
  },
  hit() {
    noise(0.15, 0.12, 900);
    tone(160, 0.15, { type: 'square', to: 60, volume: 0.07 });
  },
  clash() {
    tone(1200, 0.2, { type: 'triangle', to: 300, volume: 0.07 });
    noise(0.2, 0.08, 3000);
  },
  fire() {
    noise(0.4, 0.12, 600);
  },
  freeze() {
    tone(1400, 0.4, { type: 'triangle', to: 2400, volume: 0.05 });
    tone(1800, 0.3, { type: 'triangle', to: 2800, volume: 0.04, delay: 0.08 });
  },
  zap() {
    noise(0.3, 0.15, 5000);
    tone(90, 0.3, { type: 'sawtooth', volume: 0.06 });
  },
  boing() {
    tone(180, 0.3, { type: 'square', to: 520, volume: 0.06, vibrato: 30 });
  },
  whistle() {
    tone(1700, 0.45, { type: 'sine', to: 2300, volume: 0.07, vibrato: 60 });
    tone(2300, 0.35, { type: 'sine', to: 1500, volume: 0.06, vibrato: 60, delay: 0.5 });
  },
  hic() {
    tone(500, 0.08, { type: 'square', to: 900, volume: 0.06 });
  },
  cluck() {
    tone(700, 0.06, { type: 'square', to: 500, volume: 0.05 });
    tone(800, 0.08, { type: 'square', to: 450, volume: 0.05, delay: 0.09 });
  },
  sneeze() {
    tone(500, 0.25, { type: 'sawtooth', to: 900, volume: 0.04 });
    noise(0.3, 0.14, 2500, 0.25);
  },
  pop() {
    noise(0.08, 0.16, 4000);
  },
  splat() {
    noise(0.2, 0.12, 500);
    tone(200, 0.12, { type: 'square', to: 90, volume: 0.05 });
  },
  snore() {
    tone(90, 0.5, { type: 'sawtooth', to: 70, volume: 0.04 });
  },
  honk() {
    tone(330, 0.14, { type: 'sawtooth', to: 290, volume: 0.07 });
    tone(330, 0.14, { type: 'sawtooth', to: 290, volume: 0.07, delay: 0.2 });
  },
  opera() {
    tone(523, 0.5, { type: 'sine', volume: 0.07, vibrato: 18 });
    tone(659, 0.5, { type: 'sine', volume: 0.07, vibrato: 18, delay: 0.45 });
    tone(784, 0.7, { type: 'sine', volume: 0.07, vibrato: 25, delay: 0.9 });
  },
  scratch() {
    [0, 0.1, 0.2].forEach((delay) => noise(0.07, 0.08, 3500, delay));
  },
  bloop() {
    tone(300, 0.2, { type: 'sine', to: 900, volume: 0.08 });
  },
  buzz() {
    tone(180, 0.5, { type: 'sawtooth', volume: 0.04, vibrato: 40 });
  },
  croak() {
    tone(140, 0.15, { type: 'square', to: 90, volume: 0.07 });
    tone(160, 0.1, { type: 'square', to: 100, volume: 0.06, delay: 0.18 });
  },
  boom() {
    noise(0.7, 0.22, 700);
    tone(120, 0.5, { type: 'sawtooth', to: 40, volume: 0.08 });
  },
  squeak() {
    tone(1200, 0.08, { type: 'square', to: 1800, volume: 0.04 });
  },
  dance() {
    [196, 247, 294, 247].forEach((f, i) => tone(f, 0.09, { type: 'triangle', volume: 0.08, delay: i * 0.11 }));
  },
  disco() {
    [523, 659, 784, 1046].forEach((f, i) => tone(f, 0.1, { type: 'square', volume: 0.04, delay: i * 0.1 }));
  },
  laugh() {
    [700, 600, 700, 600].forEach((f, i) => tone(f, 0.07, { type: 'square', volume: 0.04, delay: i * 0.09 }));
  },
  summon() {
    [784, 988, 1175, 1568].forEach((f, i) => tone(f, 0.12, { type: 'triangle', volume: 0.07, delay: i * 0.07 }));
  },
  ko() {
    [523, 392, 330, 262].forEach((f, i) => tone(f, 0.2, { type: 'square', volume: 0.06, delay: i * 0.18 }));
  },
  win() {
    [523, 659, 784, 1046, 784, 1046].forEach((f, i) =>
      tone(f, 0.14, { type: 'square', volume: 0.06, delay: 0.8 + i * 0.12 }),
    );
  },
};