Isle-Slingers/js/audio.js
Relay ee0c4f942a Initial commit: Isle Slingers, RC-0D193E 3-bug fix
Fixes applied and verified (Hermies Qwen, RC-0D193E):
- Rubber band no longer tracks the bird after launch (bug #14, renderer.js)
- Launch direction fixed: toWorld divides by camera zoom (bug #15, main.js)
- Balance retune: stone hp 3400->1400, dmgFactor 0.35->0.8, plus an
  enemy-hit damage floor so soft lobs still kill (core.js)
- New test/balance.test.mjs: RED on pre-fix constants, GREEN after

Suite: 27/27 headless + 10/10 real-browser (real mouse events, real canvas).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 22:20:17 +00:00

137 lines
4.5 KiB
JavaScript

/* ISLE SLINGERS — audio.js
* All sound is synthesized with WebAudio oscillators/noise buffers.
* No external audio assets, fully original.
*/
(function (root, factory) {
var api = factory();
if (typeof module !== 'undefined' && module.exports) module.exports = api;
else root.ISLE_AUDIO = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
'use strict';
function AudioEngine() {
this.ctx = null;
this.master = null;
this.muted = false;
}
AudioEngine.prototype.init = function () {
if (this.ctx || typeof window === 'undefined') return;
var AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return;
this.ctx = new AC();
this.master = this.ctx.createGain();
this.master.gain.value = 0.5;
this.master.connect(this.ctx.destination);
};
AudioEngine.prototype.resume = function () {
if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume();
};
AudioEngine.prototype.setMuted = function (m) {
this.muted = m;
if (this.master) this.master.gain.value = m ? 0 : 0.5;
};
AudioEngine.prototype.noise = function (dur, filterFreq, gainVal) {
if (!this.ctx || this.muted) return;
var sr = this.ctx.sampleRate;
var buf = this.ctx.createBuffer(1, Math.max(1, (sr * dur) | 0), sr);
var data = buf.getChannelData(0);
for (var i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
var src = this.ctx.createBufferSource();
src.buffer = buf;
var filt = this.ctx.createBiquadFilter();
filt.type = 'lowpass';
filt.frequency.value = filterFreq;
var g = this.ctx.createGain();
var t0 = this.ctx.currentTime;
g.gain.setValueAtTime(gainVal, t0);
g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
src.connect(filt); filt.connect(g); g.connect(this.master);
src.start(t0);
};
AudioEngine.prototype.tone = function (freq, dur, type, gainVal, slideTo) {
if (!this.ctx || this.muted) return;
var t0 = this.ctx.currentTime;
var o = this.ctx.createOscillator();
o.type = type || 'sine';
o.frequency.setValueAtTime(freq, t0);
if (slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(30, slideTo), t0 + dur);
var g = this.ctx.createGain();
g.gain.setValueAtTime(gainVal || 0.2, t0);
g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
o.connect(g); g.connect(this.master);
o.start(t0);
o.stop(t0 + dur + 0.02);
};
AudioEngine.prototype.play = function (ev) {
if (!this.ctx || this.muted) return;
var s = ev.strength || 0;
switch (ev.type) {
case 'launch':
this.noise(0.25, 900, 0.25);
this.tone(300, 0.22, 'triangle', 0.15, 90);
break;
case 'hit':
case 'block_impact':
this.noise(0.16, ev.mat === 'ice' ? 3200 : 1200, 0.3);
this.tone(ev.mat === 'stone' ? 70 : 110, 0.12, 'sine', 0.3, 45);
break;
case 'break':
if (ev.mat === 'wood') { this.noise(0.3, 1600, 0.4); this.tone(140, 0.18, 'triangle', 0.3, 60); }
else if (ev.mat === 'ice') { this.noise(0.35, 5000, 0.45); this.tone(2200, 0.15, 'sine', 0.15, 3200); }
else { this.noise(0.4, 500, 0.5); this.tone(60, 0.3, 'sine', 0.45, 38); }
break;
case 'crack':
this.noise(0.1, 2000, 0.2);
break;
case 'pop':
this.tone(520, 0.16, 'sine', 0.35, 160);
this.noise(0.12, 2500, 0.2);
break;
case 'crush':
this.noise(0.2, 700, 0.3);
break;
case 'bird_ground':
this.noise(0.2, 800, 0.25);
break;
case 'ability_fury':
this.noise(0.3, 2600, 0.35);
this.tone(700, 0.25, 'sawtooth', 0.12, 1600);
break;
case 'ability_dive':
this.tone(900, 0.4, 'sawtooth', 0.14, 220);
break;
case 'ability_quake':
this.tone(60, 0.55, 'sine', 0.6, 30);
this.noise(0.5, 400, 0.5);
break;
case 'bird_gone':
this.tone(360, 0.2, 'sine', 0.12, 200);
break;
case 'level_won': {
var self = this;
[523, 659, 784, 1047].forEach(function (f, i) {
setTimeout(function () { self.tone(f, 0.3, 'triangle', 0.3); }, i * 140);
});
break;
}
case 'level_lost':
this.tone(300, 0.5, 'sine', 0.3, 120);
this.tone(200, 0.7, 'sine', 0.25, 80);
break;
case 'star':
this.tone(880, 0.25, 'triangle', 0.3, 1320);
break;
case 'ui':
this.tone(600, 0.06, 'sine', 0.12);
break;
}
};
return { AudioEngine: AudioEngine };
});