/** HTML `` для эффекта «Взрыв» — WebM с альфой (Pixi video/webp давали чёрный фон). */ import React, { useEffect, useMemo, useState } from 'react'; import type { EffectInstance, EffectsState, ExplosionInstance } from '../../../shared/types/effects'; import styles from './ExplosionVideoOverlay.module.css'; function explosionEffectVideoUrl(): string { return new URL('vfx/explosion/aerial-debris-smoke.webm', window.location.href).href; } type Viewport = { x: number; y: number; w: number; h: number }; type Props = { state: EffectsState | null; viewport: Viewport | null | undefined; /** Draft с пульта (пока ведём кисть) — тоже через video с альфой. */ draft?: ExplosionInstance | null; }; function isLiveExplosion(inst: EffectInstance, nowMs: number): inst is ExplosionInstance { if (inst.type !== 'explosion') return false; return nowMs - inst.createdAtMs < inst.lifetimeMs; } export function ExplosionVideoOverlay({ state, viewport, draft = null }: Props) { const [nowMs, setNowMs] = useState(() => state?.serverNowMs ?? Date.now()); const explosions = useMemo(() => { const list = (state?.instances ?? []).filter((i): i is ExplosionInstance => isLiveExplosion(i, nowMs), ); if (draft && draft.type === 'explosion') { return [...list.filter((i) => i.id !== '__draft__'), draft]; } return list; }, [draft, nowMs, state?.instances]); useEffect(() => { if (explosions.length === 0) return; const id = window.setInterval(() => { setNowMs(state?.serverNowMs ?? Date.now()); }, 100); return () => window.clearInterval(id); }, [explosions.length, state?.serverNowMs]); useEffect(() => { setNowMs(state?.serverNowMs ?? Date.now()); }, [state?.revision, state?.serverNowMs]); if (!viewport || explosions.length === 0) return null; const minDim = Math.min(viewport.w, viewport.h); return ( {explosions.map((inst) => { const sizePx = Math.max(16, inst.radiusN * minDim); const width = Math.max(sizePx * 4.2, minDim * 0.18); const isDraft = inst.id === '__draft__'; return ( { if (!el) return; el.pause(); try { el.currentTime = 0.05; } catch { /* ignore */ } } : undefined } /> ); })} ); }