Back to Stick Brawl
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.

src/game/sounds.ts
/**
 * 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);
}