7.7 KiB
7.7 KiB
Логика работы центральных 6 игровых портов в клиентской части на React
Основной компонент портов — GamePort.tsx (рендерится 6 раз в Dashboard для портов 1-6). Каждый порт показывает статус: свободен (серый), игрок готов (зелёный), активная игра (синий/оранжевый/голубой в зависимости от типа). Логика построена на Redux (состояние портов в store/gameSlice), WebSocket (отправка команд на сервер), модалах и drag&drop. Права доступа: оператор/админ — полные, техник — только пристрелки (game=1 или 2, без игроков/обычных игр).
1. Клик по свободному порту (создание новой игры)
- Пользователь (оператор/админ) кликает по порту → проверка: смена открыта? (systemState.smena=true), пауза выключена? (systemState.pause=false), ESP подключён? (systemState.esp !== false).
- Если техник: показывается модал подтверждения "Техническая пристрелка?" → если да, отправка WS:
{do: 'start', line: portNumber, game: 2, patr: 0, gamerId: null, isBonus: false}(запуск пристрелки без игрока). - Если оператор/админ: открывается модал
GameSelectionModal.tsx(onStartGame(portNumber, false)). - В модале:
- Шаг 1: Выбор типа игры (категории хардкод в GAME_CATEGORIES: id=10 "По банкам", id=11 "По мишеням", id=12 "Беспроигрышные"; или бонусные, если isBonus=true).
- Категории берутся из статичного массива (GAME_CATEGORIES).
- Кнопки категорий: градиентные карточки с иконками (fas fa-bullseye и т.д.), счётчиком игр (фильтр по gameInfo.games).
- Если смена не открыта — кнопки заблокированы, сообщение "Откройте смену".
- Выбор категории (handleCategorySelect) → переход к шагу 2, setCurrentStep('game').
- Шаг 2: Выбор конкретной игры (игры из API).
- При открытии модала: fetch('/api/game-info') → gameInfo = {groups: {10: {title: "По банкам"}, ...}, games: {_id: 99, group: 10, patr: 20, title: "Малая призовая", cost: 400, isGameActive: "вкл", ...}}.
- Фильтр: game.group === selectedCategory, isGameActive === "вкл", !isTestGame(_id !==1 && !==2).
- Сортировка по _id, setAvailableGames (requestAnimationFrame для плавности).
- Кнопки игр: карточки с названием, патронами (patr), сложностью (по patr: 1=лёгкая, 2=средняя, 3=сложная), ценой (cost ₽, бесплатно для бонуса).
- Выбор игры (handleGameSelect) → setSelectedGame(game), переход к шагу 3, setCurrentStep('payment').
- Шаг 3: Выбор типа оплаты (если !isBonus).
- Кнопки: "Наличные" (cash, зелёная) или "Безнал" (card, синяя), с иконками (fa-money-bill-wave, fa-credit-card).
- Для бонуса: оплата не нужна, но шаг показывается с "БОНУС".
- Выбор (handlePaymentSelect) → формирование команды.
- Шаг 1: Выбор типа игры (категории хардкод в GAME_CATEGORIES: id=10 "По банкам", id=11 "По мишеням", id=12 "Беспроигрышные"; или бонусные, если isBonus=true).
- Запуск игры: отправка WS (sendMessage):
{do: 'start', line: portNumber, game: selectedGame._id, gamerId: port.gamer.gamerId (или null), isBonus: isBonus, tel: user.phone, patrons: selectedGame.patr, paymentType: 'cash'|'card'}. - Модал закрывается (onClose), порт обновляется из WS-ответа (статус "активная игра", таймер start).
2. Работа с активным портом (во время игры)
- Показ: номер порта, название игры (getGameName: e.g. "Малая призовая" для id=99), тип (бонус/обычная), игрок #gamerId, таймер (useEffect interval 1000ms от startTime), патроны (patr), статистика (patrOk хитов, patrAdd добавленных).
- Изменение патронов: кнопки +/- (если canManageGame, !paused) → WS
{do: 'addp', line: portNumber, patr: +1|-1}. Уведомление "добавлено/убрано 1 патрон". - Кнопки управления (верхний правый угол, если canCloseGame):
- "Перенести игру" (IoSwapHorizontalOutline, если canDragGame): dispatch(gameMovingStarted), вход в режим drag (или кнопочный movingGame=portNumber).
- "Отменить игру" (IoClose): модал ConfirmationModal(type='cancelGame') → WS
{do: 'cancelGame', line: portNumber}. - "Удалить" (IoTrashOutline, для порта с игроком): модал PlayerDeleteModal → WS
{do: 'deleteGamer', gamerId, line: portNumber}.
- Завершение игры: модал ConfirmationModal(type='endGame' или 'forceEndGame') → WS
{do: 'endGame'|'forceEndGame', line: portNumber, priz: true/false}(с призом/без). - Подсветка: ring-4 по типу (зелёный hit, жёлтый shot, красный miss) из port.highlight (из WS).
3. Перенос игры/игрока (drag&drop или кнопка)
- Если movingGame/movingPlayer !== null (из Redux): клик по свободному порту → анимация (createFlyingPortAnimation: полёт от source к target, частицы takeoff), задержка 400ms → WS
{do: 'move', stopLine: from, startLine: to}(для игры) или{do: 'moveGamer', oldLine: from, newLine: to}(для игрока). - Drag: draggable=true (если canDragGame/Player), handleDragStart (set dataTransfer JSON {type, fromPort}), handleDrop (парсинг, анимация, WS).
- Визуал: source — opacity-50 scale-98, target — ring-green bg-green/20, overlay "Переместить сюда".
- Права: оператор — игры (кроме tech=2), игроки; техник — только пристрелки (1/2).
- После: dispatch(movingCancelled), уведомление "Перемещено".
4. Общие проверки и обновления
- WS-ответы обновляют Redux (ports[portNumber]: game, gamer, patr, startTime, etc.).
- Пауза (systemState.pause): все действия заблокированы, grayscale opacity-50.
- Бонус: onStartGame(portNumber, true) → модал с isBonus=true, игры все (без фильтра), бесплатно, paymentType игнорируется.
- Таймер: только для active (diff от startTime).
- Модалы: ConfirmationModal/PlayerDeleteModal — для подтверждений, с деталями (игра, время, счётчик игр).
- Анимации: 3D-эффекты (perspective rotate), glow для игроков, pulse для highlight.
Логика фокусируется на UI (Tailwind, responsive mobile/tablet/desktop), правах ролей и WS-синхронизации с сервером. Нет прямого доступа к серверу в клиенте — только fetch для gameInfo и WS для команд.