Files
vue-pult/docs/CLIENT_GAMEPORTS_LOGIC.md
2025-10-01 11:54:13 +03:00

53 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Логика работы центральных 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 для команд.