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