import { Box, Typography, Button, Grid, FormControl, Select, MenuItem, InputLabel } from '@mui/material'; import { Cape, fetchCapes, fetchCapesStore, purchaseCape, StoreCape, Case, CaseItem, fetchCases, fetchCase, openCase, Server, fetchPlayer, BonusType, UserBonus, fetchBonusTypes, fetchUserBonuses, purchaseBonus, upgradeBonus, toggleBonusActivation, } from '../api'; import { useEffect, useState } from 'react'; import { FullScreenLoader } from '../components/FullScreenLoader'; import { getPlayerServer } from '../utils/playerOnlineCheck'; import CaseRoulette from '../components/CaseRoulette'; import BonusShopItem from '../components/BonusShopItem'; import ShopItem from '../components/ShopItem'; import { playBuySound, primeSounds } from '../utils/sounds'; import CustomNotification from '../components/Notifications/CustomNotification'; import type { NotificationPosition } from '../components/Notifications/CustomNotification'; import { isNotificationsEnabled, getNotifPositionFromSettings } from '../utils/notifications'; import { translateServer } from '../utils/serverTranslator'; 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([]); const [userCapes, setUserCapes] = useState([]); const [username, setUsername] = useState(''); const [uuid, setUuid] = useState(''); const [loading, setLoading] = useState(false); const [playerSkinUrl, setPlayerSkinUrl] = useState(''); const [selectedCaseServerIp, setSelectedCaseServerIp] = useState(''); // Уведомления const [notifOpen, setNotifOpen] = useState(false); const [notifMsg, setNotifMsg] = useState(''); const [notifSeverity, setNotifSeverity] = useState< 'success' | 'info' | 'warning' | 'error' >('info'); const [notifPos, setNotifPos] = useState({ vertical: 'bottom', horizontal: 'center', }); // Прокачка const [bonusTypes, setBonusTypes] = useState([]); const [userBonuses, setUserBonuses] = useState([]); const [bonusesLoading, setBonusesLoading] = useState(false); const [processingBonusIds, setProcessingBonusIds] = useState([]); // Кейсы const [cases, setCases] = useState([]); const [casesLoading, setCasesLoading] = useState(false); // Онлайн/сервер (по аналогии с Marketplace) const [isOnline, setIsOnline] = useState(false); const [playerServer, setPlayerServer] = useState(null); const [onlineCheckLoading, setOnlineCheckLoading] = useState(true); // Рулетка const [isOpening, setIsOpening] = useState(false); const [selectedCase, setSelectedCase] = useState(null); const [rouletteOpen, setRouletteOpen] = useState(false); const [rouletteCaseItems, setRouletteCaseItems] = useState([]); const [rouletteReward, setRouletteReward] = useState(null); // Уведомления const [notification, setNotification] = useState<{ open: boolean; message: string; type: 'success' | 'error'; }>({ open: false, message: '', type: 'success', }); const loadBonuses = async (username: string) => { try { setBonusesLoading(true); const [types, user] = await Promise.all([ fetchBonusTypes(), fetchUserBonuses(username), ]); setBonusTypes(types); setUserBonuses(user); } catch (error) { console.error('Ошибка при получении прокачек:', error); if (!isNotificationsEnabled()) return; setNotifMsg('Ошибка при загрузке прокачки!'); setNotifSeverity('error'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); } finally { setBonusesLoading(false); } }; const loadPlayerSkin = async (uuid: string) => { try { const player = await fetchPlayer(uuid); setPlayerSkinUrl(player.skin_url); } catch (error) { console.error('Ошибка при получении скина игрока:', error); setPlayerSkinUrl(''); } }; // Функция для загрузки плащей из магазина const loadStoreCapes = async () => { try { const capes = await fetchCapesStore(); setStoreCapes(capes); } catch (error) { console.error('Ошибка при получении плащей магазина:', error); setStoreCapes([]); } }; // Функция для загрузки плащей пользователя const loadUserCapes = async (username: string) => { try { const userCapes = await fetchCapes(username); setUserCapes(userCapes); } catch (error) { console.error('Ошибка при получении плащей пользователя:', error); setUserCapes([]); } }; const handlePurchaseCape = async (cape_id: string) => { try { await purchaseCape(username, cape_id); await loadUserCapes(username); playBuySound(); if (!isNotificationsEnabled()) return; setNotifMsg('Плащ успешно куплен!'); setNotifSeverity('success'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); } catch (error) { console.error('Ошибка при покупке плаща:', error); if (!isNotificationsEnabled()) return; setNotifMsg('Ошибка при покупке плаща!'); setNotifSeverity('error'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); } }; // Загрузка кейсов 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) { const config = JSON.parse(savedConfig); if (config.uuid && config.username) { setUsername(config.username); setUuid(config.uuid); setLoading(true); Promise.all([ loadStoreCapes(), loadUserCapes(config.username), loadCases(), loadPlayerSkin(config.uuid), loadBonuses(config.username), ]) .catch((err) => console.error(err)) .finally(() => { setLoading(false); }); } } }, []); // Проверяем онлайн после того, как знаем username useEffect(() => { if (username) { checkPlayerStatus(); } }, [username]); const withProcessing = async (id: string, fn: () => Promise) => { setProcessingBonusIds((prev) => [...prev, id]); try { await fn(); } finally { setProcessingBonusIds((prev) => prev.filter((x) => x !== id)); } }; const handlePurchaseBonus = async (bonusTypeId: string) => { if (!username) { if (!isNotificationsEnabled()) return; setNotifMsg('Не найдено имя игрока. Авторизируйтесь в лаунчере!'); setNotifSeverity('error'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); return; } await withProcessing(bonusTypeId, async () => { try { const res = await purchaseBonus(username, bonusTypeId); playBuySound(); await loadBonuses(username); if (!isNotificationsEnabled()) return; setNotifMsg('Прокачка успешно куплена!'); setNotifSeverity('success'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); } catch (error) { console.error('Ошибка при покупке прокачки:', error); if (!isNotificationsEnabled()) return; setNotifMsg('Ошибка при прокачке!'); setNotifSeverity('error'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); } }); }; const handleUpgradeBonus = async (bonusId: string) => { if (!username) return; await withProcessing(bonusId, async () => { try { await upgradeBonus(username, bonusId); await loadBonuses(username); if (!isNotificationsEnabled()) return; setNotifMsg('Бонус улучшен!'); setNotifSeverity('success'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); } catch (error) { console.error('Ошибка при улучшении бонуса:', error); if (!isNotificationsEnabled()) return; setNotifMsg('Ошибка при улучшении бонуса!'); setNotifSeverity('error'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); } }); }; const handleToggleBonusActivation = async (bonusId: string) => { if (!username) return; await withProcessing(bonusId, async () => { try { await toggleBonusActivation(username, bonusId); await loadBonuses(username); } catch (error) { console.error('Ошибка при переключении бонуса:', error); if (!isNotificationsEnabled()) return; setNotifMsg('Ошибка при переключении бонуса!'); setNotifSeverity('error'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); } }); }; const caseServers = Array.from( new Set( (cases || []) .flatMap((c) => c.server_ips || []) .filter(Boolean), ), ); useEffect(() => { if (caseServers.length > 0) { // если игрок онлайн — по умолчанию его сервер, если он есть в кейсах const preferred = playerServer?.ip && caseServers.includes(playerServer.ip) ? playerServer.ip : caseServers[0]; setSelectedCaseServerIp(preferred); } }, [caseServers.length, playerServer?.ip]); const filteredCases = (cases || []).filter((c) => { const allowed = c.server_ips || []; // если список пуст — значит кейс доступен везде if (allowed.length === 0) return true; // иначе — показываем только те, где выбранный сервер разрешён return !!selectedCaseServerIp && allowed.includes(selectedCaseServerIp); }); // Фильтруем плащи, которые уже куплены пользователем const availableCapes = storeCapes.filter( (storeCape) => !userCapes.some((userCape) => userCape.cape_id === storeCape.id), ); const handleOpenCase = async (caseData: Case) => { if (!username) { if (!isNotificationsEnabled()) return; setNotifMsg('Не найдено имя игрока. Авторизуйтесь в лаунчере!'); setNotifSeverity('error'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); return; } if (!selectedCaseServerIp) { if (!isNotificationsEnabled()) return; setNotifMsg('Выберите сервер для открытия кейса!'); setNotifSeverity('warning'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); return; } const allowedIps = caseData.server_ips || []; if (allowedIps.length > 0 && !allowedIps.includes(selectedCaseServerIp)) { if (!isNotificationsEnabled()) return; setNotifMsg( `Этот кейс доступен на: ${allowedIps .map((ip) => translateServer(`Server ${ip}`)) .join(', ')}`, ); setNotifSeverity('warning'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); return; } if (isOpening) return; try { setIsOpening(true); const fullCase = await fetchCase(caseData.id); const caseItems: CaseItem[] = fullCase.items || []; setSelectedCase(fullCase); // ✅ открываем на выбранном сервере (даже если игрок не на сервере) const result = await openCase(fullCase.id, username, selectedCaseServerIp); setRouletteCaseItems(caseItems); setRouletteReward(result.reward); setRouletteOpen(true); playBuySound(); if (!isNotificationsEnabled()) return; setNotifMsg('Кейс открыт!'); setNotifSeverity('success'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); } catch (error) { console.error('Ошибка при открытии кейса:', error); if (!isNotificationsEnabled()) return; setNotifMsg(String(error instanceof Error ? error.message : 'Ошибка при открытии кейса!')); setNotifSeverity('error'); setNotifPos({ vertical: 'top', horizontal: 'center' }); setNotifOpen(true); } finally { setIsOpening(false); } }; const handleCloseNotification = () => { setNotification((prev) => ({ ...prev, open: false })); }; const handleCloseRoulette = () => { setRouletteOpen(false); }; useEffect(() => { const onFirstUserGesture = () => primeSounds(); window.addEventListener('pointerdown', onFirstUserGesture, { once: true }); return () => window.removeEventListener('pointerdown', onFirstUserGesture); }, []); return ( {(loading || onlineCheckLoading) && ( )} {!loading && !onlineCheckLoading && ( {/* Блок прокачки */} Прокачка {bonusesLoading ? ( ) : bonusTypes.length > 0 ? ( {bonusTypes.map((bt) => { const userBonus = userBonuses.find( (ub) => ub.bonus_type_id === bt.id, ); const owned = !!userBonus; const level = owned ? userBonus!.level : 0; const effectValue = owned ? userBonus!.effect_value : bt.base_effect_value; const nextEffectValue = owned && userBonus!.can_upgrade ? bt.base_effect_value + userBonus!.level * bt.effect_increment : undefined; const isActive = owned ? userBonus!.is_active : false; const isPermanent = owned ? userBonus!.is_permanent : bt.duration === 0; const cardId = owned ? userBonus!.id : bt.id; const processing = processingBonusIds.includes(cardId); return ( handlePurchaseBonus(bt.id) : undefined } onUpgrade={ owned ? () => handleUpgradeBonus(userBonus!.id) : undefined } onToggleActive={ owned ? () => handleToggleBonusActivation(userBonus!.id) : undefined } /> ); })} ) : ( Прокачка временно недоступна. )} {/* Блок кейсов */} Кейсы {caseServers.length > 0 && ( Сервер )} {casesLoading ? ( ) : cases.length > 0 ? ( {filteredCases.map((c) => ( handleOpenCase(c)} /> ))} ) : ( Кейсы временно недоступны. )} {/* Блок плащей (как был) */} {/* Блок плащей */} Плащи {availableCapes.length > 0 ? ( {availableCapes.map((cape) => ( handlePurchaseCape(cape.id)} /> ))} ) : ( У вас уже есть все доступные плащи! )} )} {/* Компонент с анимацией рулетки */} {/* Уведомления */} setNotifOpen(false)} autoHideDuration={2500} /> ); }