/** Звук и длительность эффекта «Заморозка» (`public/zamorozka.mp3`). */ const DEFAULT_FREEZE_LIFE_MS = 820; const FREEZE_SFX_BASE_VOLUME = 0.88; /** На 25% тише базовой громкости эффекта. */ export const FREEZE_SFX_VOLUME = FREEZE_SFX_BASE_VOLUME * 0.75; export function freezeEffectSoundUrl(): string { return new URL('zamorozka.mp3', window.location.href).href; } let cachedFreezeSfxDurationMs: number | null = null; /** Длительность трека в мс (кэш после первого чтения метаданных). */ export function getFreezeSfxDurationMs(): Promise { if (cachedFreezeSfxDurationMs !== null) { return Promise.resolve(cachedFreezeSfxDurationMs); } const url = freezeEffectSoundUrl(); return new Promise((resolve) => { const a = new Audio(); const done = (ms: number): void => { cachedFreezeSfxDurationMs = ms; a.removeAttribute('src'); resolve(ms); }; a.addEventListener('loadedmetadata', () => { const d = a.duration; done(Number.isFinite(d) && d > 0 ? Math.round(d * 1000) : DEFAULT_FREEZE_LIFE_MS); }); a.addEventListener('error', () => done(DEFAULT_FREEZE_LIFE_MS)); a.src = url; a.load(); }); } /** Длительность полноэкранной заморозки и льда — по времени звука, с разумными пределами. */ export async function getFreezeEffectLifeMs(): Promise { const raw = await getFreezeSfxDurationMs(); return Math.min(45_000, Math.max(400, raw)); } export function playFreezeEffectSound(): void { try { const el = new Audio(freezeEffectSoundUrl()); el.volume = FREEZE_SFX_VOLUME; void el.play().catch(() => undefined); } catch { /* ignore */ } }