Isle-Slingers/js/audio/index.js
Relay dc68c6e3dd Physics upgrade + repo modularization (RC-375D84)
Delivered by Hermies Qwen, gated and deployed by Relay.

- Board tip/topple: full continuous rotation when a board's center of
  mass passes its support edge (Q1: smooth pivot swing, no discrete
  snap). Falling boards deal impact damage and can kill enemies in
  their fall path.
- Falling-object impact damage: blocks and birds can now kill enemies
  from vertical drops and side plows (velocity-aware crush gate),
  chain momentum transfers correctly through stacks (mass-weighted
  contact), repeated impacts finish tougher enemies.
- Landed-bird physics: a bird resting on a board remains a real
  physical body under continuous gravity (Q2: no lifetime timer). If
  its platform is destroyed it falls, damages what it lands on
  (chain reaction), and can kill enemies.
- Repo modularized: folders = modules, subfolders = submodules,
  architecture.md + MODULES.md at root, an agents.md card in every
  module and submodule (17 total).
- Test suite grown from 27 to 47 (20 new RED-watched tests for the
  above behaviors), all green. Golden differential against the
  original core confirmed the only behavioral changes are the
  intended ones.

Live at http://10.244.136.121:8093/, independently re-verified by
Relay (full suite re-run, module structure check, headless Chromium
smoke) before this push.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:21:18 +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 };
});