diff --git a/src/community/docsGamesBuildersLua.js b/src/community/docsGamesBuildersLua.js index 60b17da..a42d6f6 100644 --- a/src/community/docsGamesBuildersLua.js +++ b/src/community/docsGamesBuildersLua.js @@ -1720,26 +1720,73 @@ end)`, // ИГРА 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 + g20_main: `-- === ИГРА «ИМЕНА НАД ВРАГАМИ» — главный скрипт (Lua) === +local UserInputService = game:GetService("UserInputService") + +__rbxl_show_text("Победи всех врагов! Кликай по ним", 3) + +local hitSound = Instance.new("Sound", workspace) +hitSound.SoundId = "hit"; hitSound.Volume = 0.6 +local winSound = Instance.new("Sound", workspace) +winSound.SoundId = "win"; winSound.Volume = 1 + +-- Данные врагов: имя, позиция, HP +local enemies = { + { name = "Гоблин", x = -5, z = 3, hp = 60, maxHp = 60, ref = nil }, + { name = "Скелет", x = 4, z = 5, hp = 80, maxHp = 80, ref = nil }, + { name = "Орк", x = 0, z = 8, hp = 100, maxHp = 100, ref = nil }, +} + +local alive = #enemies +local won = false + +-- Спавним всех врагов и метки над ними +for i, e in ipairs(enemies) do + e.ref = __rbxl_spawn_npc("character-b", e.x, 1, e.z, e.name, e.hp, 0) + __rbxl_set_label(e.ref, e.name .. " HP: " .. e.hp, "#ff5555", 3) + -- Callback на смерть NPC + __rbxl_npc_on_death(e.ref, function() + if e._dead then return end + e._dead = true + __rbxl_clear_label(e.ref) + alive = alive - 1 + hitSound:Play() + if alive <= 0 and not won then + won = true + 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 + end) 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)) +-- Клик по сцене — бьём ближайшего врага в радиусе 4 +UserInputService.InputBegan:Connect(function(input, gp) + if gp then return end + if input.UserInputType ~= Enum.UserInputType.MouseButton1 then return end + if won then return end + local px = __rbxl_player_x() + local pz = __rbxl_player_z() + for _, e in ipairs(enemies) do + if not e._dead and e.hp > 0 then + local dx = e.x - px + local dz = e.z - pz + local dist = math.sqrt(dx*dx + dz*dz) + if dist < 4 then + e.hp = e.hp - 30 + if e.hp < 0 then e.hp = 0 end + __rbxl_npc_damage(e.ref, 30) + __rbxl_set_label(e.ref, e.name .. " HP: " .. e.hp, "#ff5555", 3) + __rbxl_spawn_particles("sparks", e.x, 2, e.z, 0.4, 1) + hitSound:Play() + break -- бьём только одного за клик + end + end end -end -print("Над врагами появились имена")`, +end)`, }, // ═══════════════════════════════════════════════════════════════ diff --git a/src/editor/engine/GameRuntime.js b/src/editor/engine/GameRuntime.js index a56b27d..b58be19 100644 --- a/src/editor/engine/GameRuntime.js +++ b/src/editor/engine/GameRuntime.js @@ -1558,6 +1558,30 @@ export class GameRuntime { const d = tryGet(this.scene3d?.modelManager); if (d) return { kind: 'model', data: d }; } + // NPC — для setLabel/clearLabel над NPC. + if (kind === 'npc' || kind == null) { + const nm = this.scene3d?.npcManager; + if (nm && nm.npcs) { + let npc = nm.npcs.get(rawId); + if (!npc) { + const n = Number(rawId); + if (Number.isFinite(n)) npc = nm.npcs.get(n); + } + if (npc) { + // Возвращаем npc в формате 'tween-target' с mesh-ссылкой. + return { + kind: 'npc', + data: { + mesh: npc.rootMesh || npc.mesh || npc.rootNode || npc, + rootMesh: npc.rootMesh || npc.rootNode, + x: npc.x ?? npc.position?.x ?? 0, + y: npc.y ?? npc.position?.y ?? 0, + z: npc.z ?? npc.position?.z ?? 0, + }, + }; + } + } + } const um = tryGet(this.scene3d?.userModelManager); if (um) return { kind: 'userModel', data: um }; return null; @@ -2125,6 +2149,13 @@ export class GameRuntime { // после spawnNpc (follow/moveTo/say) — они ждали // резолва ref в очереди. this._flushPendingNpcCmds(payload.ref, npcId); + // Также сообщаем Lua-sandbox-ам маппинг, чтобы + // npc.onDeath по локальному ref находил npcId. + for (const sb of this.sandboxes) { + if (sb.api?._localToRealNpc) { + try { sb.api._localToRealNpc.set(payload.ref, 'npc:' + npcId); } catch (_) {} + } + } } // Сообщаем воркеру маппинг localRef → npcId, чтобы // npc.onDeath по локальному ref находил правильного NPC. diff --git a/src/editor/engine/lua/RobloxShim.js b/src/editor/engine/lua/RobloxShim.js index 984e8ae..cd86def 100644 --- a/src/editor/engine/lua/RobloxShim.js +++ b/src/editor/engine/lua/RobloxShim.js @@ -1849,6 +1849,7 @@ export function registerRobloxShim(lua, opts) { // Возвращает локальный ref (строку 'npc_lua_N'), который можно передавать // в __rbxl_npc_say(ref, text, duration). let _nextNpcRef = 0; + api._localToRealNpc = new Map(); global.set('__rbxl_spawn_npc', (modelType, x, y, z, name, hp, speed) => { const ref = 'npc_lua_' + (_nextNpcRef++); send('npc.spawn', { @@ -1868,6 +1869,33 @@ export function registerRobloxShim(lua, opts) { duration: +duration || 3, }); }); + global.set('__rbxl_npc_damage', (ref, amount) => { + send('npc.damage', { + ref: String(ref || ''), + amount: +amount || 0, + }); + }); + // Метка с именем/HP над NPC или примитивом — паритет с JS scene.setLabel. + global.set('__rbxl_set_label', (ref, text, color, height) => { + send('scene.setLabel', { + ref: String(ref || ''), + text: String(text || ''), + opts: { + color: color || '#ff5555', + height: Number(height) || 3, + }, + }); + }); + global.set('__rbxl_clear_label', (ref) => { + send('scene.clearLabel', { ref: String(ref || '') }); + }); + // Регистрация коллбэка onDeath для NPC. GameRuntime шлёт globalEvent + // 'npcDeath' с {ref} при смерти. Shim фильтрует по ref и зовёт. + const _npcDeathCbs = new Map(); // ref → fn + global.set('__rbxl_npc_on_death', (ref, fn) => { + if (typeof fn === 'function') _npcDeathCbs.set(String(ref || ''), fn); + }); + api._npcDeathCbs = _npcDeathCbs; // Инвентарь invUI — паритет с JS game.inventory.add(itemId, count). // Сначала определяем итем (один раз), потом добавляем. const _localInventory = new Map(); @@ -2214,6 +2242,25 @@ export function registerRobloxShim(lua, opts) { if (part && humanoid.Touched) humanoid.Touched.Fire(part); } } + // NPC погиб — фейерим registered cb для конкретного локального ref. + if (p.type === 'npcDeath' && p.npcId != null) { + const realRef = 'npc:' + p.npcId; + // Ищем локальный ref по реальному + let localRef = null; + if (api._localToRealNpc) { + for (const [k, v] of api._localToRealNpc.entries()) { + if (v === realRef) { localRef = k; break; } + } + } + // Вызываем все cb с подходящим ref + if (_npcDeathCbs.size > 0) { + for (const [ref, fn] of _npcDeathCbs.entries()) { + if (ref === realRef || ref === localRef) { + _pendingHandlerQueue.push({ fn, args: [] }); + } + } + } + } // GUI-клик: GameRuntime шлёт {type:'guiClick', localId, id} if (p.type === 'guiClick') { const ref = p.localId || p.id;