diff --git a/src/community/KubikonDocs.jsx b/src/community/KubikonDocs.jsx
index 4e0de2a..53fa92a 100644
--- a/src/community/KubikonDocs.jsx
+++ b/src/community/KubikonDocs.jsx
@@ -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 }) => {
)}
- {/* Тело урока */}
+ {/* Тело урока с переключателем JS/Lua */}
- {lesson.body}
+
+
+
+ {lesson.body}
+
);
};
+// При выбранном 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 (
+
+
Lua-версия в работе.
+
+ Для этого урока пока готова только JS-версия (показана ниже).
+ Если откроешь копию с языком Lua — получишь скрипт-заглушку
+ с подсказкой переключить язык в редакторе.
+
+
+ );
+ }
+ const entries = Object.entries(overrides);
+ return (
+
+
+ Готовые Lua-скрипты для этой игры
+
+ Эти скрипты автоматически попадут в твою копию, если откроешь её на Lua.
+
+
+ {entries.map(([id, codeOrFn]) => {
+ const code = typeof codeOrFn === 'function' ? codeOrFn({ id }) : codeOrFn;
+ return (
+
+ {id}
+ {code}
+
+ );
+ })}
+
+ );
+};
+
// ══════════════════════════════════════════════════════════════════
// Модалка выбора языка скриптов при «Открыть копию»
// ══════════════════════════════════════════════════════════════════
diff --git a/src/community/docsGamesBuilders.js b/src/community/docsGamesBuilders.js
index 77b2da3..25e8ba2 100644
--- a/src/community/docsGamesBuilders.js
+++ b/src/community/docsGamesBuilders.js
@@ -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,
};
});
}
diff --git a/src/community/docsGamesBuildersLua.js b/src/community/docsGamesBuildersLua.js
new file mode 100644
index 0000000..2d7e897
--- /dev/null
+++ b/src/community/docsGamesBuildersLua.js
@@ -0,0 +1,1024 @@
+/**
+ * docsGamesBuildersLua.js — Lua-эквиваленты скриптов для уроков.
+ *
+ * Структура: LUA_OVERRIDES[gameId][scriptId] = 'lua-code'
+ * Может быть строкой или функцией (script) => 'lua-code' для случаев,
+ * когда код зависит от target/name (например, имя примитива).
+ *
+ * Когда юзер нажимает «Открыть копию → Lua» в LessonPage,
+ * buildGameProject(id, {lang:'lua'}) подменяет JS-скрипт на Lua-версию
+ * отсюда. Геометрия (примитивы, блоки) остаётся той же — отличается
+ * только язык скриптов.
+ *
+ * Lua-код пишется в стандартном Roblox-стиле:
+ * game:GetService("Players"), workspace, Instance.new, Vector3, CFrame,
+ * :Connect, RunService.Heartbeat, BindableEvent через ReplicatedStorage.
+ *
+ * Конвенции:
+ * - target=null (глобальный JS-скрипт) → в Lua это просто script в workspace,
+ * общение через ReplicatedStorage:BindableEvent.
+ * - target.kind='primitive' (на объекте) → script лежит ВНУТРИ части,
+ * обращение к ней через script.Parent. Имя части совпадает с тем что
+ * в JS-builder указано (Монетка_N, Платформа_N и т.д.).
+ *
+ * Помощники общего назначения:
+ * getPlayerFromHit — извлекает Player из hit события Touched.
+ * getOrCreateEvent — общая BindableEvent в ReplicatedStorage для broadcast.
+ */
+
+// ══════════════════════════════════════════════════════════════════
+// Общие сниппеты — вставляются в начало многих скриптов.
+// ══════════════════════════════════════════════════════════════════
+const SNIPPET_BROADCAST = `local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local function getEvent(name)
+ local ev = ReplicatedStorage:FindFirstChild(name)
+ if not ev then
+ ev = Instance.new("BindableEvent")
+ ev.Name = name
+ ev.Parent = ReplicatedStorage
+ end
+ return ev
+end`;
+
+const SNIPPET_PLAYER_HIT = `local Players = game:GetService("Players")
+local function getPlayerFromHit(hit)
+ if not hit or not hit.Parent then return nil end
+ return Players:GetPlayerFromCharacter(hit.Parent)
+end`;
+
+export const LUA_OVERRIDES = {
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 1 — «Собери монетки»
+ // ═══════════════════════════════════════════════════════════════
+ 'collect-coins': {
+ g1_main: `-- === ИГРА «СОБЕРИ МОНЕТКИ» — главный скрипт (Lua) ===
+${SNIPPET_BROADCAST}
+
+local score = 0
+local TOTAL = 8
+
+-- Создаём leaderstats для отображения счёта в правом верхнем углу
+local Players = game:GetService("Players")
+Players.PlayerAdded:Connect(function(player)
+ local stats = Instance.new("Folder", player)
+ stats.Name = "leaderstats"
+ local coins = Instance.new("IntValue", stats)
+ coins.Name = "Монеты"
+ coins.Value = 0
+end)
+-- Для уже зашедшего LocalPlayer (Рублокс дублирует PlayerAdded при init):
+for _, p in ipairs(Players:GetPlayers()) do
+ if not p:FindFirstChild("leaderstats") then
+ local stats = Instance.new("Folder", p)
+ stats.Name = "leaderstats"
+ local coins = Instance.new("IntValue", stats); coins.Name = "Монеты"
+ end
+end
+
+print("Собери все монетки!")
+
+local coinEvent = getEvent("CoinCollected")
+coinEvent.Event:Connect(function()
+ score = score + 1
+ for _, p in ipairs(Players:GetPlayers()) do
+ local stats = p:FindFirstChild("leaderstats")
+ if stats and stats:FindFirstChild("Монеты") then
+ stats.Монеты.Value = score
+ end
+ end
+ print("Собрано: " .. score)
+ if score >= TOTAL then
+ print("Победа! Все монетки твои!")
+ end
+end)`,
+ // Скрипт каждой монетки — генератор по script-объекту
+ g1_coin_1: makeCoinScript(),
+ g1_coin_2: makeCoinScript(),
+ g1_coin_3: makeCoinScript(),
+ g1_coin_4: makeCoinScript(),
+ g1_coin_5: makeCoinScript(),
+ g1_coin_6: makeCoinScript(),
+ g1_coin_7: makeCoinScript(),
+ g1_coin_8: makeCoinScript(),
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 2 — «Прыгай по платформам»
+ // ═══════════════════════════════════════════════════════════════
+ 'platform-jump': {
+ g2_main: `-- === ИГРА «ПРЫГАЙ ПО ПЛАТФОРМАМ» — главный скрипт (Lua) ===
+local Players = game:GetService("Players")
+local RunService = game:GetService("RunService")
+
+print("Допрыгай до зелёного финиша!")
+
+-- Каждый кадр проверяем не упал ли игрок
+RunService.Heartbeat:Connect(function()
+ for _, player in ipairs(Players:GetPlayers()) do
+ local char = player.Character
+ if char then
+ local hrp = char:FindFirstChild("HumanoidRootPart")
+ if hrp and hrp.Position.Y < -10 then
+ -- Респаун
+ player:LoadCharacter()
+ print("Упал! Попробуй ещё раз")
+ end
+ end
+ end
+end)`,
+ g2_finish: `-- === Скрипт финишной платформы (Lua) ===
+local part = script.Parent
+
+part.Touched:Connect(function(hit)
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if not h then return end
+ print("ПОБЕДА! Ты допрыгал!")
+ -- Заморозить
+ h.WalkSpeed = 0
+ h.JumpPower = 0
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 3 — «Не упади» (платформа сужается)
+ // ═══════════════════════════════════════════════════════════════
+ 'dont-fall': {
+ g3_main: `-- === ИГРА «НЕ УПАДИ» — главный скрипт (Lua) ===
+local Players = game:GetService("Players")
+local RunService = game:GetService("RunService")
+
+print("Удержись на платформе как можно дольше!")
+
+local startTime = tick()
+local alive = true
+
+RunService.Heartbeat:Connect(function()
+ if not alive then return end
+ for _, player in ipairs(Players:GetPlayers()) do
+ local char = player.Character
+ if char then
+ local hrp = char:FindFirstChild("HumanoidRootPart")
+ if hrp and hrp.Position.Y < -5 then
+ alive = false
+ local elapsed = math.floor(tick() - startTime)
+ print("Ты упал! Продержался: " .. elapsed .. " сек")
+ task.delay(2, function()
+ player:LoadCharacter()
+ startTime = tick()
+ alive = true
+ end)
+ end
+ end
+ end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 4 — «Кнопка и дверь»
+ // ═══════════════════════════════════════════════════════════════
+ 'button-door': {
+ g4_main: `-- === ИГРА «КНОПКА И ДВЕРЬ» — главный скрипт (Lua) ===
+${SNIPPET_BROADCAST}
+print("Найди кнопку и открой дверь!")
+-- Глобальный канал для оповещения двери
+getEvent("DoorOpen")`,
+ g4_button: `-- === Скрипт кнопки (Lua) ===
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local part = script.Parent
+
+part.Touched:Connect(function(hit)
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if not h then return end
+ local ev = ReplicatedStorage:FindFirstChild("DoorOpen")
+ if ev then ev:Fire() end
+ print("Кнопка нажата!")
+ part.Color = Color3.fromRGB(100, 255, 100) -- зелёная
+end)`,
+ g4_door: `-- === Скрипт двери (Lua) ===
+local TweenService = game:GetService("TweenService")
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local door = script.Parent
+local startPos = door.Position
+
+local ev = ReplicatedStorage:WaitForChild("DoorOpen")
+ev.Event:Connect(function()
+ -- Поднимаем дверь вверх
+ local goal = { Position = startPos + Vector3.new(0, 5, 0) }
+ TweenService:Create(door, TweenInfo.new(1), goal):Play()
+ door.CanCollide = false
+ print("Дверь открыта!")
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 5 — «Лабиринт»
+ // ═══════════════════════════════════════════════════════════════
+ 'maze': {
+ g5_main: `-- === ИГРА «ЛАБИРИНТ» — главный скрипт (Lua) ===
+print("Найди выход из лабиринта!")`,
+ g5_finish: `-- === Финиш лабиринта (Lua) ===
+local part = script.Parent
+part.Touched:Connect(function(hit)
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if not h then return end
+ print("ПОБЕДА! Ты нашёл выход!")
+ h.WalkSpeed = 0
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 6 — «Угадай цвет»
+ // ═══════════════════════════════════════════════════════════════
+ 'color-tiles': {
+ g6_main: `-- === ИГРА «УГАДАЙ ЦВЕТ» — главный скрипт (Lua) ===
+${SNIPPET_BROADCAST}
+
+local colors = { "red", "green", "blue", "yellow" }
+local target = colors[math.random(1, #colors)]
+print("Встань на плитку цвета: " .. target)
+
+local ev = getEvent("TileStepped")
+ev.Event:Connect(function(color)
+ if color == target then
+ print("Верно! +1 очко")
+ target = colors[math.random(1, #colors)]
+ print("Теперь встань на: " .. target)
+ else
+ print("Неверно! Нужен " .. target)
+ end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 7 — «Ловишка предметов»
+ // ═══════════════════════════════════════════════════════════════
+ 'catch-falling': {
+ g7_main: `-- === ИГРА «ЛОВИШКА ПРЕДМЕТОВ» — главный скрипт (Lua) ===
+local Debris = game:GetService("Debris")
+
+local score = 0
+local function showScore()
+ print("Поймано: " .. score)
+end
+showScore()
+
+-- Каждую секунду создаём падающий предмет
+task.spawn(function()
+ while true do
+ task.wait(1)
+ local ball = Instance.new("Part")
+ ball.Shape = Enum.PartType.Ball
+ ball.Size = Vector3.new(1, 1, 1)
+ ball.Position = Vector3.new(math.random(-8, 8), 20, math.random(-8, 8))
+ ball.Color = Color3.fromRGB(255, 215, 0)
+ ball.Material = Enum.Material.Neon
+ ball.Anchored = false
+ ball.Parent = workspace
+ Debris:AddItem(ball, 10)
+
+ -- Скрипт на ловлю
+ ball.Touched:Connect(function(hit)
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if h and ball.Parent then
+ score = score + 1
+ showScore()
+ ball:Destroy()
+ end
+ end)
+ end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 8 — «Беги до финиша»
+ // ═══════════════════════════════════════════════════════════════
+ 'run-to-finish': {
+ g8_main: `-- === ИГРА «БЕГИ ДО ФИНИША» — главный скрипт (Lua) ===
+print("Беги к зелёной плите!")`,
+ g8_finish: `-- === Финишная плита (Lua) ===
+local part = script.Parent
+local won = false
+part.Touched:Connect(function(hit)
+ if won then return end
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if not h then return end
+ won = true
+ print("ПОБЕДА! Ты добежал!")
+ h.WalkSpeed = 0
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 9 — «Светофор»
+ // ═══════════════════════════════════════════════════════════════
+ 'traffic-light': {
+ g9_main: `-- === ИГРА «СВЕТОФОР» — главный скрипт (Lua) ===
+local Players = game:GetService("Players")
+local RunService = game:GetService("RunService")
+
+local isGreen = true
+print("ЗЕЛЁНЫЙ — беги!")
+
+-- Каждые 3-5 сек переключаем свет
+task.spawn(function()
+ while true do
+ task.wait(math.random(3, 5))
+ isGreen = not isGreen
+ if isGreen then
+ print("ЗЕЛЁНЫЙ — беги!")
+ else
+ print("КРАСНЫЙ — стой!")
+ end
+ end
+end)
+
+-- Следим за движением игрока во время красного
+local lastPos = {}
+RunService.Heartbeat:Connect(function()
+ if isGreen then return end
+ for _, player in ipairs(Players:GetPlayers()) do
+ local char = player.Character
+ local hrp = char and char:FindFirstChild("HumanoidRootPart")
+ if hrp then
+ local prev = lastPos[player]
+ if prev and (hrp.Position - prev).Magnitude > 0.5 then
+ print(player.Name .. " двигался на красный! Респаун")
+ player:LoadCharacter()
+ end
+ lastPos[player] = hrp.Position
+ end
+ end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 10 — «Прыжки на пружинах»
+ // ═══════════════════════════════════════════════════════════════
+ 'spring-jump': {
+ g10_main: `-- === ИГРА «ПРЫЖКИ НА ПРУЖИНАХ» — главный скрипт (Lua) ===
+print("Прыгай с пружины на пружину до финиша!")`,
+ g10_spring: `-- === Скрипт пружины (Lua) ===
+local part = script.Parent
+part.Touched:Connect(function(hit)
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ local hrp = hit.Parent and hit.Parent:FindFirstChild("HumanoidRootPart")
+ if h and hrp then
+ -- Подбрасываем игрока вверх
+ hrp.Velocity = Vector3.new(hrp.Velocity.X, 80, hrp.Velocity.Z)
+ end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 11 — «Эхо» (нажми кнопку → звук)
+ // ═══════════════════════════════════════════════════════════════
+ 'echo-room': {
+ g11_main: `-- === ИГРА «ЭХО» (Lua) ===
+print("Касайся блоков — они отвечают звуком!")`,
+ g11_block: `-- === Скрипт звукового блока (Lua) ===
+local part = script.Parent
+local lastTouch = 0
+part.Touched:Connect(function(hit)
+ local now = tick()
+ if now - lastTouch < 0.5 then return end
+ lastTouch = now
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if not h then return end
+ print("Блок " .. part.Name .. " звенит!")
+ part.Color = Color3.fromRGB(math.random(0,255), math.random(0,255), math.random(0,255))
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 12 — «Кодовая дверь»
+ // ═══════════════════════════════════════════════════════════════
+ 'code-door': {
+ g12_main: `-- === ИГРА «КОДОВАЯ ДВЕРЬ» — главный скрипт (Lua) ===
+${SNIPPET_BROADCAST}
+
+local correctCode = "1234"
+local currentInput = ""
+
+print("Введи код 1234 (касайся кнопок по порядку)")
+
+local ev = getEvent("CodeButton")
+ev.Event:Connect(function(digit)
+ currentInput = currentInput .. tostring(digit)
+ print("Ввод: " .. currentInput)
+ if #currentInput == 4 then
+ if currentInput == correctCode then
+ print("Верно! Дверь открывается")
+ local doorEv = getEvent("DoorOpen")
+ doorEv:Fire()
+ else
+ print("Неверный код, попробуй ещё")
+ end
+ currentInput = ""
+ end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 13 — «Торговец»
+ // ═══════════════════════════════════════════════════════════════
+ 'trader': {
+ g13_main: `-- === ИГРА «ТОРГОВЕЦ» (Lua) ===
+local Players = game:GetService("Players")
+
+Players.PlayerAdded:Connect(function(player)
+ local stats = Instance.new("Folder", player); stats.Name = "leaderstats"
+ local coins = Instance.new("IntValue", stats); coins.Name = "Монеты"; coins.Value = 10
+end)
+for _, p in ipairs(Players:GetPlayers()) do
+ if not p:FindFirstChild("leaderstats") then
+ local stats = Instance.new("Folder", p); stats.Name = "leaderstats"
+ local coins = Instance.new("IntValue", stats); coins.Name = "Монеты"; coins.Value = 10
+ end
+end
+print("У тебя 10 монет. Купи зелье у торговца!")`,
+ g13_npc: `-- === Скрипт торговца (Lua) ===
+local Players = game:GetService("Players")
+local part = script.Parent
+part.Touched:Connect(function(hit)
+ local player = Players:GetPlayerFromCharacter(hit.Parent)
+ if not player then return end
+ local stats = player:FindFirstChild("leaderstats")
+ if not stats then return end
+ if stats.Монеты.Value >= 5 then
+ stats.Монеты.Value = stats.Монеты.Value - 5
+ local h = hit.Parent:FindFirstChild("Humanoid")
+ if h then h.Health = math.min(h.MaxHealth, h.Health + 50) end
+ print("Купил зелье! +50 HP. Осталось монет: " .. stats.Монеты.Value)
+ else
+ print("Не хватает монет! Нужно 5")
+ end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 14 — «Собери по тегу»
+ // ═══════════════════════════════════════════════════════════════
+ 'collect-by-tag': {
+ g14_main: `-- === ИГРА «СОБЕРИ ПО ТЕГУ» (Lua) ===
+local CollectionService = game:GetService("CollectionService")
+${SNIPPET_BROADCAST}
+
+local total = #CollectionService:GetTagged("звезда")
+local collected = 0
+print("Собери все " .. total .. " звёзд!")
+
+local ev = getEvent("StarCollected")
+ev.Event:Connect(function()
+ collected = collected + 1
+ print("Собрано: " .. collected .. "/" .. total)
+ if collected >= total then
+ print("ПОБЕДА! Все звёзды собраны!")
+ end
+end)`,
+ g14_star: `-- === Скрипт звезды (Lua) ===
+local CollectionService = game:GetService("CollectionService")
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local part = script.Parent
+CollectionService:AddTag(part, "звезда")
+
+part.Touched:Connect(function(hit)
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if not h then return end
+ local ev = ReplicatedStorage:FindFirstChild("StarCollected")
+ if ev then ev:Fire() end
+ part:Destroy()
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 15 — «Тир»
+ // ═══════════════════════════════════════════════════════════════
+ 'shooting-range': {
+ g15_main: `-- === ИГРА «ТИР» (Lua) ===
+local UserInputService = game:GetService("UserInputService")
+local Players = game:GetService("Players")
+${SNIPPET_BROADCAST}
+
+local score = 0
+print("Стреляй ЛКМ по красным шарам!")
+
+local ev = getEvent("TargetHit")
+ev.Event:Connect(function()
+ score = score + 1
+ print("Попал! Очки: " .. score)
+end)
+
+-- ЛКМ — пускаем луч из камеры
+UserInputService.InputBegan:Connect(function(input, gp)
+ if gp then return end
+ if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end
+ local player = Players.LocalPlayer
+ local mouse = player:GetMouse()
+ local target = mouse.Target
+ if target and target:GetAttribute("IsTarget") then
+ local hitEv = workspace:FindFirstChild("TargetHit") or ev
+ ev:Fire()
+ target:Destroy()
+ end
+end)`,
+ g15_target: `-- === Скрипт мишени (Lua) ===
+local part = script.Parent
+part:SetAttribute("IsTarget", true)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 16 — «Лавовый пол»
+ // ═══════════════════════════════════════════════════════════════
+ 'lava-floor': {
+ g16_main: `-- === ИГРА «ЛАВОВЫЙ ПОЛ» (Lua) ===
+print("Прыгай по островкам, не упади в лаву!")`,
+ g16_lava: `-- === Скрипт лавы (Lua) ===
+local part = script.Parent
+part.Touched:Connect(function(hit)
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if h then h.Health = 0 end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 17 — «Ключ от сундука»
+ // ═══════════════════════════════════════════════════════════════
+ 'key-chest': {
+ g17_main: `-- === ИГРА «КЛЮЧ ОТ СУНДУКА» (Lua) ===
+local Players = game:GetService("Players")
+Players.PlayerAdded:Connect(function(player)
+ local stats = Instance.new("Folder", player); stats.Name = "leaderstats"
+ local key = Instance.new("BoolValue", stats); key.Name = "Ключ"
+end)
+for _, p in ipairs(Players:GetPlayers()) do
+ if not p:FindFirstChild("leaderstats") then
+ local stats = Instance.new("Folder", p); stats.Name = "leaderstats"
+ local key = Instance.new("BoolValue", stats); key.Name = "Ключ"
+ end
+end
+print("Найди ключ и открой сундук!")`,
+ g17_key: `-- === Скрипт ключа (Lua) ===
+local Players = game:GetService("Players")
+local part = script.Parent
+part.Touched:Connect(function(hit)
+ local player = Players:GetPlayerFromCharacter(hit.Parent)
+ if not player then return end
+ local stats = player:FindFirstChild("leaderstats")
+ if stats and stats:FindFirstChild("Ключ") then
+ stats.Ключ.Value = true
+ print("Подобрал ключ!")
+ part:Destroy()
+ end
+end)`,
+ g17_chest: `-- === Скрипт сундука (Lua) ===
+local Players = game:GetService("Players")
+local part = script.Parent
+part.Touched:Connect(function(hit)
+ local player = Players:GetPlayerFromCharacter(hit.Parent)
+ if not player then return end
+ local stats = player:FindFirstChild("leaderstats")
+ if stats and stats.Ключ and stats.Ключ.Value then
+ print("Сундук открыт! ПОБЕДА!")
+ part.Color = Color3.fromRGB(255, 215, 0)
+ else
+ print("Нужен ключ!")
+ end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 18 — «Качели»
+ // ═══════════════════════════════════════════════════════════════
+ 'swing': {
+ g18_main: `-- === ИГРА «КАЧЕЛИ» (Lua) ===
+local TweenService = game:GetService("TweenService")
+local swing = workspace:WaitForChild("Качели")
+local startPos = swing.Position
+
+-- Качаем туда-сюда бесконечно
+task.spawn(function()
+ while true do
+ local up = TweenService:Create(swing,
+ TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut),
+ { Position = startPos + Vector3.new(0, 0, 5) })
+ up:Play(); up.Completed:Wait()
+ local down = TweenService:Create(swing,
+ TweenInfo.new(1, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut),
+ { Position = startPos + Vector3.new(0, 0, -5) })
+ down:Play(); down.Completed:Wait()
+ end
+end)
+print("Запрыгни на качающуюся платформу!")`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 19 — «Лифт»
+ // ═══════════════════════════════════════════════════════════════
+ 'elevator': {
+ g19_main: `-- === ИГРА «ЛИФТ» (Lua) ===
+local TweenService = game:GetService("TweenService")
+local elevator = workspace:WaitForChild("Лифт")
+local startPos = elevator.Position
+local topPos = startPos + Vector3.new(0, 10, 0)
+
+local goingUp = true
+elevator.Touched:Connect(function(hit)
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if not h then return end
+ local goal = { Position = goingUp and topPos or startPos }
+ TweenService:Create(elevator, TweenInfo.new(3), goal):Play()
+ goingUp = not goingUp
+end)
+print("Встань на лифт — он повезёт тебя наверх!")`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 20 — «Имена врагов»
+ // ═══════════════════════════════════════════════════════════════
+ 'enemy-names': {
+ g20_main: `-- === ИГРА «ИМЕНА ВРАГОВ» (Lua) ===
+local function addLabel(part, text, color)
+ local bb = Instance.new("BillboardGui", part)
+ bb.Size = UDim2.new(4, 0, 1, 0)
+ bb.StudsOffset = Vector3.new(0, 2.5, 0)
+ bb.AlwaysOnTop = true
+ local label = Instance.new("TextLabel", bb)
+ label.Size = UDim2.new(1, 0, 1, 0)
+ label.BackgroundTransparency = 1
+ label.Text = text
+ label.TextColor3 = color or Color3.new(1, 1, 1)
+ label.TextScaled = true
+end
+
+for _, child in ipairs(workspace:GetChildren()) do
+ if child:IsA("BasePart") and child.Name:match("^Враг") then
+ addLabel(child, child.Name, Color3.fromRGB(255, 80, 80))
+ end
+end
+print("Над врагами появились имена")`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 21 — «Догонялки»
+ // ═══════════════════════════════════════════════════════════════
+ 'chaser': {
+ g21_main: `-- === ИГРА «ДОГОНЯЛКИ» (Lua) ===
+local Players = game:GetService("Players")
+local RunService = game:GetService("RunService")
+
+local enemy = workspace:WaitForChild("Догонщик")
+
+RunService.Heartbeat:Connect(function(dt)
+ local player = Players:GetPlayers()[1]
+ if not player or not player.Character then return end
+ local target = player.Character:FindFirstChild("HumanoidRootPart")
+ if not target then return end
+ -- Двигаемся в сторону игрока со скоростью 5
+ local dir = (target.Position - enemy.Position)
+ if dir.Magnitude > 1 then
+ enemy.Position = enemy.Position + dir.Unit * 5 * dt
+ else
+ -- Поймал
+ local h = player.Character:FindFirstChild("Humanoid")
+ if h then h:TakeDamage(10) end
+ end
+end)
+print("Убегай от догонщика!")`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 22 — «Опасная зона»
+ // ═══════════════════════════════════════════════════════════════
+ 'danger-zone': {
+ g22_main: `-- === ИГРА «ОПАСНАЯ ЗОНА» (Lua) ===
+print("Не стой в красной зоне!")`,
+ g22_zone: `-- === Скрипт опасной зоны (Lua) ===
+local part = script.Parent
+local insiders = {}
+
+part.Touched:Connect(function(hit)
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if h then insiders[h] = true end
+end)
+part.TouchEnded:Connect(function(hit)
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if h then insiders[h] = nil end
+end)
+
+-- Урон каждые 0.5 сек пока стоят
+while true do
+ task.wait(0.5)
+ for h in pairs(insiders) do
+ if h.Parent then h:TakeDamage(5) end
+ end
+end`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 23 — «3 переключателя»
+ // ═══════════════════════════════════════════════════════════════
+ 'switches': {
+ g23_main: `-- === ИГРА «3 ПЕРЕКЛЮЧАТЕЛЯ» (Lua) ===
+${SNIPPET_BROADCAST}
+
+local activated = {false, false, false}
+print("Активируй все 3 переключателя!")
+
+local ev = getEvent("SwitchToggled")
+ev.Event:Connect(function(idx)
+ activated[idx] = true
+ print("Переключатель " .. idx .. " активирован")
+ if activated[1] and activated[2] and activated[3] then
+ print("ПОБЕДА! Все 3 активированы!")
+ end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 24 — «Падающий мост»
+ // ═══════════════════════════════════════════════════════════════
+ 'falling-bridge': {
+ g24_main: `-- === ИГРА «ПАДАЮЩИЙ МОСТ» (Lua) ===
+print("Беги по мосту — плиты падают!")`,
+ g24_plate: `-- === Скрипт падающей плиты (Lua) ===
+local part = script.Parent
+local fell = false
+part.Touched:Connect(function(hit)
+ if fell then return end
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if not h then return end
+ fell = true
+ task.delay(0.5, function()
+ part.Anchored = false
+ part.CanCollide = false
+ game:GetService("Debris"):AddItem(part, 3)
+ end)
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 25 — «Облёт камеры»
+ // ═══════════════════════════════════════════════════════════════
+ 'flyby-camera': {
+ g25_main: `-- === ИГРА «ОБЛЁТ КАМЕРЫ» (Lua) ===
+local TweenService = game:GetService("TweenService")
+local camera = workspace.CurrentCamera
+camera.CameraType = Enum.CameraType.Scriptable
+
+local points = {
+ CFrame.new(Vector3.new(0, 20, -30), Vector3.new(0, 5, 0)),
+ CFrame.new(Vector3.new(20, 15, 0), Vector3.new(0, 5, 0)),
+ CFrame.new(Vector3.new(0, 25, 30), Vector3.new(0, 5, 0)),
+}
+
+for _, cf in ipairs(points) do
+ local tween = TweenService:Create(camera, TweenInfo.new(2), { CFrame = cf })
+ tween:Play(); tween.Completed:Wait()
+end
+
+camera.CameraType = Enum.CameraType.Custom
+print("Облёт окончен — теперь играй!")`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 26 — «Магнит монет»
+ // ═══════════════════════════════════════════════════════════════
+ 'coin-magnet': {
+ g26_main: `-- === ИГРА «МАГНИТ МОНЕТ» (Lua) ===
+local Players = game:GetService("Players")
+local RunService = game:GetService("RunService")
+local CollectionService = game:GetService("CollectionService")
+${SNIPPET_BROADCAST}
+
+local score = 0
+local ev = getEvent("CoinCollected")
+ev.Event:Connect(function()
+ score = score + 1
+ print("Собрано: " .. score)
+end)
+
+RunService.Heartbeat:Connect(function(dt)
+ local player = Players:GetPlayers()[1]
+ if not player or not player.Character then return end
+ local hrp = player.Character:FindFirstChild("HumanoidRootPart")
+ if not hrp then return end
+ for _, coin in ipairs(CollectionService:GetTagged("magnetcoin")) do
+ local dist = (coin.Position - hrp.Position).Magnitude
+ if dist < 8 then
+ coin.Position = coin.Position + (hrp.Position - coin.Position).Unit * 20 * dt
+ if dist < 1 then
+ ev:Fire()
+ coin:Destroy()
+ end
+ end
+ end
+end)
+print("Монетки сами летят к тебе!")`,
+ g26_coin: `-- === Скрипт магнит-монетки (Lua) ===
+local CollectionService = game:GetService("CollectionService")
+CollectionService:AddTag(script.Parent, "magnetcoin")`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 27 — «Двойной прыжок»
+ // ═══════════════════════════════════════════════════════════════
+ 'double-jump': {
+ g27_main: `-- === ИГРА «ДВОЙНОЙ ПРЫЖОК» (Lua) ===
+local Players = game:GetService("Players")
+local UserInputService = game:GetService("UserInputService")
+
+local function setupDoubleJump(player)
+ local jumpsLeft = 2
+ local char = player.Character or player.CharacterAdded:Wait()
+ local h = char:WaitForChild("Humanoid")
+
+ -- Восстанавливаем прыжки при касании земли
+ h.StateChanged:Connect(function(_, newState)
+ if newState == Enum.HumanoidStateType.Landed then
+ jumpsLeft = 2
+ end
+ end)
+
+ UserInputService.InputBegan:Connect(function(input, gp)
+ if gp then return end
+ if input.KeyCode == Enum.KeyCode.Space and jumpsLeft > 0 then
+ jumpsLeft = jumpsLeft - 1
+ if jumpsLeft == 1 then
+ local hrp = char:FindFirstChild("HumanoidRootPart")
+ if hrp then
+ hrp.Velocity = Vector3.new(hrp.Velocity.X, 50, hrp.Velocity.Z)
+ end
+ end
+ end
+ end)
+end
+
+Players.PlayerAdded:Connect(setupDoubleJump)
+for _, p in ipairs(Players:GetPlayers()) do setupDoubleJump(p) end
+print("Жми Space дважды — двойной прыжок!")`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 28 — «Призрачные стены»
+ // ═══════════════════════════════════════════════════════════════
+ 'ghost-walls': {
+ g28_main: `-- === ИГРА «ПРИЗРАЧНЫЕ СТЕНЫ» (Lua) ===
+print("Некоторые стены — призрачные. Найди проход!")`,
+ g28_ghost: `-- === Скрипт призрачной стены (Lua) ===
+local part = script.Parent
+part.CanCollide = false
+part.Transparency = 0.5`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 29 — «Магазин»
+ // ═══════════════════════════════════════════════════════════════
+ 'shop': {
+ g29_main: `-- === ИГРА «МАГАЗИН» (Lua) ===
+local Players = game:GetService("Players")
+Players.PlayerAdded:Connect(function(player)
+ local stats = Instance.new("Folder", player); stats.Name = "leaderstats"
+ local coins = Instance.new("IntValue", stats); coins.Name = "Монеты"; coins.Value = 20
+end)
+for _, p in ipairs(Players:GetPlayers()) do
+ if not p:FindFirstChild("leaderstats") then
+ local stats = Instance.new("Folder", p); stats.Name = "leaderstats"
+ local coins = Instance.new("IntValue", stats); coins.Name = "Монеты"; coins.Value = 20
+ end
+end
+print("У тебя 20 монет — купи что-нибудь!")`,
+ g29_item: `-- === Скрипт товара (Lua) ===
+local Players = game:GetService("Players")
+local part = script.Parent
+local price = part:GetAttribute("Price") or 5
+local bought = false
+
+part.Touched:Connect(function(hit)
+ if bought then return end
+ local player = Players:GetPlayerFromCharacter(hit.Parent)
+ if not player then return end
+ local stats = player:FindFirstChild("leaderstats")
+ if not stats then return end
+ if stats.Монеты.Value >= price then
+ stats.Монеты.Value = stats.Монеты.Value - price
+ bought = true
+ print("Куплено! Цена: " .. price)
+ part.Transparency = 0.7
+ else
+ print("Не хватает! Нужно " .. price .. " монет")
+ end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 30 — «Квесты»
+ // ═══════════════════════════════════════════════════════════════
+ 'quest-tasks': {
+ g30_main: `-- === ИГРА «КВЕСТЫ» (Lua) ===
+${SNIPPET_BROADCAST}
+
+local quests = {
+ { name = "Собери 5 ягод", goal = 5, current = 0 },
+ { name = "Победи врага", goal = 1, current = 0 },
+ { name = "Дойди до башни", goal = 1, current = 0 },
+}
+
+print("Квесты:")
+for i, q in ipairs(quests) do print(" " .. i .. ". " .. q.name) end
+
+local ev = getEvent("QuestProgress")
+ev.Event:Connect(function(idx, amount)
+ quests[idx].current = quests[idx].current + (amount or 1)
+ local q = quests[idx]
+ print(q.name .. ": " .. q.current .. "/" .. q.goal)
+ if q.current >= q.goal then
+ print("Квест выполнен: " .. q.name)
+ end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРА 31-50: упрощённые версии (главные скрипты)
+ // ═══════════════════════════════════════════════════════════════
+ 'base-defense': { g31_main: simpleMain("Защити базу от волн врагов!") },
+ 'lap-race': { g32_main: simpleMain("Проедь все круги первым!") },
+ 'boss-platformer': { g33_main: simpleMain("Победи босса прыжками на голову!") },
+ 'harvest': { g34_main: simpleMain("Собирай урожай, продавай в магазин!") },
+ 'hide-from-npc': { g35_main: simpleMain("Прячься от NPC — не попадайся!") },
+ 'box-puzzle': { g36_main: simpleMain("Двигай ящики на места!") },
+ 'obstacle-course': { g37_main: simpleMain("Пройди полосу препятствий!") },
+ 'music-game': { g38_main: simpleMain("Жми клавиши в ритм музыки!") },
+ 'tower-build': { g39_main: simpleMain("Построй самую высокую башню!") },
+ 'wave-survival': { g40_main: simpleMain("Выживай в волнах врагов!") },
+ 'adventure-platformer': { g41_main: simpleMain("Приключение — собирай артефакты!") },
+ 'rpg-village': { g42_main: simpleMain("Бегай по деревне, выполняй квесты!") },
+ 'obstacle-race': { g43_main: simpleMain("Гонка с препятствиями — финишируй!") },
+ 'tower-defense': { g44_main: simpleMain("Расставь башни — не пускай врагов!") },
+ 'arena-shooter': { g45_main: simpleMain("Стреляй по противникам на арене!") },
+ 'clicker': { g46_main: simpleClicker() },
+ 'escape-quest': { g47_main: simpleMain("Найди подсказки и выберись!") },
+ 'mp-tag': { g48_main: simpleMain("Поймай других игроков (мультиплеер)!") },
+ 'mp-race': { g49_main: simpleMain("Гонка на нескольких игроков!") },
+ 'make-your-own': { g50_main: simpleMain("Это твоя пустая площадка — твори!") },
+};
+
+// ══════════════════════════════════════════════════════════════════
+// Хелперы для генерации часто повторяющихся скриптов
+// ══════════════════════════════════════════════════════════════════
+
+/** Возвращает Lua-код скрипта монетки. */
+function makeCoinScript() {
+ return `-- === Скрипт монетки (Lua) ===
+local ReplicatedStorage = game:GetService("ReplicatedStorage")
+local part = script.Parent
+
+part.Touched:Connect(function(hit)
+ local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid")
+ if not h then return end
+ local ev = ReplicatedStorage:FindFirstChild("CoinCollected")
+ if ev then ev:Fire() end
+ part:Destroy()
+end)`;
+}
+
+/** Простой главный скрипт со стартовой подсказкой. */
+function simpleMain(message) {
+ return `-- === Главный скрипт (Lua) ===
+print("${message.replace(/"/g, '\\"')}")
+-- TODO: эта игра-урок ещё не имеет полной Lua-реализации.
+-- Переключи язык на JS в редакторе, чтобы увидеть рабочую механику.`;
+}
+
+/** Кликер. */
+function simpleClicker() {
+ return `-- === КЛИКЕР (Lua) ===
+local Players = game:GetService("Players")
+local UserInputService = game:GetService("UserInputService")
+
+Players.PlayerAdded:Connect(function(player)
+ local stats = Instance.new("Folder", player); stats.Name = "leaderstats"
+ local cnt = Instance.new("IntValue", stats); cnt.Name = "Клики"; cnt.Value = 0
+end)
+for _, p in ipairs(Players:GetPlayers()) do
+ if not p:FindFirstChild("leaderstats") then
+ local stats = Instance.new("Folder", p); stats.Name = "leaderstats"
+ local cnt = Instance.new("IntValue", stats); cnt.Name = "Клики"; cnt.Value = 0
+ end
+end
+
+local function onClick(player)
+ local stats = player:FindFirstChild("leaderstats")
+ if stats and stats:FindFirstChild("Клики") then
+ stats.Клики.Value = stats.Клики.Value + 1
+ end
+end
+
+UserInputService.InputBegan:Connect(function(input, gp)
+ if gp then return end
+ if input.UserInputType == Enum.UserInputType.MouseButton1 then
+ onClick(Players.LocalPlayer)
+ end
+end)
+print("Кликай ЛКМ — копи клики!")`;
+}
diff --git a/src/community/docsLang.jsx b/src/community/docsLang.jsx
index 394a4b3..a47fa98 100644
--- a/src/community/docsLang.jsx
+++ b/src/community/docsLang.jsx
@@ -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; }
`;
diff --git a/src/editor/engine/GameRuntime.js b/src/editor/engine/GameRuntime.js
index c189888..71d8985 100644
--- a/src/editor/engine/GameRuntime.js
+++ b/src/editor/engine/GameRuntime.js
@@ -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;
}
diff --git a/src/editor/engine/lua/RobloxShim.js b/src/editor/engine/lua/RobloxShim.js
index 9663929..22a8174 100644
--- a/src/editor/engine/lua/RobloxShim.js
+++ b/src/editor/engine/lua/RobloxShim.js
@@ -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
+ const instTags = new WeakMap(); // instance → Set
+ 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');