From 8f0524cbb3310b89279e85a1e33a9b7a7ecd972d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=98=D0=9D?= Date: Sat, 30 May 2026 21:46:24 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=BE=D1=80=D1=82=203D-=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B5=D0=BB=D0=BA=D0=B8-=D1=83=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20=D0=B2=20=D0=BF=D0=BB=D0=B5?= =?UTF-8?q?=D0=B5=D1=80=20(=D1=84=D0=B8=D1=87=D0=B0-=D0=BF=D0=B0=D1=80?= =?UTF-8?q?=D0=BD=D0=BE=D1=81=D1=82=D1=8C)=20+=20dev=20JWT-=D0=BF=D0=B0?= =?UTF-8?q?=D0=BD=D0=B5=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - game.fx.pointer + расширенный game.fx.beam: BeamManager (текстуры/curved/ градиент/quest-marker), ScriptSandboxWorker (_normFxPoint от DataCloneError), GameRuntime (fx.createPointer/pointerTarget/pointerUpdate/beamUpdate/ beamVisible), BabylonScene._activatePointers. 1-в-1 со студией. - Dev JWT-панель на экране «Нужен JWT» (только localhost): кнопка → инпут → localStorage.player_jwt + reload. Co-Authored-By: Claude Opus 4.8 --- .env.production | 5 + API_USAGE.md | 104 ++++ src/App.jsx | 3 +- src/KubikonPlayer/GameMenu.jsx | 24 +- src/KubikonPlayer/KubikonPlayer.jsx | 26 +- src/LoadingScreen.jsx | 103 +++- src/engine/BabylonScene.js | 66 +++ src/engine/BeamManager.js | 711 +++++++++++++++++++++++++--- src/engine/GameRuntime.js | 58 +++ src/engine/README.md | 109 +++++ src/engine/ScriptSandboxWorker.js | 70 ++- 11 files changed, 1204 insertions(+), 75 deletions(-) create mode 100644 .env.production create mode 100644 API_USAGE.md create mode 100644 src/engine/README.md diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..59a4c78 --- /dev/null +++ b/.env.production @@ -0,0 +1,5 @@ +VITE_API_BASE=https://minecraftia-school.ru +VITE_REALTIME_HTTP=https://minecraftia-school.ru/api-game +VITE_REALTIME_WS=wss://minecraftia-school.ru/api-game +VITE_RUBLOX_HOME=https://rublox.pro/app +VITE_STANDALONE=false diff --git a/API_USAGE.md b/API_USAGE.md new file mode 100644 index 0000000..53a7f6a --- /dev/null +++ b/API_USAGE.md @@ -0,0 +1,104 @@ +# API, которые использует плеер + +Плеер — это **клиент**. Все данные он берёт с серверных API. Этот документ — полный список эндпоинтов которые он дёргает, зачем и от чего ломается если API изменится. + +## Базовый URL + +В prod: `https://api.rublox.pro` (alias на `minecraftia-school.ru/api-storys`) +В dev: `https://dev-api.rublox.pro` (staging, см. [reference-staging-env]) +Локально: `http://localhost:8674` (storys-микросервис) + +## Эндпоинты — список + +### Игры и лента + +| Method | Path | Когда зовётся | Сломается если... | +|---|---|---|---| +| GET | `/kubikon3d/feed?tab=hot\|new\|popular\|topweek` | при заходе на главную | поле `games[]` отсутствует | +| GET | `/kubikon3d/trending` | главная, секция трендов | возвращает не-массив | +| GET | `/kubikon3d/search?q=` | поиск игр | поле `results[]` отсутствует | +| GET | `/kubikon3d/collections` | главная, секции «Хиты», «Новинки» | возвращает не-массив | +| GET | `/kubikon3d/projects/` | при открытии конкретной игры | поле `project_data` отсутствует или поломан JSON-формат | +| GET | `/kubikon3d/my-projects` | страница «Мои игры» | в ответе нет `projects[]` | +| GET | `/kubikon3d/top-authors` | страница «Топ авторов» | возвращает не-массив | +| GET | `/kubikon3d/events` | главная, баннер мероприятий | возвращает не-массив | + +### Игровой процесс (telemetry) + +| Method | Path | Когда | Что шлёт | +|---|---|---|---| +| POST | `/kubikon3d/play/heartbeat` | каждые 30 сек пока игрок в игре | `{ game_id, play_time_ms }` | +| POST | `/kubikon3d/activity` | при входе/выходе из игры | `{ game_id, action: 'enter'\|'leave' }` | +| POST | `/kubikon3d/perf-log` | при детектировании просадки FPS <20 на 5+ сек | `{ game_id, fps, draw_calls, mem }` | +| POST | `/kubikon3d/bug-reports` | юзер жмёт «Сообщить о баге» | `{ game_id, description, screenshot_b64 }` | +| POST | `/kubikon3d/reports` | юзер репортит игру | `{ game_id, reason, comment }` | + +### Скины и аватары (Рублокс-персонажи) + +| Method | Path | Когда | +|---|---|---| +| GET | `/kubikon3d/rublox/equipped-skin/` | при спавне аватара любого игрока | +| GET | `/kubikon3d/rublox/owned-skins` | страница «Мои скины» | +| POST | `/kubikon3d/rublox/equipped-skin` | юзер сменил скин | +| GET | `/api-storys/rublox/avatars/` | список аватаров юзера | +| GET | `/api-storys/rublox/outfit/` | детали одежды | +| GET | `/kubikon-assets/characters/skins_manifest.json` | при загрузке плеера, список всех доступных скинов | + +### Эмоушены (R15 анимации) + +| Method | Path | Когда | +|---|---|---| +| GET | `/api-storys/rublox/emotes/list` | при заходе в игру (для меню эмоушенов) | +| POST | `/api-storys/rublox/emotes/play/` | юзер выбрал эмоушен | + +### Модели и ассеты + +| Method | Path | Когда | +|---|---|---| +| GET | `/kubikon3d/models/public` | при загрузке игры — список public GLB-моделей | +| GET | `/kubikon3d/models/mine` | в редакторе (плеер запускает превью своих моделей) | +| GET | `/kubikon3d/models/` | при первом упоминании модели в проекте | + +### Админка (только видна юзерам с ролью admin) + +Все `/kubikon3d/admin/*` доступны только если в JWT юзер имеет роль `admin`. Иначе бэк возвращает 403. + +| Method | Path | Что показывает | +|---|---|---| +| GET | `/kubikon3d/admin/dashboard` | сводка: онлайн, активные игры | +| GET | `/kubikon3d/admin/online` | список онлайн-юзеров | +| GET | `/kubikon3d/admin/all-games` | все игры с фильтрами | +| GET | `/kubikon3d/admin/moderation-queue` | очередь премодерации (для review-игр) | +| GET | `/kubikon3d/admin/reports` | репорты на игры | +| GET | `/kubikon3d/admin/bug-reports` | баг-репорты | +| GET | `/kubikon3d/admin/comments` | модерация комментариев | +| GET | `/kubikon3d/admin/chat/messages` | чат-сообщения | +| GET | `/kubikon3d/admin/chat/bans` | список банов в чате | +| GET | `/kubikon3d/admin/authors` | топ авторов с детальной статистикой | + +### Multiplayer (Colyseus) + +WebSocket-соединение, не HTTP. Адрес: `wss://multiplayer.rublox.pro/`. Сейчас работает через `kubikon-realtime` микросервис (VM 110, port 8685, Node.js+Colyseus+Redis). + +## Аутентификация + +Все приватные эндпоинты ожидают **JWT в заголовке** `Authorization: Bearer `. JWT выдаёт `/api-user/auth/login`. Срок жизни access-токена — 1 час, refresh-токена — 30 дней. + +В плеере токен хранится в `localStorage.jwt`, рефрешится автоматически при 401 через `localStorage.refresh_token` → `/api-user/auth/refresh`. + +## Изменения API + +**До publish:** если меняешь сигнатуру эндпоинта (убираешь поле, переименовываешь, меняешь тип) — это **breaking change**. Объявление за 48 часов в канале `#разработка` на https://team.rublox.pro. + +Записывай в `API_CHANGELOG.md` админ-репо (приватный, `minecraftia-school.ru/...`) — это позволяет отследить какие изменения когда были. + +## Что делать если эндпоинт пропал + +1. Открой issue в репо плеера: «API endpoint /xxx not found». +2. Прикрепи console-лог с ошибкой. +3. Кто-то из core-команды посмотрит в `API_CHANGELOG.md` админ-репо и ответит — это было умышленное изменение или баг. + +## Контакты + +- Issue tracker: https://git.rublox.pro/rublox/player/issues +- Чат: `#разработка` на https://team.rublox.pro diff --git a/src/App.jsx b/src/App.jsx index 3c292fa..28433b3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -38,7 +38,8 @@ function PlayerRoute() { return ( ); } diff --git a/src/KubikonPlayer/GameMenu.jsx b/src/KubikonPlayer/GameMenu.jsx index c6cfe92..b6e1119 100644 --- a/src/KubikonPlayer/GameMenu.jsx +++ b/src/KubikonPlayer/GameMenu.jsx @@ -1217,6 +1217,17 @@ function TabReport({ gameId, gameTitle }) { setStatus({ text: 'Отправка...', error: false }); try { const token = getToken(); + // Бэкенд /kubikon3d/reports требует reporter_user_id и target_id и + // принимает поле text. Раньше TabReport слал {title, message, + // game_id, game_title} БЕЗ reporter_user_id → бэк отвечал + // 400 'reporter_user_id required' → жалоба падала. Приводим к + // формату бэкенда (как нижняя кнопка «Жалоба»). + const me = getMyProfile(); + if (!me || !me.id) { + setStatus({ text: 'Войдите, чтобы отправить жалобу.', error: true }); + setSending(false); + return; + } const res = await fetch(`${STORYS_addres}/kubikon3d/reports`, { method: 'POST', headers: { @@ -1224,11 +1235,12 @@ function TabReport({ gameId, gameTitle }) { ...(token ? { Authorization: token } : {}), }, body: JSON.stringify({ + reporter_user_id: me.id, + target_type: 'project', + target_id: gameId || null, category, - title: title.trim(), - message: message.trim(), - game_id: gameId || null, - game_title: gameTitle || null, + text: title.trim() + '\n\n' + message.trim() + + (gameTitle ? `\n\n(игра: ${gameTitle})` : ''), }), }); if (res.ok) { @@ -1236,7 +1248,9 @@ function TabReport({ gameId, gameTitle }) { setTitle(''); setMessage(''); } else { - setStatus({ text: 'Не удалось отправить. Попробуйте позже.', error: true }); + let detail = ''; + try { const j = await res.json(); if (j && j.error) detail = ': ' + j.error; } catch (e) {} + setStatus({ text: 'Не удалось отправить' + detail + '.', error: true }); } } catch (e) { setStatus({ text: 'Сеть недоступна.', error: true }); diff --git a/src/KubikonPlayer/KubikonPlayer.jsx b/src/KubikonPlayer/KubikonPlayer.jsx index c4b6540..7ac0043 100644 --- a/src/KubikonPlayer/KubikonPlayer.jsx +++ b/src/KubikonPlayer/KubikonPlayer.jsx @@ -505,22 +505,26 @@ const KubikonPlayer = () => { }); scene.setOnPlayChange?.((playing) => { setIsPlaying(playing); - // ESC обрабатывается через pointerlockchange-перехват в плеере - // (см. отдельный useEffect ниже). Сюда мы попадаем только если - // exitPlayMode вызвался по другой причине — тогда просто открываем - // меню, чтобы пользователь мог выйти/вернуться, и пересоздаём Play - // в UI-cursor режиме. + // ВНИМАНИЕ: при обычном ESC сюда мы больше НЕ попадаем — ESC теперь + // открывает меню через setOnEscMenu (ниже), не выходя из Play. + // exitPlayMode(false) случается только по-настоящему (напр. движок + // сам остановил Play). В этом случае просто открываем меню, чтобы + // юзер мог выйти/перезапустить. НЕ пересоздаём Play автоматически — + // повторный enterPlayMode респавнил игрока и перезапускал скрипты + // («перезапуск плейса» при ESC). Перезапуск делается явной кнопкой. if (!playing) { - setTimeout(() => { - const s = sceneRef.current; - if (!s) return; - s.enterPlayMode?.(); - s.player?.setUiCursorMode?.(true); - }, 30); + const s = sceneRef.current; + s?.player?.setUiCursorMode?.(true); setChatOpen(false); setTopMenuOpen(true); } }); + // ESC в Play → меню-оверлей поверх ЖИВОЙ игры (Roblox-style). Play не + // прерывается, скрипты продолжают идти, игрок не респавнится. + scene.setOnEscMenu?.(() => { + setChatOpen(false); + setTopMenuOpen(true); + }); // Загружаем проект. // STANDALONE-режим (VITE_STANDALONE=true) подсовывает встроенный diff --git a/src/LoadingScreen.jsx b/src/LoadingScreen.jsx index 0cde05b..73ce609 100644 --- a/src/LoadingScreen.jsx +++ b/src/LoadingScreen.jsx @@ -9,11 +9,62 @@ // // CSS-анимации, без JS-фрейма каждый кадр. -import React from 'react'; +import React, { useState } from 'react'; const TITLE = 'Рублокс'; -export default function LoadingScreen({ text = 'Подключение', subText = null }) { +/** + * Dev-only панель вставки JWT. Показывается на экране «Нужен JWT» (только + * localhost). Кнопка → инпут → сохраняет в localStorage['player_jwt'] и + * перезагружает страницу. На проде этот экран не наступает (там redirect). + */ +function DevJwtPanel() { + const [open, setOpen] = useState(false); + const [val, setVal] = useState(''); + + const apply = () => { + const t = (val || '').trim(); + if (!t) return; + try { + localStorage.setItem('player_jwt', t); + // совместимость с другими местами чтения токена + localStorage.setItem('Authorization', t); + } catch (e) { /* ignore */ } + window.location.reload(); + }; + + if (!open) { + return ( + + ); + } + return ( +
+