import {
Box,
Typography,
Button,
Grid,
FormControl,
Select,
MenuItem,
InputLabel,
Tabs,
Tab,
TextField,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
} 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 {
executePrank,
fetchPrankCommands,
fetchPrankServers,
PrankCommand,
PrankServer,
} from '../api/commands';
import {
isNotificationsEnabled,
getNotifPositionFromSettings,
} from '../utils/notifications';
import { translateServer } from '../utils/serverTranslator';
import { Server } from 'http';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
{value === index && {children}}
);
}
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)'; // сероватый
}
}
const GRADIENT =
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)';
const GLASS_DIALOG_SX = {
borderRadius: '1.4vw',
background:
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.16), transparent 55%), rgba(10,10,20,0.92)',
border: '1px solid rgba(255,255,255,0.10)',
boxShadow: '0 1.6vw 4vw rgba(0,0,0,0.6)',
backdropFilter: 'blur(16px)',
};
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);
// TABS
const [tabValue, setTabValue] = useState(0);
const handleTabChange = (_: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
//команды
const [prankCommands, setPrankCommands] = useState([]);
const [prankServers, setPrankServers] = useState([]);
const [pranksLoading, setPranksLoading] = useState(false);
const [selectedPrankServer, setSelectedPrankServer] = useState('');
const [targetPlayer, setTargetPlayer] = useState('');
const [processingCommandId, setProcessingCommandId] = useState(
null,
);
const [prankDialogOpen, setPrankDialogOpen] = useState(false);
const [selectedPrank, setSelectedPrank] = useState(null);
const [prankTarget, setPrankTarget] = useState('');
const [prankServerId, setPrankServerId] = useState('');
const [prankProcessing, setPrankProcessing] = useState(false);
// Уведомления
const [notification, setNotification] = useState<{
open: boolean;
message: string;
type: 'success' | 'error';
}>({
open: false,
message: '',
type: 'success',
});
const showNotification = (
message: React.ReactNode,
severity: 'success' | 'info' | 'warning' | 'error' = 'info',
position: NotificationPosition = getNotifPositionFromSettings(),
) => {
if (!isNotificationsEnabled()) return;
setNotifMsg(message);
setNotifSeverity(severity);
setNotifPos(position);
setNotifOpen(true);
};
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;
showNotification('Ошибка при загрузке прокачки!', 'error');
} 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([]);
}
};
// Загрузка команд
useEffect(() => {
if (tabValue !== 3) return;
const load = async () => {
try {
setPranksLoading(true);
const [commands, servers] = await Promise.all([
fetchPrankCommands(),
fetchPrankServers(),
]);
setPrankCommands(commands);
setPrankServers(servers);
if (servers.length) setSelectedPrankServer(servers[0].id);
} catch (e) {
console.error(e);
showNotification('Ошибка загрузки пакостей', 'error');
} finally {
setPranksLoading(false);
}
};
load();
}, [tabValue]);
// Функция для загрузки плащей пользователя
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;
showNotification('Плащ успешно куплен!', 'success');
} catch (error) {
console.error('Ошибка при покупке плаща:', error);
if (!isNotificationsEnabled()) return;
showNotification('Ошибка при покупке плаща!', '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) {
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;
showNotification(
'Не найдено имя игрока. Авторизируйтесь в лаунчере!',
'error',
);
return;
}
await withProcessing(bonusTypeId, async () => {
try {
const res = await purchaseBonus(username, bonusTypeId);
playBuySound();
await loadBonuses(username);
if (!isNotificationsEnabled()) return;
showNotification('Прокачка успешно куплена!', 'success');
} catch (error) {
console.error('Ошибка при покупке прокачки:', error);
if (!isNotificationsEnabled()) return;
showNotification('Ошибка при прокачке!', 'error');
}
});
};
const handleUpgradeBonus = async (bonusId: string) => {
if (!username) return;
await withProcessing(bonusId, async () => {
try {
await upgradeBonus(username, bonusId);
await loadBonuses(username);
if (!isNotificationsEnabled()) return;
showNotification('Бонус улучшен!', 'success');
} catch (error) {
console.error('Ошибка при улучшении бонуса:', error);
if (!isNotificationsEnabled()) return;
showNotification('Ошибка при улучшении бонуса!', 'error');
}
});
};
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;
showNotification('Ошибка при переключении бонуса!', 'error');
}
});
};
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;
showNotification(
'Не найдено имя игрока. Авторизуйтесь в лаунчере!',
'error',
);
return;
}
if (!selectedCaseServerIp) {
if (!isNotificationsEnabled()) return;
showNotification('Выберите сервер для открытия кейса!', 'warning');
return;
}
const allowedIps = caseData.server_ips || [];
if (allowedIps.length > 0 && !allowedIps.includes(selectedCaseServerIp)) {
if (!isNotificationsEnabled()) return;
showNotification(
`Этот кейс доступен на: ${allowedIps
.map((ip) => translateServer(`Server ${ip}`))
.join(', ')}`,
'warning',
);
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;
showNotification('Кейс открыт!', 'success');
} catch (error) {
console.error('Ошибка при открытии кейса:', error);
if (!isNotificationsEnabled()) return;
showNotification(
String(
error instanceof Error ? error.message : 'Ошибка при открытии кейса!',
),
'error',
);
} 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 && (
{['Прокачка', 'Кейсы', 'Плащи', 'Предметы'].map((label) => (
))}
{/* Блок прокачки */}
{/*
Прокачка
*/}
{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 > 1 ? (
Выберите сервер:
) : (
Сервер:
{caseServers.map((ip) => (
))}
)}
{casesLoading ? (
) : cases.length > 0 ? (
{filteredCases.map((c) => (
handleOpenCase(c)}
/>
))}
) : (
Кейсы временно недоступны.
)}
{/* Блок плащей */}
Плащи
{availableCapes.length > 0 ? (
{availableCapes.map((cape) => (
handlePurchaseCape(cape.id)}
/>
))}
) : (
У вас уже есть все доступные плащи!
)}
{prankCommands.map((cmd) => (
{/* Картинка */}
{/* Контент */}
{cmd.name}
{cmd.description}
Цена: {cmd.price} монет
))}
)}
{/* Компонент с анимацией рулетки */}
{/* Уведомления */}
setNotifOpen(false)}
autoHideDuration={2500}
/>
);
}