feat: 50 игр на Lua + импорт Roblox для всех + поддержка Lua в плеере #39
@ -14,6 +14,7 @@ import { LESSONS, hasLesson } from './docsLessons';
|
|||||||
import { buildGameProject } from './docsGamesBuilders';
|
import { buildGameProject } from './docsGamesBuilders';
|
||||||
import DocIcon from './docsIcons';
|
import DocIcon from './docsIcons';
|
||||||
import { DocsLangProvider, DocsLangPicker, DOCS_LANG_STYLES, useDocsLang } from './docsLang';
|
import { DocsLangProvider, DocsLangPicker, DOCS_LANG_STYLES, useDocsLang } from './docsLang';
|
||||||
|
import { LUA_OVERRIDES } from './docsGamesBuildersLua';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KubikonDocs — вика редактора Рублокс.
|
* KubikonDocs — вика редактора Рублокс.
|
||||||
@ -500,14 +501,59 @@ const LessonPage = ({ game, navigate }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Тело урока */}
|
{/* Тело урока с переключателем JS/Lua */}
|
||||||
<article className="docsChapter lessonBody">
|
<article className="docsChapter lessonBody">
|
||||||
<div className="docsSectionBody">{lesson.body}</div>
|
<DocsLangProvider>
|
||||||
|
<DocsLangPicker />
|
||||||
|
<LuaLessonBanner gameId={game.id} />
|
||||||
|
<div className="docsSectionBody">{lesson.body}</div>
|
||||||
|
</DocsLangProvider>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// При выбранном Lua показывает плашку с готовыми Lua-скриптами для урока
|
||||||
|
// (если они есть в LUA_OVERRIDES). Скрипты ниже в основном теле остаются
|
||||||
|
// на JS как референс — Lua-версия здесь сверху для копирования.
|
||||||
|
const LuaLessonBanner = ({ gameId }) => {
|
||||||
|
const { lang } = useDocsLang();
|
||||||
|
if (lang !== 'lua') return null;
|
||||||
|
const overrides = LUA_OVERRIDES[gameId];
|
||||||
|
if (!overrides) {
|
||||||
|
return (
|
||||||
|
<div className="luaLessonBanner luaLessonBanner--missing">
|
||||||
|
<b>Lua-версия в работе.</b>
|
||||||
|
<p>
|
||||||
|
Для этого урока пока готова только JS-версия (показана ниже).
|
||||||
|
Если откроешь копию с языком Lua — получишь скрипт-заглушку
|
||||||
|
с подсказкой переключить язык в редакторе.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const entries = Object.entries(overrides);
|
||||||
|
return (
|
||||||
|
<div className="luaLessonBanner">
|
||||||
|
<div className="luaLessonBanner__head">
|
||||||
|
<b>Готовые Lua-скрипты для этой игры</b>
|
||||||
|
<span className="luaLessonBanner__hint">
|
||||||
|
Эти скрипты автоматически попадут в твою копию, если откроешь её на Lua.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{entries.map(([id, codeOrFn]) => {
|
||||||
|
const code = typeof codeOrFn === 'function' ? codeOrFn({ id }) : codeOrFn;
|
||||||
|
return (
|
||||||
|
<details key={id} className="luaLessonBanner__script">
|
||||||
|
<summary>{id}</summary>
|
||||||
|
<pre className="docCode" data-lang="lua"><code>{code}</code></pre>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════
|
||||||
// Модалка выбора языка скриптов при «Открыть копию»
|
// Модалка выбора языка скриптов при «Открыть копию»
|
||||||
// ══════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@ -6158,6 +6158,15 @@ export function hasGameBuilder(id) {
|
|||||||
return typeof GAME_BUILDERS[id] === 'function';
|
return typeof GAME_BUILDERS[id] === 'function';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
// LUA_OVERRIDES — реестр Lua-версий скриптов для уроков.
|
||||||
|
// Структура: { gameId: { scriptId: 'lua code' | (script) => 'lua code' } }
|
||||||
|
// Если скрипт описан здесь — при buildGameProject(id, {lang:'lua'}) его
|
||||||
|
// code будет заменён на Lua-версию.
|
||||||
|
// См. docsGamesBuildersLua.js для содержимого.
|
||||||
|
// ══════════════════════════════════════════════════════════════════
|
||||||
|
import { LUA_OVERRIDES } from './docsGamesBuildersLua';
|
||||||
|
|
||||||
/** Построить project_data для игры-урока. Возвращает объект или null.
|
/** Построить project_data для игры-урока. Возвращает объект или null.
|
||||||
* opts.lang: 'js' (default) | 'lua' — на каком языке скрипты в копии.
|
* opts.lang: 'js' (default) | 'lua' — на каком языке скрипты в копии.
|
||||||
*/
|
*/
|
||||||
@ -6166,26 +6175,30 @@ export function buildGameProject(id, opts = {}) {
|
|||||||
if (!fn) return null;
|
if (!fn) return null;
|
||||||
const project = fn();
|
const project = fn();
|
||||||
if (opts.lang === 'lua' && project) {
|
if (opts.lang === 'lua' && project) {
|
||||||
// Если в скрипте есть code_lua слот — делаем его активным.
|
|
||||||
// Иначе ставим stub с заметкой что Lua-версия в работе.
|
|
||||||
const scene = project.scene || {};
|
const scene = project.scene || {};
|
||||||
if (Array.isArray(scene.scripts)) {
|
if (Array.isArray(scene.scripts)) {
|
||||||
|
const overrides = LUA_OVERRIDES[id] || {};
|
||||||
scene.scripts = scene.scripts.map(s => {
|
scene.scripts = scene.scripts.map(s => {
|
||||||
if (s.language === 'lua') return s;
|
if (s.language === 'lua') return s;
|
||||||
if (s.code_lua && s.code_lua.trim()) {
|
// Приоритет: явный code_lua → override из реестра → stub.
|
||||||
return { ...s, language: 'lua', code: s.code_lua, code_js: s.code_js || s.code };
|
let luaCode = s.code_lua;
|
||||||
|
if (!luaCode) {
|
||||||
|
const ov = overrides[s.id];
|
||||||
|
if (typeof ov === 'function') luaCode = ov(s);
|
||||||
|
else if (typeof ov === 'string') luaCode = ov;
|
||||||
}
|
}
|
||||||
const luaStub = `-- TODO: Lua-версия этого скрипта пока не готова.
|
if (!luaCode || !luaCode.trim()) {
|
||||||
|
luaCode = `-- TODO: Lua-версия этого скрипта пока не готова.
|
||||||
-- Переключи язык на JS в редакторе (кнопка JS вверху), чтобы увидеть рабочий код.
|
-- Переключи язык на JS в редакторе (кнопка JS вверху), чтобы увидеть рабочий код.
|
||||||
-- Lua-API: game:GetService("Players"), workspace, script.Parent
|
print("Lua-скрипт " .. (script and script.Name or "?") .. " запущен (заглушка)")
|
||||||
print("Lua-скрипт запущен (заглушка)")
|
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...s,
|
...s,
|
||||||
language: 'lua',
|
language: 'lua',
|
||||||
code: luaStub,
|
code: luaCode,
|
||||||
code_js: s.code_js || s.code,
|
code_js: s.code_js || s.code,
|
||||||
code_lua: luaStub,
|
code_lua: luaCode,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
1024
src/community/docsGamesBuildersLua.js
Normal file
1024
src/community/docsGamesBuildersLua.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -419,4 +419,39 @@ export const DOCS_LANG_STYLES = `
|
|||||||
.docCode .hl-number { color: #bd93f9; } /* 42, 3.14 */
|
.docCode .hl-number { color: #bd93f9; } /* 42, 3.14 */
|
||||||
.docCode .hl-comment { color: #6272a4; font-style: italic; } /* // или -- */
|
.docCode .hl-comment { color: #6272a4; font-style: italic; } /* // или -- */
|
||||||
.docCode .hl-fn { color: #50fa7b; } /* myFunc() */
|
.docCode .hl-fn { color: #50fa7b; } /* myFunc() */
|
||||||
|
|
||||||
|
/* ══════════════════════════════════════════════════════════════════
|
||||||
|
Баннер «Lua-скрипты для урока»
|
||||||
|
══════════════════════════════════════════════════════════════════ */
|
||||||
|
.luaLessonBanner {
|
||||||
|
background: #eef4ff;
|
||||||
|
border: 1px solid #c7d8f5;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
margin: 14px 0 22px;
|
||||||
|
}
|
||||||
|
.luaLessonBanner--missing {
|
||||||
|
background: #fff7e0;
|
||||||
|
border-color: #f0d599;
|
||||||
|
color: #5a4500;
|
||||||
|
}
|
||||||
|
.luaLessonBanner--missing p { margin: 4px 0 0; font-size: 13px; }
|
||||||
|
.luaLessonBanner__head { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; }
|
||||||
|
.luaLessonBanner__head b { font-size: 14px; color: #1e3a8a; }
|
||||||
|
.luaLessonBanner__hint { font-size: 12px; color: #475569; font-style: italic; }
|
||||||
|
.luaLessonBanner__script { margin: 6px 0; }
|
||||||
|
.luaLessonBanner__script summary {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #d0dcf0;
|
||||||
|
font-family: Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #1e3a8a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.luaLessonBanner__script summary:hover { background: #f4f8ff; }
|
||||||
|
.luaLessonBanner__script[open] summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
|
||||||
|
.luaLessonBanner__script pre { margin: 0; border-top-left-radius: 0; border-top-right-radius: 0; }
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -4285,6 +4285,28 @@ export class GameRuntime {
|
|||||||
player.maxHp = max;
|
player.maxHp = max;
|
||||||
if (player.hp > max) player.hp = max;
|
if (player.hp > max) player.hp = max;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
} else if (payload.prop === 'position') {
|
||||||
|
// Lua-вызов hrp.Position = ... — телепорт игрока
|
||||||
|
try {
|
||||||
|
const v = payload.value || {};
|
||||||
|
if (player.body && player.body.position) {
|
||||||
|
player.body.position.set(v.x || 0, v.y || 0, v.z || 0);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
} else if (payload.prop === 'respawn') {
|
||||||
|
// Lua-вызов player:LoadCharacter() — телепорт к spawn и сброс HP
|
||||||
|
try {
|
||||||
|
if (typeof player.respawn === 'function') player.respawn();
|
||||||
|
else {
|
||||||
|
const sp = this.scene3d?.projectData?.scene?.spawnPoint
|
||||||
|
|| this.projectData?.scene?.spawnPoint
|
||||||
|
|| { x: 0, y: 5, z: 0 };
|
||||||
|
if (player.body && player.body.position) {
|
||||||
|
player.body.position.set(sp.x, sp.y, sp.z);
|
||||||
|
}
|
||||||
|
player.hp = player.maxHp || 100;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -769,7 +769,16 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
localPlayer.Team = undefined;
|
localPlayer.Team = undefined;
|
||||||
localPlayer.TeamColor = { Name: 'White', Color: new RbxColor3(1, 1, 1) };
|
localPlayer.TeamColor = { Name: 'White', Color: new RbxColor3(1, 1, 1) };
|
||||||
localPlayer.Kick = function () {};
|
localPlayer.Kick = function () {};
|
||||||
localPlayer.LoadCharacter = function () {};
|
localPlayer.LoadCharacter = function () {
|
||||||
|
// Респаун: возвращаем HP и шлём команду в плеер на телепорт к spawn.
|
||||||
|
// Plus сбрасываем humanoid.Health на MaxHealth.
|
||||||
|
try {
|
||||||
|
if (humanoid && humanoid.MaxHealth) {
|
||||||
|
humanoid.Health = humanoid.MaxHealth;
|
||||||
|
}
|
||||||
|
send('playerSet', { prop: 'respawn', value: true });
|
||||||
|
} catch (_) {}
|
||||||
|
};
|
||||||
localPlayer.HasAppearanceLoaded = function () { return true; };
|
localPlayer.HasAppearanceLoaded = function () { return true; };
|
||||||
// Backpack — инвентарь, содержит Tools. В Roblox StarterPack автоматически
|
// Backpack — инвентарь, содержит Tools. В Roblox StarterPack автоматически
|
||||||
// клонируется в Backpack каждого спавнящегося игрока.
|
// клонируется в Backpack каждого спавнящегося игрока.
|
||||||
@ -896,8 +905,45 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
|
|
||||||
const hrp = newInstance('Part', 'HumanoidRootPart');
|
const hrp = newInstance('Part', 'HumanoidRootPart');
|
||||||
hrp.Parent = character;
|
hrp.Parent = character;
|
||||||
hrp.Position = new RbxVector3(0, 5, 0);
|
hrp._position = new RbxVector3(0, 5, 0);
|
||||||
hrp.Size = new RbxVector3(2, 2, 1);
|
hrp.Size = new RbxVector3(2, 2, 1);
|
||||||
|
// Реактивные Position и Velocity — Lua скрипт может задавать.
|
||||||
|
Object.defineProperty(hrp, 'Position', {
|
||||||
|
get() { return hrp._position; },
|
||||||
|
set(v) {
|
||||||
|
if (!v) return;
|
||||||
|
hrp._position = new RbxVector3(v.X || 0, v.Y || 0, v.Z || 0);
|
||||||
|
try { send('playerSet', { prop: 'position',
|
||||||
|
value: { x: hrp._position.X, y: hrp._position.Y, z: hrp._position.Z } }); }
|
||||||
|
catch (_) {}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let _hrpCFrame = null;
|
||||||
|
Object.defineProperty(hrp, 'CFrame', {
|
||||||
|
get() { return _hrpCFrame || { Position: hrp._position, p: hrp._position }; },
|
||||||
|
set(v) {
|
||||||
|
if (!v) return;
|
||||||
|
_hrpCFrame = v;
|
||||||
|
const pos = v.Position || v.p || v;
|
||||||
|
if (pos && pos.X !== undefined) {
|
||||||
|
hrp._position = new RbxVector3(pos.X, pos.Y, pos.Z);
|
||||||
|
try { send('playerSet', { prop: 'position',
|
||||||
|
value: { x: pos.X, y: pos.Y, z: pos.Z } }); }
|
||||||
|
catch (_) {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Object.defineProperty(hrp, 'Velocity', {
|
||||||
|
get() { return hrp._velocity || new RbxVector3(0, 0, 0); },
|
||||||
|
set(v) {
|
||||||
|
if (!v) return;
|
||||||
|
hrp._velocity = new RbxVector3(v.X || 0, v.Y || 0, v.Z || 0);
|
||||||
|
if (v.Y > 10) {
|
||||||
|
try { send('playerSet', { prop: 'jumpVelocity', value: v.Y * 0.3 }); }
|
||||||
|
catch (_) {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
character.Children.push(hrp);
|
character.Children.push(hrp);
|
||||||
character.HumanoidRootPart = hrp;
|
character.HumanoidRootPart = hrp;
|
||||||
character.PrimaryPart = hrp;
|
character.PrimaryPart = hrp;
|
||||||
@ -1021,7 +1067,66 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
if (sound && typeof sound.Play === 'function') sound.Play();
|
if (sound && typeof sound.Play === 'function') sound.Play();
|
||||||
};
|
};
|
||||||
makeService('PathfindingService');
|
makeService('PathfindingService');
|
||||||
makeService('CollectionService');
|
|
||||||
|
// CollectionService — теги на инстансах
|
||||||
|
const cs = makeService('CollectionService');
|
||||||
|
const tagMap = new Map(); // tag → Set<instance>
|
||||||
|
const instTags = new WeakMap(); // instance → Set<tag>
|
||||||
|
const tagAddSignals = new Map(); // tag → Signal (InstanceAddedSignal)
|
||||||
|
const tagRemoveSignals = new Map(); // tag → Signal (InstanceRemovedSignal)
|
||||||
|
cs.AddTag = function (inst, tag) {
|
||||||
|
if (!inst || !tag) return;
|
||||||
|
let set = tagMap.get(tag);
|
||||||
|
if (!set) { set = new Set(); tagMap.set(tag, set); }
|
||||||
|
if (set.has(inst)) return;
|
||||||
|
set.add(inst);
|
||||||
|
let tags = instTags.get(inst);
|
||||||
|
if (!tags) { tags = new Set(); instTags.set(inst, tags); }
|
||||||
|
tags.add(tag);
|
||||||
|
const sig = tagAddSignals.get(tag);
|
||||||
|
if (sig) try { sig.Fire(inst); } catch (_) {}
|
||||||
|
};
|
||||||
|
cs.RemoveTag = function (inst, tag) {
|
||||||
|
const set = tagMap.get(tag);
|
||||||
|
if (set) set.delete(inst);
|
||||||
|
const tags = instTags.get(inst);
|
||||||
|
if (tags) tags.delete(tag);
|
||||||
|
const sig = tagRemoveSignals.get(tag);
|
||||||
|
if (sig) try { sig.Fire(inst); } catch (_) {}
|
||||||
|
};
|
||||||
|
cs.HasTag = function (inst, tag) {
|
||||||
|
const set = tagMap.get(tag);
|
||||||
|
return !!(set && set.has(inst));
|
||||||
|
};
|
||||||
|
cs.GetTagged = function (tag) {
|
||||||
|
const set = tagMap.get(tag);
|
||||||
|
return set ? [...set] : [];
|
||||||
|
};
|
||||||
|
cs.GetTags = function (inst) {
|
||||||
|
const tags = instTags.get(inst);
|
||||||
|
return tags ? [...tags] : [];
|
||||||
|
};
|
||||||
|
cs.GetInstanceAddedSignal = function (tag) {
|
||||||
|
let sig = tagAddSignals.get(tag);
|
||||||
|
if (!sig) { sig = makeSignal(); tagAddSignals.set(tag, sig); }
|
||||||
|
return sig;
|
||||||
|
};
|
||||||
|
cs.GetInstanceRemovedSignal = function (tag) {
|
||||||
|
let sig = tagRemoveSignals.get(tag);
|
||||||
|
if (!sig) { sig = makeSignal(); tagRemoveSignals.set(tag, sig); }
|
||||||
|
return sig;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debris — удаление инстансов через N секунд
|
||||||
|
const debris = makeService('Debris');
|
||||||
|
debris.AddItem = function (inst, lifetime) {
|
||||||
|
if (!inst || typeof inst.Destroy !== 'function') return;
|
||||||
|
const t = Math.max(0, Number(lifetime) || 0);
|
||||||
|
setTimeout(() => {
|
||||||
|
try { inst.Destroy(); } catch (_) {}
|
||||||
|
}, t * 1000);
|
||||||
|
};
|
||||||
|
|
||||||
makeService('MarketplaceService');
|
makeService('MarketplaceService');
|
||||||
|
|
||||||
const ds = makeService('DataStoreService');
|
const ds = makeService('DataStoreService');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user