SOURCE / PINNED RELEASE
Made of little things.
Stick Brawl
- Release
- 3ef3c9faba5c…
- Author-recorded commit
- 14482dfed9d1…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ny7l44q0au5xkv3mjgt6zks5f0e2ydefslq4nhh2ve2xypgc2mqqat8qx2/wss%3A%2F%2Fgit.napplet.soy%2F/n-554b329db93
Archive hash verified: 1d3a7d5688a99948…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
/**
* Little sound effects, made on the spot with the Web Audio API (no sound files needed).
* Browsers only allow sound after the player presses something, so the audio starts on the first key or tap.
*/
let ctx: AudioContext | null = null;
let noise: AudioBuffer | null = null;
function unlock(): void {
if (!ctx) {
try {
ctx = new AudioContext();
} catch {
return;
}
// Half a second of random hiss, shared by every whoosh and footstep.
noise = ctx.createBuffer(1, Math.floor(ctx.sampleRate * 0.5), ctx.sampleRate);
const data = noise.getChannelData(0);
for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
}
if (ctx.state === 'suspended') void ctx.resume();
}
window.addEventListener('keydown', unlock);
window.addEventListener('pointerdown', unlock);
function ready(): AudioContext | null {
return ctx && ctx.state === 'running' ? ctx : null;
}
/** A burst of hiss through a filter: footsteps and whooshes. */
function hiss(length: number, volume: number, fromFreq: number, toFreq: number, q = 1): void {
const audio = ready();
if (!audio || !noise) return;
const now = audio.currentTime;
const source = audio.createBufferSource();
source.buffer = noise;
const filter = audio.createBiquadFilter();
filter.type = 'bandpass';
filter.Q.value = q;
filter.frequency.setValueAtTime(fromFreq, now);
filter.frequency.exponentialRampToValueAtTime(toFreq, now + length);
const gain = audio.createGain();
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(volume, now + length * 0.25);
gain.gain.exponentialRampToValueAtTime(0.0001, now + length);
source.connect(filter).connect(gain).connect(audio.destination);
source.start(now, Math.random() * 0.2);
source.stop(now + length);
}
/** A short tone that slides in pitch. */
function tone(length: number, volume: number, fromFreq: number, toFreq: number, type: OscillatorType = 'square'): void {
const audio = ready();
if (!audio) return;
const now = audio.currentTime;
const osc = audio.createOscillator();
osc.type = type;
osc.frequency.setValueAtTime(fromFreq, now);
osc.frequency.exponentialRampToValueAtTime(toFreq, now + length);
const gain = audio.createGain();
gain.gain.setValueAtTime(volume, now);
gain.gain.exponentialRampToValueAtTime(0.0001, now + length);
osc.connect(gain).connect(audio.destination);
osc.start(now);
osc.stop(now + length);
}
/** A soft tap of a foot on the floor. */
export function playStep(): void {
const pitch = 0.85 + Math.random() * 0.3;
hiss(0.07, 0.25, 900 * pitch, 300 * pitch, 2);
tone(0.05, 0.06, 140 * pitch, 70 * pitch, 'sine');
}
/** A quick swish of a fist. */
export function playPunch(): void {
hiss(0.12, 0.35, 2500, 900, 1.5);
}
/** A bigger, lower swoosh for a kick. */
export function playKick(): void {
hiss(0.22, 0.45, 1400, 350, 1.2);
tone(0.12, 0.05, 220, 110, 'triangle');
}
/** A springy boing going up. */
export function playJump(): void {
tone(0.18, 0.08, 260, 620, 'square');
}
/** A loud crack and boom from the revolver. */
export function playShot(): void {
hiss(0.25, 0.9, 3500, 250, 0.6);
tone(0.18, 0.3, 160, 40, 'sine');
}
/** A ringing metal "shing" as the sword comes off the back. */
export function playSwordDraw(): void {
hiss(0.15, 0.15, 6000, 3500, 3);
tone(0.35, 0.04, 2600, 2400, 'sine');
tone(0.3, 0.03, 3900, 3600, 'sine');
}
/** A fast, sharp swish of the blade. */
export function playSwordSlash(): void {
hiss(0.16, 0.5, 4500, 1200, 2);
}
/** A hollow wooden clomp on a stair, with a little creak now and then. */
export function playStairStep(): void {
const pitch = 0.85 + Math.random() * 0.3;
hiss(0.09, 0.3, 700 * pitch, 250 * pitch, 3);
tone(0.1, 0.12, 190 * pitch, 90 * pitch, 'triangle');
if (Math.random() < 0.3) tone(0.22, 0.025, 480 * pitch, 430 * pitch, 'sawtooth');
}
/** A crunchy wooden smash as a door splinters. Far-away doors are quieter (loudness 0 to 1). */
export function playDoorBreak(loudness: number): void {
hiss(0.35, 0.9 * loudness, 2200, 300, 0.8);
hiss(0.2, 0.5 * loudness, 900, 150, 1.5);
tone(0.2, 0.25 * loudness, 120, 45, 'sine');
for (let i = 0; i < 3; i++) tone(0.05, 0.05 * loudness, 900 + Math.random() * 900, 400, 'square');
}
/** A dry little click: the gun is empty. */
export function playEmptyClick(): void {
tone(0.03, 0.08, 1800, 1200, 'square');
}
/** Click-clack: the revolver is loaded again. */
export function playReloaded(): void {
tone(0.04, 0.08, 1500, 900, 'square');
setTimeout(() => tone(0.06, 0.1, 1100, 600, 'square'), 120);
}
