// audio.js — procedural sound for REIKHELM: DESCENT. No assets: every cue is
// synthesized on a shared AudioContext (oscillators + filtered noise), so the
// game ships as code only. Context unlocks on the first user gesture.

let ctx = null;
let master = null;
let ambient = null;

function ac() {
  if (!ctx) {
    ctx = new (window.AudioContext || window.webkitAudioContext)();
    master = ctx.createGain();
    master.gain.value = 0.5;
    master.connect(ctx.destination);
  }
  if (ctx.state === 'suspended') ctx.resume();
  return ctx;
}

// One reusable noise buffer (1s of white noise).
let noiseBuf = null;
function noise() {
  const c = ac();
  if (!noiseBuf) {
    noiseBuf = c.createBuffer(1, c.sampleRate, c.sampleRate);
    const d = noiseBuf.getChannelData(0);
    for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
  }
  const src = c.createBufferSource();
  src.buffer = noiseBuf;
  return src;
}

// A tone with an exponential decay envelope.
function tone({ freq = 440, type = 'sine', dur = 0.2, vol = 0.3, when = 0, slide = 0 }) {
  const c = ac();
  const t0 = c.currentTime + when;
  const osc = c.createOscillator();
  const g = c.createGain();
  osc.type = type;
  osc.frequency.setValueAtTime(freq, t0);
  if (slide) osc.frequency.exponentialRampToValueAtTime(Math.max(20, freq + slide), t0 + dur);
  g.gain.setValueAtTime(vol, t0);
  g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
  osc.connect(g).connect(master);
  osc.start(t0);
  osc.stop(t0 + dur + 0.02);
}

// A burst of filtered noise with a decay envelope.
function thump({ cutoff = 400, dur = 0.15, vol = 0.4, when = 0, q = 1, type = 'lowpass' }) {
  const c = ac();
  const t0 = c.currentTime + when;
  const src = noise();
  const f = c.createBiquadFilter();
  f.type = type;
  f.frequency.value = cutoff;
  f.Q.value = q;
  const g = c.createGain();
  g.gain.setValueAtTime(vol, t0);
  g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
  src.connect(f).connect(g).connect(master);
  src.start(t0);
  src.stop(t0 + dur + 0.02);
}

export const sfx = {
  unlock() { ac(); },

  step() {
    thump({ cutoff: 220 + Math.random() * 80, dur: 0.12, vol: 0.35 });
    thump({ cutoff: 150, dur: 0.08, vol: 0.2, when: 0.05 });
  },
  bump() {
    thump({ cutoff: 120, dur: 0.2, vol: 0.5 });
    tone({ freq: 65, type: 'triangle', dur: 0.18, vol: 0.3 });
  },
  turn() {
    thump({ cutoff: 500, dur: 0.05, vol: 0.12 });
  },
  rest() {
    tone({ freq: 196, type: 'sine', dur: 0.4, vol: 0.06, slide: 30 });
  },
  loot() {
    tone({ freq: 880, type: 'triangle', dur: 0.1, vol: 0.2 });
    tone({ freq: 1320, type: 'triangle', dur: 0.15, vol: 0.2, when: 0.07 });
  },
  relic() {
    [523, 659, 784, 1046].forEach((f, i) => tone({ freq: f, type: 'triangle', dur: 0.3, vol: 0.18, when: i * 0.09 }));
  },
  potion() {
    tone({ freq: 300, type: 'sine', dur: 0.25, vol: 0.2, slide: 260 });
  },
  encounter() {
    tone({ freq: 110, type: 'sawtooth', dur: 0.5, vol: 0.25, slide: -45 });
    thump({ cutoff: 800, dur: 0.4, vol: 0.25, when: 0.05 });
    tone({ freq: 55, type: 'triangle', dur: 0.7, vol: 0.3, when: 0.1 });
  },
  growl() {
    tone({ freq: 70, type: 'sawtooth', dur: 0.5, vol: 0.07, slide: -20 });
  },
  hit() {
    thump({ cutoff: 900, dur: 0.1, vol: 0.4, type: 'bandpass', q: 1.5 });
    tone({ freq: 180, type: 'square', dur: 0.07, vol: 0.12 });
  },
  heavyHit() {
    thump({ cutoff: 500, dur: 0.25, vol: 0.55, type: 'bandpass' });
    tone({ freq: 90, type: 'sawtooth', dur: 0.3, vol: 0.3, slide: -30 });
  },
  playerHit() {
    thump({ cutoff: 300, dur: 0.2, vol: 0.5 });
    tone({ freq: 140, type: 'square', dur: 0.12, vol: 0.15, slide: -40 });
  },
  cast() {
    tone({ freq: 440, type: 'sine', dur: 0.35, vol: 0.12, slide: 320 });
  },
  fizzle() {
    tone({ freq: 600, type: 'sawtooth', dur: 0.25, vol: 0.15, slide: -480 });
  },
  daze() {
    tone({ freq: 660, type: 'triangle', dur: 0.6, vol: 0.2, slide: -200 });
    tone({ freq: 663, type: 'triangle', dur: 0.6, vol: 0.2, slide: -210 });
  },
  guard() {
    thump({ cutoff: 2500, dur: 0.08, vol: 0.2, type: 'highpass' });
    tone({ freq: 520, type: 'square', dur: 0.06, vol: 0.08 });
  },
  recovery() {
    tone({ freq: 392, type: 'sine', dur: 0.4, vol: 0.18, slide: 130 });
  },
  death() {
    tone({ freq: 220, type: 'sawtooth', dur: 1.4, vol: 0.3, slide: -170 });
    thump({ cutoff: 200, dur: 1.0, vol: 0.4, when: 0.1 });
  },
  monsterDeath() {
    tone({ freq: 160, type: 'sawtooth', dur: 0.7, vol: 0.25, slide: -110 });
    thump({ cutoff: 350, dur: 0.5, vol: 0.3, when: 0.05 });
  },
  fled() {
    [400, 320, 260].forEach((f, i) => thump({ cutoff: f, dur: 0.1, vol: 0.3, when: i * 0.09 }));
  },
  descend() {
    [200, 160, 120, 90].forEach((f, i) => {
      thump({ cutoff: f * 2, dur: 0.25, vol: 0.35, when: i * 0.22 });
      tone({ freq: f, type: 'triangle', dur: 0.3, vol: 0.15, when: i * 0.22 });
    });
  },
  victory() {
    [392, 523, 659, 784, 1046].forEach((f, i) =>
      tone({ freq: f, type: 'triangle', dur: 0.5, vol: 0.2, when: i * 0.13 }));
    thump({ cutoff: 4000, dur: 1.2, vol: 0.08, when: 0.6, type: 'highpass' });
  },

  // A faint dungeon bed: low drone + slow filtered rumble. Idempotent.
  ambientOn() {
    const c = ac();
    if (ambient) return;
    const g = c.createGain();
    g.gain.value = 0.05;
    const drone = c.createOscillator();
    drone.type = 'sine';
    drone.frequency.value = 55;
    const drone2 = c.createOscillator();
    drone2.type = 'sine';
    drone2.frequency.value = 55.7; // beat frequency shimmer
    const n = noise();
    n.loop = true;
    const f = c.createBiquadFilter();
    f.type = 'lowpass';
    f.frequency.value = 90;
    const ng = c.createGain();
    ng.gain.value = 0.4;
    drone.connect(g);
    drone2.connect(g);
    n.connect(f).connect(ng).connect(g);
    g.connect(master);
    drone.start();
    drone2.start();
    n.start();
    ambient = { g, drone, drone2, n };
  },
  ambientOff() {
    if (!ambient) return;
    const { g, drone, drone2, n } = ambient;
    g.gain.linearRampToValueAtTime(0, ac().currentTime + 0.6);
    setTimeout(() => { drone.stop(); drone2.stop(); n.stop(); }, 700);
    ambient = null;
  },
};
