From ea1308d5394445518fac0bfff8ecbcf7689c90c1 Mon Sep 17 00:00:00 2001 From: min Date: Tue, 9 Jun 2026 21:23:53 +0300 Subject: [PATCH] =?UTF-8?q?feat(g21):=20=D0=BF=D0=BE=D0=BB=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=BF=D0=B0=D1=80=D0=B8=D1=82=D0=B5=D1=82=20=C2=AB?= =?UTF-8?q?=D0=9F=D1=80=D0=B5=D1=81=D0=BB=D0=B5=D0=B4=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=C2=BB=20+=20npc.follow/stop/pos=20?= =?UTF-8?q?=D0=B2=20shim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JS: - spawnNpc 'Охотник' speed=4 follow('player') - onTick: dist(player,enemy) < 1.6 → respawn + 'Пойман!' + lose - onMessage 'win' → enemy.stop() + 'Победа!' + win + confetti - g21_finish: onTouch → broadcast 'win' Lua (паритет): - __rbxl_show_text + Sounds - __rbxl_spawn_npc('character-b', ..., 'Охотник', 100, 4) - __rbxl_npc_follow(ref, 'player') — велим NPC следовать за игроком - Heartbeat: __rbxl_npc_x/z для расстояния, при <1.6 → LoadCharacter + 'Пойман!' + lose Sound (с throttle 2с) - BindableEvent WinReached + g21_finish.Touched → ev:Fire - При победе: npc_stop + showText + win + confetti Shim хелперы: - __rbxl_npc_follow(ref, target='player') - __rbxl_npc_stop(ref) - __rbxl_npc_x/y/z(ref) — позиция NPC - api.updateNpcPos(localRef, x, y, z) — GameRuntime синкает каждый кадр GameRuntime.tick собирает позиции всех NPC из npcManager.npcs через _localToReal и шлёт sb.api.updateNpcPos. --- src/community/docsGamesBuildersLua.js | 80 +++++++++++++++++++++------ src/editor/engine/GameRuntime.js | 20 +++++++ src/editor/engine/lua/RobloxShim.js | 25 +++++++++ 3 files changed, 109 insertions(+), 16 deletions(-) diff --git a/src/community/docsGamesBuildersLua.js b/src/community/docsGamesBuildersLua.js index ef08d8c..b2e26fc 100644 --- a/src/community/docsGamesBuildersLua.js +++ b/src/community/docsGamesBuildersLua.js @@ -1784,28 +1784,76 @@ end`, // ИГРА 21 — «Догонялки» // ═══════════════════════════════════════════════════════════════ 'chaser': { - g21_main: `-- === ИГРА «ДОГОНЯЛКИ» (Lua) === + g21_main: `-- === ИГРА «ПРЕСЛЕДОВАТЕЛЬ» — главный скрипт (Lua) === +${SNIPPET_BROADCAST} + local Players = game:GetService("Players") local RunService = game:GetService("RunService") +local player = Players.LocalPlayer +local won = false -local enemy = workspace:WaitForChild("Догонщик") +__rbxl_show_text("Убегай от врага! Добеги до укрытия!", 3) -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 +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 + +-- Спавним NPC-преследователя (speed=4, follow за игроком) +local enemyRef = __rbxl_spawn_npc("character-b", 0, 1, -3, "Охотник", 100, 4) +-- Велим NPC следовать за игроком +__rbxl_npc_follow(enemyRef, "player") + +-- Каждый кадр проверяем — не догнал ли враг +local lastCaughtTime = 0 +RunService.Heartbeat:Connect(function() + if won then return end + local px = __rbxl_player_x() + local pz = __rbxl_player_z() + local ex = __rbxl_npc_x(enemyRef) + local ez = __rbxl_npc_z(enemyRef) + -- Если позиции NPC ещё не пришли (ex=0,ez=0 = до спавна) — пропускаем + if ex == 0 and ez == 0 then return end + local dx = px - ex + local dz = pz - ez + local dist = math.sqrt(dx*dx + dz*dz) + if dist < 1.6 then + local now = tick() + if now - lastCaughtTime > 2 then + lastCaughtTime = now + player:LoadCharacter() + __rbxl_show_text("Пойман! Беги снова!", 2) + loseSound:Play() + end end end) -print("Убегай от догонщика!")`, + +-- Финиш сообщает о победе +local winEvent = getEvent("WinReached") +winEvent.Event:Connect(function() + if won then return end + won = true + __rbxl_npc_stop(enemyRef) + winSound:Play() + __rbxl_show_text("Победа! Ты убежал от врага!", 5) + local px = __rbxl_player_x() + local py = __rbxl_player_y() + local pz = __rbxl_player_z() + __rbxl_spawn_particles("confetti", px, py + 3, pz, 3, 3) +end)`, + g21_finish: `-- === Скрипт финиша (Lua) === +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local part = script.Parent +local fired = false + +part.Touched:Connect(function(hit) + if fired then return end + local h = hit.Parent and hit.Parent:FindFirstChild("Humanoid") + if not h then return end + fired = true + local ev = ReplicatedStorage:FindFirstChild("WinReached") + if ev then ev:Fire() end +end)`, }, // ═══════════════════════════════════════════════════════════════ diff --git a/src/editor/engine/GameRuntime.js b/src/editor/engine/GameRuntime.js index 6f199fd..8bc1bbd 100644 --- a/src/editor/engine/GameRuntime.js +++ b/src/editor/engine/GameRuntime.js @@ -986,6 +986,20 @@ export class GameRuntime { } } } catch (_) {} + // Собираем позиции NPC для Lua-shim + const npcPositions = []; + try { + const nm = this.scene3d?.npcManager; + if (nm && nm.npcs && this._localToReal) { + // localRef ('npc_lua_N') → реальный 'npc:' → npc + for (const [localRef, realRef] of this._localToReal.entries()) { + if (typeof realRef !== 'string' || !realRef.startsWith('npc:')) continue; + const npcId = Number(realRef.slice(4)); + const npc = nm.npcs.get(npcId); + if (npc) npcPositions.push([localRef, npc.x, npc.y, npc.z]); + } + } + } catch (_) {} for (const sb of this.sandboxes) { // Обновляем реальную позицию игрока для Lua-shim if (realPos && sb.api?.updatePlayerPos) { @@ -997,6 +1011,12 @@ export class GameRuntime { try { sb.api.updateSpawnedPos(id, x, y, z); } catch (_) {} } } + // Синк позиций NPC + if (npcPositions.length > 0 && sb.api?.updateNpcPos) { + for (const [ref, x, y, z] of npcPositions) { + try { sb.api.updateNpcPos(ref, x, y, z); } catch (_) {} + } + } // Для скриптов с target — добавляем актуальную позицию self const stateForSb = sb.target ? { ...state, selfPosition: this._collectSelfPosition(sb.target) } diff --git a/src/editor/engine/lua/RobloxShim.js b/src/editor/engine/lua/RobloxShim.js index b10a99f..369c713 100644 --- a/src/editor/engine/lua/RobloxShim.js +++ b/src/editor/engine/lua/RobloxShim.js @@ -1869,6 +1869,27 @@ export function registerRobloxShim(lua, opts) { duration: +duration || 3, }); }); + global.set('__rbxl_npc_follow', (ref, targetRef) => { + send('npc.follow', { + ref: String(ref || ''), + target: String(targetRef || 'player'), + }); + }); + global.set('__rbxl_npc_stop', (ref) => { + send('npc.stop', { ref: String(ref || '') }); + }); + // Позиция NPC — резолвится через GameRuntime по локальному ref. + // GameRuntime в tick шлёт api.updateNpcPos(localRef, x, y, z). + const _npcPositions = new Map(); // localRef → {x,y,z} + global.set('__rbxl_npc_pos', (ref) => { + const p = _npcPositions.get(String(ref || '')); + if (!p) return { x: 0, y: 0, z: 0, ok: false }; + return { x: p.x, y: p.y, z: p.z, ok: true }; + }); + // Отдельные x/y/z — обходим wasmoon userdata-proxy. + global.set('__rbxl_npc_x', (ref) => (_npcPositions.get(String(ref || ''))?.x ?? 0)); + global.set('__rbxl_npc_y', (ref) => (_npcPositions.get(String(ref || ''))?.y ?? 0)); + global.set('__rbxl_npc_z', (ref) => (_npcPositions.get(String(ref || ''))?.z ?? 0)); global.set('__rbxl_npc_damage', (ref, amount) => { send('npc.damage', { ref: String(ref || ''), @@ -2034,6 +2055,10 @@ export function registerRobloxShim(lua, opts) { setNpcLocalRef(localRef, realRef) { _localToRealNpc.set(String(localRef), String(realRef)); }, + // GameRuntime каждый кадр обновляет позиции NPC для Lua-скриптов. + updateNpcPos(localRef, x, y, z) { + _npcPositions.set(String(localRef), { x: +x, y: +y, z: +z }); + }, onSceneSnapshot(snap) { try { const prims = snap?.primitives || [];