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();