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
@@ -0,0 +1,92 @@
/** Одноразовая активация ловушек с собственным VFX/SFX (`public/vfx/<type>/…` + `public/<type>.mp3`).
* Яд/взрыв идут через effects store (как инструменты пульта) — см. ControlApp.
*/
import type { SceneTrapType } from '../../../shared/types';
import { getEffectsSfxGain } from '../../control/effectsSfxGain';
export type TrapMediaActivationKind = 'mimic' | 'pit' | 'arrow' | 'laser';
type TrapMediaSpec = {
videoPath: string;
soundPath: string;
/** Длительность видео + запас на старт (мс). */
lifeMs: number;
sfxVolume: number;
/**
* Точка кадра (0…1), которая совпадает с центром ловушки.
* По умолчанию центр; у лазера — правый конец луча.
*/
anchorNx?: number;
anchorNy?: number;
};
const TRAP_MEDIA: Record<TrapMediaActivationKind, TrapMediaSpec> = {
mimic: {
videoPath: 'vfx/mimic/tentacle-at-camera.webm',
soundPath: 'mimic.mp3',
lifeMs: 4500,
sfxVolume: 0.9,
},
pit: {
videoPath: 'vfx/pit/infinity-portal.webm',
soundPath: 'pit.mp3',
lifeMs: 6000,
sfxVolume: 0.9,
},
arrow: {
videoPath: 'vfx/arrow/arrows-grounding.webm',
soundPath: 'arrow.mp3',
lifeMs: 2000,
sfxVolume: 0.9,
},
laser: {
videoPath: 'vfx/laser/eye-lasers.webm',
soundPath: 'laser.mp3',
lifeMs: 1800,
sfxVolume: 0.9,
// правый кончик луча в кадре ≈ (0.82, 0.39)
anchorNx: 0.82,
anchorNy: 0.39,
},
};
export function isTrapMediaActivationKind(type: SceneTrapType): type is TrapMediaActivationKind {
return type === 'mimic' || type === 'pit' || type === 'arrow' || type === 'laser';
}
export function trapActivationLifeMs(kind: TrapMediaActivationKind): number {
return TRAP_MEDIA[kind].lifeMs;
}
export function trapActivationVideoUrl(kind: TrapMediaActivationKind): string {
return new URL(TRAP_MEDIA[kind].videoPath, window.location.href).href;
}
/** CSS transform, чтобы `anchor` кадра оказался в точке позиционирования. */
export function trapActivationAnchorTransform(kind: TrapMediaActivationKind): string {
const spec = TRAP_MEDIA[kind];
const ax = Math.max(0, Math.min(1, spec.anchorNx ?? 0.5));
const ay = Math.max(0, Math.min(1, spec.anchorNy ?? 0.5));
return `translate(${(-ax * 100).toFixed(2)}%, ${(-ay * 100).toFixed(2)}%)`;
}
export function playTrapActivationSound(kind: TrapMediaActivationKind): void {
try {
const spec = TRAP_MEDIA[kind];
const el = new Audio(new URL(spec.soundPath, window.location.href).href);
el.volume = Math.max(0, Math.min(1, spec.sfxVolume * getEffectsSfxGain()));
void el.play().catch(() => undefined);
} catch {
/* ignore */
}
}
/** @deprecated используй trapActivation* */
export const MIMIC_ACTIVATION_MS = TRAP_MEDIA.mimic.lifeMs;
export function mimicActivationVideoUrl(): string {
return trapActivationVideoUrl('mimic');
}
export function playMimicActivationSound(): void {
playTrapActivationSound('mimic');
}