add cases to Shop (roulette don't work :( )
This commit is contained in:
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user