feat(traps): activation VFX/SFX, explosion effect, and help section
Wire mimic/pit/arrow/laser media on activate, poison/explosion via effects, and document the scene editor and traps in Instructions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
/** Звук и длительность эффекта «Взрыв» (`public/explosion.mp3` + WebM с альфой). */
|
||||
|
||||
import { getEffectsSfxGain } from './effectsSfxGain';
|
||||
|
||||
const EXPLOSION_SFX_VOLUME = 0.95;
|
||||
|
||||
/** Запас, если метаданные не прочитались (длина webm ~3.97 с). */
|
||||
const DEFAULT_EXPLOSION_LIFE_MS = 4200;
|
||||
|
||||
export function explosionEffectSoundUrl(): string {
|
||||
return new URL('explosion.mp3', window.location.href).href;
|
||||
}
|
||||
|
||||
/** VP8/VP9 WebM с `alpha_mode=1` — рендер через HTML `<video>`, не Pixi. */
|
||||
export function explosionEffectVideoUrl(): string {
|
||||
return new URL('vfx/explosion/aerial-debris-smoke.webm', window.location.href).href;
|
||||
}
|
||||
|
||||
let cachedExplosionSfxDurationMs: number | null = null;
|
||||
|
||||
/** Длительность трека в мс (кэш после первого чтения метаданных). */
|
||||
export function getExplosionSfxDurationMs(): Promise<number> {
|
||||
if (cachedExplosionSfxDurationMs !== null) {
|
||||
return Promise.resolve(cachedExplosionSfxDurationMs);
|
||||
}
|
||||
const url = explosionEffectSoundUrl();
|
||||
return new Promise((resolve) => {
|
||||
const a = new Audio();
|
||||
const done = (ms: number): void => {
|
||||
cachedExplosionSfxDurationMs = ms;
|
||||
a.removeAttribute('src');
|
||||
resolve(ms);
|
||||
};
|
||||
a.addEventListener('loadedmetadata', () => {
|
||||
const d = a.duration;
|
||||
done(Number.isFinite(d) && d > 0 ? Math.round(d * 1000) : DEFAULT_EXPLOSION_LIFE_MS);
|
||||
});
|
||||
a.addEventListener('error', () => done(DEFAULT_EXPLOSION_LIFE_MS));
|
||||
a.src = url;
|
||||
a.load();
|
||||
});
|
||||
}
|
||||
|
||||
/** Длительность визуала ≈ max(звук, webm), с разумными пределами. */
|
||||
export async function getExplosionEffectLifeMs(): Promise<number> {
|
||||
const sfxMs = await getExplosionSfxDurationMs();
|
||||
const raw = Math.max(sfxMs, DEFAULT_EXPLOSION_LIFE_MS);
|
||||
return Math.min(60_000, Math.max(600, raw));
|
||||
}
|
||||
|
||||
export async function playExplosionEffectSound(lifeMs: number): Promise<void> {
|
||||
try {
|
||||
const rawMs = await getExplosionSfxDurationMs();
|
||||
const target = Math.max(200, lifeMs);
|
||||
const rate = Math.max(0.25, Math.min(4, rawMs / target));
|
||||
const el = new Audio(explosionEffectSoundUrl());
|
||||
el.volume = Math.max(0, Math.min(1, EXPLOSION_SFX_VOLUME * getEffectsSfxGain()));
|
||||
el.playbackRate = rate;
|
||||
void el.play().catch(() => undefined);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user