From 901c249c297f3dc92e177f80b60885e4349b71b7 Mon Sep 17 00:00:00 2001
From: min
Date: Tue, 9 Jun 2026 22:11:15 +0300
Subject: [PATCH] =?UTF-8?q?docs(29)=20+=20feat(g31):=20=C2=AB=D0=97=D0=B0?=
=?UTF-8?q?=D1=89=D0=B8=D1=82=D0=B0=20=D0=B1=D0=B0=D0=B7=D1=8B=C2=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
g30 docs CodeBoth 4 скрипта.
g31:
- killed counter (GOAL=12) + leaked (MAX_LEAK=5)
- Heartbeat spawn враг каждые 2с в (random(-8,8), 1, 38),
spawn 'character-b' speed 2.5
- task.delay 0.3 npc.moveTo(0, 2) — к базе
- __rbxl_npc_on_click(ref, fn) → шлёт ref в общий BindableEvent
- При клике главный скрипт проверяет dist<5, наносит урон
- Каждые 0.4с проверка прорыва (z<4) → leaked++ + lose sound
- 12 убитых → 'Победа!' + confetti
- 5 прорывов → 'База разрушена!'
Shim: __rbxl_npc_moveto/__rbxl_npc_remove.
---
src/community/docsGamesBuildersLua.js | 124 +++++++++++++++++++++++++-
src/community/docsLessons.jsx | 16 ++--
src/editor/engine/lua/RobloxShim.js | 9 ++
3 files changed, 140 insertions(+), 9 deletions(-)
diff --git a/src/community/docsGamesBuildersLua.js b/src/community/docsGamesBuildersLua.js
index 84cfddd..8ab55dc 100644
--- a/src/community/docsGamesBuildersLua.js
+++ b/src/community/docsGamesBuildersLua.js
@@ -2734,7 +2734,129 @@ end)`,
},
// ═══════════════════════════════════════════════════════════════
- // ИГРЫ 31-50: явных Lua-версий пока нет.
+ // ИГРА 31 — «Защита базы»
+ // ═══════════════════════════════════════════════════════════════
+ 'base-defense': {
+ g31_main: `-- === ИГРА «ЗАЩИТА БАЗЫ» — главный скрипт (Lua) ===
+${SNIPPET_BROADCAST}
+
+local RunService = game:GetService("RunService")
+local UserInputService = game:GetService("UserInputService")
+
+local killed = 0
+local leaked = 0
+local total = 0
+local over = false
+local GOAL = 12
+local MAX_LEAK = 5
+
+__rbxl_score_set(0)
+__rbxl_show_text("Защити базу! Кликай по врагам", 3)
+
+local hitSound = Instance.new("Sound", workspace)
+hitSound.SoundId = "hit"; hitSound.Volume = 0.6
+local loseSound = Instance.new("Sound", workspace)
+loseSound.SoundId = "lose"; loseSound.Volume = 0.7
+local winSound = Instance.new("Sound", workspace)
+winSound.SoundId = "win"; winSound.Volume = 1
+
+-- Все живые враги: { ref, dead }
+local enemies = {}
+
+-- Клик по NPC (target.kind=npc) — наносим урон ближайшему в радиусе 5
+local clickEvent = getEvent("EnemyClicked")
+clickEvent.Event:Connect(function(localRef)
+ if over then return end
+ for _, e in ipairs(enemies) do
+ if not e.dead and e.ref == localRef then
+ -- Проверка расстояния (в радиусе 5)
+ local px = __rbxl_player_x()
+ local pz = __rbxl_player_z()
+ local ex = __rbxl_npc_x(e.ref)
+ local ez = __rbxl_npc_z(e.ref)
+ local dx = px - ex
+ local dz = pz - ez
+ local dist = math.sqrt(dx*dx + dz*dz)
+ if dist < 5 then
+ e.dead = true
+ __rbxl_spawn_particles("explosion", ex, 2, ez, 0.4, 1)
+ __rbxl_npc_remove(e.ref)
+ hitSound:Play()
+ killed = killed + 1
+ __rbxl_score_set(killed)
+ if killed >= GOAL and not over then
+ over = true
+ winSound:Play()
+ __rbxl_show_text("Победа! База защищена!", 5)
+ local px2 = __rbxl_player_x()
+ local py2 = __rbxl_player_y()
+ local pz2 = __rbxl_player_z()
+ __rbxl_spawn_particles("confetti", px2, py2 + 3, pz2, 3, 3)
+ end
+ end
+ return
+ end
+ end
+end)
+
+-- Регистрируем общий callback на клик по NPC — он шлёт ref в общий event
+-- (фейерим один раз — при появлении каждого врага зовём __rbxl_npc_on_click)
+
+-- Спавн врага каждые 2 секунды
+local spawnTimer = 0
+RunService.Heartbeat:Connect(function(dt)
+ if over then return end
+ if total >= GOAL + MAX_LEAK then return end
+ spawnTimer = spawnTimer + dt
+ if spawnTimer >= 2 then
+ spawnTimer = 0
+ total = total + 1
+ local x = math.random(-8, 8)
+ local ref = __rbxl_spawn_npc("character-b", x, 1, 38, "Враг", 30, 2.5)
+ local e = { ref = ref, dead = false }
+ table.insert(enemies, e)
+ -- Отложим moveTo пока NPC создастся
+ task.delay(0.3, function()
+ __rbxl_npc_moveto(ref, 0, 2)
+ end)
+ -- Клик по этому NPC → шлём в общий event
+ __rbxl_npc_on_click(ref, function()
+ local ev = game:GetService("ReplicatedStorage"):FindFirstChild("EnemyClicked")
+ if ev then ev:Fire(ref) end
+ end)
+ end
+end)
+
+-- Проверка прорыва каждые 0.4с
+local leakTimer = 0
+RunService.Heartbeat:Connect(function(dt)
+ if over then return end
+ leakTimer = leakTimer + dt
+ if leakTimer < 0.4 then return end
+ leakTimer = 0
+ for _, e in ipairs(enemies) do
+ if not e.dead then
+ local ez = __rbxl_npc_z(e.ref)
+ local ex = __rbxl_npc_x(e.ref)
+ -- ez=0 ex=0 пока NPC не зарезолвлен — пропускаем
+ if not (ex == 0 and ez == 0) and ez < 4 then
+ e.dead = true
+ __rbxl_npc_remove(e.ref)
+ leaked = leaked + 1
+ loseSound:Play()
+ __rbxl_show_text("Враг прорвался! (" .. leaked .. "/" .. MAX_LEAK .. ")", 2)
+ if leaked >= MAX_LEAK and not over then
+ over = true
+ __rbxl_show_text("База разрушена! Поражение.", 5)
+ end
+ end
+ end
+ end
+end)`,
+ },
+
+ // ═══════════════════════════════════════════════════════════════
+ // ИГРЫ 32-50: явных Lua-версий пока нет.
// buildGameProject в docsGamesBuilders.js использует generateFallbackLua
// (главный скрипт → показ подсказки + слушает FinishReached →
// победа+конфетти; скрипт на финиш-примитиве → шлёт FinishReached;
diff --git a/src/community/docsLessons.jsx b/src/community/docsLessons.jsx
index 80ff32e..bdb1036 100644
--- a/src/community/docsLessons.jsx
+++ b/src/community/docsLessons.jsx
@@ -4197,7 +4197,7 @@ game.self.onTouch(() => {
по заданиям.
- {`// === ИГРА «КВЕСТ С ЗАДАНИЯМИ» — главный скрипт ===
+ {`// === ИГРА «КВЕСТ С ЗАДАНИЯМИ» — главный скрипт ===
// этап квеста: 0=не начат, 1=собрать монетку, 2=дойти до флага,
// 3=вернуться к NPC, 4=готово
@@ -4245,7 +4245,7 @@ game.onMessage('flag-done', () => {
stage = 3;
game.sound.play('pickup');
game.ui.showText('Квест: вернись к квестодателю', 3);
-});`}
+});`}
Главное здесь — переменная stage:
stage хранит, на каком шаге квеста
@@ -4275,23 +4275,23 @@ game.onMessage('flag-done', () => {
Шаг 3. Скрипт квестодателя
- {`// === Скрипт квестодателя ===
+ {`// === Скрипт квестодателя ===
game.self.onInteract(() => {
game.broadcast('talk');
-}, { text: 'Поговорить', distance: 4 });`}
+}, { text: 'Поговорить', distance: 4 });`}
Шаг 4. Скрипты монетки и флага
- {`// === Скрипт квест-монетки ===
+ {`// === Скрипт квест-монетки ===
game.self.onTouch(() => {
game.broadcast('coin-done');
game.self.delete();
-});`}
+});`}
- {`// === Скрипт квест-флага ===
+ {`// === Скрипт квест-флага ===
game.self.onTouch(() => {
game.broadcast('flag-done');
-});`}
+});`}
Шаг 5. Проверка
diff --git a/src/editor/engine/lua/RobloxShim.js b/src/editor/engine/lua/RobloxShim.js
index 7f9a77f..9ed5698 100644
--- a/src/editor/engine/lua/RobloxShim.js
+++ b/src/editor/engine/lua/RobloxShim.js
@@ -1878,6 +1878,15 @@ export function registerRobloxShim(lua, opts) {
global.set('__rbxl_npc_stop', (ref) => {
send('npc.stop', { ref: String(ref || '') });
});
+ global.set('__rbxl_npc_moveto', (ref, x, z) => {
+ send('npc.moveTo', {
+ ref: String(ref || ''),
+ x: +x || 0, z: +z || 0,
+ });
+ });
+ global.set('__rbxl_npc_remove', (ref) => {
+ send('npc.remove', { ref: String(ref || '') });
+ });
// Позиция NPC — резолвится через GameRuntime по локальному ref.
// GameRuntime в tick шлёт api.updateNpcPos(localRef, x, y, z).
const _npcPositions = new Map(); // localRef → {x,y,z}