2c0e6fbf09
Water and rain erase fire under a tight brush hit radius; add looping fire/rain ambients and an Effects sound slider that also scales one-shot effect SFX. Co-authored-by: Cursor <cursoragent@cursor.com>
62 lines
2.3 KiB
TypeScript
62 lines
2.3 KiB
TypeScript
/** Звук и длительность эффекта «Облако яда» (`public/oblako-yada.mp3`). */
|
|
|
|
import { getEffectsSfxGain } from './effectsSfxGain';
|
|
|
|
const POISON_CLOUD_SFX_VOLUME = 0.92;
|
|
|
|
/** Запас, если метаданные не прочитались. */
|
|
const DEFAULT_POISON_CLOUD_LIFE_MS = 1600;
|
|
|
|
export function poisonCloudEffectSoundUrl(): string {
|
|
return new URL('oblako-yada.mp3', window.location.href).href;
|
|
}
|
|
|
|
let cachedPoisonCloudSfxDurationMs: number | null = null;
|
|
|
|
/** Длительность трека в мс (кэш после первого чтения метаданных). */
|
|
export function getPoisonCloudSfxDurationMs(): Promise<number> {
|
|
if (cachedPoisonCloudSfxDurationMs !== null) {
|
|
return Promise.resolve(cachedPoisonCloudSfxDurationMs);
|
|
}
|
|
const url = poisonCloudEffectSoundUrl();
|
|
return new Promise((resolve) => {
|
|
const a = new Audio();
|
|
const done = (ms: number): void => {
|
|
cachedPoisonCloudSfxDurationMs = ms;
|
|
a.removeAttribute('src');
|
|
resolve(ms);
|
|
};
|
|
a.addEventListener('loadedmetadata', () => {
|
|
const d = a.duration;
|
|
done(Number.isFinite(d) && d > 0 ? Math.round(d * 1000) : DEFAULT_POISON_CLOUD_LIFE_MS);
|
|
});
|
|
a.addEventListener('error', () => done(DEFAULT_POISON_CLOUD_LIFE_MS));
|
|
a.src = url;
|
|
a.load();
|
|
});
|
|
}
|
|
|
|
/** Длительность визуала = длительности файла (с разумными пределами). */
|
|
export async function getPoisonCloudEffectLifeMs(): Promise<number> {
|
|
const raw = await getPoisonCloudSfxDurationMs();
|
|
return Math.min(60_000, Math.max(600, raw));
|
|
}
|
|
|
|
/**
|
|
* Воспроизведение с подгонкой скорости: фактическое время звука по часам ≈ `lifeMs`,
|
|
* чтобы совпасть с анимацией.
|
|
*/
|
|
export async function playPoisonCloudEffectSound(lifeMs: number): Promise<void> {
|
|
try {
|
|
const rawMs = await getPoisonCloudSfxDurationMs();
|
|
const target = Math.max(200, lifeMs);
|
|
const rate = Math.max(0.25, Math.min(4, rawMs / target));
|
|
const el = new Audio(poisonCloudEffectSoundUrl());
|
|
el.volume = Math.max(0, Math.min(1, POISON_CLOUD_SFX_VOLUME * getEffectsSfxGain()));
|
|
el.playbackRate = rate;
|
|
void el.play().catch(() => undefined);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|