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 нет.
Куб с физикой отталкивается от игрока (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 — чтобы было видно
сразу что именно запущено.
Проблема: 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.
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.
Не передаём 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.
ИНФРАСТРУКТУРА:
- 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), будут доработаны итеративно.
Roblox-конвенция: Players.PlayerAdded не срабатывает для игроков уже
на сервере к моменту подключения хендлера. Юзер пишет:
Players.PlayerAdded:Connect(function(p) print(p.Name) end)
и удивляется почему лог пустой — игрок-то уже есть.
В реальном Roblox делают:
for _, p in ipairs(Players:GetPlayers()) do print(p.Name) end
Players.PlayerAdded:Connect(...)
Но мало кто помнит про этот workaround.
Решение: после kickoff всех скриптов (когда все Connect'ы установлены)
из LuaSharedSandbox шлём через api.fireExistingPlayers() →
PlayerAdded.Fire(localPlayer) + CharacterAdded.Fire(character).
Также:
- Добавлены localPlayer.CharacterAdded/CharacterRemoving/AppearanceLoaded
signals (раньше не было).
- Шаблон LUA_TEMPLATE_GLOBAL обновлён: для всех Players:GetPlayers()
делаем print, плюс PlayerAdded:Connect для будущих. Юзер видит
результат сразу при первом Запустить.
- Шаблон LUA_TEMPLATE_PART сразу пишет 'Скрипт детали X запущен'.
Раньше при смене языка код оставался как есть → подсветка кричит
ошибками, юзер пишет JS в Lua-режиме и наоборот.
Сейчас:
- Скрипт хранит code_js и code_lua отдельно (плюс активный code).
- При переключении JS↔Lua: текущий code сохраняется в слот ТЕКУЩЕГО
языка, достаётся слот ЦЕЛЕВОГО языка. Если слот пустой — шаблон.
- При обычном save (печатает юзер) код зеркалится в слот активного
языка чтобы не потерять при swap.
- Никаких модалок: переключение мгновенное, ничего не пропадает.
Сценарии:
- Новый проект: js-шаблон в code_js. Клик Lua → подставляется lua-шаблон,
code_js сохранён.
- Юзер написал JS-код, переключился на Lua: JS улетел в code_js,
показывается пустой Lua-шаблон. Клик обратно JS → код возвращается.
- Старые проекты (без code_js/code_lua): первое переключение
засеет слот текущего языка из code.
При переключении JS↔Lua показывалась модалка "Сменить язык?" даже
если код не пустой. Юзер: не нужна, переключай сразу.
Сейчас: код остаётся как есть, меняется только подсветка синтаксиса
Monaco. Если код был пустой шаблон — подставляется новый шаблон языка.
Скрипты Roblox 2009 содержат паттерн:
while not parent:FindFirstChild(name) do
parent.ChildAdded:wait()
end
Наш sig.Wait() возвращает -1 синхронно (без yield), цикл крутится
бесконечно без шанса coroutine.yield. debug.sethook не помогает
если код находится в C-call boundary к моменту срабатывания.
Решение: regex-фильтр в GameRuntime.js перед добавлением в batch.
Скрипты с такими паттернами не запускаются — пишется warn в консоль.
В ROBLOX Battle это ~10-15 скриптов: RoundScript, Spawner,
ReEquipLastWeapon, LeaderboardV3, Leaderstats и др. Карта потеряет
эту функциональность (раунды, респавн), но играется.
Прошлый коммит pcall(coroutine.yield) дал бесконечный цикл:
yield внутри C-call падал → pcall ловил → hook возвращался → счётчик
не сбросился → срабатывал опять моментально → вис.
Новая стратегия:
1. Голый coroutine.yield в watchdog: если внутри C-call упадёт с
ошибкой — pcall(fn,...) внутри coroutine её поймает, скрипт
завершится. Лучше чем вис.
2. Frequency 100k→20k инструкций — yield чаще, меньше времени на
tight-loop перед уступкой управления UI.
3. Batch kickoff 20→5 скриптов с delay 20мс (было 0). 55 скриптов
ROBLOX Battle = ~200мс распределено, UI отзывается.
Page-hang при init должен исчезнуть. Скрипты с tight-loop типа
WaitForChild через ChildAdded:wait() упадут с ошибкой про yield,
но не повесят страницу.
Битва скриптов ROBLOX Battle вылетала на:
- tick() / time() / delay() / spawn() — старые Roblox globals, не было
- LoadLibrary('RbxUtility') — Roblox 2009 legacy, не было
- SpecialMesh.MeshType — класс не реализован, доступ к полю крашил
- attempt to yield across C-call boundary — debug.sethook yield без pcall
Фиксы:
1. Lua-prelude: tick=os.time, time=os.clock*1000, delay/spawn через
coroutine, LoadLibrary возвращает proxy-стаб через metatable.
2. Instance.new('SpecialMesh'/'BlockMesh'/'CylinderMesh'/'FileMesh')
стабы с MeshType/MeshId/Scale полями.
3. debug.sethook: pcall(coroutine.yield, ...) вместо голого yield —
если внутри C-call, ошибка молча проглатывается, hook сработает
позже когда Lua вернётся из C. Frequency 50k→100k.
4. script.Parent в Lua-обёртке: setmetatable __index → workspace
fallback для script.Foo:Bar() паттернов. Гарантия что
_scriptParent.Parent ~= nil.
ROBLOX Battle должна показать меньше errors на этом запуске.
8. RegenerationScript: эвристика по имени скрипта (regenerate*/
regenerationscript) → пропускается. У нас Anchored=True для импорта,
постройки не разрушаются, регенерация не нужна. Их работа дала бы
визуальные глитчи (model:remove + Clone каждые 2 мин).
9. BattleArmor: Humanoid.MaxHealth/Health/WalkSpeed/JumpPower теперь
реактивные (Object.defineProperty). При смене .MaxHealth=N шлёт
playerSet → player.maxHp обновляется → HUD HP-бар. BattleArmor
touch'нул → Humanoid.MaxHealth=20, Health=20 → игрок видит броню.
10. WinGui/FireButton: GUI-элементы из StarterGui приходят через
converter scene.gui[] и рендерятся стандартно. Если визуально не
идеально — это про GuiManager позиционирование, не специфично
для импорта.
11. AdminConsole: no-op, скрипт-заглушка, ничего не делает.
13. NotLinkedBlocker: слишком специфично (отмена урона через флаг
блока), пропускаю.
ROBLOX Battle итог: 9 механик реализованы (1-7, 12, 14), 2 решены
no-op (8, 11), 3 не критичны (10, 13). Карта должна играться.
Реализовано из 14 механик:
1. Teams (game.Teams, Player.Team, TeamColor): scene.teams[] из конвертера,
эвристика TeamBeacon-Model → автоматически создаются 4 команды.
В shim создаются Team-инстансы при snapshot, авто-эквип игрока в первую.
2. Leaderstats UI: IntValue.Value реактивно шлёт leaderstatSet → существующий
LeaderstatsManager (define + set). HUD автоматически рисуется в правом
верхнем по родительскому Name='leaderstats'.
3. BindableFunction + RemoteFunction + Message/Hint класс. Message с
реактивным .Text и .Parent шлёт hudMessage в наш RbxlHudOverlay.
4. KillFeed UI + creator-tag tracking. RbxlHudOverlay.addKillFeed() рисует
А → [weapon] → Б в правом верхнем. Humanoid.TakeDamage при Health=0
ищет creator-ObjectValue и шлёт killFeed. Авто-respawn через 2с.
5. SpawnLocation.TeamColor → scene.team_spawns[] для будущей логики
команд-спавна.
6. Tool:Clone() / Model:Clone() / :clone(): поверхностный клон + lowercase
alias. Также :MakeJoints/:BreakJoints/:Remove/:remove no-op методы.
7. Creator-tag handling в TakeDamage (см. пункт 4).
12. Bouncer/батут: BodyVelocity с +Y и Parent=Torso/HumanoidRootPart →
эвристика "толкаем вверх" → playerSet jumpVelocity → реальный jump
через player._vy.
14. Mouse.Icon → CSS cursor на canvas (crosshair для не-пустых).
Также:
- RbxlHudOverlay.js — новый модуль DOM-оверлей для HUD-элементов
(KillFeed/Message/WinGui). Lazy-создаётся при первом hudMessage/killFeed.
- BabylonScene.serialize включает scene.teams и scene.team_spawns.
- Converter: scene = teams[] + team_spawns[]. TeamBeacon Model'и → команды.
- Deploy converter.py на VM 130.
Остались: 8 Regeneration, 9 BattleArmor, 10 WinGui/FireButton кастомное
позиционирование, 11 AdminConsole (no-op уже ok), 13 NotLinkedBlocker.
1. CFrame.to_euler_xyz переписан под Babylon YXZ convention:
rx = asin(-r12), ry = atan2(r02, r22), rz = atan2(r10, r11).
Раньше извлекал XYZ-Euler → Babylon применял как YXZ → клины,
мостики, наклонные постройки рендерились повёрнутые
(примеры из ROBLOX Battle: мостик торчал в стену).
Учтён gimbal-lock на X=±90°.
2. Lua watchdog в _startSingleScript и __rbxl_drain_handler:
debug.sethook(yield_50ms, '', 50000) — каждые 50k Lua-инструкций
принудительно yield 1 кадр. Защищает от:
while not workspace:FindFirstChild('X') do
workspace.ChildAdded:wait()
end
где наш stub :wait() возвращает -1 мгновенно — раньше скрипт
подвешивал вкладку (50k+ итераций в секунду). Сейчас yield'ит,
tickScheduler возобновляет.
3. Signal.Wait возвращает -1 как 'no-arg yield marker'. Сейчас
не используется в Lua, но если позже сделаем wrapper — будет.
ROBLOX Battle карта (arch1_ROBLOX_Battle_v2.rbxl, 1677 примитивов,
66 скриптов) — теперь не должна подвешивать.
Деплой rbxl_types.py на VM 130.
Слайдеры sun/hemi/ambient/exposure/contrast/saturation теперь
сохраняются в projectData.scene.lighting при save и применяются
обратно при load.
Раньше параметры жили только в текущей сессии — после refresh
страницы возвращались к дефолтам.
Импортированные .rbxl карты также сохраняют выставленные пользователем
параметры света.
1. PrimitiveManager: mat.ambientColor=(1,1,1). Теперь scene.ambientColor
("Заливка теней" слайдер) реально влияет на тени. Юзер крутит
значение и видит изменение.
2. converter.py: Roblox-Part импортируется всегда с Anchored=True
(force-anchored). Welds у нас заглушки, без них unanchored Part'ы
рассыпаются физикой. Если юзеру нужно падающее — снимет в
инспекторе вручную.
Деплой converter.py на VM 130 + systemctl restart.
В Свет и атмосфера добавлено:
- Заливка теней (scene.ambientColor) — позволяет окрасить тени в
сером тоне без пересвета diffuse материалов.
- Экспозиция (ipc.exposure 0.3-2) — общая яркость через
imageProcessingConfiguration.
- Контраст (ipc.contrast 0.5-2)
- Насыщенность (colorCurves.globalSaturation -100..+100)
Юзер крутит слайдеры до момента когда импортированная Roblox-карта
выглядит как оригинал. Дефолты: ambient 0.3, exposure 1.0, contrast
1.0, saturation 1.0.
Также убрал mat.ambientColor=цвет — теперь default (0,0,0). Освещение
управляется глобально через панель.
Состояние пока не сохраняется в проект (только сессия). Persistence
добавим в следующем шаге.
ambient=diffuse (100%) суммировался с прямым светом и
давал пересвет (особенно на дорогах/полу с #cccccc).
40% — баланс: тень окрашена (видна как цвет, не чёрная),
прямой свет = чистый diffuse без пересвета.
Прошлая итерация без ambient давала почти-чёрные грани в тени —
sun под углом + hemi только вверх = низ/бок граней получают только
скудный hemi.groundColor=(0.3,0.3,0.4) = тёмные пятна.
Roblox-look: тень это просто менее яркий вариант цвета (не чёрный).
Фикс: mat.ambientColor = mat.diffuseColor (= цвет примитива).
scene.ambientColor=(0.3) × ambient(цвет) = 30% цвета в тени.
На прямом свете diffuse доминирует — белые остаются белыми,
зелёные зелёными.
Это даёт тени окрашенные (как в Roblox), сохраняя контраст со
светом и точность цвета.
Главная причина пересвета:
1. BrickColor 151 (Earth green = трава Crossroads) ОТСУТСТВОВАЛ
в таблице. Пол получал дефолт #cccccc и выглядел белым.
После анализа карты 344 примитива использовали дефолт.
2. mat.ambientColor=(1,1,1) + scene.ambientColor=(0.3) делало белые
цвета пересветлёнными — серый выглядел белым.
Фикс:
- BRICKCOLOR_TO_HEX расширен с ~50 до ~120 цветов. Добавлены:
151 (Earth green), 26 темный, 18, 115-148, 168-301, 1021-1032 и др.
После: #cccccc дефолт 344→68 (бОльшая часть теперь правильных).
- Убран mat.ambientColor — оставлен default (0,0,0). Lambert чистый:
освещённая грань = diffuse, тень = почти чёрная (scene.ambient смягчает).
Цвета теперь точно как в diffuse, без пересвета.
Деплой: converter.py скопирован на VM 130 + systemctl restart.