Files
battleship/data/game_sounds.js
T

56 lines
4.4 KiB
JavaScript

(() => {
const COUNTS = Object.freeze({ select: 4, shot: 8, miss: 10, hit: 10, sunk: 8, dodged: 6, damage: 6, turn: 6, waiting: 4, start: 6, victory: 8, defeat: 5, recovery: 5 });
const PRIORITY = Object.freeze({ waiting: 0, select: 0, recovery: 1, shot: 2, turn: 3, miss: 4, dodged: 4, damage: 5, hit: 5, sunk: 6, start: 6, defeat: 7, victory: 8, summary: 8 });
const BASE = Object.freeze({ select: [540, 100], shot: [190, 220], miss: [410, 260], hit: [155, 280], sunk: [120, 720], dodged: [460, 190], damage: [135, 260], turn: [620, 160], waiting: [350, 120], start: [250, 460], victory: [330, 850], defeat: [310, 430], recovery: [480, 150] });
function presetsFor(family, count) {
const [frequency, durationMs] = BASE[family];
return Array.from({ length: count }, (_, index) => ({
id: `${family}-${index + 1}`, family, priority: PRIORITY[family], frequency: Math.max(100, frequency + ((index * 67) % 240) - 90),
endFrequency: Math.max(100, frequency + ((index * 43) % 180) - 70), durationMs: Math.min(900, durationMs + (index % 4) * 35),
filterHz: 900 + (index * 347) % 2600, gain: family === 'select' || family === 'waiting' ? 0.18 : family === 'recovery' ? 0.14 : 0.7,
wave: ['sine', 'triangle', 'square', 'sawtooth'][index % 4], noise: ['miss', 'hit', 'sunk', 'damage', 'dodged'].includes(family) && index % 3 === 1,
pan: ((index % 5) - 2) * 0.18, delayMs: index % 3 === 2 ? 28 : 0
}));
}
const CATALOG = Object.freeze(Object.fromEntries(Object.entries(COUNTS).map(([family, count]) => [family, presetsFor(family, count)])));
const EVENT_FAMILY = Object.freeze({ 'watch-miss': 'miss', 'watch-hit': 'hit', 'watch-sunk': 'sunk', 'sunk-damage': 'damage', finish: 'victory', rematch: 'defeat', summary: 'victory', 'recovery-open': 'recovery', 'recovery-cancel': 'recovery', 'recovery-leave': 'recovery', 'recovery-profile': 'recovery', 'recovery-game': 'recovery' });
const RECOVERY_PRESET = Object.freeze({ 'recovery-open': 0, 'recovery-cancel': 1, 'recovery-leave': 2, 'recovery-profile': 3, 'recovery-game': 4 });
function createSoundDirector({ engine, random = Math.random, now = () => Date.now(), historyLimit = 3, waitingIntervalMs = 12000 } = {}) {
const history = new Map();
let lastVersion = -1; let lastWaitingMs = -waitingIntervalMs;
function select(family) {
const presets = CATALOG[family] || [];
if (!presets.length) return undefined;
const previous = history.get(family) || [];
const options = presets.filter(preset => !previous.includes(preset.id));
const pool = options.length ? options : presets.filter(preset => preset.id !== previous.at(-1));
const preset = pool[Math.min(pool.length - 1, Math.max(0, Math.floor(random() * pool.length)))];
history.set(family, [...previous, preset.id].slice(-historyLimit));
return preset;
}
function play(event, { version, combo = 0, reduced = false, summary = false } = {}) {
if (Number.isInteger(version)) { if (version <= lastVersion) return false; lastVersion = version; }
let family = summary ? 'summary' : EVENT_FAMILY[event] || event;
if (family === 'summary') family = 'victory';
if (!CATALOG[family]) return false;
if (family === 'waiting') { if (reduced || now() - lastWaitingMs < waitingIntervalMs) return false; lastWaitingMs = now(); }
const forcedPreset = RECOVERY_PRESET[event];
const preset = forcedPreset === undefined ? select(family) : CATALOG.recovery[forcedPreset];
if (!preset) return false;
const played = engine?.playPreset?.(preset);
if (!played) return false;
if (!reduced && (event === 'hit' || event === 'sunk') && combo >= 2) {
const comboPreset = { ...select(combo >= 4 ? 'victory' : combo === 3 ? 'turn' : 'hit'), id: `combo-${combo}`, priority: PRIORITY.sunk, gain: 0.12, durationMs: combo >= 4 ? 380 : 160, delayMs: 55 };
engine.playPreset(comboPreset);
}
return { preset, combo: combo >= 4 ? 'ultra' : combo === 3 ? 'mega' : combo === 2 ? 'combo' : undefined };
}
return { play, select, getCatalog: () => CATALOG, getHistory: family => [...(history.get(family) || [])], reset: () => { history.clear(); lastVersion = -1; lastWaitingMs = -waitingIntervalMs; }, getPriority: event => PRIORITY[EVENT_FAMILY[event] || event] || 0 };
}
const api = { createSoundDirector, CATALOG, COUNTS, PRIORITY };
if (typeof module !== 'undefined' && module.exports) module.exports = api;
else globalThis.BattleshipSounds = api;
})();