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

@ -1,5 +1,16 @@
import { Box } from '@mui/material';
import { Typography } from '@mui/material';
import {
Box,
Typography,
Button,
Grid,
Card,
CardMedia,
CardContent,
Snackbar,
Alert,
Dialog,
DialogContent,
} from '@mui/material';
import CapeCard from '../components/CapeCard';
import {
Cape,
@ -7,9 +18,43 @@ import {
fetchCapesStore,
purchaseCape,
StoreCape,
Case,
CaseItem,
fetchCases,
fetchCase,
openCase,
Server,
} from '../api';
import { useEffect, useState } from 'react';
import { FullScreenLoader } from '../components/FullScreenLoader';
import { getPlayerServer } from '../utils/playerOnlineCheck';
import CaseRoulette from '../components/CaseRoulette';
function getRarityByWeight(
weight?: number,
): 'common' | 'rare' | 'epic' | 'legendary' {
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 Shop() {
const [storeCapes, setStoreCapes] = useState<StoreCape[]>([]);
@ -18,6 +63,39 @@ export default function Shop() {
const [uuid, setUuid] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
// Кейсы
const [cases, setCases] = useState<Case[]>([]);
const [casesLoading, setCasesLoading] = useState<boolean>(false);
// Онлайн/сервер (по аналогии с Marketplace)
const [isOnline, setIsOnline] = useState<boolean>(false);
const [playerServer, setPlayerServer] = useState<Server | null>(null);
const [onlineCheckLoading, setOnlineCheckLoading] = useState<boolean>(true);
// Рулетка
const [isOpening, setIsOpening] = useState<boolean>(false);
const [selectedCase, setSelectedCase] = useState<Case | null>(null);
const [rouletteOpen, setRouletteOpen] = useState(false);
const [rouletteCaseItems, setRouletteCaseItems] = useState<CaseItem[]>([]);
const [rouletteReward, setRouletteReward] = useState<CaseItem | null>(null);
// Уведомления
const [notification, setNotification] = useState<{
open: boolean;
message: string;
type: 'success' | 'error';
}>({
open: false,
message: '',
type: 'success',
});
const ITEM_WIDTH = 110; // ширина "карточки" предмета в рулетке
const ITEM_GAP = 8;
const VISIBLE_ITEMS = 21; // сколько элементов в линии
const CENTER_INDEX = Math.floor(VISIBLE_ITEMS / 2);
// Функция для загрузки плащей из магазина
const loadStoreCapes = async () => {
try {
@ -44,12 +122,55 @@ export default function Shop() {
try {
await purchaseCape(username, cape_id);
await loadUserCapes(username);
setNotification({
open: true,
message: 'Плащ успешно куплен!',
type: 'success',
});
} catch (error) {
console.error('Ошибка при покупке плаща:', error);
setNotification({
open: true,
message:
error instanceof Error ? error.message : 'Ошибка при покупке плаща',
type: 'error',
});
}
};
// Загружаем данные при монтировании
// Загрузка кейсов
const loadCases = async () => {
try {
setCasesLoading(true);
const casesData = await fetchCases();
setCases(casesData);
} catch (error) {
console.error('Ошибка при получении кейсов:', error);
setCases([]);
} finally {
setCasesLoading(false);
}
};
// Проверка онлайна игрока (по аналогии с Marketplace.tsx)
const checkPlayerStatus = async () => {
if (!username) return;
try {
setOnlineCheckLoading(true);
const { online, server } = await getPlayerServer(username);
setIsOnline(online);
setPlayerServer(server || null);
} catch (error) {
console.error('Ошибка при проверке онлайн-статуса:', error);
setIsOnline(false);
setPlayerServer(null);
} finally {
setOnlineCheckLoading(false);
}
};
// Загружаем базовые данные при монтировании
useEffect(() => {
const savedConfig = localStorage.getItem('launcher_config');
if (savedConfig) {
@ -60,22 +181,118 @@ export default function Shop() {
setLoading(true);
// Загружаем оба списка плащей
Promise.all([loadStoreCapes(), loadUserCapes(config.username)]).finally(
() => {
Promise.all([
loadStoreCapes(),
loadUserCapes(config.username),
loadCases(),
])
.catch((err) => console.error(err))
.finally(() => {
setLoading(false);
},
);
});
}
}
}, []);
// Проверяем онлайн после того, как знаем username
useEffect(() => {
if (username) {
checkPlayerStatus();
}
}, [username]);
// Фильтруем плащи, которые уже куплены пользователем
const availableCapes = storeCapes.filter(
(storeCape) =>
!userCapes.some((userCape) => userCape.cape_id === storeCape.id),
);
// Генерация массива предметов для рулетки
const generateRouletteItems = (
allItems: Case['items'],
winningItemMaterial: string,
): Case['items'] => {
if (!allItems || allItems.length === 0) return [];
const result: Case['items'] = [];
for (let i = 0; i < VISIBLE_ITEMS; i++) {
const randomItem = allItems[Math.floor(Math.random() * allItems.length)];
result.push(randomItem);
}
// Принудительно ставим выигрышный предмет в центр
const winningSource =
allItems.find((i) => i.material === winningItemMaterial) || allItems[0];
result[CENTER_INDEX] = winningSource;
return result;
};
const handleOpenCase = async (caseData: Case) => {
if (!username) {
setNotification({
open: true,
message: 'Не найдено имя игрока. Авторизуйтесь в лаунчере.',
type: 'error',
});
return;
}
if (!isOnline || !playerServer) {
setNotification({
open: true,
message: 'Для открытия кейсов необходимо находиться на сервере в игре.',
type: 'error',
});
return;
}
if (isOpening) return;
try {
setIsOpening(true);
// 1. получаем полный кейс
const fullCase = await fetchCase(caseData.id);
const caseItems: CaseItem[] = fullCase.items || [];
setSelectedCase(fullCase);
// 2. открываем кейс на бэке
const result = await openCase(fullCase.id, username, playerServer.id);
// 3. сохраняем данные для рулетки
setRouletteCaseItems(caseItems);
setRouletteReward(result.reward);
setRouletteOpen(true);
// 4. уведомление
setNotification({
open: true,
message: result.message || 'Кейс открыт!',
type: 'success',
});
setIsOpening(false);
} catch (error) {
console.error('Ошибка при открытии кейса:', error);
setNotification({
open: true,
message:
error instanceof Error ? error.message : 'Ошибка при открытии кейса',
type: 'error',
});
setIsOpening(false);
}
};
const handleCloseNotification = () => {
setNotification((prev) => ({ ...prev, open: false }));
};
const handleCloseRoulette = () => {
setRouletteOpen(false);
};
return (
<Box
sx={{
@ -88,9 +305,11 @@ export default function Shop() {
height: '100%',
}}
>
{loading ? (
<FullScreenLoader message="Загрузка..." />
) : (
{(loading || onlineCheckLoading) && (
<FullScreenLoader message="Загрузка магазина..." />
)}
{!loading && !onlineCheckLoading && (
<Box
sx={{
display: 'flex',
@ -102,6 +321,106 @@ export default function Shop() {
gap: '2vw',
}}
>
{/* Блок кейсов */}
<Typography variant="h6" sx={{ mb: 1 }}>
Кейсы
</Typography>
{!isOnline && (
<Typography
variant="body1"
color="error"
sx={{ mb: 2, maxWidth: '600px' }}
>
Для открытия кейсов вам необходимо находиться на одном из серверов
игры. Зайдите в игру и обновите страницу.
</Typography>
)}
{casesLoading ? (
<FullScreenLoader fullScreen={false} message="Загрузка кейсов..." />
) : cases.length > 0 ? (
<Grid container spacing={2} sx={{ mb: 4 }}>
{cases.map((c) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={c.id}>
<Card
sx={{
bgcolor: 'rgba(255, 255, 255, 0.05)',
borderRadius: '1vw',
}}
>
{c.image_url && (
<CardMedia
component="img"
image={c.image_url}
alt={c.name}
sx={{
minWidth: '10vw',
minHeight: '10vw',
maxHeight: '10vw',
objectFit: 'cover',
p: '0.5vw',
borderRadius: '1vw 1vw 0 0',
}}
/>
)}
<CardContent>
<Typography variant="h6" color="white">
{c.name}
</Typography>
{c.description && (
<Typography
variant="body2"
color="white"
sx={{ opacity: 0.7 }}
>
{c.description}
</Typography>
)}
<Typography variant="body2" color="white" sx={{ mt: 1 }}>
Цена: {c.price} монет
</Typography>
{typeof c.items_count === 'number' && (
<Typography
variant="body2"
color="white"
sx={{ opacity: 0.7 }}
>
Предметов в кейсе: {c.items_count}
</Typography>
)}
<Button
variant="contained"
fullWidth
sx={{
mt: '1vw',
borderRadius: '20px',
p: '0.3vw 0vw',
color: 'white',
backgroundColor: 'rgb(255, 77, 77)',
'&:hover': {
backgroundColor: 'rgba(255, 77, 77, 0.5)',
},
fontFamily: 'Benzin-Bold',
fontSize: '1vw',
}}
disabled={!isOnline || isOpening}
onClick={() => handleOpenCase(c)}
>
{isOpening && selectedCase?.id === c.id
? 'Открываем...'
: 'Открыть кейс'}
</Button>
</CardContent>
</Card>
</Grid>
))}
</Grid>
) : (
<Typography>Кейсы временно недоступны.</Typography>
)}
{/* Блок плащей (как был) */}
<Typography variant="h6">Доступные плащи</Typography>
{availableCapes.length > 0 ? (
<Box
@ -126,6 +445,30 @@ export default function Shop() {
)}
</Box>
)}
{/* Компонент с анимацией рулетки */}
<CaseRoulette
open={rouletteOpen}
onClose={handleCloseRoulette}
caseName={selectedCase?.name}
items={rouletteCaseItems}
reward={rouletteReward}
/>
{/* Уведомления */}
<Snackbar
open={notification.open}
autoHideDuration={6000}
onClose={handleCloseNotification}
>
<Alert
onClose={handleCloseNotification}
severity={notification.type}
sx={{ width: '100%' }}
>
{notification.message}
</Alert>
</Snackbar>
</Box>
);
}