# Логика работы центральных 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) → формирование команды. - **Запуск игры**: отправка 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 для команд.