Files
deepseek c06786518c Wildlife Kingdom — original browser zoo-management sim
- Procedural canvas art: 15 species × 4 poses, 15 buildings, modular terrain, UI icons
- Habitat enclosure detection, biome suitability, staff/guest AI pathfinding
- Economy: tickets, shops, wages, star rating, research tiers, 15 missions
- Day/night cycle, particles, WebAudio soundtrack & synth SFX
- Mouse + touch controls, responsive UI, localStorage autosave
- Verified: 31/31 logic tests (test/smoke.js) · 12/12 render audits (test/qa.html)
2026-08-23 07:01:21 +00:00

140 lines
6.0 KiB
JavaScript

/* ============================================================
Wildlife Kingdom — js/audio.js
Procedural WebAudio: gentle marimba soundtrack + synthesized
SFX. No audio files, fully original. Global: WK.AudioSys
============================================================ */
(function(){
'use strict';
const WK = window.WK;
const A = {
ctx:null, master:null, musicBus:null, sfxBus:null,
enabled:true, musicOn:true, scene:'menu',
timer:null, nextNote:0, step:0,
};
function ensure(){
if(A.ctx){ if(A.ctx.state==='suspended') A.ctx.resume(); return true; }
const AC = window.AudioContext || window.webkitAudioContext;
if(!AC) return false;
A.ctx = new AC();
A.master = A.ctx.createGain(); A.master.gain.value=0.9; A.master.connect(A.ctx.destination);
A.musicBus = A.ctx.createGain(); A.musicBus.gain.value = 0.5; A.musicBus.connect(A.master);
A.sfxBus = A.ctx.createGain(); A.sfxBus.gain.value = 0.85; A.sfxBus.connect(A.master);
loadPrefs();
startScheduler();
return true;
}
function loadPrefs(){
try{
const p = JSON.parse(localStorage.getItem('wk.audio')||'{}');
if(p.enabled===false) A.enabled=false;
if(p.musicOn===false) A.musicOn=false;
}catch(e){}
}
function savePrefs(){
try{ localStorage.setItem('wk.audio', JSON.stringify({enabled:A.enabled,musicOn:A.musicOn})); }catch(e){}
}
/* ---------- tiny synth helpers ---------- */
function tone(opt){
// {f, f2, t='sine', dur, vol, bus, at, attack, release}
const c=A.ctx; if(!c) return;
const at = opt.at || c.currentTime;
const o=c.createOscillator(), g=c.createGain();
o.type=opt.t||'sine';
o.frequency.setValueAtTime(opt.f,at);
if(opt.f2) o.frequency.exponentialRampToValueAtTime(Math.max(20,opt.f2), at+(opt.dur||0.2));
const v=(opt.vol==null?0.2:opt.vol);
g.gain.setValueAtTime(0.0001,at);
g.gain.linearRampToValueAtTime(v, at+(opt.attack||0.008));
g.gain.exponentialRampToValueAtTime(0.0001, at+(opt.dur||0.2));
o.connect(g); g.connect(opt.bus||A.sfxBus);
o.start(at); o.stop(at+(opt.dur||0.2)+0.05);
}
function noiseHit(opt){
// {dur,vol,f,q,bus,at}
const c=A.ctx; if(!c) return;
const at=opt.at||c.currentTime;
const len=Math.max(1,Math.floor(c.sampleRate*(opt.dur||0.15)));
const buf=c.createBuffer(1,len,c.sampleRate);
const d=buf.getChannelData(0);
for(let i=0;i<len;i++) d[i]=(Math.random()*2-1)*(1-i/len);
const src=c.createBufferSource(); src.buffer=buf;
const flt=c.createBiquadFilter(); flt.type='bandpass'; flt.frequency.value=opt.f||800; flt.Q.value=opt.q||1;
const g=c.createGain(); g.gain.value=opt.vol==null?0.25:opt.vol;
src.connect(flt); flt.connect(g); g.connect(opt.bus||A.sfxBus);
src.start(at);
}
/* ---------- SFX vocabulary (all synthesized) ---------- */
const SFX = {
click(){ tone({f:660,f2:880,t:'triangle',dur:0.07,vol:0.12}); },
place(){ noiseHit({dur:0.09,vol:0.3,f:420,q:0.8}); tone({f:180,f2:120,t:'sine',dur:0.12,vol:0.22}); },
dig(){ noiseHit({dur:0.14,vol:0.22,f:300,q:0.7}); },
coin(){ tone({f:988,t:'square',dur:0.06,vol:0.08}); tone({f:1319,t:'square',dur:0.16,vol:0.09,at:A.ctx&&A.ctx.currentTime+0.06}); },
error(){ tone({f:220,f2:160,t:'sawtooth',dur:0.18,vol:0.12}); },
pop(){ tone({f:520,f2:1040,t:'sine',dur:0.09,vol:0.15}); },
cheer(){ [523,659,784,1047].forEach((f,i)=>tone({f,t:'triangle',dur:0.18,vol:0.1,at:A.ctx&&A.ctx.currentTime+i*0.07})); },
roar(){ tone({f:150,f2:70,t:'sawtooth',dur:0.5,vol:0.14}); noiseHit({dur:0.4,vol:0.1,f:200,q:0.6}); },
chirp(){ tone({f:1400,f2:2100,t:'sine',dur:0.12,vol:0.1}); tone({f:1800,f2:2400,t:'sine',dur:0.1,vol:0.08,at:A.ctx&&A.ctx.currentTime+0.1}); },
splash(){ noiseHit({dur:0.3,vol:0.28,f:900,q:0.4}); },
trash(){ noiseHit({dur:0.12,vol:0.25,f:250,q:0.9}); },
unlock(){ [392,523,659,784].forEach((f,i)=>tone({f,t:'sine',dur:0.25,vol:0.11,at:A.ctx&&A.ctx.currentTime+i*0.09})); },
};
WK.AudioSys = {
ensure,
get enabled(){return A.enabled;},
get musicOn(){return A.musicOn;},
sfx(name){
if(!A.enabled) return;
if(!ensure()) return;
const fn=SFX[name]; if(fn) fn();
},
setScene(s){ A.scene=s; },
toggleSound(){ A.enabled=!A.enabled; savePrefs(); if(A.enabled) ensure(); WK.UI&&WK.UI.refreshSoundBtn&&WK.UI.refreshSoundBtn(); return A.enabled; },
toggleMusic(){ A.musicOn=!A.musicOn; savePrefs(); return A.musicOn; },
};
/* ---------- music scheduler ----------
Relaxed pentatonic marimba + warm pad, randomized gentle walk.
Menu: slower & dreamier. Game: slightly brighter tempo. */
const PENTA=[0,2,4,7,9];
function nfreq(semi, base){ return base*Math.pow(2,semi/12); }
function startScheduler(){
if(A.timer) return;
A.timer=setInterval(()=>{
if(!A.ctx || !A.enabled || !A.musicOn) return;
const ahead = 0.45;
const menu = A.scene==='menu';
const stepDur = menu? 60/68/2 : 60/76/2; // eighth notes
const root = menu? 220 : 246.94; // A3 / B3
while(A.nextNote < A.ctx.currentTime + ahead){
const t=Math.max(A.nextNote, A.ctx.currentTime+0.02);
const bar=Math.floor(A.step/8), beat=A.step%8;
// pad chord every bar
if(beat===0){
const deg=PENTA[(bar*2)%5];
[0,7,12].forEach(iv=>tone({f:nfreq(deg+iv,root/2)*2,t:'sine',dur:stepDur*8,vol:0.035,bus:A.musicBus,at:t,attack:0.6}));
tone({f:nfreq(PENTA[bar%5],root)/2,t:'triangle',dur:stepDur*3.6,vol:0.10,bus:A.musicBus,at:t,attack:0.01});
}
// pluck melody: sparse random walk on pentatonic
const density = menu?0.42:0.55;
if(WK.hash2(A.step,bar,7) < density){
const idx = Math.floor(WK.hash2(A.step*13,bar,3)*PENTA.length);
const oct = (WK.hash2(A.step,bar*3,11)<0.3)?12:0;
tone({f:nfreq(PENTA[idx]+oct,root),t:'triangle',dur:stepDur*2.4,vol:0.085,bus:A.musicBus,at:t});
if(WK.hash2(A.step,bar,23)<0.25)
tone({f:nfreq(PENTA[idx]+oct+12,root)*2,t:'sine',dur:stepDur*1.6,vol:0.04,bus:A.musicBus,at:t+stepDur*0.5});
}
// soft shaker
if(beat%2===1 && !menu) noiseHit({dur:0.03,vol:0.018,f:6000,q:1.2,bus:A.musicBus,at:t});
A.nextNote = t + stepDur;
A.step++;
}
},120);
}
})();