/** * rbxl-lua-integration.test.js — реалистичные Roblox-сниппеты из obby/simulator карт. */ import { LuaFactory } from 'wasmoon'; import { registerRobloxApi } from '../src/engine/roblox-shim.js'; import { RobloxScheduler } from '../src/engine/roblox-scheduler.js'; import { installRobloxServices } from '../src/engine/roblox-services.js'; import { RobloxTweenManager } from '../src/engine/roblox-tween.js'; import { RobloxPhysicsManager } from '../src/engine/roblox-physics.js'; function makeScene() { return { primitives: { 10: { id: 10, type: 'cube', name: 'KillPart', x: 5, y: 1, z: 0, sx: 4, sy: 1, sz: 4, color: '#ff0000', material: 'neon', anchored: true, canCollide: true, opacity: 1 }, 11: { id: 11, type: 'cube', name: 'WinPart', x: 30, y: 1, z: 0, sx: 4, sy: 1, sz: 4, color: '#00ff00', material: 'neon', anchored: true, canCollide: true, opacity: 1 }, 12: { id: 12, type: 'cube', name: 'Conveyor', x: 15, y: 1, z: 0, sx: 8, sy: 0.5, sz: 4, color: '#888888', material: 'metal', anchored: true, canCollide: true, opacity: 1 }, 13: { id: 13, type: 'cube', name: 'Door', x: 20, y: 3, z: 0, sx: 2, sy: 6, sz: 4, color: '#a0522d', material: 'matte', anchored: true, canCollide: true, opacity: 1 }, }, }; } const STORE = new Map(); async function run(luaSource, targetPrimId = 10, ticks = []) { const factory = new LuaFactory(); const lua = await factory.createEngine(); const sent = []; const send = (cmd, payload) => sent.push({ cmd, payload }); let playerState = { x: 0, y: 5, z: 0, hp: 100 }; registerRobloxApi(lua, { getSceneSnap: makeScene, targetPrimitiveId: targetPrimId, send }); const sched = new RobloxScheduler(lua); sched.install(); installRobloxServices(lua, { send, getPlayerState: () => playerState, loadSave: (k) => STORE.get(k), saveSave: (k, v) => STORE.set(k, v), removeSave: (k) => STORE.delete(k), }); const tween = new RobloxTweenManager(); tween.install(lua); const phys = new RobloxPhysicsManager(send); phys.install(lua); await sched.spawnMain(luaSource); for (const dt of ticks) { await sched.tick(dt); tween.tick(dt); phys.tick(dt); } lua.global.close(); return { logs: sent.filter(s => s.cmd === 'log').map(s => s.payload), partSets: sent.filter(s => s.cmd === 'partSet').map(s => s.payload), partVels: sent.filter(s => s.cmd === 'partVel').map(s => s.payload), playerCmds: sent.filter(s => s.cmd === 'playerCmd').map(s => s.payload), }; } const TESTS = [ { name: 'KillBrick (Touched → Humanoid.Health = 0)', lua: ` local part = script.Parent part.Touched:Connect(function(hit) local hum = hit.Parent and hit.Parent:FindFirstChild("Humanoid") if hum then hum.Health = 0 end end) print("kill brick armed") `, ticks: [], check: (r) => r.logs.some(l => l.text === 'kill brick armed'), }, { name: 'WalkSpeed boost через trigger', lua: ` local h = game:GetService("Players").LocalPlayer.Character.Humanoid h.WalkSpeed = 32 print("speed boosted to", h.WalkSpeed) `, check: (r) => r.playerCmds.some(c => c.method === 'setWalkSpeed' && c.args[0] === 32) && r.logs.some(l => l.text.includes('speed boosted')), }, { name: 'Door open: TweenService двигает дверь вверх', lua: ` local door = workspace:FindFirstChild("Door") local TS = game:GetService("TweenService") local goal = { Position = Vector3.new(door.Position.X, door.Position.Y + 10, door.Position.Z) } local info = TweenInfo.new(1, Enum.EasingStyle.Linear, Enum.EasingDirection.Out) local tw = TS:Create(door, info, goal) tw:Play() print("door opening") `, ticks: [0.5, 0.5, 0.1], check: (r) => r.partSets.some(p => p.primId === 13 && p.prop === 'position'), }, { name: 'Конвейер: BodyVelocity толкает игрока', lua: ` local conv = workspace:FindFirstChild("Conveyor") local bv = Instance.new("BodyVelocity", conv) bv.Velocity = Vector3.new(20, 0, 0) bv.MaxForce = Vector3.new(4000, 0, 4000) print("conveyor started") `, ticks: [0.1], check: (r) => r.partVels.some(v => v.primId === 12 && v.vx === 20), }, { name: 'leaderstats (как в tycoon)', lua: ` local Players = game:GetService("Players") local plr = Players.LocalPlayer local money = Instance.new("IntValue", plr.leaderstats) money.Name = "Money" money.Value = 100 print("money:", money.Value) `, check: (r) => r.logs.some(l => l.text === 'money:\t100'), }, { name: 'Checkpoint сохраняется в DataStore', lua: ` local DSS = game:GetService("DataStoreService") local store = DSS:GetDataStore("checkpoints") store:SetAsync("player1", 5) local cp = store:GetAsync("player1") print("checkpoint:", cp) `, check: (r) => r.logs.some(l => l.text === 'checkpoint:\t5'), }, { name: 'Цикл с wait — подсчёт', lua: ` for i = 1, 3 do print("count:", i) wait(0.3) end print("done") `, ticks: [0.3, 0.3, 0.3, 0.3], check: (r) => { const texts = r.logs.map(l => l.text); return texts.includes('count:\t1') && texts.includes('count:\t2') && texts.includes('count:\t3') && texts.includes('done'); }, }, { name: 'task.spawn — параллельные функции', lua: ` task.spawn(function() print("parallel A") end) task.spawn(function() print("parallel B") end) print("main") `, check: (r) => { const texts = r.logs.map(l => l.text); return texts.includes('parallel A') && texts.includes('parallel B') && texts.includes('main'); }, }, { name: 'Color3 + Material смена при Touched', lua: ` local part = workspace:FindFirstChild("KillPart") part.Touched:Connect(function() part.Color = Color3.fromRGB(0, 0, 255) part.Material = "Neon" end) -- симулируем touch part.Touched:Fire(workspace) `, check: (r) => r.partSets.some(p => p.primId === 10 && p.prop === 'color') && r.partSets.some(p => p.primId === 10 && p.prop === 'material'), }, { name: 'RemoteEvent: client→server message', lua: ` local re = Instance.new("RemoteEvent", workspace) re.Name = "Coins" re.OnServerEvent:Connect(function(player, amount) print("server received:", amount) end) re:FireServer(50) `, check: (r) => r.logs.some(l => l.text === 'server received:\t50'), }, { name: 'Heartbeat: счётчик через RunService', lua: ` local RS = game:GetService("RunService") local count = 0 RS.Heartbeat:Connect(function(dt) count = count + 1 if count == 3 then print("tick3") end end) `, ticks: [0.1, 0.1, 0.1], check: (r) => r.logs.some(l => l.text === 'tick3'), }, { name: 'Math: Vector3 arithmetic', lua: ` local a = Vector3.new(1, 2, 3) local b = Vector3.new(4, 5, 6) local sum = a:add(b) print("sum:", sum.X, sum.Y, sum.Z) local d = a:Dot(b) print("dot:", d) `, check: (r) => { const texts = r.logs.map(l => l.text); return texts.some(t => t === 'sum:\t5\t7\t9') && texts.some(t => t === 'dot:\t32'); }, }, ]; (async () => { let passed = 0, failed = 0; for (const t of TESTS) { try { const r = await run(t.lua, t.targetPrimId, t.ticks || []); const ok = t.check(r); if (ok) { console.log(`✓ ${t.name}`); passed++; } else { console.log(`✗ ${t.name}`); console.log(` logs: ${JSON.stringify(r.logs.map(l => l.text))}`); if (r.partSets.length) console.log(` partSets: ${JSON.stringify(r.partSets)}`); if (r.partVels.length) console.log(` partVels: ${JSON.stringify(r.partVels)}`); if (r.playerCmds.length) console.log(` playerCmds: ${JSON.stringify(r.playerCmds)}`); failed++; } } catch (e) { console.log(`✗ ${t.name} — exception: ${e.message || e}`); failed++; } } console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed > 0 ? 1 : 0); })();