feat(lua): Этап 5 — GUI (Frame, TextLabel, TextButton, ImageLabel, TextBox, ScrollingFrame)
В Lua теперь работает Roblox-style GUI:
local sg = Instance.new('ScreenGui')
local label = Instance.new('TextLabel', sg)
label.Text = 'Hello'
label.TextColor3 = Color3.fromRGB(255, 255, 0)
label.Position = UDim2.new(0.5, 0, 0.5, 0)
label.Size = UDim2.new(0.2, 0, 0.05, 0)
local btn = Instance.new('TextButton', sg)
btn.Text = 'Click me'
btn.MouseButton1Click:Connect(function()
print('clicked!')
end)
Реализация: GUI-обёртка newGuiInstance создаёт элемент через gui.create
команду → GameRuntime.scene3d.guiManager. Setter'ы Text/Visible/
BackgroundColor3/TextColor3/TextSize/Position/Size шлют gui.update.
Destroy шлёт gui.remove. Клики через guiClick → guiByLocalRef →
inst.MouseButton1Click.Fire().
Добавлен localPlayer.PlayerGui для совместимости с Roblox-скриптами.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
936f93a42c
commit
371ddaaae8
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user