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.
// 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 }));
},
};
