/** * rbxl-lua-integration.js — вспомогательные функции для импорта .rbxl-карт. * * Старый отдельный Worker-based runtime удалён (2026-06-08): импортированные * Lua-скрипты теперь идут через тот же LuaSharedSandbox что и user-Lua * (см. GameRuntime.start()). Этот файл оставлен только для: * - unpackRobloxLuaCode() — распаковка Lua из JS-комментария-обёртки; * - handleLuaCommand() — обработка partSet/sceneCreate/sceneDelete/playerCmd * команд от Lua-VM в BabylonScene. */ /** Распаковка lua_source из packed-кода. */ export function unpackRobloxLuaCode(code) { const openTag = '/[*] lua_source:\n'.replace('[*]', '*'); const i = code.indexOf(openTag); if (i < 0) return null; const start = i + openTag.length; const closeIdx = code.lastIndexOf('\n*' + '/'); if (closeIdx < start) return null; return code.slice(start, closeIdx); } /** Парсит JSON-метадату из 2-й строки packed-кода (`// {"roblox_class":..., "enabled": true}`). */ export function parseRobloxLuaMeta(code) { if (typeof code !== 'string') return null; const lines = code.split('\n'); if (lines.length < 2) return null; const metaLine = lines[1]; if (!metaLine.startsWith('// ')) return null; try { return JSON.parse(metaLine.slice(3)); } catch (_) { return null; } } /** Сцена → snap для shim'а (workspace:GetChildren). */ export function buildLuaSceneSnap(primitives) { const out = { primitives: {} }; if (!Array.isArray(primitives)) return out; for (const p of primitives) { out.primitives[p.id] = { id: p.id, type: p.type, name: p.name, x: p.x, y: p.y, z: p.z, sx: p.sx, sy: p.sy, sz: p.sz, color: p.color, material: p.material, anchored: !!p.anchored, canCollide: p.canCollide !== false, opacity: typeof p.opacity === 'number' ? p.opacity : 1, }; } return out; } /** * GUI-tree для shim'а. Mapping origin → __roblox_class. * scene.gui — массив элементов с {id, type, name, parentId, ...origin}. * Возвращаем массив сохраняя порядок parent → child (важно для tree-сборки). */ export function buildLuaGuiTree(guiElements) { if (!Array.isArray(guiElements)) return []; const out = []; for (const el of guiElements) { // origin = 'roblox-textbutton' → 'TextButton' let rblClass = 'Frame'; const origin = el.origin || ''; if (origin.startsWith('roblox-')) { const tail = origin.slice(7); rblClass = tail.charAt(0).toUpperCase() + tail.slice(1); // Camel-case "textbutton" → "TextButton" if (rblClass.toLowerCase() === 'textbutton') rblClass = 'TextButton'; else if (rblClass.toLowerCase() === 'textlabel') rblClass = 'TextLabel'; else if (rblClass.toLowerCase() === 'imagebutton') rblClass = 'ImageButton'; else if (rblClass.toLowerCase() === 'imagelabel') rblClass = 'ImageLabel'; else if (rblClass.toLowerCase() === 'textbox') rblClass = 'TextBox'; else if (rblClass.toLowerCase() === 'scrollingframe') rblClass = 'ScrollingFrame'; else if (rblClass.toLowerCase() === 'frame') rblClass = 'Frame'; } else { // Если origin не задан — гадаем по type const t = el.type; if (t === 'button') rblClass = 'TextButton'; else if (t === 'text') rblClass = 'TextLabel'; else if (t === 'image') rblClass = 'ImageLabel'; else if (t === 'textbox') rblClass = 'TextBox'; } out.push({ id: el.id, name: el.name || rblClass, parentId: el.parentId || null, visible: el.visible !== false, text: el.text || '', __roblox_class: rblClass, }); } return out; } /** * Обработка IPC команд от worker'а — мапим на действия в Babylon-сцене. */ export function handleLuaCommand(_scriptId, cmd, payload, runtime) { if (cmd === 'log') { const fn = payload?.level === 'error' ? console.error : payload?.level === 'warn' ? console.warn : console.log; fn('[rbxl-lua]', payload?.text || ''); return; } if (cmd === 'partSet') { const pm = runtime.scene3d?.primitiveManager; if (!pm) { console.warn('[partSet] no primitiveManager. scene3d=', !!runtime.scene3d); return; } const primId = payload?.primId; const prop = payload?.prop; const value = payload?.value; const patch = {}; if (prop === 'position' && value) { patch.x = value.x; patch.y = value.y; patch.z = value.z; } else if (prop === 'cframe' && value) { patch.x = value.x; patch.y = value.y; patch.z = value.z; patch.rotationX = value.rx; patch.rotationY = value.ry; patch.rotationZ = value.rz; } else if (prop === 'size' && value) { patch.sx = value.sx; patch.sy = value.sy; patch.sz = value.sz; } else if (prop === 'color') patch.color = value; else if (prop === 'material') patch.material = value; else if (prop === 'anchored') patch.anchored = value; else if (prop === 'canCollide') patch.canCollide = value; else if (prop === 'opacity') patch.opacity = value; try { if (typeof pm.updateInstance === 'function') pm.updateInstance(primId, patch); else if (typeof pm.applyPatch === 'function') pm.applyPatch(primId, patch); else if (typeof pm.update === 'function') pm.update(primId, patch); } catch (e) { console.error('[partSet] updateInstance failed:', e); } return; } if (cmd === 'sceneCreate') { // Lua: Instance.new("Part") + part.Parent = workspace → создание примитива. // payload: { primId (предложенный id), type, x, y, z, sx, sy, sz, color, anchored } try { const pm = runtime.scene3d?.primitiveManager; if (!pm || typeof pm.addInstance !== 'function') return; const opts = { id: payload?.primId, x: payload?.x || 0, y: payload?.y || 0, z: payload?.z || 0, sx: payload?.sx || 1, sy: payload?.sy || 1, sz: payload?.sz || 1, color: payload?.color, anchored: payload?.anchored !== false, canCollide: payload?.canCollide !== false, }; pm.addInstance(payload?.type || 'cube', opts); // Если unanchored — регистрируем в физике на лету, иначе он не падает. if (opts.anchored === false) { try { const dm = runtime.scene3d?.dynamics; const data = pm.instances?.get?.(opts.id); if (dm && data && typeof dm.registerPrimitive === 'function') { dm.registerPrimitive(data); } } catch (e) { console.warn('[sceneCreate] registerPrimitive failed', e); } } } catch (e) { console.error('[sceneCreate]', e); } return; } if (cmd === 'sceneDelete') { // Lua: part:Destroy() → удаление примитива. try { const pm = runtime.scene3d?.primitiveManager; if (!pm || typeof pm.removeInstance !== 'function') return; const id = payload?.primId; if (id != null) pm.removeInstance(Number(id)); } catch (e) { console.error('[sceneDelete]', e); } return; } if (cmd === 'partVel') { try { const pm = runtime.scene3d?.primitiveManager; if (pm && typeof pm.setVelocity === 'function') { pm.setVelocity(payload.primId, payload.vx, payload.vy, payload.vz); } } catch (e) {} return; } if (cmd === 'playerCmd') { try { const p = runtime.game?.player; if (!p) return; const method = payload?.method; const args = payload?.args || []; if (method === 'teleport') p.teleport && p.teleport(args[0], args[1], args[2]); else if (method === 'setWalkSpeed') p.setWalkSpeed && p.setWalkSpeed(args[0]); else if (method === 'setJumpPower') p.setJumpPower && p.setJumpPower(args[0]); else if (method === 'setHealth') p.setHealth && p.setHealth(args[0]); else if (method === 'die') p.die && p.die(); else if (method === 'damage' || method === 'takeDamage') p.damage && p.damage(args[0]); } catch (e) {} return; } if (cmd === 'guiUpdate') { // TODO: scripts setting Visible/Text/Color on GUI → передать в GuiManager return; } }