(() => { 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; })();