211 lines
9.1 KiB
JavaScript
211 lines
9.1 KiB
JavaScript
/**
|
||
* 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;
|
||
}
|
||
}
|