feat(rbxl): XML-������ ������ .rbxl + Day/Night + Tool/Mouse/Backpack flow #38
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; }
|
if (meta && meta.enabled === false) { rbxlSkipped++; continue; }
|
||||||
const luaSource = unpackRobloxLuaCode(s.code);
|
const luaSource = unpackRobloxLuaCode(s.code);
|
||||||
if (luaSource && luaSource.trim()) {
|
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({
|
luaUserBatch.push({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
target: s.target,
|
target: s.target,
|
||||||
|
toolName,
|
||||||
language: 'lua',
|
language: 'lua',
|
||||||
code: luaSource,
|
code: luaSource,
|
||||||
_rbxlImported: true,
|
_rbxlImported: true,
|
||||||
@ -195,6 +206,11 @@ export class GameRuntime {
|
|||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('[GameRuntime] handleLuaCommand failed:', cmd, payload, e);
|
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 {
|
} else {
|
||||||
this._handleCommand(null, cmd, payload);
|
this._handleCommand(null, cmd, payload);
|
||||||
}
|
}
|
||||||
@ -204,7 +220,7 @@ export class GameRuntime {
|
|||||||
const snap = this._buildSceneSnapshot();
|
const snap = this._buildSceneSnapshot();
|
||||||
sb.sendSceneSnapshot(snap);
|
sb.sendSceneSnapshot(snap);
|
||||||
} catch (_) {}
|
} 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();
|
sb.start();
|
||||||
this.sandboxes.push(sb);
|
this.sandboxes.push(sb);
|
||||||
this._luaUserSandbox = sb;
|
this._luaUserSandbox = sb;
|
||||||
@ -524,6 +540,83 @@ export class GameRuntime {
|
|||||||
return null;
|
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() {
|
stop() {
|
||||||
if (this.sandboxes.length > 0) {
|
if (this.sandboxes.length > 0) {
|
||||||
this._log('info', 'Остановка скриптов');
|
this._log('info', 'Остановка скриптов');
|
||||||
|
|||||||
@ -43,12 +43,13 @@ export class LuaSharedSandbox {
|
|||||||
get target() { return null; }
|
get target() { return null; }
|
||||||
tick(_dt, _state) { /* no-op: main-loop запускается изнутри (setInterval) */ }
|
tick(_dt, _state) { /* no-op: main-loop запускается изнутри (setInterval) */ }
|
||||||
|
|
||||||
addScript(id, code, target, name) {
|
addScript(id, code, target, name, extra) {
|
||||||
const entry = {
|
const entry = {
|
||||||
id: String(id || `lua_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`),
|
id: String(id || `lua_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`),
|
||||||
code: String(code || ''),
|
code: String(code || ''),
|
||||||
target: target == null ? null : target,
|
target: target == null ? null : target,
|
||||||
name: name || null,
|
name: name || null,
|
||||||
|
toolName: extra?.toolName || null,
|
||||||
};
|
};
|
||||||
this._scriptsById.set(entry.id, entry);
|
this._scriptsById.set(entry.id, entry);
|
||||||
if (!this._isKickedOff) {
|
if (!this._isKickedOff) {
|
||||||
@ -152,12 +153,26 @@ export class LuaSharedSandbox {
|
|||||||
// Регистрируем coroutine в __rbxl_coroutines с id для возобновления.
|
// Регистрируем coroutine в __rbxl_coroutines с id для возобновления.
|
||||||
// Скрипт оборачиваем в coroutine. task.wait()→coroutine.yield(sec) возвращает
|
// Скрипт оборачиваем в coroutine. task.wait()→coroutine.yield(sec) возвращает
|
||||||
// delay из resume → планируем следующий resume через scheduleResume.
|
// delay из resume → планируем следующий resume через scheduleResume.
|
||||||
// Fallback Parent = workspace для скриптов без target (или с невалидным
|
// Fallback Parent: если скрипт связан с Tool (по имени Tool из metadata) —
|
||||||
// target). Это спасает массу Roblox-скриптов которые делают
|
// подсовываем виртуальный Tool как script.Parent. Иначе primitive по id,
|
||||||
// script.Parent.Parent — если бы Parent был nil, упало бы сразу.
|
// иначе workspace.
|
||||||
const parentExpr = primId != null
|
let parentExpr;
|
||||||
? `(__rbxl_get_part_by_id(${Number(primId)}) or workspace)`
|
if (entry.toolName) {
|
||||||
: 'workspace';
|
// 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 = `
|
const wrapped = `
|
||||||
do
|
do
|
||||||
local script = {
|
local script = {
|
||||||
|
|||||||
@ -733,9 +733,48 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
localPlayer.Children.push(playerGui);
|
localPlayer.Children.push(playerGui);
|
||||||
localPlayer.PlayerGui = playerGui;
|
localPlayer.PlayerGui = playerGui;
|
||||||
localPlayer.DisplayName = 'Player';
|
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.Children.push(localPlayer);
|
||||||
players.LocalPlayer = 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');
|
const character = newInstance('Model', 'Player');
|
||||||
character.Parent = localPlayer;
|
character.Parent = localPlayer;
|
||||||
localPlayer.Children.push(character);
|
localPlayer.Children.push(character);
|
||||||
@ -1175,8 +1214,6 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
inst = newGuiInstance(className);
|
inst = newGuiInstance(className);
|
||||||
guiByLocalRef.set(inst.__guiLocalRef, inst);
|
guiByLocalRef.set(inst.__guiLocalRef, inst);
|
||||||
} else if (className === 'Tool' || className === 'HopperBin') {
|
} else if (className === 'Tool' || className === 'HopperBin') {
|
||||||
// Tool — оружие в Roblox. У нас нет инвентаря, поэтому это
|
|
||||||
// просто контейнер с правильными событиями + Handle (заглушка).
|
|
||||||
inst = newInstance(className, 'Tool');
|
inst = newInstance(className, 'Tool');
|
||||||
inst.Equipped = makeSignal();
|
inst.Equipped = makeSignal();
|
||||||
inst.Unequipped = makeSignal();
|
inst.Unequipped = makeSignal();
|
||||||
@ -1191,6 +1228,21 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
inst.RequiresHandle = true;
|
inst.RequiresHandle = true;
|
||||||
inst.TextureId = '';
|
inst.TextureId = '';
|
||||||
inst.ToolTip = '';
|
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'
|
} else if (className === 'IntValue' || className === 'NumberValue'
|
||||||
|| className === 'BoolValue' || className === 'StringValue'
|
|| className === 'BoolValue' || className === 'StringValue'
|
||||||
|| className === 'ObjectValue' || className === 'CFrameValue'
|
|| className === 'ObjectValue' || className === 'CFrameValue'
|
||||||
@ -1265,6 +1317,7 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
// === Helpers для скриптов ===
|
// === Helpers для скриптов ===
|
||||||
const partById = new Map();
|
const partById = new Map();
|
||||||
global.set('__rbxl_get_part_by_id', (id) => partById.get(Number(id)) || undefined);
|
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) => {
|
global.set('__rbxl_send_error', (id, errStr) => {
|
||||||
send('log', { level: 'error', text: `[Lua ${id}] ${errStr || 'unknown error'}` });
|
send('log', { level: 'error', text: `[Lua ${id}] ${errStr || 'unknown error'}` });
|
||||||
});
|
});
|
||||||
@ -1452,7 +1505,59 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
guiEl.MouseButton1Click.Fire();
|
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,
|
partById, localPlayer, humanoid, character, workspace, players, game,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user