commit 31adbf151b3a38bb2405cb1af80c0ba8caae3a20 Author: МИН Date: Wed May 27 23:41:10 2026 +0300 Initial public release: Студия Рублокса v1.0 Open-source веб-студия для создания игр Рублокса, двойная лицензия AGPL-3.0 + Коммерческая. Главное: - Vite 5 + React 18 + Babylon 7.54.3 + Monaco Editor + Colyseus 0.16 - Самодостаточный движок ~28к строк (66 файлов): BlockManager, TerrainVoxelBuilder, ModelManager, DecoManager, PlayerController, ScriptSandboxWorker, MultiplayerSync, 30+ GD-гейммодов - Главный редактор KubikonEditor (~37к строк) + панели, ScriptEditor (Monaco) - Витрина игр (KubikonFeed, KubikonStudio, KubikonDocs, KubikonLearn) - Geometry Dash sub-app (GdMenu, GdShop, GdRules, GdCoverArt) - 10 admin-preview каталогов для дизайнеров (скины, музыка, порталы и т.д.) - Конфигурируемый бэкенд через VITE_API_BASE — работает со staging (dev-api.rublox.pro) без настройки - Standalone-режим (VITE_STANDALONE=true) — открыть пустой редактор без бэка - Полная документация (на русском): README, ARCHITECTURE, CONTRIBUTING, SECURITY, CHANGELOG - ESLint + Prettier + EditorConfig - Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md - Issue templates: bug_report, feature_request, security_disclosure Перед публикацией: - Все импорты из minecraftia заменены на локальные - Все хардкоды URL (minecraftia-school.ru) и внутренних IP убраны → env - Admin-эндпоинты Kubikon3DService вырезаны (остаются в приватном репо) - AdminKubikonModeration не публикуется (модерация — в team.rublox.pro) - 93 МБ ассетов public/kubikon-assets вынесены в .gitignore (раздаются через release artifact) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..86e884b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..90d7fc2 --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# ============================================================================ +# Переменные окружения rublox-studio +# ============================================================================ +# Скопируй этот файл в .env (`cp .env.example .env`) и поправь. +# Дефолты указывают на публичный staging — студия заработает сразу. + +# Базовый URL HTTP-API. +# Оставь пустым чтобы использовать vite-proxy в dev (http://localhost:5174 → staging). +VITE_API_BASE= + +# (только dev) куда vite-proxy шлёт /api-* запросы. +VITE_API_PROXY_TARGET=https://dev-api.rublox.pro + +# Colyseus realtime (мультиплеер). +VITE_REALTIME_HTTP=https://dev-api.rublox.pro/api-game +VITE_REALTIME_WS=wss://dev-api.rublox.pro/api-game + +# Главный сайт Рублокса и плеер. +VITE_RUBLOX_HOME=https://rublox.pro/app +VITE_PLAYER_URL=https://player.rublox.pro + +# Standalone-режим — открывает пустой редактор без бэкенда (для первого запуска). +# Save-кнопка отключена. Все API-вызовы возвращают моки. +# Значения: "true" | "false" +VITE_STANDALONE=false diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..c510105 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,45 @@ +{ + "root": true, + "env": { + "browser": true, + "es2022": true, + "node": true, + "worker": true + }, + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "settings": { + "react": { + "version": "detect" + } + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:react-hooks/recommended" + ], + "rules": { + "no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], + "no-console": "off", + "react/prop-types": "off", + "react/react-in-jsx-scope": "off", + "react/display-name": "off", + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "warn", + "no-eval": "error", + "no-new-func": "error", + "no-implied-eval": "error" + }, + "ignorePatterns": [ + "build/", + "dist/", + "node_modules/", + "public/kubikon-assets/", + "*.config.js" + ] +} diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.md b/.gitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b79daec --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,47 @@ +--- +name: 🐛 Сообщить о баге +about: Что-то работает не так как должно +title: '[BUG] ' +labels: bug +assignees: '' +--- + +## Описание + + + +## Шаги для воспроизведения + +1. +2. +3. + +## Ожидаемое поведение + + + +## Фактическое поведение + + + +## Скриншоты / видео + + + +## Окружение + +- ОС: +- Браузер + версия: +- Node.js + npm версия: +- Git-ветка / коммит: +- Окружение: [ ] localhost [ ] staging [ ] prod + +## Логи / стек-трейс + +``` +вставь сюда +``` + +## Дополнительный контекст + + diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.md b/.gitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..8d60472 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,37 @@ +--- +name: 💡 Предложить фичу +about: Идея новой функциональности или улучшения +title: '[FEATURE] ' +labels: enhancement +assignees: '' +--- + +## Проблема, которую решает фича + + + +## Предлагаемое решение + + + +## Альтернативы, которые рассматривал + + + +## Mockup / схема (опционально) + + + +## Влияние на другие части системы + +- [ ] Изменяет API +- [ ] Требует миграции БД +- [ ] Меняет UX-flow существующих фич +- [ ] Требует изменений в плеере / студии / минке + +## Готов реализовать сам? + +- [ ] Да, открою PR +- [ ] Нет, прошу команду + +## Дополнительный контекст diff --git a/.gitea/ISSUE_TEMPLATE/security_disclosure.md b/.gitea/ISSUE_TEMPLATE/security_disclosure.md new file mode 100644 index 0000000..1ad0c55 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/security_disclosure.md @@ -0,0 +1,45 @@ +--- +name: 🔒 Уязвимость безопасности (ПРИВАТНО) +about: НЕ публикуй уязвимости здесь — пиши на security@rublox.pro +title: '[SECURITY] Не публикуй детали публично' +labels: security +assignees: '' +--- + +## ⚠️ СТОП + +**Если ты нашёл уязвимость безопасности — НЕ публикуй детали в публичном issue.** + +Уязвимости в open-source проекте могут быть использованы злоумышленниками против работающего prod-сервиса до того, как мы успеем выпустить патч. + +## Куда писать + +📧 Email: **security@rublox.pro** (читает только Лид) + +В письме укажи: +1. Тип уязвимости (XSS, SQL-injection, RCE, IDOR, auth-bypass, etc.) +2. Шаги воспроизведения +3. Уровень воздействия (что злоумышленник может сделать) +4. Твой контакт для уточнений +5. Хочешь ли ты публичную благодарность после фикса (Hall of Fame) + +## Что ты получишь в ответ + +- Подтверждение получения — в течение 24 часов +- Оценка серьёзности и план исправления — в течение 3 рабочих дней +- Уведомление о выпуске патча +- Публичное упоминание в CHANGELOG (если хочешь) +- Для критических уязвимостей — символическое вознаграждение (по договорённости) + +## Что НЕ считать уязвимостью + +- Отсутствие rate-limit на публичных эндпоинтах документации +- Mising HSTS / CSP headers (мы знаем, работаем над этим) +- Self-XSS (требует чтобы юзер сам выполнил JS у себя в DevTools) +- Уязвимости в third-party библиотеках (репортить туда напрямую) + +--- + +## Если ты ошибся и это НЕ безопасность + +Закрой этот issue и открой новый по шаблону **🐛 Сообщить о баге** или **💡 Предложить фичу**. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..209dc22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +node_modules/ +dist/ +dist-ssr/ +build/ + +# Environment (НИКОГДА не коммитить .env, только .env.example) +.env +.env.local +.env.*.local +*.local + +# Editor +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Локальные заметки и приватные deploy-скрипты +disaster-recovery/ +*.local.md +NOTES.md + +# Большие бинарные ассеты (93 МБ GLB/PNG/MP3) — раздаются отдельно через release. +public/kubikon-assets/ + +# OS +Thumbs.db diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..650be1a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +build/ +dist/ +node_modules/ +public/kubikon-assets/ +package-lock.json +*.glb +*.png +*.jpg +*.jpeg +*.mp3 +*.wav +*.ico diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8202a4e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..617028f --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,165 @@ +# Архитектура студии Рублокса + +Как редактор превращает действия мыши в воксельный мир, GLB-модели, скрипты на JS — и сохраняет в JSON для запуска в плеере. Чтение ~10 минут. + +## Общий поток (открытие проекта) + +``` +URL = /edit/ + │ + ▼ +AuthProvider читает JWT из localStorage + │ + ▼ +KubikonEditor.jsx (~3500 строк) главный контейнер, useParams().id = projectId + │ + ├── GET /api-storys/kubikon3d/projects/{id} → project_data (JSON) + │ + ▼ +BabylonScene.create() создание Babylon engine, scene, lights, skybox + │ + ▼ +GameRuntime.loadProject() парсит project_data, спавнит: + │ - BlockManager.placeBlock() × N (воксели) + │ - ModelManager.spawnModel() × N (GLB Kenney) + │ - DecoManager.placeDeco() × N (трава, камни) + │ - PrimitiveManager.add() × N (box/sphere/cylinder) + │ - PlayerController.spawn() (камера-«редактор») + │ + ▼ +ОСНОВНОЙ UI на сцене: + │ - TopRibbon (тулбар) + │ - HierarchyPanel (дерево объектов слева) + │ - InspectorPanel (свойства справа) + │ - TerrainPanel (кисти ландшафта) + │ - ScriptEditor (Monaco) + │ - ToolboxModal (выбор моделей) + │ - GuiOverlay/Hotbar (HUD режима игры) + │ + ▼ +ЦИКЛ РЕДАКТИРОВАНИЯ юзер: + - кликает мышью → SelectionManager + - drag-and-drop → GizmoManager + - переключает кисти → TerrainManager + - пишет скрипт → ScriptEditor + Babylon-эмулятор + - жмёт «Сохранить» → POST /kubikon3d/projects/:id + - жмёт «Тест» → embedded preview-player +``` + +## Главные модули редактора + +### `editor/KubikonEditor.jsx` (3452 строки) + +Корневой компонент. Хранит ссылки на scene, engine, runtime, managers. Обрабатывает горячие клавиши, undo/redo через `HistoryManager`. Обвязывает все панели вокруг ``. + +### `editor/engine/` (66 файлов, ~28к строк) + +Самодостаточный Babylon-движок. Точно такой же как в плеере, но с дополнительными редакторскими функциями: + +| Менеджер | Файл | Что делает | +|---|---|---| +| `BabylonScene` | `BabylonScene.js` | Wrapper над Engine+Scene, освещение | +| `GameRuntime` | `GameRuntime.js` | Оркестратор: loadProject/start/pause/dispose | +| `PlayerController` | `PlayerController.js` | R15-персонаж, камеры FPV/TPV, контролы | +| `BlockManager` | `BlockManager.js` | Воксельные блоки uint16-сетка | +| `TerrainVoxelBuilder` | `terrain/*` | Greedy meshing для чанков | +| `ModelManager` | `ModelManager.js` | Загрузка GLB через AssetContainer + кеш | +| `DecoManager` | `DecoManager.js` | ThinInstances для тысяч пропсов | +| `PrimitiveManager` | `PrimitiveManager.js` | Box/sphere/cylinder примитивы | +| `SelectionManager` | `SelectionManager.js` | Клик → выделение → highlight | +| `GizmoManager` | `GizmoManager.js` | Стрелки перемещения/вращения/масштаба | +| `HistoryManager` | `HistoryManager.js` | Undo/redo стек | +| `ScriptSandbox` | `scripts/ScriptSandbox.js` | Запуск user JS в Web Worker | +| `MultiplayerSync` | `multiplayer/MultiplayerSync.js` | Colyseus 0.16 клиент | +| `Audio/Weapon/Zombie/Npc/Dynamics` | `*Manager.js` | Игровые менеджеры | + +### `editor/engine/scripts/ScriptSandbox*.js` + +Безопасный JS-runtime для пользовательских скриптов. Запускается в Web Worker, без доступа к window/document/fetch. Через postMessage-мост даёт API: + +```js +game.onTick((dt) => { ... }); +game.onKey('space', () => { player.jump(); }); +scene.findOne('Cube1').rotateY(0.1); +ui.set({ score: 42 }); +``` + +### `editor/engine/types/` + +TypeScript-определения для Monaco-автокомплита. Когда юзер пишет скрипт — IDE показывает все доступные методы. + +## UI-панели + +``` ++----------------------------------------------------------+ +| TopRibbon: Файл | Правка | Тест | Опубликовать | Помощь | ++--------+-----------------------------------+-------------+ +| Hierar | | Inspector | +| chyPan | | Panel | +| el | | (свойства | +| | | выделен- | +| Дерево | | ного) | +| объек- | | | +| тов | | | +| | | | +| +-----------------------------------+ | +| | Hotbar / TerrainPanel (внизу) | ++--------+-------------------------------------------------+ +``` + +## Скриптовый редактор (Monaco) + +`editor/ScriptEditor.jsx` запускает Monaco (~5МБ vendor chunk, lazy-loaded). Подгружает .d.ts из `engine/types/` для автокомплита. При сохранении скрипт идёт в `ScriptSandbox`, который запускает Worker и шлёт результат обратно. + +## Поток данных при сохранении + +``` +Save кнопка → собрать снимок: + - blocks[] (uint16 array → RLE-compress → base64) + - models[] {id, type, x, y, z, rx, ry, rz} + - primitives[] {id, type, color, size} + - deco[] {type, instances[]} + - script.code (текст из Monaco) + - settings {sky, fog, multiplayer, isGd} + - spawnPoint {x, y, z} +→ JSON.stringify +→ PUT /api-storys/kubikon3d/projects/ { project_data: "" } +``` + +## Чего НЕТ в студии + +- **Само воспроизведение игры в проде** — это [Плеер](https://git.rublox.pro/rublox/player) (отдельный домен `player.rublox.pro`). В студии есть `preview-player/` для теста «прямо здесь». +- **Лента / поиск опубликованных игр** — на главном сайте `rublox.pro/app`. +- **Админка модерации** — в `team.rublox.pro/moderator/*` (приватный фронт команды). + +## С чего начать новую фичу + +| Что хочешь добавить | Начни здесь | +|---|---| +| Новый тип блока | `editor/engine/CONST/blockTypes.js` | +| Новая декор-фабрика | `admin-preview/gd<тип>/` (фабрика) + регистрация в DecoManager | +| Новое API скриптов | `editor/engine/scripts/ScriptSandboxAPI.js` + .d.ts в `engine/types/` | +| Новая GD-механика | `editor/engine/Gd<тип>.js` + регистрация в `GdGameModeRegistry.js` | +| Новый UI-блок инспектора | `editor/InspectorPanel.jsx` | +| Новая кисть ландшафта | `editor/TerrainPanel.jsx` + `TerrainManager.brush*` | + +## Производительность + +Главные узкие места (что смотреть первым делом): + +1. **`game.ui.set()` каждый кадр** — React setState 60Hz убивает FPS. Дросселируй 250мс + diff. +2. **`scene.findOne()` на старте скрипта** — sceneSnapshot приходит через rAF. Звать в `onTick` или setTimeout(0). +3. **`blockMaterialDirtyMechanism=true`** в Babylon — НЕ включать (ломает рендер новых мешей). +4. **GLB через `SceneLoader` вместо `AssetContainer`** — течёт материалами. +5. **Monaco-instance создаётся повторно при ремаунте** — закешируй ссылку. +6. **Воксельный rebuild на каждый блок** — батчить через requestIdleCallback. + +## Связь со студией / плеером / минкой + +- **Бэкенд один на всех** — `api-storys` микросервис (см. `disaster-recovery/` в приватном репо мейнтейнера). +- **Авторизация общая** — JWT с rublox.pro и player.rublox.pro работают здесь. +- **API-контракт описан в `api/Kubikon3DService.js`** — все эндпоинты `/kubikon3d/*`. + +## Лицензионные заметки для контрибьюторов + +Контрибьютя, ты соглашаешься лицензировать свои изменения под AGPL-3.0 И предоставляешь мейнтейнеру неисключительную безотзывную лицензию на сублицензирование (см. [CLA.md](./CLA.md)). Это нужно чтобы проект мог продавать коммерческие лицензии корпорациям. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f412fc7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,37 @@ +# Changelog + +Все значимые изменения документируются здесь. + +Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/). + +## [Unreleased] + +### Добавлено +- Open-source релиз: двойная лицензия AGPL-3.0 + Коммерческая. +- Выделен из `minecraftia-school.ru/Фронтенд/React/src/pages/KubikonEditor` + и связанных папок (KubikonPlayer, KubikonStudio, GdShop, AdminPreview/gd*). +- Перевод с Create React App на Vite 5 (быстрая dev-сборка, HMR). +- Standalone-режим (`VITE_STANDALONE=true`) — открывает пустой редактор без бэкенда. +- Конфигурируемый бэкенд через `VITE_API_BASE`, `VITE_RUBLOX_HOME`, `VITE_PLAYER_URL`. +- Свой роутер с 25+ роутами (раньше встраивался в Menu.jsx минки). +- Лёгкие версии AuthContext / SanctionsContext без зависимости от UserService минки. +- Шаблоны Issue и PR в `.gitea/`. +- `CONTRIBUTING.md`, `SECURITY.md`, `ARCHITECTURE.md`, `CLA.md`. +- ESLint + Prettier + EditorConfig. + +### Удалено +- Все импорты из minecraftia (`API/`, `context/`, `components/UI/`) → заменены на локальные. +- Захардкоженные URL `minecraftia-school.ru` (все теперь через env). +- Внутренние IP серверов (`85.175.x.x`, `192.168.x.x`) из комментариев. +- Admin-эндпоинты из `Kubikon3DService.js` — вынесены в приватный репо. +- `AdminKubikonModeration.jsx` (модерация контента) — остаётся в `team.rublox.pro`. + +### Безопасность +- Чистый `git init` (без истории минки, которая содержала credentials). +- Pre-commit hook блокирует пуш файлов с паттернами секретов. + +## [0.1.0] — начальный приватный билд (до публикации) + +- CRA + React 18 + Babylon 7.54.3 базовая сборка (в составе minecraftia). +- Редактор воксельного ландшафта, GLB-моделей, скриптов на JS. +- 30+ Geometry Dash гейммодов. diff --git a/CLA.md b/CLA.md new file mode 100644 index 0000000..c90bfe2 --- /dev/null +++ b/CLA.md @@ -0,0 +1,134 @@ +# Contributor License Agreement (CLA) + +**Лицензионное соглашение с контрибьютором** + +Версия: 1.0 +Дата вступления в силу: 2026-05-27 + +--- + +## Преамбула + +Спасибо за желание внести вклад в проекты Рублокса (rublox-player, rublox-studio и связанные репозитории). + +Чтобы правообладатель проекта мог использовать ваш вклад без юридических ограничений (включая возможность распространять проект под двойной лицензией AGPL-3.0 + Commercial), необходимо подписать настоящее CLA. + +Это стандартная практика крупных open-source проектов (Apache Foundation, Google, Microsoft, Notion и др.). + +--- + +## Стороны + +**Правообладатель:** +Индивидуальный предприниматель Иванкова Виктория Сергеевна +ОГРНИП: 322237500039230 +ИНН: 233507286445 +Адрес: Краснодарский край, г. Кореновск, ул. Мира 93 +Email: maksimivankov26@yandex.ru + +**Контрибьютор:** +Вы — физическое или юридическое лицо, подписывающее это соглашение через систему team.rublox.pro путём нажатия кнопки «Я ознакомился и согласен с CLA» либо комментарием `/sign-cla` в Gitea. + +--- + +## 1. Определения + +- **«Вклад»** (Contribution) — любой исходный код, документация, графика, конфигурация или иной материал, который вы отправляете в репозиторий правообладателя через Pull Request, патч, issue, комментарий или иным способом. +- **«Проект»** — программное обеспечение, репозитории и сопутствующие материалы, размещённые в организации `rublox` на git.rublox.pro и связанных платформах. + +--- + +## 2. Передаваемые права + +Подписывая это CLA, вы предоставляете правообладателю: + +### 2.1. Лицензия на использование (Copyright License) + +Неисключительную, бессрочную, безотзывную, всемирную, безвозмездную, сублицензируемую и передаваемую лицензию на: + +- Воспроизведение, изменение, отображение, распространение вашего Вклада; +- Включение Вклада в производные работы; +- **Распространение Вклада под любой лицензией по выбору правообладателя, включая, но не ограничиваясь:** + - GNU Affero General Public License v3.0; + - Любую коммерческую (проприетарную) лицензию; + - Любую другую open-source лицензию (MIT, Apache 2.0 и т.д.). + +### 2.2. Лицензия на патенты (Patent License) + +Если ваш Вклад содержит реализацию запатентованной вами технологии — вы предоставляете правообладателю и пользователям проекта неисключительную, бессрочную, безотзывную, безвозмездную лицензию на использование, изготовление и продажу этих патентов в составе проекта. + +--- + +## 3. Ваши гарантии + +Подписывая CLA, вы заявляете и гарантируете, что: + +1. **Вам исполнилось 18 лет** (подписывать CLA от лица несовершеннолетнего могут только родители/законные представители, и таким контрибьюциям мы предпочитаем отказывать). +2. Вы имеете полное право предоставлять перечисленные в разделе 2 лицензии. +3. Ваш Вклад является вашей оригинальной работой ИЛИ корректно атрибутирован к источнику и совместим с лицензией проекта. +4. Ваш Вклад не нарушает права третьих лиц (включая авторские права, патенты, торговые знаки, коммерческие тайны). +5. Если вы работаете по найму или контракту и ваш работодатель имеет права на ваши разработки — вы получили разрешение работодателя на этот вклад. При необходимости работодатель подписывает [Corporate CLA](#corporate-cla) отдельно. +6. Вы понимаете, что после слияния (merge) ваш Вклад становится частью проекта и не может быть отозван в одностороннем порядке. + +--- + +## 4. Отказ от гарантий + +Вы предоставляете Вклад «как есть», без каких-либо явных или подразумеваемых гарантий, включая, но не ограничиваясь, гарантии товарного качества, пригодности для конкретной цели и ненарушения прав. + +--- + +## 5. Применимое право + +Это CLA регулируется законодательством Российской Федерации. Все споры подлежат разрешению в судах по месту нахождения правообладателя (Краснодарский край). + +--- + +## 6. Версионирование CLA + +Правообладатель оставляет за собой право обновлять текст CLA. При публикации новой версии действующие контрибьюторы будут уведомлены и должны подписать новую версию для дальнейшего внесения вкладов. Уже принятые Вклады остаются под действовавшей на момент их merge версии CLA. + +Текущая версия и история изменений: https://git.rublox.pro/rublox/legal/blob/main/CLA.md + +--- + +## 7. Corporate CLA + +Если вы работаете в компании и она имеет права на ваши разработки, необходимо отдельное соглашение между компанией и правообладателем. Свяжитесь по адресу `maksimivankov26@yandex.ru` с темой "Corporate CLA Request". + +--- + +## Подписание + +Подписать CLA можно одним из способов: + +### Способ 1: Через team.rublox.pro +1. Войдите в https://team.rublox.pro +2. Откройте `/developer/cla` +3. Внимательно прочитайте текст +4. Нажмите «Я ознакомился и согласен подписать CLA версии 1.0» +5. Подпись с указанием вашего ID, IP, времени и user-agent сохраняется в базе + +### Способ 2: Через комментарий в PR +1. Откройте свой Pull Request в Gitea +2. Напишите комментарий: `/sign-cla` +3. Бот проверит вашу учётку в team.rublox.pro и зарегистрирует подпись + +### Способ 3: По email (для Corporate CLA) +Напишите на `maksimivankov26@yandex.ru` с темой "CLA Request — [Ваше имя/Компания]" и подписанным PDF-сканом. + +--- + +## Контрольный список перед подписанием + +Перед нажатием «Я согласен» убедитесь, что: + +- [ ] Вы прочитали и поняли текст CLA полностью +- [ ] Вам исполнилось 18 лет +- [ ] У вас есть права на код, который вы планируете контрибьютить +- [ ] Если у вас есть работодатель/контракт — вы проверили, что можете передавать права на свой код +- [ ] Вы понимаете, что AGPL-3.0 — copyleft-лицензия, но правообладатель может распространять ваш вклад также и под коммерческой лицензией + +--- + +*Этот документ адаптирован на основе Apache Software Foundation Individual Contributor License Agreement v2.0 и приведён в соответствие с законодательством Российской Федерации.* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9b9f055 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,150 @@ +# Контрибьютинг в студию Рублокса + +Спасибо за интерес! Всё что нужно знать. + +## Перед первым PR + +1. **Подпиши [CLA](./CLA.md)** — открой `https://team.rublox.pro/developer/cla` (нужна роль `developer` — попроси у мейнтейнера) ИЛИ комментарий `/sign-cla` на PR. +2. **Заведи Gitea-аккаунт** через OAuth-привязку к team.rublox.pro. +3. **Добавь SSH-ключ** в профиль Gitea (иначе пуш по HTTPS + пароль). + +## Workflow + +```bash +# 1. Клонируем (или форкаем — если есть доступ, можно сразу пушить в feature-ветку) +git clone ssh://git@git.rublox.pro:2222/rublox/studio.git +cd player +npm install +cp .env.example .env + +# 2. Создаём feature-ветку +git checkout -b feature/короткое-описание + +# 3. Делаем изменения +npm run dev # проверяем локально +npm run lint # ESLint +npm run format # Prettier + +# 4. Коммитим (Conventional Commits) +git commit -m "feat: добавить анимацию прыжка для GdBall" +# Другие префиксы: fix:, chore:, docs:, refactor:, test:, perf:, ci: + +# 5. Пушим и открываем PR +git push origin feature/короткое-описание +# Открыть PR через https://git.rublox.pro/rublox/studio +``` + +## Стиль кода + +Используем **Prettier** (авто-формат) + **ESLint** (линт). Конфиги закоммичены в `.prettierrc` и `.eslintrc.json`. + +Главные правила: +- 2-space отступ +- Одинарные кавычки (`'foo'`, не `"foo"`) +- Точки с запятой обязательны +- Trailing comma в многострочных литералах +- Ширина строки 100 символов +- Никаких `eval`, `new Function`, `innerHTML = ...` +- React: hooks-правила обязательны (`react-hooks/rules-of-hooks`) + +Запускай перед коммитом: +```bash +npm run format # авто-фикс большинства +npm run lint # предупреждения для остального +``` + +Pre-commit-хук (Husky, если установлен) заблокирует если Prettier-чек провален. + +## Именование веток + +| Префикс | Использовать для | +|---|---| +| `feature/...` | Новые фичи | +| `fix/...` | Багфиксы | +| `chore/...` | Рефакторинг, зависимости, тулинг | +| `docs/...` | Только документация | +| `perf/...` | Улучшения производительности | + +## Commit-сообщения (Conventional Commits) + +``` +<тип>: <короткое описание> + +[опционально тело] + +[опционально футер: BREAKING CHANGE / Closes #N] +``` + +Типы: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `perf`, `ci`, `style`. + +**Хорошо:** +- `feat: добавить WaveMode в GdGameModeRegistry` +- `fix: ScriptSandbox падает при таймауте Worker` +- `perf: дросселировать ui.set() до 250мс с diff-проверкой` + +**Плохо:** +- `update code` +- `wip` +- `пофиксил всякое` + +## Шаблон PR + +При открытии PR заполни: + +```markdown +## Что + +(1-2 предложения: что делает этот PR?) + +## Зачем + +(мотивация: репорт бага, фича-реквест, связанный issue #N) + +## Как протестить + +(воспроизводимые шаги для ревьюера) + +## Скриншоты / видео (если UI-изменение) + +## Чек-лист + +- [ ] `npm run lint` проходит +- [ ] `npm run format:check` проходит +- [ ] `npm run build` собирается +- [ ] Протестил локально через `npm run dev` +- [ ] Обновил соответствующие доки (README / ARCHITECTURE / CHANGELOG) +- [ ] CLA подписан +``` + +## Процесс ревью + +- Мейнтейнер репо (МИН) ревьюит каждый PR. +- Ожидай фидбек в течение **48 часов** (часто в тот же день). +- Маленькие PR (<300 строк) ревьюятся быстро. PR'ы >1000 строк попросят раздробить. +- После approval **мерджит мейнтейнер** (право merge только у него). +- После merge автоматический webhook запускает деплой в прод. + +## Что не смерджим + +- PR с новыми внешними зависимостями без обсуждения +- PR трогающие чувствительные пути (`engine/scripts/ScriptSandbox*.js`, `engine/multiplayer/*`) без security-ревью +- PR ломающие сборку или линт +- PR от контрибьюторов без подписанного CLA +- Массивные рефакторинги без заранее обсуждённого tracking-issue + +## Что любим + +- Багфиксы с воспроизводимыми тест-кейсами +- Улучшения производительности с before/after метриками +- Новые `engine/gd/Gd*.js` гейммоды +- Новые API для скриптов (в `ScriptSandboxAPI.js`) +- Улучшения документации (особенно с примерами) + +## Безопасность + +Нашёл уязвимость? **Не открывай публичный issue.** Пиши на `security@rublox.pro` напрямую. См. [SECURITY.md](./SECURITY.md). + +## Вопросы? + +- Открой issue с лейблом **Question** +- Или приходи в канал `#разработка` на https://team.rublox.pro/chat diff --git a/COPYRIGHT.md b/COPYRIGHT.md new file mode 100644 index 0000000..d2c6794 --- /dev/null +++ b/COPYRIGHT.md @@ -0,0 +1,62 @@ +# Copyright + +Copyright © 2026 Иванкова Виктория Сергеевна (ИП) + +**Контактные данные правообладателя:** +- ИП Иванкова Виктория Сергеевна +- ОГРНИП: 322237500039230 +- ИНН: 233507286445 +- Адрес: Краснодарский край, г. Кореновск, ул. Мира 93 +- Email: maksimivankov26@yandex.ru + +--- + +## Лицензирование + +Этот проект распространяется под **двойной лицензией**: + +### 1. AGPL-3.0-or-later (для open-source использования) + +См. файл [LICENSE](./LICENSE) — полный текст GNU Affero General Public License v3.0. + +Кратко: вы можете свободно использовать, изменять и распространять этот код, при условии что: +- Производные работы и форки распространяются под той же AGPL-3.0 лицензией; +- Исходный код всех модификаций доступен пользователям, в том числе при использовании по сети (SaaS); +- Сохраняется указание авторства и копирайта. + +### 2. Commercial License (для проприетарного использования) + +Если вы хотите использовать этот код в проприетарном (закрытом) продукте, в коммерческом SaaS без раскрытия исходников модификаций, или иным образом несовместимым с AGPL — приобретите коммерческую лицензию. + +См. [LICENSE-COMMERCIAL.md](./LICENSE-COMMERCIAL.md) для подробностей и контактов. + +--- + +## Contributor License Agreement + +Все контрибьюторы перед первым merge обязаны подписать [CLA](./CLA.md), передавая лицензионные права на свой вклад правообладателю. Это позволяет правообладателю выпускать продукт под обеими лицензиями (AGPL + Commercial). + +--- + +## Используемые открытые библиотеки + +Полный список зависимостей с лицензиями доступен в `package.json` каждого репозитория и через команду `npm ls --all --json`. + +Основные библиотеки: +- **Babylon.js** — Apache 2.0 +- **React** — MIT +- **Vite** — MIT +- **Colyseus** — MIT +- **Monaco Editor** — MIT + +Этот проект **не модифицирует** код этих библиотек — только использует их через стандартные API. Условия их лицензий выполняются автоматически при использовании через npm. + +--- + +## Активы (ассеты) + +3D-модели и текстуры в папке `public/kubikon-assets/` либо: +- Созданы автором проекта (© Иванкова В.С., лицензируются по той же двойной лицензии), +- Либо взяты из открытых наборов с указанной авторской лицензией (Kenney Nature Kit — CC0 1.0). + +Полный список ассетов с источниками — в `public/kubikon-assets/CREDITS.md` (создаётся при подготовке репо к публикации). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/LICENSE-COMMERCIAL.md b/LICENSE-COMMERCIAL.md new file mode 100644 index 0000000..115bb16 --- /dev/null +++ b/LICENSE-COMMERCIAL.md @@ -0,0 +1,54 @@ +# Commercial License + +Этот проект распространяется под **двойной лицензией**: +1. AGPL-3.0-or-later (см. [LICENSE](./LICENSE)) — для open-source использования +2. **Commercial License** (этот документ) — для проприетарного использования + +## Когда нужна Commercial License + +AGPL-3.0 — copyleft-лицензия, которая требует: +- Открывать исходный код всех модификаций и форков; +- При использовании по сети (SaaS, web-сервис) — предоставлять пользователям полный исходник; +- Производные работы распространять под той же AGPL-3.0. + +**Commercial License нужна, если вы хотите:** + +- Использовать код в проприетарном продукте без открытия модификаций; +- Запустить SaaS-сервис на базе этого кода без публикации исходников; +- Распространять модифицированную версию под другой лицензией (например, MIT для своих клиентов); +- Получить дополнительные гарантии, индемнификацию или техническую поддержку; +- Интегрировать код в продукт с несовместимой open-source лицензией. + +## Как получить + +Напишите письмо на адрес правообладателя: + +**Email:** maksimivankov26@yandex.ru +**Юрлицо:** ИП Иванкова Виктория Сергеевна (ОГРНИП 322237500039230) +**Тема письма:** "Commercial License Request — Rublox [player/studio]" + +В письме укажите: +1. Название вашей компании / ИП / физлица; +2. Краткое описание продукта, в котором планируется использование; +3. Объём использования (количество пользователей, географическая зона); +4. Сроки внедрения. + +В ответ вы получите коммерческое предложение с условиями и стоимостью лицензии. Стоимость зависит от объёма использования и типа продукта. + +## Сроки ответа + +- Первоначальный ответ — в течение 3 рабочих дней; +- Полное коммерческое предложение — в течение 10 рабочих дней с момента получения уточняющих данных. + +## Что НЕ требует Commercial License + +Вам **не нужна** Commercial License, если вы: + +- Используете код для личного некоммерческого изучения; +- Используете в open-source проекте, совместимом с AGPL-3.0, и публикуете все модификации; +- Запускаете для внутреннего использования в своей компании **без предоставления доступа извне** (AGPL допускает это — обязательство по открытию кода возникает только при предоставлении сервиса внешним пользователям); +- Делаете контрибьюции в основной репозиторий — для этого нужен только [CLA](./CLA.md), бесплатно. + +--- + +*Этот документ является публичной офертой на заключение лицензионного договора. Полные юридические условия фиксируются отдельным договором при покупке.* diff --git a/README.md b/README.md new file mode 100644 index 0000000..3bf5c2f --- /dev/null +++ b/README.md @@ -0,0 +1,169 @@ +# Студия Рублокса + +Open-source веб-студия для создания игр Рублокса — платформы 3D-игр в стиле Roblox. + +Построена на **Babylon.js 7** + **React 18** + **Vite 5** + **Monaco Editor**. Делай вокселные карты, ставь модели, пиши скрипты на JS в Web Worker-песочнице. Опубликованные игры открываются в [Плеере Рублокса](https://git.rublox.pro/rublox/player) (отдельный репозиторий). + +[![Лицензия: AGPL-3.0](https://img.shields.io/badge/%D0%9B%D0%B8%D1%86%D0%B5%D0%BD%D0%B7%D0%B8%D1%8F-AGPL--3.0-blue.svg)](./LICENSE) +[![Коммерческая лицензия](https://img.shields.io/badge/%D0%9A%D0%BE%D0%BC%D0%BC%D0%B5%D1%80%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F-%D0%B4%D0%BE%D1%81%D1%82%D1%83%D0%BF%D0%BD%D0%B0-green.svg)](./LICENSE-COMMERCIAL.md) + +--- + +## Быстрый старт (5 минут) + +**Требования:** +- Node.js 18+ и npm 10+ +- Браузер с WebGL2 (Chrome 90+ / Firefox 90+ / Safari 15+) +- 8 ГБ свободной памяти (студия тяжелее плеера — Monaco + большой ландшафт) + +**Установка:** +```bash +git clone ssh://git@git.rublox.pro:2222/rublox/studio.git +cd studio +npm install +cp .env.example .env # дефолты указывают на публичный staging — работает сразу +npm run dev +``` + +Открой `http://localhost:5174/edit/` — например `http://localhost:5174/edit/265`. + +**Без JWT** ты увидишь экран авторизации. Чтобы обойти: +```js +// В DevTools браузера: +localStorage.setItem('Authorization', '<твой-jwt-с-rublox.pro>'); +location.reload(); +``` + +--- + +## Ассеты (GLB-модели / текстуры / звуки) + +Папка `public/kubikon-assets/` (~93 МБ) **не хранится в git** чтобы репозиторий был лёгким. Скачать отдельно: + +```bash +curl -L -o assets.zip https://git.rublox.pro/rublox/studio/releases/download/latest/kubikon-assets.zip +unzip assets.zip -d public/ +``` + +Без ассетов студия запустится, но ландшафт будет пустой (нет текстур блоков). + +--- + +## Standalone-режим (без бэкенда) + +Хочешь попробовать без авторизации? + +```bash +echo "VITE_STANDALONE=true" >> .env +npm run dev +``` + +Открой `http://localhost:5174/edit/sample` — пустой редактор, авторизация-мок, save отключён. + +--- + +## Структура проекта + +``` +src/ +├── editor/ # Главный редактор (KubikonEditor) ~37к строк +│ ├── engine/ # Babylon-движок — 66 файлов, ~28к строк +│ │ # BabylonScene, GameRuntime, MultiplayerSync, +│ │ # PlayerController, BlockManager, TerrainVoxelBuilder, +│ │ # ModelManager, DecoManager, ScriptSandbox, +│ │ # 30+ GD-гейммодов +│ └── *Panel.jsx, *Modal.jsx — UI редактора (Hierarchy/Inspector/Terrain/ScriptEditor) +├── preview-player/ # Встроенный мини-плеер для теста («Play»-кнопка в редакторе) +├── community/ # Витрина/лента: KubikonFeed, KubikonStudio, KubikonDocs, +│ # KubikonLearn, KubikonGamePage, KubikonHeroKit, RealtimeTest +├── gd-shop/ # Geometry Dash: GdMenu, GdShop, GdRules, GdCoverArt, GdPlayWrapper +├── admin-preview/ # Превью-каталоги для дизайнеров: GdSkins, GdBosses, +│ # GdSpikes, GdArches, GdFinishes, GdPortals, GdMusic, +│ # GdSfx, GdShipSkins, GdDeco +├── components/ # Общий UI: KubikonBugReport, KubikonLeaderboard, +│ # KubikonPerfOverlay, EmailConfirmNotice, PleeseReg, +│ # RublocsLogo +├── api/ # API.js (конфиг) + Kubikon3DService.js + KubikonCadService +│ # + SanctionsService + playTicket +├── auth/ # AuthContext, SanctionsContext (лёгкие версии, +│ # без UserService от минки) +├── hooks/ # useDeviceType +├── utils/ # kubikonTheme, kubikonTime +├── App.jsx # Router + Suspense (15+ роутов) +├── main.jsx +├── index.css +└── LoadingScreen.jsx +``` + +Подробнее об архитектуре — в [ARCHITECTURE.md](./ARCHITECTURE.md). + +--- + +## Переменные окружения + +Скопируй `.env.example` в `.env` и поправь: + +| Переменная | Дефолт | Что означает | +|---|---|---| +| `VITE_API_BASE` | _(пустая)_ | Базовый URL HTTP-API. Пустой = vite-proxy (для dev). | +| `VITE_API_PROXY_TARGET` | `https://dev-api.rublox.pro` | (dev) куда vite-proxy шлёт `/api-*`. | +| `VITE_REALTIME_HTTP` | `https://dev-api.rublox.pro/api-game` | Colyseus HTTP. | +| `VITE_REALTIME_WS` | `wss://dev-api.rublox.pro/api-game` | Colyseus WS. | +| `VITE_RUBLOX_HOME` | `https://rublox.pro/app` | Редирект на главный сайт если нет авторизации. | +| `VITE_PLAYER_URL` | `https://player.rublox.pro` | URL плеера (для кнопки «Тест в плеере»). | +| `VITE_STANDALONE` | `false` | Пропустить API, открыть пустой редактор. | + +--- + +## Команды + +```bash +npm run dev # Dev-сервер (vite, порт 5174) +npm run build # Прод-билд → build/ +npm run preview # Превью прод-билда локально +npm run lint # ESLint +npm run format # Prettier (записать) +npm run format:check # Prettier (проверить) +``` + +--- + +## Контрибьютинг + +Мы рады PR'ам! Перед первым: + +1. Прочитай [CONTRIBUTING.md](./CONTRIBUTING.md) — стиль, ветки, шаблон PR. +2. Подпиши [CLA](./CLA.md) (через `https://team.rublox.pro/developer/cla` или комментарий `/sign-cla`). +3. Открывай issues через шаблоны в `.gitea/ISSUE_TEMPLATE/`. + +**Быстрый цикл:** +```bash +git checkout -b feature/моя-фича +# ...пишешь код... +npm run lint && npm run format +git commit -m "feat: описание" +git push origin feature/моя-фича +``` + +--- + +## Лицензия + +Двойная лицензия: + +- **[AGPL-3.0-or-later](./LICENSE)** — для open-source использования. Форки и производные работы (включая SaaS) обязаны публиковать свой исходник под той же лицензией. +- **[Коммерческая лицензия](./LICENSE-COMMERCIAL.md)** — для проприетарных продуктов. Контакт: `maksimivankov26@yandex.ru`. + +Все контрибьюторы обязаны подписать [CLA](./CLA.md) перед первым merge. + +© 2026 Иванкова Виктория Сергеевна (ИП). Все права защищены. + +--- + +## Ссылки + +- Главный сайт: https://rublox.pro +- Студия (демо): https://studio.rublox.pro +- Плеер (отдельный репо): https://git.rublox.pro/rublox/player +- Issues и PR: https://git.rublox.pro/rublox/studio +- Безопасность: [SECURITY.md](./SECURITY.md) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..078afcb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,64 @@ +# Политика безопасности + +## Поддерживаемые версии + +Только ветка `main` получает security-обновления. Тегированные релизы (`v0.x`) — best-effort. + +## Сообщение об уязвимости + +**НЕ открывай публичный issue для уязвимостей безопасности.** + +Уязвимости в этой студии могут напрямую повлиять на наш продакшен `studio.rublox.pro` и созданные через неё игры. Публичное раскрытие до выпуска патча может привести к реальному вреду реальным пользователям (включая несовершеннолетних, играющих в опубликованные игры). + +Пиши на: **`security@rublox.pro`** (читает только мейнтейнер проекта) + +Включи в письмо: + +1. Описание проблемы +2. Шаги воспроизведения (или proof-of-concept) +3. Затронутые версии / файлы +4. Твою оценку воздействия (XSS / RCE / IDOR / побег из песочницы / и т.д.) +5. Твой контакт для уточнений +6. Хочешь ли публичное упоминание после фикса (добавим в Hall of Fame в CHANGELOG) + +## Что ожидать + +| Шаг | Сроки | +|---|---| +| Подтверждение получения | в течение 24 часов | +| Оценка серьёзности | в течение 3 рабочих дней | +| Фикс в приватной ветке | в течение 7 дней для критичных | +| Публичный релиз патча | когда готов, с благодарностью | + +## Особенно ценные находки + +Следующие классы багов **высоко ценятся** (потому что могут скомпрометировать пользователей): + +- **Побег из script-песочницы** (`engine/scripts/ScriptSandbox*.js`) — скрипт юзера получает доступ к `window`, `document`, `fetch`, `localStorage`, либо угоняет сессии других игроков. +- **XSS в рендере контента игр** — названия игр, чат-сообщения, тексты комментов рендерятся как HTML. +- **Инъекция мультиплеер-сообщений** — кастомные Colyseus-сообщения роняют других клиентов или выполняют код атакующего. +- **Утечка auth-токена** — JWT или ticket появляется в URL, query-string, console-логах, error-страницах или analytics-событиях. +- **Инъекция в asset-URL** — префиксы `url:...` (модели от дизайнеров) загружают произвольные файлы вне нашего asset-бакета. + +## Что НЕ является уязвимостью + +(Не теряй своё и наше время — эти отчёты отклоняются.) + +- Отсутствующий `X-Frame-Options` или `Content-Security-Policy` (знаем, работаем над этим на стороне сервера). +- Self-XSS (юзер сам вставляет JS в свой DevTools). +- Устаревшие `npm audit` warning'и без рабочего эксплойта. +- Раскрытие информации о файлах в репо (это же open-source!). +- Отчёты от автоматических сканеров без ручной проверки. + +## Награда + +Мы маленький проект и не можем платить bounty, но за любую настоящую уязвимость: + +- Публичное упоминание в `CHANGELOG.md` (если хочешь) +- Включение в раздел «Hall of Fame» этого файла +- Личное спасибо от мейнтейнера +- За high-impact-репорты: разовый денежный подарок по усмотрению мейнтейнера (редко, но возможно) + +## Hall of Fame + +_Пока пусто — стань первым!_ diff --git a/index.html b/index.html new file mode 100644 index 0000000..907c7f4 --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + + + + Студия Рублокса + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..934951e --- /dev/null +++ b/package.json @@ -0,0 +1,68 @@ +{ + "name": "rublox-studio", + "version": "1.0.0", + "type": "module", + "description": "Open-source веб-студия для создания игр Рублокса — Babylon.js 7 + React 18 + Vite 5. Скриптинг на JS в Web Worker-песочнице.", + "keywords": [ + "rublox", + "game-editor", + "babylonjs", + "3d-editor", + "voxel-editor", + "react", + "vite", + "geometry-dash", + "monaco" + ], + "homepage": "https://rublox.pro", + "bugs": { + "url": "https://git.rublox.pro/rublox/studio/issues", + "email": "security@rublox.pro" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@git.rublox.pro:2222/rublox/studio.git" + }, + "license": "AGPL-3.0-or-later", + "author": { + "name": "Иванкова Виктория Сергеевна (ИП)", + "email": "maksimivankov26@yandex.ru", + "url": "https://rublox.pro" + }, + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --ext .js,.jsx --max-warnings 200", + "format": "prettier --write \"src/**/*.{js,jsx,json,md,css}\"", + "format:check": "prettier --check \"src/**/*.{js,jsx,json,md,css}\"" + }, + "dependencies": { + "@babylonjs/core": "7.54.3", + "@babylonjs/loaders": "7.54.3", + "@monaco-editor/react": "^4.7.0", + "axios": "1.8.4", + "colyseus.js": "0.16.22", + "dompurify": "^3.0.0", + "html2canvas": "^1.4.0", + "jwt-decode": "4.0.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-router-dom": "7.4.0", + "socket.io-client": "^4.8.3" + }, + "devDependencies": { + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", + "@vitejs/plugin-react": "4.3.3", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.0", + "eslint-plugin-react-hooks": "^4.6.0", + "prettier": "^3.2.5", + "vite": "5.4.10" + }, + "engines": { + "node": ">=18" + } +} diff --git a/public/kubikon-learn/assets.jpg b/public/kubikon-learn/assets.jpg new file mode 100644 index 0000000..6c23d0a Binary files /dev/null and b/public/kubikon-learn/assets.jpg differ diff --git a/public/kubikon-learn/beta.jpg b/public/kubikon-learn/beta.jpg new file mode 100644 index 0000000..bedfb63 Binary files /dev/null and b/public/kubikon-learn/beta.jpg differ diff --git a/public/kubikon-learn/players.jpg b/public/kubikon-learn/players.jpg new file mode 100644 index 0000000..da115b2 Binary files /dev/null and b/public/kubikon-learn/players.jpg differ diff --git a/public/kubikon-learn/publish.jpg b/public/kubikon-learn/publish.jpg new file mode 100644 index 0000000..f339ab6 Binary files /dev/null and b/public/kubikon-learn/publish.jpg differ diff --git a/public/kubikon-learn/scripts.jpg b/public/kubikon-learn/scripts.jpg new file mode 100644 index 0000000..97eef4d Binary files /dev/null and b/public/kubikon-learn/scripts.jpg differ diff --git a/public/kubikon-learn/start.jpg b/public/kubikon-learn/start.jpg new file mode 100644 index 0000000..234868f Binary files /dev/null and b/public/kubikon-learn/start.jpg differ diff --git a/public/kubikon-templates/.gitkeep b/public/kubikon-templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/kubikon-templates/city.jpg b/public/kubikon-templates/city.jpg new file mode 100644 index 0000000..4c4d50e Binary files /dev/null and b/public/kubikon-templates/city.jpg differ diff --git a/public/kubikon-templates/hills.jpg b/public/kubikon-templates/hills.jpg new file mode 100644 index 0000000..96890e3 Binary files /dev/null and b/public/kubikon-templates/hills.jpg differ diff --git a/public/kubikon-templates/island.jpg b/public/kubikon-templates/island.jpg new file mode 100644 index 0000000..3c17949 Binary files /dev/null and b/public/kubikon-templates/island.jpg differ diff --git a/public/kubikon-templates/plain.jpg b/public/kubikon-templates/plain.jpg new file mode 100644 index 0000000..3e4fe02 Binary files /dev/null and b/public/kubikon-templates/plain.jpg differ diff --git a/public/kubikon-templates/platformer.jpg b/public/kubikon-templates/platformer.jpg new file mode 100644 index 0000000..1c5c53b Binary files /dev/null and b/public/kubikon-templates/platformer.jpg differ diff --git a/public/kubikon-templates/racing.jpg b/public/kubikon-templates/racing.jpg new file mode 100644 index 0000000..78a429d Binary files /dev/null and b/public/kubikon-templates/racing.jpg differ diff --git a/public/kubikon-templates/shooter.jpg b/public/kubikon-templates/shooter.jpg new file mode 100644 index 0000000..2b3e676 Binary files /dev/null and b/public/kubikon-templates/shooter.jpg differ diff --git a/public/kubikon-templates/village.jpg b/public/kubikon-templates/village.jpg new file mode 100644 index 0000000..bd2334c Binary files /dev/null and b/public/kubikon-templates/village.jpg differ diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..911d16b --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,94 @@ +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import { lazy, Suspense } from 'react'; +import { AuthProvider } from './auth/AuthContext.jsx'; +import { SanctionsProvider } from './auth/SanctionsContext.jsx'; +import LoadingScreen from './LoadingScreen.jsx'; + +// Code-splitting: каждая большая страница в своём чанке. +const KubikonEditor = lazy(() => import('./editor/KubikonEditor.jsx')); +const KubikonPlayer = lazy(() => import('./preview-player/KubikonPlayer.jsx')); +const KubikonFeed = lazy(() => import('./community/KubikonFeed.jsx')); +const KubikonStudio = lazy(() => import('./community/KubikonStudio.jsx')); +const KubikonDocs = lazy(() => import('./community/KubikonDocs.jsx')); +const KubikonRules = lazy(() => import('./community/KubikonRules.jsx')); +const KubikonHeroKit = lazy(() => import('./community/KubikonHeroKit.jsx')); +const KubikonLearn = lazy(() => import('./community/KubikonLearn.jsx')); +const KubikonGamePage = lazy(() => import('./community/KubikonGamePage.jsx')); +const KubikonUserGames = lazy(() => import('./community/KubikonUserGames.jsx')); +const RealtimeTest = lazy(() => import('./community/RealtimeTest.jsx')); + +// Geometry Dash sub-app +const GdShop = lazy(() => import('./gd-shop/GdShop.jsx')); +const GdMenu = lazy(() => import('./gd-shop/GdMenu.jsx')); +const GdRules = lazy(() => import('./gd-shop/GdRules.jsx')); +const GdCoverArt = lazy(() => import('./gd-shop/GdCoverArt.jsx')); +const GdPlayWrapper = lazy(() => import('./gd-shop/GdPlayWrapper.jsx')); + +// Превью-роуты для команды дизайнеров (закрытые в проде по роли, +// в opensource доступны всем — это просто галереи ассетов). +const GdBossesPreview = lazy(() => import('./admin-preview/GdBossesPreview.jsx')); +const GdSkinsPreview = lazy(() => import('./admin-preview/GdSkinsPreview.jsx')); +const GdSpikesPreview = lazy(() => import('./admin-preview/GdSpikesPreview.jsx')); +const GdArchesPreview = lazy(() => import('./admin-preview/GdArchesPreview.jsx')); +const GdFinishesPreview = lazy(() => import('./admin-preview/GdFinishesPreview.jsx')); +const GdPortalsPreview = lazy(() => import('./admin-preview/GdPortalsPreview.jsx')); +const GdMusicPreview = lazy(() => import('./admin-preview/GdMusicPreview.jsx')); +const GdSfxPreview = lazy(() => import('./admin-preview/GdSfxPreview.jsx')); +const GdShipSkinsPreview = lazy(() => import('./admin-preview/GdShipSkinsPreview.jsx')); +const GdDecoPreview = lazy(() => import('./admin-preview/GdDecoPreview.jsx')); + +function FallbackLoader() { + return ; +} + +export default function App() { + return ( + + + + }> + + {/* Редактор и плеер (fullscreen) */} + } /> + } /> + } /> + + {/* Лента, профиль, доки */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Geometry Dash */} + } /> + } /> + } /> + } /> + } /> + + {/* Каталоги ассетов (для дизайнеров) */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + {/* 404 → главная */} + } /> + + + + + + ); +} diff --git a/src/LoadingScreen.jsx b/src/LoadingScreen.jsx new file mode 100644 index 0000000..4bd6d61 --- /dev/null +++ b/src/LoadingScreen.jsx @@ -0,0 +1,36 @@ +export default function LoadingScreen({ text = 'Загрузка...', subText = '' }) { + return ( +
+
+
{text}
+ {subText && ( +
+ {subText} +
+ )} + +
+ ); +} diff --git a/src/admin-preview/GdArchesPreview.jsx b/src/admin-preview/GdArchesPreview.jsx new file mode 100644 index 0000000..f37989b --- /dev/null +++ b/src/admin-preview/GdArchesPreview.jsx @@ -0,0 +1,216 @@ +/** + * GdArchesPreview — превью стартовых арок (10 эпох × 5). + * + * Маршрут: /admin-preview/gd-arches + * + * Юзер выбирает по одной арке на эпоху → POST в kubikon3d_savegame + * (project_id=295, namespace='gd_arch_choices'). + * + * Carousel-canvas с lazy IntersectionObserver — иначе WebGL-контексты упрутся в лимит. + */ +import React, { useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../../auth/AuthContext.jsx'; +import { jwtDecode } from 'jwt-decode'; +import axios from 'axios'; +import { STORYS_addres } from '../../api/API'; + +import { Engine } from '@babylonjs/core/Engines/engine'; +import { Scene } from '@babylonjs/core/scene'; +import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera'; +import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight'; +import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight'; +import { Vector3, Color3, Color4 } from '@babylonjs/core/Maths/math'; +import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder'; +import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; + +import { ARCH_CATALOG, EPOCH_INFO, getArchesByEpoch } from './gdArches/archFactories'; + +const CHOICES_PID = 295; +const CHOICES_NS = 'gd_arch_choices'; + +function getUserId() { + try { + const t = localStorage.getItem('Authorization'); + if (!t) return 0; + return Number(jwtDecode(t).id) || 0; + } catch (e) { return 0; } +} +const api = axios.create({ baseURL: STORYS_addres, timeout: 15000 }); +api.interceptors.request.use((cfg) => { + try { + const token = localStorage.getItem('Authorization'); + if (token) cfg.headers.Authorization = token; + } catch (e) {} + return cfg; +}); + +function ArchCard({ arch, isChosen, onChoose }) { + const wrapRef = useRef(null); + const canvasRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + if (!wrapRef.current) return; + const io = new IntersectionObserver((entries) => { + for (const e of entries) setIsVisible(e.isIntersecting); + }, { rootMargin: '200px', threshold: 0.01 }); + io.observe(wrapRef.current); + return () => io.disconnect(); + }, []); + + useEffect(() => { + if (!isVisible || !canvasRef.current) return; + let engine = null, scene = null; + try { + engine = new Engine(canvasRef.current, true, { stencil: false, antialias: true }); + scene = new Scene(engine); + scene.clearColor = new Color4(0.05, 0.07, 0.12, 1); + const camera = new ArcRotateCamera('cam', -Math.PI / 2.5, Math.PI / 2.6, 8, new Vector3(0, 2, 0), scene); + camera.attachControl(canvasRef.current, false); + camera.minZ = 0.1; + camera.maxZ = 50; + new HemisphericLight('hemi', new Vector3(0, 1, 0), scene).intensity = 0.7; + const sun = new DirectionalLight('sun', new Vector3(-0.5, -1, -0.3), scene); + sun.intensity = 0.8; + const floor = MeshBuilder.CreateGround('floor', { width: 7, height: 4 }, scene); + const fmat = new StandardMaterial('fmat', scene); + fmat.diffuseColor = new Color3(0.15, 0.20, 0.18); + floor.material = fmat; + + const handle = arch.make(scene, `prev_${arch.id}`); + if (handle && handle.root) handle.root.position.y = 0; + + scene.onBeforeRenderObservable.add(() => { + if (handle && handle.root) handle.root.rotation.y += 0.005; + }); + engine.runRenderLoop(() => scene.render()); + + return () => { + try { engine && engine.stopRenderLoop(); } catch (e) {} + try { handle && handle.dispose && handle.dispose(); } catch (e) {} + try { scene && scene.dispose(); } catch (e) {} + try { engine && engine.dispose(); } catch (e) {} + }; + } catch (e) { console.warn('[ArchCard] init failed', e); return () => {}; } + }, [isVisible, arch]); + + return ( +
+ {isVisible + ? + :
•••
} +
+
+
{arch.title}
+
{arch.id}
+
+ {isChosen && } +
+
+ ); +} + +export default function GdArchesPreview() { + const { userRole, isLoading } = useAuth(); + const navigate = useNavigate(); + const [choices, setChoices] = useState({}); + const [status, setStatus] = useState('idle'); + + useEffect(() => { + const uid = getUserId(); + if (!uid) return; + api.get(`/kubikon3d/savegame/${CHOICES_PID}/${uid}/${CHOICES_NS}`) + .then((r) => setChoices(r.data?.data || {})) + .catch(() => {}); + }, []); + + const choose = (epoch, archId) => { + setChoices(prev => ({ ...prev, [epoch]: archId })); + setStatus('idle'); + }; + + const save = async () => { + const uid = getUserId(); + if (!uid) { setStatus('error'); return; } + setStatus('loading'); + try { + await api.post(`/kubikon3d/savegame/${CHOICES_PID}/${uid}/${CHOICES_NS}`, { data: choices }); + setStatus('saved'); + } catch (e) { console.warn('[GdArchesPreview] save failed', e); setStatus('error'); } + }; + + if (isLoading) return
Загрузка...
; + if (userRole !== 'admin') { + return ( +
+

Доступ только для администратора

+ +
+ ); + } + const chosenCount = Object.keys(choices).length; + return ( +
+
+ +

GD — Стартовые арки по эпохам ({ARCH_CATALOG.length})

+
+ Выбрано: {chosenCount}/10 эпох +
+ + {status === 'error' && Ошибка} +
+ {EPOCH_INFO.map(epoch => { + const arches = getArchesByEpoch(epoch.n); + const chosenId = choices[epoch.n]; + return ( +
+

+ {epoch.emoji} Эпоха {epoch.n} — {epoch.name} + L{(epoch.n - 1) * 10 + 1} – L{epoch.n * 10} + {chosenId && ( + + ✓ выбран: {chosenId} + + )} +

+
+ {arches.map(arch => ( + choose(epoch.n, arch.id)} /> + ))} +
+
+ ); + })} +
+ ); +} + +const cardStyle = { background: '#0e1525', borderRadius: 10, overflow: 'hidden', transition: 'all 0.15s' }; +const btnStyle = { padding: '10px 18px', background: '#2a3146', color: '#cdd4e0', border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 600, fontSize: 14 }; +const btnPrimary = { padding: '10px 24px', background: 'linear-gradient(135deg, #22ff66, #44aaff)', color: '#000', border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 700, fontSize: 15 }; diff --git a/src/admin-preview/GdBossesPreview.jsx b/src/admin-preview/GdBossesPreview.jsx new file mode 100644 index 0000000..cb7ce2e --- /dev/null +++ b/src/admin-preview/GdBossesPreview.jsx @@ -0,0 +1,278 @@ +/** + * GdBossesPreview — превью-страница для просмотра всех 10 GD-боссов разом. + * + * Маршрут: /admin-preview/gd-bosses + * + * Для каждого босса делает мини-Babylon-сцену с вращающейся моделью. + * МИН открывает страницу, смотрит все 10 в виде сетки, по каждому даёт фидбэк. + * Доступ — только для админа. + * + * Источник данных: kubikon3d_user_models (id 164-173, см. RUBLOX_GD_BOSSES_REGISTRY.md). + */ +import React, { useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../../auth/AuthContext.jsx'; +import { getUserModel } from '../../api/Kubikon3DService'; +import { decodeVoxelModel } from '../KubikonEditor/engine/voxelModelCodec'; +import styles from './GdBossesPreview.module.css'; + +import { Engine } from '@babylonjs/core/Engines/engine'; +import { Scene } from '@babylonjs/core/scene'; +import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera'; +import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight'; +import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight'; +import { Vector3, Color3, Color4 } from '@babylonjs/core/Maths/math'; +import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder'; +import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; +import { VertexData } from '@babylonjs/core/Meshes/mesh.vertexData'; +import { Mesh } from '@babylonjs/core/Meshes/mesh'; + +// ID из реестра RUBLOX_GD_BOSSES_REGISTRY.md +const BOSSES = [ + { level: 10, title: 'Стражник леса', modelId: 164, description: 'Деревянный голем с горящими жёлтыми глазами. Корона из веток.' }, + { level: 20, title: 'Король горы', modelId: 165, description: 'Йети с ледяной короной. Снежно-белый мех.' }, + { level: 30, title: 'Хозяин города', modelId: 166, description: 'Робот-небоскрёб с окнами-глазами и красным маяком.' }, + { level: 40, title: 'Призрак ночи', modelId: 167, description: 'Полупрозрачный фиолетовый фантом с magenta-глазами.' }, + { level: 50, title: 'Король пустыни', modelId: 168, description: 'Сфинкс с золотой маской фараона.' }, + { level: 60, title: 'Левиафан', modelId: 169, description: 'Морской змей с большими белыми зубами.' }, + { level: 70, title: 'Хранитель глубин', modelId: 170, description: 'Каменный голем-минотавр с лавовыми жилами и кристальными рогами.' }, + { level: 80, title: 'Дракон вулкана', modelId: 171, description: 'Огнедышащий дракон с шипастой спиной и крыльями.' }, + { level: 90, title: 'Космический владыка', modelId: 172, description: 'НЛО-корабль с куполом, неоновыми окнами и тремя двигателями.' }, + { level: 100, title: 'АРХИТЕКТОР', modelId: 173, description: 'Финальный босс. Неоновая спираль + пирамида со всевидящим глазом.' }, +]; + +const VOXEL_SIZE = 0.0625; // совпадает с UserModelManager — 1/16 м + +/** + * Построить меш одной воксельной модели в сцене. + * Один меш = один воксель (для простоты). При size=64 это до 262144 кубов, + * но реально вокселей ~10000 — Babylon справляется. + */ +function buildVoxelMesh(scene, decoded, name) { + if (!decoded || !decoded.voxels) return null; + + // Группируем воксели по цвету для оптимизации (один меш на цвет) + const byColor = new Map(); + for (const v of decoded.voxels) { + const c = v.c || '#ffffff'; + if (!byColor.has(c)) byColor.set(c, []); + byColor.get(c).push(v); + } + + const parent = new Mesh(name + '_root', scene); + const halfSize = decoded.size / 2; + + let colorIdx = 0; + for (const [hex, voxels] of byColor.entries()) { + // Собираем геометрию вручную (vertex buffer) — без instances для простоты + const positions = []; + const normals = []; + const indices = []; + let vIdx = 0; + + for (const v of voxels) { + // Центр воксели в мире + const wx = (v.x - halfSize) * VOXEL_SIZE; + const wy = (v.y) * VOXEL_SIZE; + const wz = (v.z - halfSize) * VOXEL_SIZE; + const s = VOXEL_SIZE / 2; + + // 8 углов куба + const corners = [ + [wx - s, wy - s, wz - s], [wx + s, wy - s, wz - s], + [wx + s, wy + s, wz - s], [wx - s, wy + s, wz - s], + [wx - s, wy - s, wz + s], [wx + s, wy - s, wz + s], + [wx + s, wy + s, wz + s], [wx - s, wy + s, wz + s], + ]; + // 6 граней (12 треугольников) + const faces = [ + [0, 1, 2, 3, 0, 0, -1], // back + [5, 4, 7, 6, 0, 0, 1], // front + [4, 0, 3, 7, -1, 0, 0], // left + [1, 5, 6, 2, 1, 0, 0], // right + [3, 2, 6, 7, 0, 1, 0], // top + [4, 5, 1, 0, 0, -1, 0], // bottom + ]; + for (const [a, b, c, d, nx, ny, nz] of faces) { + for (const i of [a, b, c, d]) { + positions.push(corners[i][0], corners[i][1], corners[i][2]); + normals.push(nx, ny, nz); + } + indices.push(vIdx, vIdx + 1, vIdx + 2, vIdx, vIdx + 2, vIdx + 3); + vIdx += 4; + } + } + + const mesh = new Mesh(`${name}_${colorIdx}`, scene); + const vd = new VertexData(); + vd.positions = positions; + vd.normals = normals; + vd.indices = indices; + vd.applyToMesh(mesh); + + const mat = new StandardMaterial(`mat_${name}_${colorIdx}`, scene); + const col = Color3.FromHexString(hex); + mat.diffuseColor = col; + mat.specularColor = new Color3(0.1, 0.1, 0.1); + // Светящиеся (emissive) цвета — определим эвристически: ярко-насыщенные + const max = Math.max(col.r, col.g, col.b); + const min = Math.min(col.r, col.g, col.b); + const sat = max > 0 ? (max - min) / max : 0; + if (sat > 0.7 && max > 0.7) { + mat.emissiveColor = col.scale(0.5); + } + mesh.material = mat; + mesh.parent = parent; + colorIdx++; + } + + return parent; +} + + +function BossPreviewCard({ boss }) { + const canvasRef = useRef(null); + const [status, setStatus] = useState('loading'); + const [stats, setStats] = useState(null); + + useEffect(() => { + let cancelled = false; + let engine = null; + + async function init() { + try { + const resp = await getUserModel(boss.modelId); + if (cancelled) return; + const modelData = resp.data?.model_data; + const decoded = decodeVoxelModel(modelData); + if (!decoded) throw new Error('decode failed'); + + if (!canvasRef.current) return; + engine = new Engine(canvasRef.current, true, { preserveDrawingBuffer: true }); + const scene = new Scene(engine); + scene.clearColor = new Color4(0.12, 0.14, 0.20, 1); + + // Камера — арк, медленно вращается вокруг + const camera = new ArcRotateCamera('cam', + -Math.PI / 2 - 0.3, Math.PI / 2.4, + decoded.size * VOXEL_SIZE * 2.2, + new Vector3(0, decoded.size * VOXEL_SIZE / 2, 0), + scene); + camera.attachControl(canvasRef.current, true); + camera.wheelPrecision = 50; + camera.lowerRadiusLimit = decoded.size * VOXEL_SIZE * 1.2; + camera.upperRadiusLimit = decoded.size * VOXEL_SIZE * 4; + + // Освещение + const hemi = new HemisphericLight('hemi', new Vector3(0, 1, 0), scene); + hemi.intensity = 0.7; + hemi.groundColor = new Color3(0.3, 0.3, 0.4); + const dir = new DirectionalLight('dir', new Vector3(-0.5, -1, -0.3), scene); + dir.intensity = 0.6; + + // Платформа под боссом + const ground = MeshBuilder.CreateDisc('ground', + { radius: decoded.size * VOXEL_SIZE * 0.7, tessellation: 32 }, scene); + ground.rotation.x = Math.PI / 2; + const groundMat = new StandardMaterial('groundMat', scene); + groundMat.diffuseColor = new Color3(0.2, 0.22, 0.28); + ground.material = groundMat; + + // Меш модели + const mesh = buildVoxelMesh(scene, decoded, `boss_${boss.modelId}`); + + // Автовращение + scene.onBeforeRenderObservable.add(() => { + camera.alpha += 0.003; + }); + + engine.runRenderLoop(() => scene.render()); + const onResize = () => engine.resize(); + window.addEventListener('resize', onResize); + + setStats({ + voxels: decoded.voxels.length, + size: decoded.size, + colors: new Set(decoded.voxels.map(v => v.c)).size, + }); + setStatus('ok'); + + // cleanup + return () => { + window.removeEventListener('resize', onResize); + }; + } catch (e) { + console.error(`[BossPreview L${boss.level}]`, e); + setStatus('error: ' + e.message); + } + } + init(); + + return () => { + cancelled = true; + if (engine) { + try { engine.dispose(); } catch (_) {} + } + }; + }, [boss.modelId]); + + return ( +
+
+ L{boss.level} +

{boss.title}

+
+
+ + {status !== 'ok' && ( +
{status}
+ )} +
+
{boss.description}
+ {stats && ( +
+ 📦 {stats.size}³ · 🎨 {stats.colors} цв. · 🧱 {stats.voxels} вокс. + · открыть в редакторе +
+ )} +
+ ); +} + + +function GdBossesPreview() { + const { userRole, isLoading } = useAuth(); + const navigate = useNavigate(); + + if (isLoading) return
Загрузка...
; + if (userRole !== 'admin') { + return ( +
+

Доступ только для администратора

+ +
+ ); + } + + return ( +
+
+

GD-Боссы — превью 10 моделей

+
+ Источник: kubikon3d_user_models id 164-173 · реестр в RUBLOX_GD_BOSSES_REGISTRY.md +
+
+ +
+ {BOSSES.map(b => )} +
+ +
+

Жми/тащи мышью на любой превью — можно покрутить модель руками.

+

Если нужны правки — скажи МИНу какие, я подкручу Python-генератор и перезалью.

+
+
+ ); +} + +export default GdBossesPreview; diff --git a/src/admin-preview/GdBossesPreview.module.css b/src/admin-preview/GdBossesPreview.module.css new file mode 100644 index 0000000..d743438 --- /dev/null +++ b/src/admin-preview/GdBossesPreview.module.css @@ -0,0 +1,151 @@ +.root { + min-height: 100vh; + background: #1f2433; + color: #e3e8f0; + padding: 24px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; +} + +.loading, .denied { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: #1f2433; + color: #e3e8f0; + gap: 16px; +} + +.backBtn { + padding: 8px 18px; + background: #228be6; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 14px; +} + +.topbar { + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid #2f3548; +} + +.h1 { + margin: 0; + font-size: 22px; + font-weight: 700; +} + +.subline { + margin-top: 6px; + color: #8a93a8; + font-size: 13px; +} + +.subline code { + background: #2a3142; + padding: 1px 6px; + border-radius: 3px; + font-size: 12px; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); + gap: 18px; +} + +.card { + background: #2a3142; + border: 1px solid #3a4156; + border-radius: 10px; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 14px; + background: #232938; + border-bottom: 1px solid #2f3548; +} + +.levelBadge { + background: linear-gradient(135deg, #3357ff, #b266ff); + color: white; + font-weight: 700; + padding: 4px 10px; + border-radius: 5px; + font-size: 13px; + min-width: 42px; + text-align: center; +} + +.title { + margin: 0; + font-size: 16px; + font-weight: 600; +} + +.canvasWrap { + position: relative; + aspect-ratio: 1; + background: #1c2030; +} + +.canvas { + width: 100%; + height: 100%; + display: block; + touch-action: none; +} + +.overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(28, 32, 48, 0.85); + color: #ff8a8a; + font-size: 13px; + text-align: center; + padding: 16px; +} + +.description { + padding: 12px 14px; + font-size: 13px; + line-height: 1.5; + color: #cdd4e0; +} + +.stats { + padding: 8px 14px 12px; + font-size: 12px; + color: #8a93a8; + border-top: 1px dashed #3a4156; +} + +.stats a { + color: #66b3ff; + margin-left: 6px; +} + +.footer { + margin-top: 28px; + padding-top: 16px; + border-top: 1px solid #2f3548; + color: #8a93a8; + font-size: 13px; +} + +.footer p { + margin: 4px 0; +} diff --git a/src/admin-preview/GdDecoPreview.jsx b/src/admin-preview/GdDecoPreview.jsx new file mode 100644 index 0000000..742009e --- /dev/null +++ b/src/admin-preview/GdDecoPreview.jsx @@ -0,0 +1,217 @@ +/** + * GdDecoPreview — превью декораций ландшафта (10 эпох × 10 = 100). + * Маршрут: /admin-preview/gd-deco + * Multi-select: до 5 моделей на эпоху. Выбор → kubikon3d_savegame + * (project_id=295, namespace='gd_deco_choices'). data = { 1: ['d1_v1','d1_v3',...] }. + */ +import React, { useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../../auth/AuthContext.jsx'; +import { jwtDecode } from 'jwt-decode'; +import axios from 'axios'; +import { STORYS_addres } from '../../api/API'; + +import { Engine } from '@babylonjs/core/Engines/engine'; +import { Scene } from '@babylonjs/core/scene'; +import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera'; +import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight'; +import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight'; +import { Vector3, Color3, Color4 } from '@babylonjs/core/Maths/math'; +import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder'; +import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; + +import { DECO_CATALOG, EPOCH_INFO, getDecoByEpoch } from './gdDeco/decoFactories'; + +const CHOICES_PID = 295; +const CHOICES_NS = 'gd_deco_choices'; +const MAX_PER_EPOCH = 5; + +function getUserId() { + try { const t = localStorage.getItem('Authorization'); if (!t) return 0; return Number(jwtDecode(t).id) || 0; } catch (e) { return 0; } +} +const api = axios.create({ baseURL: STORYS_addres, timeout: 15000 }); +api.interceptors.request.use((cfg) => { + try { const token = localStorage.getItem('Authorization'); if (token) cfg.headers.Authorization = token; } catch (e) {} + return cfg; +}); + +function DecoCard({ deco, isChosen, onToggle, disabled }) { + const wrapRef = useRef(null); + const canvasRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); + useEffect(() => { + if (!wrapRef.current) return; + const io = new IntersectionObserver((entries) => { + for (const e of entries) setIsVisible(e.isIntersecting); + }, { rootMargin: '200px', threshold: 0.01 }); + io.observe(wrapRef.current); + return () => io.disconnect(); + }, []); + useEffect(() => { + if (!isVisible || !canvasRef.current) return; + let engine = null, scene = null; + try { + engine = new Engine(canvasRef.current, true, { stencil: false, antialias: true }); + scene = new Scene(engine); + scene.clearColor = new Color4(0.05, 0.07, 0.12, 1); + const camera = new ArcRotateCamera('cam', -Math.PI / 2.5, Math.PI / 2.7, 5, new Vector3(0, 1.2, 0), scene); + camera.attachControl(canvasRef.current, false); + camera.minZ = 0.1; camera.maxZ = 50; + new HemisphericLight('hemi', new Vector3(0, 1, 0), scene).intensity = 0.7; + const sun = new DirectionalLight('sun', new Vector3(-0.5, -1, -0.3), scene); + sun.intensity = 0.8; + const floor = MeshBuilder.CreateGround('floor', { width: 4, height: 4 }, scene); + const fmat = new StandardMaterial('fmat', scene); + fmat.diffuseColor = new Color3(0.18, 0.22, 0.20); + floor.material = fmat; + const handle = deco.make(scene, `prev_${deco.id}`); + if (handle && handle.root) handle.root.position.y = 0; + scene.onBeforeRenderObservable.add(() => { + if (handle && handle.root) handle.root.rotation.y += 0.008; + }); + engine.runRenderLoop(() => scene.render()); + return () => { + try { engine && engine.stopRenderLoop(); } catch (e) {} + try { handle && handle.dispose && handle.dispose(); } catch (e) {} + try { scene && scene.dispose(); } catch (e) {} + try { engine && engine.dispose(); } catch (e) {} + }; + } catch (e) { console.warn('[DecoCard]', e); return () => {}; } + }, [isVisible, deco]); + + return ( +
{ if (!disabled || isChosen) onToggle(); }} + style={{ + ...cardStyle, + border: isChosen ? '3px solid #22ff66' : '2px solid #2a3146', + boxShadow: isChosen ? '0 0 16px rgba(34,255,102,0.4)' : 'none', + cursor: disabled && !isChosen ? 'not-allowed' : 'pointer', + opacity: disabled && !isChosen ? 0.45 : 1, + }} + > + {isVisible + ? + :
•••
} +
+
+
{deco.title}
+
{deco.id}
+
+ {isChosen && } +
+
+ ); +} + +export default function GdDecoPreview() { + const { userRole, isLoading } = useAuth(); + const navigate = useNavigate(); + // choices: { '1': ['d1_v1','d1_v4',...], '2': [...], ... } + const [choices, setChoices] = useState({}); + const [status, setStatus] = useState('idle'); + + useEffect(() => { + const uid = getUserId(); + if (!uid) return; + api.get(`/kubikon3d/savegame/${CHOICES_PID}/${uid}/${CHOICES_NS}`) + .then((r) => setChoices(r.data?.data || {})) + .catch(() => {}); + }, []); + + const toggle = (epoch, decoId) => { + setChoices(prev => { + const arr = prev[epoch] || []; + const has = arr.includes(decoId); + let next; + if (has) { + next = arr.filter(x => x !== decoId); + } else { + if (arr.length >= MAX_PER_EPOCH) return prev; + next = [...arr, decoId]; + } + return { ...prev, [epoch]: next }; + }); + setStatus('idle'); + }; + + const save = async () => { + const uid = getUserId(); + if (!uid) { setStatus('error'); return; } + setStatus('loading'); + try { + await api.post(`/kubikon3d/savegame/${CHOICES_PID}/${uid}/${CHOICES_NS}`, { data: choices }); + setStatus('saved'); + } catch (e) { console.warn('[GdDecoPreview] save failed', e); setStatus('error'); } + }; + + if (isLoading) return
Загрузка...
; + if (userRole !== 'admin') { + return ( +
+

Доступ только для администратора

+ +
+ ); + } + + const totalChosen = Object.values(choices).reduce((s, arr) => s + (arr?.length || 0), 0); + + return ( +
+
+ +

GD — Декорации ландшафта ({DECO_CATALOG.length})

+
+ Выбрано: {totalChosen} (до 5 на эпоху) +
+ + {status === 'error' && Ошибка} +
+ {EPOCH_INFO.map(epoch => { + const items = getDecoByEpoch(epoch.n); + const arr = choices[epoch.n] || []; + const fullyChosen = arr.length >= MAX_PER_EPOCH; + return ( +
+

+ {epoch.emoji} Эпоха {epoch.n} — {epoch.name} + L{(epoch.n - 1) * 10 + 1} – L{epoch.n * 10} + + выбрано {arr.length}/{MAX_PER_EPOCH} + +

+
+ {items.map(deco => ( + toggle(epoch.n, deco.id)} + disabled={fullyChosen} + /> + ))} +
+
+ ); + })} +
+ ); +} + +const cardStyle = { background: '#0e1525', borderRadius: 10, overflow: 'hidden', transition: 'all 0.15s' }; +const btnStyle = { padding: '10px 18px', background: '#2a3146', color: '#cdd4e0', border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 600, fontSize: 14 }; +const btnPrimary = { padding: '10px 24px', background: 'linear-gradient(135deg, #22ff66, #44aaff)', color: '#000', border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 700, fontSize: 15 }; diff --git a/src/admin-preview/GdFinishesPreview.jsx b/src/admin-preview/GdFinishesPreview.jsx new file mode 100644 index 0000000..e2c698d --- /dev/null +++ b/src/admin-preview/GdFinishesPreview.jsx @@ -0,0 +1,179 @@ +/** + * GdFinishesPreview — превью финишных ворот (10 эпох × 5 = 50). + * Маршрут: /admin-preview/gd-finishes + * Выбор юзера → kubikon3d_savegame (project_id=295, namespace='gd_finish_choices'). + */ +import React, { useEffect, useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from '../../auth/AuthContext.jsx'; +import { jwtDecode } from 'jwt-decode'; +import axios from 'axios'; +import { STORYS_addres } from '../../api/API'; + +import { Engine } from '@babylonjs/core/Engines/engine'; +import { Scene } from '@babylonjs/core/scene'; +import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera'; +import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight'; +import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight'; +import { Vector3, Color3, Color4 } from '@babylonjs/core/Maths/math'; +import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder'; +import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; + +import { FINISH_CATALOG, EPOCH_INFO, getFinishesByEpoch } from './gdFinishes/finishFactories'; + +const CHOICES_PID = 295; +const CHOICES_NS = 'gd_finish_choices'; +function getUserId() { + try { const t = localStorage.getItem('Authorization'); if (!t) return 0; return Number(jwtDecode(t).id) || 0; } catch (e) { return 0; } +} +const api = axios.create({ baseURL: STORYS_addres, timeout: 15000 }); +api.interceptors.request.use((cfg) => { + try { const token = localStorage.getItem('Authorization'); if (token) cfg.headers.Authorization = token; } catch (e) {} + return cfg; +}); + +function FinCard({ fin, isChosen, onChoose }) { + const wrapRef = useRef(null); + const canvasRef = useRef(null); + const [isVisible, setIsVisible] = useState(false); + useEffect(() => { + if (!wrapRef.current) return; + const io = new IntersectionObserver((entries) => { + for (const e of entries) setIsVisible(e.isIntersecting); + }, { rootMargin: '200px', threshold: 0.01 }); + io.observe(wrapRef.current); + return () => io.disconnect(); + }, []); + useEffect(() => { + if (!isVisible || !canvasRef.current) return; + let engine = null, scene = null; + try { + engine = new Engine(canvasRef.current, true, { stencil: false, antialias: true }); + scene = new Scene(engine); + scene.clearColor = new Color4(0.05, 0.07, 0.12, 1); + const camera = new ArcRotateCamera('cam', -Math.PI / 2.5, Math.PI / 2.6, 8.5, new Vector3(0, 2.2, 0), scene); + camera.attachControl(canvasRef.current, false); + camera.minZ = 0.1; camera.maxZ = 50; + new HemisphericLight('hemi', new Vector3(0, 1, 0), scene).intensity = 0.7; + const sun = new DirectionalLight('sun', new Vector3(-0.5, -1, -0.3), scene); + sun.intensity = 0.8; + const floor = MeshBuilder.CreateGround('floor', { width: 7, height: 4 }, scene); + const fmat = new StandardMaterial('fmat', scene); + fmat.diffuseColor = new Color3(0.15, 0.20, 0.18); + floor.material = fmat; + const handle = fin.make(scene, `prev_${fin.id}`); + if (handle && handle.root) handle.root.position.y = 0; + scene.onBeforeRenderObservable.add(() => { + if (handle && handle.root) handle.root.rotation.y += 0.005; + }); + engine.runRenderLoop(() => scene.render()); + return () => { + try { engine && engine.stopRenderLoop(); } catch (e) {} + try { handle && handle.dispose && handle.dispose(); } catch (e) {} + try { scene && scene.dispose(); } catch (e) {} + try { engine && engine.dispose(); } catch (e) {} + }; + } catch (e) { console.warn('[FinCard] init failed', e); return () => {}; } + }, [isVisible, fin]); + return ( +
+ {isVisible + ? + :
•••
} +
+
+
{fin.title}
+
{fin.id}
+
+ {isChosen && } +
+
+ ); +} + +export default function GdFinishesPreview() { + const { userRole, isLoading } = useAuth(); + const navigate = useNavigate(); + const [choices, setChoices] = useState({}); + const [status, setStatus] = useState('idle'); + useEffect(() => { + const uid = getUserId(); + if (!uid) return; + api.get(`/kubikon3d/savegame/${CHOICES_PID}/${uid}/${CHOICES_NS}`) + .then((r) => setChoices(r.data?.data || {})) + .catch(() => {}); + }, []); + const choose = (epoch, finId) => { setChoices(prev => ({ ...prev, [epoch]: finId })); setStatus('idle'); }; + const save = async () => { + const uid = getUserId(); + if (!uid) { setStatus('error'); return; } + setStatus('loading'); + try { + await api.post(`/kubikon3d/savegame/${CHOICES_PID}/${uid}/${CHOICES_NS}`, { data: choices }); + setStatus('saved'); + } catch (e) { console.warn('[GdFinishesPreview] save failed', e); setStatus('error'); } + }; + if (isLoading) return
Загрузка...
; + if (userRole !== 'admin') { + return ( +
+

Доступ только для администратора

+ +
+ ); + } + const chosenCount = Object.keys(choices).length; + return ( +
+
+ +

GD — Финишные ворота ({FINISH_CATALOG.length})

+
+ Выбрано: {chosenCount}/10 эпох +
+ + {status === 'error' && Ошибка} +
+ {EPOCH_INFO.map(epoch => { + const fins = getFinishesByEpoch(epoch.n); + const chosenId = choices[epoch.n]; + return ( +
+

+ {epoch.emoji} Эпоха {epoch.n} — {epoch.name} + L{(epoch.n - 1) * 10 + 1} – L{epoch.n * 10} + {chosenId && (✓ выбран: {chosenId})} +

+
+ {fins.map(fin => ( + choose(epoch.n, fin.id)} /> + ))} +
+
+ ); + })} +
+ ); +} + +const cardStyle = { background: '#0e1525', borderRadius: 10, overflow: 'hidden', transition: 'all 0.15s' }; +const btnStyle = { padding: '10px 18px', background: '#2a3146', color: '#cdd4e0', border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 600, fontSize: 14 }; +const btnPrimary = { padding: '10px 24px', background: 'linear-gradient(135deg, #22ff66, #44aaff)', color: '#000', border: 'none', borderRadius: 8, cursor: 'pointer', fontWeight: 700, fontSize: 15 }; diff --git a/src/admin-preview/GdMusicPreview.jsx b/src/admin-preview/GdMusicPreview.jsx new file mode 100644 index 0000000..4d49c6a --- /dev/null +++ b/src/admin-preview/GdMusicPreview.jsx @@ -0,0 +1,193 @@ +/** + * GdMusicPreview — превью 20 треков GD-музыки. + * + * Маршрут: /admin-preview/gd-music + * + * Для каждого трека: + * - проверяет есть ли реальный mp3 в /music/gd/ + * - если есть — играет файл через