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:
Ivan Fontosh
2026-07-25 12:35:22 +08:00
parent 02d73ddf81
commit fed5674468
36 changed files with 697 additions and 34 deletions
+63
View File
@@ -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 */
}
}