diff --git a/src/community/KubikonDocs.jsx b/src/community/KubikonDocs.jsx index 8f386d4..a424cb7 100644 --- a/src/community/KubikonDocs.jsx +++ b/src/community/KubikonDocs.jsx @@ -13,6 +13,7 @@ import { GAMES, GAME_GROUPS } from './docsGames'; import { LESSONS, hasLesson } from './docsLessons'; import { buildGameProject } from './docsGamesBuilders'; import DocIcon from './docsIcons'; +import { DocsLangProvider, DocsLangPicker, DOCS_LANG_STYLES, useDocsLang } from './docsLang'; /** * KubikonDocs — вика редактора Рублокс. @@ -76,6 +77,7 @@ const KubikonDocs = () => { return (
+ {/* === Левая боковая панель === */}
+ {state === 'choosing' && ( + createCopyWithLang(lang)} + onCancel={() => setState('idle')} + /> + )} {state === 'error' && (
Не получилось открыть игру. Проверь, что ты вошёл в аккаунт, @@ -492,6 +508,81 @@ const LessonPage = ({ game, navigate }) => { ); }; +// ══════════════════════════════════════════════════════════════════ +// Модалка выбора языка скриптов при «Открыть копию» +// ══════════════════════════════════════════════════════════════════ +const LangChoiceModal = ({ onPick, onCancel }) => { + return ( +
+
e.stopPropagation()}> +

На каком языке открыть копию?

+

+ Скрипты в твоей копии будут написаны на выбранном языке. + Логика игры одинаковая — отличается только запись кода. +

+
+ + +
+ +
+
+ ); +}; + +/** + * Конвертирует все JS-скрипты в project_data в Lua-эквивалент. + * Сейчас простая стратегия: если в скрипте есть code_lua слот, делает его + * активным. Иначе ставит флаг language='lua' и пустой Lua-шаблон с TODO. + * Полноценная транспиляция JS→Lua невозможна без AST-анализа. + */ +function convertProjectScriptsToLua(projectData) { + const scene = projectData?.scene; + if (!scene || !Array.isArray(scene.scripts)) return projectData; + scene.scripts = scene.scripts.map(s => { + if (s.language === 'lua') return s; + // Если уже есть готовый Lua-слот — используем его + if (s.code_lua && s.code_lua.trim()) { + return { + ...s, + language: 'lua', + code: s.code_lua, + code_js: s.code_js || s.code, + code_lua: s.code_lua, + }; + } + // Иначе ставим заглушку с подсказкой + const luaStub = `-- TODO: версия этого скрипта на Lua пока не готова. +-- Оригинальный JS-код сохранён ниже (переключи язык назад на JS в редакторе). +-- Доступные API: game:GetService("Players"), game.Workspace, script.Parent +-- +-- Например, простой пример: +local Players = game:GetService("Players") +print("Привет от Lua-скрипта") +`; + return { + ...s, + language: 'lua', + code: luaStub, + code_js: s.code_js || s.code, + code_lua: luaStub, + }; + }); + return projectData; +} + // ══════════════════════════════════════════════════════════════════ // Инлайн-стили // ══════════════════════════════════════════════════════════════════ diff --git a/src/community/docsData.jsx b/src/community/docsData.jsx index ae4fcc7..e802e4c 100644 --- a/src/community/docsData.jsx +++ b/src/community/docsData.jsx @@ -1,5 +1,6 @@ import React from 'react'; import DocIcon from './docsIcons'; +import { LangTabs } from './docsLang'; /** * docsData.jsx — контент вики редактора Рублокс (разделы A-J). @@ -1122,6 +1123,119 @@ game.gui.onSubmit(boxId, (text) => { title: 'Скрипты — основы', summary: 'Самый важный раздел: что такое скрипт, переменные, события, таймеры.', sections: [ + { + id: 'js-or-lua', + title: 'D0. Скриптинг: JS или Lua — что выбрать?', + body: ( + <> +

+ В Рублоксе можно писать скрипты на двух языках: + JavaScript и Lua. Оба работают одинаково + хорошо. Игра не отличает их между собой — внутри одного + проекта одни скрипты могут быть на JS, другие на Lua, + и они общаются между собой как будто это один язык. +

+

Чем они отличаются?

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
JavaScriptLua
Где ещё используетсяСайты, мобильные приложения, серверы. + Самый популярный язык в мире.Roblox, World of Warcraft (моды), + многие игры. Простой и быстрый.
Главный APIgame.* (game.player, + game.log, game.scene)game.* в Roblox-стиле + (game:GetService("Players"), workspace)
Похож наRoblox-LUA если знаешь RobloxRoblox-Studio — те же команды
Когда выбратьЕсли планируешь делать сайты и приложения + — JS пригодится везде.Если играешь в Roblox и видел там скрипты + — Lua тебе знаком.
+ +

Один и тот же пример на двух языках:

+

Когда игрок касается синего блока — печатаем «Привет».

+ {`// JS — глобальный скрипт +game.onTouch('синий-блок', (player) => { + game.log('Привет, ' + player.name + '!'); +});`}} + lua={{`-- Lua — скрипт на самом блоке +local part = script.Parent + +part.Touched:Connect(function(hit) + local player = game.Players:GetPlayerFromCharacter(hit.Parent) + if player then + print("Привет, " .. player.Name .. "!") + end +end)`}} + /> +

+ Видишь — оба варианта делают одно и то же. Но + запись отличается. JS короче для простых вещей через + game.onTouch, Lua даёт точный Roblox-стиль + через :Connect и события. +

+ +

Что выбирать новичку?

+ + Если ты совсем новичок — бери JavaScript. + Команды game.* в JS короче и проще читать. + Большая часть уроков в этой вике написана с примерами + на JS — все они работают, просто копируй. + + + Если ты уже играл в Roblox и видел там скрипты + — бери Lua. Команды почти один в один как + в Roblox Studio: game:GetService, + :Connect, workspace, + script.Parent. + + +

Можно ли менять язык в одном скрипте?

+

+ Да. В редакторе скрипта вверху есть две кнопки — + JS и Lua. Просто нажми на нужную. + Твой код на текущем языке сохранится, а + на другом языке откроется пустой шаблон или то, что + ты писал там раньше. Никогда ничего не теряется. +

+ + Создай новый скрипт. Напиши пару строк на JS. Нажми + кнопку Lua — JS-код спрячется, появится + Lua-шаблон. Напиши там что-то. Нажми JS + обратно — твой JS-код вернётся. Магия! + + +

А что под капотом?

+

+ JS-скрипты исполняются в WebWorker — + это отдельный поток в браузере, чтобы скрипт не + тормозил саму игру. Lua-скрипты исполняются через + wasmoon — Lua-интерпретатор, скомпилированный + в WebAssembly. Оба варианта работают на любом + устройстве без установки. +

+ + ), + }, { id: 'what-is-script', title: 'D1. Что такое скрипт и как его создать', diff --git a/src/community/docsGamesBuilders.js b/src/community/docsGamesBuilders.js index 1e92045..77b2da3 100644 --- a/src/community/docsGamesBuilders.js +++ b/src/community/docsGamesBuilders.js @@ -6158,8 +6158,37 @@ export function hasGameBuilder(id) { return typeof GAME_BUILDERS[id] === 'function'; } -/** Построить project_data для игры-урока. Возвращает объект или null. */ -export function buildGameProject(id) { +/** Построить project_data для игры-урока. Возвращает объект или null. + * opts.lang: 'js' (default) | 'lua' — на каком языке скрипты в копии. + */ +export function buildGameProject(id, opts = {}) { const fn = GAME_BUILDERS[id]; - return fn ? fn() : null; + if (!fn) return null; + const project = fn(); + if (opts.lang === 'lua' && project) { + // Если в скрипте есть code_lua слот — делаем его активным. + // Иначе ставим stub с заметкой что Lua-версия в работе. + const scene = project.scene || {}; + if (Array.isArray(scene.scripts)) { + scene.scripts = scene.scripts.map(s => { + if (s.language === 'lua') return s; + if (s.code_lua && s.code_lua.trim()) { + return { ...s, language: 'lua', code: s.code_lua, code_js: s.code_js || s.code }; + } + const luaStub = `-- TODO: Lua-версия этого скрипта пока не готова. +-- Переключи язык на JS в редакторе (кнопка JS вверху), чтобы увидеть рабочий код. +-- Lua-API: game:GetService("Players"), workspace, script.Parent +print("Lua-скрипт запущен (заглушка)") +`; + return { + ...s, + language: 'lua', + code: luaStub, + code_js: s.code_js || s.code, + code_lua: luaStub, + }; + }); + } + } + return project; } diff --git a/src/community/docsLang.jsx b/src/community/docsLang.jsx new file mode 100644 index 0000000..55df3ec --- /dev/null +++ b/src/community/docsLang.jsx @@ -0,0 +1,324 @@ +/** + * docsLang.jsx — поддержка вкладок JS/Lua в статьях вики. + * + * Компоненты: + * — оборачивает страницу статьи, хранит выбранный язык + * в localStorage 'rublox.docs.lang' ('js' | 'lua'). + * — большой переключатель JS/Lua над статьёй. + * — вкладка-переключатель внутри статьи. Показывает + * либо js, либо lua, согласно текущему языку. + * useDocsLang() — хук: возвращает {lang, setLang}. + * + * Если в статье нет ни одного — она одинаково выглядит на обоих + * языках (общая теория, не зависящая от языка скриптов). + */ +import React, { createContext, useContext, useEffect, useState } from 'react'; + +const LS_KEY = 'rublox.docs.lang'; +const DEFAULT_LANG = 'js'; + +const DocsLangContext = createContext({ + lang: DEFAULT_LANG, + setLang: () => {}, +}); + +export function DocsLangProvider({ children }) { + const [lang, setLangState] = useState(() => { + try { + const v = localStorage.getItem(LS_KEY); + return v === 'lua' ? 'lua' : 'js'; + } catch (_) { + return DEFAULT_LANG; + } + }); + const setLang = (next) => { + const v = next === 'lua' ? 'lua' : 'js'; + setLangState(v); + try { localStorage.setItem(LS_KEY, v); } catch (_) {} + }; + useEffect(() => { + // Слушаем смену из других вкладок + const onStorage = (e) => { + if (e.key === LS_KEY && (e.newValue === 'js' || e.newValue === 'lua')) { + setLangState(e.newValue); + } + }; + window.addEventListener('storage', onStorage); + return () => window.removeEventListener('storage', onStorage); + }, []); + return ( + + {children} + + ); +} + +export function useDocsLang() { + return useContext(DocsLangContext); +} + +/** Большой переключатель над статьёй: «На каком языке смотреть код?» */ +export function DocsLangPicker() { + const { lang, setLang } = useDocsLang(); + return ( +
+
+ Язык скриптов в этой статье: +
+
+ + +
+
+ Не знаешь что выбрать? Смотри статью D0. Скриптинг: JS или Lua? +
+
+ ); +} + +/** + * Локальный переключатель вкладок внутри статьи. Если js/lua — + * прямой контент (children), если на странице нет — + * показываем оба заголовками. + * + * Использование: + * game.log('Привет')} + * lua={print('Привет')} + * /> + */ +export function LangTabs({ js, lua }) { + const { lang, setLang } = useDocsLang(); + const hasJs = js !== undefined && js !== null; + const hasLua = lua !== undefined && lua !== null; + if (!hasJs && !hasLua) return null; + // Если есть только один язык — показываем без переключателя + if (hasJs && !hasLua) return <>{js}; + if (!hasJs && hasLua) return <>{lua}; + return ( +
+
+ + +
+
+ {lang === 'lua' ? lua : js} +
+
+ ); +} + +export const DOCS_LANG_STYLES = ` +.docsLangPicker { + background: linear-gradient(135deg, #1a1d2e 0%, #14172b 100%); + border: 1px solid #2a3050; + border-radius: 10px; + padding: 14px 18px; + margin: 16px 0 24px; + display: flex; + flex-direction: column; + gap: 10px; +} +.docsLangPicker__label { + font-size: 13px; + font-weight: 600; + color: #c8cce0; +} +.docsLangPicker__tabs { + display: flex; + gap: 8px; +} +.docsLangPicker__tab { + flex: 1; + padding: 10px 16px; + border-radius: 6px; + border: 1px solid transparent; + background: #232842; + color: #aab0c8; + font-size: 14px; + font-weight: 700; + cursor: pointer; + transition: all 0.15s; +} +.docsLangPicker__tab:hover { background: #2a304f; color: #fff; } +.docsLangPicker__tab--js.is-active { + background: linear-gradient(135deg, #f7df1e 0%, #d4b500 100%); + color: #1a1a1c; + border-color: #d4b500; +} +.docsLangPicker__tab--lua.is-active { + background: linear-gradient(135deg, #2196f3 0%, #1565c0 100%); + color: #fff; + border-color: #1565c0; +} +.docsLangPicker__hint { + font-size: 12px; + color: #8a90a8; + font-style: italic; +} + +.docsLangTabs { + margin: 12px 0; + border-radius: 8px; + overflow: hidden; + border: 1px solid #2a3050; +} +.docsLangTabs__head { + display: flex; + background: #181b2c; + border-bottom: 1px solid #2a3050; +} +.docsLangTabs__tab { + padding: 8px 18px; + border: none; + background: transparent; + color: #888da6; + font-size: 12px; + font-weight: 700; + cursor: pointer; + letter-spacing: 0.5px; + border-bottom: 2px solid transparent; +} +.docsLangTabs__tab:hover { color: #c8cce0; } +.docsLangTabs__tab.is-active { + color: #fff; + border-bottom-color: #4a8bff; + background: #1f2338; +} +.docsLangTabs__body { + padding: 0; +} +.docsLangTabs__body > pre, +.docsLangTabs__body > .docCode { margin: 0; border-radius: 0; } + +.docTable { + width: 100%; + border-collapse: collapse; + margin: 12px 0; + font-size: 13px; +} +.docTable th, .docTable td { + border: 1px solid #2a3050; + padding: 8px 10px; + text-align: left; + vertical-align: top; +} +.docTable th { + background: #1a1d2e; + color: #c8cce0; + font-weight: 600; +} +.docTable td:first-child { + background: #181b2c; + width: 25%; + color: #aab0c8; +} +.docTable code { + background: #0e1020; + padding: 1px 5px; + border-radius: 3px; + font-size: 12px; +} + +.langChoiceOverlay { + position: fixed; inset: 0; + background: rgba(0,0,0,0.75); + display: flex; align-items: center; justify-content: center; + z-index: 10000; +} +.langChoiceDialog { + background: #1a1d2e; + border: 1px solid #2a3050; + border-radius: 14px; + padding: 28px; + width: 100%; + max-width: 520px; + box-shadow: 0 20px 60px rgba(0,0,0,0.6); +} +.langChoiceTitle { + font-size: 20px; + margin: 0 0 8px; + color: #fff; +} +.langChoiceSub { + margin: 0 0 20px; + font-size: 13px; + color: #aab0c8; + line-height: 1.5; +} +.langChoiceBtns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin-bottom: 14px; +} +.langChoiceBtn { + padding: 18px 16px; + border-radius: 10px; + border: 2px solid transparent; + text-align: left; + cursor: pointer; + transition: all 0.15s; + color: #fff; +} +.langChoiceBtn--js { + background: linear-gradient(135deg, #f7df1e 0%, #d4b500 100%); + color: #1a1a1c; +} +.langChoiceBtn--js:hover { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(247,223,30,0.3); } +.langChoiceBtn--lua { + background: linear-gradient(135deg, #2196f3 0%, #1565c0 100%); +} +.langChoiceBtn--lua:hover { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(33,150,243,0.4); } +.langChoiceBtn__name { + font-size: 18px; + font-weight: 800; + margin-bottom: 4px; +} +.langChoiceBtn__hint { + font-size: 12px; + font-weight: 400; + opacity: 0.85; +} +.langChoiceCancel { + width: 100%; + padding: 10px; + background: transparent; + border: 1px solid #2a3050; + color: #aab0c8; + border-radius: 8px; + font-size: 13px; + cursor: pointer; +} +.langChoiceCancel:hover { background: #232842; color: #fff; } +`;