import React, { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import type { SceneTrap, SceneTrapsState } from '../../../shared/types'; import { defaultTrapRuntime } from '../../../shared/types/sceneTraps'; import { TrapGlyph } from './TrapGlyph'; import styles from './SceneTrapsOverlay.module.css'; type Props = { traps: readonly SceneTrap[]; session: SceneTrapsState | null; viewport: { x: number; y: number; w: number; h: number } | null; /** Пульт: показывать все ловушки + RMB меню. Презентация: только revealed. */ mode: 'control' | 'presentation'; onReveal?: (trapId: string) => void; onActivate?: (trapId: string) => void; onDisarm?: (trapId: string) => void; }; function menuPosition(clientX: number, clientY: number): { x: number; y: number } { const menuW = 200; const menuH = 140; const pad = 8; return { x: Math.max(pad, Math.min(clientX, window.innerWidth - menuW - pad)), y: Math.max(pad, Math.min(clientY, window.innerHeight - menuH - pad)), }; } export function SceneTrapsOverlay({ traps, session, viewport, mode, onReveal, onActivate, onDisarm, }: Props) { const [menu, setMenu] = useState<{ trapId: string; x: number; y: number } | null>(null); const [flashToken, setFlashToken] = useState<{ trapId: string; token: number } | null>(null); useEffect(() => { const act = session?.lastActivation; if (!act) return; setFlashToken(act); const t = window.setTimeout(() => setFlashToken(null), 750); return () => window.clearTimeout(t); }, [session?.lastActivation?.token, session?.lastActivation?.trapId]); useEffect(() => { if (!menu) return; const close = (e: PointerEvent) => { const t = e.target; if (t instanceof Element && t.closest('[data-trap-menu-root="1"]')) return; setMenu(null); }; window.addEventListener('pointerdown', close, true); return () => window.removeEventListener('pointerdown', close, true); }, [menu]); if (!viewport || traps.length === 0) return null; const minDim = Math.min(viewport.w, viewport.h); return (