Compare commits
13 Commits
dc669a51f4
...
8a66ce03f9
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a66ce03f9 | |||
| 09a927bbfc | |||
| eedac4379d | |||
| 61026a1df0 | |||
| ddeb8ff93f | |||
| 3a95cd148a | |||
| e78f585fd4 | |||
| 57c5779644 | |||
| d758fdfbe6 | |||
| 1c5e5fe5bb | |||
| 9e89fb1936 | |||
|
|
b95e5c7401 | ||
|
|
4757bf95a0 |
@ -10,7 +10,7 @@ export const USER_addres = BASE + '/api-user';
|
|||||||
export const ACHIVES_addres = BASE + '/api-achievs';
|
export const ACHIVES_addres = BASE + '/api-achievs';
|
||||||
export const COMMENTS_addres = BASE + '/api-comments';
|
export const COMMENTS_addres = BASE + '/api-comments';
|
||||||
export const STORYS_addres = BASE + '/api-storys';
|
export const STORYS_addres = BASE + '/api-storys';
|
||||||
// rbxl-importer: только для МИНа (тест-фича импорта .rbxl карт Roblox)
|
// rbxl-importer: импорт .rbxl карт Roblox (см. вики «Импорт из Roblox»)
|
||||||
export const RBXL_addres = BASE + '/api-rbxl';
|
export const RBXL_addres = BASE + '/api-rbxl';
|
||||||
export const NOTICES_addres = BASE + '/api-notices';
|
export const NOTICES_addres = BASE + '/api-notices';
|
||||||
export const HELP_addres = BASE + '/api-help';
|
export const HELP_addres = BASE + '/api-help';
|
||||||
|
|||||||
@ -390,18 +390,16 @@ const KubikonStudio = () => {
|
|||||||
<span className={cl.navIcon}><Icon name="book-open" size={15} /></span>
|
<span className={cl.navIcon}><Icon name="book-open" size={15} /></span>
|
||||||
<span>ВИКИ</span>
|
<span>ВИКИ</span>
|
||||||
</button>
|
</button>
|
||||||
{/* Импорт Roblox .rbxl — только для МИНа (user_id=1) */}
|
{/* Импорт Roblox .rbxl — доступно всем */}
|
||||||
{getCurrentUserId() === 1 && (
|
|
||||||
<button
|
<button
|
||||||
className={cl.navItem}
|
className={cl.navItem}
|
||||||
onClick={() => setRbxlImportOpen(true)}
|
onClick={() => setRbxlImportOpen(true)}
|
||||||
title="Импортировать игру из Roblox (.rbxl файл) — тест-фича"
|
title="Импортировать игру из Roblox (.rbxl файл)"
|
||||||
style={{ marginTop: 8, borderTop: '1px solid #333', paddingTop: 12 }}
|
style={{ marginTop: 8, borderTop: '1px solid #333', paddingTop: 12 }}
|
||||||
>
|
>
|
||||||
<span className={cl.navIcon}>📦</span>
|
<span className={cl.navIcon}>📦</span>
|
||||||
<span>Импорт Roblox</span>
|
<span>Импорт Roblox</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<RbxlImportModal
|
<RbxlImportModal
|
||||||
|
|||||||
@ -5073,6 +5073,350 @@ end)`}</Code>}
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
// РАЗДЕЛ — ИМПОРТ ИЗ ROBLOX (.rbxl)
|
||||||
|
// ════════════════════════════════════════════════════
|
||||||
|
{
|
||||||
|
id: 'rbxl-import',
|
||||||
|
icon: 'package',
|
||||||
|
title: 'Импорт из Roblox',
|
||||||
|
summary: 'Загрузи .rbxl-файл готовой Roblox-карты в Рублокс: геометрия, цвета и материалы переносятся хорошо, скрипты лучше отключить при первом импорте и включать постепенно.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: 'rbxl-overview',
|
||||||
|
title: 'I1. Что это и зачем',
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<b>Импорт из Roblox</b> — это возможность загрузить
|
||||||
|
в Рублокс <b>готовую карту из Roblox Studio</b> в формате
|
||||||
|
<b> .rbxl</b> или <b>.rbxlx</b>. Часть карты — геометрия,
|
||||||
|
цвета, материалы, GUI — переносится в Рублокс
|
||||||
|
автоматически. Импорт превращает её в обычный проект
|
||||||
|
Рублокса, который можно редактировать, как любой
|
||||||
|
свой проект.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Кнопка <b>«📦 Импорт Roblox»</b> находится в левой
|
||||||
|
панели студии — внизу под кнопкой «ВИКИ». Откроется
|
||||||
|
модалка, куда можно перетащить .rbxl-файл.
|
||||||
|
</p>
|
||||||
|
<h3 className="lessonH">Зачем это нужно</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Перенести свою старую Roblox-карту в Рублокс,
|
||||||
|
чтобы продолжить работу здесь;</li>
|
||||||
|
<li>Изучить, как устроены большие карты опытных
|
||||||
|
разработчиков (классические <i>Crossroads</i>,
|
||||||
|
<i> ROBLOX Battle</i> и др.);</li>
|
||||||
|
<li>Использовать готовую сцену как «костяк» для
|
||||||
|
своей игры — добавить свои скрипты и механики.</li>
|
||||||
|
</ul>
|
||||||
|
<Note>
|
||||||
|
Импорт работает с файлами до <b>50 МБ</b>. Этого
|
||||||
|
хватает для большинства карт. Очень большие карты
|
||||||
|
с тысячами объектов могут импортироваться долго
|
||||||
|
(20–60 секунд).
|
||||||
|
</Note>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rbxl-how-to-use',
|
||||||
|
title: 'I2. Как импортировать карту',
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<h3 className="lessonH">Шаг 1. Получи .rbxl-файл</h3>
|
||||||
|
<p>
|
||||||
|
В Roblox Studio открой свою карту и сохрани её
|
||||||
|
через меню <b>File → Save to File</b>. Получится файл
|
||||||
|
с расширением <b>.rbxl</b> (бинарный) или
|
||||||
|
<b> .rbxlx</b> (текстовый XML). Оба формата подходят.
|
||||||
|
</p>
|
||||||
|
<Note>
|
||||||
|
Если у тебя нет своей карты, можно скачать
|
||||||
|
любую <b>opensource Roblox-карту</b> с GitHub —
|
||||||
|
их там много (поиск «roblox places .rbxl»).
|
||||||
|
Классика — карта <i>Crossroads</i>.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
<h3 className="lessonH">Шаг 2. Загрузи в студию</h3>
|
||||||
|
<Step n="1">
|
||||||
|
Открой студию Рублокса и нажми кнопку
|
||||||
|
<b> «📦 Импорт Roblox»</b> в левой панели.
|
||||||
|
</Step>
|
||||||
|
<Step n="2">
|
||||||
|
Перетащи .rbxl-файл в окно модалки или нажми
|
||||||
|
<b> «Выбрать файл»</b>. Кликни <b>«Анализировать»</b>.
|
||||||
|
Это займёт 5–30 секунд: сервер читает структуру карты,
|
||||||
|
считает объекты, скрипты, ассеты и показывает отчёт.
|
||||||
|
</Step>
|
||||||
|
<Step n="3">
|
||||||
|
В отчёте увидишь, сколько в карте Part-ов, моделей,
|
||||||
|
скриптов, текстур и какие будут <b>предупреждения</b>.
|
||||||
|
Например: «найдены неподдерживаемые сетки», «много
|
||||||
|
BillboardGui — может тормозить» и т.п.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<h3 className="lessonH">Шаг 3. Настрой режим импорта</h3>
|
||||||
|
<p>
|
||||||
|
Перед созданием проекта выбери, как обращаться
|
||||||
|
со скриптами и GUI карты:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>Скрипты:</b>
|
||||||
|
<ul>
|
||||||
|
<li><b>Отключены</b> (рекомендуется) — все
|
||||||
|
скрипты импортируются, но <b>не запускаются</b>.
|
||||||
|
Карта оживает как декорация: можно ходить
|
||||||
|
и смотреть, но без логики.</li>
|
||||||
|
<li><b>Включены</b> — Lua-скрипты карты пытаются
|
||||||
|
запуститься. Часть API не поддерживается
|
||||||
|
(Roblox-only), некоторые карты могут зависать
|
||||||
|
или давать ошибки в консоли.</li>
|
||||||
|
<li><b>Удалить</b> — скрипты вообще не сохраняются.
|
||||||
|
Останется только геометрия.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>GUI:</b>
|
||||||
|
<ul>
|
||||||
|
<li><b>Все</b> — импортируется и HUD (ScreenGui),
|
||||||
|
и подписи над объектами (BillboardGui).</li>
|
||||||
|
<li><b>Только ScreenGui</b> — переносится
|
||||||
|
только HUD. Хорошо если в карте было
|
||||||
|
200+ табличек-вывесок города.</li>
|
||||||
|
<li><b>Пропустить</b> — не импортировать GUI.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 className="lessonH">Шаг 4. Создай проект</h3>
|
||||||
|
<Step n="1">
|
||||||
|
Введи название игры. Под этим именем карта появится
|
||||||
|
в твоих проектах.
|
||||||
|
</Step>
|
||||||
|
<Step n="2">
|
||||||
|
Нажми <b>«Создать игру»</b>. Сервер скачает текстуры
|
||||||
|
и сетки с Roblox CDN, соберёт проект и переведёт тебя
|
||||||
|
в редактор.
|
||||||
|
</Step>
|
||||||
|
<Step n="3">
|
||||||
|
Нажми <kbd className="kbd">Запустить</kbd> и осмотри
|
||||||
|
карту вживую.
|
||||||
|
</Step>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rbxl-graphics-vs-scripts',
|
||||||
|
title: 'I3. Графика хорошо, скрипты — осторожно',
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Главное, что нужно знать про импорт: <b>графика
|
||||||
|
переносится хорошо, скрипты — нет</b>. Это связано
|
||||||
|
с тем, что движок Рублокса (Babylon.js) и движок
|
||||||
|
Roblox — разные. Геометрия и материалы — это
|
||||||
|
«стандартный 3D», он одинаков везде. А скрипты
|
||||||
|
опираются на сотни Roblox-API, которые в Рублоксе
|
||||||
|
реализованы лишь частично.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 className="lessonH">Что переносится хорошо</h3>
|
||||||
|
<ul>
|
||||||
|
<li><b>Все Part-ы</b> (Brick, Cube, Sphere, Cylinder,
|
||||||
|
Wedge, CornerWedge) — позиция, размер, поворот,
|
||||||
|
цвет, материал, прозрачность;</li>
|
||||||
|
<li><b>Модели (Models)</b> — собранные группы Part-ов,
|
||||||
|
включая <i>Welds</i> (склейки);</li>
|
||||||
|
<li><b>Текстуры</b> и <b>Decals</b> — скачиваются
|
||||||
|
с Roblox CDN и применяются на Part-ы;</li>
|
||||||
|
<li><b>MeshPart-ы</b> — пользовательские 3D-сетки;</li>
|
||||||
|
<li><b>SpawnLocation</b> — точка появления игрока;</li>
|
||||||
|
<li><b>Lighting / Sky</b> — время суток, цвет неба;</li>
|
||||||
|
<li><b>GUI</b> (ScreenGui, TextLabel, TextButton,
|
||||||
|
ImageLabel, Frame) — простая разметка интерфейса
|
||||||
|
переносится верно.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 className="lessonH">Что переносится так себе</h3>
|
||||||
|
<ul>
|
||||||
|
<li><b>Lua-скрипты</b> — выполняются через нашу
|
||||||
|
реализацию Roblox API. <b>Базовые вещи работают</b>:
|
||||||
|
Touched, ClickDetector, TweenService, BindableEvent,
|
||||||
|
Vector3, CFrame, Workspace, Players. Но <b>десятки
|
||||||
|
специальных сервисов нет</b>: DataStoreService,
|
||||||
|
HttpService, MarketplaceService, MessagingService,
|
||||||
|
TeleportService и т.д.;</li>
|
||||||
|
<li><b>Анимации</b> — простые работают, сложные
|
||||||
|
rig-анимации (R15, custom) могут вести себя
|
||||||
|
не так;</li>
|
||||||
|
<li><b>Физика</b> — общая работает, но <i>Constraint-ы</i>,
|
||||||
|
<i> Motors</i>, <i>BodyMover-ы</i> (BodyVelocity,
|
||||||
|
BodyPosition и др.) имеют упрощённую реализацию;</li>
|
||||||
|
<li><b>Звуки</b> — переносятся, но Roblox SoundId
|
||||||
|
(<i>rbxassetid://</i>) у нас не играет напрямую —
|
||||||
|
лучше заменить на наши звуки (<i>'click'</i>,
|
||||||
|
<i> 'win'</i>, <i>'coin'</i> и т.д.).</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 className="lessonH">Что точно не переносится</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Мультиплеерная логика на Roblox-Remote-Event-ах —
|
||||||
|
у нас своя система мультиплеера;</li>
|
||||||
|
<li>Покупки, бонусы, премиум — Roblox-only;</li>
|
||||||
|
<li>Облачное сохранение (DataStore) — у нас другое;</li>
|
||||||
|
<li>Чат, аватары пользователей Roblox.</li>
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rbxl-recommended-flow',
|
||||||
|
title: 'I4. Правильный порядок: сначала графика, потом скрипты',
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Импорт <b>хорошо работает в два прохода</b>. Не
|
||||||
|
пытайся всё запустить сразу — карта может встать
|
||||||
|
колом из-за ошибок в чужих скриптах. Делай так:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 className="lessonH">Проход 1. Импорт без скриптов</h3>
|
||||||
|
<Step n="1">
|
||||||
|
Перед созданием проекта выбери в режиме скриптов
|
||||||
|
<b> «Отключены»</b>.
|
||||||
|
</Step>
|
||||||
|
<Step n="2">
|
||||||
|
Создай проект. Запусти игру и пройдись по карте.
|
||||||
|
Смотри только на <b>графику</b>:
|
||||||
|
<ul>
|
||||||
|
<li>как лежат блоки и модели;</li>
|
||||||
|
<li>правильные ли материалы и цвета;</li>
|
||||||
|
<li>работают ли текстуры;</li>
|
||||||
|
<li>как выглядит освещение и небо;</li>
|
||||||
|
<li>нет ли провалов / летающих в воздухе кусков.</li>
|
||||||
|
</ul>
|
||||||
|
</Step>
|
||||||
|
<Step n="3">
|
||||||
|
Если что-то выглядит криво — это <b>хорошо</b>:
|
||||||
|
твой проект не сломан скриптами, можно спокойно
|
||||||
|
поправить геометрию руками в редакторе.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<h3 className="lessonH">Проход 2. Аккуратно включай скрипты</h3>
|
||||||
|
<p>
|
||||||
|
После того как графика тебя устраивает, можно
|
||||||
|
<b> по одному</b> включать скрипты карты. Это
|
||||||
|
делается уже в редакторе проекта, в панели
|
||||||
|
<b> «Скрипты»</b>:
|
||||||
|
</p>
|
||||||
|
<Step n="1">
|
||||||
|
Открой панель скриптов. У каждого скрипта рядом
|
||||||
|
с названием есть тумблер <b>«Включён»</b>.
|
||||||
|
По умолчанию после импорта все они выключены.
|
||||||
|
</Step>
|
||||||
|
<Step n="2">
|
||||||
|
Включай скрипты <b>по одному</b>, начиная с самых
|
||||||
|
простых — те, что висят на Touched-частях
|
||||||
|
(кнопки, телепорты, ловушки). Запускай игру,
|
||||||
|
смотри, всё ли работает.
|
||||||
|
</Step>
|
||||||
|
<Step n="3">
|
||||||
|
Если включил скрипт и игра <b>зависла или замусорила
|
||||||
|
консоль ошибками</b> — выключи его обратно. Это
|
||||||
|
нормально. У этого скрипта, скорее всего, есть
|
||||||
|
Roblox-API, которого у нас нет.
|
||||||
|
</Step>
|
||||||
|
<Step n="4">
|
||||||
|
Для важных скриптов, которые не работают, можно
|
||||||
|
либо <b>переписать</b> их под Рублокс (у нас простой
|
||||||
|
JS-API через <code>game.*</code>), либо
|
||||||
|
<b> заменить</b> на свой скрипт с похожей логикой.
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Не расстраивайся, если из 200 скриптов сразу
|
||||||
|
заработает только 50. Это нормально — большие
|
||||||
|
Roblox-карты опираются на сложные сервисы. Зато
|
||||||
|
графика уже на месте, и тебе остаётся написать
|
||||||
|
несколько коротких скриптов на привычном JS.
|
||||||
|
</Note>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rbxl-tips-and-tricks',
|
||||||
|
title: 'I5. Советы и частые проблемы',
|
||||||
|
body: (
|
||||||
|
<>
|
||||||
|
<h3 className="lessonH">Частые ошибки</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>«Карта пустая после импорта»</b> — спавн-точка
|
||||||
|
могла оказаться под полом или в стене. В редакторе
|
||||||
|
переставь её в Game-вкладке: <b>точка спавна</b>
|
||||||
|
{' '}→ кликни в нужное место.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>«Текстуры серые»</b> — изредка Roblox CDN не
|
||||||
|
отдаёт текстуру (она удалена). Поставь свой цвет
|
||||||
|
или текстуру на этих Part-ах в редакторе.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>«Игра тормозит»</b> — обычно дело в большом
|
||||||
|
числе BillboardGui (вывески города) или unanchored
|
||||||
|
Part-ов. Импортируй заново с режимом GUI
|
||||||
|
<i> «Только ScreenGui»</i>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>«Скрипт даёт ошибку DataStoreService»</b> —
|
||||||
|
это Roblox-only сервис. Выключи скрипт или
|
||||||
|
замени логику сохранения на нашу.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>«Игрок проваливается под пол»</b> — иногда
|
||||||
|
у Part-а пола стоит <i>CanCollide=false</i>.
|
||||||
|
Выдели пол в редакторе и включи коллизию.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3 className="lessonH">Что делать после импорта</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Переименуй важные объекты на русский — так
|
||||||
|
удобнее писать новые скрипты;</li>
|
||||||
|
<li>Сгруппируй похожие Part-ы в модели (можно через
|
||||||
|
инспектор);</li>
|
||||||
|
<li>Поставь свои точки спавна и финиша на местах,
|
||||||
|
которые тебе нужны;</li>
|
||||||
|
<li>Сохрани <b>копию проекта</b> до того, как начнёшь
|
||||||
|
включать скрипты — на случай, если карта сломается.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Импорт — это <b>стартовая площадка</b>, а не готовый
|
||||||
|
продукт. Лучше всего он работает, когда ты берёшь
|
||||||
|
из Roblox-карты только <b>геометрию</b>, а механики
|
||||||
|
и интерактив пишешь сам на нашем JS-API
|
||||||
|
(через <code>game.*</code>). Так получится игра,
|
||||||
|
которая <b>стабильно работает</b> у тебя и у игроков.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
<h3 className="lessonH">Что почитать дальше</h3>
|
||||||
|
<p>
|
||||||
|
Если хочешь узнать, как переписать импортированный
|
||||||
|
Roblox-скрипт под наш движок — посмотри раздел
|
||||||
|
<b> «Скрипты»</b> в вики (D-G) и сравни Roblox-API
|
||||||
|
с нашим <code>game.*</code>. Многое делается похоже,
|
||||||
|
только короче.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════
|
||||||
// РАЗДЕЛ — СОВМЕСТНОЕ РЕДАКТИРОВАНИЕ (Team Create)
|
// РАЗДЕЛ — СОВМЕСТНОЕ РЕДАКТИРОВАНИЕ (Team Create)
|
||||||
// ════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════
|
||||||
|
|||||||
@ -4228,14 +4228,502 @@ end)`;
|
|||||||
})(),
|
})(),
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// ИГРЫ 45-50: явных Lua-версий пока нет.
|
// ИГРА 45 — «Стрелялка-арена»
|
||||||
// buildGameProject в docsGamesBuilders.js использует generateFallbackLua
|
// ═══════════════════════════════════════════════════════════════
|
||||||
// (главный скрипт → показ подсказки + слушает FinishReached →
|
'arena-shooter': {
|
||||||
// победа+конфетти; скрипт на финиш-примитиве → шлёт FinishReached;
|
g45_main: `-- === ИГРА «СТРЕЛЯЛКА-АРЕНА» — главный скрипт (Lua) ===
|
||||||
// остальные target-скрипты → красят примитив на касание).
|
${SNIPPET_BROADCAST}
|
||||||
// Это даёт «хоть что-то рабочее» в любой игре до того как напишем
|
|
||||||
// полноценный Lua-скрипт. Когда дописываем игру — добавляем сюда явный override.
|
local Players = game:GetService("Players")
|
||||||
'clicker': { g46_main: simpleClicker() },
|
local RunService = game:GetService("RunService")
|
||||||
|
local player = Players.LocalPlayer
|
||||||
|
|
||||||
|
local GOAL = 15
|
||||||
|
local score = 0
|
||||||
|
local over = false
|
||||||
|
|
||||||
|
-- Враги: { ref → { ref, alive, lastDmg } }
|
||||||
|
local enemies = {}
|
||||||
|
|
||||||
|
__rbxl_score_set(0)
|
||||||
|
__rbxl_show_text("Перебей 15 врагов! Кликай по ним", 3)
|
||||||
|
|
||||||
|
local hitSound = Instance.new("Sound", workspace)
|
||||||
|
hitSound.SoundId = "hit"; hitSound.Volume = 0.6
|
||||||
|
local winSound = Instance.new("Sound", workspace)
|
||||||
|
winSound.SoundId = "win"; winSound.Volume = 1
|
||||||
|
|
||||||
|
-- Подписка на смерть игрока
|
||||||
|
task.delay(0.5, function()
|
||||||
|
local char = player.Character or player.CharacterAdded:Wait()
|
||||||
|
local h = char:WaitForChild("Humanoid", 2)
|
||||||
|
if h then
|
||||||
|
h.Died:Connect(function()
|
||||||
|
if over then return end
|
||||||
|
over = true
|
||||||
|
__rbxl_show_text("Поражение! Тебя одолели враги.", 5)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Клик по врагу: EnemyClicked:Fire(ref)
|
||||||
|
local clickEvent = getEvent("EnemyClicked")
|
||||||
|
clickEvent.Event:Connect(function(localRef)
|
||||||
|
if over then return end
|
||||||
|
local e = enemies[localRef]
|
||||||
|
if not e or not e.alive then return end
|
||||||
|
local px = __rbxl_player_x()
|
||||||
|
local pz = __rbxl_player_z()
|
||||||
|
local ex = __rbxl_npc_x(localRef)
|
||||||
|
local ez = __rbxl_npc_z(localRef)
|
||||||
|
if ex == 0 and ez == 0 then return end
|
||||||
|
local dx = px - ex
|
||||||
|
local dz = pz - ez
|
||||||
|
local dist = math.sqrt(dx*dx + dz*dz)
|
||||||
|
if dist < 6 then
|
||||||
|
e.alive = false
|
||||||
|
__rbxl_npc_remove(localRef)
|
||||||
|
__rbxl_spawn_particles("explosion", ex, 2, ez, 0.4, 1)
|
||||||
|
hitSound:Play()
|
||||||
|
score = score + 1
|
||||||
|
__rbxl_score_set(score)
|
||||||
|
if score >= GOAL and not over then
|
||||||
|
over = true
|
||||||
|
__rbxl_show_text("Победа! Арена зачищена!", 5)
|
||||||
|
winSound:Play()
|
||||||
|
__rbxl_spawn_particles("confetti", px, 3, pz, 3, 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Спавн каждые 1.8с
|
||||||
|
local spawnTimer = 0
|
||||||
|
RunService.Heartbeat:Connect(function(dt)
|
||||||
|
if over or score >= GOAL then return end
|
||||||
|
spawnTimer = spawnTimer + dt
|
||||||
|
if spawnTimer < 1.8 then return end
|
||||||
|
spawnTimer = 0
|
||||||
|
local angle = math.random() * math.pi * 2
|
||||||
|
local ex = math.cos(angle) * 11
|
||||||
|
local ez = math.sin(angle) * 11
|
||||||
|
local ref = __rbxl_spawn_npc("character-b", ex, 1, ez, "Враг", 30, 2.2)
|
||||||
|
enemies[ref] = { ref = ref, alive = true, lastDmg = 0 }
|
||||||
|
task.delay(0.3, function()
|
||||||
|
__rbxl_npc_follow(ref, "player")
|
||||||
|
end)
|
||||||
|
__rbxl_npc_on_click(ref, function()
|
||||||
|
local ev = game:GetService("ReplicatedStorage"):FindFirstChild("EnemyClicked")
|
||||||
|
if ev then ev:Fire(ref) end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Враги бьют игрока вблизи (каждые 0.7с)
|
||||||
|
RunService.Heartbeat:Connect(function()
|
||||||
|
if over then return end
|
||||||
|
local px = __rbxl_player_x()
|
||||||
|
local pz = __rbxl_player_z()
|
||||||
|
local now = tick()
|
||||||
|
for _, e in pairs(enemies) do
|
||||||
|
if e.alive then
|
||||||
|
local ex = __rbxl_npc_x(e.ref)
|
||||||
|
local ez = __rbxl_npc_z(e.ref)
|
||||||
|
if not (ex == 0 and ez == 0) then
|
||||||
|
local dx = px - ex
|
||||||
|
local dz = pz - ez
|
||||||
|
local dist = math.sqrt(dx*dx + dz*dz)
|
||||||
|
if dist < 1.8 and now - e.lastDmg > 0.7 then
|
||||||
|
e.lastDmg = now
|
||||||
|
__rbxl_damage_player(10)
|
||||||
|
hitSound:Play()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// ИГРА 47 — «Квест-побег»
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
'escape-quest': (function() {
|
||||||
|
const BTN_COUNT = 3;
|
||||||
|
const overrides = {
|
||||||
|
g47_main: `-- === ИГРА «КВЕСТ-ПОБЕГ» — главный скрипт (Lua) ===
|
||||||
|
${SNIPPET_BROADCAST}
|
||||||
|
|
||||||
|
local TweenService = game:GetService("TweenService")
|
||||||
|
local TOTAL = ${BTN_COUNT}
|
||||||
|
local pressed = 0
|
||||||
|
local escaped = false
|
||||||
|
|
||||||
|
__rbxl_show_text("Найди и нажми 3 кнопки, чтобы выйти!", 4)
|
||||||
|
|
||||||
|
local clickSound = Instance.new("Sound", workspace)
|
||||||
|
clickSound.SoundId = "click"; clickSound.Volume = 0.6
|
||||||
|
local winSound = Instance.new("Sound", workspace)
|
||||||
|
winSound.SoundId = "win"; winSound.Volume = 1
|
||||||
|
|
||||||
|
local btnEvent = getEvent("ButtonPressed")
|
||||||
|
btnEvent.Event:Connect(function()
|
||||||
|
pressed = pressed + 1
|
||||||
|
clickSound:Play()
|
||||||
|
__rbxl_show_text("Кнопка " .. pressed .. " из " .. TOTAL, 1.5)
|
||||||
|
if pressed >= TOTAL then
|
||||||
|
local door = workspace:FindFirstChild("Дверь")
|
||||||
|
if door then
|
||||||
|
local dp = door.Position
|
||||||
|
local goal = { Position = Vector3.new(dp.X, dp.Y + 6, dp.Z) }
|
||||||
|
TweenService:Create(door, TweenInfo.new(1.2), goal):Play()
|
||||||
|
door.CanCollide = false
|
||||||
|
end
|
||||||
|
__rbxl_show_text("Все кнопки нажаты! Дверь открыта!", 3)
|
||||||
|
winSound:Play()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
local escEvent = getEvent("Escape")
|
||||||
|
escEvent.Event:Connect(function()
|
||||||
|
if escaped then return end
|
||||||
|
escaped = true
|
||||||
|
__rbxl_show_text("Победа! Ты сбежал из комнаты!", 5)
|
||||||
|
winSound:Play()
|
||||||
|
local px = __rbxl_player_x()
|
||||||
|
local py = __rbxl_player_y()
|
||||||
|
local pz = __rbxl_player_z()
|
||||||
|
__rbxl_spawn_particles("confetti", px, py + 3, pz, 3, 3)
|
||||||
|
end)`,
|
||||||
|
g47_finish: `-- === Скрипт финиша (Lua) ===
|
||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
local part = script.Parent
|
||||||
|
local fired = false
|
||||||
|
|
||||||
|
part.Touched:Connect(function(hit)
|
||||||
|
if fired then return end
|
||||||
|
local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
|
||||||
|
if not h then return end
|
||||||
|
fired = true
|
||||||
|
local ev = ReplicatedStorage:FindFirstChild("Escape")
|
||||||
|
if ev then ev:Fire() end
|
||||||
|
end)`,
|
||||||
|
};
|
||||||
|
for (let i = 1; i <= BTN_COUNT; i++) {
|
||||||
|
overrides['g47_btn_' + i] = `-- === Скрипт кнопки ${i} (Lua) ===
|
||||||
|
local UserInputService = game:GetService("UserInputService")
|
||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
local RunService = game:GetService("RunService")
|
||||||
|
local part = script.Parent
|
||||||
|
local used = false
|
||||||
|
local hintVisible = false
|
||||||
|
|
||||||
|
RunService.Heartbeat:Connect(function()
|
||||||
|
if used then return end
|
||||||
|
local px = __rbxl_player_x()
|
||||||
|
local pz = __rbxl_player_z()
|
||||||
|
local dx = part.Position.X - px
|
||||||
|
local dz = part.Position.Z - pz
|
||||||
|
local dist = math.sqrt(dx*dx + dz*dz)
|
||||||
|
local near = dist <= 3
|
||||||
|
if near ~= hintVisible then
|
||||||
|
hintVisible = near
|
||||||
|
if near then
|
||||||
|
__rbxl_hud_set("g47_btn_${i}_hint", "[E] Нажать кнопку", 50, 75, "#ffe44a", 20)
|
||||||
|
else
|
||||||
|
__rbxl_hud_set("g47_btn_${i}_hint", nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
UserInputService.InputBegan:Connect(function(input, gp)
|
||||||
|
if gp then return end
|
||||||
|
if not hintVisible then return end
|
||||||
|
if used then return end
|
||||||
|
if input.KeyCode ~= Enum.KeyCode.E then return end
|
||||||
|
used = true
|
||||||
|
__rbxl_hud_set("g47_btn_${i}_hint", nil)
|
||||||
|
part.Color = Color3.fromRGB(34, 221, 85)
|
||||||
|
local ev = ReplicatedStorage:FindFirstChild("ButtonPressed")
|
||||||
|
if ev then ev:Fire() end
|
||||||
|
end)`;
|
||||||
|
}
|
||||||
|
return overrides;
|
||||||
|
})(),
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// ИГРА 48 — «Мультиплеер: Салки»
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
'mp-tag': {
|
||||||
|
g48_main: `-- === ИГРА «МУЛЬТИПЛЕЕР: САЛКИ» — главный скрипт (Lua) ===
|
||||||
|
-- Это МУЛЬТИПЛЕЕРНАЯ игра. Чтобы играть с друзьями, опубликуй её
|
||||||
|
-- с галочкой «Мультиплеер» — тогда в комнату смогут зайти несколько
|
||||||
|
-- игроков. В одиночку игра показывает только правила.
|
||||||
|
|
||||||
|
local Players = game:GetService("Players")
|
||||||
|
|
||||||
|
__rbxl_show_text("Салки! Опубликуй игру для игры с друзьями", 4)
|
||||||
|
|
||||||
|
-- Показываем сколько игроков в комнате (постоянная плашка вверху)
|
||||||
|
local function refresh()
|
||||||
|
local n = #Players:GetPlayers()
|
||||||
|
__rbxl_hud_set("info", "Игроков в комнате: " .. n, 50, 8, "#ffe066", 22)
|
||||||
|
end
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
-- Подписки на вход/выход
|
||||||
|
Players.PlayerAdded:Connect(function(p)
|
||||||
|
__rbxl_show_text(p.Name .. " присоединился к салкам!", 2)
|
||||||
|
refresh()
|
||||||
|
end)
|
||||||
|
Players.PlayerRemoving:Connect(function()
|
||||||
|
refresh()
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- В одиночке роли не назначаются — показываем правила
|
||||||
|
task.delay(2, function()
|
||||||
|
__rbxl_show_text("Водящий — первый зашедший. Он догоняет остальных.", 4)
|
||||||
|
end)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// ИГРА 49 — «Мультиплеер: Гонка»
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
'mp-race': {
|
||||||
|
g49_main: `-- === ИГРА «МУЛЬТИПЛЕЕР: ГОНКА» — главный скрипт (Lua) ===
|
||||||
|
-- Мультиплеерная гонка. Чтобы соревноваться с друзьями — опубликуй
|
||||||
|
-- игру с галочкой «Мультиплеер».
|
||||||
|
${SNIPPET_BROADCAST}
|
||||||
|
|
||||||
|
local Players = game:GetService("Players")
|
||||||
|
local winnerName = nil
|
||||||
|
local won = false
|
||||||
|
|
||||||
|
__rbxl_show_text("Гонка! Беги к финишу первым", 3)
|
||||||
|
|
||||||
|
local winSound = Instance.new("Sound", workspace)
|
||||||
|
winSound.SoundId = "win"; winSound.Volume = 1
|
||||||
|
|
||||||
|
local function refresh()
|
||||||
|
local n = #Players:GetPlayers()
|
||||||
|
local txt = "Игроков: " .. n
|
||||||
|
if winnerName then txt = txt .. " | Победил: " .. winnerName end
|
||||||
|
__rbxl_hud_set("info", txt, 50, 8, "#ffe066", 22)
|
||||||
|
end
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
Players.PlayerAdded:Connect(refresh)
|
||||||
|
Players.PlayerRemoving:Connect(refresh)
|
||||||
|
|
||||||
|
-- Финиш
|
||||||
|
local finEvent = getEvent("FinishReached")
|
||||||
|
finEvent.Event:Connect(function()
|
||||||
|
if won then return end
|
||||||
|
won = true
|
||||||
|
-- В одиночке — мы и есть первый
|
||||||
|
local me = Players.LocalPlayer
|
||||||
|
winnerName = me and me.Name or "Игрок"
|
||||||
|
refresh()
|
||||||
|
__rbxl_show_text("Ты пришёл первым! Победа!", 5)
|
||||||
|
winSound:Play()
|
||||||
|
local px = __rbxl_player_x()
|
||||||
|
local py = __rbxl_player_y()
|
||||||
|
local pz = __rbxl_player_z()
|
||||||
|
__rbxl_spawn_particles("confetti", px, py + 3, pz, 3, 3)
|
||||||
|
end)`,
|
||||||
|
g49_finish: `-- === Скрипт финиша (Lua) ===
|
||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
local part = script.Parent
|
||||||
|
local fired = false
|
||||||
|
|
||||||
|
part.Touched:Connect(function(hit)
|
||||||
|
if fired then return end
|
||||||
|
local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
|
||||||
|
if not h then return end
|
||||||
|
fired = true
|
||||||
|
local ev = ReplicatedStorage:FindFirstChild("FinishReached")
|
||||||
|
if ev then ev:Fire() end
|
||||||
|
end)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// ИГРА 50 — «Своя игра» (песочница)
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
'make-your-own': {
|
||||||
|
g50_main: `-- === «СВОЯ ИГРА» — твоя песочница (Lua) ===
|
||||||
|
--
|
||||||
|
-- Это пустая площадка. Здесь ты придумываешь и собираешь
|
||||||
|
-- СВОЮ игру с нуля. Удали этот текст и пиши свой код.
|
||||||
|
--
|
||||||
|
-- С чего начать:
|
||||||
|
-- 1. Реши, КАКАЯ это игра (паркур / гонка / стрелялка / квест).
|
||||||
|
-- 2. Построй сцену из блоков и примитивов.
|
||||||
|
-- 3. Поставь точку спавна.
|
||||||
|
-- 4. Добавь цель — финиш, счёт или врагов.
|
||||||
|
-- 5. Напиши скрипты, оживляющие игру.
|
||||||
|
--
|
||||||
|
-- Всё, что нужно, ты уже знаешь из уроков 1-49. Удачи!
|
||||||
|
|
||||||
|
__rbxl_show_text("Твоя песочница! Создай свою игру", 4)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
'clicker': {
|
||||||
|
g46_main: `-- === ИГРА «КЛИКЕР» — главный скрипт (Lua) ===
|
||||||
|
${SNIPPET_BROADCAST}
|
||||||
|
|
||||||
|
local RunService = game:GetService("RunService")
|
||||||
|
local GOAL = 200
|
||||||
|
local points = 0
|
||||||
|
local perClick = 1
|
||||||
|
local autoIncome = 0
|
||||||
|
local won = false
|
||||||
|
|
||||||
|
__rbxl_score_set(0)
|
||||||
|
__rbxl_show_text("Кликай по жёлтому кубу! Цель: 200 очков", 4)
|
||||||
|
|
||||||
|
local clickSound = Instance.new("Sound", workspace)
|
||||||
|
clickSound.SoundId = "click"; clickSound.Volume = 0.6
|
||||||
|
local pickupSound = Instance.new("Sound", workspace)
|
||||||
|
pickupSound.SoundId = "pickup"; pickupSound.Volume = 0.7
|
||||||
|
local winSound = Instance.new("Sound", workspace)
|
||||||
|
winSound.SoundId = "win"; winSound.Volume = 1
|
||||||
|
|
||||||
|
local function checkWin()
|
||||||
|
if not won and points >= GOAL then
|
||||||
|
won = true
|
||||||
|
__rbxl_show_text("Победа! Накоплено 200 очков!", 5)
|
||||||
|
winSound:Play()
|
||||||
|
local px = __rbxl_player_x()
|
||||||
|
local py = __rbxl_player_y()
|
||||||
|
local pz = __rbxl_player_z()
|
||||||
|
__rbxl_spawn_particles("confetti", px, py + 3, pz, 3, 3)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Авто-доход каждую секунду
|
||||||
|
local autoTimer = 0
|
||||||
|
RunService.Heartbeat:Connect(function(dt)
|
||||||
|
if won then return end
|
||||||
|
autoTimer = autoTimer + dt
|
||||||
|
if autoTimer < 1 then return end
|
||||||
|
autoTimer = 0
|
||||||
|
if autoIncome > 0 then
|
||||||
|
points = points + autoIncome
|
||||||
|
__rbxl_score_set(points)
|
||||||
|
checkWin()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Клик по кубу
|
||||||
|
local clickEvent = getEvent("CubeClicked")
|
||||||
|
clickEvent.Event:Connect(function()
|
||||||
|
if won then return end
|
||||||
|
points = points + perClick
|
||||||
|
__rbxl_score_set(points)
|
||||||
|
clickSound:Play()
|
||||||
|
checkWin()
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Покупка силы клика (20)
|
||||||
|
local powerEvent = getEvent("BuyPower")
|
||||||
|
powerEvent.Event:Connect(function()
|
||||||
|
if points < 20 then
|
||||||
|
__rbxl_show_text("Нужно 20 очков для улучшения!", 1.5)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
points = points - 20
|
||||||
|
perClick = perClick + 2
|
||||||
|
__rbxl_score_set(points)
|
||||||
|
pickupSound:Play()
|
||||||
|
__rbxl_show_text("Сила клика: +" .. perClick .. " за клик", 2)
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Покупка авто-дохода (40)
|
||||||
|
local autoEvent = getEvent("BuyAuto")
|
||||||
|
autoEvent.Event:Connect(function()
|
||||||
|
if points < 40 then
|
||||||
|
__rbxl_show_text("Нужно 40 очков для авто-дохода!", 1.5)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
points = points - 40
|
||||||
|
autoIncome = autoIncome + 3
|
||||||
|
__rbxl_score_set(points)
|
||||||
|
pickupSound:Play()
|
||||||
|
__rbxl_show_text("Авто-доход: +" .. autoIncome .. " в секунду", 2)
|
||||||
|
end)`,
|
||||||
|
g46_cube: `-- === Скрипт куба-кликера (Lua) ===
|
||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
local part = script.Parent
|
||||||
|
|
||||||
|
-- ClickDetector делает куб кликабельным
|
||||||
|
local cd = Instance.new("ClickDetector")
|
||||||
|
cd.Parent = part
|
||||||
|
|
||||||
|
cd.MouseClick:Connect(function()
|
||||||
|
__rbxl_spawn_particles("sparks", part.Position.X, part.Position.Y + 1, part.Position.Z, 0.3, 1)
|
||||||
|
local ev = ReplicatedStorage:FindFirstChild("CubeClicked")
|
||||||
|
if ev then ev:Fire() end
|
||||||
|
end)`,
|
||||||
|
g46_up1: `-- === Скрипт улучшения «сила клика» (20 очков) (Lua) ===
|
||||||
|
local UserInputService = game:GetService("UserInputService")
|
||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
local RunService = game:GetService("RunService")
|
||||||
|
local part = script.Parent
|
||||||
|
local hintVisible = false
|
||||||
|
|
||||||
|
RunService.Heartbeat:Connect(function()
|
||||||
|
local px = __rbxl_player_x()
|
||||||
|
local pz = __rbxl_player_z()
|
||||||
|
local dx = part.Position.X - px
|
||||||
|
local dz = part.Position.Z - pz
|
||||||
|
local dist = math.sqrt(dx*dx + dz*dz)
|
||||||
|
local near = dist <= 3
|
||||||
|
if near ~= hintVisible then
|
||||||
|
hintVisible = near
|
||||||
|
if near then
|
||||||
|
__rbxl_hud_set("g46_up1_hint", "[E] Купить +силу клика (20)", 50, 75, "#ffe44a", 20)
|
||||||
|
else
|
||||||
|
__rbxl_hud_set("g46_up1_hint", nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
UserInputService.InputBegan:Connect(function(input, gp)
|
||||||
|
if gp then return end
|
||||||
|
if not hintVisible then return end
|
||||||
|
if input.KeyCode ~= Enum.KeyCode.E then return end
|
||||||
|
local ev = ReplicatedStorage:FindFirstChild("BuyPower")
|
||||||
|
if ev then ev:Fire() end
|
||||||
|
end)`,
|
||||||
|
g46_up2: `-- === Скрипт улучшения «авто-доход» (40 очков) (Lua) ===
|
||||||
|
local UserInputService = game:GetService("UserInputService")
|
||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
local RunService = game:GetService("RunService")
|
||||||
|
local part = script.Parent
|
||||||
|
local hintVisible = false
|
||||||
|
|
||||||
|
RunService.Heartbeat:Connect(function()
|
||||||
|
local px = __rbxl_player_x()
|
||||||
|
local pz = __rbxl_player_z()
|
||||||
|
local dx = part.Position.X - px
|
||||||
|
local dz = part.Position.Z - pz
|
||||||
|
local dist = math.sqrt(dx*dx + dz*dz)
|
||||||
|
local near = dist <= 3
|
||||||
|
if near ~= hintVisible then
|
||||||
|
hintVisible = near
|
||||||
|
if near then
|
||||||
|
__rbxl_hud_set("g46_up2_hint", "[E] Купить авто-доход (40)", 50, 75, "#ffe44a", 20)
|
||||||
|
else
|
||||||
|
__rbxl_hud_set("g46_up2_hint", nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
UserInputService.InputBegan:Connect(function(input, gp)
|
||||||
|
if gp then return end
|
||||||
|
if not hintVisible then return end
|
||||||
|
if input.KeyCode ~= Enum.KeyCode.E then return end
|
||||||
|
local ev = ReplicatedStorage:FindFirstChild("BuyAuto")
|
||||||
|
if ev then ev:Fire() end
|
||||||
|
end)`,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@ -113,7 +113,11 @@ export function highlightCode(text, lang) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const LS_KEY = 'rublox.docs.lang';
|
// v2 — раньше при первом включении lua-режима сохранялся в LS и юзер
|
||||||
|
// потом всегда видел Lua-таб по умолчанию. Бамп ключа = сброс на JS
|
||||||
|
// у всех уже-открытых вкладок.
|
||||||
|
const LS_KEY = 'rublox.docs.lang.v2';
|
||||||
|
const LS_KEY_OLD = 'rublox.docs.lang';
|
||||||
const DEFAULT_LANG = 'js';
|
const DEFAULT_LANG = 'js';
|
||||||
|
|
||||||
const DocsLangContext = createContext({
|
const DocsLangContext = createContext({
|
||||||
@ -124,6 +128,8 @@ const DocsLangContext = createContext({
|
|||||||
export function DocsLangProvider({ children }) {
|
export function DocsLangProvider({ children }) {
|
||||||
const [lang, setLangState] = useState(() => {
|
const [lang, setLangState] = useState(() => {
|
||||||
try {
|
try {
|
||||||
|
// Очищаем старый ключ — у части юзеров там залип 'lua'
|
||||||
|
localStorage.removeItem(LS_KEY_OLD);
|
||||||
const v = localStorage.getItem(LS_KEY);
|
const v = localStorage.getItem(LS_KEY);
|
||||||
return v === 'lua' ? 'lua' : 'js';
|
return v === 'lua' ? 'lua' : 'js';
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Code, ScriptKind, Step, Note, Try, Shot } from './docsData';
|
import { Code, ScriptKind, Step, Note, Try, Shot } from './docsData';
|
||||||
import { LangTabs } from './docsLang';
|
import { LangTabs, useDocsLang } from './docsLang';
|
||||||
import { LUA_OVERRIDES } from './docsGamesBuildersLua';
|
import { LUA_OVERRIDES } from './docsGamesBuildersLua';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,6 +20,17 @@ function CodeBoth({ game, script, children }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инлайн-API-имена в тексте уроков, меняющиеся в зависимости от JS/Lua вкладки.
|
||||||
|
* <Api js="game.ui.showText('Победа!', 5)" lua={'__rbxl_show_text("Победа!", 5)'} />
|
||||||
|
* Если lua не задан — показывает js в обоих режимах.
|
||||||
|
*/
|
||||||
|
function Api({ js, lua }) {
|
||||||
|
const { lang } = useDocsLang();
|
||||||
|
const txt = lang === 'lua' && lua ? lua : js;
|
||||||
|
return <code>{txt}</code>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* docsLessons.jsx — тексты уроков для 50 мини-игр (раздел K вики).
|
* docsLessons.jsx — тексты уроков для 50 мини-игр (раздел K вики).
|
||||||
*
|
*
|
||||||
@ -6291,7 +6302,7 @@ game.self.onTouch(() => {
|
|||||||
стрелять и проверяет, кто победил.
|
стрелять и проверяет, кто победил.
|
||||||
</p>
|
</p>
|
||||||
<ScriptKind kind="global" />
|
<ScriptKind kind="global" />
|
||||||
<Code>{`// === ИГРА «TOWER DEFENSE» — главный скрипт ===
|
<CodeBoth game="tower-defense" script="g44_main">{`// === ИГРА «TOWER DEFENSE» — главный скрипт ===
|
||||||
|
|
||||||
let leaked = 0; // врагов прошло до базы
|
let leaked = 0; // врагов прошло до базы
|
||||||
const MAX_LEAK = 8;
|
const MAX_LEAK = 8;
|
||||||
@ -6374,7 +6385,7 @@ game.every(0.5, () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});`}</Code>
|
});`}</CodeBoth>
|
||||||
<p>Разберём:</p>
|
<p>Разберём:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>towers</code> и <code>enemies</code> — два списка:
|
<li><code>towers</code> и <code>enemies</code> — два списка:
|
||||||
@ -6395,7 +6406,7 @@ game.every(0.5, () => {
|
|||||||
|
|
||||||
<h3 className="lessonH">Шаг 3. Скрипт площадки под башню</h3>
|
<h3 className="lessonH">Шаг 3. Скрипт площадки под башню</h3>
|
||||||
<ScriptKind kind="object" on="каждую площадку" />
|
<ScriptKind kind="object" on="каждую площадку" />
|
||||||
<Code>{`// === Скрипт площадки под башню ===
|
<CodeBoth game="tower-defense" script="g44_slot_1">{`// === Скрипт площадки под башню ===
|
||||||
let built = false;
|
let built = false;
|
||||||
game.self.onInteract(() => {
|
game.self.onInteract(() => {
|
||||||
if (built) return;
|
if (built) return;
|
||||||
@ -6408,7 +6419,7 @@ game.self.onInteract(() => {
|
|||||||
color: '#ffcc33',
|
color: '#ffcc33',
|
||||||
});
|
});
|
||||||
game.broadcast('addTower', { x: pos.x, z: pos.z });
|
game.broadcast('addTower', { x: pos.x, z: pos.z });
|
||||||
}, { text: 'Построить башню', distance: 4 });`}</Code>
|
}, { text: 'Построить башню', distance: 4 });`}</CodeBoth>
|
||||||
<p>
|
<p>
|
||||||
При нажатии <kbd className="kbd">E</kbd> скрипт создаёт
|
При нажатии <kbd className="kbd">E</kbd> скрипт создаёт
|
||||||
жёлтый цилиндр-башню над площадкой и шлёт сообщение
|
жёлтый цилиндр-башню над площадкой и шлёт сообщение
|
||||||
@ -6515,7 +6526,7 @@ game.scene.spawn('user:3', {
|
|||||||
|
|
||||||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||||||
<ScriptKind kind="global" />
|
<ScriptKind kind="global" />
|
||||||
<Code>{`// === ИГРА «СТРЕЛЯЛКА-АРЕНА» — главный скрипт ===
|
<CodeBoth game="arena-shooter" script="g45_main">{`// === ИГРА «СТРЕЛЯЛКА-АРЕНА» — главный скрипт ===
|
||||||
|
|
||||||
let score = 0;
|
let score = 0;
|
||||||
const GOAL = 15;
|
const GOAL = 15;
|
||||||
@ -6577,7 +6588,7 @@ game.every(1.8, () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});`}</Code>
|
});`}</CodeBoth>
|
||||||
<p>Разберём:</p>
|
<p>Разберём:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>game.onHpChange((e) ={'>'} ...)</code> —
|
<li><code>game.onHpChange((e) ={'>'} ...)</code> —
|
||||||
@ -6660,7 +6671,7 @@ game.every(1.8, () => {
|
|||||||
|
|
||||||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||||||
<ScriptKind kind="global" />
|
<ScriptKind kind="global" />
|
||||||
<Code>{`// === ИГРА «КЛИКЕР» — главный скрипт ===
|
<CodeBoth game="clicker" script="g46_main">{`// === ИГРА «КЛИКЕР» — главный скрипт ===
|
||||||
|
|
||||||
let points = 0; // очки
|
let points = 0; // очки
|
||||||
let perClick = 1; // очков за клик
|
let perClick = 1; // очков за клик
|
||||||
@ -6725,7 +6736,7 @@ game.onMessage('buyAuto', () => {
|
|||||||
game.ui.score = points;
|
game.ui.score = points;
|
||||||
game.sound.play('pickup');
|
game.sound.play('pickup');
|
||||||
game.ui.showText('Авто-доход: +' + autoIncome + ' в секунду', 2);
|
game.ui.showText('Авто-доход: +' + autoIncome + ' в секунду', 2);
|
||||||
});`}</Code>
|
});`}</CodeBoth>
|
||||||
<p>Разберём:</p>
|
<p>Разберём:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>points</code> — очки, <code>perClick</code> —
|
<li><code>points</code> — очки, <code>perClick</code> —
|
||||||
@ -6745,22 +6756,22 @@ game.onMessage('buyAuto', () => {
|
|||||||
|
|
||||||
<h3 className="lessonH">Шаг 3. Скрипты куба и кнопок</h3>
|
<h3 className="lessonH">Шаг 3. Скрипты куба и кнопок</h3>
|
||||||
<ScriptKind kind="object" on="жёлтый куб-кликер" />
|
<ScriptKind kind="object" on="жёлтый куб-кликер" />
|
||||||
<Code>{`// === Скрипт куба-кликера ===
|
<CodeBoth game="clicker" script="g46_cube">{`// === Скрипт куба-кликера ===
|
||||||
game.self.onClick(() => {
|
game.self.onClick(() => {
|
||||||
game.broadcast('click');
|
game.broadcast('click');
|
||||||
// куб слегка вспыхивает
|
// куб слегка вспыхивает
|
||||||
game.scene.spawnParticles('sparks', game.self.position, { duration: 0.3 });
|
game.scene.spawnParticles('sparks', game.self.position, { duration: 0.3 });
|
||||||
});`}</Code>
|
});`}</CodeBoth>
|
||||||
<ScriptKind kind="object" on="красную кнопку" />
|
<ScriptKind kind="object" on="красную кнопку" />
|
||||||
<Code>{`// === Скрипт улучшения «сила клика» (20 очков) ===
|
<CodeBoth game="clicker" script="g46_up1">{`// === Скрипт улучшения «сила клика» (20 очков) ===
|
||||||
game.self.onInteract(() => {
|
game.self.onInteract(() => {
|
||||||
game.broadcast('buyPower');
|
game.broadcast('buyPower');
|
||||||
}, { text: 'Купить +силу клика (20)', distance: 3 });`}</Code>
|
}, { text: 'Купить +силу клика (20)', distance: 3 });`}</CodeBoth>
|
||||||
<ScriptKind kind="object" on="синюю кнопку" />
|
<ScriptKind kind="object" on="синюю кнопку" />
|
||||||
<Code>{`// === Скрипт улучшения «авто-доход» (40 очков) ===
|
<CodeBoth game="clicker" script="g46_up2">{`// === Скрипт улучшения «авто-доход» (40 очков) ===
|
||||||
game.self.onInteract(() => {
|
game.self.onInteract(() => {
|
||||||
game.broadcast('buyAuto');
|
game.broadcast('buyAuto');
|
||||||
}, { text: 'Купить авто-доход (40)', distance: 3 });`}</Code>
|
}, { text: 'Купить авто-доход (40)', distance: 3 });`}</CodeBoth>
|
||||||
<Note>
|
<Note>
|
||||||
Главная идея кликера: сначала кликаешь руками, потом
|
Главная идея кликера: сначала кликаешь руками, потом
|
||||||
покупаешь улучшения — и игра «играет сама». Это экономика:
|
покупаешь улучшения — и игра «играет сама». Это экономика:
|
||||||
@ -6830,7 +6841,7 @@ game.self.onInteract(() => {
|
|||||||
|
|
||||||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||||||
<ScriptKind kind="global" />
|
<ScriptKind kind="global" />
|
||||||
<Code>{`// === ИГРА «КВЕСТ-ПОБЕГ» — главный скрипт ===
|
<CodeBoth game="escape-quest" script="g47_main">{`// === ИГРА «КВЕСТ-ПОБЕГ» — главный скрипт ===
|
||||||
|
|
||||||
let pressed = 0; // сколько кнопок нажато
|
let pressed = 0; // сколько кнопок нажато
|
||||||
const TOTAL = 3;
|
const TOTAL = 3;
|
||||||
@ -6862,7 +6873,7 @@ game.onMessage('escape', () => {
|
|||||||
const p = game.player.position;
|
const p = game.player.position;
|
||||||
game.scene.spawnParticles('confetti',
|
game.scene.spawnParticles('confetti',
|
||||||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||||||
});`}</Code>
|
});`}</CodeBoth>
|
||||||
<p>Разберём:</p>
|
<p>Разберём:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>pressed</code> — счётчик нажатых кнопок,
|
<li><code>pressed</code> — счётчик нажатых кнопок,
|
||||||
@ -6878,19 +6889,19 @@ game.onMessage('escape', () => {
|
|||||||
|
|
||||||
<h3 className="lessonH">Шаг 3. Скрипты кнопки и финиша</h3>
|
<h3 className="lessonH">Шаг 3. Скрипты кнопки и финиша</h3>
|
||||||
<ScriptKind kind="object" on="каждую красную кнопку" />
|
<ScriptKind kind="object" on="каждую красную кнопку" />
|
||||||
<Code>{`// === Скрипт кнопки 1 ===
|
<CodeBoth game="escape-quest" script="g47_btn_1">{`// === Скрипт кнопки 1 ===
|
||||||
let used = false;
|
let used = false;
|
||||||
game.self.onInteract(() => {
|
game.self.onInteract(() => {
|
||||||
if (used) return;
|
if (used) return;
|
||||||
used = true;
|
used = true;
|
||||||
game.scene.setColor(game.self.ref, '#22dd55'); // нажата — зелёная
|
game.scene.setColor(game.self.ref, '#22dd55'); // нажата — зелёная
|
||||||
game.broadcast('pressButton');
|
game.broadcast('pressButton');
|
||||||
}, { text: 'Нажать кнопку', distance: 3 });`}</Code>
|
}, { text: 'Нажать кнопку', distance: 3 });`}</CodeBoth>
|
||||||
<ScriptKind kind="object" on="зелёный финиш" />
|
<ScriptKind kind="object" on="зелёный финиш" />
|
||||||
<Code>{`// === Скрипт финиша ===
|
<CodeBoth game="escape-quest" script="g47_finish">{`// === Скрипт финиша ===
|
||||||
game.self.onTouch(() => {
|
game.self.onTouch(() => {
|
||||||
game.broadcast('escape');
|
game.broadcast('escape');
|
||||||
});`}</Code>
|
});`}</CodeBoth>
|
||||||
<p>
|
<p>
|
||||||
Кнопка при нажатии становится зелёной (видно, что нажата),
|
Кнопка при нажатии становится зелёной (видно, что нажата),
|
||||||
шлёт <code>game.broadcast('pressButton')</code> и больше
|
шлёт <code>game.broadcast('pressButton')</code> и больше
|
||||||
@ -6969,7 +6980,7 @@ game.self.onTouch(() => {
|
|||||||
|
|
||||||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||||||
<ScriptKind kind="global" />
|
<ScriptKind kind="global" />
|
||||||
<Code>{`// === ИГРА «МУЛЬТИПЛЕЕР: САЛКИ» — главный скрипт ===
|
<CodeBoth game="mp-tag" script="g48_main">{`// === ИГРА «МУЛЬТИПЛЕЕР: САЛКИ» — главный скрипт ===
|
||||||
//
|
//
|
||||||
// Это МУЛЬТИПЛЕЕРНАЯ игра. Чтобы играть с друзьями, опубликуй её
|
// Это МУЛЬТИПЛЕЕРНАЯ игра. Чтобы играть с друзьями, опубликуй её
|
||||||
// с галочкой «Мультиплеер» — тогда в комнату смогут зайти несколько
|
// с галочкой «Мультиплеер» — тогда в комнату смогут зайти несколько
|
||||||
@ -7006,7 +7017,7 @@ game.room.onChange('tagger', (taggerId) => {
|
|||||||
} else {
|
} else {
|
||||||
game.ui.showText('Убегай от водящего!', 3);
|
game.ui.showText('Убегай от водящего!', 3);
|
||||||
}
|
}
|
||||||
});`}</Code>
|
});`}</CodeBoth>
|
||||||
<p>Разберём:</p>
|
<p>Разберём:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>game.players.count()</code> — сколько игроков
|
<li><code>game.players.count()</code> — сколько игроков
|
||||||
@ -7097,7 +7108,7 @@ game.room.onChange('tagger', (taggerId) => {
|
|||||||
|
|
||||||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||||||
<ScriptKind kind="global" />
|
<ScriptKind kind="global" />
|
||||||
<Code>{`// === ИГРА «МУЛЬТИПЛЕЕР: ГОНКА» — главный скрипт ===
|
<CodeBoth game="mp-race" script="g49_main">{`// === ИГРА «МУЛЬТИПЛЕЕР: ГОНКА» — главный скрипт ===
|
||||||
//
|
//
|
||||||
// Мультиплеерная гонка. Чтобы соревноваться с друзьями — опубликуй
|
// Мультиплеерная гонка. Чтобы соревноваться с друзьями — опубликуй
|
||||||
// игру с галочкой «Мультиплеер».
|
// игру с галочкой «Мультиплеер».
|
||||||
@ -7137,7 +7148,7 @@ game.onMessage('finish', () => {
|
|||||||
} else {
|
} else {
|
||||||
game.ui.showText('Финиш! Но кто-то был быстрее.', 4);
|
game.ui.showText('Финиш! Но кто-то был быстрее.', 4);
|
||||||
}
|
}
|
||||||
});`}</Code>
|
});`}</CodeBoth>
|
||||||
<p>Разберём:</p>
|
<p>Разберём:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>game.room.get('winner')</code> — читаем общую
|
<li><code>game.room.get('winner')</code> — читаем общую
|
||||||
@ -7157,10 +7168,10 @@ game.onMessage('finish', () => {
|
|||||||
|
|
||||||
<h3 className="lessonH">Шаг 3. Скрипт финиша</h3>
|
<h3 className="lessonH">Шаг 3. Скрипт финиша</h3>
|
||||||
<ScriptKind kind="object" on="зелёный финиш" />
|
<ScriptKind kind="object" on="зелёный финиш" />
|
||||||
<Code>{`// === Скрипт финиша ===
|
<CodeBoth game="mp-race" script="g49_finish">{`// === Скрипт финиша ===
|
||||||
game.self.onTouch(() => {
|
game.self.onTouch(() => {
|
||||||
game.broadcast('finish');
|
game.broadcast('finish');
|
||||||
});`}</Code>
|
});`}</CodeBoth>
|
||||||
<p>
|
<p>
|
||||||
Когда любой игрок касается финиша, скрипт шлёт сообщение
|
Когда любой игрок касается финиша, скрипт шлёт сообщение
|
||||||
<code> game.broadcast('finish')</code> — а главный скрипт
|
<code> game.broadcast('finish')</code> — а главный скрипт
|
||||||
@ -7270,8 +7281,10 @@ game.self.onTouch(() => {
|
|||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
И обязательно покажи игроку, когда он <b>победил</b> —
|
И обязательно покажи игроку, когда он <b>победил</b> —
|
||||||
надписью <code>game.ui.showText('Победа!', 5)</code>,
|
надписью <Api js="game.ui.showText('Победа!', 5)"
|
||||||
звуком <code>game.sound.play('win')</code> и конфетти.
|
lua={'__rbxl_show_text("Победа!", 5)'} />,
|
||||||
|
звуком <Api js="game.sound.play('win')"
|
||||||
|
lua={'winSound:Play()'} /> и конфетти.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 className="lessonH">Шаг 4. Напиши скрипты</h3>
|
<h3 className="lessonH">Шаг 4. Напиши скрипты</h3>
|
||||||
@ -7279,34 +7292,48 @@ game.self.onTouch(() => {
|
|||||||
Сцена сама по себе не «живая» — её оживляют скрипты.
|
Сцена сама по себе не «живая» — её оживляют скрипты.
|
||||||
Начинай с <b>главного скрипта</b>: в нём заводи переменные
|
Начинай с <b>главного скрипта</b>: в нём заводи переменные
|
||||||
(счёт, флажок победы) и <b>лови сообщения</b> через
|
(счёт, флажок победы) и <b>лови сообщения</b> через
|
||||||
<code> game.onMessage('имя', fn)</code>. На объекты вешай
|
<> </><Api js="game.onMessage('имя', fn)"
|
||||||
небольшие скрипты — они шлют сообщения главному через
|
lua={'getEvent("Имя").Event:Connect(fn)'} />.
|
||||||
<code> game.broadcast('имя')</code>. Так главный скрипт
|
На объекты вешай небольшие скрипты — они шлют сообщения
|
||||||
узнаёт, что монетку собрали или кнопку нажали. Ты делал
|
главному через <Api js="game.broadcast('имя')"
|
||||||
так в каждом уроке.
|
lua={'getEvent("Имя"):Fire()'} />.
|
||||||
|
Так главный скрипт узнаёт, что монетку собрали или кнопку
|
||||||
|
нажали. Ты делал так в каждом уроке.
|
||||||
</p>
|
</p>
|
||||||
<Note>
|
<Note>
|
||||||
Каждый скрипт работает в своей «песочнице» — переменные
|
Каждый скрипт работает в своей «песочнице» — переменные
|
||||||
одного скрипта не видны другому. Поэтому скрипты общаются
|
одного скрипта не видны другому. Поэтому скрипты общаются
|
||||||
сообщениями: один шлёт <code>game.broadcast('имя')</code>,
|
сообщениями: один шлёт <Api js="game.broadcast('имя')"
|
||||||
другой ловит <code>game.onMessage('имя', fn)</code>. Можно
|
lua={'getEvent("Имя"):Fire()'} />,
|
||||||
передать данные: <code>game.broadcast('имя', {'{'} ... {'}'})</code>.
|
другой ловит <Api js="game.onMessage('имя', fn)"
|
||||||
|
lua={'getEvent("Имя").Event:Connect(fn)'} />.
|
||||||
|
Можно передать данные: <Api
|
||||||
|
js="game.broadcast('имя', { ... })"
|
||||||
|
lua={'getEvent("Имя"):Fire(data)'} />.
|
||||||
</Note>
|
</Note>
|
||||||
<p>Базовый набор инструментов, который ты знаешь:</p>
|
<p>Базовый набор инструментов, который ты знаешь:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>game.self.onTouch</code> — реакция на касание;</li>
|
<li><Api js="game.self.onTouch"
|
||||||
<li><code>game.self.onInteract</code> — реакция на
|
lua={'part.Touched:Connect(fn)'} /> — реакция на касание;</li>
|
||||||
|
<li><Api js="game.self.onInteract"
|
||||||
|
lua={'UserInputService.InputBegan + Heartbeat (дистанция)'} /> — реакция на
|
||||||
<kbd className="kbd">E</kbd>;</li>
|
<kbd className="kbd">E</kbd>;</li>
|
||||||
<li><code>game.self.onClick</code> — реакция на клик;</li>
|
<li><Api js="game.self.onClick"
|
||||||
<li><code>game.broadcast</code> и <code>game.onMessage</code>
|
lua={'ClickDetector + MouseClick:Connect'} /> — реакция на клик;</li>
|
||||||
|
<li><Api js="game.broadcast" lua={'BindableEvent:Fire'} /> и
|
||||||
|
<> </><Api js="game.onMessage" lua={'BindableEvent.Event:Connect'} />
|
||||||
— связь между скриптами;</li>
|
— связь между скриптами;</li>
|
||||||
<li><code>game.onTick</code> — каждый кадр;</li>
|
<li><Api js="game.onTick"
|
||||||
<li><code>game.after</code> и <code>game.every</code> —
|
lua={'RunService.Heartbeat:Connect'} /> — каждый кадр;</li>
|
||||||
таймеры;</li>
|
<li><Api js="game.after / game.every"
|
||||||
<li><code>game.tween</code> — плавное движение;</li>
|
lua={'task.delay / task.spawn'} /> — таймеры;</li>
|
||||||
<li><code>game.scene.spawnNpc</code> — враги и NPC;</li>
|
<li><Api js="game.tween"
|
||||||
<li><code>game.ui.score</code> и
|
lua={'TweenService:Create(part, TweenInfo.new(t), goal):Play()'} /> — плавное движение;</li>
|
||||||
<code> game.ui.showText</code> — счёт и подсказки.</li>
|
<li><Api js="game.scene.spawnNpc"
|
||||||
|
lua={'__rbxl_spawn_npc("character-b", x, y, z, name, hp, speed)'} /> — враги и NPC;</li>
|
||||||
|
<li><Api js="game.ui.score" lua={'__rbxl_score_set(N)'} /> и
|
||||||
|
<> </><Api js="game.ui.showText"
|
||||||
|
lua={'__rbxl_show_text(text, duration)'} /> — счёт и подсказки.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 className="lessonH">Шаг 5. Проверяй и улучшай</h3>
|
<h3 className="lessonH">Шаг 5. Проверяй и улучшай</h3>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* RbxlImportModal — модалка импорта .rbxl Roblox-карт в Rublox.
|
* RbxlImportModal — модалка импорта .rbxl Roblox-карт в Rublox.
|
||||||
*
|
*
|
||||||
* Доступна ТОЛЬКО МИНу (user_id === 1) — это тест-фича.
|
* Доступна всем пользователям (см. вики «Импорт из Roblox» о нюансах).
|
||||||
*
|
*
|
||||||
* Поток:
|
* Поток:
|
||||||
* 1. Юзер дропает или выбирает .rbxl файл.
|
* 1. Юзер дропает или выбирает .rbxl файл.
|
||||||
@ -13,8 +13,6 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { analyzeRbxl, createRbxlProject } from '../api/rbxlImporterApi.js';
|
import { analyzeRbxl, createRbxlProject } from '../api/rbxlImporterApi.js';
|
||||||
|
|
||||||
const ALLOWED_USER_ID = 1; // МИН
|
|
||||||
|
|
||||||
const MAX_SIZE = 50 * 1024 * 1024; // 50 MB
|
const MAX_SIZE = 50 * 1024 * 1024; // 50 MB
|
||||||
|
|
||||||
export default function RbxlImportModal({ open, onClose, currentUserId, onCreated }) {
|
export default function RbxlImportModal({ open, onClose, currentUserId, onCreated }) {
|
||||||
@ -37,18 +35,6 @@ export default function RbxlImportModal({ open, onClose, currentUserId, onCreate
|
|||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
if (currentUserId !== ALLOWED_USER_ID) {
|
|
||||||
return (
|
|
||||||
<div style={overlayStyle} onClick={onClose}>
|
|
||||||
<div style={dialogStyle} onClick={(e) => e.stopPropagation()}>
|
|
||||||
<h2 style={{ marginTop: 0 }}>Импорт из Roblox</h2>
|
|
||||||
<p>Эта тест-функция доступна только администратору.</p>
|
|
||||||
<button style={btnStyle} onClick={onClose}>Закрыть</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setFile(null); setReport(null); setPreviewHash(null);
|
setFile(null); setReport(null); setPreviewHash(null);
|
||||||
setTitle(''); setError(null); setAnalyzing(false); setCreating(false);
|
setTitle(''); setError(null); setAnalyzing(false); setCreating(false);
|
||||||
|
|||||||
@ -204,6 +204,96 @@ export class StudioCollab {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GUI-элементы (GuiManager: create/update/remove/setParent).
|
||||||
|
// Передаём полный набор полей с id, чтобы у соавтора создался ТОТ ЖЕ id.
|
||||||
|
const gm = this.scene.guiManager;
|
||||||
|
if (gm && !gm.__collabPatched) {
|
||||||
|
gm.__collabPatched = true;
|
||||||
|
const origGCreate = gm.create.bind(gm);
|
||||||
|
gm.create = function (type, opts = {}) {
|
||||||
|
const id = origGCreate(type, opts);
|
||||||
|
if (id != null && !self._applyingRemote) {
|
||||||
|
const el = gm.get?.(id);
|
||||||
|
self.sendOp({ op: 'guiCreate', id, type, def: el ? { ...el } : { ...opts, id } });
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
const origGUpd = gm.update.bind(gm);
|
||||||
|
gm.update = function (id, patch) {
|
||||||
|
const r = origGUpd(id, patch);
|
||||||
|
if (!self._applyingRemote && patch && typeof patch === 'object') {
|
||||||
|
self.sendOp({ op: 'guiUpdate', id, patch: { ...patch } });
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
const origGRem = gm.remove.bind(gm);
|
||||||
|
gm.remove = function (id) {
|
||||||
|
const r = origGRem(id);
|
||||||
|
if (!self._applyingRemote) self.sendOp({ op: 'guiRemove', id });
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Папки (FolderManager: createFolder/renameFolder/removeFolder).
|
||||||
|
const fm = this.scene.folderManager;
|
||||||
|
if (fm && !fm.__collabPatched) {
|
||||||
|
fm.__collabPatched = true;
|
||||||
|
if (typeof fm.createFolder === 'function') {
|
||||||
|
const origFC = fm.createFolder.bind(fm);
|
||||||
|
fm.createFolder = function (name, parentId = null) {
|
||||||
|
const r = origFC(name, parentId);
|
||||||
|
if (!self._applyingRemote) {
|
||||||
|
// r может быть id или объектом папки — нормализуем.
|
||||||
|
const id = (r && typeof r === 'object') ? (r.id ?? r) : r;
|
||||||
|
self.sendOp({ op: 'folderCreate', id, name, parentId: parentId ?? null });
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typeof fm.renameFolder === 'function') {
|
||||||
|
const origFR = fm.renameFolder.bind(fm);
|
||||||
|
fm.renameFolder = function (id, name) {
|
||||||
|
const r = origFR(id, name);
|
||||||
|
if (!self._applyingRemote) self.sendOp({ op: 'folderRename', id, name });
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typeof fm.removeFolder === 'function') {
|
||||||
|
const origFRem = fm.removeFolder.bind(fm);
|
||||||
|
fm.removeFolder = function (id, deleteContent = false) {
|
||||||
|
const r = origFRem(id, deleteContent);
|
||||||
|
if (!self._applyingRemote) self.sendOp({ op: 'folderRemove', id, deleteContent: !!deleteContent });
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пользовательские модели (UserModelManager: addInstance async / removeInstance).
|
||||||
|
const um = this.scene.userModelManager;
|
||||||
|
if (um && !um.__collabPatched) {
|
||||||
|
um.__collabPatched = true;
|
||||||
|
if (typeof um.addInstance === 'function') {
|
||||||
|
const origUMAdd = um.addInstance.bind(um);
|
||||||
|
um.addInstance = function (userModelTypeId, x, y, z, rotationY = 0, options = {}) {
|
||||||
|
const ret = origUMAdd(userModelTypeId, x, y, z, rotationY, options);
|
||||||
|
Promise.resolve(ret).then((id) => {
|
||||||
|
if (id != null && !self._applyingRemote) {
|
||||||
|
self.sendOp({ op: 'userModelAdd', id, userModelTypeId, x, y, z, rotationY });
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (typeof um.removeInstance === 'function') {
|
||||||
|
const origUMRem = um.removeInstance.bind(um);
|
||||||
|
um.removeInstance = function (id) {
|
||||||
|
const r = origUMRem(id);
|
||||||
|
if (!self._applyingRemote) self.sendOp({ op: 'userModelRemove', id });
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Снять обёртки (при выходе из коллаба восстановить оригиналы — простой флаг). */
|
/** Снять обёртки (при выходе из коллаба восстановить оригиналы — простой флаг). */
|
||||||
@ -441,6 +531,52 @@ export function applyRemoteOp(scene, op) {
|
|||||||
scene.removeScript?.(op.id);
|
scene.removeScript?.(op.id);
|
||||||
scene._onCollabScriptsChange?.();
|
scene._onCollabScriptsChange?.();
|
||||||
return;
|
return;
|
||||||
|
// GUI-элементы
|
||||||
|
case 'guiCreate':
|
||||||
|
scene.guiManager?.create?.(op.type, op.def || { id: op.id });
|
||||||
|
return;
|
||||||
|
case 'guiUpdate':
|
||||||
|
scene.guiManager?.update?.(op.id, op.patch || {});
|
||||||
|
return;
|
||||||
|
case 'guiRemove':
|
||||||
|
scene.guiManager?.remove?.(op.id);
|
||||||
|
return;
|
||||||
|
// Папки
|
||||||
|
case 'folderCreate':
|
||||||
|
// createFolder(name, parentId) генерит свой автоинкремент-id, но нам
|
||||||
|
// нужен ТОТ ЖЕ id что у автора (иначе rename/remove и folderId
|
||||||
|
// объектов разойдутся). folders — Map<id, data>; перевешиваем запись.
|
||||||
|
{
|
||||||
|
const fm2 = scene.folderManager;
|
||||||
|
if (fm2?.createFolder) {
|
||||||
|
const madeId = fm2.createFolder(op.name, op.parentId ?? null);
|
||||||
|
if (op.id != null && madeId !== op.id && fm2.folders?.has?.(madeId)) {
|
||||||
|
const data = fm2.folders.get(madeId);
|
||||||
|
fm2.folders.delete(madeId);
|
||||||
|
data.id = op.id;
|
||||||
|
fm2.folders.set(op.id, data);
|
||||||
|
if (fm2._nextId != null && op.id >= fm2._nextId) fm2._nextId = op.id + 1;
|
||||||
|
fm2._notifyChange?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 'folderRename':
|
||||||
|
scene.folderManager?.renameFolder?.(op.id, op.name);
|
||||||
|
return;
|
||||||
|
case 'folderRemove':
|
||||||
|
scene.folderManager?.removeFolder?.(op.id, !!op.deleteContent);
|
||||||
|
return;
|
||||||
|
// Пользовательские модели — forceInstanceId, чтобы id совпал у всех
|
||||||
|
// (иначе move/remove по id автора не найдут инстанс у соавтора).
|
||||||
|
case 'userModelAdd':
|
||||||
|
scene.userModelManager?.addInstance?.(
|
||||||
|
op.userModelTypeId, op.x, op.y, op.z, op.rotationY || 0,
|
||||||
|
{ forceInstanceId: op.id });
|
||||||
|
return;
|
||||||
|
case 'userModelRemove':
|
||||||
|
scene.userModelManager?.removeInstance?.(op.id);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case 'add': {
|
case 'add': {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user