player/tests/rbxl-lua-mvp.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

188 lines
6.3 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-mvp.test.js — headless smoke-тест Roblox Lua API shim.
*
* НЕ запускает Worker (это требует браузерного Worker API). Вместо этого
* напрямую импортирует roblox-shim.js и инициализирует Lua в текущем потоке.
*
* Запуск: node --experimental-vm-modules tests/rbxl-lua-mvp.test.js
*/
import { LuaFactory } from 'wasmoon';
import { registerRobloxApi } from '../src/engine/roblox-shim.js';
const FAKE_SCENE_SNAP = {
primitives: {
1: { id: 1, type: 'cube', name: 'Floor', x: 0, y: 0, z: 0, sx: 10, sy: 1, sz: 10,
color: '#888888', material: 'glossy', anchored: true, canCollide: true, opacity: 1 },
2: { id: 2, type: 'cube', name: 'KillBrick', x: 5, y: 1, z: 0, sx: 2, sy: 1, sz: 2,
color: '#ff0000', material: 'neon', anchored: true, canCollide: true, opacity: 1 },
},
};
const SNIPPETS = [
{
name: 'print hello',
lua: `print("Hello from Lua!")`,
expectLogs: [{ level: 'info', text: 'Hello from Lua!' }],
},
{
name: 'Vector3 math',
lua: `
local v = Vector3.new(3, 4, 0)
print("magnitude:", v.Magnitude)
local u = v.Unit
print("unit:", u.X, u.Y, u.Z)
`,
expectLogs: [
{ level: 'info', text: 'magnitude:\t5' },
],
},
{
name: 'workspace iteration',
lua: `
local children = workspace:GetChildren()
print("count:", #children)
for i, c in ipairs(children) do
print("child:", c.Name, "class:", c.ClassName)
end
`,
expectLogs: [
{ level: 'info', text: 'count:\t2' },
],
},
{
name: 'FindFirstChild',
lua: `
local kb = workspace:FindFirstChild("KillBrick")
if kb then print("found:", kb.Name)
else print("not found") end
`,
expectLogs: [{ level: 'info', text: 'found:\tKillBrick' }],
},
{
name: 'Part.Position get',
lua: `
local kb = workspace:FindFirstChild("KillBrick")
print("position:", kb.Position.X, kb.Position.Y, kb.Position.Z)
`,
expectLogs: [{ level: 'info', text: 'position:\t5\t1\t0' }],
},
{
name: 'Part.Color set',
lua: `
local kb = workspace:FindFirstChild("KillBrick")
kb.Color = Color3.new(0, 1, 0)
print("new color hex (via Position):", kb.Color.R, kb.Color.G, kb.Color.B)
`,
expectPartSet: { primId: 2, prop: 'color' },
},
{
name: 'CFrame.Angles',
lua: `
local cf = CFrame.Angles(0, math.pi/2, 0)
print("lookvector:", cf.LookVector.X, cf.LookVector.Y, cf.LookVector.Z)
`,
expectLogs: [],
},
{
name: 'Instance.new + Parent',
lua: `
local f = Instance.new("Folder", workspace)
f.Name = "MyFolder"
print("folder name:", f.Name, "parent:", f.Parent.Name)
`,
expectLogs: [{ level: 'info', text: 'folder name:\tMyFolder\tparent:\tWorkspace' }],
},
{
name: 'IsA hierarchy',
lua: `
local kb = workspace:FindFirstChild("KillBrick")
print("isa Part:", kb:IsA("Part"))
print("isa BasePart:", kb:IsA("BasePart"))
print("isa Instance:", kb:IsA("Instance"))
print("isa Sound:", kb:IsA("Sound"))
`,
expectLogs: [
{ level: 'info', text: 'isa Part:\ttrue' },
{ level: 'info', text: 'isa BasePart:\ttrue' },
{ level: 'info', text: 'isa Instance:\ttrue' },
{ level: 'info', text: 'isa Sound:\tfalse' },
],
},
];
async function runSnippet(snippet) {
const factory = new LuaFactory();
const lua = await factory.createEngine();
const logs = [];
const sent = [];
const send = (cmd, payload) => sent.push({ cmd, payload });
registerRobloxApi(lua, {
getSceneSnap: () => FAKE_SCENE_SNAP,
targetPrimitiveId: 2, // как будто скрипт прикреплён к KillBrick
send,
});
// Перехват print через send('log', ...)
let errMsg = null;
try {
await lua.doString(snippet.lua);
} catch (e) {
errMsg = e && e.message ? e.message : String(e);
}
lua.global.close();
const captured = sent.filter(s => s.cmd === 'log');
return { logs: captured.map(s => s.payload), partSets: sent.filter(s => s.cmd === 'partSet'), error: errMsg };
}
(async () => {
let passed = 0;
let failed = 0;
for (const s of SNIPPETS) {
const result = await runSnippet(s);
const ok = checkExpectations(s, result);
if (ok.success) {
console.log(`${s.name}`);
passed++;
} else {
console.log(`${s.name}`);
console.log(` error: ${result.error || 'none'}`);
console.log(` logs received:`);
for (const l of result.logs) console.log(` [${l.level}] ${JSON.stringify(l.text)}`);
if (result.partSets.length) {
console.log(` partSets:`, JSON.stringify(result.partSets));
}
console.log(` reason: ${ok.reason}`);
failed++;
}
}
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);
})();
function checkExpectations(snippet, result) {
if (result.error) {
return { success: false, reason: `lua error: ${result.error}` };
}
if (snippet.expectLogs) {
for (const exp of snippet.expectLogs) {
const found = result.logs.find(l => l.level === exp.level && l.text === exp.text);
if (!found) {
return { success: false, reason: `missing log: [${exp.level}] ${JSON.stringify(exp.text)}` };
}
}
}
if (snippet.expectPartSet) {
const found = result.partSets.find(s =>
s.payload.primId === snippet.expectPartSet.primId &&
s.payload.prop === snippet.expectPartSet.prop
);
if (!found) {
return { success: false, reason: `missing partSet ${JSON.stringify(snippet.expectPartSet)}` };
}
}
return { success: true };
}