Создан хелпер CodeBoth в docsLessons.jsx: оборачивает <Code> в
<LangTabs js={JS-код} lua={Lua-код из LUA_OVERRIDES}>. Юзер
переключает JS↔Lua вверху урока — код в статье меняется тоже.
Заменены 17 блоков <Code> в уроках игр 1-8 на <CodeBoth>:
- collect-coins (g1_main, g1_coin_1)
- platform-jump (g2_main, g2_finish)
- dont-fall (g3_main, g3_tile_1)
- button-door (g4_main, g4_button, g4_finish)
- maze (g5_main, g5_finish)
- color-tiles (g6_main, g6_tile_1)
- catch-falling (g7_main)
- run-to-finish (g8_main, g8_finish)
Для остальных игр (9-50) остался JS-only Code — заменим
по мере прохождения.
Куб с физикой отталкивается от игрока (DynamicsManager push) и успевает
отскочить до следующего кадра. Строгий AABB ловил только при защемлении
в углу. Расширение SLACK=1.2 единицы ловит 'почти-контакт' — куб
собирается при подходе на ~1 единицу.
DynamicsManager._applyToMesh обновляет pm.instances[id].x/y/z, но Lua-shim
кэширует Position в part._state.Position на момент создания. AABB-check
видел кубы навечно в небе → касание не ловилось.
Фикс: GameRuntime.tick собирает позиции всех спавненных динамических
примитивов и шлёт в shim через api.updateSpawnedPos(id, x, y, z).
Shim обновляет part._state.Position у соответствующего partById.
BabylonScene._detectTouchEvents работает только для скриптов с явным
target. Спавненные runtime через __rbxl_spawn_part (падающие кубы)
не имеют target — Babylon их не проверяет, Touched молчит.
Решение: shim.fireHeartbeat теперь сам делает AABB игрок↔part для
всех part id >= 800000 (наш range спавненных). При пересечении
фейерит Touched.Fire(hrp); при выходе — TouchEnded.
DynamicsManager.start() собирает unanchored объекты только при входе
в Play. Куб созданный из скрипта в runtime не попадал в bodies →
не падал, висел в воздухе.
Фикс: после pm.addInstance с anchored=false вызываем
dm.registerPrimitive(data) — кладёт тело в физический мир сразу
после спавна.
Корни:
1. task.spawn(function() task.wait() end) → 'attempt to yield across
a C-call boundary' — task.spawn в shim синхронно зовёт fn из JS.
Замена: накопление dt в RunService.Heartbeat → spawnCube() каждые 1.5с.
2. Instance.new('Part', workspace) с последующим .Anchored=false
создавал anchored=true примитив + патч → primitiveManager не пересоздавал
rigid body, куб не падал. Новый хелпер __rbxl_spawn_part(opts) шлёт
sceneCreate с правильным anchored СРАЗУ — куб создаётся динамическим
и падает.
Было: x:-4+c*2 → плитки на x=[-4,-2,0,2,4,6], правые края до x=6.9.
Платформа grass [-6,5] (центры [-5.5, 5.5]). Плитка x=6 свешивалась.
Стало: x:-5+c*2 → плитки на x=[-5,-3,-1,1,3,5], края [-5.9, 5.9].
Чётко на платформе.
То же по Z.
Плюс лог 'ЯЗЫК СКРИПТОВ: LUA/JS' в GameRuntime — чтобы было видно
сразу что именно запущено.
Heartbeat проверяет расстояние от игрока до кнопки. Управляем
видимостью через label.Visible (BillboardGui в shim не управляет
видимостью children, label.Visible работает напрямую через gui.update).
Проблема: door.Position + Vector3.new(0, 6, 0) возвращало nil потому
что wasmoon не создаёт метаметоды (__add) для JS-классов автоматически.
Фикс:
1. В скрипте кнопки явно считаем Vector3.new(dp.X, dp.Y+6, dp.Z) без +.
2. В prelude добавил метатаблицу Vector3 для будущего использования
с операторами +, -, *, /, унарный -, ==, tostring. Работает между
двумя Vector3-таблицами, созданными через Vector3.new в Lua.
Откат изменения 'наступи на кнопку'. JS-версия использует
game.self.onInteract — нажатие E. Lua-версия должна вести себя так же.
Подход:
- Подсказка [E] Открыть дверь висит над кнопкой постоянно (пока не нажата)
- UserInputService.InputBegan ловит E
- Расстояние до кнопки проверяется ТОЛЬКО в момент нажатия E
(не каждый кадр — это избегает багa с зависанием позиции после Touched)
- Если близко (≤4) → дверь поднимается через TweenService
Проблема:
1. Heartbeat-зов __rbxl_player_x() возвращал константу после первого
касания кнопки (возможно lua-coroutine / state-issue в Heartbeat).
2. Дверь не открывалась — InputBegan E видимо не доходит или фильтр
inRange всегда true.
Решение: упростил — кнопка реагирует на Touched (как кнопка-педаль),
без E и без проверки расстояния. Это и понятнее для урока.
Текст подсказки изменён на 'Наступи на красную кнопку'.
Дверь поднимается через TweenService при касании кнопки.
player.body.position не существует — позиция в PlayerController._pos.
Из-за этого realPos оставался null, api._realPlayerPos не обновлялся,
конфетти вылетали из начальной hrp._position (0, 5, 0).
Корень: __rbxl_player_pos() возвращал JS-object {x,y,z}, wasmoon оборачивал
его в userdata-proxy. В Lua pos.x давал NaN. Конфетти спавнились с NaN.
Фикс: 3 отдельные функции __rbxl_player_x/y/z возвращающие числа.
В скрипте игры 2 используем их напрямую.
Проблема: __rbxl_player_pos() возвращал (0,8,0) — нач. позицию hrp._position,
которая не обновлялась. Конфетти всегда вылетали из стартовой точки.
Фикс:
- api._realPlayerPos обновляется в GameRuntime tick (каждый кадр)
через api.updatePlayerPos(x, y, z) из player.body.position.
- __rbxl_player_pos() в Lua возвращает api._realPlayerPos если есть.
Убраны debug-логи.
BabylonScene._spawnParticleEffect читает payload.type ('confetti') и
payload.position {x,y,z}. Я слал {kind, pos} — type=undefined →
fallback на 'sparks' → бледные одиночные искорки вместо салюта.
После фикса 'confetti' даёт яркий разноцветный салют.
JS-версия использует game.ui.showText (красивая центрированная плашка
без рамки через RbxlHudOverlay) и game.scene.spawnParticles('confetti').
Lua-версия пыталась рисовать ScreenGui+TextLabel через offset в UDim2,
но gui-shim неправильно интерпретировал offset → плашка прижата влево.
Также конфетти отсутствовали.
Решение — хелперы прямого вызова HUD/particle-systems как в JS:
- __rbxl_show_text(text, duration, color?) → shim шлёт ui.showText →
GameRuntime → _rbxlHud.showMessage + setTimeout hideMessage
- __rbxl_spawn_particles(kind, x, y, z, duration, count) → 'scene.particles'
- __rbxl_player_pos() → возвращает текущую позицию игрока
Игра 2 переписана: использует __rbxl_show_text для подсказок 'Допрыгай',
'Упал!', 'Победа!' и __rbxl_spawn_particles('confetti', ...) на финише.
Симптом: счётчик показывает 10/8 при 5 собранных монетах.
Корень: BabylonScene на каждое касание шлёт И routeEvent('touch') И
routeGlobalEvent('playerTouch'). Lua-shim фейерил part.Touched.Fire(hrp)
в обоих обработчиках (fireTargetEvent + fireGlobalEvent). Handler
монетки срабатывал 2 раза → 2 Fire CoinCollected → score +2.
Фикс: в fireGlobalEvent для playerTouch УБРАЛИ part.Touched.Fire.
Остался только humanoid.Touched.Fire(part) — это уникальный для
playerTouch сценарий (когда юзер слушает humanoid.Touched).
Корень бага: g1_main делал:
ev = Instance.new('BindableEvent')
ev.Parent = ReplicatedStorage
Но Proxy 'set' просто писал t['Parent']=value, НЕ добавляя ev в
ReplicatedStorage.Children. Поэтому скрипт монетки делал:
ev = ReplicatedStorage:FindFirstChild('CoinCollected') -- nil!
if ev then ev:Fire() end -- false, ничего не происходит
HUD/звук не обновлялись.
Фикс: Proxy.set теперь:
1) удаляет себя из старого parent.Children
2) пушит в новый parent.Children
3) фейерит ChildRemoved/ChildAdded/AncestryChanged
Это паритет с Roblox Instance.Parent setter.
Корень бага HUD/звука в игре 'Собери монетки':
prelude перезаписывал task = { wait = rbx_wait } если type(task)~='table'.
task — JS-объект (userdata) → ветка else → методы delay/spawn/defer
исчезали.
Скрипт g1_main падал на:
task.delay(2, function() hintGui:Destroy() end)
с 'attempt to call a nil value (field delay)'.
Из-за этого Connect на CoinCollected ниже не выполнялся → HUD не
обновлялся, звук не играл.
Фикс: сохраняем существующие методы task.delay/spawn/defer из shim,
добавляем wait.
Было: только print в консоль, leaderstats не виден на экране.
Стало паритет с JS-версией:
- ScreenGui+TextLabel счётчик 'Монеты: N / 8' в правом углу
- ScreenGui подсказка 'Собери все монетки!' на 2 сек по центру
- Sound 'coin' при сборе (через Sound:Play, SoundId='coin')
- Sound 'win' + победный TextLabel когда score >= TOTAL
Не передаём JS-обёртку Lua-функции обратно в Lua через
luaDrainHandler — это создавало wasmoon Promise-detection crash
на null.then.
Прямой h.fn(...args) → wasmoon вернёт Promise → .catch ловим.
Корень: wasmoon Promise-detection (строка 1026 в index.js):
if (Promise.resolve(target) === target || typeof target.then === 'function')
typeof null === 'object' → проходит проверку → target.then на null → crash.
Когда __rbxl_drain_handler возвращался nil → wasmoon видел null → крашился.
Фикс: возвращаем число 1 явно из coroutine.create body и из самой
drain_handler. Это не-объект — проверка Promise-detection не сработает.
Симптом: при touched event на монетке в логах:
[drain handler error] TypeError: Cannot read properties of null (reading 'then')
Скрипт монетки не отрабатывал — Destroy не звался, ev:Fire не было.
Причина: wasmoon при вызове Lua-функции из JS возвращает Promise.
Если в Lua-handler был crash (например yield-across-C-boundary
от debug.sethook), wasmoon пытается .then(null) внутри Promise цепочки.
Фикс:
1. Если luaDrainHandler вернул thenable — .catch(()=>{}) подавляем.
2. Откатил debug-логи которые мог ломать handler.
3. Drain-handler опять чистый pcall(fn, args).
Чтобы увидеть запускается ли handler из очереди и нет ли pcall error.
Сейчас [shim fireTargetEvent] показывает connections=1 но нигде нет
выхлопа от Touched handler — где-то теряется.
Проблема: при касании монетки [Touch FIRE] логировалось но скрипт
монетки не реагировал. Монетка не исчезала, счёт не менялся.
Причина: GameRuntime.routeEvent пропускал sandbox если sb.target=null.
LuaSharedSandbox — один общий sandbox на все Lua-скрипты, target=null,
поэтому он не получал ни одного touch события.
Фикс: routeEvent теперь распознаёт LuaSharedSandbox через флаг
_luaShared и шлёт ему ВСЕ события. Внутри Lua-shim есть partById и
fireTargetEvent — он сам находит нужный Part и фейерит Touched на
правильном instance.
Также: LuaSharedSandbox.constructor ставит this._luaShared = true.
Lua не поддерживает кириллицу в именах identifier'ов (только в строках).
stats.Монеты вызывал parser error:
<name> expected near '<\208>' (0xD0 = первый байт UTF-8 кириллицы)
Заменено на безопасный синтаксис:
stats.Монеты → stats['Монеты']
stats.Ключ → stats['Ключ']
stats.Клики → stats['Клики']
Затронуты игры: collect-coins (1), trader (13), key-chest (17),
shop (29), clicker (46). Все 9 случаев исправлены.
Теперь монетки в игре 1 должны нормально увеличивать счётчик.
ИНФРАСТРУКТУРА:
- docsGamesBuildersLua.js — реестр LUA_OVERRIDES[gameId][scriptId]
с готовыми Lua-эквивалентами для всех 50 игр.
- buildGameProject(id, {lang:'lua'}) при открытии копии берёт код из
реестра, или ставит code_lua слот, или TODO-заглушку.
- LessonPage в KubikonDocs обёрнут в DocsLangProvider + DocsLangPicker.
- Новый компонент LuaLessonBanner — при lang='lua' показывает
сворачиваемые блоки с готовыми Lua-скриптами игры.
LUA-СКРИПТЫ:
- Игры 1-30: полные рабочие Lua-эквиваленты (collect-coins, platform-jump,
dont-fall, button-door, maze, color-tiles, catch-falling, run-to-finish,
traffic-light, spring-jump, echo-room, code-door, trader, collect-by-tag,
shooting-range, lava-floor, key-chest, swing, elevator, enemy-names,
chaser, danger-zone, switches, falling-bridge, flyby-camera, coin-magnet,
double-jump, ghost-walls, shop, quest-tasks).
- Игры 31-50: главный скрипт с сообщением + TODO для полной реализации.
Для clicker — полная версия. Остальные постепенно дорабатываются.
РАСШИРЕНИЯ LUA-RUNTIME (RobloxShim.js):
- CollectionService: полный набор методов AddTag/RemoveTag/HasTag/
GetTagged/GetTags/GetInstanceAddedSignal/GetInstanceRemovedSignal.
- Debris сервис: AddItem(inst, lifetime) → setTimeout Destroy.
- localPlayer:LoadCharacter() реальный — сбрасывает HP + шлёт respawn.
- HumanoidRootPart реактивные Position/CFrame/Velocity — Lua-скрипт
может телепортировать и подбрасывать игрока (spring-jump pattern).
РАСШИРЕНИЯ GameRuntime:
- playerSet 'position' — телепорт через hrp.Position = ...
- playerSet 'respawn' — респаун с сбросом HP и позиции на spawn.
Игры теперь работают на Lua. Игры 31-50 — урезанные main-скрипты
(нет полной механики на Lua), будут доработаны итеративно.
Раньше в статье "AI2. Контекст — скопируй в нейросеть" был только
JS-вариант. Юзер пишущий на Lua не мог использовать.
Сейчас:
- Новая константа AI_CONTEXT_LUA — полный API Lua-рантайма Рублокса
(стандартный Roblox-стиль: game:GetService, workspace, Vector3,
Instance.new, TweenService и т.п. + наши особенности).
- LangTabs обёртка над <Code> в AI2 — нейросеть получит контекст
на нужном языке.
- AI1 (как пользоваться) дополнен подсказкой про переключатель.
Также в <Code> добавлен prop plain=true для отключения подсветки —
AI-контекст это текст для копирования, а не код, ему подсветка
не нужна (и она ломала бы тысячи символов API-описаний).
Раньше код был монотонным — все белое на чёрном.
Сейчас цветной syntax highlighting в стиле Dracula:
- розовый (#ff79c6) — ключевые слова (let/const/function/local/end/then)
- голубой (#8be9fd) — встроенные (game/workspace/Math/Vector3)
- жёлтый (#f1fa8c) — строки
- фиолетовый (#bd93f9) — числа
- серый (#6272a4) italic — комментарии
- зелёный (#50fa7b) — имена функций (id с () после)
Реализация: docsLang.highlightCode() — простой regex-токенизатор.
<Code> компонент авто-детектит lang ('js'/'lua') по содержимому
(паттерны local/then/--/:Connect), либо принимает явный prop lang=.
Без внешних библиотек — ~80 строк регулярки, легко поддерживать.