Files
arcane-tycoon/js/core/audio.js
T
deepseek ac00687480 Arcane Tycoon — Heroes & Magic theme park tycoon game
Complete browser game inspired by OpenRCT2 with fantasy twist:
- Custom roller coaster designer with physics-based ratings + on-ride POV
- 10 animated rides, 7 shops, 16 scenery items, path network & guest AI
- Heroes guild vs monster invasions (5 classes, XP/gear/bosses)
- Magic spell system (8 spells), research tree, economy/marketing/loans
- Day-night cycle, weather, park rating, awards, 4 scenarios
- Save/load slots + autosave, procedural WebAudio SFX/music
- Isometric canvas renderer, minimap, diagnostics overlay
- Test suites: smoke(13), linkcheck, inputcheck, framecheck, rendercheck, flow
2026-08-23 06:59:21 +00:00

112 lines
4.3 KiB
JavaScript

// ============ audio.js — procedural WebAudio SFX & ambient music ============
let AC = null;
let masterGain = null, musicGain = null, sfxGain = null;
let musicTimer = null;
let enabled = true;
const settings = { master: 0.7, music: 0.5, sfx: 0.8 };
export function initAudio() {
if (AC) return;
try {
AC = new (window.AudioContext || window.webkitAudioContext)();
masterGain = AC.createGain();
musicGain = AC.createGain();
sfxGain = AC.createGain();
musicGain.connect(masterGain);
sfxGain.connect(masterGain);
masterGain.connect(AC.destination);
applyVolumes();
} catch (e) { console.warn('Audio unavailable', e); enabled = false; }
}
export function audioSupported() { return !!AC; }
export function setVolumes(v) {
Object.assign(settings, v);
try { localStorage.setItem('at_audio', JSON.stringify(settings)); } catch { }
applyVolumes();
}
export function getVolumes() { return { ...settings }; }
function applyVolumes() {
if (!AC) return;
masterGain.gain.value = settings.master;
musicGain.gain.value = settings.music * 0.5;
sfxGain.gain.value = settings.sfx;
}
// resume on first user gesture
export function unlockAudio() {
initAudio();
if (AC && AC.state === 'suspended') AC.resume();
}
function tone(freq, dur, type = 'sine', vol = 0.3, dest, slideTo) {
if (!AC || !enabled) return;
const o = AC.createOscillator();
const g = AC.createGain();
o.type = type;
o.frequency.value = freq;
if (slideTo) o.frequency.exponentialRampToValueAtTime(Math.max(20, slideTo), AC.currentTime + dur);
g.gain.setValueAtTime(vol, AC.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, AC.currentTime + dur);
o.connect(g); g.connect(dest || sfxGain);
o.start(); o.stop(AC.currentTime + dur + 0.02);
}
function noise(dur, vol = 0.2, filterFreq = 800) {
if (!AC || !enabled) return;
const len = AC.sampleRate * dur;
const buf = AC.createBuffer(1, len, AC.sampleRate);
const d = buf.getChannelData(0);
for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
const src = AC.createBufferSource();
src.buffer = buf;
const f = AC.createBiquadFilter();
f.type = 'lowpass'; f.frequency.value = filterFreq;
const g = AC.createGain();
g.gain.setValueAtTime(vol, AC.currentTime);
g.gain.exponentialRampToValueAtTime(0.0001, AC.currentTime + dur);
src.connect(f); f.connect(g); g.connect(sfxGain);
src.start();
}
export const sfx = {
click() { tone(660, .06, 'square', .12); },
place() { noise(.08, .25, 500); tone(220, .1, 'triangle', .2); },
demolish() { noise(.22, .3, 300); },
cash() { tone(880, .09, 'sine', .18); setTimeout(() => tone(1320, .12, 'sine', .16), 70); },
error() { tone(180, .18, 'sawtooth', .15, null, 120); },
openRide() { [440, 554, 659, 880].forEach((f, i) => setTimeout(() => tone(f, .14, 'triangle', .15), i * 90)); },
whoosh() { noise(.5, .18, 900); },
spell() {
if (!AC) return;
[660, 830, 990, 1245].forEach((f, i) => setTimeout(() => tone(f, .3, 'sine', .12), i * 60));
},
hit() { noise(.07, .28, 700); tone(140, .08, 'square', .14); },
monsterRoar() { tone(90, .5, 'sawtooth', .25, null, 50); noise(.4, .2, 250); },
victory() { [523, 659, 784, 1046].forEach((f, i) => setTimeout(() => tone(f, .35, 'triangle', .2), i * 160)); },
defeat() { [400, 340, 280, 200].forEach((f, i) => setTimeout(() => tone(f, .4, 'sawtooth', .15), i * 200)); },
levelup() { [600, 750, 900].forEach((f, i) => setTimeout(() => tone(f, .12, 'square', .1), i * 80)); },
};
// ------- generative ambient music: gentle fantasy pad loop -------
const SCALE = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25]; // C pentatonic-ish
let musicOn = false;
export function startMusic() {
if (!AC || musicOn) return;
musicOn = true;
const stepFn = () => {
if (!musicOn) return;
// soft chord every ~2s
const root = SCALE[Math.floor(Math.random() * 3)];
const third = SCALE[Math.floor(Math.random() * SCALE.length)];
tone(root / 2, 2.4, 'sine', .10, musicGain);
tone(third, 2.2, 'triangle', .05, musicGain);
if (Math.random() < 0.4) tone(root * 2, 1.8, 'sine', .04, musicGain);
musicTimer = setTimeout(stepFn, 1800 + Math.random() * 900);
};
stepFn();
}
export function stopMusic() {
musicOn = false;
if (musicTimer) clearTimeout(musicTimer);
}
export function isMusicOn() { return musicOn; }