add cases to Shop (roulette don't work :( )
This commit is contained in:
254
src/renderer/components/CaseRoulette.tsx
Normal file
254
src/renderer/components/CaseRoulette.tsx
Normal file
@ -0,0 +1,254 @@
|
||||
import { Box, Typography, Button, Dialog, DialogContent } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CaseItem } from '../api';
|
||||
|
||||
type Rarity = 'common' | 'rare' | 'epic' | 'legendary';
|
||||
|
||||
interface CaseRouletteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
caseName?: string;
|
||||
items: CaseItem[];
|
||||
reward: CaseItem | null; // что реально выпало с бэка
|
||||
}
|
||||
|
||||
// --- настройки рулетки ---
|
||||
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';
|
||||
if (weight <= 20) return 'epic';
|
||||
if (weight <= 50) return 'rare';
|
||||
return 'common';
|
||||
}
|
||||
|
||||
function getRarityColor(weight?: number): string {
|
||||
const rarity = getRarityByWeight(weight);
|
||||
switch (rarity) {
|
||||
case 'legendary':
|
||||
return 'rgba(255, 215, 0, 1)';
|
||||
case 'epic':
|
||||
return 'rgba(186, 85, 211, 1)';
|
||||
case 'rare':
|
||||
return 'rgba(65, 105, 225, 1)';
|
||||
case 'common':
|
||||
default:
|
||||
return 'rgba(255, 255, 255, 0.25)';
|
||||
}
|
||||
}
|
||||
|
||||
export default function CaseRoulette({
|
||||
open,
|
||||
onClose,
|
||||
caseName,
|
||||
items,
|
||||
reward,
|
||||
}: CaseRouletteProps) {
|
||||
const [sequence, setSequence] = useState<CaseItem[]>([]);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [animating, setAnimating] = useState(false);
|
||||
|
||||
const winningName =
|
||||
reward?.meta?.display_name || reward?.name || reward?.material || '';
|
||||
|
||||
// генерируем полосу предметов и запускаем анимацию,
|
||||
// когда диалог открыт и есть reward
|
||||
useEffect(() => {
|
||||
if (!open || !reward || !items || items.length === 0) return;
|
||||
|
||||
// 1. генерим последовательность
|
||||
const seq: CaseItem[] = [];
|
||||
for (let i = 0; i < VISIBLE_ITEMS; i++) {
|
||||
const randomItem = items[Math.floor(Math.random() * items.length)];
|
||||
seq.push(randomItem);
|
||||
}
|
||||
|
||||
// 2. подменяем центр на тот предмет, который реально выпал
|
||||
const fromCase =
|
||||
items.find((i) => i.material === reward.material) || reward;
|
||||
seq[CENTER_INDEX] = fromCase;
|
||||
|
||||
setSequence(seq);
|
||||
|
||||
// 3. считаем финальный offset, при котором CENTER_INDEX оказывается под линией
|
||||
const centerItemCenter =
|
||||
CENTER_INDEX * (ITEM_WIDTH + ITEM_GAP) + ITEM_WIDTH / 2;
|
||||
const finalOffset = Math.max(0, centerItemCenter - LINE_X);
|
||||
|
||||
// стартуем анимацию
|
||||
setAnimating(false);
|
||||
setOffset(0);
|
||||
|
||||
// маленькая задержка, чтобы браузер применил начальное состояние
|
||||
const id = setTimeout(() => {
|
||||
setAnimating(true);
|
||||
setOffset(finalOffset);
|
||||
}, 50);
|
||||
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}, [open, reward, items]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: 'rgba(15, 15, 20, 0.95)',
|
||||
borderRadius: '1vw',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Typography
|
||||
variant="h6"
|
||||
color="white"
|
||||
sx={{ textAlign: 'center', mb: 2 }}
|
||||
>
|
||||
Открытие кейса {caseName}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderRadius: '1vw',
|
||||
border: '2px solid rgba(255, 255, 255, 0.1)',
|
||||
px: 2,
|
||||
py: 3,
|
||||
width: `${CONTAINER_WIDTH}px`,
|
||||
maxWidth: '100%',
|
||||
mx: 'auto',
|
||||
}}
|
||||
>
|
||||
{/* Линия центра */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: `${LINE_X}px`,
|
||||
transform: 'translateX(-1px)',
|
||||
width: '2px',
|
||||
bgcolor: 'rgba(255, 77, 77, 0.8)',
|
||||
zIndex: 2,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Лента с предметами */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: `${ITEM_GAP}px`,
|
||||
transform: `translateX(-${offset}px)`,
|
||||
transition: animating
|
||||
? 'transform 3s cubic-bezier(0.1, 0.8, 0.2, 1)'
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
{sequence.map((item, index) => {
|
||||
const color = getRarityColor(item.weight);
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
width: `120px`,
|
||||
height: '120px',
|
||||
borderRadius: '0.7vw',
|
||||
bgcolor: 'rgba(255, 255, 255, 0.04)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border:
|
||||
index === CENTER_INDEX
|
||||
? `2px solid ${color}`
|
||||
: `1px solid ${color}`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src={`https://cdn.minecraft.popa-popa.ru/textures/${item.material.toLowerCase()}.png`}
|
||||
alt={item.material}
|
||||
sx={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
objectFit: 'contain',
|
||||
imageRendering: 'pixelated',
|
||||
mb: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color: 'white',
|
||||
textAlign: 'center',
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
{item.meta?.display_name ||
|
||||
item.name ||
|
||||
item.material
|
||||
.replace(/_/g, ' ')
|
||||
.toLowerCase()
|
||||
.replace(/\b\w/g, (l) => l.toUpperCase())}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{winningName && (
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="white"
|
||||
sx={{ textAlign: 'center', mt: 2 }}
|
||||
>
|
||||
Вам выпало: <b>{winningName}</b>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
mt: 3,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={onClose}
|
||||
sx={{
|
||||
borderRadius: '20px',
|
||||
p: '0.5vw 2.5vw',
|
||||
color: 'white',
|
||||
backgroundColor: 'rgb(255, 77, 77)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'rgba(255, 77, 77, 0.5)',
|
||||
},
|
||||
fontFamily: 'Benzin-Bold',
|
||||
}}
|
||||
>
|
||||
Закрыть
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user