Vanilla JS + Canvas 2D, zero dependencies. Gameplay: campaign waves vs 12 dino species + 3 bosses, arena mode, 7 weapons, hero talents, pets, ult/bombs, card roguelite picks. World: day/night cycle, weather fronts (rain+lightning), Blood Moon weekends, 3 act biomes (jungle -> swamp -> ashlands with lava veins). Meta: gems economy, upgrade tree, quests, bestiary, achievements, shop (weapons/heroes/skins incl. premium), revive, ad-cap system, endless scaling past wave 25, live-painted shop/bestiary icons, pro main menu, auto-aim toggle (F). Headless smoke suite included.
233 lines
9.0 KiB
JavaScript
233 lines
9.0 KiB
JavaScript
/* Neon Rampage — synthesized audio (WebAudio). All sounds generated at runtime. */
|
|
(function () {
|
|
'use strict';
|
|
|
|
var US = (window.US = window.US || {});
|
|
var ctx = null;
|
|
var master = null;
|
|
var sfxGain = null;
|
|
var musicGain = null;
|
|
var musicTimer = null;
|
|
var step = 0;
|
|
var windSrc = null; // { src, lfo }
|
|
|
|
function ensure() {
|
|
if (ctx) return true;
|
|
try {
|
|
var AC = window.AudioContext || window.webkitAudioContext;
|
|
if (!AC) return false;
|
|
ctx = new AC();
|
|
master = ctx.createGain();
|
|
master.gain.value = 0.9;
|
|
master.connect(ctx.destination);
|
|
sfxGain = ctx.createGain();
|
|
sfxGain.gain.value = US.save.get().settings.sfx;
|
|
sfxGain.connect(master);
|
|
musicGain = ctx.createGain();
|
|
musicGain.gain.value = US.save.get().settings.music * 0.5;
|
|
musicGain.connect(master);
|
|
return true;
|
|
} catch (e) { return false; }
|
|
}
|
|
|
|
function resume() {
|
|
if (!ensure()) return;
|
|
if (ctx.state === 'suspended') ctx.resume();
|
|
}
|
|
|
|
// Generic one-shot: oscillator with pitch slide + optional noise burst.
|
|
function tone(opts) {
|
|
if (!ensure() || ctx.state === 'suspended') { if (!resume()) return; }
|
|
var t = ctx.currentTime;
|
|
var o = ctx.createOscillator();
|
|
var g = ctx.createGain();
|
|
o.type = opts.type || 'square';
|
|
o.frequency.setValueAtTime(opts.f0 || 440, t);
|
|
if (opts.f1) o.frequency.exponentialRampToValueAtTime(Math.max(20, opts.f1), t + (opts.dur || 0.15));
|
|
g.gain.setValueAtTime(0.0001, t);
|
|
g.gain.exponentialRampToValueAtTime(opts.vol || 0.2, t + 0.008);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + (opts.dur || 0.15));
|
|
o.connect(g); g.connect(sfxGain);
|
|
o.start(t); o.stop(t + (opts.dur || 0.15) + 0.05);
|
|
|
|
if (opts.noise) {
|
|
var len = Math.floor(ctx.sampleRate * (opts.noiseDur || 0.12));
|
|
var buf = ctx.createBuffer(1, len, ctx.sampleRate);
|
|
var d = buf.getChannelData(0);
|
|
for (var i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / len);
|
|
var src = ctx.createBufferSource();
|
|
src.buffer = buf;
|
|
var ng = ctx.createGain();
|
|
ng.gain.value = opts.noiseVol || 0.25;
|
|
src.connect(ng); ng.connect(sfxGain);
|
|
src.start(t);
|
|
}
|
|
}
|
|
|
|
var SFX = {
|
|
shoot: function () { tone({ type: 'square', f0: 720, f1: 160, dur: 0.07, vol: 0.10 }); },
|
|
shotgun: function () { tone({ type: 'sawtooth', f0: 300, f1: 90, dur: 0.16, vol: 0.18, noise: true, noiseVol: 0.2 }); },
|
|
rail: function () { tone({ type: 'sawtooth', f0: 1400, f1: 200, dur: 0.22, vol: 0.14 }); },
|
|
rocket: function () { tone({ type: 'triangle', f0: 220, f1: 60, dur: 0.3, vol: 0.2, noise: true }); },
|
|
smg: function () { tone({ type: 'square', f0: 520, f1: 140, dur: 0.05, vol: 0.07 }); },
|
|
hit: function () { tone({ type: 'triangle', f0: 300, f1: 120, dur: 0.05, vol: 0.08 }); },
|
|
kill: function () { tone({ type: 'square', f0: 260, f1: 520, dur: 0.09, vol: 0.10 }); },
|
|
explode: function () { tone({ type: 'sawtooth', f0: 130, f1: 40, dur: 0.4, vol: 0.28, noise: true, noiseVol: 0.35, noiseDur: 0.3 }); },
|
|
hurt: function () { tone({ type: 'sawtooth', f0: 180, f1: 70, dur: 0.18, vol: 0.22 }); },
|
|
pickup: function () { tone({ type: 'sine', f0: 620, f1: 1240, dur: 0.09, vol: 0.12 }); },
|
|
heal: function () { tone({ type: 'sine', f0: 440, f1: 880, dur: 0.18, vol: 0.14 }); },
|
|
card: function () { tone({ type: 'sine', f0: 500, f1: 1000, dur: 0.14, vol: 0.13 }); },
|
|
onfire: function () {
|
|
[523, 659, 784, 1047].forEach(function (f, i) {
|
|
setTimeout(function () { tone({ type: 'square', f0: f, dur: 0.12, vol: 0.16 }); }, i * 70);
|
|
});
|
|
},
|
|
ult: function () { tone({ type: 'sawtooth', f0: 80, f1: 900, dur: 0.5, vol: 0.3, noise: true, noiseVol: 0.3 }); },
|
|
bomb: function () { tone({ type: 'sawtooth', f0: 100, f1: 30, dur: 0.6, vol: 0.32, noise: true, noiseVol: 0.45, noiseDur: 0.5 }); },
|
|
bossRoar:function () { tone({ type: 'sawtooth', f0: 90, f1: 45, dur: 0.7, vol: 0.3, noise: true, noiseVol: 0.2 }); },
|
|
stomp: function (vol) {
|
|
var v = vol || 0.18;
|
|
tone({ type: 'sine', f0: 85, f1: 26, dur: 0.17, vol: v });
|
|
tone({ type: 'triangle', f0: 55, f1: 22, dur: 0.24, vol: v * 0.7 });
|
|
},
|
|
ui: function () { tone({ type: 'sine', f0: 700, f1: 900, dur: 0.05, vol: 0.08 }); },
|
|
buy: function () { [660, 880].forEach(function (f, i) { setTimeout(function () { tone({ type: 'sine', f0: f, dur: 0.1, vol: 0.12 }); }, i * 90); }); },
|
|
lose: function () { [392, 330, 262, 196].forEach(function (f, i) { setTimeout(function () { tone({ type: 'triangle', f0: f, dur: 0.22, vol: 0.16 }); }, i * 160); }); },
|
|
win: function () { [523, 659, 784, 1047, 1319].forEach(function (f, i) { setTimeout(function () { tone({ type: 'square', f0: f, dur: 0.15, vol: 0.14 }); }, i * 110); }); }
|
|
};
|
|
|
|
/* --- tiny procedural music loop: dark synthwave bass + hats --- */
|
|
var BASS = [0, 0, 7, 0, 5, 5, 3, 7]; // semitone offsets from A1
|
|
function note(semi) { return 55 * Math.pow(2, semi / 12); }
|
|
|
|
function musicStep() {
|
|
if (!ctx || !musicOn) return;
|
|
var t = ctx.currentTime;
|
|
// bass
|
|
var o = ctx.createOscillator();
|
|
var g = ctx.createGain();
|
|
o.type = 'sawtooth';
|
|
var root = BASS[Math.floor(step / 2) % BASS.length];
|
|
var playingBoss = US.game && US.game.isBossWave && US.game.isBossWave();
|
|
var mul = playingBoss ? Math.pow(2, 3 / 12) : 1;
|
|
o.frequency.value = note(root) * mul;
|
|
g.gain.setValueAtTime(0.0001, t);
|
|
g.gain.exponentialRampToValueAtTime(0.22, t + 0.02);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.24);
|
|
o.connect(g); g.connect(musicGain);
|
|
o.start(t); o.stop(t + 0.3);
|
|
// hat every other step
|
|
if (step % 2 === 1) {
|
|
var len = Math.floor(ctx.sampleRate * 0.03);
|
|
var buf = ctx.createBuffer(1, len, ctx.sampleRate);
|
|
var d = buf.getChannelData(0);
|
|
for (var i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / len);
|
|
var src = ctx.createBufferSource();
|
|
src.buffer = buf;
|
|
var hg = ctx.createGain(); hg.gain.value = 0.05;
|
|
src.connect(hg); hg.connect(musicGain);
|
|
src.start(t);
|
|
}
|
|
step++;
|
|
}
|
|
|
|
var musicOn = false;
|
|
|
|
/* --- wind ambience: filtered brown noise with slow gusts --- */
|
|
var ambGain = null;
|
|
|
|
function startWind() {
|
|
if (!ctx || windSrc || !musicOn) return;
|
|
var len = ctx.sampleRate * 2;
|
|
var buf = ctx.createBuffer(1, len, ctx.sampleRate);
|
|
var d = buf.getChannelData(0);
|
|
var lastV = 0;
|
|
for (var i = 0; i < len; i++) {
|
|
var wn = Math.random() * 2 - 1;
|
|
lastV = (lastV + 0.02 * wn) / 1.02; // brown-ish noise
|
|
d[i] = lastV * 3.5;
|
|
}
|
|
var src = ctx.createBufferSource();
|
|
src.buffer = buf;
|
|
src.loop = true;
|
|
var lp = ctx.createBiquadFilter();
|
|
lp.type = 'lowpass';
|
|
lp.frequency.value = 300;
|
|
lp.Q.value = 0.7;
|
|
// gust LFO breathing on the filter
|
|
var lfo = ctx.createOscillator();
|
|
lfo.frequency.value = 0.11;
|
|
var lfoG = ctx.createGain();
|
|
lfoG.gain.value = 150;
|
|
lfo.connect(lfoG);
|
|
lfoG.connect(lp.frequency);
|
|
ambGain = ctx.createGain();
|
|
ambGain.gain.value = 0;
|
|
src.connect(lp);
|
|
lp.connect(ambGain);
|
|
ambGain.connect(musicGain);
|
|
try {
|
|
ambGain.gain.linearRampToValueAtTime(0.5, ctx.currentTime + 2.5);
|
|
src.start();
|
|
lfo.start();
|
|
windSrc = { src: src, lfo: lfo };
|
|
} catch (e) { /* autoplay quirks */ }
|
|
}
|
|
|
|
function stopWind() {
|
|
if (!windSrc) return;
|
|
try {
|
|
ambGain.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.4);
|
|
var w = windSrc;
|
|
setTimeout(function () {
|
|
try { w.src.stop(); w.lfo.stop(); } catch (e) {}
|
|
}, 450);
|
|
} catch (e) {}
|
|
windSrc = null;
|
|
}
|
|
|
|
US.audio = {
|
|
resume: resume,
|
|
sfx: function (name) {
|
|
if (!ensure()) return;
|
|
sfxGain.gain.value = US.save.get().settings.sfx;
|
|
if (SFX[name]) SFX[name].apply(null, Array.prototype.slice.call(arguments, 1));
|
|
},
|
|
setVolumes: function () {
|
|
if (!ctx) return;
|
|
sfxGain.gain.value = US.save.get().settings.sfx;
|
|
musicGain.gain.value = US.save.get().settings.music * 0.5;
|
|
},
|
|
startMusic: function () {
|
|
if (!ensure()) return;
|
|
resume();
|
|
if (!musicTimer) {
|
|
musicOn = true;
|
|
musicStep();
|
|
musicTimer = setInterval(musicStep, 250);
|
|
}
|
|
startWind();
|
|
},
|
|
stopMusic: function () {
|
|
musicOn = false;
|
|
if (musicTimer) { clearInterval(musicTimer); musicTimer = null; }
|
|
stopWind();
|
|
},
|
|
hoverTick: function () {
|
|
if (!ensure()) return;
|
|
tone({ type: 'sine', f0: 900, f1: 1100, dur: 0.035, vol: 0.05 });
|
|
},
|
|
thunder: function () {
|
|
if (!ensure()) return;
|
|
tone({ type: 'sawtooth', f0: 52, f1: 22, dur: 1.2, vol: 0.3, noise: true, noiseVol: 0.55, noiseDur: 1.3 });
|
|
},
|
|
distantRoar: function () {
|
|
if (!ensure()) return;
|
|
tone({ type: 'sawtooth', f0: 120, f1: 36, dur: 1.5, vol: 0.09 });
|
|
setTimeout(function () {
|
|
tone({ type: 'triangle', f0: 70, f1: 28, dur: 1.9, vol: 0.07 });
|
|
}, 180);
|
|
}
|
|
};
|
|
})();
|