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