fix(studio): сундук→счётчик монет через broadcast, удаление точки спавна + фолбэк 0,0
- Киты «Сундук» и «Счётчик монет» связаны через 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 <noreply@anthropic.com>
This commit is contained in:
parent
7242e80602
commit
471af1cdeb
@ -242,7 +242,7 @@ const HierarchyPanel = ({
|
|||||||
onSelectSound, onSelectPlayer, onSelectPlayerProps, onSelectFloor,
|
onSelectSound, onSelectPlayer, onSelectPlayerProps, onSelectFloor,
|
||||||
guiElements = [], onSelectGui, onCreateGui, onDeleteGui, onRenameGui, onMoveGuiZ, onSetGuiParent,
|
guiElements = [], onSelectGui, onCreateGui, onDeleteGui, onRenameGui, onMoveGuiZ, onSetGuiParent,
|
||||||
guiOverlayHidden = false, onToggleGuiOverlay,
|
guiOverlayHidden = false, onToggleGuiOverlay,
|
||||||
floorEnabled = true, onCreateFloor, onDeleteFloor,
|
floorEnabled = true, onCreateFloor, onDeleteFloor, onDeleteSpawn,
|
||||||
scripts = [], onSelectScript, onCreateScript, onDeleteScript,
|
scripts = [], onSelectScript, onCreateScript, onDeleteScript,
|
||||||
onRenameModel, onRenamePrimitive, onRenameScript,
|
onRenameModel, onRenamePrimitive, onRenameScript,
|
||||||
/**
|
/**
|
||||||
@ -786,8 +786,8 @@ const HierarchyPanel = ({
|
|||||||
className={`${cl.item} ${selection?.type === 'spawn' ? cl.itemSelected : ''}`}
|
className={`${cl.item} ${selection?.type === 'spawn' ? cl.itemSelected : ''}`}
|
||||||
onClick={() => onSelectSpawn?.()}
|
onClick={() => onSelectSpawn?.()}
|
||||||
onDoubleClick={() => { onSelectSpawn?.(); onFocusSelection?.(); }}
|
onDoubleClick={() => { onSelectSpawn?.(); onFocusSelection?.(); }}
|
||||||
onContextMenu={(e) => { e.preventDefault(); onSelectSpawn?.(); }}
|
onContextMenu={(e) => { onSelectSpawn?.(); handleContextMenu(e, { type: 'spawn' }); }}
|
||||||
title="Точка спавна игрока (ПКМ — выбрать и открыть свойства)"
|
title="Точка спавна игрока (ПКМ — меню, Delete — удалить)"
|
||||||
>
|
>
|
||||||
<span className={cl.itemIcon} style={{ display: 'inline-flex' }}><Icon name="flag" size={14} /></span>
|
<span className={cl.itemIcon} style={{ display: 'inline-flex' }}><Icon name="flag" size={14} /></span>
|
||||||
<span className={cl.itemLabel}>Точка спавна</span>
|
<span className={cl.itemLabel}>Точка спавна</span>
|
||||||
@ -1314,6 +1314,21 @@ const HierarchyPanel = ({
|
|||||||
<Icon name="delete" size={13} /> Удалить пол
|
<Icon name="delete" size={13} /> Удалить пол
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
) : contextMenu.item.type === 'spawn' ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={cl.contextItem}
|
||||||
|
onClick={() => { onSelectSpawn?.(); onFocusSelection?.(); closeContext(); }}
|
||||||
|
>
|
||||||
|
<Icon name="target" size={13} /> Навести камеру
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${cl.contextItem} ${cl.contextDanger}`}
|
||||||
|
onClick={() => { onDeleteSpawn?.(); closeContext(); }}
|
||||||
|
>
|
||||||
|
<Icon name="delete" size={13} /> Удалить точку спавна
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
) : contextMenu.item.type === 'script' ? (
|
) : contextMenu.item.type === 'script' ? (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -3309,6 +3309,11 @@ const KubikonEditor = () => {
|
|||||||
// Активируем гизмо «Двигать» чтобы можно было сразу таскать
|
// Активируем гизмо «Двигать» чтобы можно было сразу таскать
|
||||||
setGizmoMode('move');
|
setGizmoMode('move');
|
||||||
}}
|
}}
|
||||||
|
onDeleteSpawn={() => {
|
||||||
|
sceneRef.current?.deleteSpawn?.();
|
||||||
|
sceneRef.current?.clearSelection?.();
|
||||||
|
markDirty();
|
||||||
|
}}
|
||||||
onSelectLighting={() => {
|
onSelectLighting={() => {
|
||||||
sceneRef.current?.selection?.selectLighting();
|
sceneRef.current?.selection?.selectLighting();
|
||||||
setActiveTool('select');
|
setActiveTool('select');
|
||||||
|
|||||||
@ -201,6 +201,9 @@ export class BabylonScene {
|
|||||||
|
|
||||||
// Точка спавна игрока в режиме Play (обновляется setSpawnPoint)
|
// Точка спавна игрока в режиме Play (обновляется setSpawnPoint)
|
||||||
this._spawnPoint = { x: 0, y: 5, z: 0 };
|
this._spawnPoint = { x: 0, y: 5, z: 0 };
|
||||||
|
// Есть ли заданная точка спавна. Если игрок её удалил (Delete) — спавн
|
||||||
|
// в (0, высота, 0). Можно вернуть постановкой новой точки.
|
||||||
|
this._spawnEnabled = true;
|
||||||
// Модель персонажа для режима Play.
|
// Модель персонажа для режима Play.
|
||||||
// Дефолт — R15-скин bacon-hair (классический Roblox-вид).
|
// Дефолт — R15-скин bacon-hair (классический Roblox-вид).
|
||||||
// 'skin_*' грузится из characters/<id>/body.glb (R15-скелет),
|
// 'skin_*' грузится из characters/<id>/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 от курсора в сцену.
|
* Raycast от курсора в сцену.
|
||||||
* Возвращает { mesh, point, normal } либо null если ни во что не попали.
|
* Возвращает { mesh, point, normal } либо null если ни во что не попали.
|
||||||
@ -5465,6 +5483,8 @@ export class BabylonScene {
|
|||||||
y: Math.max(0, Math.floor(p.y) - 1),
|
y: Math.max(0, Math.floor(p.y) - 1),
|
||||||
z: Math.round(p.z),
|
z: Math.round(p.z),
|
||||||
};
|
};
|
||||||
|
this._spawnEnabled = true; // вернуть точку, если была удалена
|
||||||
|
this._setSpawnMarkerVisible(true);
|
||||||
this._updateSpawnMarker();
|
this._updateSpawnMarker();
|
||||||
this.history?.markChange();
|
this.history?.markChange();
|
||||||
if (this._onSceneChange) this._onSceneChange();
|
if (this._onSceneChange) this._onSceneChange();
|
||||||
@ -5851,7 +5871,17 @@ export class BabylonScene {
|
|||||||
});
|
});
|
||||||
if (this._onPlayerHpChange) this.player.setOnHpChange(this._onPlayerHpChange);
|
if (this._onPlayerHpChange) this.player.setOnHpChange(this._onPlayerHpChange);
|
||||||
if (this._onPlayerDeath) this.player.setOnDeath(this._onPlayerDeath);
|
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).
|
// Запускаем пользовательские скрипты (этап 2.1).
|
||||||
// Async PlayerController.start даёт нам тик чтобы дать ему собрать сцену,
|
// Async PlayerController.start даёт нам тик чтобы дать ему собрать сцену,
|
||||||
@ -7377,6 +7407,7 @@ export class BabylonScene {
|
|||||||
gui: this.guiManager ? this.guiManager.serialize() : [],
|
gui: this.guiManager ? this.guiManager.serialize() : [],
|
||||||
inventory: this.inventory ? this.inventory.serialize() : null,
|
inventory: this.inventory ? this.inventory.serialize() : null,
|
||||||
spawnPoint: { ...this._spawnPoint },
|
spawnPoint: { ...this._spawnPoint },
|
||||||
|
spawnEnabled: this._spawnEnabled !== false,
|
||||||
playerModelType: this._playerModelType,
|
playerModelType: this._playerModelType,
|
||||||
skins: this._skinsConfig ? {
|
skins: this._skinsConfig ? {
|
||||||
default: this._skinsConfig.default || null,
|
default: this._skinsConfig.default || null,
|
||||||
@ -7799,6 +7830,9 @@ export class BabylonScene {
|
|||||||
this._spawnPoint = { ...state.scene.spawnPoint };
|
this._spawnPoint = { ...state.scene.spawnPoint };
|
||||||
this._updateSpawnMarker();
|
this._updateSpawnMarker();
|
||||||
}
|
}
|
||||||
|
// Удалена ли точка спавна (спавн в 0,0 при отсутствии).
|
||||||
|
this._spawnEnabled = state.scene.spawnEnabled !== false;
|
||||||
|
this._setSpawnMarkerVisible(this._spawnEnabled);
|
||||||
// === Авто-fix спавна для smooth terrain ===
|
// === Авто-fix спавна для smooth terrain ===
|
||||||
// Если есть RobloxTerrain и spawnPoint оказался НИЖЕ поверхности —
|
// Если есть RobloxTerrain и spawnPoint оказался НИЖЕ поверхности —
|
||||||
// поднимаем его, чтобы игрок не провалился в момент нажатия "Запустить".
|
// поднимаем его, чтобы игрок не провалился в момент нажатия "Запустить".
|
||||||
|
|||||||
@ -62,16 +62,15 @@ game.every(8, () => {
|
|||||||
{
|
{
|
||||||
id: 'currency-counter',
|
id: 'currency-counter',
|
||||||
name: 'Счётчик монет',
|
name: 'Счётчик монет',
|
||||||
desc: 'Показывает счётчик монет в углу HUD. Метод game.addCoins(n) прибавляет монеты.',
|
desc: 'Счётчик монет в углу HUD. Другие механики шлют game.broadcast("coins", {add: N}) — счётчик обновляется.',
|
||||||
icon: 'circle', category: 'ui',
|
icon: 'circle', category: 'ui',
|
||||||
scripts: [{ attachTo: 'global', code:
|
scripts: [{ attachTo: 'global', code:
|
||||||
`// Счётчик монет в HUD
|
`// Счётчик монет в HUD. Прибавить монеты из любого скрипта:
|
||||||
|
// game.broadcast('coins', { add: 100 });
|
||||||
let coins = 0;
|
let coins = 0;
|
||||||
function showCoins() { game.ui.set('coins', '🪙 ' + coins, { x: 92, y: 6, anchor: 'top', color: '#ffd23a', size: 22 }); }
|
function show() { game.ui.set('coins', '🪙 ' + coins, { x: 92, y: 6, anchor: 'top', color: '#ffd23a', size: 22 }); }
|
||||||
showCoins();
|
show();
|
||||||
// Глобальный помощник: вызывай game.scene.setData('_coins','add',N) или меняй coins из других скриптов.
|
game.onMessage('coins', (m) => { coins += (m && m.add) ? m.add : 1; show(); });` }],
|
||||||
game.every(0.3, () => showCoins());
|
|
||||||
globalThis.__addCoins = (n) => { coins += (n||1); showCoins(); };` }],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'start-pad',
|
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 },
|
{ 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:
|
scripts: [{ attachTo: 'on-target', code:
|
||||||
`// Сундук с лутом
|
`// Сундук с лутом — даёт 100 монет (через счётчик монет, если он добавлен).
|
||||||
let opened = false;
|
let opened = false;
|
||||||
game.self.onInteract(() => {
|
game.self.onInteract(() => {
|
||||||
if (opened) { game.ui.set('loot', 'Сундук уже открыт.', { x: 50, y: 85, anchor: 'bottom', color: '#bbb', size: 16 }); return; }
|
if (opened) { game.ui.set('loot', 'Сундук уже открыт.', { x: 50, y: 85, anchor: 'bottom', color: '#bbb', size: 16 }); return; }
|
||||||
opened = true;
|
opened = true;
|
||||||
|
game.broadcast('coins', { add: 100 }); // обновит «Счётчик монет», если он есть
|
||||||
game.ui.set('loot', '🎁 Ты получил награду: +100 монет!', { x: 50, y: 85, anchor: 'bottom', color: '#ffd23a', size: 18 });
|
game.ui.set('loot', '🎁 Ты получил награду: +100 монет!', { x: 50, y: 85, anchor: 'bottom', color: '#ffd23a', size: 18 });
|
||||||
game.after(3, () => game.ui.set('loot', ''));
|
game.after(3, () => game.ui.set('loot', ''));
|
||||||
}, { text: 'Открыть сундук', key: 'f', distance: 4 });` }],
|
}, { text: 'Открыть сундук', key: 'f', distance: 4 });` }],
|
||||||
|
|||||||
@ -685,6 +685,9 @@ export class SelectionManager {
|
|||||||
this.userModelManager.removeInstance(this._selection.instanceId);
|
this.userModelManager.removeInstance(this._selection.instanceId);
|
||||||
} else if (this._selection.type === 'primitive') {
|
} else if (this._selection.type === 'primitive') {
|
||||||
this.primitiveManager.removeInstance(this._selection.id);
|
this.primitiveManager.removeInstance(this._selection.id);
|
||||||
|
} else if (this._selection.type === 'spawn') {
|
||||||
|
// Удаление точки спавна → игрок будет появляться в (0, высота, 0).
|
||||||
|
this._scene3d?.deleteSpawn?.();
|
||||||
}
|
}
|
||||||
this.clear();
|
this.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user