Раньше: проверка дистанции до врага при ЛКМ через InputBegan
+ MouseButton1. Никак не работало из-за десятка причин.
Теперь как в Тире:
- BabylonScene._meshToTarget теперь возвращает {kind:'npc', id:N}
для меша с metadata.npcId.
- routeGlobalEvent('click', {target}) — этим уже шлёт в Lua-shim
с target.
- Shim добавлен __rbxl_npc_on_click(ref, fn) — регистрация callback'а.
В fireGlobalEvent при type='click'+target.kind='npc' резолвим
локальный ref и фейерим cb.
- В скрипте игры 20 регистрируем callback на каждого врага.
Клик ЛКМ по NPC (raycast попадает в мешa NPC) → callback → урон.
BabylonScene routeGlobalEvent('click') при ЛКМ — это и есть событие
мыши, но shim ждал только 'mouseButton1Down' (от плеера).
Фикс: мапим p.type === 'click' тоже на mouseButton1Down branch.
Внутри уже фейерим Mouse.Button1Down + UserInputService.InputBegan
с UserInputType.MouseButton1.
Раньше при ЛКМ фейерился только playerMouse.Button1Down.Fire(),
а UserInputService.InputBegan/InputEnded — нет. Lua-скрипт игры 20
ловил клик через InputBegan + UserInputType.MouseButton1 → нет
события → урон не наносился.
Фикс: при mouseButton1Down/Up фейерим InputBegan/InputEnded с
InputObject {UserInputType=Enum.UserInputType.MouseButton1} —
ссылка на тот же объект, что Lua использует в сравнении.
Метки рисовались в куче в (0,0,0) — anchorMesh был сам npc (объект,
не mesh). LabelManager делал plane.parent = anchorMesh, но позиция
не наследовалась.
Фикс: _resolveTweenTarget для 'npc' возвращает data.mesh =
npc.data.rootMesh (реальная модель NPC).
_npcCmd проверял только индекс 'npc:_local_' (для JS-воркера),
ref от Lua-shim 'npc_lua_0' пропускался — отложенные setLabel/damage
терялись.
Также: проверка sb.api?._localToRealNpc была false после рефакторинга
(перенёс в local closure). Замена на sb.api?.setNpcLocalRef — публичный
метод.
Убрал debug-логи.
setLabel сразу после spawnNpc возвращал null от _resolveTweenTarget
(маппинг localRef→realId ещё не записан). Метки молча терялись.
Фикс: если ref начинается с npc_lua_ — откладываем через _npcCmd
(re-вызов после npcSpawned). Иначе retry через 0.3с.
Lua-runtime FATAL: 'Cannot access api before initialization' — я
обращался к api._localToRealNpc на 1852 до const api = на 2027.
Фикс:
- _localToRealNpc объявил как const до api (доступен в closures)
- api.setNpcLocalRef(localRef, realRef) — публичный метод
- GameRuntime: sb.api.setNpcLocalRef?.(ref, 'npc:'+npcId)
- В fireGlobalEvent npcDeath используем _localToRealNpc напрямую
Игра 18:
- Падало на WaitForChild + task.spawn + Completed:Wait (yield-across-C).
- Полный переписан: Heartbeat-таймер раскачивает swing по синусу
с амплитудой 4 и периодом 2.8с (вместо TweenService).
- task.delay 0.2с чтобы дождаться появления свинга в scene.
- Vector3.new напрямую (без оператора + который не работает).
- BindableEvent WinReached + g18_finish.Touched → ev:Fire.
- Полный паритет: showText, win Sound, confetti, respawn при y<-3.
Модалка выхода:
- Заменил window.confirm в KubikonEditor.handleBack на ConfirmModal.
- 3 варианта: Сохранить и выйти / Выйти без сохранения / Отмена.
- ConfirmModal расширен onCancel prop (отделяет 'cancel-кнопка'
от 'клик мимо/Escape').
Раньше: фильтр по Y-капсулы не работал — игрок стоит на островке на
realPos.y=1.54 > порога 1.35, фильтр пропускал.
Стало: проверяем где игрок по X/Z. Если над островком (любой из 6) или
над финишной площадкой — пропускаем урон. Иначе — урон каждый кадр пока
Touched активно (RunService.Heartbeat). У damage есть i-frames ~0.5с
так что урон ~2/сек.
Также: переход с Touched-разово на Heartbeat-постоянно. Раньше Touched
срабатывал только при ВХОДЕ в лаву — если игрок стоит в лаве долго,
TouchEnded не вызывался, и урон шёл только раз. Теперь Touched/TouchEnded
выставляют inLava-флаг, Heartbeat считает урон каждый кадр.
В 3-м лице курсор свободный. _handlePlayClick ВСЕГДА пикал центром
экрана через _pickFromCenter — независимо от того где находится курсор.
Поэтому клик мышкой по мишени не срабатывал — пик шёл из центра.
Фикс: проверяем document.pointerLockElement:
- locked (1-е лицо) → _pickFromCenter (прицел всегда в центре)
- !locked (3-е лицо) → scene.pick(clickX, clickY) по реальной позиции
курсора при клике.
Убрал debug-логи.
Раньше для игр 15-50 при открытии 'Открыть мою копию на Lua' юзер
получал TODO-заглушки которые ничего не делали (или simpleMain
который только print). Каждая новая игра без явного override
была полностью неиграбельной.
Новый generateFallbackLua(s, gameTitle) в buildGameProject:
Главный скрипт (target=null):
- __rbxl_show_text(gameTitle) подсказка
- Слушает BindableEvent FinishReached → win Sound + Победа! + confetti
Скрипт-финиш (target=primitive с именем 'Финиш'/'ФинишЗона'/'Final'):
- Touched → создаёт/находит BindableEvent FinishReached → Fire
- fired-флаг чтобы 1 раз
Прочие target-скрипты:
- Touched → красит примитив зелёным (визуальный feedback)
- touched-флаг
Удалил статичные simpleMain stub-ы для игр 31-50 — теперь они
используют умный fallback. Когда дописываем полную Lua-версию
игры — добавляем явный override в LUA_OVERRIDES, fallback
автоматически перестаёт использоваться.
Это даёт минимум: победа на финише + цвет на касании во всех
35 не-готовых играх (15-50).
Юзер: подсказка в Lua была слева от центра, белая. В JS — точно по
центру внизу, жёлтая.
Корень: я использовал BillboardGui+TextLabel с явной Position, но
GUI-shim позиционирует label некорректно (UDim2 offset плохо
интерпретируется → плашка плыла).
Фикс: используем тот же механизм что JS — game.ui.set (HUD через
React). Добавил хелпер __rbxl_hud_set(id, text, x, y, color, size)
шлющий 'ui.set' cmd, GameRuntime пробрасывает в _onHud → GameHud.jsx
рендерит точно как для JS-скриптов.
В g13_counter/g13_door: при near=true → hud_set с (50, 75, #ffe44a, 20)
(центр по X, ниже центра по Y, жёлтый — точно как JS interact hint).
При выходе из зоны → hud_set(id, nil) убирает.
1) Торговец-NPC отсутствовал. Спавним фигурку из 3 частей:
- Тело (синий куб) на (0, 2.0, 5) — за прилавком
- Голова (бежевая сфера) на (0, 3.2, 5)
- Шляпа (коричневый цилиндр) на (0, 3.8, 5)
2) Подсказки [E] плыли в центр экрана. Задал явную позицию:
нижняя часть, по центру, с тёмной плашкой и тенью.
Применил к g13_counter и g13_door.
JS:
- 6 цветных плиток-цилиндров со своими звуками
- ui.score, ui.showText 'Наступи на все цветные плитки!'
- onMessage 'step' → score++ + при 6 'Иди на финиш'
- onMessage 'finish' → если < TOTAL то 'Сначала пройди все плитки'
иначе showText + win + confetti
- g11_tile_N: onTouch → sound + sparks particles, при первом — broadcast
- g11_finish: onTouch → broadcast 'finish'
Lua (паритет):
- ScreenGui 'Плитки: N / 6'
- BindableEvent EchoStep (плитка) + EchoFinish (зона)
- 6 g11_tile_N: каждая со своим Sound (coin/jump/pickup/click/hit/coin)
+ __rbxl_spawn_particles('sparks', x, y+1, z) при касании
+ Throttle 0.4с между звуками + used-флаг
- g11_main: 'Все плитки звучали! Иди на финиш' при 6
'Сначала пройди все 6 плиток!' если рано пришёл на финиш
При win — Sound 'win' + confetti
GameRuntime обрабатывал prop:'position' и prop:'respawn' через
player.body.position.set() — но PlayerController хранит позицию в
player._pos (не body). Поэтому LoadCharacter молча ничего не делал.
Фикс: ставим player._pos.set(x, y+halfH, z) + сброс _vy.
Fallback на body если _pos нет.
Создан хелпер 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 СРАЗУ — куб создаётся динамическим
и падает.