/** * Lane F demo — audio soundboard. * * Verifies every synthesized one-shot in isolation. Buttons emit the SAME frozen * wire events the game emits, through the real `installAudio()` wiring (so the * event → sound mapping and the mute button are exercised too, not just the raw * synth functions). An impact slider drives the squish scaling, a colour picker * drives the per-colour splat pitch, and a "new best" toggle drives the fanfare * flourish. * * Autoplay: nothing makes sound until the first click (which unlocks the * AudioContext) — matching the browser policy. There is no title screen here, so * the first-gesture fallback in installAudio does the unlocking. * * Buttons / keys (also handy for integration): * squish landing → blob:landed {impact} key 1 * boing jump → blob:jumped key 2 * splat (colour) → paint:splatted {blob} key 3 * sparkle cleanse → paint:request-cleanse key 4 * clank + windup → machine:signal key 5 * race start tick → race:started key 6 * fanfare → race:finished key 7 * SPLAT STORM (10) → 10× paint:splatted key 8 (voice-cap stress test) * ambient hum → toggle conveyor room tone key 9 */ import type { PaintColor, World } from '../contracts' import { PALETTE } from '../contracts' import { installAudio } from '../audio/install' import { startAmbientHum, type Ambient } from '../audio/sounds' // --- minimal event bus (same shape as world.ts's, so installAudio is happy) --- type Handler = (p: unknown) => void function makeBus(): World['events'] { const map = new Map>() return { on(name: string, fn: Handler) { if (!map.has(name)) map.set(name, new Set()) map.get(name)!.add(fn) return () => map.get(name)!.delete(fn) }, emit(name: string, payload?: unknown) { map.get(name)?.forEach((fn) => fn(payload)) }, } } const events = makeBus() // installAudio only ever touches world.events — a partial cast is safe here. const { engine } = installAudio({ events } as unknown as World) // ---------------------------------------------------------------- unlock badge const unlockBadge = document.getElementById('unlock')! const refreshUnlock = (): void => { if (engine.ready) { unlockBadge.textContent = '🔓 AudioContext live — hit the pads.' unlockBadge.classList.add('on') } } addEventListener('pointerdown', () => setTimeout(refreshUnlock, 0), { once: false }) // ------------------------------------------------------------------ controls UI const colors = Object.keys(PALETTE) as PaintColor[] let impact = 6 let color: PaintColor = 'red' let newBest = false const pads = document.getElementById('pads')! interface PadDef { label: string sub: string key: string fire: () => void } let ambient: Ambient | null = null const defs: PadDef[] = [ { label: '💥 squish', sub: 'blob:landed', key: '1', fire: () => events.emit('blob:landed', { impact }) }, { label: '🟢 boing', sub: 'blob:jumped', key: '2', fire: () => events.emit('blob:jumped', {}) }, { label: '🎨 splat', sub: 'paint:splatted', key: '3', fire: () => events.emit('paint:splatted', { color, target: 'blob' }) }, { label: '✨ sparkle', sub: 'paint:request-cleanse', key: '4', fire: () => events.emit('paint:request-cleanse', { fraction: 1 }) }, { label: '⚙️ clank', sub: 'machine:signal', key: '5', fire: () => events.emit('machine:signal', { id: 'demo' }) }, { label: '🏁 tick', sub: 'race:started', key: '6', fire: () => events.emit('race:started', {}) }, { label: '🎉 fanfare', sub: 'race:finished', key: '7', fire: () => events.emit('race:finished', newBest ? { time: 9, best: 9 } : { time: 12, best: 9 }) }, { label: '🌪 splat storm', sub: '10× in 1s (voice cap)', key: '8', fire: () => { for (let i = 0; i < 10; i++) { const c = colors[i % colors.length] setTimeout(() => events.emit('paint:splatted', { color: c, target: 'blob' }), i * 100) } }, }, { label: '🛗 ambient', sub: 'toggle room tone', key: '9', fire: () => { if (ambient) { ambient.stop(); ambient = null } else ambient = startAmbientHum(engine) }, }, ] for (const d of defs) { const b = document.createElement('button') b.className = 'pad' b.innerHTML = `${d.label}${d.sub} · key ${d.key}` b.addEventListener('click', () => { d.fire(); refreshUnlock() }) pads.appendChild(b) } addEventListener('keydown', (e) => { const d = defs.find((x) => x.key === e.key) if (d) { d.fire(); refreshUnlock() } }) // ---------------------------------------------------------------- sliders panel const panel = document.createElement('div') panel.className = 'ctl' panel.innerHTML = `

parameters

${impact.toFixed(1)}
` document.getElementById('app')!.appendChild(panel) const impactEl = document.getElementById('impact') as HTMLInputElement const impactVal = document.getElementById('impactVal')! impactEl.addEventListener('input', () => { impact = Number(impactEl.value) impactVal.textContent = impact.toFixed(1) }) const colorEl = document.getElementById('color') as HTMLSelectElement const swatch = document.getElementById('colorSwatch') as HTMLElement const paintSwatch = (): void => { swatch.style.color = PALETTE[color] } colorEl.addEventListener('change', () => { color = colorEl.value as PaintColor; paintSwatch() }) paintSwatch() const bestEl = document.getElementById('best') as HTMLInputElement bestEl.addEventListener('change', () => { newBest = bestEl.checked }) console.log('[lane-f] soundboard ready — click a pad (or keys 1-9) to fire wire events.')