import React, { useEffect, useId, useRef, useState } from 'react'; import DiceBox from '@3d-dice/dice-box-threejs'; import { formatPredeterminedDiceNotation, isDiceSides, type DiceRollState, } from '../../../shared/types/diceRoll'; import { useDiceRollState } from './useDiceRollState'; import styles from './DiceRollLayer.module.css'; function diceAssetPath(): string { const base = import.meta.env.BASE_URL || '/'; const joined = `${base}dice-box/`.replace(/([^:]\/)\/+/g, '$1'); return joined; } function parseNotationSides(notation: string): number | null { const m = /^(\d+)d(\d+)$/i.exec(notation.trim()); if (!m) return null; const sides = Number(m[2]); return Number.isFinite(sides) ? sides : null; } function parseNotationCount(notation: string): number { const m = /^(\d+)d(\d+)$/i.exec(notation.trim()); if (!m) return 1; return Math.max(1, Number(m[1]) || 1); } /** Полноэкранный оверлей 3D-броска на превью и презентации. */ export function DiceRollLayer() { const [state] = useDiceRollState(); const reactId = useId().replace(/:/g, ''); const hostId = `dice-roll-host-${reactId}`; const boxRef = useRef(null); const rollingRef = useRef(false); const lastRevisionRef = useRef(0); const [ready, setReady] = useState(false); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; const box = new DiceBox(`#${hostId}`, { assetPath: diceAssetPath(), sounds: false, shadows: true, theme_surface: 'green-felt', theme_colorset: 'white', theme_material: 'glass', strength: 1, gravity_multiplier: 400, }); boxRef.current = box; void box .initialize() .then(() => { if (!cancelled) setReady(true); }) .catch((err: unknown) => { if (cancelled) return; setError(err instanceof Error ? err.message : String(err)); }); return () => { cancelled = true; try { box.clearDice?.(); } catch { /* ignore */ } boxRef.current = null; const el = document.getElementById(hostId); el?.replaceChildren(); }; }, [hostId]); useEffect(() => { if (!ready || !state?.active) return; if (state.revision === lastRevisionRef.current) return; if (rollingRef.current) return; const box = boxRef.current; if (!box) return; const sidesRaw = parseNotationSides(state.notation); if (sidesRaw == null || !isDiceSides(sidesRaw)) return; const count = parseNotationCount(state.notation); const notation = formatPredeterminedDiceNotation(count, sidesRaw, state.values); const revision = state.revision; lastRevisionRef.current = revision; rollingRef.current = true; void (async () => { try { await box.roll(notation); } catch (err) { console.error('[dice] roll failed', err); } finally { rollingRef.current = false; } })(); }, [ready, state]); const visible = Boolean(state?.active); return (
{error ?
{error}
: null} {visible && state ? : null}
); } function DiceResultBadge({ state }: { state: DiceRollState }) { if (!state.values.length) return null; return (
{state.notation} {state.values.join(' + ')} = {state.total}
); }