Большой консолидирующий коммит после поднятия studio.rublox.pro (28 мая 2026). Содержит изменения которые делались в процессе подготовки прод-окружения: Фиксы импортов после выноса из minecraftia: - Массовая замена путей ../../components → ../components (40+ файлов в src/community/, src/admin-preview/) - Замена ../KubikonEditor/ → ../editor/, ../KubikonStudio/ → ../community/, ../AdminPreview/ → ../admin-preview/ - API.js скопирован из минки целиком (было 8 экспортов, стало 312) - Добавлены PLAYER_URL, MyButton_1, недостающие компоненты - Заменены require() на статические ES-imports в BabylonScene, PrimitiveManager, GameRuntime (Vite не поддерживает CJS require) Структура ассетов: - public/kubikon-templates/ → public/assets/kubikon-templates/ - public/kubikon-learn/ → public/assets/kubikon-learn/ - (код искал в /assets/, файлы лежали без /assets/) Навигация роутов внутри студии: - /kubikon-studio/docs → /docs (90+ навигационных вызовов sed-replaced) - /kubikon-editor/X → /edit/X, /kubikon/play/X → /play/X, /kubikon/gd/X → /gd/X UI: - Новый компонент StudioHeader (61px, как в минке) + копия favicon - WithHeader wrapper в App.jsx для всех страниц кроме fullscreen-редактора/плеера - SSO ticket-flow в AuthContext (auto-redeem #ticket= при загрузке) - Тёмная тема карточек игр в ВИКИ (фон #1c2231 вместо #fff, картинка впритык) Документация: - docs/ONBOARDING.md — путь нового контрибьютора от нуля до PR - docs/TUTORIAL_ADD_SCRIPT_API.md — как добавить game.* API - API_USAGE.md — список эндпоинтов backend - README в подпапках engine/, engine/terrain/, engine/voxel/, engine/robloxterrain/, engine/types/ .gitignore: - public/wiki/ исключён (73МБ PNG, будут на CDN отдельной задачей) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
986 lines
44 KiB
JavaScript
986 lines
44 KiB
JavaScript
import React, { useState, useEffect, useRef } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import cl from './KubikonStudio.module.css';
|
||
import RublocsLogo from '../components/RublocsLogo/RublocsLogo';
|
||
import useDeviceType from '../hooks/useDeviceType';
|
||
import KubikonDesktopOnlyStub from './KubikonDesktopOnlyStub';
|
||
import Icon from '../editor/Icon';
|
||
import DocIcon from './docsIcons';
|
||
|
||
/**
|
||
* KubikonRules — «Правила создания игр» платформы Рублокс.
|
||
* URL: /rules
|
||
*
|
||
* Отдельная страница: подробно расписано, какие игры можно создавать
|
||
* и публиковать, чтобы их не заблокировали и они были популярными.
|
||
* Опирается на реальную систему умной ленты Рублокса:
|
||
* - автопроверка скриптов при публикации (опасные → очередь review);
|
||
* - статусы игр draft / published / review / blocked;
|
||
* - алгоритм ленты hot_score (Wilson-рейтинг + вовлечённость +
|
||
* популярность + свежесть);
|
||
* - мягкое автоскрытие (demoted) игр с низким рейтингом.
|
||
*
|
||
* Стиль и вёрстка повторяют вику (KubikonDocs.jsx): своя боковая
|
||
* панель, hero, разделы с оглавлением. Все стили — в INLINE_STYLES,
|
||
* чтобы страница была самодостаточной. Иконки — самописные SVG.
|
||
*/
|
||
|
||
// ── Маленькие хелперы разметки ──────────────────────────────────────
|
||
|
||
// Плашка «можно» — зелёная
|
||
const Ok = ({ children }) => (
|
||
<div className="ruleBox ruleBox--ok">
|
||
<span className="ruleBox__ico"><DocIcon name="flag" size={18} /></span>
|
||
<div>{children}</div>
|
||
</div>
|
||
);
|
||
|
||
// Плашка «нельзя» — красная
|
||
const No = ({ children }) => (
|
||
<div className="ruleBox ruleBox--no">
|
||
<span className="ruleBox__ico"><DocIcon name="warning" size={18} /></span>
|
||
<div>{children}</div>
|
||
</div>
|
||
);
|
||
|
||
// Жёлтая плашка-подсказка
|
||
const Tip = ({ children }) => (
|
||
<div className="ruleBox ruleBox--tip">
|
||
<span className="ruleBox__ico"><DocIcon name="lightbulb" size={18} /></span>
|
||
<div>{children}</div>
|
||
</div>
|
||
);
|
||
|
||
// Карточка статуса игры
|
||
const Status = ({ color, name, children }) => (
|
||
<div className="statusRow">
|
||
<span className="statusDot" style={{ background: color }} />
|
||
<div>
|
||
<b className="statusName">{name}</b>
|
||
<span className="statusText"> — {children}</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// ── Контент: разделы правил ─────────────────────────────────────────
|
||
// Каждый раздел: { id, icon, title, body }. body — готовый JSX.
|
||
const RULES = [
|
||
{
|
||
id: 'how-publish',
|
||
icon: 'rocket',
|
||
title: 'Как игра попадает в ленту',
|
||
body: (
|
||
<>
|
||
<p>
|
||
В Рублоксе <b>нет предварительной модерации каждой игры
|
||
человеком</b>. Ты сам нажимаешь «Опубликовать» — и игра
|
||
почти сразу появляется в общей ленте. Это значит свободу,
|
||
но и ответственность: правила нужно соблюдать самому.
|
||
</p>
|
||
<p>Что происходит, когда ты жмёшь «Опубликовать»:</p>
|
||
<ol>
|
||
<li>
|
||
Игра автоматически проверяется — система смотрит
|
||
твои скрипты на опасный код.
|
||
</li>
|
||
<li>
|
||
Если всё чисто — игра <b>сразу в ленте</b>, её видят
|
||
все игроки.
|
||
</li>
|
||
<li>
|
||
Если в скриптах нашлось что-то подозрительное — игра
|
||
уходит на ручную проверку к админу (это бывает
|
||
редко). После проверки её либо публикуют, либо
|
||
отправляют на доработку.
|
||
</li>
|
||
</ol>
|
||
<Tip>
|
||
Пока игра в статусе «черновик», её видишь только ты.
|
||
Тестируй сколько угодно — публикуй, только когда игра
|
||
реально готова и в неё интересно играть.
|
||
</Tip>
|
||
</>
|
||
),
|
||
},
|
||
{
|
||
id: 'statuses',
|
||
icon: 'flag',
|
||
title: 'Статусы игры — что они значат',
|
||
body: (
|
||
<>
|
||
<p>У каждой твоей игры есть статус. Вот что они означают:</p>
|
||
<div className="statusList">
|
||
<Status color="#94a3b8" name="Черновик">
|
||
игра ещё не опубликована, её видишь только ты.
|
||
Можно свободно тестировать и переделывать.
|
||
</Status>
|
||
<Status color="#16a34a" name="Опубликована">
|
||
игра в общей ленте, в неё играют все. Главное
|
||
состояние успешной игры.
|
||
</Status>
|
||
<Status color="#f59e0b" name="На проверке">
|
||
в игре нашли подозрительные скрипты, её смотрит
|
||
админ. Пока проверяет — игра не в ленте.
|
||
</Status>
|
||
<Status color="#dc2626" name="Заблокирована">
|
||
игра нарушила правила. Её убрали из ленты. Причину
|
||
админ пишет в комментарии — исправь и можно
|
||
опубликовать заново.
|
||
</Status>
|
||
</div>
|
||
<p>
|
||
Отдельно есть «мягкое скрытие»: даже опубликованная игра
|
||
может <b>выпасть из ленты</b>, если в неё совсем не
|
||
играют или ставят сплошные дизлайки. Она не блокируется —
|
||
её по-прежнему можно найти поиском и открыть по ссылке,
|
||
но в рекомендациях её не показывают. Подробнее — в
|
||
разделе «Как стать популярным».
|
||
</p>
|
||
</>
|
||
),
|
||
},
|
||
{
|
||
id: 'allowed',
|
||
icon: 'sparkles',
|
||
title: 'Какие игры можно публиковать',
|
||
body: (
|
||
<>
|
||
<p>
|
||
Рублокс — площадка для <b>детей и подростков</b>. Можно
|
||
публиковать любые игры, в которые было бы не стыдно
|
||
поиграть на уроке. Примеры хороших жанров:
|
||
</p>
|
||
<Ok>
|
||
<b>Платформеры и паркур</b> — прыгай по платформам,
|
||
доберись до финиша, собирай монетки.
|
||
</Ok>
|
||
<Ok>
|
||
<b>Гонки и аркады</b> — проедь трассу на время, обгони
|
||
соперников, уворачивайся от препятствий.
|
||
</Ok>
|
||
<Ok>
|
||
<b>Головоломки и квесты</b> — лабиринты, кнопки и двери,
|
||
логические загадки.
|
||
</Ok>
|
||
<Ok>
|
||
<b>Тиры и «выживалки»</b> — стрельба по мишеням,
|
||
мультяшные зомби, защита базы. Бой допустим, если он
|
||
мультяшный и без крови.
|
||
</Ok>
|
||
<Ok>
|
||
<b>Симуляторы и «тайкуны»</b> — построй город, ферму,
|
||
магазин; собирай ресурсы, прокачивай постройки.
|
||
</Ok>
|
||
<Ok>
|
||
<b>Ролевые игры и приключения</b> — исследуй мир,
|
||
говори с NPC, выполняй задания.
|
||
</Ok>
|
||
<Ok>
|
||
<b>Мини-игры и обучалки</b> — викторины, игры на
|
||
реакцию, игры, которые чему-то учат.
|
||
</Ok>
|
||
<Tip>
|
||
Не знаешь, с чего начать — открой вику, раздел «50
|
||
игр-уроков». Там готовые игры разных жанров с пошаговыми
|
||
инструкциями: бери за основу и делай свою.
|
||
</Tip>
|
||
</>
|
||
),
|
||
},
|
||
{
|
||
id: 'forbidden-content',
|
||
icon: 'warning',
|
||
title: 'Что публиковать запрещено — контент',
|
||
body: (
|
||
<>
|
||
<p>
|
||
Игру <b>заблокируют</b>, если в ней есть что-то из
|
||
списка ниже. Это правила про содержание игры — текст,
|
||
картинки, название, тему.
|
||
</p>
|
||
<No>
|
||
<b>Жестокость и кровь.</b> Реалистичное насилие, кровь,
|
||
расчленёнка, сцены смерти людей. Мультяшный «бой» без
|
||
крови — можно, реализм — нельзя.
|
||
</No>
|
||
<No>
|
||
<b>Взрослый контент.</b> Любая эротика, намёки на секс,
|
||
раздетые персонажи. Площадка детская — это под строгим
|
||
запретом.
|
||
</No>
|
||
<No>
|
||
<b>Мат и оскорбления.</b> Нецензурная брань в названии,
|
||
описании, в текстах внутри игры, на табличках и в
|
||
репликах NPC.
|
||
</No>
|
||
<No>
|
||
<b>Вражда и травля.</b> Оскорбление людей по
|
||
национальности, религии, полу; призывы кого-то
|
||
травить; буллинг конкретных игроков.
|
||
</No>
|
||
<No>
|
||
<b>Опасное и противозаконное.</b> Пропаганда наркотиков,
|
||
алкоголя, курения, оружия; инструкции, как навредить
|
||
себе или другим.
|
||
</No>
|
||
<No>
|
||
<b>Страшный «шок-контент».</b> Скример-игры, цель
|
||
которых — напугать; крик и страшные лица «в лоб».
|
||
Лёгкая «страшилка» в мультяшном стиле — можно.
|
||
</No>
|
||
<No>
|
||
<b>Реклама и обман.</b> Реклама посторонних сайтов,
|
||
«накрутка», просьбы прислать деньги или данные аккаунта,
|
||
фейковые «розыгрыши».
|
||
</No>
|
||
<No>
|
||
<b>Чужое.</b> Нельзя выдавать чужую игру за свою.
|
||
Скопировал чужую игру целиком и опубликовал как свою —
|
||
это нарушение.
|
||
</No>
|
||
<Tip>
|
||
Простое правило: если ты не показал бы эту игру учителю
|
||
или родителям — её не стоит публиковать.
|
||
</Tip>
|
||
</>
|
||
),
|
||
},
|
||
{
|
||
id: 'forbidden-scripts',
|
||
icon: 'shield',
|
||
title: 'Что запрещено в скриптах',
|
||
body: (
|
||
<>
|
||
<p>
|
||
Скрипты в игре выполняются в <b>песочнице</b> — у них нет
|
||
доступа к «настоящему» интернету и к чужим данным. При
|
||
публикации система автоматически проверяет код. Если она
|
||
находит опасные команды — игра уходит на ручную проверку.
|
||
</p>
|
||
<p>В скриптах нельзя использовать:</p>
|
||
<No>
|
||
<b>Выполнение «кода из строки»</b> — <code>eval</code>,
|
||
{' '}<code>Function(...)</code>. Так прячут вредоносный
|
||
код, поэтому это запрещено.
|
||
</No>
|
||
<No>
|
||
<b>Сетевые запросы</b> — <code>fetch</code>,
|
||
{' '}<code>XMLHttpRequest</code>, WebSocket к чужим
|
||
адресам. Игра не должна сама лазить в интернет.
|
||
</No>
|
||
<No>
|
||
<b>Доступ к странице браузера</b> — <code>window</code>,
|
||
{' '}<code>document</code>, <code>localStorage</code>,
|
||
{' '}<code>cookie</code>. Скрипт игры работает только с
|
||
игровым миром через <code>game.*</code>, не со страницей.
|
||
</No>
|
||
<No>
|
||
<b>Попытки «зависить» игру нарочно</b> — бесконечные
|
||
циклы без выхода, создание миллионов объектов, чтобы
|
||
у других всё затормозило.
|
||
</No>
|
||
<Tip>
|
||
Всё, что нужно для игры, уже есть в безопасном API
|
||
{' '}<code>game.*</code>: движение игрока, спавн
|
||
объектов, очки, звуки, интерфейс. Полный список — в вике,
|
||
раздел про скрипты. Если пишешь игру по урокам из вики —
|
||
запрещённый код туда просто не попадёт.
|
||
</Tip>
|
||
<p>
|
||
Важно: автопроверка не наказывает за случайность. Если
|
||
игра честная, а слово просто совпало — админ увидит это
|
||
при ручной проверке и опубликует игру.
|
||
</p>
|
||
</>
|
||
),
|
||
},
|
||
{
|
||
id: 'reports',
|
||
icon: 'bug',
|
||
title: 'Жалобы игроков и блокировки',
|
||
body: (
|
||
<>
|
||
<p>
|
||
Под каждой игрой есть кнопка <b>«Пожаловаться»</b>. Если
|
||
игроки массово жалуются на твою игру — её посмотрит
|
||
админ. Найдёт нарушение — заблокирует.
|
||
</p>
|
||
<p>За что чаще всего прилетает жалоба и блокировка:</p>
|
||
<ul>
|
||
<li>в игре мат, грубость или обидные надписи;</li>
|
||
<li>
|
||
игра — пустышка: зашёл, а играть не во что (нет цели,
|
||
нет геймплея);
|
||
</li>
|
||
<li>
|
||
название и обложка обещают одно, а в игре совсем
|
||
другое (обман игрока);
|
||
</li>
|
||
<li>игра намеренно тормозит или «вылетает»;</li>
|
||
<li>это копия чужой игры.</li>
|
||
</ul>
|
||
<p>Если твою игру заблокировали:</p>
|
||
<ol>
|
||
<li>прочитай комментарий админа — там написана причина;</li>
|
||
<li>исправь то, на что указали;</li>
|
||
<li>опубликуй игру заново.</li>
|
||
</ol>
|
||
<No>
|
||
Не пытайся обойти блокировку: переименовать игру и залить
|
||
то же самое снова, спамить одинаковыми играми, делать
|
||
несколько аккаунтов. За это блокируют уже не игру, а
|
||
аккаунт.
|
||
</No>
|
||
</>
|
||
),
|
||
},
|
||
{
|
||
id: 'popular',
|
||
icon: 'trophy',
|
||
title: 'Как сделать игру популярной',
|
||
body: (
|
||
<>
|
||
<p>
|
||
В ленте Рублокса игры показываются не по порядку, а по
|
||
«рейтингу интереса». Чем интереснее игра, тем выше она в
|
||
рекомендациях. На рейтинг влияют четыре вещи:
|
||
</p>
|
||
<div className="factorList">
|
||
<div className="factorRow">
|
||
<span className="factorIco"><DocIcon name="star" size={18} /></span>
|
||
<div>
|
||
<b>Лайки и дизлайки.</b> Игроки голосуют после
|
||
игры. Важно не само число лайков, а их доля:
|
||
маленькая честная игра с одними лайками
|
||
обгоняет большую с дизлайками.
|
||
</div>
|
||
</div>
|
||
<div className="factorRow">
|
||
<span className="factorIco"><DocIcon name="run" size={18} /></span>
|
||
<div>
|
||
<b>Сколько в неё играют.</b> Чем больше игрок
|
||
проводит времени в игре и не закрывает её через
|
||
10 секунд — тем выше рейтинг.
|
||
</div>
|
||
</div>
|
||
<div className="factorRow">
|
||
<span className="factorIco"><DocIcon name="globeIcon" size={18} /></span>
|
||
<div>
|
||
<b>Количество запусков.</b> Сколько раз игру
|
||
вообще открыли. Популярные игры поднимаются выше.
|
||
</div>
|
||
</div>
|
||
<div className="factorRow">
|
||
<span className="factorIco"><DocIcon name="sparkles" size={18} /></span>
|
||
<div>
|
||
<b>Свежесть.</b> Новым играм лента даёт «фору» —
|
||
их какое-то время показывают чаще, чтобы их
|
||
успели заметить.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p>
|
||
Что реально делать, чтобы игра нравилась людям:
|
||
</p>
|
||
<Tip>
|
||
<b>Дай игроку понятную цель.</b> С первых секунд должно
|
||
быть ясно: куда идти и что делать. «Добеги до флага»,
|
||
«собери 10 монет», «победи всех зомби».
|
||
</Tip>
|
||
<Tip>
|
||
<b>Сделай красивую обложку и честное название.</b> По
|
||
карточке игрок решает, заходить ли. Обложка должна
|
||
показывать саму игру, а не что-то постороннее.
|
||
</Tip>
|
||
<Tip>
|
||
<b>Добавь звук.</b> Игра без звука кажется «мёртвой».
|
||
Звук на прыжок, на монетку, на победу и проигрыш — и
|
||
игра сразу живее.
|
||
</Tip>
|
||
<Tip>
|
||
<b>Сделай так, чтобы было не слишком сложно и не слишком
|
||
скучно.</b> Начало — лёгкое, дальше — постепенно сложнее.
|
||
Если игрок умирает на первой секунде, он закроет игру.
|
||
</Tip>
|
||
<Tip>
|
||
<b>Проверь игру перед публикацией.</b> Пройди её сам от
|
||
начала до конца. Нет ли мест, где застреваешь, не падаешь
|
||
ли сквозь пол, доходит ли дело до победы.
|
||
</Tip>
|
||
<Tip>
|
||
<b>Обновляй игру.</b> Чини баги, добавляй уровни. Лента
|
||
любит игры, которые живут и развиваются.
|
||
</Tip>
|
||
<No>
|
||
Не пытайся «накрутить» рейтинг: лайкать свою игру с
|
||
кучи аккаунтов, просить друзей спамить запуски. Система
|
||
это замечает, и игра наоборот падает в ленте.
|
||
</No>
|
||
</>
|
||
),
|
||
},
|
||
{
|
||
id: 'checklist',
|
||
icon: 'target',
|
||
title: 'Чек-лист перед публикацией',
|
||
body: (
|
||
<>
|
||
<p>
|
||
Пройдись по списку перед тем, как нажать «Опубликовать».
|
||
Если на все пункты ответ «да» — игру можно публиковать.
|
||
</p>
|
||
<div className="checkList">
|
||
{[
|
||
'В игре есть понятная цель, и игрок сразу её понимает.',
|
||
'Игру можно пройти от начала до конца — я проверил сам.',
|
||
'Нигде нет мата, грубостей и обидных надписей.',
|
||
'Нет жестокости, крови и взрослого контента.',
|
||
'Название и обложка честно показывают, что это за игра.',
|
||
'Игра не тормозит и не вылетает.',
|
||
'В игре есть звук на главные события.',
|
||
'Это моя игра, а не копия чужой.',
|
||
'Я не использовал запрещённый код в скриптах.',
|
||
].map((t, i) => (
|
||
<div key={i} className="checkItem">
|
||
<span className="checkBox">
|
||
<Icon name="check" size={12} />
|
||
</span>
|
||
<span>{t}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<Tip>
|
||
Сомневаешься, можно ли публиковать игру — лучше спроси
|
||
учителя или оставь её черновиком. Заблокированная игра
|
||
портит твой профиль, а исправить и переопубликовать
|
||
можно всегда.
|
||
</Tip>
|
||
</>
|
||
),
|
||
},
|
||
];
|
||
|
||
const KubikonRules = () => {
|
||
const navigate = useNavigate();
|
||
const { isDesktop } = useDeviceType();
|
||
const mainRef = useRef(null);
|
||
|
||
// активный раздел для подсветки в оглавлении
|
||
const [activeSec, setActiveSec] = useState(RULES[0].id);
|
||
|
||
// подсветка активного раздела при скролле
|
||
useEffect(() => {
|
||
const main = mainRef.current;
|
||
if (!main) return;
|
||
const onScroll = () => {
|
||
const top = main.scrollTop + 140;
|
||
let cur = RULES[0].id;
|
||
for (const r of RULES) {
|
||
const el = main.querySelector(`#rule-${r.id}`);
|
||
if (el && el.offsetTop <= top) cur = r.id;
|
||
}
|
||
setActiveSec(cur);
|
||
};
|
||
main.addEventListener('scroll', onScroll);
|
||
return () => main.removeEventListener('scroll', onScroll);
|
||
}, []);
|
||
|
||
if (!isDesktop) {
|
||
return <KubikonDesktopOnlyStub feature="Правила создания игр Рублокс" />;
|
||
}
|
||
|
||
const scrollToSec = (id) => {
|
||
const main = mainRef.current;
|
||
const el = main && main.querySelector(`#rule-${id}`);
|
||
if (el) main.scrollTo({ top: el.offsetTop - 16, behavior: 'smooth' });
|
||
};
|
||
|
||
return (
|
||
<div className={cl.studio}>
|
||
<style>{INLINE_STYLES}</style>
|
||
|
||
{/* === Левая боковая панель === */}
|
||
<aside className={cl.sidebar}>
|
||
<div className={cl.sidebarHeader}>
|
||
<div className={cl.sidebarLogo}>
|
||
<RublocsLogo size={44} bg="#16181d" />
|
||
</div>
|
||
<div className={cl.sidebarTitle}>
|
||
<div className={cl.brandName}>Рублокс</div>
|
||
<div className={cl.brandSub}>Студия</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Меню — единое со страницей Studio (7 пунктов). */}
|
||
<nav className={cl.sidebarNav}>
|
||
<button
|
||
className={cl.navNewGame}
|
||
onClick={() => navigate('/edit/new')}
|
||
>
|
||
<span className={cl.navNewGameIco}><Icon name="plus" size={15} /></span>
|
||
<span>Новая игра</span>
|
||
</button>
|
||
<button className={cl.navItem} onClick={() => navigate('/?tab=recent')}>
|
||
<span className={cl.navIcon}><Icon name="clock" size={15} /></span>
|
||
<span>История</span>
|
||
</button>
|
||
<button className={cl.navItem} onClick={() => navigate('/?tab=home')}>
|
||
<span className={cl.navIcon}><Icon name="home" size={15} /></span>
|
||
<span>Главная</span>
|
||
</button>
|
||
<button className={cl.navItem} onClick={() => navigate('/?tab=my')}>
|
||
<span className={cl.navIcon}><Icon name="folder" size={15} /></span>
|
||
<span>Мои игры</span>
|
||
</button>
|
||
<button className={cl.navItem} onClick={() => navigate('/?tab=templates')}>
|
||
<span className={cl.navIcon}><Icon name="grid" size={15} /></span>
|
||
<span>Шаблоны</span>
|
||
</button>
|
||
<button className={cl.navItem} onClick={() => navigate('/?tab=archive')}>
|
||
<span className={cl.navIcon}><Icon name="archive" size={15} /></span>
|
||
<span>Архив</span>
|
||
</button>
|
||
<button className={cl.navItem} onClick={() => navigate('/docs')}>
|
||
<span className={cl.navIcon}><Icon name="book-open" size={15} /></span>
|
||
<span>ВИКИ</span>
|
||
</button>
|
||
</nav>
|
||
|
||
<div className={cl.sidebarFooter}>
|
||
<button
|
||
className={cl.docsBtn}
|
||
style={{
|
||
background: '#3357ff', color: '#fff', borderColor: 'transparent',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||
}}
|
||
onClick={() => navigate('/rules')}
|
||
>
|
||
<Icon name="shield" size={15} /> Правила создания игр
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
|
||
{/* === Основной контент === */}
|
||
<main className={cl.main} ref={mainRef}>
|
||
<header className={cl.topBar}>
|
||
<div>
|
||
<h1 className={cl.pageTitle}>
|
||
<Icon name="shield" size={13} /> Правила создания игр
|
||
</h1>
|
||
<p className={cl.pageSub}>
|
||
Какие игры можно публиковать, чтобы их не
|
||
заблокировали и в них играли
|
||
</p>
|
||
</div>
|
||
<div className={cl.topBarActions}>
|
||
<button className={cl.searchBox} onClick={() => navigate('/')}>
|
||
← В Studio
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Hero */}
|
||
<section className="rulesHero">
|
||
<div className="rulesHeroContent">
|
||
<div className="rulesHeroBadge">
|
||
<DocIcon name="shield" size={13} /> Правила платформы
|
||
</div>
|
||
<h2 className="rulesHeroTitle">
|
||
Создавай игры, которые полюбят
|
||
</h2>
|
||
<p className="rulesHeroDesc">
|
||
В Рублоксе ты публикуешь игры сам, без долгой
|
||
модерации. Эти правила помогут: твою игру не
|
||
заблокируют, а игроки будут в неё играть.
|
||
Прочитай их один раз — и всё будет получаться.
|
||
</p>
|
||
</div>
|
||
<div className="rulesHeroEmoji"><DocIcon name="shield" size={88} /></div>
|
||
</section>
|
||
|
||
{/* Body: оглавление + разделы */}
|
||
<section className="rulesBody">
|
||
<aside className="rulesToc">
|
||
<div className="rulesTocTitle">
|
||
<DocIcon name="scroll" size={16} /> Разделы
|
||
</div>
|
||
{RULES.map((r) => (
|
||
<button
|
||
key={r.id}
|
||
className={`rulesTocItem ${activeSec === r.id ? 'rulesTocItemActive' : ''}`}
|
||
onClick={() => scrollToSec(r.id)}
|
||
>
|
||
{r.title}
|
||
</button>
|
||
))}
|
||
</aside>
|
||
|
||
<div className="rulesContent">
|
||
{RULES.map((r) => (
|
||
<article key={r.id} id={`rule-${r.id}`} className="ruleChapter">
|
||
<h3 className="ruleTitle">
|
||
<span className="ruleTitle__ico">
|
||
<DocIcon name={r.icon} size={20} />
|
||
</span>
|
||
{r.title}
|
||
</h3>
|
||
<div className="ruleText">{r.body}</div>
|
||
</article>
|
||
))}
|
||
|
||
{/* Финальный CTA */}
|
||
<div className="rulesCta">
|
||
<div className="rulesCtaIcon"><DocIcon name="rocket" size={48} /></div>
|
||
<div className="rulesCtaTitle">Готов создавать?</div>
|
||
<div className="rulesCtaText">
|
||
Открой вику с 50 играми-уроками — там готовые
|
||
игры с инструкциями. Бери за основу и делай
|
||
свою по этим правилам.
|
||
</div>
|
||
<button
|
||
className="rulesCtaBtn"
|
||
onClick={() => navigate('/docs')}
|
||
>
|
||
Открыть вику →
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// ══════════════════════════════════════════════════════════════════
|
||
// Инлайн-стили — страница самодостаточна, не зависит от вики.
|
||
// ══════════════════════════════════════════════════════════════════
|
||
const INLINE_STYLES = `
|
||
.rulesHero {
|
||
background: linear-gradient(135deg, #3357ff 0%, #1e2da5 60%, #6d28d9 100%);
|
||
background-size: 200% 200%;
|
||
animation: rulesGradient 16s ease-in-out infinite;
|
||
border-radius: 28px;
|
||
padding: 36px 40px;
|
||
margin-bottom: 28px;
|
||
color: #fff;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 24px;
|
||
box-shadow: 0 24px 56px rgba(51, 87, 255, 0.28);
|
||
}
|
||
@keyframes rulesGradient {
|
||
0%, 100% { background-position: 0% 50%; }
|
||
50% { background-position: 100% 50%; }
|
||
}
|
||
.rulesHeroContent { flex: 1; min-width: 0; }
|
||
.rulesHeroBadge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
background: rgba(255, 255, 255, 0.20);
|
||
border: 1px solid rgba(255, 255, 255, 0.30);
|
||
color: #fff;
|
||
padding: 5px 14px;
|
||
border-radius: 999px;
|
||
font-size: 11px;
|
||
font-weight: 800;
|
||
letter-spacing: 1.4px;
|
||
text-transform: uppercase;
|
||
margin-bottom: 12px;
|
||
}
|
||
.rulesHeroTitle {
|
||
margin: 0 0 10px;
|
||
font-size: 32px;
|
||
font-weight: 900;
|
||
color: #fff;
|
||
letter-spacing: -1px;
|
||
line-height: 1.1;
|
||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.18);
|
||
}
|
||
.rulesHeroDesc {
|
||
margin: 0;
|
||
font-size: 15px;
|
||
color: rgba(255, 255, 255, 0.94);
|
||
line-height: 1.55;
|
||
max-width: 680px;
|
||
}
|
||
.rulesHeroEmoji {
|
||
flex-shrink: 0;
|
||
color: #fff;
|
||
opacity: 0.92;
|
||
animation: rulesFloat 6s ease-in-out infinite;
|
||
filter: drop-shadow(0 12px 24px rgba(0, 0, 0, 0.20));
|
||
}
|
||
@keyframes rulesFloat {
|
||
0%, 100% { transform: translateY(0) rotate(-2deg); }
|
||
50% { transform: translateY(-10px) rotate(3deg); }
|
||
}
|
||
|
||
/* === Body: оглавление + контент === */
|
||
.rulesBody {
|
||
display: grid;
|
||
grid-template-columns: 250px minmax(0, 1fr);
|
||
gap: 28px;
|
||
align-items: flex-start;
|
||
}
|
||
.rulesToc {
|
||
position: sticky;
|
||
top: 8px;
|
||
background: #fff;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 16px;
|
||
padding: 12px 8px;
|
||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
|
||
}
|
||
.rulesTocTitle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 12px 10px;
|
||
font-size: 13px;
|
||
font-weight: 900;
|
||
color: #0f172a;
|
||
border-bottom: 1px solid #eef2f7;
|
||
margin-bottom: 6px;
|
||
}
|
||
.rulesTocTitle svg { color: #3357ff; flex-shrink: 0; }
|
||
.rulesTocItem {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 9px;
|
||
color: #475569;
|
||
font-size: 12.5px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
transition: all 160ms ease;
|
||
font-family: inherit;
|
||
line-height: 1.4;
|
||
}
|
||
.rulesTocItem:hover { background: #f1f5f9; color: #0f172a; }
|
||
.rulesTocItemActive, .rulesTocItemActive:hover {
|
||
background: linear-gradient(135deg, #3357ff 0%, #1e2da5 100%);
|
||
color: #fff;
|
||
}
|
||
|
||
.rulesContent {
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18px;
|
||
}
|
||
|
||
/* === Раздел правил === */
|
||
.ruleChapter {
|
||
background: #fff;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 18px;
|
||
padding: 24px 28px;
|
||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
|
||
scroll-margin-top: 16px;
|
||
}
|
||
.ruleTitle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin: 0 0 16px;
|
||
font-size: 20px;
|
||
font-weight: 900;
|
||
color: #0f172a;
|
||
letter-spacing: -0.4px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid #eef2f7;
|
||
}
|
||
.ruleTitle__ico {
|
||
flex-shrink: 0;
|
||
width: 38px; height: 38px;
|
||
border-radius: 11px;
|
||
background: linear-gradient(135deg, #3357ff 0%, #1e2da5 100%);
|
||
color: #fff;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.ruleText { color: #334155; font-size: 14.5px; line-height: 1.65; }
|
||
.ruleText p { margin: 0 0 12px; }
|
||
.ruleText p:last-child { margin-bottom: 0; }
|
||
.ruleText ul, .ruleText ol { margin: 0 0 12px; padding-left: 22px; }
|
||
.ruleText li { margin-bottom: 8px; line-height: 1.55; }
|
||
.ruleText li:last-child { margin-bottom: 0; }
|
||
.ruleText b { color: #0f172a; font-weight: 800; }
|
||
.ruleText code {
|
||
background: #e0e8ff;
|
||
color: #3357ff;
|
||
padding: 2px 7px;
|
||
border-radius: 6px;
|
||
font-family: Consolas, Menlo, "Courier New", monospace;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
/* === Плашки можно / нельзя / совет === */
|
||
.ruleBox {
|
||
display: flex;
|
||
gap: 11px;
|
||
align-items: flex-start;
|
||
border-radius: 11px;
|
||
padding: 12px 14px;
|
||
margin: 10px 0;
|
||
font-size: 13.5px;
|
||
line-height: 1.55;
|
||
border: 1px solid;
|
||
border-left-width: 4px;
|
||
}
|
||
.ruleBox__ico { flex-shrink: 0; line-height: 0; margin-top: 1px; }
|
||
.ruleBox--ok {
|
||
background: #f0fdf4;
|
||
border-color: #bbf7d0;
|
||
border-left-color: #16a34a;
|
||
color: #14532d;
|
||
}
|
||
.ruleBox--ok .ruleBox__ico { color: #16a34a; }
|
||
.ruleBox--no {
|
||
background: #fef2f2;
|
||
border-color: #fecaca;
|
||
border-left-color: #dc2626;
|
||
color: #7f1d1d;
|
||
}
|
||
.ruleBox--no .ruleBox__ico { color: #dc2626; }
|
||
.ruleBox--no code { background: #fee2e2; color: #b91c1c; }
|
||
.ruleBox--tip {
|
||
background: #fffbeb;
|
||
border-color: #fde68a;
|
||
border-left-color: #f59e0b;
|
||
color: #78350f;
|
||
}
|
||
.ruleBox--tip .ruleBox__ico { color: #b45309; }
|
||
.ruleBox--tip code { background: #fef3c7; color: #92400e; }
|
||
|
||
/* === Список статусов === */
|
||
.statusList {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
margin: 12px 0;
|
||
}
|
||
.statusRow {
|
||
display: flex;
|
||
gap: 11px;
|
||
align-items: flex-start;
|
||
background: #f8fafc;
|
||
border: 1px solid #eef2f7;
|
||
border-radius: 10px;
|
||
padding: 11px 14px;
|
||
}
|
||
.statusDot {
|
||
flex-shrink: 0;
|
||
width: 12px; height: 12px;
|
||
border-radius: 50%;
|
||
margin-top: 4px;
|
||
}
|
||
.statusName { color: #0f172a; font-weight: 800; }
|
||
.statusText { color: #475569; }
|
||
|
||
/* === Факторы рейтинга === */
|
||
.factorList {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
margin: 12px 0;
|
||
}
|
||
.factorRow {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: flex-start;
|
||
background: #f5f7ff;
|
||
border: 1px solid #e0e8ff;
|
||
border-radius: 11px;
|
||
padding: 12px 14px;
|
||
}
|
||
.factorIco {
|
||
flex-shrink: 0;
|
||
width: 32px; height: 32px;
|
||
border-radius: 9px;
|
||
background: #fff;
|
||
border: 1px solid #d6e0ff;
|
||
color: #3357ff;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
|
||
/* === Чек-лист === */
|
||
.checkList {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
margin: 12px 0;
|
||
}
|
||
.checkItem {
|
||
display: flex;
|
||
gap: 11px;
|
||
align-items: center;
|
||
background: #f0fdf4;
|
||
border: 1px solid #bbf7d0;
|
||
border-radius: 10px;
|
||
padding: 10px 14px;
|
||
font-size: 13.5px;
|
||
color: #14532d;
|
||
line-height: 1.45;
|
||
}
|
||
.checkBox {
|
||
flex-shrink: 0;
|
||
width: 20px; height: 20px;
|
||
border-radius: 6px;
|
||
background: #16a34a;
|
||
color: #fff;
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
|
||
/* === Финальный CTA === */
|
||
.rulesCta {
|
||
background: linear-gradient(135deg, #3357ff 0%, #1e2da5 100%);
|
||
border-radius: 24px;
|
||
padding: 32px;
|
||
text-align: center;
|
||
color: #fff;
|
||
box-shadow: 0 16px 40px rgba(51, 87, 255, 0.32);
|
||
margin-top: 8px;
|
||
}
|
||
.rulesCtaIcon {
|
||
display: flex;
|
||
justify-content: center;
|
||
color: #fff;
|
||
margin-bottom: 10px;
|
||
}
|
||
.rulesCtaTitle { font-size: 22px; font-weight: 900; margin-bottom: 8px; }
|
||
.rulesCtaText {
|
||
font-size: 14px;
|
||
color: rgba(255, 255, 255, 0.90);
|
||
margin: 0 auto 20px;
|
||
max-width: 520px;
|
||
line-height: 1.55;
|
||
}
|
||
.rulesCtaBtn {
|
||
padding: 12px 26px;
|
||
background: #fff;
|
||
color: #3357ff;
|
||
border: none;
|
||
border-radius: 10px;
|
||
font-size: 14px;
|
||
font-weight: 900;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
}
|
||
.rulesCtaBtn:hover { transform: translateY(-2px); }
|
||
|
||
/* Адаптив */
|
||
@media (max-width: 980px) {
|
||
.rulesBody { grid-template-columns: 1fr; }
|
||
.rulesToc { position: static; }
|
||
.rulesHero { flex-direction: column; padding: 28px 24px; text-align: center; }
|
||
.ruleChapter { padding: 20px; }
|
||
}
|
||
`;
|
||
|
||
export default KubikonRules;
|