Deepgrain Foundry: move from a claude.ai artifact to a local repository
The game as of artifact version 24, ready for local development with coding agents. - Source split from three large files into 52 small ones under src/sim, src/render and src/ui, joined in the order in src/manifest.json by build.py. The joined code is unchanged apart from comments; every headless suite gives the same output, and screenshots in both looks are pixel-identical to before. - Light tidying with no change in behaviour: one movement scan loop, shared hazard-stripe and belt shader helpers, unused code removed, a shared check helper for the test suites. - World files can be exported as an ordinary download outside claude.ai. - Tests: broken old scripts removed, experiments moved to tools/, test/run_all.py added. Tools: shot.py (repeatable screenshots), where.py, map.py. - Docs: AGENTS.md, CLAUDE.md, NOTES.md (the owner's to-do inbox), docs/PROJECT_MAP.md, DESIGN.md, TESTING.md, systems notes, the owner's requests and an agent log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9kLmYADKSEjuwUN3rFxDA
This commit is contained in:
+209
@@ -0,0 +1,209 @@
|
||||
/* =====================================================================
|
||||
Audio: chiptune effects and generative music that changes with depth.
|
||||
Everything is synthesized with WebAudio, no samples.
|
||||
===================================================================== */
|
||||
|
||||
const AU = {
|
||||
ctx: null, master: null, music: null, sfx: null, amb: null, noise: null, pulse12: null, pulse25: null,
|
||||
last: {}, started: false,
|
||||
zone: 0, nextNote: 0, step: 0, bar: 0, chordIdx: 0, timer: 0, melody: null, melPos: 0, lastDeg: 0,
|
||||
hum: null, humGain: null, crackleT: 0, wind: null, windGain: null,
|
||||
};
|
||||
|
||||
function auInit() {
|
||||
if (AU.ctx) { if (AU.ctx.state === 'suspended') AU.ctx.resume(); return; }
|
||||
const AC = window.AudioContext || window.webkitAudioContext;
|
||||
if (!AC) return;
|
||||
const ctx = new AC();
|
||||
AU.ctx = ctx;
|
||||
const comp = ctx.createDynamicsCompressor();
|
||||
comp.threshold.value = -16; comp.ratio.value = 4;
|
||||
comp.connect(ctx.destination);
|
||||
AU.master = ctx.createGain(); AU.master.connect(comp);
|
||||
AU.music = ctx.createGain(); AU.music.connect(AU.master);
|
||||
AU.sfx = ctx.createGain(); AU.sfx.connect(AU.master);
|
||||
AU.amb = ctx.createGain(); AU.amb.connect(AU.sfx);
|
||||
// white noise buffer
|
||||
const len = ctx.sampleRate * 2, buf = ctx.createBuffer(1, len, ctx.sampleRate), d = buf.getChannelData(0);
|
||||
for (let i = 0; i < len; i++) d[i] = Math.random() * 2 - 1;
|
||||
AU.noise = buf;
|
||||
const pulse = duty => {
|
||||
const n = 64, re = new Float32Array(n), im = new Float32Array(n);
|
||||
for (let k = 1; k < n; k++) { re[k] = 0; im[k] = (2 / (k * Math.PI)) * Math.sin(k * Math.PI * duty); }
|
||||
return ctx.createPeriodicWave(re, im);
|
||||
};
|
||||
AU.pulse12 = pulse(0.125); AU.pulse25 = pulse(0.25);
|
||||
// machine hum
|
||||
AU.hum = ctx.createOscillator(); AU.hum.setPeriodicWave(AU.pulse25); AU.hum.frequency.value = 55;
|
||||
const lp = ctx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 320;
|
||||
AU.humGain = ctx.createGain(); AU.humGain.gain.value = 0;
|
||||
AU.hum.connect(lp); lp.connect(AU.humGain); AU.humGain.connect(AU.amb); AU.hum.start();
|
||||
// wind / surface air
|
||||
const ws = ctx.createBufferSource(); ws.buffer = buf; ws.loop = true;
|
||||
const wf = ctx.createBiquadFilter(); wf.type = 'bandpass'; wf.frequency.value = 500; wf.Q.value = 0.7;
|
||||
AU.windGain = ctx.createGain(); AU.windGain.gain.value = 0;
|
||||
ws.connect(wf); wf.connect(AU.windGain); AU.windGain.connect(AU.amb); ws.start();
|
||||
AU.windFilter = wf;
|
||||
auVolumes();
|
||||
AU.nextNote = ctx.currentTime + 0.3;
|
||||
AU.timer = setInterval(musicScheduler, 60);
|
||||
AU.started = true;
|
||||
}
|
||||
function auVolumes() {
|
||||
if (!AU.ctx) return;
|
||||
const m = SET.muted ? 0 : SET.volMaster;
|
||||
AU.master.gain.setTargetAtTime(m, AU.ctx.currentTime, 0.05);
|
||||
AU.music.gain.setTargetAtTime(SET.volMusic * 0.55, AU.ctx.currentTime, 0.1);
|
||||
AU.sfx.gain.setTargetAtTime(SET.volSfx, AU.ctx.currentTime, 0.05);
|
||||
}
|
||||
function rateOk(key, ms) {
|
||||
const now = performance.now();
|
||||
if (AU.last[key] && now - AU.last[key] < ms) return false;
|
||||
AU.last[key] = now; return true;
|
||||
}
|
||||
function tone(freq, dur, opts = {}) {
|
||||
const ctx = AU.ctx; if (!ctx) return;
|
||||
const t = (opts.at || ctx.currentTime) + 0.005;
|
||||
const o = ctx.createOscillator();
|
||||
if (opts.wave === 'p12') o.setPeriodicWave(AU.pulse12); else if (opts.wave === 'p25') o.setPeriodicWave(AU.pulse25); else o.type = opts.wave || 'square';
|
||||
o.frequency.setValueAtTime(freq, t);
|
||||
if (opts.to) o.frequency.exponentialRampToValueAtTime(opts.to, t + dur);
|
||||
if (opts.vib) { const l = ctx.createOscillator(), lg = ctx.createGain(); l.frequency.value = 5.5; lg.gain.value = freq * 0.012; l.connect(lg); lg.connect(o.frequency); l.start(t); l.stop(t + dur + 0.05); }
|
||||
const g = ctx.createGain();
|
||||
const v = opts.vol || 0.1, a = opts.attack || 0.005, rel = opts.release || 0.04;
|
||||
g.gain.setValueAtTime(0.0001, t);
|
||||
g.gain.exponentialRampToValueAtTime(v, t + a);
|
||||
g.gain.setValueAtTime(v, t + Math.max(a, dur - rel));
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
||||
o.connect(g); g.connect(opts.bus || AU.sfx);
|
||||
o.start(t); o.stop(t + dur + 0.02);
|
||||
}
|
||||
function noiseBurst(dur, opts = {}) {
|
||||
const ctx = AU.ctx; if (!ctx) return;
|
||||
const t = (opts.at || ctx.currentTime) + 0.005;
|
||||
const s = ctx.createBufferSource(); s.buffer = AU.noise;
|
||||
s.playbackRate.value = opts.rate || 1;
|
||||
const f = ctx.createBiquadFilter(); f.type = opts.type || 'bandpass'; f.frequency.value = opts.freq || 1200; f.Q.value = opts.q || 1;
|
||||
if (opts.sweep) f.frequency.exponentialRampToValueAtTime(opts.sweep, t + dur);
|
||||
const g = ctx.createGain(); const v = opts.vol || 0.08;
|
||||
g.gain.setValueAtTime(v, t); g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
||||
s.connect(f); f.connect(g); g.connect(opts.bus || AU.sfx);
|
||||
s.start(t, Math.random() * 1.5); s.stop(t + dur + 0.02);
|
||||
}
|
||||
const SFX = {
|
||||
dig(h) { if (!rateOk('dig', 75)) return; noiseBurst(0.06, { freq: 260 + h * 260, q: 1.6, vol: 0.09 }); if (h >= 3) tone(120 + h * 40, 0.04, { wave: 'p12', vol: 0.03 }); },
|
||||
// a resonance pulse: a low thrum, higher and tighter for a narrow pulse
|
||||
pulse(w) { if (!rateOk('pulse', 70)) return; const f = [150, 110, 82][w | 0] || 110; tone(f, 0.16, { wave: 'sine', to: f * 0.72, vol: 0.09, release: 0.12 }); tone(f * 3.02, 0.07, { wave: 'triangle', vol: 0.012, release: 0.06 }); },
|
||||
place() { if (!rateOk('place', 70)) return; tone(520, 0.045, { wave: 'p25', to: 660, vol: 0.05 }); },
|
||||
build() { if (!rateOk('build', 90)) return; tone(440, 0.05, { wave: 'p25', to: 520, vol: 0.035 }); },
|
||||
decon() { if (!rateOk('decon', 80)) return; tone(520, 0.06, { wave: 'p25', to: 300, vol: 0.05 }); },
|
||||
spark() { if (!rateOk('spark', 90)) return; noiseBurst(0.05, { type: 'highpass', freq: 3000, vol: 0.06 }); },
|
||||
ignite() { if (!rateOk('ignite', 400)) return; noiseBurst(0.5, { type: 'lowpass', freq: 400, sweep: 2400, vol: 0.12 }); },
|
||||
sizzle() { if (!rateOk('sizzle', 220)) return; noiseBurst(0.22, { type: 'highpass', freq: 4500, vol: 0.05 }); },
|
||||
ui() { if (!rateOk('ui', 40)) return; tone(1100, 0.025, { wave: 'p25', vol: 0.035 }); },
|
||||
error() { if (!rateOk('err', 300)) return; tone(110, 0.14, { wave: 'p12', vol: 0.07 }); tone(98, 0.14, { wave: 'p12', vol: 0.05, at: AU.ctx && AU.ctx.currentTime + 0.07 }); },
|
||||
gate() { noiseBurst(0.12, { type: 'lowpass', freq: 600, vol: 0.12 }); tone(90, 0.12, { wave: 'triangle', to: 60, vol: 0.12 }); },
|
||||
cast() { if (!AU.ctx) return; const t = AU.ctx.currentTime; [523, 659, 784, 1047].forEach((f, k) => tone(f, 0.12, { wave: 'p25', vol: 0.06, at: t + k * 0.08 })); },
|
||||
upgrade() {
|
||||
if (!AU.ctx) return; const t = AU.ctx.currentTime;
|
||||
[392, 523, 659, 784, 659, 784, 1047].forEach((f, k) => tone(f, k === 6 ? 0.5 : 0.11, { wave: k % 2 ? 'p25' : 'p12', vol: 0.07, at: t + k * 0.09, vib: k === 6 }));
|
||||
[196, 262, 330].forEach((f, k) => tone(f, 0.35, { wave: 'triangle', vol: 0.08, at: t + 0.27 + k * 0.18 }));
|
||||
},
|
||||
milestone() {
|
||||
if (!AU.ctx) return; const t = AU.ctx.currentTime;
|
||||
[659, 784, 988, 1319].forEach((f, k) => tone(f, k === 3 ? 0.35 : 0.1, { wave: 'p25', vol: 0.06, at: t + k * 0.07, vib: k === 3 }));
|
||||
},
|
||||
suck() { if (!rateOk('suck', 110)) return; noiseBurst(0.1, { type: 'bandpass', freq: 900, sweep: 2600, q: 2.2, vol: 0.045 }); },
|
||||
blow() { if (!rateOk('blow', 110)) return; noiseBurst(0.12, { type: 'bandpass', freq: 2200, sweep: 700, q: 1.6, vol: 0.05 }); },
|
||||
sale() { if (!AU.ctx) return; const t = AU.ctx.currentTime; [784, 988, 1175, 1568].forEach((f, k) => tone(f, 0.09, { wave: 'p25', vol: 0.05, at: t + k * 0.06 })); },
|
||||
discover() { if (!rateOk('disc', 500)) return; const t = AU.ctx && AU.ctx.currentTime; tone(880, 0.06, { wave: 'p25', vol: 0.04, at: t }); tone(1175, 0.1, { wave: 'p25', vol: 0.04, at: t + 0.06 }); },
|
||||
};
|
||||
function playSfx(name, arg) { if (!AU.ctx || !SFX[name]) return; try { SFX[name](arg); } catch (e) { /* ignore */ } }
|
||||
|
||||
// Ambient layer driven by what is on screen.
|
||||
function auAmbient(info) {
|
||||
if (!AU.ctx) return;
|
||||
const t = AU.ctx.currentTime;
|
||||
AU.humGain.gain.setTargetAtTime(Math.min(0.05, info.power * 0.004), t, 0.4);
|
||||
AU.hum.frequency.setTargetAtTime(48 + Math.min(30, info.power * 0.6), t, 0.6);
|
||||
// wind on the surface; rain turns it into a hiss
|
||||
const rain = info.rain || 0;
|
||||
AU.windGain.gain.setTargetAtTime(info.surface ? 0.018 + 0.01 * Math.sin(t * 0.3) + rain * 0.05 : 0.004 + rain * 0.008, t, 1.2);
|
||||
AU.windFilter.frequency.setTargetAtTime(info.surface ? 420 + 160 * Math.sin(t * 0.21) + rain * 1900 : 260 + rain * 400, t, 1.5);
|
||||
// fire crackle
|
||||
if (info.fire > 0 && performance.now() > AU.crackleT) {
|
||||
const n = Math.min(4, 1 + (info.fire / 60) | 0);
|
||||
for (let k = 0; k < n; k++) noiseBurst(0.02 + Math.random() * 0.03, { type: 'bandpass', freq: 900 + Math.random() * 2200, q: 2, vol: 0.02 + Math.min(0.05, info.fire * 0.0004), at: t + Math.random() * 0.15, bus: AU.amb });
|
||||
AU.crackleT = performance.now() + 120 + Math.random() * Math.max(60, 700 - info.fire * 4);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Generative music ----------
|
||||
const ZONES = [
|
||||
{ name: 'surface', root: 60, scale: [0, 2, 4, 5, 7, 9, 11], prog: [0, 5, 3, 4], tempo: 92, density: 0.62, hat: 0.5, lead: 0.55, oct: 0 },
|
||||
{ name: 'shallows', root: 57, scale: [0, 2, 3, 5, 7, 9, 10], prog: [0, 3, 6, 0], tempo: 84, density: 0.5, hat: 0.3, lead: 0.45, oct: 0 },
|
||||
{ name: 'caverns', root: 62, scale: [0, 2, 3, 5, 7, 8, 10], prog: [0, 5, 6, 4], tempo: 76, density: 0.4, hat: 0.18, lead: 0.35, oct: -12 },
|
||||
{ name: 'deep', root: 52, scale: [0, 1, 3, 5, 7, 8, 10], prog: [0, 1, 0, 6], tempo: 66, density: 0.3, hat: 0.08, lead: 0.25, oct: -12 },
|
||||
{ name: 'magma', root: 49, scale: [0, 1, 4, 5, 7, 8, 10], prog: [0, 1, 0, 1], tempo: 60, density: 0.24, hat: 0.04, lead: 0.2, oct: -12 },
|
||||
];
|
||||
function mtof(m) { return 440 * Math.pow(2, (m - 69) / 12); }
|
||||
function zoneForDepth() {
|
||||
const cx = Math.max(0, Math.min(W - 1, Math.floor(bot.x)));
|
||||
const y = bot.y;
|
||||
if (y < surfY[cx] + 12) return 0;
|
||||
const u = depthU(y);
|
||||
if (u < 0.3) return 1;
|
||||
if (y < graniteY[cx]) return 2;
|
||||
if (u < 0.86) return 3;
|
||||
return 4;
|
||||
}
|
||||
function scaleNote(z, deg, octave) {
|
||||
const n = z.scale.length;
|
||||
const o = Math.floor(deg / n), d = ((deg % n) + n) % n;
|
||||
return z.root + z.scale[d] + 12 * (o + octave) + z.oct;
|
||||
}
|
||||
function musicScheduler() {
|
||||
const ctx = AU.ctx;
|
||||
if (!ctx || !AU.started || SET.volMusic <= 0 || (window.GAME && !GAME.running)) { if (ctx) AU.nextNote = Math.max(AU.nextNote, ctx.currentTime + 0.1); return; }
|
||||
const z = ZONES[AU.zone];
|
||||
const spb = 60 / z.tempo / 4; // seconds per 16th
|
||||
while (AU.nextNote < ctx.currentTime + 0.25) {
|
||||
const s = AU.step % 16, t = AU.nextNote;
|
||||
if (s === 0) {
|
||||
// new bar: maybe change zone, advance chord
|
||||
const nz = W ? zoneForDepth() : 0;
|
||||
if (nz !== AU.zone) { AU.zone = nz; }
|
||||
AU.chordIdx = (AU.chordIdx + 1) % 4;
|
||||
if (Math.random() < ZONES[AU.zone].lead * 0.5 && !AU.melody) {
|
||||
const len = 4 + ((Math.random() * 5) | 0), mel = [];
|
||||
let d = AU.lastDeg;
|
||||
for (let k = 0; k < len; k++) { d += [-2, -1, -1, 1, 1, 2, 0][(Math.random() * 7) | 0]; d = Math.max(-2, Math.min(9, d)); mel.push({ d, at: k * (Math.random() < 0.6 ? 2 : 4) }); }
|
||||
AU.melody = mel; AU.melPos = AU.step; AU.lastDeg = d;
|
||||
}
|
||||
}
|
||||
const zz = ZONES[AU.zone];
|
||||
const chordRoot = zz.prog[AU.chordIdx];
|
||||
// bass
|
||||
if (s === 0 || (s === 8 && Math.random() < 0.6)) {
|
||||
const deg = s === 8 && Math.random() < 0.5 ? chordRoot + 4 : chordRoot;
|
||||
tone(mtof(scaleNote(zz, deg, -1)), spb * (s === 0 ? 7 : 5), { wave: 'p12', vol: 0.07, bus: AU.music, release: 0.15, at: t });
|
||||
}
|
||||
// arpeggio
|
||||
if (s % 2 === 0 && Math.random() < zz.density) {
|
||||
const pat = [0, 2, 4, 2, 0, 4, 2, 4][(s >> 1) & 7] + (Math.random() < 0.15 ? 7 : 0);
|
||||
tone(mtof(scaleNote(zz, chordRoot + pat, 1)), spb * 1.6, { wave: 'triangle', vol: 0.05, bus: AU.music, release: 0.06, at: t });
|
||||
}
|
||||
// melody phrase
|
||||
if (AU.melody) {
|
||||
const rel = AU.step - AU.melPos;
|
||||
for (const n of AU.melody) if (n.at === rel) tone(mtof(scaleNote(zz, n.d, 1)), spb * 2.6, { wave: 'p25', vol: 0.035, bus: AU.music, vib: true, attack: 0.02, release: 0.1, at: t });
|
||||
if (rel > 40) AU.melody = null;
|
||||
}
|
||||
// hats
|
||||
if ((s % 4 === 2) && Math.random() < zz.hat) noiseBurst(0.03, { type: 'highpass', freq: 6000, vol: 0.025, at: t, bus: AU.music });
|
||||
if (s === 0 && AU.zone <= 1 && Math.random() < 0.7) tone(110, 0.12, { wave: 'triangle', to: 50, vol: 0.06, at: t, bus: AU.music });
|
||||
if (AU.zone >= 3 && s === 0 && AU.chordIdx % 2 === 0) tone(mtof(scaleNote(zz, chordRoot, -2)), spb * 30, { wave: 'p12', vol: 0.03, at: t, bus: AU.music, attack: 0.6, release: 1.2 });
|
||||
AU.nextNote += spb;
|
||||
AU.step++;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user