feat(rbxl): XML-������ ������ .rbxl + Day/Night + Tool/Mouse/Backpack flow #38
@ -480,6 +480,12 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
const localPlayer = newInstance('Player', 'Player');
|
const localPlayer = newInstance('Player', 'Player');
|
||||||
localPlayer.Parent = players;
|
localPlayer.Parent = players;
|
||||||
localPlayer.UserId = 1;
|
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';
|
localPlayer.DisplayName = 'Player';
|
||||||
players.Children.push(localPlayer);
|
players.Children.push(localPlayer);
|
||||||
players.LocalPlayer = localPlayer;
|
players.LocalPlayer = localPlayer;
|
||||||
@ -643,6 +649,136 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
global.set('Workspace', workspace);
|
global.set('Workspace', workspace);
|
||||||
|
|
||||||
// === Instance.new ===
|
// === 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"):
|
// Счётчик для новых Part'ов, создаваемых через Instance.new("Part"):
|
||||||
// primitiveManager.addInstance даст уникальный id, мы используем временный
|
// primitiveManager.addInstance даст уникальный id, мы используем временный
|
||||||
// отрицательный id пока не пришёл sceneCreate ACK. Но проще: даём свой
|
// отрицательный id пока не пришёл sceneCreate ACK. Но проще: даём свой
|
||||||
@ -693,6 +829,18 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
inst.Health = 100; inst.MaxHealth = 100;
|
inst.Health = 100; inst.MaxHealth = 100;
|
||||||
inst.Died = makeSignal(); inst.HealthChanged = makeSignal();
|
inst.Died = makeSignal(); inst.HealthChanged = makeSignal();
|
||||||
inst.TakeDamage = function (n) { this.Health = Math.max(0, this.Health - (n || 0)); };
|
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 {
|
} else {
|
||||||
inst = newInstance(className, className);
|
inst = newInstance(className, className);
|
||||||
}
|
}
|
||||||
@ -885,6 +1033,14 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
if (humanoid.Touched) humanoid.Touched.Fire(part);
|
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,
|
partById, localPlayer, humanoid, character, workspace, players, game,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user