/** * roblox-shim.js — регистрация Roblox API внутри Lua-VM (wasmoon). * * Используется из RobloxLuaWorker.js. Регистрирует глобалы: * - game, workspace, script ← Instance-прокси * - Vector3, Color3, CFrame, UDim, UDim2 ← конструкторы математических классов * - Instance.new(class) ← фабрика * - wait, task, tick, os, print, warn ← стандартные глобалы * - Enum ← enum-таблица * * Архитектура: * - JS-классы (RbxVector3, RbxCFrame, ...) — обычные дата-объекты с * перегруженными методами. * - Instance — прокси-объект который хранит { className, properties, children, parent }. * Геттеры/сеттеры эмулируются через __index/__newindex (mt в wasmoon). * - RBXScriptSignal — JS-объект с Connect/Wait/Disconnect. * * Sandbox-side: при изменении Part.Position и т.п. отсылаем в main thread * `partSet` → main применит к Babylon-сцене. */ /* ──────── Math classes ──────── */ class RbxVector3 { constructor(x, y, z) { this.X = +x || 0; this.Y = +y || 0; this.Z = +z || 0; } get Magnitude() { return Math.sqrt(this.X*this.X + this.Y*this.Y + this.Z*this.Z); } get Unit() { const m = this.Magnitude || 1; return new RbxVector3(this.X / m, this.Y / m, this.Z / m); } Dot(o) { return this.X*o.X + this.Y*o.Y + this.Z*o.Z; } Cross(o) { return new RbxVector3( this.Y*o.Z - this.Z*o.Y, this.Z*o.X - this.X*o.Z, this.X*o.Y - this.Y*o.X, ); } Lerp(o, alpha) { return new RbxVector3( this.X + (o.X - this.X) * alpha, this.Y + (o.Y - this.Y) * alpha, this.Z + (o.Z - this.Z) * alpha, ); } add(o) { return new RbxVector3(this.X + o.X, this.Y + o.Y, this.Z + o.Z); } sub(o) { return new RbxVector3(this.X - o.X, this.Y - o.Y, this.Z - o.Z); } mul(scalar) { if (typeof scalar === 'number') { return new RbxVector3(this.X * scalar, this.Y * scalar, this.Z * scalar); } return new RbxVector3(this.X * scalar.X, this.Y * scalar.Y, this.Z * scalar.Z); } toString() { return `${this.X}, ${this.Y}, ${this.Z}`; } } class RbxColor3 { constructor(r, g, b) { this.R = +r || 0; this.G = +g || 0; this.B = +b || 0; } static fromRGB(r, g, b) { return new RbxColor3((r||0)/255, (g||0)/255, (b||0)/255); } static fromHex(hex) { const h = String(hex || '#000000').replace('#',''); return new RbxColor3( parseInt(h.slice(0,2), 16)/255, parseInt(h.slice(2,4), 16)/255, parseInt(h.slice(4,6), 16)/255, ); } Lerp(o, alpha) { return new RbxColor3( this.R + (o.R - this.R) * alpha, this.G + (o.G - this.G) * alpha, this.B + (o.B - this.B) * alpha, ); } toHex() { const h = (n) => Math.max(0, Math.min(255, Math.round(n * 255))).toString(16).padStart(2, '0'); return `#${h(this.R)}${h(this.G)}${h(this.B)}`; } toString() { return `${this.R}, ${this.G}, ${this.B}`; } } class RbxCFrame { constructor(x, y, z, r00=1, r01=0, r02=0, r10=0, r11=1, r12=0, r20=0, r21=0, r22=1) { this.X = +x || 0; this.Y = +y || 0; this.Z = +z || 0; // Row-major 3x3 this.r00 = r00; this.r01 = r01; this.r02 = r02; this.r10 = r10; this.r11 = r11; this.r12 = r12; this.r20 = r20; this.r21 = r21; this.r22 = r22; } static new(x, y, z) { if (x instanceof RbxVector3) return new RbxCFrame(x.X, x.Y, x.Z); return new RbxCFrame(x || 0, y || 0, z || 0); } static Angles(rx, ry, rz) { // Euler XYZ → 3x3 (intrinsic) const cx = Math.cos(rx), sx = Math.sin(rx); const cy = Math.cos(ry), sy = Math.sin(ry); const cz = Math.cos(rz), sz = Math.sin(rz); // R = Rx * Ry * Rz const r00 = cy*cz, r01 = -cy*sz, r02 = sy; const r10 = sx*sy*cz + cx*sz, r11 = -sx*sy*sz + cx*cz, r12 = -sx*cy; const r20 = -cx*sy*cz + sx*sz, r21 = cx*sy*sz + sx*cz, r22 = cx*cy; return new RbxCFrame(0, 0, 0, r00, r01, r02, r10, r11, r12, r20, r21, r22); } static fromEulerAnglesXYZ(rx, ry, rz) { return RbxCFrame.Angles(rx, ry, rz); } get Position() { return new RbxVector3(this.X, this.Y, this.Z); } get LookVector() { return new RbxVector3(-this.r02, -this.r12, -this.r22); } get RightVector() { return new RbxVector3(this.r00, this.r10, this.r20); } get UpVector() { return new RbxVector3(this.r01, this.r11, this.r21); } Lerp(o, a) { // Линейная интерполяция (без правильного slerp на матрицах — для MVP сойдёт) return new RbxCFrame( this.X + (o.X - this.X) * a, this.Y + (o.Y - this.Y) * a, this.Z + (o.Z - this.Z) * a, this.r00, this.r01, this.r02, this.r10, this.r11, this.r12, this.r20, this.r21, this.r22, ); } Inverse() { // Транспонируем 3x3 (для rotation matrix Inverse == Transpose) return new RbxCFrame( -this.X, -this.Y, -this.Z, this.r00, this.r10, this.r20, this.r01, this.r11, this.r21, this.r02, this.r12, this.r22, ); } toEulerXYZ() { const rx = Math.atan2(this.r21, this.r22); const ry = Math.atan2(-this.r20, Math.sqrt(this.r21*this.r21 + this.r22*this.r22)); const rz = Math.atan2(this.r10, this.r00); return [rx, ry, rz]; } toString() { return `${this.X}, ${this.Y}, ${this.Z}`; } } class RbxUDim { constructor(scale, offset) { this.Scale = +scale || 0; this.Offset = +offset | 0; } toString() { return `${this.Scale}, ${this.Offset}`; } } class RbxUDim2 { constructor(xs, xo, ys, yo) { this.X = new RbxUDim(xs, xo); this.Y = new RbxUDim(ys, yo); } static new(xs, xo, ys, yo) { return new RbxUDim2(xs, xo, ys, yo); } static fromScale(xs, ys) { return new RbxUDim2(xs, 0, ys, 0); } static fromOffset(xo, yo) { return new RbxUDim2(0, xo, 0, yo); } toString() { return `${this.X.Scale}, ${this.X.Offset}, ${this.Y.Scale}, ${this.Y.Offset}`; } } /* ──────── RBXScriptSignal ──────── */ let _signalIdCounter = 1000; class RbxSignal { constructor(name) { this.name = name; this.id = _signalIdCounter++; this.connections = []; } Connect(callback) { const conn = { callback, connected: true }; this.connections.push(conn); return { Disconnect: () => { conn.connected = false; }, disconnect: () => { conn.connected = false; }, Connected: () => conn.connected, }; } // Legacy Roblox API — lowercase alias connect(callback) { return this.Connect(callback); } Wait() { return null; } wait() { return null; } Fire(...args) { for (const c of this.connections) { if (!c.connected) continue; try { c.callback(...args); } catch (e) { /* swallow */ } } } fire(...args) { return this.Fire(...args); } } /* ──────── Instance прокси ──────── */ let _instanceCounter = 1; // Null-stub: возвращается из FindFirstChild/WaitForChild когда объект не найден. // Имеет все методы Instance как no-op, чтобы Lua-цепочки вроде // game.Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("X").Touched:Connect(fn) // не падали с "attempt to call js_null", когда промежуточный объект не существует. // Все методы возвращают сам _nullStub чтобы цепочка не разорвалась. // nullSignal: callable proxy. Lua делает x:Connect(fn) = x.Connect(x, fn), // но также pattern signal:Connect(fn) сначала достаёт signal.Connect (это функция), // потом вызывает её. Мы возвращаем функцию которая безусловно возвращает {Disconnect}. const _nullConn = { Disconnect: () => {}, disconnect: () => {}, Connected: false }; const _nullSignalFn = () => _nullConn; const _nullSignal = new Proxy(_nullSignalFn, { get(_, k) { if (k === 'Connect' || k === 'connect') return _nullSignalFn; if (k === 'Wait' || k === 'wait') return () => null; if (k === 'Fire' || k === 'fire') return () => {}; return undefined; }, }); // Известные имена сигналов (Touched, Changed, MouseButton1Click, ...) const _SIGNAL_NAMES = new Set([ 'Touched','TouchEnded','Changed','Activated', 'MouseButton1Click','MouseButton1Down','MouseButton1Up', 'MouseButton2Click','MouseButton2Down','MouseButton2Up', 'MouseEnter','MouseLeave','InputBegan','InputEnded','InputChanged', 'PlayerAdded','PlayerRemoving','CharacterAdded','CharacterRemoving', 'Heartbeat','Stepped','RenderStepped','Died','HealthChanged', 'FocusLost','Focused','ChildAdded','ChildRemoved', 'AncestryChanged','DescendantAdded','DescendantRemoving', // Tool сигналы 'Equipped','Unequipped','Selected','Deselected', // прочие популярные 'OnInvoke','OnServerInvoke','OnClientInvoke', 'OnServerEvent','OnClientEvent','Fired','Triggered', 'ChatMakeSystemMessage','ChatMade', ]); // _makeDeepStub — рекурсивный proxy которому всё равно сколько раз его // индексируют. На любом уровне: // - caps-имя из _SIGNAL_NAMES → возвращает _nullSignal // - 'Parent' → возвращает _nullStub // - любое другое имя → callable proxy + рекурсивная глубина // Это позволяет цепочкам типа `tool.Selected:Connect(fn)` или // `script.Parent.Parent.Frame.Visible` молча no-op'аться. // Вместо JS Proxy (который wasmoon оборачивает в js_promise) — используем // специальный маркер. Реальный stub живёт на Lua-стороне. const NULL_STUB_MARKER = { __isNullStubMarker: true }; function _makeDeepStub() { return NULL_STUB_MARKER; } const _nullStubBase = { __isNullStub: true, ClassName: 'Nil', Name: 'Nil', Children: [], Value: 0, Text: '', Visible: false }; // _nullStub оставлен как маркер, но не используется как реальный stub — // debug.setmetatable(nil) в Lua перехватывает всё это. const _nullStub = _nullStubBase; class RbxInstance { constructor(className, init = {}) { this.__id = _instanceCounter++; this.ClassName = className; this.Name = init.Name || className; this.Parent = init.Parent || null; this.Children = []; this.__props = {}; // raw properties (для Position и т.п.) // Signals доступны как прямые свойства, плюс дублируются в __signals для serv-кода this.Touched = new RbxSignal('Touched'); this.TouchEnded = new RbxSignal('TouchEnded'); this.Changed = new RbxSignal('Changed'); this.AncestryChanged = new RbxSignal('AncestryChanged'); this.ChildAdded = new RbxSignal('ChildAdded'); this.ChildRemoved = new RbxSignal('ChildRemoved'); this.__signals = { Touched: this.Touched, TouchEnded: this.TouchEnded, Changed: this.Changed, AncestryChanged: this.AncestryChanged, ChildAdded: this.ChildAdded, ChildRemoved: this.ChildRemoved, }; this.__sceneState = null; } GetChildren() { return [...this.Children]; } GetDescendants() { const out = []; const walk = (n) => { for (const c of n.Children) { out.push(c); walk(c); } }; walk(this); return out; } FindFirstChild(name, recursive) { for (const c of this.Children) { if (c.Name === name) return c; if (recursive) { const found = c.FindFirstChild(name, true); if (found) return found; } } // Возвращаем undefined — wasmoon отдаст это как nil. // Lua-side debug.setmetatable(nil) перехватит дальнейшую индексацию. return undefined; } FindFirstChildOfClass(className) { for (const c of this.Children) { if (c.ClassName === className) return c; } return undefined; } FindFirstAncestor(name) { let p = this.Parent; while (p) { if (p.Name === name) return p; p = p.Parent; } return undefined; } WaitForChild(name, _timeout) { // В MVP — синхронный поиск без ожидания. В 4.7 через корутины можно ждать. return this.FindFirstChild(name); } IsA(className) { if (this.ClassName === className) return true; // Roblox class hierarchy: Part isA BasePart isA PVInstance isA Instance. const hierarchy = { 'Part': ['BasePart', 'PVInstance', 'Instance'], 'WedgePart': ['BasePart', 'PVInstance', 'Instance'], 'CornerWedgePart': ['BasePart', 'PVInstance', 'Instance'], 'MeshPart': ['BasePart', 'PVInstance', 'Instance'], 'UnionOperation': ['PartOperation', 'BasePart', 'PVInstance', 'Instance'], 'TrussPart': ['BasePart', 'PVInstance', 'Instance'], 'SpawnLocation': ['Part', 'BasePart', 'PVInstance', 'Instance'], 'Script': ['BaseScript', 'LuaSourceContainer', 'Instance'], 'LocalScript': ['BaseScript', 'LuaSourceContainer', 'Instance'], 'ModuleScript': ['LuaSourceContainer', 'Instance'], 'Folder': ['Instance'], 'Model': ['PVInstance', 'Instance'], 'Sound': ['Instance'], 'PointLight': ['Light', 'Instance'], 'SpotLight': ['Light', 'Instance'], 'Humanoid': ['Instance'], }; const ancestors = hierarchy[this.ClassName] || []; return ancestors.includes(className); } Destroy() { if (this.Parent && this.Parent.Children) { const idx = this.Parent.Children.indexOf(this); if (idx >= 0) this.Parent.Children.splice(idx, 1); } this.Parent = null; this.__destroyed = true; } Clone() { const cl = new RbxInstance(this.ClassName); cl.Name = this.Name; cl.__props = JSON.parse(JSON.stringify(this.__props)); for (const c of this.Children) { const cc = c.Clone(); cc.Parent = cl; cl.Children.push(cc); } return cl; } GetPropertyChangedSignal(propName) { const sigName = `Changed:${propName}`; if (!this.__signals[sigName]) this.__signals[sigName] = new RbxSignal(sigName); return this.__signals[sigName]; } } /* ──────── Part — наследник Instance с реальными свойствами сцены ──────── */ class RbxPart extends RbxInstance { constructor(primId, init = {}) { super(init.ClassName || 'Part', init); this.__primId = primId; // id примитива в Rublox-сцене this.__sendFn = null; // setter из shim init // Кешированные свойства (mirror'ятся через handleTick) this._snap = init.snap || {}; } get Position() { return new RbxVector3(this._snap.x || 0, this._snap.y || 0, this._snap.z || 0); } set Position(v) { if (v instanceof RbxVector3) { this._snap.x = v.X; this._snap.y = v.Y; this._snap.z = v.Z; this.__sendFn?.('partSet', { primId: this.__primId, prop: 'position', value: { x: v.X, y: v.Y, z: v.Z } }); } } get CFrame() { return new RbxCFrame(this._snap.x || 0, this._snap.y || 0, this._snap.z || 0); } set CFrame(cf) { if (cf instanceof RbxCFrame) { this._snap.x = cf.X; this._snap.y = cf.Y; this._snap.z = cf.Z; const [rx, ry, rz] = cf.toEulerXYZ(); this._snap.rx = rx; this._snap.ry = ry; this._snap.rz = rz; this.__sendFn?.('partSet', { primId: this.__primId, prop: 'cframe', value: { x: cf.X, y: cf.Y, z: cf.Z, rx, ry, rz } }); } } get Size() { return new RbxVector3(this._snap.sx || 1, this._snap.sy || 1, this._snap.sz || 1); } set Size(v) { if (v instanceof RbxVector3) { this._snap.sx = v.X; this._snap.sy = v.Y; this._snap.sz = v.Z; this.__sendFn?.('partSet', { primId: this.__primId, prop: 'size', value: { sx: v.X, sy: v.Y, sz: v.Z } }); } } get Color() { return RbxColor3.fromHex(this._snap.color || '#cccccc'); } set Color(c) { if (c instanceof RbxColor3) { const hex = c.toHex(); this._snap.color = hex; this.__sendFn?.('partSet', { primId: this.__primId, prop: 'color', value: hex }); } } get BrickColor() { return { Color: this.Color, Name: 'Medium stone grey' }; } set BrickColor(b) { if (b && b.Color) this.Color = b.Color; } get Material() { return this._snap.material || 'glossy'; } set Material(m) { this._snap.material = m; this.__sendFn?.('partSet', { primId: this.__primId, prop: 'material', value: m }); } get Anchored() { return !!this._snap.anchored; } set Anchored(v) { this._snap.anchored = !!v; this.__sendFn?.('partSet', { primId: this.__primId, prop: 'anchored', value: !!v }); } get CanCollide() { return this._snap.canCollide !== false; } set CanCollide(v) { this._snap.canCollide = !!v; this.__sendFn?.('partSet', { primId: this.__primId, prop: 'canCollide', value: !!v }); } get Transparency() { return 1.0 - (this._snap.opacity ?? 1.0); } set Transparency(v) { this._snap.opacity = 1.0 - (+v || 0); this.__sendFn?.('partSet', { primId: this.__primId, prop: 'opacity', value: this._snap.opacity }); } get Velocity() { return new RbxVector3(0, 0, 0); } set Velocity(v) { if (v instanceof RbxVector3) { this.__sendFn?.('partVel', { primId: this.__primId, vx: v.X, vy: v.Y, vz: v.Z }); } } } /* ──────── Главный entry-point: регистрация в Lua-VM ──────── */ export function registerRobloxApi(lua, ctx) { const { getSceneSnap, targetPrimitiveId, send, getGuiTree, scheduler } = ctx; // 1. Math classes — как глобалы с .new factory const wrap = (cls) => ({ new: (...args) => new cls(...args), }); lua.global.set('Vector3', { new: (x, y, z) => new RbxVector3(x, y, z), zero: new RbxVector3(0, 0, 0), one: new RbxVector3(1, 1, 1), xAxis: new RbxVector3(1, 0, 0), yAxis: new RbxVector3(0, 1, 0), zAxis: new RbxVector3(0, 0, 1), }); lua.global.set('Color3', { new: (r, g, b) => new RbxColor3(r, g, b), fromRGB: RbxColor3.fromRGB, fromHex: RbxColor3.fromHex, }); lua.global.set('CFrame', { new: RbxCFrame.new, Angles: RbxCFrame.Angles, fromEulerAnglesXYZ: RbxCFrame.fromEulerAnglesXYZ, }); lua.global.set('UDim', { new: (s, o) => new RbxUDim(s, o) }); lua.global.set('UDim2', { new: RbxUDim2.new, fromScale: RbxUDim2.fromScale, fromOffset: RbxUDim2.fromOffset, }); // 2. Сцена — собираем JS-структуру из snap'а // Workspace — корень. const workspace = new RbxInstance('Workspace', { Name: 'Workspace' }); const part_by_id = new Map(); const snap = getSceneSnap(); if (snap && snap.primitives) { for (const [id, p] of Object.entries(snap.primitives)) { const part = new RbxPart(+id, { ClassName: p.type === 'wedge' ? 'WedgePart' : p.type === 'cornerwedge' ? 'CornerWedgePart' : 'Part', Name: p.name || 'Part', snap: { ...p }, }); part.__sendFn = send; // Сигналы Part: Touched/TouchEnded существуют на каждом по умолчанию part.Touched = new RbxSignal('Touched'); part.TouchEnded = new RbxSignal('TouchEnded'); part.Parent = workspace; workspace.Children.push(part); part_by_id.set(+id, part); } } // 2b. GUI-tree: предсоздаём ScreenGui + Frame/Button/Label/etc по дереву // конвертера. Каждый button получает MouseButton1Click/MouseButton1Down/Up // сигналы которые fire'аются из main через sendGlobalEvent('guiClick'). const gui_by_id = new Map(); // PlayerGui контейнер внутри Players.LocalPlayer const playerGui = new RbxInstance('PlayerGui', { Name: 'PlayerGui' }); if (getGuiTree) { const tree = getGuiTree() || []; // первый проход — создаём instances for (const el of tree) { const cls = el.__roblox_class || 'Frame'; const inst = new RbxInstance(cls, { Name: el.name || cls }); inst.__guiId = el.id; inst.Visible = el.visible !== false; inst.Text = el.text || ''; // Стандартные сигналы кнопок if (cls === 'TextButton' || cls === 'ImageButton') { inst.MouseButton1Click = new RbxSignal('MouseButton1Click'); inst.MouseButton1Down = new RbxSignal('MouseButton1Down'); inst.MouseButton1Up = new RbxSignal('MouseButton1Up'); inst.Activated = new RbxSignal('Activated'); inst.MouseEnter = new RbxSignal('MouseEnter'); inst.MouseLeave = new RbxSignal('MouseLeave'); } // FocusLost для textboxes if (cls === 'TextBox') { inst.FocusLost = new RbxSignal('FocusLost'); inst.Focused = new RbxSignal('Focused'); } // Changed-сигнал у каждого inst.Changed = new RbxSignal('Changed'); gui_by_id.set(el.id, inst); } // второй проход — parent-связи (parentId → Instance) for (const el of tree) { const inst = gui_by_id.get(el.id); if (!inst) continue; const parentInst = el.parentId ? gui_by_id.get(el.parentId) : playerGui; if (parentInst) { inst.Parent = parentInst; parentInst.Children.push(inst); } } } // 3. script — в shared-режиме не глобал, а локально создаётся при addScript. // Здесь только заглушка чтобы простые non-shared скрипты не падали. if (targetPrimitiveId != null && part_by_id.has(targetPrimitiveId)) { const parentPart = part_by_id.get(targetPrimitiveId); const scriptInst = new RbxInstance('LocalScript', { Name: 'Script' }); scriptInst.Parent = parentPart; parentPart.Children.push(scriptInst); lua.global.set('script', scriptInst); } // 4. game / game:GetService const services = new Map(); const game = new RbxInstance('DataModel', { Name: 'Game' }); game.Children.push(workspace); workspace.Parent = game; // Builtin services: const lighting = new RbxInstance('Lighting', { Name: 'Lighting' }); lighting.Parent = game; game.Children.push(lighting); services.set('Lighting', lighting); const replicatedStorage = new RbxInstance('ReplicatedStorage', { Name: 'ReplicatedStorage' }); replicatedStorage.Parent = game; game.Children.push(replicatedStorage); services.set('ReplicatedStorage', replicatedStorage); const runService = new RbxInstance('RunService', { Name: 'RunService' }); runService.Heartbeat = new RbxSignal('Heartbeat'); runService.Stepped = new RbxSignal('Stepped'); runService.RenderStepped = new RbxSignal('RenderStepped'); services.set('RunService', runService); const playersService = new RbxInstance('Players', { Name: 'Players' }); playersService.PlayerAdded = new RbxSignal('PlayerAdded'); playersService.PlayerRemoving = new RbxSignal('PlayerRemoving'); // LocalPlayer с PlayerGui + Character const localPlayer = new RbxInstance('Player', { Name: 'Player1' }); localPlayer.UserId = 1; localPlayer.PlayerGui = playerGui; playerGui.Parent = localPlayer; localPlayer.Children.push(playerGui); // Character заглушка с Humanoid и HumanoidRootPart const character = new RbxInstance('Model', { Name: 'Character' }); const humanoid = new RbxInstance('Humanoid', { Name: 'Humanoid' }); humanoid.WalkSpeed = 16; humanoid.JumpPower = 50; humanoid.Health = 100; humanoid.MaxHealth = 100; humanoid.Died = new RbxSignal('Died'); humanoid.HealthChanged = new RbxSignal('HealthChanged'); humanoid.Touched = new RbxSignal('Touched'); humanoid.Parent = character; character.Children.push(humanoid); character.Humanoid = humanoid; const hrp = new RbxPart(-1, { ClassName: 'Part', Name: 'HumanoidRootPart' }); hrp.Touched = new RbxSignal('Touched'); hrp.Parent = character; character.Children.push(hrp); character.HumanoidRootPart = hrp; localPlayer.Character = character; localPlayer.CharacterAdded = new RbxSignal('CharacterAdded'); localPlayer.CharacterRemoving = new RbxSignal('CharacterRemoving'); playersService.LocalPlayer = localPlayer; playersService.Children.push(localPlayer); services.set('Players', playersService); game.GetService = function(svc) { if (services.has(svc)) return services.get(svc); if (svc === 'Workspace') return workspace; if (svc === 'Workspace') return workspace; // Неизвестный сервис — создаём заглушку, чтобы не падало const stub = new RbxInstance(svc, { Name: svc }); services.set(svc, stub); return stub; }; game.Workspace = workspace; game.Lighting = lighting; game.Players = playersService; game.ReplicatedStorage = replicatedStorage; lua.global.set('game', game); lua.global.set('workspace', workspace); lua.global.set('Workspace', workspace); // 5. Instance.new lua.global.set('Instance', { new: (className, parent) => { const inst = new RbxInstance(className); if (parent && parent instanceof RbxInstance) { inst.Parent = parent; parent.Children.push(inst); } return inst; }, }); // 6. wait/task.wait через scheduler. scheduler — main-side, поддерживает // schedule(sec, fn) что fire'ит fn после задержки в следующих tick'ах. // spawn/delay/defer запускают функцию через scheduler.spawn (отдельная корутина). const sched = scheduler || { schedule: (sec, fn) => { try { fn(); } catch (e) {} }, spawn: (fn) => { try { fn(); } catch (e) {} }, now: () => Date.now() / 1000, }; lua.global.set('wait', (sec) => { // В корутине: yield на (sec || 0). Scheduler сам resume'ит. // Тут мы синхронны (вызов из Lua) — реальный yield делается в lua-wrapper // через coroutine.yield, который мы оборачиваем в addScript. // Здесь просто возвращаем длительность для совместимости. return [sec || 0, 0]; }); lua.global.set('task', { wait: (sec) => sec || 0, spawn: (fn, ...args) => { sched.spawn(() => { try { fn(...args); } catch (e) {} }); return null; }, delay: (sec, fn, ...args) => { sched.schedule(sec || 0, () => { try { fn(...args); } catch (e) {} }); return null; }, defer: (fn, ...args) => { sched.spawn(() => { try { fn(...args); } catch (e) {} }); return null; }, }); lua.global.set('spawn', (fn) => { sched.spawn(() => { try { fn(); } catch (e) {} }); }); lua.global.set('delay', (sec, fn) => { sched.schedule(sec || 0, () => { try { fn(); } catch (e) {} }); }); // require(ModuleScript) — возвращаем nil, debug.setmetatable перехватит. lua.global.set('require', (_arg) => undefined); lua.global.set('tick', () => Date.now() / 1000); lua.global.set('time', () => Date.now() / 1000); lua.global.set('elapsedTime', () => Date.now() / 1000); // 7. print / warn / error — пробрасываем в main как log lua.global.set('print', (...args) => { const text = args.map(a => luaToString(a)).join('\t'); send('log', { level: 'info', text }); }); lua.global.set('warn', (...args) => { const text = args.map(a => luaToString(a)).join('\t'); send('log', { level: 'warn', text }); }); // 8. Enum — упрощённая заглушка для самых популярных enums const enumTable = { Material: { Plastic: { Value: 256, Name: 'Plastic' }, Neon: { Value: 784, Name: 'Neon' }, Metal: { Value: 512, Name: 'Metal' }, Glass: { Value: 1024, Name: 'Glass' }, Wood: { Value: 272, Name: 'Wood' }, SmoothPlastic: { Value: 496, Name: 'SmoothPlastic' } }, PartType: { Ball: { Value: 0, Name: 'Ball' }, Block: { Value: 1, Name: 'Block' }, Cylinder: { Value: 2, Name: 'Cylinder' } }, KeyCode: { Space: { Value: 32, Name: 'Space' }, W: { Value: 87, Name: 'W' }, A: { Value: 65, Name: 'A' }, S: { Value: 83, Name: 'S' }, D: { Value: 68, Name: 'D' } }, EasingStyle: { Linear: { Value: 0, Name: 'Linear' }, Quad: { Value: 1, Name: 'Quad' }, Sine: { Value: 5, Name: 'Sine' } }, EasingDirection: { In: { Value: 0, Name: 'In' }, Out: { Value: 1, Name: 'Out' }, InOut: { Value: 2, Name: 'InOut' } }, }; lua.global.set('Enum', enumTable); return { workspace, game, part_by_id, services, gui_by_id, localPlayer, character, humanoid }; } function luaToString(v) { if (v == null) return 'nil'; if (typeof v === 'string') return v; if (typeof v === 'number') return String(v); if (typeof v === 'boolean') return String(v); if (v.toString) return v.toString(); return ''; } export { RbxVector3, RbxColor3, RbxCFrame, RbxUDim, RbxUDim2, RbxInstance, RbxPart, RbxSignal };