fix(rbxl): invUI вместо inventory + Day/Night портирован + искры Sparkles

Что чинит:
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, искры из персонажа, плавная смена дня/ночи.
This commit is contained in:
min 2026-06-08 14:08:09 +03:00
parent bb0726b4ad
commit 52724ab9c8
2 changed files with 88 additions and 2 deletions

View File

@ -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 они остаются на сцене
// и накапливаются при повторных запусках.

View File

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