From 52724ab9c8cf6c3ba241d4316778c9b1e2e6e855 Mon Sep 17 00:00:00 2001 From: min Date: Mon, 8 Jun 2026 14:08:09 +0300 Subject: [PATCH] =?UTF-8?q?fix(rbxl):=20invUI=20=D0=B2=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=20inventory=20+=20Day/Night=20=D0=BF=D0=BE=D1=80?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=20+=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BA=D1=80=D1=8B=20Sparkles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Что чинит: 1. _registerRbxlTool падал: использовал scene3d.inventory (InventoryManager, старый, без defineItem). Меняю на scene3d.invUI (новый InventoryUI с defineItem) — теперь hotbar реально заполняется. 2. Lighting.SetMinutesAfterMidnight теперь шлёт lightingTimeUpdate в GameRuntime → scene3d.setTimeOfDay(hour). Тротлинг 4 раза/сек. Roblox Day&Night скрипт теперь визуально меняет небо в нашем плеере! 3. Instance.new('Sparkles') шлёт particleCreated в GameRuntime. При эквипе Tool — _startRbxlToolParticles() запускает каждые 200мс burst у позиции игрока (имитация искр из руки). 4. Авто-эквип первого Tool через 100мс после регистрации — юзеру не нужно нажимать 1, инвентарь не очевиден. 5. stop() корректно гасит интервалы и сбрасывает state. Эти 4 фикса должны дать Zapper-демке базовое визуальное поведение: видный hotbar, искры из персонажа, плавная смена дня/ночи. --- src/editor/engine/GameRuntime.js | 81 ++++++++++++++++++++++++++++- src/editor/engine/lua/RobloxShim.js | 9 ++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/editor/engine/GameRuntime.js b/src/editor/engine/GameRuntime.js index c6fad87..273ed31 100644 --- a/src/editor/engine/GameRuntime.js +++ b/src/editor/engine/GameRuntime.js @@ -211,6 +211,24 @@ export class GameRuntime { try { this._registerRbxlTool(payload); } catch (e) { console.warn('[GameRuntime] toolRegistered failed', e); } + } else if (cmd === 'lightingTimeUpdate') { + // Roblox Lighting:SetMinutesAfterMidnight → Babylon небо. + // Скрипты делают это каждый кадр — троттлим до 4 раз/сек. + const now = performance.now(); + if (!this._lastLightUpdate || now - this._lastLightUpdate > 250) { + this._lastLightUpdate = now; + try { + const hour = Number(payload?.hour); + if (hour >= 0 && hour < 24) { + this.scene3d?.setTimeOfDay?.(hour); + } + } catch (_) {} + } + } else if (cmd === 'particleCreated') { + // Roblox Instance.new('Sparkles') — запомнили какие + // partlcle-эффекты есть у Tool. При equip покажем у руки. + this._rbxlPendingParticles = this._rbxlPendingParticles || []; + this._rbxlPendingParticles.push(payload); } else { this._handleCommand(null, cmd, payload); } @@ -545,8 +563,12 @@ export class GameRuntime { * Слушает клики ЛКМ → шлёт mouseButton1Down (Tool.Activated fires там). */ _registerRbxlTool(payload) { if (!payload || payload.index == null) return; - const invUI = this.scene3d?.inventory; - if (!invUI) return; + // invUI — это новая drag-drop система с defineItem, а не inventory (старая) + const invUI = this.scene3d?.invUI; + if (!invUI || typeof invUI.defineItem !== 'function') { + console.warn('[GameRuntime] invUI not available for tool', payload); + return; + } const itemId = `rbxlTool_${payload.index}`; const toolName = String(payload.name || `Tool ${payload.index}`); invUI.defineItem({ @@ -565,6 +587,19 @@ export class GameRuntime { if (!this._rbxlToolHooks) { this._rbxlToolHooks = true; this._rbxlActiveSlot = -1; + // Авто-эквип первого Tool сразу при регистрации — иначе юзер + // не понимает что нажимать. В Roblox StarterPack тоже сразу + // в Backpack попадает и юзер жмёт 1 для эквипа. + setTimeout(() => { + if (this._rbxlActiveSlot < 0) { + invUI.setActiveHotbar?.(slot); + const sb = this._luaUserSandbox; + sb?.sendGlobalEvent?.({ type: 'equipTool', index: payload.index }); + this._rbxlActiveSlot = slot; + // Если у Tool были Sparkles — рисуем непрерывно у руки игрока + this._startRbxlToolParticles(); + } + }, 100); invUI.on('slot', () => { const sl = invUI.active; const item = invUI.hotbar[sl]; @@ -574,9 +609,11 @@ export class GameRuntime { const idx = +item.itemId.slice('rbxlTool_'.length); sb.sendGlobalEvent?.({ type: 'equipTool', index: idx }); this._rbxlActiveSlot = sl; + this._startRbxlToolParticles(); } else if (this._rbxlActiveSlot >= 0) { sb.sendGlobalEvent?.({ type: 'unequipTool' }); this._rbxlActiveSlot = -1; + this._stopRbxlToolParticles(); } }); // Клики мыши при экипированном Tool — Activated/mouseButton1Down @@ -602,6 +639,41 @@ export class GameRuntime { } } + /** Запускает непрерывный эмиттер Sparkles у руки игрока, пока Tool экипирован. */ + _startRbxlToolParticles() { + if (this._rbxlSparkInterval) return; + const particles = this._rbxlPendingParticles || []; + if (particles.length === 0) return; + // RayGun Color3.new(0,0,1) → #0000ff. Берём цвет первой партиклы. + const p0 = particles[0] || {}; + const col = p0.color || [0, 0, 1]; + const hexCol = '#' + [col[0], col[1], col[2]].map(c => { + const v = Math.max(0, Math.min(255, Math.round((Number(c) || 0) * 255))); + return v.toString(16).padStart(2, '0'); + }).join(''); + // Каждые 200мс — короткий burst у руки игрока (приблизительно) + this._rbxlSparkInterval = setInterval(() => { + try { + const pl = this.scene3d?.player; + if (!pl || !pl._pos) return; + this.scene3d?._spawnParticleEffect?.({ + type: 'sparks', + position: { x: pl._pos.x + 0.3, y: pl._pos.y + 0.4, z: pl._pos.z + 0.3 }, + color: hexCol, + duration: 0.4, + count: 0.5, + }); + } catch (_) {} + }, 200); + } + + _stopRbxlToolParticles() { + if (this._rbxlSparkInterval) { + clearInterval(this._rbxlSparkInterval); + this._rbxlSparkInterval = null; + } + } + /** Простой raycast от камеры — для mouse.Hit. */ _raycastFromCamera() { try { @@ -624,6 +696,11 @@ export class GameRuntime { console.log('[GameRuntime] stopping', this.sandboxes.length, 'sandboxes'); for (const sb of this.sandboxes) sb.stop(); } + // Останавливаем эффекты импортированных Tools + this._stopRbxlToolParticles?.(); + this._rbxlToolHooks = false; + this._rbxlActiveSlot = -1; + this._rbxlPendingParticles = null; // Удаляем все объекты, которые скрипты наспавнили через // game.scene.spawn/clone — иначе после Stop они остаются на сцене // и накапливаются при повторных запусках. diff --git a/src/editor/engine/lua/RobloxShim.js b/src/editor/engine/lua/RobloxShim.js index bb6b7af..5136f53 100644 --- a/src/editor/engine/lua/RobloxShim.js +++ b/src/editor/engine/lua/RobloxShim.js @@ -907,6 +907,8 @@ export function registerRobloxShim(lua, opts) { lighting.SetMinutesAfterMidnight = function (m) { lighting._minutes = (Number(m) || 0) % 1440; lighting.ClockTime = lighting._minutes / 60; + // Шлём в GameRuntime для обновления реального неба Babylon + send('lightingTimeUpdate', { hour: lighting.ClockTime }); }; lighting.GetMoonDirection = function () { return new RbxVector3(0, 1, 0); }; lighting.GetSunDirection = function () { return new RbxVector3(0, 1, 0); }; @@ -1283,6 +1285,13 @@ export function registerRobloxShim(lua, opts) { inst.Lifetime = { Min: 1, Max: 1 }; inst.Brightness = 1; inst.Range = 8; + inst.__particleKind = className.toLowerCase(); + // Шлём событие "создан particle-effect" — GameRuntime может его + // привязать к мешу на сцене (например, рукам игрока). + send('particleCreated', { + kind: inst.__particleKind, + color: [inst.Color.R, inst.Color.G, inst.Color.B], + }); } else if (className === 'Mouse') { inst = newInstance('Mouse', 'Mouse'); inst.Button1Down = makeSignal();