feat: 50 игр на Lua + импорт Roblox для всех + поддержка Lua в плеере #39

Merged
min merged 215 commits from feat/lua-50-games-bundle into main 2026-06-09 21:59:25 +00:00
3 changed files with 77 additions and 8 deletions
Showing only changes of commit 5342c079d1 - Show all commits

View File

@ -121,12 +121,11 @@ export class GameRuntime {
// скрипты (с маркером // @roblox-lua) теперь идут через ОДИН LuaSharedSandbox. // скрипты (с маркером // @roblox-lua) теперь идут через ОДИН LuaSharedSandbox.
// .rbxl-скрипты распаковываем из JS-комментария-обёртки в чистый Lua. // .rbxl-скрипты распаковываем из JS-комментария-обёртки в чистый Lua.
const luaUserBatch = []; const luaUserBatch = [];
// По умолчанию импортированные .rbxl-скрипты НЕ выполняются: // Импортированные .rbxl-скрипты выполняются по умолчанию (батчами по 20
// - типичная Roblox-карта = 500-2000 скриптов, многие используют // через setTimeout, чтобы не подвешивать UI). Падения скриптов изолированы
// DataStore/Tool/PlayerGui/UserInputService, которых у нас нет; // pcall'ом в shim — один битый скрипт не валит остальных.
// - даже с stub'ами сотни падений → tab подвисает; // Выключить можно через window.__RBXL_SKIP_IMPORTED = true.
// Юзер может включить через window.__RBXL_RUN_IMPORTED = true в консоли. const runImportedRbxl = !(typeof window !== 'undefined' && window.__RBXL_SKIP_IMPORTED === true);
const runImportedRbxl = typeof window !== 'undefined' && window.__RBXL_RUN_IMPORTED === true;
let rbxlSkipped = 0; let rbxlSkipped = 0;
for (const s of scripts) { for (const s of scripts) {
if (s && typeof s.code === 'string' && s.code.startsWith('// @roblox-lua')) { if (s && typeof s.code === 'string' && s.code.startsWith('// @roblox-lua')) {
@ -221,7 +220,7 @@ export class GameRuntime {
this._log('info', `Запущено Roblox-Lua скриптов (импортированных): ${rbxlImported}`); this._log('info', `Запущено Roblox-Lua скриптов (импортированных): ${rbxlImported}`);
} }
if (rbxlSkipped > 0) { if (rbxlSkipped > 0) {
this._log('info', `Импортированных .rbxl-скриптов пропущено: ${rbxlSkipped}. Включить: window.__RBXL_RUN_IMPORTED = true`); this._log('info', `Импортированных .rbxl-скриптов пропущено: ${rbxlSkipped}. Вернуть: убрать window.__RBXL_SKIP_IMPORTED`);
} }
if (luaWritten > 0) { if (luaWritten > 0) {
this._log('info', `Запущено Lua-скриптов юзера: ${luaWritten}`); this._log('info', `Запущено Lua-скриптов юзера: ${luaWritten}`);

View File

@ -109,10 +109,33 @@ export class LuaSharedSandbox {
this._isKickedOff = true; this._isKickedOff = true;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`[LuaSharedSandbox] kickoff: starting ${this._pendingScripts.length} scripts`); console.log(`[LuaSharedSandbox] kickoff: starting ${this._pendingScripts.length} scripts`);
for (const entry of this._pendingScripts) this._startSingleScript(entry); const pending = this._pendingScripts;
this._pendingScripts = []; this._pendingScripts = [];
// Запускаем main-loop сразу — он начнёт tick'ать как только будут coroutines.
this._lastTickAt = performance.now(); this._lastTickAt = performance.now();
this._startMainLoop(); this._startMainLoop();
// Init батчами по 20 с yield между ними, чтобы UI не подвисал на 700+ скриптах.
const BATCH_SIZE = 20;
let idx = 0;
const initBatch = () => {
if (this._isStopped) return;
const end = Math.min(idx + BATCH_SIZE, pending.length);
for (let i = idx; i < end; i++) {
try { this._startSingleScript(pending[i]); }
catch (e) {
// eslint-disable-next-line no-console
console.error('[LuaSharedSandbox] init batch err:', e);
}
}
idx = end;
if (idx < pending.length) {
setTimeout(initBatch, 0);
} else {
// eslint-disable-next-line no-console
console.log(`[LuaSharedSandbox] all ${pending.length} scripts kicked off`);
}
};
setTimeout(initBatch, 0);
} }
_startSingleScript(entry) { _startSingleScript(entry) {

View File

@ -541,6 +541,53 @@ export function registerRobloxShim(lua, opts) {
HumanoidStateType: mkE(['Running','Jumping','Freefall','Landed','Dead','Climbing','Swimming','Seated']), HumanoidStateType: mkE(['Running','Jumping','Freefall','Landed','Dead','Climbing','Swimming','Seated']),
EasingStyle: mkE(['Linear','Sine','Quad','Cubic','Quart','Quint','Bounce','Elastic']), EasingStyle: mkE(['Linear','Sine','Quad','Cubic','Quart','Quint','Bounce','Elastic']),
EasingDirection: mkE(['In','Out','InOut']), EasingDirection: mkE(['In','Out','InOut']),
// Часто используемые в туториалах
InfoType: mkE(['Asset','BundleDetails','Subscription','GamePass','UserProductsInExperience']),
SortOrder: mkE(['Name','Custom','LayoutOrder']),
FillDirection: mkE(['Horizontal','Vertical']),
HorizontalAlignment: mkE(['Left','Center','Right']),
VerticalAlignment: mkE(['Top','Center','Bottom']),
Font: mkE(['Legacy','Arial','SourceSans','Code','Highway','SciFi','Cartoon','Gotham','GothamBold']),
TextXAlignment: mkE(['Left','Center','Right']),
TextYAlignment: mkE(['Top','Center','Bottom']),
ScaleType: mkE(['Stretch','Slice','Tile','Fit','Crop']),
AspectType: mkE(['FitWithinMaxSize','ScaleWithParentSize']),
DominantAxis: mkE(['Width','Height']),
BorderMode: mkE(['Outline','Middle','Inset']),
FormFactor: mkE(['Symmetric','Brick','Plate','Custom']),
PartType: mkE(['Ball','Block','Cylinder','Wedge','CornerWedge']),
SurfaceType: mkE(['Smooth','Glue','Weld','Studs','Inlet','Universal']),
ContextActionResult: mkE(['Pass','Sink']),
UserInputState: mkE(['Begin','Change','End','Cancel','None']),
});
// TweenInfo — конструктор объекта с параметрами анимации
// Сигнатура: TweenInfo.new(time, easingStyle, easingDirection, repeatCount, reverses, delayTime)
global.set('TweenInfo', {
new(time, easingStyle, easingDirection, repeatCount, reverses, delayTime) {
return {
Time: time || 1,
EasingStyle: easingStyle,
EasingDirection: easingDirection,
RepeatCount: repeatCount || 0,
Reverses: !!reverses,
DelayTime: delayTime || 0,
};
},
});
// NumberSequence, ColorSequence — упрощённые конструкторы для GUI-эффектов
global.set('NumberSequence', {
new(...args) { return { Keypoints: [], __ns: true }; },
});
global.set('ColorSequence', {
new(...args) { return { Keypoints: [], __cs: true }; },
});
global.set('NumberRange', {
new(min, max) { return { Min: min, Max: max == null ? min : max }; },
});
global.set('Rect', {
new(minX, minY, maxX, maxY) { return { Min: { X: minX, Y: minY }, Max: { X: maxX, Y: maxY } }; },
}); });
// === print / warn === // === print / warn ===