player/tests/rbxl-lua-integration.test.js
min f34320db91
All checks were successful
CI / Lint (pull_request) Successful in 54s
CI / Build (pull_request) Successful in 1m33s
CI / Secret scan (pull_request) Successful in 20s
CI / PR size check (pull_request) Successful in 6s
CI / Deploy to S1 + S2 (pull_request) Has been skipped
feat(rbxl-import): Lua-runtime (wasmoon) для Roblox-скриптов
Часть тест-фичи импорта Roblox-карт (см. rublox/studio rbxl-importer/).

Что добавлено:
- wasmoon (Lua 5.4 WASM) как dep.
- RobloxLuaWorker.js — Worker-хост Lua-VM.
- RobloxLuaSandbox.js — main-side обёртка (по аналогии с ScriptSandbox).
- roblox-shim.js — math (Vector3/Color3/CFrame/UDim2),
  Instance прокси (game/workspace/script/GetService/IsA),
  Part свойства (Position/Color/Material/Anchored/CanCollide),
  RBXScriptSignal (Touched/Heartbeat/Stepped/Connect/Wait).
- roblox-scheduler.js — корутины + wait/task.wait/task.delay/task.spawn,
  автоматический fire Heartbeat/Stepped/RenderStepped на tick.
- roblox-tween.js — TweenService с 10 easing-функциями
  (Linear, Quad, Cubic, Quart, Quint, Sine, Bounce, Elastic, Back, Exponential).
- roblox-services.js — Players/LocalPlayer/Character/Humanoid
  (Health, WalkSpeed, JumpPower, TakeDamage, Died, LoadAnimation),
  UserInputService, RemoteEvent (FireServer/FireClient),
  RemoteFunction, DataStoreService, HttpService.
- roblox-physics.js — BodyVelocity/BodyGyro/BodyPosition/BodyForce/
  BodyAngularVelocity/AlignPosition/LinearVelocity.

Интеграция в GameRuntime:
- В start() проверяется script.kind === 'roblox-lua' →
  _startRobloxLuaScript() запускает RobloxLuaSandbox.
- _handleRobloxLuaCommand() мапит IPC команды (partSet/partVel/playerCmd)
  на PrimitiveManager и game.player API.
- _buildRobloxLuaSceneSnap() готовит snap для workspace:GetChildren.

Тесты: **36/36 passed**.
- mvp (9): math, Instance proxy, Part, IsA.
- wait (5): корутины, wait/task.wait/task.delay.
- tween (2): TweenInfo + Linear easing.
- services (8): Humanoid, DataStore, HttpService, RemoteEvent.
- integration (12): KillBrick, WalkSpeed, Tween-door, BodyVelocity конвейер,
  leaderstats, Checkpoint, циклы с wait, task.spawn, Color/Material,
  RemoteEvent client→server, Heartbeat, Vector3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 18:23:32 +03:00

244 lines
9.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);
})();