feat: create a large randomized library of playful game sounds and audio combos

This commit is contained in:
2026-08-31 23:48:03 +03:00
parent 17ccc0cb0f
commit c6544716e7
10 changed files with 175 additions and 21 deletions
+11 -8
View File
@@ -21,6 +21,7 @@
setEnabled: async () => false, setVolume: () => {}, setReduced: () => {}, play: () => false, setPageHidden: () => {}, cleanup: () => {}
};
const audio = globalThis.BattleshipAudio?.createAudioEngine?.() || silentAudio;
const sounds = globalThis.BattleshipSounds?.createSoundDirector?.({ engine: audio }) || { play: () => false, reset: () => {} };
let token = localStorage.getItem(storage.token) || '';
let role = '';
let info;
@@ -176,7 +177,7 @@
}
}
function showFeedback(event) {
function showFeedback(event, version, summary = false) {
const content = pickReaction(event?.type);
if (!content) return;
let { motion, burst } = content;
@@ -195,6 +196,7 @@
void document.body.offsetWidth;
document.body.classList.add(kind === 'damage' ? 'fx-damage' : 'fx-hit');
if (navigator.vibrate) navigator.vibrate(vibration);
sounds.play(event.type, { version, combo: hitStreak, reduced: audio.getPreferences().reduced, summary });
effectTimer = window.setTimeout(() => {
ui.effects.hidden = true; document.body.classList.remove('fx-hit', 'fx-damage');
}, kind === 'victory' ? 2200 : combo ? 1800 : 1350);
@@ -218,7 +220,7 @@
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; audio.cleanup();
activeBoard = 'own'; hitStreak = 0; state = undefined; role = ''; info = undefined; targetActivator = undefined; sounds.reset(); audio.cleanup();
document.body.classList.remove('fx-hit', 'fx-damage'); ui.effects.hidden = true; notify('');
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));
@@ -246,7 +248,7 @@
}
function openRecovery() {
if (!token) return; recoveryOpener = document.activeElement; ui.recoveryDialog.hidden = false; ui.recoveryButton.setAttribute('aria-expanded', 'true');
if (!token) return; sounds.play('recovery-open', { reduced: audio.getPreferences().reduced }); recoveryOpener = document.activeElement; ui.recoveryDialog.hidden = false; ui.recoveryButton.setAttribute('aria-expanded', 'true');
ui.recoveryChoices.hidden = false; ui.recoveryConfirmation.hidden = true; ui.recoveryGlobal.hidden = !isPlayer(); ui.recoveryClose.focus();
}
@@ -263,7 +265,7 @@
if (action === 'leave') await request('/api/session/leave', { method: 'POST', headers: headers(), body: JSON.stringify({ token, gameId: state?.gameId || 0 }) });
else if (action === 'profile') await request('/api/session/profile-reset', { method: 'POST', headers: headers(), body: JSON.stringify({ token, gameId: state?.gameId || 0 }) });
else await request('/api/game/reset', { method: 'POST', headers: headers(), body: JSON.stringify({ token, gameId: state?.gameId || 0 }) });
clearLocalProfile(action !== 'leave');
sounds.play(`recovery-${action}`, { reduced: audio.getPreferences().reduced }); clearLocalProfile(action !== 'leave');
} catch (error) { recoveryPending = false; ui.recoveryConfirm.disabled = false; ui.recoveryLive.textContent = 'Не получилось. Можно повторить или отменить.'; notify(error.message, true); }
}
@@ -373,7 +375,7 @@
selectedTarget = selectedTarget && payload.boards[opponentIndex()][selectedTarget.y * 10 + selectedTarget.x] === '0' ? selectedTarget : undefined;
document.title = `Морской бой — версия ${payload.version}`;
render();
showFeedback(globalThis.BattleshipTargetInteraction.detectFeedback(previousState, payload, role));
showFeedback(globalThis.BattleshipTargetInteraction.detectFeedback(previousState, payload, role), payload.version, hadGap);
}
function isPlayer() { return role === 'player1' || role === 'player2'; }
@@ -401,6 +403,7 @@
renderGame();
try {
await post('/api/game/shot', { token, gameId: state.gameId, ...target });
sounds.play('shot', { reduced: audio.getPreferences().reduced });
return true;
} catch (_) {
return false;
@@ -414,7 +417,7 @@
if (!targetActivator) {
targetActivator = globalThis.BattleshipTargetInteraction.createTargetActivator({
canFire: canFireTarget,
select: nextTarget => { selectedTarget = nextTarget; renderGame(); },
select: nextTarget => { selectedTarget = nextTarget; sounds.play('select', { reduced: audio.getPreferences().reduced }); renderGame(); },
fire: fireTarget,
});
}
@@ -710,7 +713,7 @@
ui.fire.addEventListener('click', () => { if (selectedTarget) fireTarget(selectedTarget); });
ui.cancel.addEventListener('click', () => { selectedTarget = undefined; renderGame(); });
ui.abort.addEventListener('click', () => post('/api/game/abort', { token, gameId: state.gameId }));
ui.rematch.addEventListener('click', () => post('/api/game/rematch', { token, gameId: state.gameId }));
ui.rematch.addEventListener('click', () => { post('/api/game/rematch', { token, gameId: state.gameId }).then(() => sounds.play('rematch', { reduced: audio.getPreferences().reduced })).catch(() => {}); });
el('retry-button').addEventListener('click', () => { refreshInfo(); pollState(); connectSocket(); });
ui.recoveryButton.addEventListener('click', openRecovery);
ui.soundToggle.addEventListener('click', async () => {
@@ -723,7 +726,7 @@
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.recoveryCancel.addEventListener('click', () => { sounds.play('recovery-cancel', { reduced: audio.getPreferences().reduced }); recoveryAction = undefined; ui.recoveryConfirmation.hidden = true; ui.recoveryChoices.hidden = false; ui.recoveryClose.focus(); });
ui.recoveryChoices.addEventListener('click', event => {
const choice = event.target.closest('[data-recovery-action]');
if (choice && !choice.hidden) showRecoveryConfirmation(choice.dataset.recoveryAction);