new style CaseRoulette

This commit is contained in:
2025-12-07 14:58:05 +05:00
parent bbd0dd11b0
commit 52336f8960
2 changed files with 223 additions and 59 deletions

View File

@ -16,13 +16,18 @@ interface CaseRouletteProps {
const ITEM_WIDTH = 110; const ITEM_WIDTH = 110;
const ITEM_GAP = 8; const ITEM_GAP = 8;
const VISIBLE_ITEMS = 21; const VISIBLE_ITEMS = 21;
const CENTER_INDEX = Math.floor(VISIBLE_ITEMS / 2);
const CONTAINER_WIDTH = 800; const CONTAINER_WIDTH = 800;
const LINE_X = CONTAINER_WIDTH / 2; const LINE_X = CONTAINER_WIDTH / 2;
const ANIMATION_DURATION = 10; // секунды const ANIMATION_DURATION = 10; // секунды
const ANIMATION_DURATION_MS = ANIMATION_DURATION * 1000; const ANIMATION_DURATION_MS = ANIMATION_DURATION * 1000;
// Удаляем майнкрафтовские цвет-коды (§a, §b, §l и т.д.)
function stripMinecraftColors(text?: string | null): string {
if (!text) return '';
return text.replace(/§[0-9A-FK-ORa-fk-or]/g, '');
}
function getRarityByWeight(weight?: number): Rarity { function getRarityByWeight(weight?: number): Rarity {
if (weight === undefined || weight === null) return 'common'; if (weight === undefined || weight === null) return 'common';
if (weight <= 5) return 'legendary'; if (weight <= 5) return 'legendary';
@ -42,7 +47,7 @@ function getRarityColor(weight?: number): string {
return 'rgba(65, 105, 225, 1)'; return 'rgba(65, 105, 225, 1)';
case 'common': case 'common':
default: default:
return 'rgba(255, 255, 255, 0.25)'; return 'rgba(255, 255, 255, 0.6)';
} }
} }
@ -61,8 +66,9 @@ export default function CaseRoulette({
const animationTimeoutRef = useRef<NodeJS.Timeout>(); const animationTimeoutRef = useRef<NodeJS.Timeout>();
const finishTimeoutRef = useRef<NodeJS.Timeout>(); const finishTimeoutRef = useRef<NodeJS.Timeout>();
const winningName = const winningNameRaw =
reward?.meta?.display_name || reward?.name || reward?.material || ''; reward?.meta?.display_name || reward?.name || reward?.material || '';
const winningName = stripMinecraftColors(winningNameRaw);
// Измеряем реальные ширины элементов // Измеряем реальные ширины элементов
const measureItemWidths = useCallback((): number[] => { const measureItemWidths = useCallback((): number[] => {
@ -121,10 +127,8 @@ export default function CaseRoulette({
winPosition * (ITEM_WIDTH + ITEM_GAP) + ITEM_WIDTH / 2; winPosition * (ITEM_WIDTH + ITEM_GAP) + ITEM_WIDTH / 2;
const finalOffset = centerItemCenter - LINE_X; const finalOffset = centerItemCenter - LINE_X;
// стартовая позиция чуть левее финальной (едем вправо к ней)
const initialOffset = Math.max(finalOffset - extraDistance, 0); const initialOffset = Math.max(finalOffset - extraDistance, 0);
// ставим ленту далеко "слева", чтобы она много проехала
setOffset(initialOffset); setOffset(initialOffset);
animationTimeoutRef.current = setTimeout(() => { animationTimeoutRef.current = setTimeout(() => {
@ -132,7 +136,6 @@ export default function CaseRoulette({
setOffset(finalOffset); setOffset(finalOffset);
}, 50); }, 50);
// 7 секунд анимации + небольшой запас
finishTimeoutRef.current = setTimeout(() => { finishTimeoutRef.current = setTimeout(() => {
setAnimationFinished(true); setAnimationFinished(true);
}, ANIMATION_DURATION_MS + 200); }, ANIMATION_DURATION_MS + 200);
@ -148,7 +151,6 @@ export default function CaseRoulette({
const centerItemCenter = cumulativeOffset + widths[winPosition] / 2; const centerItemCenter = cumulativeOffset + widths[winPosition] / 2;
const finalOffset = centerItemCenter - LINE_X; const finalOffset = centerItemCenter - LINE_X;
// стартовая позиция чуть левее финальной (едем вправо к ней)
const initialOffset = Math.max(finalOffset - extraDistance, 0); const initialOffset = Math.max(finalOffset - extraDistance, 0);
setOffset(initialOffset); setOffset(initialOffset);
@ -194,16 +196,31 @@ export default function CaseRoulette({
fullWidth fullWidth
PaperProps={{ PaperProps={{
sx: { sx: {
bgcolor: 'rgba(15, 15, 20, 0.95)', bgcolor: 'rgba(5, 5, 10, 0.96)',
borderRadius: '1vw', borderRadius: '24px',
border: '1px solid rgba(255,255,255,0.12)',
boxShadow: '0 25px 80px rgba(0,0,0,0.8)',
overflow: 'hidden',
}, },
}} }}
> >
<DialogContent> <DialogContent
sx={{
px: 3,
py: 3.5,
background:
'radial-gradient(circle at top, rgba(255,255,255,0.06), transparent 60%)',
}}
>
<Typography <Typography
variant="h6" variant="h6"
color="white" color="white"
sx={{ textAlign: 'center', mb: 2 }} sx={{
textAlign: 'center',
mb: 2.5,
fontFamily: 'Benzin-Bold',
letterSpacing: 0.5,
}}
> >
Открытие кейса {caseName} Открытие кейса {caseName}
</Typography> </Typography>
@ -212,15 +229,31 @@ export default function CaseRoulette({
sx={{ sx={{
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
borderRadius: '1vw', borderRadius: '18px',
border: '2px solid rgba(255, 255, 255, 0.1)', border: '1px solid rgba(255, 255, 255, 0.15)',
px: 2, px: 2,
py: 3, py: 3,
width: `${CONTAINER_WIDTH}px`, width: `${CONTAINER_WIDTH}px`,
maxWidth: '100%', maxWidth: '100%',
mx: 'auto', mx: 'auto',
background:
'linear-gradient(135deg, rgba(10,10,20,0.9), rgba(25,25,45,0.9))',
backdropFilter: 'blur(18px)',
boxShadow: '0 0 40px rgba(0,0,0,0.7)',
}} }}
> >
{/* затемнённые края */}
<Box
sx={{
position: 'absolute',
inset: 0,
pointerEvents: 'none',
background:
'linear-gradient(90deg, rgba(0,0,0,0.7) 0%, transparent 20%, transparent 80%, rgba(0,0,0,0.7) 100%)',
zIndex: 1,
}}
/>
{/* Линия центра */} {/* Линия центра */}
<Box <Box
sx={{ sx={{
@ -230,7 +263,8 @@ export default function CaseRoulette({
left: `${LINE_X}px`, left: `${LINE_X}px`,
transform: 'translateX(-1px)', transform: 'translateX(-1px)',
width: '2px', width: '2px',
bgcolor: 'rgba(255, 77, 77, 0.8)', bgcolor: 'rgba(255, 77, 77, 0.9)',
boxShadow: '0 0 18px rgba(255,77,77,0.9)',
zIndex: 2, zIndex: 2,
}} }}
/> />
@ -246,6 +280,8 @@ export default function CaseRoulette({
transition: animating transition: animating
? `transform ${ANIMATION_DURATION}s cubic-bezier(0.15, 0.85, 0.25, 1)` ? `transform ${ANIMATION_DURATION}s cubic-bezier(0.15, 0.85, 0.25, 1)`
: 'none', : 'none',
position: 'relative',
zIndex: 0,
}} }}
> >
{sequence.map((item, index) => { {sequence.map((item, index) => {
@ -253,6 +289,15 @@ export default function CaseRoulette({
const isWinningItem = const isWinningItem =
animationFinished && index === Math.floor(sequence.length / 2); animationFinished && index === Math.floor(sequence.length / 2);
const rawName =
item.meta?.display_name ||
item.name ||
item.material
.replace(/_/g, ' ')
.toLowerCase()
.replace(/\b\w/g, (l) => l.toUpperCase());
const displayName = stripMinecraftColors(rawName);
return ( return (
<Box <Box
key={index} key={index}
@ -260,19 +305,22 @@ export default function CaseRoulette({
sx={{ sx={{
minWidth: `${ITEM_WIDTH}px`, minWidth: `${ITEM_WIDTH}px`,
width: 'auto', width: 'auto',
height: '120px', height: '130px',
borderRadius: '0.7vw', borderRadius: '14px',
bgcolor: 'rgba(255, 255, 255, 0.04)', bgcolor: 'rgba(255, 255, 255, 0.03)',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
border: isWinningItem border: isWinningItem
? `3px solid ${color}` ? `2px solid ${color}`
: `1px solid ${color}`, : `1px solid ${color}`,
boxShadow: isWinningItem ? `0 0 20px ${color}` : 'none', boxShadow: isWinningItem
? `0 0 22px ${color}`
: '0 0 8px rgba(0,0,0,0.6)',
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
padding: '0 10px', padding: '0 10px',
transform: isWinningItem ? 'scale(1.08)' : 'scale(1)',
}} }}
> >
<Box <Box
@ -280,8 +328,8 @@ export default function CaseRoulette({
src={`https://cdn.minecraft.popa-popa.ru/textures/${item.material.toLowerCase()}.png`} src={`https://cdn.minecraft.popa-popa.ru/textures/${item.material.toLowerCase()}.png`}
alt={item.material} alt={item.material}
sx={{ sx={{
width: '48px', width: '52px',
height: '48px', height: '52px',
objectFit: 'contain', objectFit: 'contain',
imageRendering: 'pixelated', imageRendering: 'pixelated',
mb: 1, mb: 1,
@ -292,19 +340,14 @@ export default function CaseRoulette({
sx={{ sx={{
color: 'white', color: 'white',
textAlign: 'center', textAlign: 'center',
fontSize: '0.7rem', fontSize: '0.72rem',
maxWidth: '100px', maxWidth: '100px',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}} }}
> >
{item.meta?.display_name || {displayName}
item.name ||
item.material
.replace(/_/g, ' ')
.toLowerCase()
.replace(/\b\w/g, (l) => l.toUpperCase())}
</Typography> </Typography>
</Box> </Box>
); );
@ -318,7 +361,7 @@ export default function CaseRoulette({
color="white" color="white"
sx={{ sx={{
textAlign: 'center', textAlign: 'center',
mt: 2, mt: 2.5,
animation: 'fadeIn 0.5s ease', animation: 'fadeIn 0.5s ease',
}} }}
> >
@ -340,16 +383,21 @@ export default function CaseRoulette({
variant="contained" variant="contained"
onClick={onClose} onClick={onClose}
sx={{ sx={{
borderRadius: '20px', borderRadius: '999px',
p: '0.5vw 2.5vw', px: '2.8vw',
py: '0.6vw',
color: 'white', color: 'white',
backgroundColor: 'rgb(255, 77, 77)', background:
'linear-gradient(135deg, rgb(255, 77, 77), rgb(255, 120, 100))',
'&:hover': { '&:hover': {
backgroundColor: 'rgba(255, 77, 77, 0.5)', background:
'linear-gradient(135deg, rgba(255, 77, 77, 0.8), rgba(255, 120, 100, 0.8))',
}, },
fontFamily: 'Benzin-Bold', fontFamily: 'Benzin-Bold',
opacity: animationFinished ? 1 : 0.5, opacity: animationFinished ? 1 : 0.5,
pointerEvents: animationFinished ? 'auto' : 'none', pointerEvents: animationFinished ? 'auto' : 'none',
textTransform: 'uppercase',
letterSpacing: 1,
}} }}
> >
Закрыть Закрыть

View File

@ -343,66 +343,182 @@ export default function Shop() {
<Grid container spacing={2} sx={{ mb: 4 }}> <Grid container spacing={2} sx={{ mb: 4 }}>
{cases.map((c) => ( {cases.map((c) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={c.id}> <Grid item xs={12} sm={6} md={4} lg={3} key={c.id}>
{/* НОВАЯ КАРТОЧКА */}
<Card <Card
sx={{ sx={{
bgcolor: 'rgba(255, 255, 255, 0.05)', position: 'relative',
borderRadius: '1vw', bgcolor: 'rgba(5, 5, 15, 0.96)',
borderRadius: '20px',
border: '1px solid rgba(255, 255, 255, 0.08)',
boxShadow: '0 18px 45px rgba(0, 0, 0, 0.8)',
overflow: 'hidden',
transition:
'transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease',
'&:hover': {
transform: 'translateY(-6px)',
boxShadow: '0 26px 60px rgba(0, 0, 0, 0.95)',
borderColor: 'rgba(255, 255, 255, 0.18)',
},
}} }}
> >
{/* верхний “свет” */}
<Box
sx={{
position: 'absolute',
inset: 0,
pointerEvents: 'none',
background:
'radial-gradient(circle at top, rgba(255,255,255,0.13), transparent 55%)',
}}
/>
{c.image_url && ( {c.image_url && (
<Box
sx={{
position: 'relative',
p: '0.9vw',
pb: 0,
}}
>
<Box
sx={{
borderRadius: '16px',
overflow: 'hidden',
border: '1px solid rgba(255, 255, 255, 0.12)',
background:
'linear-gradient(135deg, rgba(40,40,80,0.9), rgba(15,15,35,0.9))',
}}
>
<CardMedia <CardMedia
component="img" component="img"
image={c.image_url} image={c.image_url}
alt={c.name} alt={c.name}
sx={{ sx={{
minWidth: '10vw', width: '100%',
minHeight: '10vw', height: '11vw',
maxHeight: '10vw', minHeight: '140px',
objectFit: 'cover', objectFit: 'cover',
p: '0.5vw', filter: 'saturate(1.1)',
borderRadius: '1vw 1vw 0 0',
}} }}
/> />
</Box>
{/* маленький бейдж сверху картинки */}
<Box
sx={{
position: 'absolute',
top: '1.2vw',
left: '1.6vw',
px: 1.2,
py: 0.4,
borderRadius: '999px',
fontSize: '0.7rem',
textTransform: 'uppercase',
letterSpacing: 0.6,
bgcolor: 'rgba(0, 0, 0, 0.6)',
border: '1px solid rgba(255, 255, 255, 0.4)',
color: 'white',
}}
>
Кейс
</Box>
</Box>
)} )}
<CardContent>
<Typography variant="h6" color="white"> <CardContent
sx={{
position: 'relative',
zIndex: 1,
pt: c.image_url ? '0.9vw' : '1.4vw',
pb: '1.3vw',
}}
>
<Typography
variant="h6"
color="white"
sx={{
fontFamily: 'Benzin-Bold',
fontSize: '1.05rem',
mb: 0.8,
}}
>
{c.name} {c.name}
</Typography> </Typography>
{c.description && ( {c.description && (
<Typography <Typography
variant="body2" variant="body2"
color="white" color="white"
sx={{ opacity: 0.7 }} sx={{
opacity: 0.75,
fontSize: '0.85rem',
mb: 1.5,
minHeight: '40px',
}}
> >
{c.description} {c.description}
</Typography> </Typography>
)} )}
<Typography variant="body2" color="white" sx={{ mt: 1 }}>
Цена: {c.price} монет <Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 1,
}}
>
<Typography
variant="body2"
color="white"
sx={{ opacity: 0.8, fontSize: '0.85rem' }}
>
Цена
</Typography> </Typography>
<Box
sx={{
px: 1.6,
py: 0.4,
borderRadius: '999px',
fontSize: '0.8rem',
bgcolor: 'rgba(255, 77, 77, 0.16)',
border: '1px solid rgba(255, 77, 77, 0.85)',
color: 'white',
fontFamily: 'Benzin-Bold',
}}
>
{c.price} поп
</Box>
</Box>
{typeof c.items_count === 'number' && ( {typeof c.items_count === 'number' && (
<Typography <Typography
variant="body2" variant="body2"
color="white" color="white"
sx={{ opacity: 0.7 }} sx={{ opacity: 0.6, fontSize: '0.8rem', mb: 1.4 }}
> >
Предметов в кейсе: {c.items_count} Предметов в кейсе: {c.items_count}
</Typography> </Typography>
)} )}
<Button <Button
variant="contained" variant="contained"
fullWidth fullWidth
sx={{ sx={{
mt: '1vw', mt: 0.5,
borderRadius: '20px', borderRadius: '999px',
p: '0.3vw 0vw', py: '0.45vw',
color: 'white', color: 'white',
backgroundColor: 'rgb(255, 77, 77)', background:
'linear-gradient(135deg, rgb(255, 77, 77), rgb(255, 120, 100))',
'&:hover': { '&:hover': {
backgroundColor: 'rgba(255, 77, 77, 0.5)', background:
'linear-gradient(135deg, rgba(255, 77, 77, 0.85), rgba(255, 120, 100, 0.9))',
}, },
fontFamily: 'Benzin-Bold', fontFamily: 'Benzin-Bold',
fontSize: '1vw', fontSize: '0.9rem',
textTransform: 'uppercase',
letterSpacing: 0.8,
}} }}
disabled={!isOnline || isOpening} disabled={!isOnline || isOpening}
onClick={() => handleOpenCase(c)} onClick={() => handleOpenCase(c)}