Compare commits
No commits in common. "8a66ce03f9b2358b5782d6fec41fa6790eb7f235" and "dc669a51f4727bca89eac4ba46c0b04d73dcec76" have entirely different histories.
8a66ce03f9
...
dc669a51f4
@ -10,7 +10,7 @@ export const USER_addres = BASE + '/api-user';
|
||||
export const ACHIVES_addres = BASE + '/api-achievs';
|
||||
export const COMMENTS_addres = BASE + '/api-comments';
|
||||
export const STORYS_addres = BASE + '/api-storys';
|
||||
// rbxl-importer: импорт .rbxl карт Roblox (см. вики «Импорт из Roblox»)
|
||||
// rbxl-importer: только для МИНа (тест-фича импорта .rbxl карт Roblox)
|
||||
export const RBXL_addres = BASE + '/api-rbxl';
|
||||
export const NOTICES_addres = BASE + '/api-notices';
|
||||
export const HELP_addres = BASE + '/api-help';
|
||||
|
||||
@ -390,16 +390,18 @@ const KubikonStudio = () => {
|
||||
<span className={cl.navIcon}><Icon name="book-open" size={15} /></span>
|
||||
<span>ВИКИ</span>
|
||||
</button>
|
||||
{/* Импорт Roblox .rbxl — доступно всем */}
|
||||
<button
|
||||
className={cl.navItem}
|
||||
onClick={() => setRbxlImportOpen(true)}
|
||||
title="Импортировать игру из Roblox (.rbxl файл)"
|
||||
style={{ marginTop: 8, borderTop: '1px solid #333', paddingTop: 12 }}
|
||||
>
|
||||
<span className={cl.navIcon}>📦</span>
|
||||
<span>Импорт Roblox</span>
|
||||
</button>
|
||||
{/* Импорт Roblox .rbxl — только для МИНа (user_id=1) */}
|
||||
{getCurrentUserId() === 1 && (
|
||||
<button
|
||||
className={cl.navItem}
|
||||
onClick={() => setRbxlImportOpen(true)}
|
||||
title="Импортировать игру из Roblox (.rbxl файл) — тест-фича"
|
||||
style={{ marginTop: 8, borderTop: '1px solid #333', paddingTop: 12 }}
|
||||
>
|
||||
<span className={cl.navIcon}>📦</span>
|
||||
<span>Импорт Roblox</span>
|
||||
</button>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<RbxlImportModal
|
||||
|
||||
@ -5073,350 +5073,6 @@ 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)
|
||||
// ════════════════════════════════════════════════════
|
||||
|
||||
@ -4228,502 +4228,14 @@ end)`;
|
||||
})(),
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// ИГРА 45 — «Стрелялка-арена»
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
'arena-shooter': {
|
||||
g45_main: `-- === ИГРА «СТРЕЛЯЛКА-АРЕНА» — главный скрипт (Lua) ===
|
||||
${SNIPPET_BROADCAST}
|
||||
|
||||
local Players = game:GetService("Players")
|
||||
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)`,
|
||||
},
|
||||
// ИГРЫ 45-50: явных Lua-версий пока нет.
|
||||
// buildGameProject в docsGamesBuilders.js использует generateFallbackLua
|
||||
// (главный скрипт → показ подсказки + слушает FinishReached →
|
||||
// победа+конфетти; скрипт на финиш-примитиве → шлёт FinishReached;
|
||||
// остальные target-скрипты → красят примитив на касание).
|
||||
// Это даёт «хоть что-то рабочее» в любой игре до того как напишем
|
||||
// полноценный Lua-скрипт. Когда дописываем игру — добавляем сюда явный override.
|
||||
'clicker': { g46_main: simpleClicker() },
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
|
||||
@ -113,11 +113,7 @@ export function highlightCode(text, lang) {
|
||||
}
|
||||
|
||||
|
||||
// v2 — раньше при первом включении lua-режима сохранялся в LS и юзер
|
||||
// потом всегда видел Lua-таб по умолчанию. Бамп ключа = сброс на JS
|
||||
// у всех уже-открытых вкладок.
|
||||
const LS_KEY = 'rublox.docs.lang.v2';
|
||||
const LS_KEY_OLD = 'rublox.docs.lang';
|
||||
const LS_KEY = 'rublox.docs.lang';
|
||||
const DEFAULT_LANG = 'js';
|
||||
|
||||
const DocsLangContext = createContext({
|
||||
@ -128,8 +124,6 @@ const DocsLangContext = createContext({
|
||||
export function DocsLangProvider({ children }) {
|
||||
const [lang, setLangState] = useState(() => {
|
||||
try {
|
||||
// Очищаем старый ключ — у части юзеров там залип 'lua'
|
||||
localStorage.removeItem(LS_KEY_OLD);
|
||||
const v = localStorage.getItem(LS_KEY);
|
||||
return v === 'lua' ? 'lua' : 'js';
|
||||
} catch (_) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Code, ScriptKind, Step, Note, Try, Shot } from './docsData';
|
||||
import { LangTabs, useDocsLang } from './docsLang';
|
||||
import { LangTabs } from './docsLang';
|
||||
import { LUA_OVERRIDES } from './docsGamesBuildersLua';
|
||||
|
||||
/**
|
||||
@ -20,17 +20,6 @@ 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 вики).
|
||||
*
|
||||
@ -6302,7 +6291,7 @@ game.self.onTouch(() => {
|
||||
стрелять и проверяет, кто победил.
|
||||
</p>
|
||||
<ScriptKind kind="global" />
|
||||
<CodeBoth game="tower-defense" script="g44_main">{`// === ИГРА «TOWER DEFENSE» — главный скрипт ===
|
||||
<Code>{`// === ИГРА «TOWER DEFENSE» — главный скрипт ===
|
||||
|
||||
let leaked = 0; // врагов прошло до базы
|
||||
const MAX_LEAK = 8;
|
||||
@ -6385,7 +6374,7 @@ game.every(0.5, () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
});`}</CodeBoth>
|
||||
});`}</Code>
|
||||
<p>Разберём:</p>
|
||||
<ul>
|
||||
<li><code>towers</code> и <code>enemies</code> — два списка:
|
||||
@ -6406,7 +6395,7 @@ game.every(0.5, () => {
|
||||
|
||||
<h3 className="lessonH">Шаг 3. Скрипт площадки под башню</h3>
|
||||
<ScriptKind kind="object" on="каждую площадку" />
|
||||
<CodeBoth game="tower-defense" script="g44_slot_1">{`// === Скрипт площадки под башню ===
|
||||
<Code>{`// === Скрипт площадки под башню ===
|
||||
let built = false;
|
||||
game.self.onInteract(() => {
|
||||
if (built) return;
|
||||
@ -6419,7 +6408,7 @@ game.self.onInteract(() => {
|
||||
color: '#ffcc33',
|
||||
});
|
||||
game.broadcast('addTower', { x: pos.x, z: pos.z });
|
||||
}, { text: 'Построить башню', distance: 4 });`}</CodeBoth>
|
||||
}, { text: 'Построить башню', distance: 4 });`}</Code>
|
||||
<p>
|
||||
При нажатии <kbd className="kbd">E</kbd> скрипт создаёт
|
||||
жёлтый цилиндр-башню над площадкой и шлёт сообщение
|
||||
@ -6526,7 +6515,7 @@ game.scene.spawn('user:3', {
|
||||
|
||||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||||
<ScriptKind kind="global" />
|
||||
<CodeBoth game="arena-shooter" script="g45_main">{`// === ИГРА «СТРЕЛЯЛКА-АРЕНА» — главный скрипт ===
|
||||
<Code>{`// === ИГРА «СТРЕЛЯЛКА-АРЕНА» — главный скрипт ===
|
||||
|
||||
let score = 0;
|
||||
const GOAL = 15;
|
||||
@ -6588,7 +6577,7 @@ game.every(1.8, () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
});`}</CodeBoth>
|
||||
});`}</Code>
|
||||
<p>Разберём:</p>
|
||||
<ul>
|
||||
<li><code>game.onHpChange((e) ={'>'} ...)</code> —
|
||||
@ -6671,7 +6660,7 @@ game.every(1.8, () => {
|
||||
|
||||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||||
<ScriptKind kind="global" />
|
||||
<CodeBoth game="clicker" script="g46_main">{`// === ИГРА «КЛИКЕР» — главный скрипт ===
|
||||
<Code>{`// === ИГРА «КЛИКЕР» — главный скрипт ===
|
||||
|
||||
let points = 0; // очки
|
||||
let perClick = 1; // очков за клик
|
||||
@ -6736,7 +6725,7 @@ game.onMessage('buyAuto', () => {
|
||||
game.ui.score = points;
|
||||
game.sound.play('pickup');
|
||||
game.ui.showText('Авто-доход: +' + autoIncome + ' в секунду', 2);
|
||||
});`}</CodeBoth>
|
||||
});`}</Code>
|
||||
<p>Разберём:</p>
|
||||
<ul>
|
||||
<li><code>points</code> — очки, <code>perClick</code> —
|
||||
@ -6756,22 +6745,22 @@ game.onMessage('buyAuto', () => {
|
||||
|
||||
<h3 className="lessonH">Шаг 3. Скрипты куба и кнопок</h3>
|
||||
<ScriptKind kind="object" on="жёлтый куб-кликер" />
|
||||
<CodeBoth game="clicker" script="g46_cube">{`// === Скрипт куба-кликера ===
|
||||
<Code>{`// === Скрипт куба-кликера ===
|
||||
game.self.onClick(() => {
|
||||
game.broadcast('click');
|
||||
// куб слегка вспыхивает
|
||||
game.scene.spawnParticles('sparks', game.self.position, { duration: 0.3 });
|
||||
});`}</CodeBoth>
|
||||
});`}</Code>
|
||||
<ScriptKind kind="object" on="красную кнопку" />
|
||||
<CodeBoth game="clicker" script="g46_up1">{`// === Скрипт улучшения «сила клика» (20 очков) ===
|
||||
<Code>{`// === Скрипт улучшения «сила клика» (20 очков) ===
|
||||
game.self.onInteract(() => {
|
||||
game.broadcast('buyPower');
|
||||
}, { text: 'Купить +силу клика (20)', distance: 3 });`}</CodeBoth>
|
||||
}, { text: 'Купить +силу клика (20)', distance: 3 });`}</Code>
|
||||
<ScriptKind kind="object" on="синюю кнопку" />
|
||||
<CodeBoth game="clicker" script="g46_up2">{`// === Скрипт улучшения «авто-доход» (40 очков) ===
|
||||
<Code>{`// === Скрипт улучшения «авто-доход» (40 очков) ===
|
||||
game.self.onInteract(() => {
|
||||
game.broadcast('buyAuto');
|
||||
}, { text: 'Купить авто-доход (40)', distance: 3 });`}</CodeBoth>
|
||||
}, { text: 'Купить авто-доход (40)', distance: 3 });`}</Code>
|
||||
<Note>
|
||||
Главная идея кликера: сначала кликаешь руками, потом
|
||||
покупаешь улучшения — и игра «играет сама». Это экономика:
|
||||
@ -6841,7 +6830,7 @@ game.self.onInteract(() => {
|
||||
|
||||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||||
<ScriptKind kind="global" />
|
||||
<CodeBoth game="escape-quest" script="g47_main">{`// === ИГРА «КВЕСТ-ПОБЕГ» — главный скрипт ===
|
||||
<Code>{`// === ИГРА «КВЕСТ-ПОБЕГ» — главный скрипт ===
|
||||
|
||||
let pressed = 0; // сколько кнопок нажато
|
||||
const TOTAL = 3;
|
||||
@ -6873,7 +6862,7 @@ game.onMessage('escape', () => {
|
||||
const p = game.player.position;
|
||||
game.scene.spawnParticles('confetti',
|
||||
{ x: p.x, y: p.y + 3, z: p.z }, { duration: 3, count: 3 });
|
||||
});`}</CodeBoth>
|
||||
});`}</Code>
|
||||
<p>Разберём:</p>
|
||||
<ul>
|
||||
<li><code>pressed</code> — счётчик нажатых кнопок,
|
||||
@ -6889,19 +6878,19 @@ game.onMessage('escape', () => {
|
||||
|
||||
<h3 className="lessonH">Шаг 3. Скрипты кнопки и финиша</h3>
|
||||
<ScriptKind kind="object" on="каждую красную кнопку" />
|
||||
<CodeBoth game="escape-quest" script="g47_btn_1">{`// === Скрипт кнопки 1 ===
|
||||
<Code>{`// === Скрипт кнопки 1 ===
|
||||
let used = false;
|
||||
game.self.onInteract(() => {
|
||||
if (used) return;
|
||||
used = true;
|
||||
game.scene.setColor(game.self.ref, '#22dd55'); // нажата — зелёная
|
||||
game.broadcast('pressButton');
|
||||
}, { text: 'Нажать кнопку', distance: 3 });`}</CodeBoth>
|
||||
}, { text: 'Нажать кнопку', distance: 3 });`}</Code>
|
||||
<ScriptKind kind="object" on="зелёный финиш" />
|
||||
<CodeBoth game="escape-quest" script="g47_finish">{`// === Скрипт финиша ===
|
||||
<Code>{`// === Скрипт финиша ===
|
||||
game.self.onTouch(() => {
|
||||
game.broadcast('escape');
|
||||
});`}</CodeBoth>
|
||||
});`}</Code>
|
||||
<p>
|
||||
Кнопка при нажатии становится зелёной (видно, что нажата),
|
||||
шлёт <code>game.broadcast('pressButton')</code> и больше
|
||||
@ -6980,7 +6969,7 @@ game.self.onTouch(() => {
|
||||
|
||||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||||
<ScriptKind kind="global" />
|
||||
<CodeBoth game="mp-tag" script="g48_main">{`// === ИГРА «МУЛЬТИПЛЕЕР: САЛКИ» — главный скрипт ===
|
||||
<Code>{`// === ИГРА «МУЛЬТИПЛЕЕР: САЛКИ» — главный скрипт ===
|
||||
//
|
||||
// Это МУЛЬТИПЛЕЕРНАЯ игра. Чтобы играть с друзьями, опубликуй её
|
||||
// с галочкой «Мультиплеер» — тогда в комнату смогут зайти несколько
|
||||
@ -7017,7 +7006,7 @@ game.room.onChange('tagger', (taggerId) => {
|
||||
} else {
|
||||
game.ui.showText('Убегай от водящего!', 3);
|
||||
}
|
||||
});`}</CodeBoth>
|
||||
});`}</Code>
|
||||
<p>Разберём:</p>
|
||||
<ul>
|
||||
<li><code>game.players.count()</code> — сколько игроков
|
||||
@ -7108,7 +7097,7 @@ game.room.onChange('tagger', (taggerId) => {
|
||||
|
||||
<h3 className="lessonH">Шаг 2. Главный скрипт</h3>
|
||||
<ScriptKind kind="global" />
|
||||
<CodeBoth game="mp-race" script="g49_main">{`// === ИГРА «МУЛЬТИПЛЕЕР: ГОНКА» — главный скрипт ===
|
||||
<Code>{`// === ИГРА «МУЛЬТИПЛЕЕР: ГОНКА» — главный скрипт ===
|
||||
//
|
||||
// Мультиплеерная гонка. Чтобы соревноваться с друзьями — опубликуй
|
||||
// игру с галочкой «Мультиплеер».
|
||||
@ -7148,7 +7137,7 @@ game.onMessage('finish', () => {
|
||||
} else {
|
||||
game.ui.showText('Финиш! Но кто-то был быстрее.', 4);
|
||||
}
|
||||
});`}</CodeBoth>
|
||||
});`}</Code>
|
||||
<p>Разберём:</p>
|
||||
<ul>
|
||||
<li><code>game.room.get('winner')</code> — читаем общую
|
||||
@ -7168,10 +7157,10 @@ game.onMessage('finish', () => {
|
||||
|
||||
<h3 className="lessonH">Шаг 3. Скрипт финиша</h3>
|
||||
<ScriptKind kind="object" on="зелёный финиш" />
|
||||
<CodeBoth game="mp-race" script="g49_finish">{`// === Скрипт финиша ===
|
||||
<Code>{`// === Скрипт финиша ===
|
||||
game.self.onTouch(() => {
|
||||
game.broadcast('finish');
|
||||
});`}</CodeBoth>
|
||||
});`}</Code>
|
||||
<p>
|
||||
Когда любой игрок касается финиша, скрипт шлёт сообщение
|
||||
<code> game.broadcast('finish')</code> — а главный скрипт
|
||||
@ -7281,10 +7270,8 @@ game.self.onTouch(() => {
|
||||
</ul>
|
||||
<p>
|
||||
И обязательно покажи игроку, когда он <b>победил</b> —
|
||||
надписью <Api js="game.ui.showText('Победа!', 5)"
|
||||
lua={'__rbxl_show_text("Победа!", 5)'} />,
|
||||
звуком <Api js="game.sound.play('win')"
|
||||
lua={'winSound:Play()'} /> и конфетти.
|
||||
надписью <code>game.ui.showText('Победа!', 5)</code>,
|
||||
звуком <code>game.sound.play('win')</code> и конфетти.
|
||||
</p>
|
||||
|
||||
<h3 className="lessonH">Шаг 4. Напиши скрипты</h3>
|
||||
@ -7292,48 +7279,34 @@ game.self.onTouch(() => {
|
||||
Сцена сама по себе не «живая» — её оживляют скрипты.
|
||||
Начинай с <b>главного скрипта</b>: в нём заводи переменные
|
||||
(счёт, флажок победы) и <b>лови сообщения</b> через
|
||||
<> </><Api js="game.onMessage('имя', fn)"
|
||||
lua={'getEvent("Имя").Event:Connect(fn)'} />.
|
||||
На объекты вешай небольшие скрипты — они шлют сообщения
|
||||
главному через <Api js="game.broadcast('имя')"
|
||||
lua={'getEvent("Имя"):Fire()'} />.
|
||||
Так главный скрипт узнаёт, что монетку собрали или кнопку
|
||||
нажали. Ты делал так в каждом уроке.
|
||||
<code> game.onMessage('имя', fn)</code>. На объекты вешай
|
||||
небольшие скрипты — они шлют сообщения главному через
|
||||
<code> game.broadcast('имя')</code>. Так главный скрипт
|
||||
узнаёт, что монетку собрали или кнопку нажали. Ты делал
|
||||
так в каждом уроке.
|
||||
</p>
|
||||
<Note>
|
||||
Каждый скрипт работает в своей «песочнице» — переменные
|
||||
одного скрипта не видны другому. Поэтому скрипты общаются
|
||||
сообщениями: один шлёт <Api js="game.broadcast('имя')"
|
||||
lua={'getEvent("Имя"):Fire()'} />,
|
||||
другой ловит <Api js="game.onMessage('имя', fn)"
|
||||
lua={'getEvent("Имя").Event:Connect(fn)'} />.
|
||||
Можно передать данные: <Api
|
||||
js="game.broadcast('имя', { ... })"
|
||||
lua={'getEvent("Имя"):Fire(data)'} />.
|
||||
сообщениями: один шлёт <code>game.broadcast('имя')</code>,
|
||||
другой ловит <code>game.onMessage('имя', fn)</code>. Можно
|
||||
передать данные: <code>game.broadcast('имя', {'{'} ... {'}'})</code>.
|
||||
</Note>
|
||||
<p>Базовый набор инструментов, который ты знаешь:</p>
|
||||
<ul>
|
||||
<li><Api js="game.self.onTouch"
|
||||
lua={'part.Touched:Connect(fn)'} /> — реакция на касание;</li>
|
||||
<li><Api js="game.self.onInteract"
|
||||
lua={'UserInputService.InputBegan + Heartbeat (дистанция)'} /> — реакция на
|
||||
<li><code>game.self.onTouch</code> — реакция на касание;</li>
|
||||
<li><code>game.self.onInteract</code> — реакция на
|
||||
<kbd className="kbd">E</kbd>;</li>
|
||||
<li><Api js="game.self.onClick"
|
||||
lua={'ClickDetector + MouseClick:Connect'} /> — реакция на клик;</li>
|
||||
<li><Api js="game.broadcast" lua={'BindableEvent:Fire'} /> и
|
||||
<> </><Api js="game.onMessage" lua={'BindableEvent.Event:Connect'} />
|
||||
<li><code>game.self.onClick</code> — реакция на клик;</li>
|
||||
<li><code>game.broadcast</code> и <code>game.onMessage</code>
|
||||
— связь между скриптами;</li>
|
||||
<li><Api js="game.onTick"
|
||||
lua={'RunService.Heartbeat:Connect'} /> — каждый кадр;</li>
|
||||
<li><Api js="game.after / game.every"
|
||||
lua={'task.delay / task.spawn'} /> — таймеры;</li>
|
||||
<li><Api js="game.tween"
|
||||
lua={'TweenService:Create(part, TweenInfo.new(t), goal):Play()'} /> — плавное движение;</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>
|
||||
<li><code>game.onTick</code> — каждый кадр;</li>
|
||||
<li><code>game.after</code> и <code>game.every</code> —
|
||||
таймеры;</li>
|
||||
<li><code>game.tween</code> — плавное движение;</li>
|
||||
<li><code>game.scene.spawnNpc</code> — враги и NPC;</li>
|
||||
<li><code>game.ui.score</code> и
|
||||
<code> game.ui.showText</code> — счёт и подсказки.</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="lessonH">Шаг 5. Проверяй и улучшай</h3>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* RbxlImportModal — модалка импорта .rbxl Roblox-карт в Rublox.
|
||||
*
|
||||
* Доступна всем пользователям (см. вики «Импорт из Roblox» о нюансах).
|
||||
* Доступна ТОЛЬКО МИНу (user_id === 1) — это тест-фича.
|
||||
*
|
||||
* Поток:
|
||||
* 1. Юзер дропает или выбирает .rbxl файл.
|
||||
@ -13,6 +13,8 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { analyzeRbxl, createRbxlProject } from '../api/rbxlImporterApi.js';
|
||||
|
||||
const ALLOWED_USER_ID = 1; // МИН
|
||||
|
||||
const MAX_SIZE = 50 * 1024 * 1024; // 50 MB
|
||||
|
||||
export default function RbxlImportModal({ open, onClose, currentUserId, onCreated }) {
|
||||
@ -35,6 +37,18 @@ export default function RbxlImportModal({ open, onClose, currentUserId, onCreate
|
||||
|
||||
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 = () => {
|
||||
setFile(null); setReport(null); setPreviewHash(null);
|
||||
setTitle(''); setError(null); setAnalyzing(false); setCreating(false);
|
||||
|
||||
@ -204,96 +204,6 @@ 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Снять обёртки (при выходе из коллаба восстановить оригиналы — простой флаг). */
|
||||
@ -531,52 +441,6 @@ export function applyRemoteOp(scene, op) {
|
||||
scene.removeScript?.(op.id);
|
||||
scene._onCollabScriptsChange?.();
|
||||
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) {
|
||||
case 'add': {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user