Back to Pickle Rustlers
SOURCE / PINNED RELEASE

Made of little things.

Pickle Rustlers

Release
039e51f55e34…
Author-recorded commit
40608586a8a1…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-bad44e7ab35

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

src/game/sound.ts
// Synthesized sound effects (Web Audio, no sound files).
// Audio starts only after the player clicks, taps or presses a key.

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

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

export function unlockAudio(): void {
  try {
    if (!audio) {
      audio = new AudioContext();
      master = audio.createGain();
      master.gain.value = 0.7;
      master.connect(audio.destination);
    }
    if (audio.state === 'suspended') void audio.resume();
  } catch {
    audio = null;
  }
}

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

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

function tone(freq: number, duration: number, options: ToneOptions = {}): void {
  const ctx = ready();
  if (!ctx || !master) 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);
  const volume = options.volume ?? 0.08;
  gain.gain.setValueAtTime(volume, start);
  gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
  osc.connect(gain).connect(master);
  osc.start(start);
  osc.stop(start + duration + 0.02);
}

function noise(duration: number, volume = 0.1, filterFreq = 1200, delay = 0, to?: number): void {
  const ctx = ready();
  if (!ctx || !master) 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.setValueAtTime(filterFreq, start);
  if (to) filter.frequency.exponentialRampToValueAtTime(to, start + duration);
  const gain = ctx.createGain();
  gain.gain.setValueAtTime(volume, start);
  gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
  source.connect(filter).connect(gain).connect(master);
  source.start(start);
}

export const sfx = {
  shotgun(): void {
    noise(0.35, 0.35, 3000, 0, 300);
    tone(90, 0.2, { type: 'triangle', to: 40, volume: 0.25 });
  },
  pump(): void {
    noise(0.05, 0.12, 2500, 0);
    noise(0.06, 0.12, 1800, 0.12);
  },
  empty(): void {
    tone(900, 0.04, { type: 'square', volume: 0.05 });
  },
  lasso(): void {
    noise(0.3, 0.06, 800, 0, 2400);
  },
  roped(): void {
    tone(520, 0.08, { type: 'triangle', volume: 0.1 });
    tone(780, 0.12, { type: 'triangle', volume: 0.1, delay: 0.07 });
  },
  miss(): void {
    tone(300, 0.15, { type: 'sine', to: 180, volume: 0.06 });
  },
  plop(i = 0): void {
    tone(260 + i * 60, 0.12, { type: 'sine', to: 700 + i * 80, volume: 0.15, delay: i * 0.12 });
    noise(0.1, 0.05, 600, i * 0.12 + 0.05);
  },
  peel(): void {
    tone(700, 0.12, { type: 'sawtooth', to: 1400, volume: 0.05 });
    noise(0.12, 0.08, 4000);
  },
  pop(): void {
    tone(180, 0.15, { type: 'square', to: 60, volume: 0.12 });
    noise(0.2, 0.15, 1800);
  },
  stolen(): void {
    tone(400, 0.12, { type: 'square', to: 300, volume: 0.06 });
    tone(300, 0.2, { type: 'square', to: 200, volume: 0.06, delay: 0.12 });
  },
  hurt(): void {
    tone(500, 0.35, { type: 'sine', to: 250, volume: 0.12 });
    tone(505, 0.35, { type: 'sine', to: 240, volume: 0.08, delay: 0.05 });
  },
  cannon(): void {
    noise(0.7, 0.4, 900, 0, 80);
    tone(60, 0.5, { type: 'sine', to: 25, volume: 0.35 });
  },
  boom(): void {
    noise(0.9, 0.4, 700, 0, 60);
    tone(45, 0.7, { type: 'sine', to: 20, volume: 0.3 });
  },
  sauce(): void {
    tone(220, 0.1, { type: 'sawtooth', to: 880, volume: 0.08 });
    tone(330, 0.5, { type: 'sawtooth', to: 1320, volume: 0.06, delay: 0.1 });
    noise(0.5, 0.06, 3000, 0.05);
  },
  mount(): void {
    tone(110, 0.12, { type: 'square', volume: 0.1 });
    tone(165, 0.12, { type: 'square', volume: 0.1, delay: 0.1 });
    tone(220, 0.25, { type: 'square', volume: 0.1, delay: 0.2 });
  },
  jump(): void {
    tone(180, 0.18, { type: 'triangle', to: 420, volume: 0.09 });
    noise(0.15, 0.05, 1500, 0, 3000);
  },
  land(): void {
    tone(110, 0.12, { type: 'triangle', to: 50, volume: 0.14 });
    noise(0.18, 0.12, 900, 0, 200);
  },
  hoof(): void {
    tone(140 + Math.random() * 40, 0.04, { type: 'triangle', to: 70, volume: 0.05 });
  },
  firework(): void {
    tone(500 + Math.random() * 300, 0.25, { type: 'sine', to: 1600, volume: 0.04 });
    noise(0.5, 0.12, 5000, 0.25, 800);
  },
  kingRoar(): void {
    tone(90, 0.9, { type: 'sawtooth', to: 55, volume: 0.12 });
    tone(135, 0.9, { type: 'sawtooth', to: 70, volume: 0.08, delay: 0.05 });
    noise(0.8, 0.12, 600, 0, 200);
  },
  victory(): void {
    const notes = [392, 494, 587, 784, 587, 784, 988, 1175];
    notes.forEach((f, i) => tone(f, 0.3, { type: 'square', volume: 0.06, delay: i * 0.14 }));
    notes.forEach((f, i) => tone(f / 2, 0.3, { type: 'triangle', volume: 0.08, delay: i * 0.14 }));
  },
  dayWon(): void {
    [392, 494, 587, 784].forEach((f, i) => tone(f, 0.25, { type: 'square', volume: 0.07, delay: i * 0.12 }));
  },
  lose(): void {
    [392, 370, 349, 262].forEach((f, i) => tone(f, 0.4, { type: 'triangle', volume: 0.1, delay: i * 0.3 }));
  },
};