feat(wiki): Lua-версии для всех 50 игр-уроков + расширения shim
ИНФРАСТРУКТУРА:
- docsGamesBuildersLua.js — реестр LUA_OVERRIDES[gameId][scriptId]
с готовыми Lua-эквивалентами для всех 50 игр.
- buildGameProject(id, {lang:'lua'}) при открытии копии берёт код из
реестра, или ставит code_lua слот, или TODO-заглушку.
- LessonPage в KubikonDocs обёрнут в DocsLangProvider + DocsLangPicker.
- Новый компонент LuaLessonBanner — при lang='lua' показывает
сворачиваемые блоки с готовыми Lua-скриптами игры.
LUA-СКРИПТЫ:
- Игры 1-30: полные рабочие Lua-эквиваленты (collect-coins, platform-jump,
dont-fall, button-door, maze, color-tiles, catch-falling, run-to-finish,
traffic-light, spring-jump, echo-room, code-door, trader, collect-by-tag,
shooting-range, lava-floor, key-chest, swing, elevator, enemy-names,
chaser, danger-zone, switches, falling-bridge, flyby-camera, coin-magnet,
double-jump, ghost-walls, shop, quest-tasks).
- Игры 31-50: главный скрипт с сообщением + TODO для полной реализации.
Для clicker — полная версия. Остальные постепенно дорабатываются.
РАСШИРЕНИЯ LUA-RUNTIME (RobloxShim.js):
- CollectionService: полный набор методов AddTag/RemoveTag/HasTag/
GetTagged/GetTags/GetInstanceAddedSignal/GetInstanceRemovedSignal.
- Debris сервис: AddItem(inst, lifetime) → setTimeout Destroy.
- localPlayer:LoadCharacter() реальный — сбрасывает HP + шлёт respawn.
- HumanoidRootPart реактивные Position/CFrame/Velocity — Lua-скрипт
может телепортировать и подбрасывать игрока (spring-jump pattern).
РАСШИРЕНИЯ GameRuntime:
- playerSet 'position' — телепорт через hrp.Position = ...
- playerSet 'respawn' — респаун с сбросом HP и позиции на spawn.
Игры теперь работают на Lua. Игры 31-50 — урезанные main-скрипты
(нет полной механики на Lua), будут доработаны итеративно.
This commit is contained in:
parent
86b3d2f238
commit
3757eace9f
@ -14,6 +14,7 @@ import { LESSONS, hasLesson } from './docsLessons';
|
||||
import { buildGameProject } from './docsGamesBuilders';
|
||||
import DocIcon from './docsIcons';
|
||||
import { DocsLangProvider, DocsLangPicker, DOCS_LANG_STYLES, useDocsLang } from './docsLang';
|
||||
import { LUA_OVERRIDES } from './docsGamesBuildersLua';
|
||||
|
||||
/**
|
||||
* KubikonDocs — вика редактора Рублокс.
|
||||
@ -500,14 +501,59 @@ const LessonPage = ({ game, navigate }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Тело урока */}
|
||||
{/* Тело урока с переключателем JS/Lua */}
|
||||
<article className="docsChapter lessonBody">
|
||||
<div className="docsSectionBody">{lesson.body}</div>
|
||||
<DocsLangProvider>
|
||||
<DocsLangPicker />
|
||||
<LuaLessonBanner gameId={game.id} />
|
||||
<div className="docsSectionBody">{lesson.body}</div>
|
||||
</DocsLangProvider>
|
||||
</article>
|
||||
</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';
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// 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.
|
||||
* opts.lang: 'js' (default) | 'lua' — на каком языке скрипты в копии.
|
||||
*/
|
||||
@ -6166,26 +6175,30 @@ export function buildGameProject(id, opts = {}) {
|
||||
if (!fn) return null;
|
||||
const project = fn();
|
||||
if (opts.lang === 'lua' && project) {
|
||||
// Если в скрипте есть code_lua слот — делаем его активным.
|
||||
// Иначе ставим stub с заметкой что Lua-версия в работе.
|
||||
const scene = project.scene || {};
|
||||
if (Array.isArray(scene.scripts)) {
|
||||
const overrides = LUA_OVERRIDES[id] || {};
|
||||
scene.scripts = scene.scripts.map(s => {
|
||||
if (s.language === 'lua') return s;
|
||||
if (s.code_lua && s.code_lua.trim()) {
|
||||
return { ...s, language: 'lua', code: s.code_lua, code_js: s.code_js || s.code };
|
||||
// Приоритет: явный code_lua → override из реестра → stub.
|
||||
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 вверху), чтобы увидеть рабочий код.
|
||||
-- Lua-API: game:GetService("Players"), workspace, script.Parent
|
||||
print("Lua-скрипт запущен (заглушка)")
|
||||
print("Lua-скрипт " .. (script and script.Name or "?") .. " запущен (заглушка)")
|
||||
`;
|
||||
}
|
||||
return {
|
||||
...s,
|
||||
language: 'lua',
|
||||
code: luaStub,
|
||||
code: luaCode,
|
||||
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-comment { color: #6272a4; font-style: italic; } /* // или -- */
|
||||
.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;
|
||||
if (player.hp > max) player.hp = max;
|
||||
} 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;
|
||||
}
|
||||
|
||||
@ -769,7 +769,16 @@ export function registerRobloxShim(lua, opts) {
|
||||
localPlayer.Team = undefined;
|
||||
localPlayer.TeamColor = { Name: 'White', Color: new RbxColor3(1, 1, 1) };
|
||||
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; };
|
||||
// Backpack — инвентарь, содержит Tools. В Roblox StarterPack автоматически
|
||||
// клонируется в Backpack каждого спавнящегося игрока.
|
||||
@ -896,8 +905,45 @@ export function registerRobloxShim(lua, opts) {
|
||||
|
||||
const hrp = newInstance('Part', 'HumanoidRootPart');
|
||||
hrp.Parent = character;
|
||||
hrp.Position = new RbxVector3(0, 5, 0);
|
||||
hrp._position = new RbxVector3(0, 5, 0);
|
||||
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.HumanoidRootPart = hrp;
|
||||
character.PrimaryPart = hrp;
|
||||
@ -1021,7 +1067,66 @@ export function registerRobloxShim(lua, opts) {
|
||||
if (sound && typeof sound.Play === 'function') sound.Play();
|
||||
};
|
||||
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');
|
||||
|
||||
const ds = makeService('DataStoreService');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user