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,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');
|
||||
}
|
||||
Reference in New Issue
Block a user