Клиент: audio.js синтезирует эффекты без внешних файлов (выстрел, взрыв, разрушение кирпича, победа/поражение) — вписывается в "чистый HTML/CSS/JS" стек. AudioContext разблокируется по клику "готов" (браузеры блокируют автовоспроизведение без жеста). Сервер: 3 варианта раскладки coop-карты (случайный выбор при старте матча) вместо одной фиксированной. Fix: сервер разрушал кирпичи в своей копии карты, но никогда не сообщал об этом клиенту — стена оставалась видимой навсегда, хотя сквозь неё уже можно было стрелять. Теперь game.state включает destroyed_tiles, клиент убирает соответствующий спрайт стены.
77 lines
2.4 KiB
JavaScript
77 lines
2.4 KiB
JavaScript
// Простой синтез звука через Web Audio API — без внешних аудиофайлов.
|
|
let ctx = null;
|
|
|
|
function ensureCtx() {
|
|
if (!ctx) {
|
|
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
ctx = new AudioCtx();
|
|
}
|
|
if (ctx.state === 'suspended') {
|
|
ctx.resume();
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
// Вызывать из обработчика реального пользовательского клика/нажатия —
|
|
// браузеры блокируют автозапуск звука без явного жеста.
|
|
export function unlockAudio() {
|
|
ensureCtx();
|
|
}
|
|
|
|
function beep({ freq, duration = 0.08, type = 'square', gain = 0.1, freqEnd = null }) {
|
|
const c = ensureCtx();
|
|
const osc = c.createOscillator();
|
|
const g = c.createGain();
|
|
osc.type = type;
|
|
osc.frequency.setValueAtTime(freq, c.currentTime);
|
|
if (freqEnd !== null) {
|
|
osc.frequency.linearRampToValueAtTime(freqEnd, c.currentTime + duration);
|
|
}
|
|
g.gain.setValueAtTime(gain, c.currentTime);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, c.currentTime + duration);
|
|
osc.connect(g).connect(c.destination);
|
|
osc.start();
|
|
osc.stop(c.currentTime + duration);
|
|
}
|
|
|
|
function noiseBurst({ duration = 0.2, gain = 0.18 } = {}) {
|
|
const c = ensureCtx();
|
|
const size = Math.max(1, Math.floor(c.sampleRate * duration));
|
|
const buffer = c.createBuffer(1, size, c.sampleRate);
|
|
const data = buffer.getChannelData(0);
|
|
for (let i = 0; i < size; i++) {
|
|
data[i] = (Math.random() * 2 - 1) * (1 - i / size);
|
|
}
|
|
const source = c.createBufferSource();
|
|
source.buffer = buffer;
|
|
const g = c.createGain();
|
|
g.gain.setValueAtTime(gain, c.currentTime);
|
|
source.connect(g).connect(c.destination);
|
|
source.start();
|
|
}
|
|
|
|
export function playShot() {
|
|
beep({ freq: 660, freqEnd: 220, duration: 0.06, type: 'square', gain: 0.08 });
|
|
}
|
|
|
|
export function playWallHit() {
|
|
noiseBurst({ duration: 0.08, gain: 0.1 });
|
|
}
|
|
|
|
export function playExplosion() {
|
|
noiseBurst({ duration: 0.25, gain: 0.18 });
|
|
beep({ freq: 120, freqEnd: 40, duration: 0.25, type: 'sawtooth', gain: 0.1 });
|
|
}
|
|
|
|
export function playWin() {
|
|
[523, 659, 784, 1047].forEach((freq, i) =>
|
|
setTimeout(() => beep({ freq, duration: 0.15, type: 'square', gain: 0.1 }), i * 110)
|
|
);
|
|
}
|
|
|
|
export function playLose() {
|
|
[392, 330, 262, 196].forEach((freq, i) =>
|
|
setTimeout(() => beep({ freq, duration: 0.2, type: 'sawtooth', gain: 0.1 }), i * 130)
|
|
);
|
|
}
|