244 lines
9.2 KiB
JavaScript
244 lines
9.2 KiB
JavaScript
/**
|
||
* 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);
|
||
})();
|