diff --git a/src/editor/engine/lua/RobloxShim.js b/src/editor/engine/lua/RobloxShim.js index 6f4d276..00c491e 100644 --- a/src/editor/engine/lua/RobloxShim.js +++ b/src/editor/engine/lua/RobloxShim.js @@ -480,6 +480,12 @@ export function registerRobloxShim(lua, opts) { const localPlayer = newInstance('Player', 'Player'); localPlayer.Parent = players; localPlayer.UserId = 1; + // PlayerGui — контейнер для GUI принадлежащих игроку. В Rublox это no-op + // (overlay глобальный), но Roblox-скрипты часто делают gui.Parent = playerGui. + const playerGui = newInstance('PlayerGui', 'PlayerGui'); + playerGui.Parent = localPlayer; + localPlayer.Children.push(playerGui); + localPlayer.PlayerGui = playerGui; localPlayer.DisplayName = 'Player'; players.Children.push(localPlayer); players.LocalPlayer = localPlayer; @@ -643,6 +649,136 @@ export function registerRobloxShim(lua, opts) { global.set('Workspace', workspace); // === Instance.new === + // === Helper: создание GUI-элемента через game.gui.create === + // Roblox: Frame/TextLabel/TextButton/ImageLabel/TextBox/ScrollingFrame. + // Шлём gui.create команду в main thread → GuiManager создаёт элемент. + // Возвращаем Lua-объект с setter'ами для основных свойств. + let _nextGuiLocalRef = 0; + function newGuiInstance(robloxClass) { + const localRef = `_gui_lua_${_nextGuiLocalRef++}`; + const inst = newInstance(robloxClass, robloxClass); + inst.__guiLocalRef = localRef; + inst.__guiClass = robloxClass; + // Маппим Roblox-класс на тип в GuiManager + const guiType = ({ + Frame: 'frame', + TextLabel: 'text', + TextButton: 'button', + ImageLabel: 'image', + ImageButton: 'button', + TextBox: 'textbox', + ScrollingFrame: 'scroll', + })[robloxClass] || 'frame'; + // Внутренние стейты + inst._gui = { + type: guiType, + text: '', + bgColor: '#3a2820', + bgOpacity: 1, + textColor: '#f0e6d8', + textSize: 16, + x: 50, y: 50, w: 20, h: 10, + visible: true, + }; + // Шлём create при первом обращении (lazy) или сейчас — лучше сейчас, чтобы + // не было гонок при моментальной правке свойств после Instance.new. + send('gui.create', { + type: guiType, + opts: { ...inst._gui, _scriptCreated: true }, + localRef, + }); + // Сигналы (для кнопок) + if (robloxClass === 'TextButton' || robloxClass === 'ImageButton') { + inst.MouseButton1Click = makeSignal(); + inst.MouseEnter = makeSignal(); + inst.MouseLeave = makeSignal(); + inst.Activated = inst.MouseButton1Click; + } + // Setters + const updateField = (field, value) => { + inst._gui[field] = value; + send('gui.update', { id: localRef, patch: { [field]: value } }); + }; + Object.defineProperty(inst, 'Text', { + get() { return inst._gui.text; }, + set(v) { updateField('text', String(v ?? '')); }, + enumerable: true, + }); + Object.defineProperty(inst, 'Visible', { + get() { return inst._gui.visible; }, + set(v) { updateField('visible', !!v); }, + enumerable: true, + }); + Object.defineProperty(inst, 'BackgroundColor3', { + get() { return RbxColor3.fromHex(inst._gui.bgColor); }, + set(v) { + if (!v) return; + const hex = v.toHex ? v.toHex() : (v instanceof RbxColor3 ? v.toHex() : '#3a2820'); + updateField('bgColor', hex); + }, + enumerable: true, + }); + Object.defineProperty(inst, 'BackgroundTransparency', { + get() { return 1 - (inst._gui.bgOpacity ?? 1); }, + set(v) { updateField('bgOpacity', 1 - Math.max(0, Math.min(1, +v || 0))); }, + enumerable: true, + }); + Object.defineProperty(inst, 'TextColor3', { + get() { return RbxColor3.fromHex(inst._gui.textColor); }, + set(v) { + if (!v) return; + const hex = v.toHex ? v.toHex() : '#f0e6d8'; + updateField('textColor', hex); + }, + enumerable: true, + }); + Object.defineProperty(inst, 'TextSize', { + get() { return inst._gui.textSize; }, + set(v) { updateField('textSize', Math.max(8, Math.min(72, +v || 16))); }, + enumerable: true, + }); + // Position: UDim2 → x,y проценты (Roblox-style: scale=%, offset=px) + // Упрощённо берём scale*100 как x/y; offset игнорируем. + Object.defineProperty(inst, 'Position', { + get() { + return new RbxUDim2(inst._gui.x / 100, 0, inst._gui.y / 100, 0); + }, + set(v) { + if (!v) return; + const xPct = (v.X?.Scale || 0) * 100 + (v.X?.Offset || 0) / 10; + const yPct = (v.Y?.Scale || 0) * 100 + (v.Y?.Offset || 0) / 10; + inst._gui.x = xPct; + inst._gui.y = yPct; + send('gui.update', { id: localRef, patch: { x: xPct, y: yPct } }); + }, + enumerable: true, + }); + Object.defineProperty(inst, 'Size', { + get() { + return new RbxUDim2(inst._gui.w / 100, 0, inst._gui.h / 100, 0); + }, + set(v) { + if (!v) return; + const wPct = (v.X?.Scale || 0) * 100 + (v.X?.Offset || 0) / 10; + const hPct = (v.Y?.Scale || 0) * 100 + (v.Y?.Offset || 0) / 10; + inst._gui.w = wPct; + inst._gui.h = hPct; + send('gui.update', { id: localRef, patch: { w: wPct, h: hPct } }); + }, + enumerable: true, + }); + // Destroy — удаление GUI + const origDestroy = inst.Destroy; + inst.Destroy = function () { + try { send('gui.remove', { id: localRef }); } catch (_) {} + origDestroy.call(inst); + }; + return inst; + } + + // Регистрация в guiByLocalRef для дальнейшей маршрутизации событий клика + const guiByLocalRef = new Map(); + // Счётчик для новых Part'ов, создаваемых через Instance.new("Part"): // primitiveManager.addInstance даст уникальный id, мы используем временный // отрицательный id пока не пришёл sceneCreate ACK. Но проще: даём свой @@ -693,6 +829,18 @@ export function registerRobloxShim(lua, opts) { inst.Health = 100; inst.MaxHealth = 100; inst.Died = makeSignal(); inst.HealthChanged = makeSignal(); inst.TakeDamage = function (n) { this.Health = Math.max(0, this.Health - (n || 0)); }; + } else if (className === 'ScreenGui') { + // ScreenGui — логический корень GUI. В Rublox overlay глобальный, + // поэтому ScreenGui это просто контейнер-no-op (без gui.create). + inst = newInstance('ScreenGui', 'ScreenGui'); + inst.__isScreenGui = true; + inst.Enabled = true; + } else if (className === 'Frame' || className === 'TextLabel' + || className === 'TextButton' || className === 'ImageLabel' + || className === 'ImageButton' || className === 'TextBox' + || className === 'ScrollingFrame') { + inst = newGuiInstance(className); + guiByLocalRef.set(inst.__guiLocalRef, inst); } else { inst = newInstance(className, className); } @@ -885,6 +1033,14 @@ export function registerRobloxShim(lua, opts) { if (humanoid.Touched) humanoid.Touched.Fire(part); } } + // GUI-клик: GameRuntime шлёт {type:'guiClick', localId, id} + if (p.type === 'guiClick') { + const ref = p.localId || p.id; + const guiEl = guiByLocalRef.get(ref); + if (guiEl?.MouseButton1Click) { + guiEl.MouseButton1Click.Fire(); + } + } }, // Доступ к ключевым объектам (для тестов и отладки) partById, localPlayer, humanoid, character, workspace, players, game,