player/tests/rbxl-lua-integration.test.js
min a5e1558c2d
All checks were successful
CI / Lint (push) Successful in 54s
CI / Build (push) Successful in 1m30s
CI / Secret scan (push) Successful in 20s
CI / PR size check (push) Has been skipped
CI / Deploy to S1 + S2 (push) Successful in 2m56s
feat(player): ������������� �� ������� (Lua + JS-API + Roblox-������ + LoadingOverlay)
2026-06-09 22:01:51 +00: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);
})();