From 471af1cdeb24acb969119c11927a74f8b29c63e8 Mon Sep 17 00:00:00 2001 From: min Date: Fri, 5 Jun 2026 02:21:38 +0300 Subject: [PATCH] =?UTF-8?q?fix(studio):=20=D1=81=D1=83=D0=BD=D0=B4=D1=83?= =?UTF-8?q?=D0=BA=E2=86=92=D1=81=D1=87=D1=91=D1=82=D1=87=D0=B8=D0=BA=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=BD=D0=B5=D1=82=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=20broadcast,=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=82=D0=BE=D1=87=D0=BA=D0=B8=20=D1=81=D0=BF=D0=B0?= =?UTF-8?q?=D0=B2=D0=BD=D0=B0=20+=20=D1=84=D0=BE=D0=BB=D0=B1=D1=8D=D0=BA?= =?UTF-8?q?=200,0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Киты «Сундук» и «Счётчик монет» связаны через game.broadcast('coins',{add}) + game.onMessage('coins') — раньше каждый кит в своём worker, счётчик не обновлялся (был globalThis, не работает между воркерами). - Точку спавна теперь МОЖНО удалить: Delete (SelectionManager.deleteSelected обрабатывает type==='spawn' → scene.deleteSpawn) + ПКМ в дереве → контекст- меню «Навести камеру / Удалить точку спавна». - Если точка спавна удалена (_spawnEnabled=false) — игрок появляется в (0, поверхность+2, 0). Постановка новой точки (setSpawnAtCamera) возвращает. - spawnEnabled сериализуется в project_data. Co-Authored-By: Claude Opus 4.8 --- src/editor/HierarchyPanel.jsx | 21 +++++++++++++--- src/editor/KubikonEditor.jsx | 5 ++++ src/editor/engine/BabylonScene.js | 36 ++++++++++++++++++++++++++- src/editor/engine/GameplayKits.js | 16 ++++++------ src/editor/engine/SelectionManager.js | 3 +++ 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/editor/HierarchyPanel.jsx b/src/editor/HierarchyPanel.jsx index d615b4c..356ea73 100644 --- a/src/editor/HierarchyPanel.jsx +++ b/src/editor/HierarchyPanel.jsx @@ -242,7 +242,7 @@ const HierarchyPanel = ({ onSelectSound, onSelectPlayer, onSelectPlayerProps, onSelectFloor, guiElements = [], onSelectGui, onCreateGui, onDeleteGui, onRenameGui, onMoveGuiZ, onSetGuiParent, guiOverlayHidden = false, onToggleGuiOverlay, - floorEnabled = true, onCreateFloor, onDeleteFloor, + floorEnabled = true, onCreateFloor, onDeleteFloor, onDeleteSpawn, scripts = [], onSelectScript, onCreateScript, onDeleteScript, onRenameModel, onRenamePrimitive, onRenameScript, /** @@ -786,8 +786,8 @@ const HierarchyPanel = ({ className={`${cl.item} ${selection?.type === 'spawn' ? cl.itemSelected : ''}`} onClick={() => onSelectSpawn?.()} onDoubleClick={() => { onSelectSpawn?.(); onFocusSelection?.(); }} - onContextMenu={(e) => { e.preventDefault(); onSelectSpawn?.(); }} - title="Точка спавна игрока (ПКМ — выбрать и открыть свойства)" + onContextMenu={(e) => { onSelectSpawn?.(); handleContextMenu(e, { type: 'spawn' }); }} + title="Точка спавна игрока (ПКМ — меню, Delete — удалить)" > Точка спавна @@ -1314,6 +1314,21 @@ const HierarchyPanel = ({ Удалить пол + ) : contextMenu.item.type === 'spawn' ? ( + <> +
{ onSelectSpawn?.(); onFocusSelection?.(); closeContext(); }} + > + Навести камеру +
+
{ onDeleteSpawn?.(); closeContext(); }} + > + Удалить точку спавна +
+ ) : contextMenu.item.type === 'script' ? ( <>
{ // Активируем гизмо «Двигать» чтобы можно было сразу таскать setGizmoMode('move'); }} + onDeleteSpawn={() => { + sceneRef.current?.deleteSpawn?.(); + sceneRef.current?.clearSelection?.(); + markDirty(); + }} onSelectLighting={() => { sceneRef.current?.selection?.selectLighting(); setActiveTool('select'); diff --git a/src/editor/engine/BabylonScene.js b/src/editor/engine/BabylonScene.js index bfda811..afc6a55 100644 --- a/src/editor/engine/BabylonScene.js +++ b/src/editor/engine/BabylonScene.js @@ -201,6 +201,9 @@ export class BabylonScene { // Точка спавна игрока в режиме Play (обновляется setSpawnPoint) this._spawnPoint = { x: 0, y: 5, z: 0 }; + // Есть ли заданная точка спавна. Если игрок её удалил (Delete) — спавн + // в (0, высота, 0). Можно вернуть постановкой новой точки. + this._spawnEnabled = true; // Модель персонажа для режима Play. // Дефолт — R15-скин bacon-hair (классический Roblox-вид). // 'skin_*' грузится из characters//body.glb (R15-скелет), @@ -2854,6 +2857,21 @@ export class BabylonScene { } } + /** Есть ли заданная точка спавна (false → игрок появится в 0,высота,0). */ + hasSpawn() { return this._spawnEnabled !== false; } + + /** + * «Удалить» точку спавна: прячем маркер и помечаем, что спавна нет. + * В Play игрок появится в (0, безопасная высота, 0). Вернуть точку — + * через setSpawnAtCamera() (кнопка «Поставить точку спавна»). + */ + deleteSpawn() { + this._spawnEnabled = false; + this._setSpawnMarkerVisible(false); + this.history?.markChange(); + if (this._onSceneChange) this._onSceneChange(); + } + /** * Raycast от курсора в сцену. * Возвращает { mesh, point, normal } либо null если ни во что не попали. @@ -5465,6 +5483,8 @@ export class BabylonScene { y: Math.max(0, Math.floor(p.y) - 1), z: Math.round(p.z), }; + this._spawnEnabled = true; // вернуть точку, если была удалена + this._setSpawnMarkerVisible(true); this._updateSpawnMarker(); this.history?.markChange(); if (this._onSceneChange) this._onSceneChange(); @@ -5851,7 +5871,17 @@ export class BabylonScene { }); if (this._onPlayerHpChange) this.player.setOnHpChange(this._onPlayerHpChange); if (this._onPlayerDeath) this.player.setOnDeath(this._onPlayerDeath); - this.player.start(this._spawnPoint); + // Если точка спавна удалена — игрок появляется в (0, безопасная высота, 0). + let startPoint = this._spawnPoint; + if (this._spawnEnabled === false) { + let sy = 3; + try { + const surf = this.physics?._sampleRobloxSurface?.(0, 0); + if (surf !== null && surf !== undefined) sy = surf + 2; + } catch (e) { /* ignore */ } + startPoint = { x: 0, y: sy, z: 0 }; + } + this.player.start(startPoint); // Запускаем пользовательские скрипты (этап 2.1). // Async PlayerController.start даёт нам тик чтобы дать ему собрать сцену, @@ -7377,6 +7407,7 @@ export class BabylonScene { gui: this.guiManager ? this.guiManager.serialize() : [], inventory: this.inventory ? this.inventory.serialize() : null, spawnPoint: { ...this._spawnPoint }, + spawnEnabled: this._spawnEnabled !== false, playerModelType: this._playerModelType, skins: this._skinsConfig ? { default: this._skinsConfig.default || null, @@ -7799,6 +7830,9 @@ export class BabylonScene { this._spawnPoint = { ...state.scene.spawnPoint }; this._updateSpawnMarker(); } + // Удалена ли точка спавна (спавн в 0,0 при отсутствии). + this._spawnEnabled = state.scene.spawnEnabled !== false; + this._setSpawnMarkerVisible(this._spawnEnabled); // === Авто-fix спавна для smooth terrain === // Если есть RobloxTerrain и spawnPoint оказался НИЖЕ поверхности — // поднимаем его, чтобы игрок не провалился в момент нажатия "Запустить". diff --git a/src/editor/engine/GameplayKits.js b/src/editor/engine/GameplayKits.js index 14209aa..096aec4 100644 --- a/src/editor/engine/GameplayKits.js +++ b/src/editor/engine/GameplayKits.js @@ -62,16 +62,15 @@ game.every(8, () => { { id: 'currency-counter', name: 'Счётчик монет', - desc: 'Показывает счётчик монет в углу HUD. Метод game.addCoins(n) прибавляет монеты.', + desc: 'Счётчик монет в углу HUD. Другие механики шлют game.broadcast("coins", {add: N}) — счётчик обновляется.', icon: 'circle', category: 'ui', scripts: [{ attachTo: 'global', code: -`// Счётчик монет в HUD +`// Счётчик монет в HUD. Прибавить монеты из любого скрипта: +// game.broadcast('coins', { add: 100 }); let coins = 0; -function showCoins() { game.ui.set('coins', '🪙 ' + coins, { x: 92, y: 6, anchor: 'top', color: '#ffd23a', size: 22 }); } -showCoins(); -// Глобальный помощник: вызывай game.scene.setData('_coins','add',N) или меняй coins из других скриптов. -game.every(0.3, () => showCoins()); -globalThis.__addCoins = (n) => { coins += (n||1); showCoins(); };` }], +function show() { game.ui.set('coins', '🪙 ' + coins, { x: 92, y: 6, anchor: 'top', color: '#ffd23a', size: 22 }); } +show(); +game.onMessage('coins', (m) => { coins += (m && m.add) ? m.add : 1; show(); });` }], }, { id: 'start-pad', @@ -175,11 +174,12 @@ game.after(5, () => game.ui.set('welcome', ''));` }], { type: 'cube', x: 0, y: 1.35, z: 0, sx: 1.7, sy: 0.4, sz: 1.3, color: '#d4a843', material: 'metal', name: 'Крышка сундука', canCollide: false }, ], scripts: [{ attachTo: 'on-target', code: -`// Сундук с лутом +`// Сундук с лутом — даёт 100 монет (через счётчик монет, если он добавлен). let opened = false; game.self.onInteract(() => { if (opened) { game.ui.set('loot', 'Сундук уже открыт.', { x: 50, y: 85, anchor: 'bottom', color: '#bbb', size: 16 }); return; } opened = true; + game.broadcast('coins', { add: 100 }); // обновит «Счётчик монет», если он есть game.ui.set('loot', '🎁 Ты получил награду: +100 монет!', { x: 50, y: 85, anchor: 'bottom', color: '#ffd23a', size: 18 }); game.after(3, () => game.ui.set('loot', '')); }, { text: 'Открыть сундук', key: 'f', distance: 4 });` }], diff --git a/src/editor/engine/SelectionManager.js b/src/editor/engine/SelectionManager.js index 24d58ef..351d3b2 100644 --- a/src/editor/engine/SelectionManager.js +++ b/src/editor/engine/SelectionManager.js @@ -685,6 +685,9 @@ export class SelectionManager { this.userModelManager.removeInstance(this._selection.instanceId); } else if (this._selection.type === 'primitive') { this.primitiveManager.removeInstance(this._selection.id); + } else if (this._selection.type === 'spawn') { + // Удаление точки спавна → игрок будет появляться в (0, высота, 0). + this._scene3d?.deleteSpawn?.(); } this.clear(); }