feat: 50 игр на Lua + импорт Roblox для всех + поддержка Lua в плеере #39
BIN
rbxl-importer/src/__pycache__/converter.cpython-314.pyc
Normal file
BIN
rbxl-importer/src/__pycache__/converter.cpython-314.pyc
Normal file
Binary file not shown.
@ -135,10 +135,21 @@ export class GameRuntime {
|
||||
if (meta && meta.enabled === false) { rbxlSkipped++; continue; }
|
||||
const luaSource = unpackRobloxLuaCode(s.code);
|
||||
if (luaSource && luaSource.trim()) {
|
||||
// Эвристика Tool: если скрипт ссылается на Equipped/Activated
|
||||
// или Tool = script.Parent — он лежит в Tool. Все Tool-скрипты
|
||||
// с target=null склеиваем в ОДИН виртуальный Tool, имя берём
|
||||
// из самого "явного" скрипта (содержит RayGun/Sword/Gun/Weapon).
|
||||
let toolName = null;
|
||||
if (s.target == null && /(script\.Parent|Tool)\.(Equipped|Unequipped|Activated|Deactivated)/.test(luaSource)) {
|
||||
// Все Tool-скрипты группируем в ОДИН виртуальный Tool с именем "Tool".
|
||||
// Для Zapper-демки этого хватит. В будущем — парсинг StarterPack из converter.
|
||||
toolName = 'Tool';
|
||||
}
|
||||
luaUserBatch.push({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
target: s.target,
|
||||
toolName,
|
||||
language: 'lua',
|
||||
code: luaSource,
|
||||
_rbxlImported: true,
|
||||
@ -195,6 +206,11 @@ export class GameRuntime {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[GameRuntime] handleLuaCommand failed:', cmd, payload, e);
|
||||
}
|
||||
} else if (cmd === 'toolRegistered') {
|
||||
// Lua-shim создал Tool — кладём в hotbar инвентаря.
|
||||
try { this._registerRbxlTool(payload); } catch (e) {
|
||||
console.warn('[GameRuntime] toolRegistered failed', e);
|
||||
}
|
||||
} else {
|
||||
this._handleCommand(null, cmd, payload);
|
||||
}
|
||||
@ -204,7 +220,7 @@ export class GameRuntime {
|
||||
const snap = this._buildSceneSnapshot();
|
||||
sb.sendSceneSnapshot(snap);
|
||||
} catch (_) {}
|
||||
for (const s of luaUserBatch) sb.addScript(s.id, s.code, s.target);
|
||||
for (const s of luaUserBatch) sb.addScript(s.id, s.code, s.target, s.name, { toolName: s.toolName });
|
||||
sb.start();
|
||||
this.sandboxes.push(sb);
|
||||
this._luaUserSandbox = sb;
|
||||
@ -524,6 +540,83 @@ export class GameRuntime {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Регистрирует Roblox-Tool в InventoryUI как item в hotbar.
|
||||
* Слушает смену активного слота → шлёт equipTool/unequipTool в Lua-shim.
|
||||
* Слушает клики ЛКМ → шлёт mouseButton1Down (Tool.Activated fires там). */
|
||||
_registerRbxlTool(payload) {
|
||||
if (!payload || payload.index == null) return;
|
||||
const invUI = this.scene3d?.inventory;
|
||||
if (!invUI) return;
|
||||
const itemId = `rbxlTool_${payload.index}`;
|
||||
const toolName = String(payload.name || `Tool ${payload.index}`);
|
||||
invUI.defineItem({
|
||||
id: itemId,
|
||||
name: toolName,
|
||||
emoji: '🔫',
|
||||
rarity: 'uncommon',
|
||||
maxStack: 1,
|
||||
description: `Импортированный Roblox-Tool: ${toolName}`,
|
||||
});
|
||||
// Кладём в конкретный hotbar-слот (index 1..9 → slot 0..8)
|
||||
const slot = Math.max(0, Math.min(8, payload.index - 1));
|
||||
invUI.hotbar[slot] = { itemId, count: 1 };
|
||||
invUI._renderHotbar?.();
|
||||
// На первом Tool — навешиваем слушатели слотов и кликов мыши.
|
||||
if (!this._rbxlToolHooks) {
|
||||
this._rbxlToolHooks = true;
|
||||
this._rbxlActiveSlot = -1;
|
||||
invUI.on('slot', () => {
|
||||
const sl = invUI.active;
|
||||
const item = invUI.hotbar[sl];
|
||||
const sb = this._luaUserSandbox;
|
||||
if (!sb) return;
|
||||
if (item && item.itemId.startsWith('rbxlTool_')) {
|
||||
const idx = +item.itemId.slice('rbxlTool_'.length);
|
||||
sb.sendGlobalEvent?.({ type: 'equipTool', index: idx });
|
||||
this._rbxlActiveSlot = sl;
|
||||
} else if (this._rbxlActiveSlot >= 0) {
|
||||
sb.sendGlobalEvent?.({ type: 'unequipTool' });
|
||||
this._rbxlActiveSlot = -1;
|
||||
}
|
||||
});
|
||||
// Клики мыши при экипированном Tool — Activated/mouseButton1Down
|
||||
try {
|
||||
const canvas = this.scene3d?.engine?.getRenderingCanvas?.();
|
||||
if (canvas) {
|
||||
const sb = this._luaUserSandbox;
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
if (e.button !== 0) return;
|
||||
if (this._rbxlActiveSlot < 0) return;
|
||||
// Hit-position: raycast от камеры в сцену
|
||||
const hit = this._raycastFromCamera?.() || { x: 0, y: 5, z: 0 };
|
||||
sb?.sendGlobalEvent?.({ type: 'mouseButton1Down', hit });
|
||||
sb?.sendGlobalEvent?.({ type: 'toolActivated' });
|
||||
});
|
||||
canvas.addEventListener('mouseup', (e) => {
|
||||
if (e.button !== 0) return;
|
||||
if (this._rbxlActiveSlot < 0) return;
|
||||
sb?.sendGlobalEvent?.({ type: 'mouseButton1Up' });
|
||||
});
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
/** Простой raycast от камеры — для mouse.Hit. */
|
||||
_raycastFromCamera() {
|
||||
try {
|
||||
const cam = this.scene3d?.scene?.activeCamera;
|
||||
if (!cam) return { x: 0, y: 5, z: 0 };
|
||||
const forward = cam.getForwardRay?.()?.direction;
|
||||
const pos = cam.position;
|
||||
if (!pos || !forward) return { x: 0, y: 5, z: 0 };
|
||||
const t = 50;
|
||||
return { x: pos.x + forward.x * t, y: pos.y + forward.y * t, z: pos.z + forward.z * t };
|
||||
} catch (_) {
|
||||
return { x: 0, y: 5, z: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.sandboxes.length > 0) {
|
||||
this._log('info', 'Остановка скриптов');
|
||||
|
||||
@ -43,12 +43,13 @@ export class LuaSharedSandbox {
|
||||
get target() { return null; }
|
||||
tick(_dt, _state) { /* no-op: main-loop запускается изнутри (setInterval) */ }
|
||||
|
||||
addScript(id, code, target, name) {
|
||||
addScript(id, code, target, name, extra) {
|
||||
const entry = {
|
||||
id: String(id || `lua_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`),
|
||||
code: String(code || ''),
|
||||
target: target == null ? null : target,
|
||||
name: name || null,
|
||||
toolName: extra?.toolName || null,
|
||||
};
|
||||
this._scriptsById.set(entry.id, entry);
|
||||
if (!this._isKickedOff) {
|
||||
@ -152,12 +153,26 @@ export class LuaSharedSandbox {
|
||||
// Регистрируем coroutine в __rbxl_coroutines с id для возобновления.
|
||||
// Скрипт оборачиваем в coroutine. task.wait()→coroutine.yield(sec) возвращает
|
||||
// delay из resume → планируем следующий resume через scheduleResume.
|
||||
// Fallback Parent = workspace для скриптов без target (или с невалидным
|
||||
// target). Это спасает массу Roblox-скриптов которые делают
|
||||
// script.Parent.Parent — если бы Parent был nil, упало бы сразу.
|
||||
const parentExpr = primId != null
|
||||
? `(__rbxl_get_part_by_id(${Number(primId)}) or workspace)`
|
||||
: 'workspace';
|
||||
// Fallback Parent: если скрипт связан с Tool (по имени Tool из metadata) —
|
||||
// подсовываем виртуальный Tool как script.Parent. Иначе primitive по id,
|
||||
// иначе workspace.
|
||||
let parentExpr;
|
||||
if (entry.toolName) {
|
||||
// Tool создаётся в shim как Instance.new('Tool'). По имени достаём.
|
||||
// Если не нашли — fallback на новый Tool того же имени.
|
||||
const safeName = JSON.stringify(entry.toolName);
|
||||
parentExpr = `(function()
|
||||
local existing = __rbxl_get_tool_by_name(${safeName})
|
||||
if existing then return existing end
|
||||
local t = Instance.new("Tool")
|
||||
t.Name = ${safeName}
|
||||
return t
|
||||
end)()`;
|
||||
} else if (primId != null) {
|
||||
parentExpr = `(__rbxl_get_part_by_id(${Number(primId)}) or workspace)`;
|
||||
} else {
|
||||
parentExpr = 'workspace';
|
||||
}
|
||||
const wrapped = `
|
||||
do
|
||||
local script = {
|
||||
|
||||
@ -733,9 +733,48 @@ export function registerRobloxShim(lua, opts) {
|
||||
localPlayer.Children.push(playerGui);
|
||||
localPlayer.PlayerGui = playerGui;
|
||||
localPlayer.DisplayName = 'Player';
|
||||
// Backpack — инвентарь, содержит Tools. В Roblox StarterPack автоматически
|
||||
// клонируется в Backpack каждого спавнящегося игрока.
|
||||
const backpack = newInstance('Backpack', 'Backpack');
|
||||
backpack.Parent = localPlayer;
|
||||
localPlayer.Children.push(backpack);
|
||||
localPlayer.Backpack = backpack;
|
||||
// Глобальный Mouse — единственный экземпляр на игрока, привязан к окну
|
||||
// браузера. Реальные Button1Down/Hit фейерятся в GameRuntime.
|
||||
const playerMouse = (function makePlayerMouse() {
|
||||
const m = newInstance('Mouse', 'Mouse');
|
||||
m.Button1Down = makeSignal();
|
||||
m.Button1Up = makeSignal();
|
||||
m.Button2Down = makeSignal();
|
||||
m.Button2Up = makeSignal();
|
||||
m.Move = makeSignal();
|
||||
m.KeyDown = makeSignal();
|
||||
m.KeyUp = makeSignal();
|
||||
m.WheelForward = makeSignal();
|
||||
m.WheelBackward = makeSignal();
|
||||
m.Idle = makeSignal();
|
||||
m.Icon = '';
|
||||
m.X = 0; m.Y = 0;
|
||||
m.ViewSizeX = 1920; m.ViewSizeY = 1080;
|
||||
m.Hit = { Position: new RbxVector3(0, 0, 0), p: new RbxVector3(0, 0, 0),
|
||||
Lookvector: new RbxVector3(0, 0, -1) };
|
||||
m.Origin = { Position: new RbxVector3(0, 5, 0) };
|
||||
m.Target = undefined;
|
||||
m.TargetFilter = undefined;
|
||||
m.TargetSurface = 'Top';
|
||||
return m;
|
||||
})();
|
||||
localPlayer.GetMouse = function () { return playerMouse; };
|
||||
localPlayer.playerMouse = playerMouse;
|
||||
players.Children.push(localPlayer);
|
||||
players.LocalPlayer = localPlayer;
|
||||
|
||||
// === Tool registry ===
|
||||
// Tracks все Tool-инстансы — для UI (hotbar) и equip-flow.
|
||||
// GameRuntime читает API equipTool/unequipTool на main-loop.
|
||||
const allTools = []; // [Tool, ...] в порядке создания (для hotbar 1-9)
|
||||
let equippedTool = null;
|
||||
|
||||
const character = newInstance('Model', 'Player');
|
||||
character.Parent = localPlayer;
|
||||
localPlayer.Children.push(character);
|
||||
@ -1175,8 +1214,6 @@ export function registerRobloxShim(lua, opts) {
|
||||
inst = newGuiInstance(className);
|
||||
guiByLocalRef.set(inst.__guiLocalRef, inst);
|
||||
} else if (className === 'Tool' || className === 'HopperBin') {
|
||||
// Tool — оружие в Roblox. У нас нет инвентаря, поэтому это
|
||||
// просто контейнер с правильными событиями + Handle (заглушка).
|
||||
inst = newInstance(className, 'Tool');
|
||||
inst.Equipped = makeSignal();
|
||||
inst.Unequipped = makeSignal();
|
||||
@ -1191,6 +1228,21 @@ export function registerRobloxShim(lua, opts) {
|
||||
inst.RequiresHandle = true;
|
||||
inst.TextureId = '';
|
||||
inst.ToolTip = '';
|
||||
// Виртуальный Handle — Roblox-скрипты делают Tool.Handle.Position
|
||||
const handle = newInstance('Part', 'Handle');
|
||||
handle.Parent = inst;
|
||||
handle.Position = new RbxVector3(0, 5, 0);
|
||||
handle.Size = new RbxVector3(1, 1, 1);
|
||||
inst.Handle = handle;
|
||||
inst.Children = inst.Children || [];
|
||||
inst.Children.push(handle);
|
||||
// Регистрируем Tool, чтобы плеер показал его в hotbar
|
||||
allTools.push(inst);
|
||||
inst.__toolIndex = allTools.length;
|
||||
send('toolRegistered', {
|
||||
index: inst.__toolIndex,
|
||||
name: inst.Name || `Tool ${inst.__toolIndex}`,
|
||||
});
|
||||
} else if (className === 'IntValue' || className === 'NumberValue'
|
||||
|| className === 'BoolValue' || className === 'StringValue'
|
||||
|| className === 'ObjectValue' || className === 'CFrameValue'
|
||||
@ -1265,6 +1317,7 @@ export function registerRobloxShim(lua, opts) {
|
||||
// === Helpers для скриптов ===
|
||||
const partById = new Map();
|
||||
global.set('__rbxl_get_part_by_id', (id) => partById.get(Number(id)) || undefined);
|
||||
global.set('__rbxl_get_tool_by_name', (name) => allTools.find(t => t.Name === name) || undefined);
|
||||
global.set('__rbxl_send_error', (id, errStr) => {
|
||||
send('log', { level: 'error', text: `[Lua ${id}] ${errStr || 'unknown error'}` });
|
||||
});
|
||||
@ -1452,7 +1505,59 @@ export function registerRobloxShim(lua, opts) {
|
||||
guiEl.MouseButton1Click.Fire();
|
||||
}
|
||||
}
|
||||
// Tool equip/unequip — клавиши 1-9 в плеере шлют
|
||||
// {type:'equipTool', index:N}, {type:'unequipTool'}
|
||||
if (p.type === 'equipTool') {
|
||||
const idx = Number(p.index) - 1;
|
||||
if (idx < 0 || idx >= allTools.length) return;
|
||||
const tool = allTools[idx];
|
||||
if (equippedTool === tool) return;
|
||||
// Снимаем предыдущий
|
||||
if (equippedTool) {
|
||||
try { equippedTool.Unequipped.Fire(); } catch (_) {}
|
||||
}
|
||||
equippedTool = tool;
|
||||
// В Roblox Tool при equip перемещается в Character
|
||||
tool.Parent = character;
|
||||
try { tool.Equipped.Fire(playerMouse); } catch (_) {}
|
||||
}
|
||||
if (p.type === 'unequipTool') {
|
||||
if (!equippedTool) return;
|
||||
try { equippedTool.Unequipped.Fire(); } catch (_) {}
|
||||
equippedTool.Parent = backpack;
|
||||
equippedTool = null;
|
||||
}
|
||||
if (p.type === 'toolActivated') {
|
||||
if (!equippedTool) return;
|
||||
try { equippedTool.Activated.Fire(); } catch (_) {}
|
||||
}
|
||||
if (p.type === 'toolDeactivated') {
|
||||
if (!equippedTool) return;
|
||||
try { equippedTool.Deactivated.Fire(); } catch (_) {}
|
||||
}
|
||||
// Mouse-события из плеера: клики, движение, клавиши при equipped Tool
|
||||
if (p.type === 'mouseButton1Down') {
|
||||
if (p.hit) {
|
||||
playerMouse.Hit.Position = new RbxVector3(p.hit.x, p.hit.y, p.hit.z);
|
||||
playerMouse.Hit.p = playerMouse.Hit.Position;
|
||||
}
|
||||
try { playerMouse.Button1Down.Fire(); } catch (_) {}
|
||||
}
|
||||
if (p.type === 'mouseButton1Up') {
|
||||
try { playerMouse.Button1Up.Fire(); } catch (_) {}
|
||||
}
|
||||
if (p.type === 'keyDown') {
|
||||
try { playerMouse.KeyDown.Fire(String(p.key || '').toLowerCase()); } catch (_) {}
|
||||
}
|
||||
if (p.type === 'keyUp') {
|
||||
try { playerMouse.KeyUp.Fire(String(p.key || '').toLowerCase()); } catch (_) {}
|
||||
}
|
||||
},
|
||||
// Tool registry (для GameRuntime: какой Tool сделать script.Parent)
|
||||
getToolByName(name) {
|
||||
return allTools.find(t => t.Name === name);
|
||||
},
|
||||
getAllTools() { return allTools.slice(); },
|
||||
// Доступ к ключевым объектам (для тестов и отладки)
|
||||
partById, localPlayer, humanoid, character, workspace, players, game,
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user