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 140 additions and 9 deletions
Showing only changes of commit 901c249c29 - Show all commits

View File

@ -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 // buildGameProject в docsGamesBuilders.js использует generateFallbackLua
// (главный скрипт → показ подсказки + слушает FinishReached → // (главный скрипт → показ подсказки + слушает FinishReached →
// победа+конфетти; скрипт на финиш-примитиве → шлёт FinishReached; // победа+конфетти; скрипт на финиш-примитиве → шлёт FinishReached;

View File

@ -4197,7 +4197,7 @@ game.self.onTouch(() => {
по заданиям. по заданиям.
</p> </p>
<ScriptKind kind="global" /> <ScriptKind kind="global" />
<Code>{`// === ИГРА «КВЕСТ С ЗАДАНИЯМИ» — главный скрипт === <CodeBoth game="quest-tasks" script="g30_main">{`// === ИГРА «КВЕСТ С ЗАДАНИЯМИ» — главный скрипт ===
// этап квеста: 0=не начат, 1=собрать монетку, 2=дойти до флага, // этап квеста: 0=не начат, 1=собрать монетку, 2=дойти до флага,
// 3=вернуться к NPC, 4=готово // 3=вернуться к NPC, 4=готово
@ -4245,7 +4245,7 @@ game.onMessage('flag-done', () => {
stage = 3; stage = 3;
game.sound.play('pickup'); game.sound.play('pickup');
game.ui.showText('Квест: вернись к квестодателю', 3); game.ui.showText('Квест: вернись к квестодателю', 3);
});`}</Code> });`}</CodeBoth>
<p>Главное здесь переменная <code>stage</code>:</p> <p>Главное здесь переменная <code>stage</code>:</p>
<ul> <ul>
<li><code>stage</code> хранит, на каком шаге квеста <li><code>stage</code> хранит, на каком шаге квеста
@ -4275,23 +4275,23 @@ game.onMessage('flag-done', () => {
<h3 className="lessonH">Шаг 3. Скрипт квестодателя</h3> <h3 className="lessonH">Шаг 3. Скрипт квестодателя</h3>
<ScriptKind kind="object" on="тумбу квестодателя" /> <ScriptKind kind="object" on="тумбу квестодателя" />
<Code>{`// === Скрипт квестодателя === <CodeBoth game="quest-tasks" script="g30_npc">{`// === Скрипт квестодателя ===
game.self.onInteract(() => { game.self.onInteract(() => {
game.broadcast('talk'); game.broadcast('talk');
}, { text: 'Поговорить', distance: 4 });`}</Code> }, { text: 'Поговорить', distance: 4 });`}</CodeBoth>
<h3 className="lessonH">Шаг 4. Скрипты монетки и флага</h3> <h3 className="lessonH">Шаг 4. Скрипты монетки и флага</h3>
<ScriptKind kind="object" on="квест-монетку" /> <ScriptKind kind="object" on="квест-монетку" />
<Code>{`// === Скрипт квест-монетки === <CodeBoth game="quest-tasks" script="g30_coin">{`// === Скрипт квест-монетки ===
game.self.onTouch(() => { game.self.onTouch(() => {
game.broadcast('coin-done'); game.broadcast('coin-done');
game.self.delete(); game.self.delete();
});`}</Code> });`}</CodeBoth>
<ScriptKind kind="object" on="квест-флаг" /> <ScriptKind kind="object" on="квест-флаг" />
<Code>{`// === Скрипт квест-флага === <CodeBoth game="quest-tasks" script="g30_flag">{`// === Скрипт квест-флага ===
game.self.onTouch(() => { game.self.onTouch(() => {
game.broadcast('flag-done'); game.broadcast('flag-done');
});`}</Code> });`}</CodeBoth>
<h3 className="lessonH">Шаг 5. Проверка</h3> <h3 className="lessonH">Шаг 5. Проверка</h3>
<ul> <ul>

View File

@ -1878,6 +1878,15 @@ export function registerRobloxShim(lua, opts) {
global.set('__rbxl_npc_stop', (ref) => { global.set('__rbxl_npc_stop', (ref) => {
send('npc.stop', { ref: String(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. // Позиция NPC — резолвится через GameRuntime по локальному ref.
// GameRuntime в tick шлёт api.updateNpcPos(localRef, x, y, z). // GameRuntime в tick шлёт api.updateNpcPos(localRef, x, y, z).
const _npcPositions = new Map(); // localRef → {x,y,z} const _npcPositions = new Map(); // localRef → {x,y,z}