feat: build a bounded browser Web Audio engine and sound controls
This commit is contained in:
+32
-3
@@ -13,8 +13,14 @@
|
||||
result: el('result-description'), error: el('error-description'), lobbyIcon: el('lobby-icon'), resultIcon: el('result-icon'),
|
||||
cue: el('action-cue'), cueIcon: el('action-cue-icon'), cueText: el('action-cue-text'), effects: el('game-effects'),
|
||||
effectIcon: el('effect-icon'), effectText: el('effect-text'), effectBurst: el('effect-burst'), effectBadge: el('effect-badge'),
|
||||
recoveryButton: el('recovery-menu-button'), recoveryDialog: el('recovery-dialog'), recoveryClose: el('recovery-close'), recoveryChoices: el('recovery-choices'), recoveryGlobal: el('recovery-global-choice'), recoveryConfirmation: el('recovery-confirmation'), recoveryCancel: el('recovery-cancel'), recoveryConfirm: el('recovery-confirm'), recoveryConfirmTitle: el('recovery-confirm-title'), recoveryConfirmText: el('recovery-confirm-text'), recoveryConfirmLabel: el('recovery-confirm-label'), recoveryHoldNote: el('recovery-hold-note'), recoveryScopeIcon: el('recovery-scope-icon'), recoveryLive: el('recovery-live')
|
||||
recoveryButton: el('recovery-menu-button'), recoveryDialog: el('recovery-dialog'), recoveryClose: el('recovery-close'), recoveryChoices: el('recovery-choices'), recoveryGlobal: el('recovery-global-choice'), recoveryConfirmation: el('recovery-confirmation'), recoveryCancel: el('recovery-cancel'), recoveryConfirm: el('recovery-confirm'), recoveryConfirmTitle: el('recovery-confirm-title'), recoveryConfirmText: el('recovery-confirm-text'), recoveryConfirmLabel: el('recovery-confirm-label'), recoveryHoldNote: el('recovery-hold-note'), recoveryScopeIcon: el('recovery-scope-icon'), recoveryLive: el('recovery-live'),
|
||||
soundToggle: el('sound-toggle'), soundToggleLabel: el('sound-toggle-label'), soundSettings: el('sound-settings'), soundVolume: el('sound-volume'), soundReduced: el('sound-reduced'), recoverySoundMute: el('recovery-sound-mute')
|
||||
};
|
||||
const silentAudio = {
|
||||
getPreferences: () => ({ enabled: false, volume: 'normal', reduced: false }),
|
||||
setEnabled: async () => false, setVolume: () => {}, setReduced: () => {}, play: () => false, setPageHidden: () => {}, cleanup: () => {}
|
||||
};
|
||||
const audio = globalThis.BattleshipAudio?.createAudioEngine?.() || silentAudio;
|
||||
let token = localStorage.getItem(storage.token) || '';
|
||||
let role = '';
|
||||
let info;
|
||||
@@ -212,9 +218,10 @@
|
||||
clearRecoveryTimers();
|
||||
if (socket) { socket.onclose = null; socket.close(); socket = undefined; }
|
||||
selectedTarget = undefined; firePending = false; cumulativeStatistics = undefined; statisticsGameId = undefined;
|
||||
activeBoard = 'own'; hitStreak = 0; state = undefined; role = ''; info = undefined; targetActivator = undefined;
|
||||
activeBoard = 'own'; hitStreak = 0; state = undefined; role = ''; info = undefined; targetActivator = undefined; audio.cleanup();
|
||||
document.body.classList.remove('fx-hit', 'fx-damage'); ui.effects.hidden = true; notify('');
|
||||
Object.keys(localStorage).filter(key => key.startsWith('battleship.')).forEach(key => localStorage.removeItem(key));
|
||||
const soundKeys = new Set(['battleship.soundEnabled', 'battleship.soundVolume', 'battleship.soundReduced']);
|
||||
Object.keys(localStorage).filter(key => key.startsWith('battleship.') && !soundKeys.has(key)).forEach(key => localStorage.removeItem(key));
|
||||
if (!profile) { const rememberedName = ui.name.value.trim(); if (rememberedName) localStorage.setItem(storage.name, rememberedName); }
|
||||
token = ''; ui.name.value = profile ? '' : (localStorage.getItem(storage.name) || '');
|
||||
closeRecovery(); showScreen('connect'); setConnection('Готово к подключению');
|
||||
@@ -271,6 +278,16 @@
|
||||
ui.notice.classList.toggle('error', error);
|
||||
}
|
||||
|
||||
function syncSoundControls() {
|
||||
const preferences = audio.getPreferences();
|
||||
ui.soundToggle.setAttribute('aria-pressed', String(preferences.enabled));
|
||||
ui.soundToggleLabel.textContent = preferences.enabled ? 'Звук включён' : 'Звук выключен';
|
||||
ui.soundSettings.hidden = !preferences.enabled;
|
||||
ui.recoverySoundMute.hidden = !preferences.enabled;
|
||||
ui.soundVolume.value = preferences.volume;
|
||||
ui.soundReduced.checked = preferences.reduced;
|
||||
}
|
||||
|
||||
function apiError(payload, fallback) {
|
||||
return payload?.message || fallback || 'Сервер временно недоступен.';
|
||||
}
|
||||
@@ -696,6 +713,15 @@
|
||||
ui.rematch.addEventListener('click', () => post('/api/game/rematch', { token, gameId: state.gameId }));
|
||||
el('retry-button').addEventListener('click', () => { refreshInfo(); pollState(); connectSocket(); });
|
||||
ui.recoveryButton.addEventListener('click', openRecovery);
|
||||
ui.soundToggle.addEventListener('click', async () => {
|
||||
const nextEnabled = !audio.getPreferences().enabled;
|
||||
await audio.setEnabled(nextEnabled);
|
||||
if (nextEnabled) audio.play('test');
|
||||
syncSoundControls();
|
||||
});
|
||||
ui.soundVolume.addEventListener('change', () => { audio.setVolume(ui.soundVolume.value); syncSoundControls(); });
|
||||
ui.soundReduced.addEventListener('change', () => { audio.setReduced(ui.soundReduced.checked); syncSoundControls(); });
|
||||
ui.recoverySoundMute.addEventListener('click', async () => { await audio.setEnabled(false); syncSoundControls(); });
|
||||
ui.recoveryClose.addEventListener('click', closeRecovery);
|
||||
ui.recoveryCancel.addEventListener('click', () => { recoveryAction = undefined; ui.recoveryConfirmation.hidden = true; ui.recoveryChoices.hidden = false; ui.recoveryClose.focus(); });
|
||||
ui.recoveryChoices.addEventListener('click', event => {
|
||||
@@ -720,8 +746,11 @@
|
||||
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
||||
}
|
||||
});
|
||||
document.addEventListener('visibilitychange', () => { audio.setPageHidden(document.hidden); });
|
||||
window.addEventListener('pagehide', () => audio.cleanup(), { once: true });
|
||||
|
||||
async function boot() {
|
||||
syncSoundControls();
|
||||
ui.name.value = localStorage.getItem(storage.name) || '';
|
||||
document.querySelectorAll('.avatar-button').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.name === ui.name.value)));
|
||||
setConnection('Проверяем ESP32…');
|
||||
|
||||
Reference in New Issue
Block a user