feat(g20): полный паритет «Имена над врагами» + shim NPC API
JS:
- 3 NPC (Гоблин, Скелет, Орк) через spawnNpc('character-b')
- setLabel над каждым с HP, обновляется при уроне
- ЛКМ → бьём ближайшего врага в радиусе 4 (damage 30)
- onDeath → clearLabel + hit sound + при 0 врагов — победа+confetti
Расширения shim/runtime:
- __rbxl_spawn_npc уже было, добавил api._localToRealNpc Map
- __rbxl_npc_damage(ref, amount) → cmd 'npc.damage'
- __rbxl_set_label(ref, text, color, height) → cmd 'scene.setLabel'
- __rbxl_clear_label(ref) → cmd 'scene.clearLabel'
- __rbxl_npc_on_death(ref, fn) — регистрирует cb. shim слушает global
event 'npcDeath' (resolveTweenTarget теперь поддерживает kind='npc')
и зовёт зарегистрированные cb с подходящим ref (local или real).
- GameRuntime.npc.spawn.then синхронизирует _localToRealNpc в Lua-sb.
Lua-скрипт игры 20 (паритет):
- showText 'Победи всех врагов! Кликай по ним'
- 3 спавна с метками HP над головой
- UserInputService.InputBegan MouseButton1 → ближайший враг в r=4 → -30hp
- На смерть: clearLabel + при 0 — Победа + win Sound + confetti
This commit is contained in:
parent
3cdbbc5049
commit
8a3405e34a
@ -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
|
||||
print("Над врагами появились имена")`,
|
||||
end
|
||||
end)`,
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user