add cases to Shop (roulette don't work :( )

This commit is contained in:
2025-12-07 02:44:25 +05:00
parent 3ddcda2cec
commit c14315b078
3 changed files with 696 additions and 11 deletions

View 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>
);
}