feat: 50 игр на Lua + импорт Roblox для всех + поддержка Lua в плеере #39
@ -1720,26 +1720,73 @@ end)`,
|
|||||||
// ИГРА 20 — «Имена врагов»
|
// ИГРА 20 — «Имена врагов»
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
'enemy-names': {
|
'enemy-names': {
|
||||||
g20_main: `-- === ИГРА «ИМЕНА ВРАГОВ» (Lua) ===
|
g20_main: `-- === ИГРА «ИМЕНА НАД ВРАГАМИ» — главный скрипт (Lua) ===
|
||||||
local function addLabel(part, text, color)
|
local UserInputService = game:GetService("UserInputService")
|
||||||
local bb = Instance.new("BillboardGui", part)
|
|
||||||
bb.Size = UDim2.new(4, 0, 1, 0)
|
__rbxl_show_text("Победи всех врагов! Кликай по ним", 3)
|
||||||
bb.StudsOffset = Vector3.new(0, 2.5, 0)
|
|
||||||
bb.AlwaysOnTop = true
|
local hitSound = Instance.new("Sound", workspace)
|
||||||
local label = Instance.new("TextLabel", bb)
|
hitSound.SoundId = "hit"; hitSound.Volume = 0.6
|
||||||
label.Size = UDim2.new(1, 0, 1, 0)
|
local winSound = Instance.new("Sound", workspace)
|
||||||
label.BackgroundTransparency = 1
|
winSound.SoundId = "win"; winSound.Volume = 1
|
||||||
label.Text = text
|
|
||||||
label.TextColor3 = color or Color3.new(1, 1, 1)
|
-- Данные врагов: имя, позиция, HP
|
||||||
label.TextScaled = true
|
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
|
end
|
||||||
|
|
||||||
for _, child in ipairs(workspace:GetChildren()) do
|
-- Клик по сцене — бьём ближайшего врага в радиусе 4
|
||||||
if child:IsA("BasePart") and child.Name:match("^Враг") then
|
UserInputService.InputBegan:Connect(function(input, gp)
|
||||||
addLabel(child, child.Name, Color3.fromRGB(255, 80, 80))
|
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
|
||||||
end
|
end)`,
|
||||||
print("Над врагами появились имена")`,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@ -1558,6 +1558,30 @@ export class GameRuntime {
|
|||||||
const d = tryGet(this.scene3d?.modelManager);
|
const d = tryGet(this.scene3d?.modelManager);
|
||||||
if (d) return { kind: 'model', data: d };
|
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);
|
const um = tryGet(this.scene3d?.userModelManager);
|
||||||
if (um) return { kind: 'userModel', data: um };
|
if (um) return { kind: 'userModel', data: um };
|
||||||
return null;
|
return null;
|
||||||
@ -2125,6 +2149,13 @@ export class GameRuntime {
|
|||||||
// после spawnNpc (follow/moveTo/say) — они ждали
|
// после spawnNpc (follow/moveTo/say) — они ждали
|
||||||
// резолва ref в очереди.
|
// резолва ref в очереди.
|
||||||
this._flushPendingNpcCmds(payload.ref, npcId);
|
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, чтобы
|
// Сообщаем воркеру маппинг localRef → npcId, чтобы
|
||||||
// npc.onDeath по локальному ref находил правильного NPC.
|
// npc.onDeath по локальному ref находил правильного NPC.
|
||||||
|
|||||||
@ -1849,6 +1849,7 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
// Возвращает локальный ref (строку 'npc_lua_N'), который можно передавать
|
// Возвращает локальный ref (строку 'npc_lua_N'), который можно передавать
|
||||||
// в __rbxl_npc_say(ref, text, duration).
|
// в __rbxl_npc_say(ref, text, duration).
|
||||||
let _nextNpcRef = 0;
|
let _nextNpcRef = 0;
|
||||||
|
api._localToRealNpc = new Map();
|
||||||
global.set('__rbxl_spawn_npc', (modelType, x, y, z, name, hp, speed) => {
|
global.set('__rbxl_spawn_npc', (modelType, x, y, z, name, hp, speed) => {
|
||||||
const ref = 'npc_lua_' + (_nextNpcRef++);
|
const ref = 'npc_lua_' + (_nextNpcRef++);
|
||||||
send('npc.spawn', {
|
send('npc.spawn', {
|
||||||
@ -1868,6 +1869,33 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
duration: +duration || 3,
|
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).
|
// Инвентарь invUI — паритет с JS game.inventory.add(itemId, count).
|
||||||
// Сначала определяем итем (один раз), потом добавляем.
|
// Сначала определяем итем (один раз), потом добавляем.
|
||||||
const _localInventory = new Map();
|
const _localInventory = new Map();
|
||||||
@ -2214,6 +2242,25 @@ export function registerRobloxShim(lua, opts) {
|
|||||||
if (part && humanoid.Touched) humanoid.Touched.Fire(part);
|
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}
|
// GUI-клик: GameRuntime шлёт {type:'guiClick', localId, id}
|
||||||
if (p.type === 'guiClick') {
|
if (p.type === 'guiClick') {
|
||||||
const ref = p.localId || p.id;
|
const ref = p.localId || p.id;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user