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>
137 lines
4.5 KiB
JavaScript
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 };
|
|
});
|