diff --git a/src/renderer/components/CaseRoulette.tsx b/src/renderer/components/CaseRoulette.tsx index 634f637..14666d7 100644 --- a/src/renderer/components/CaseRoulette.tsx +++ b/src/renderer/components/CaseRoulette.tsx @@ -1,5 +1,5 @@ import { Box, Typography, Button, Dialog, DialogContent } from '@mui/material'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef, useCallback } from 'react'; import { CaseItem } from '../api'; type Rarity = 'common' | 'rare' | 'epic' | 'legendary'; @@ -9,7 +9,7 @@ interface CaseRouletteProps { onClose: () => void; caseName?: string; items: CaseItem[]; - reward: CaseItem | null; // что реально выпало с бэка + reward: CaseItem | null; } // --- настройки рулетки --- @@ -17,12 +17,9 @@ const ITEM_WIDTH = 110; const ITEM_GAP = 8; const VISIBLE_ITEMS = 21; const CENTER_INDEX = Math.floor(VISIBLE_ITEMS / 2); - -// ширина видимой области и позиция линии const CONTAINER_WIDTH = 800; const LINE_X = CONTAINER_WIDTH / 2; -// редкость по weight (только фронт) function getRarityByWeight(weight?: number): Rarity { if (weight === undefined || weight === null) return 'common'; if (weight <= 5) return 'legendary'; @@ -56,48 +53,124 @@ export default function CaseRoulette({ const [sequence, setSequence] = useState([]); const [offset, setOffset] = useState(0); const [animating, setAnimating] = useState(false); + const [animationFinished, setAnimationFinished] = useState(false); + const itemRefs = useRef<(HTMLDivElement | null)[]>([]); + const animationTimeoutRef = useRef(); + const finishTimeoutRef = useRef(); const winningName = reward?.meta?.display_name || reward?.name || reward?.material || ''; - // генерируем полосу предметов и запускаем анимацию, - // когда диалог открыт и есть reward + // Измеряем реальные ширины элементов + const measureItemWidths = useCallback((): number[] => { + return itemRefs.current.map((ref) => + ref ? ref.getBoundingClientRect().width : ITEM_WIDTH, + ); + }, []); + + // Основной эффект для инициализации useEffect(() => { if (!open || !reward || !items || items.length === 0) return; - // 1. генерим последовательность + // Очистка предыдущих таймеров + if (animationTimeoutRef.current) clearTimeout(animationTimeoutRef.current); + if (finishTimeoutRef.current) clearTimeout(finishTimeoutRef.current); + + // Сброс состояний + setAnimating(false); + setAnimationFinished(false); + setOffset(0); + itemRefs.current = []; + + // 1. Генерируем последовательность + const totalItems = VISIBLE_ITEMS * 3; const seq: CaseItem[] = []; - for (let i = 0; i < VISIBLE_ITEMS; i++) { + + for (let i = 0; i < totalItems; i++) { const randomItem = items[Math.floor(Math.random() * items.length)]; seq.push(randomItem); } - // 2. подменяем центр на тот предмет, который реально выпал + const winPosition = Math.floor(totalItems / 2); const fromCase = items.find((i) => i.material === reward.material) || reward; - seq[CENTER_INDEX] = fromCase; + seq[winPosition] = fromCase; setSequence(seq); + }, [open, reward, items]); - // 3. считаем финальный offset, при котором CENTER_INDEX оказывается под линией - const centerItemCenter = - CENTER_INDEX * (ITEM_WIDTH + ITEM_GAP) + ITEM_WIDTH / 2; - const finalOffset = Math.max(0, centerItemCenter - LINE_X); + // Эффект для запуска анимации после рендера элементов + useEffect(() => { + if (sequence.length === 0 || !open) return; - // стартуем анимацию - setAnimating(false); - setOffset(0); + const startAnimation = () => { + const widths = measureItemWidths(); + if (widths.length === 0 || widths.length !== sequence.length) { + // Если не удалось измерить, используем базовую ширину + const winPosition = Math.floor(sequence.length / 2); + const centerItemCenter = + winPosition * (ITEM_WIDTH + ITEM_GAP) + ITEM_WIDTH / 2; + const initialOffset = centerItemCenter - CONTAINER_WIDTH; + const finalOffset = centerItemCenter - LINE_X; - // маленькая задержка, чтобы браузер применил начальное состояние - const id = setTimeout(() => { - setAnimating(true); - setOffset(finalOffset); - }, 50); + setOffset(initialOffset); + + animationTimeoutRef.current = setTimeout(() => { + setAnimating(true); + setOffset(finalOffset); + }, 200); + + finishTimeoutRef.current = setTimeout(() => { + setAnimationFinished(true); + }, 7000); + + return; + } + + const winPosition = Math.floor(sequence.length / 2); + let cumulativeOffset = 0; + for (let i = 0; i < winPosition; i++) { + cumulativeOffset += widths[i] + ITEM_GAP; + } + const centerItemCenter = cumulativeOffset + widths[winPosition] / 2; + const initialOffset = centerItemCenter - CONTAINER_WIDTH; + const finalOffset = centerItemCenter - LINE_X; + + setOffset(initialOffset); + + animationTimeoutRef.current = setTimeout(() => { + setAnimating(true); + setOffset(finalOffset); + }, 100); + + finishTimeoutRef.current = setTimeout(() => { + setAnimationFinished(true); + }, 3100); + }; + + // Даем время на рендер элементов + const renderTimeout = setTimeout(startAnimation, 100); return () => { - clearTimeout(id); + clearTimeout(renderTimeout); + if (animationTimeoutRef.current) + clearTimeout(animationTimeoutRef.current); + if (finishTimeoutRef.current) clearTimeout(finishTimeoutRef.current); }; - }, [open, reward, items]); + }, [sequence, open, measureItemWidths]); + + // Очистка при закрытии + useEffect(() => { + if (!open) { + if (animationTimeoutRef.current) + clearTimeout(animationTimeoutRef.current); + if (finishTimeoutRef.current) clearTimeout(finishTimeoutRef.current); + setSequence([]); + setAnimating(false); + setAnimationFinished(false); + setOffset(0); + } + }, [open]); return ( {sequence.map((item, index) => { const color = getRarityColor(item.weight); + const isWinningItem = + animationFinished && index === Math.floor(sequence.length / 2); return ( (itemRefs.current[index] = el)} sx={{ - width: `120px`, + minWidth: `${ITEM_WIDTH}px`, + width: 'auto', height: '120px', borderRadius: '0.7vw', bgcolor: 'rgba(255, 255, 255, 0.04)', @@ -175,10 +253,12 @@ export default function CaseRoulette({ flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - border: - index === CENTER_INDEX - ? `2px solid ${color}` - : `1px solid ${color}`, + border: isWinningItem + ? `3px solid ${color}` + : `1px solid ${color}`, + boxShadow: isWinningItem ? `0 0 20px ${color}` : 'none', + transition: 'all 0.3s ease', + padding: '0 10px', }} > {item.meta?.display_name || @@ -214,13 +298,20 @@ export default function CaseRoulette({ - {winningName && ( + {animationFinished && winningName && ( - Вам выпало: {winningName} + Вам выпало:{' '} + + {winningName} + )} @@ -243,6 +334,8 @@ export default function CaseRoulette({ backgroundColor: 'rgba(255, 77, 77, 0.5)', }, fontFamily: 'Benzin-Bold', + opacity: animationFinished ? 1 : 0.5, + pointerEvents: animationFinished ? 'auto' : 'none', }} > Закрыть