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

7.7 KiB
Raw Blame History

Логика работы центральных 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 для команд.