feat: Initial commit Plants vs Zombies Web Edition v2.4
- 5 Biomes: Day, Night, Pool, Fog, Roof (25 Campaign levels) - Minigames: Wall-nut Bowling, I Zombie, Roguelike, Boss Rush & Dr. Zomboss - Systems: Zen Garden, Workshop Crafting, Almanac, Tree of Wisdom, Achievements - Backend: REST API + SQLite Accounts & Save Synchronization - Infrastructure: Nginx HTTPS & Docker deployment
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
// Web Audio API Procedural Sound Synthesizer and Asset Helpers
|
||||
window.PvZAudio = (function() {
|
||||
let ctx = null;
|
||||
let muted = false;
|
||||
|
||||
let musicInterval = null;
|
||||
let musicPlaying = false;
|
||||
let musicStep = 0;
|
||||
|
||||
const N = {
|
||||
D2: 73.42, E2: 82.41, F2: 87.31, G2: 98.00, A2: 110.00, Bb2: 116.54, B2: 123.47,
|
||||
C3: 130.81, D3: 146.83, Eb3: 155.56, E3: 164.81, F3: 174.61, G3: 196.00, A3: 220.00, Bb3: 233.08, B3: 246.94,
|
||||
C4: 261.63, Csharp4: 277.18, D4: 293.66, Eb4: 311.13, E4: 329.63, F4: 349.23, Fsharp4: 369.99, G4: 392.00, Gsharp4: 415.30, A4: 440.00, Bb4: 466.16, B4: 493.88,
|
||||
C5: 523.25, Csharp5: 554.37, D5: 587.33, Eb5: 622.25, E5: 659.25, F5: 698.46, Fsharp5: 739.99, G5: 783.99, A5: 880.00, Bb5: 932.33, B5: 987.77,
|
||||
C6: 1046.50
|
||||
};
|
||||
|
||||
let currentThemeName = 'day';
|
||||
|
||||
const THEMES = {
|
||||
day: {
|
||||
tempo: 230,
|
||||
bassType: 'triangle',
|
||||
leadType: 'sine',
|
||||
bassVol: 0.10,
|
||||
leadVol: 0.08,
|
||||
bass: [
|
||||
N.A2, null, N.E3, null, N.A2, null, N.E3, null,
|
||||
N.D3, null, N.A3, null, N.E3, null, N.G3, null
|
||||
],
|
||||
lead: [
|
||||
N.A4, null, N.C5, N.E5, null, N.D5, N.C5, N.B4,
|
||||
N.A4, null, N.C5, N.A4, null, N.B4, N.C5, N.B4
|
||||
]
|
||||
},
|
||||
night: {
|
||||
tempo: 280,
|
||||
bassType: 'sine',
|
||||
leadType: 'triangle',
|
||||
bassVol: 0.12,
|
||||
leadVol: 0.07,
|
||||
bass: [
|
||||
N.C3, null, N.G3, null, N.A2, null, N.E3, null,
|
||||
N.F3, null, N.C4, null, N.G3, null, N.B3, null
|
||||
],
|
||||
lead: [
|
||||
N.C5, null, N.Eb5, N.G5, null, N.F5, null, N.D5,
|
||||
N.Eb5, null, N.C5, null, N.G4, null, N.Bb4, null
|
||||
]
|
||||
},
|
||||
pool: {
|
||||
tempo: 210,
|
||||
bassType: 'triangle',
|
||||
leadType: 'sine',
|
||||
bassVol: 0.11,
|
||||
leadVol: 0.09,
|
||||
bass: [
|
||||
N.G2, null, N.D3, N.G3, N.C3, null, N.G3, N.C4,
|
||||
N.D3, null, N.A3, N.D4, N.G3, null, N.B3, null
|
||||
],
|
||||
lead: [
|
||||
N.B4, N.D5, null, N.G5, N.Fsharp5, N.D5, null, N.B4,
|
||||
N.C5, N.E5, null, N.A5, N.G5, N.E5, N.D5, null
|
||||
]
|
||||
},
|
||||
fog: {
|
||||
tempo: 310,
|
||||
bassType: 'sawtooth',
|
||||
leadType: 'triangle',
|
||||
bassVol: 0.07,
|
||||
leadVol: 0.07,
|
||||
bass: [
|
||||
N.D2, null, N.A2, null, N.D2, null, N.Bb2, null,
|
||||
N.A2, null, N.F2, null, N.G2, null, N.A2, null
|
||||
],
|
||||
lead: [
|
||||
N.D5, null, null, N.F5, null, N.E5, null, N.Csharp5,
|
||||
N.D5, null, N.A4, null, null, N.F5, N.E5, null
|
||||
]
|
||||
},
|
||||
roof: {
|
||||
tempo: 190,
|
||||
bassType: 'triangle',
|
||||
leadType: 'sawtooth',
|
||||
bassVol: 0.11,
|
||||
leadVol: 0.06,
|
||||
bass: [
|
||||
N.F3, N.F3, N.C4, N.F3, N.Bb3, N.Bb3, N.F4, N.Bb3,
|
||||
N.C3, N.C3, N.G3, N.C4, N.F3, null, N.C4, null
|
||||
],
|
||||
lead: [
|
||||
N.F4, N.A4, N.C5, null, N.D5, N.F5, null, N.C5,
|
||||
N.Bb4, N.D5, N.F5, null, N.A5, N.G5, N.F5, null
|
||||
]
|
||||
},
|
||||
boss: {
|
||||
tempo: 165,
|
||||
bassType: 'sawtooth',
|
||||
leadType: 'square',
|
||||
bassVol: 0.09,
|
||||
leadVol: 0.06,
|
||||
bass: [
|
||||
N.E2, N.E2, N.G2, N.E2, N.Bb2, N.A2, N.G2, N.E2,
|
||||
N.E2, N.E2, N.B2, N.Bb2, N.A2, N.G2, N.E2, N.D3
|
||||
],
|
||||
lead: [
|
||||
N.E5, N.E5, N.G5, null, N.Bb5, N.A5, N.G5, N.E5,
|
||||
null, N.B5, N.Bb5, N.A5, null, N.G5, N.F5, N.E5
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function initCtx() {
|
||||
if (!ctx) {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
if (AudioContext) {
|
||||
ctx = new AudioContext();
|
||||
}
|
||||
}
|
||||
if (ctx && ctx.state === 'suspended') {
|
||||
ctx.resume();
|
||||
}
|
||||
}
|
||||
|
||||
function playTone(freq, type, duration, startVol = 0.2, endVol = 0.001) {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
try {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = type;
|
||||
osc.frequency.setValueAtTime(freq, ctx.currentTime);
|
||||
gain.gain.setValueAtTime(startVol, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(endVol, ctx.currentTime + duration);
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + duration);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return {
|
||||
init: initCtx,
|
||||
toggleMute: function() {
|
||||
muted = !muted;
|
||||
return muted;
|
||||
},
|
||||
isMuted: function() {
|
||||
return muted;
|
||||
},
|
||||
playShoot: function() {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
try {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(600, ctx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(120, ctx.currentTime + 0.12);
|
||||
gain.gain.setValueAtTime(0.25, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.12);
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.12);
|
||||
} catch (e) {}
|
||||
},
|
||||
playHit: function() {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
try {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = 'square';
|
||||
osc.frequency.setValueAtTime(180, ctx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(60, ctx.currentTime + 0.08);
|
||||
gain.gain.setValueAtTime(0.2, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.08);
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.08);
|
||||
} catch (e) {}
|
||||
},
|
||||
playSun: function() {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
try {
|
||||
const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
|
||||
notes.forEach((freq, idx) => {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, ctx.currentTime + idx * 0.05);
|
||||
gain.gain.setValueAtTime(0.18, ctx.currentTime + idx * 0.05);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + idx * 0.05 + 0.2);
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start(ctx.currentTime + idx * 0.05);
|
||||
osc.stop(ctx.currentTime + idx * 0.05 + 0.2);
|
||||
});
|
||||
} catch (e) {}
|
||||
},
|
||||
playPlant: function() {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
try {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(220, ctx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.1);
|
||||
gain.gain.setValueAtTime(0.3, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15);
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.15);
|
||||
} catch (e) {}
|
||||
},
|
||||
playChomp: function() {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
try {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(140, ctx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(80, ctx.currentTime + 0.1);
|
||||
gain.gain.setValueAtTime(0.15, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.1);
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.1);
|
||||
} catch (e) {}
|
||||
},
|
||||
playExplosion: function() {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
try {
|
||||
// Noise buffer for explosion
|
||||
const bufferSize = ctx.sampleRate * 0.5;
|
||||
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
const noise = ctx.createBufferSource();
|
||||
noise.buffer = buffer;
|
||||
const filter = ctx.createBiquadFilter();
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.setValueAtTime(800, ctx.currentTime);
|
||||
filter.frequency.exponentialRampToValueAtTime(50, ctx.currentTime + 0.5);
|
||||
const gain = ctx.createGain();
|
||||
gain.gain.setValueAtTime(0.5, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);
|
||||
noise.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
noise.start();
|
||||
} catch (e) {}
|
||||
},
|
||||
playMower: function() {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
try {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(90, ctx.currentTime);
|
||||
osc.frequency.linearRampToValueAtTime(240, ctx.currentTime + 1.2);
|
||||
gain.gain.setValueAtTime(0.3, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 1.2);
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 1.2);
|
||||
} catch (e) {}
|
||||
},
|
||||
playCoin: function() {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(987.77, ctx.currentTime); // B5
|
||||
osc.frequency.setValueAtTime(1318.51, ctx.currentTime + 0.06); // E6
|
||||
gain.gain.setValueAtTime(0.2, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.3);
|
||||
},
|
||||
playSizzle: function() {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
try {
|
||||
const bufferSize = ctx.sampleRate * 0.8;
|
||||
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
||||
const data = buffer.getChannelData(0);
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
data[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
const noise = ctx.createBufferSource();
|
||||
noise.buffer = buffer;
|
||||
const filter = ctx.createBiquadFilter();
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.setValueAtTime(1400, ctx.currentTime);
|
||||
filter.frequency.exponentialRampToValueAtTime(300, ctx.currentTime + 0.8);
|
||||
const gain = ctx.createGain();
|
||||
gain.gain.setValueAtTime(0.4, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.8);
|
||||
noise.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
noise.start();
|
||||
} catch (e) {}
|
||||
},
|
||||
playMine: function() {
|
||||
this.playExplosion();
|
||||
playTone(550, 'sine', 0.15, 0.3, 0.01);
|
||||
},
|
||||
playVault: function() {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(180, ctx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(620, ctx.currentTime + 0.3);
|
||||
gain.gain.setValueAtTime(0.25, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3);
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.3);
|
||||
},
|
||||
playGulp: function() {
|
||||
playTone(180, 'sine', 0.18, 0.35, 0.01);
|
||||
setTimeout(() => playTone(120, 'sine', 0.22, 0.3, 0.01), 90);
|
||||
},
|
||||
playStrike: function() {
|
||||
playTone(240, 'square', 0.1, 0.3, 0.01);
|
||||
playTone(120, 'sawtooth', 0.2, 0.35, 0.01);
|
||||
},
|
||||
playHugeWave: function() {
|
||||
playTone(180, 'sawtooth', 0.6, 0.35, 0.01);
|
||||
},
|
||||
playWin: function() {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
const fanfare = [392, 523, 659, 784, 1046];
|
||||
fanfare.forEach((f, i) => {
|
||||
setTimeout(() => playTone(f, 'triangle', 0.35, 0.3, 0.01), i * 160);
|
||||
});
|
||||
},
|
||||
playLose: function() {
|
||||
if (muted) return;
|
||||
initCtx();
|
||||
if (!ctx) return;
|
||||
const doom = [400, 360, 320, 280, 220];
|
||||
doom.forEach((f, i) => {
|
||||
setTimeout(() => playTone(f, 'sawtooth', 0.4, 0.3, 0.01), i * 200);
|
||||
});
|
||||
},
|
||||
// Background Music (Multi-Biome Procedural Chiptune Synthesizer)
|
||||
startMusic: function(themeName = 'day') {
|
||||
const targetTheme = THEMES[themeName] ? themeName : 'day';
|
||||
|
||||
if (musicPlaying && currentThemeName === targetTheme && musicInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentThemeName = targetTheme;
|
||||
musicPlaying = true;
|
||||
initCtx();
|
||||
|
||||
if (musicInterval) {
|
||||
clearInterval(musicInterval);
|
||||
musicInterval = null;
|
||||
}
|
||||
|
||||
musicStep = 0;
|
||||
const theme = THEMES[currentThemeName];
|
||||
|
||||
musicInterval = setInterval(() => {
|
||||
if (!musicPlaying || muted) return;
|
||||
const b = theme.bass[musicStep % theme.bass.length];
|
||||
const l = theme.lead[musicStep % theme.lead.length];
|
||||
if (b) {
|
||||
playTone(b, theme.bassType, (theme.tempo / 1000) * 1.05, theme.bassVol, 0.001);
|
||||
}
|
||||
if (l) {
|
||||
playTone(l, theme.leadType, (theme.tempo / 1000) * 0.95, theme.leadVol, 0.001);
|
||||
}
|
||||
musicStep++;
|
||||
}, theme.tempo);
|
||||
},
|
||||
setTheme: function(themeName) {
|
||||
if (musicPlaying) {
|
||||
this.startMusic(themeName);
|
||||
} else {
|
||||
currentThemeName = THEMES[themeName] ? themeName : 'day';
|
||||
}
|
||||
},
|
||||
getTheme: function() {
|
||||
return currentThemeName;
|
||||
},
|
||||
stopMusic: function() {
|
||||
musicPlaying = false;
|
||||
if (musicInterval) {
|
||||
clearInterval(musicInterval);
|
||||
musicInterval = null;
|
||||
}
|
||||
},
|
||||
toggleMusic: function() {
|
||||
if (musicPlaying) {
|
||||
this.stopMusic();
|
||||
return false;
|
||||
} else {
|
||||
this.startMusic(currentThemeName);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
isMusicPlaying: function() {
|
||||
return musicPlaying;
|
||||
}
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user