Compare commits

..

No commits in common. "322dd089d9c34a05cd8cc6af32d649c68fe50ec5" and "ae83926a5a7e9f64eadb3c3ca19e5458c1dde9ee" have entirely different histories.

4 changed files with 48 additions and 71 deletions

View File

@ -517,20 +517,13 @@ const KubikonPlayer = () => {
s?.player?.setUiCursorMode?.(true); s?.player?.setUiCursorMode?.(true);
setChatOpen(false); setChatOpen(false);
setTopMenuOpen(true); setTopMenuOpen(true);
try { if (s) s._playerMenuOpen = true; } catch (e) { /* ignore */ }
} }
}); });
// ESC в Play TOGGLE меню-оверлея поверх ЖИВОЙ игры (Roblox-style). // ESC в Play меню-оверлей поверх ЖИВОЙ игры (Roblox-style). Play не
// Движок сам решает open/close (единый источник истины _playerMenuOpen) // прерывается, скрипты продолжают идти, игрок не респавнится.
// и передаёт сюда. Это убирает гонку двух ESC-обработчиков, из-за которой scene.setOnEscMenu?.(() => {
// меню открывалось поверх меню, а orbit-камера по ПКМ зависала. setChatOpen(false);
scene.setOnEscMenu?.((open) => { setTopMenuOpen(true);
if (open) {
setChatOpen(false);
setTopMenuOpen(true);
} else {
setTopMenuOpen(false);
}
}); });
// Загружаем проект. // Загружаем проект.
@ -741,20 +734,26 @@ const KubikonPlayer = () => {
p._uiCursorMode = true; p._uiCursorMode = true;
setChatOpen(false); setChatOpen(false);
setTopMenuOpen(true); setTopMenuOpen(true);
// Синхронизируем единый флаг меню в движке, чтобы следующий ESC
// сработал как toggle-закрытие (а не открыл второе меню).
try { s._playerMenuOpen = true; } catch (e) { /* ignore */ }
}; };
// capture-фаза, чтобы успеть раньше PlayerController // capture-фаза, чтобы успеть раньше PlayerController
document.addEventListener('pointerlockchange', onLockChange, true); document.addEventListener('pointerlockchange', onLockChange, true);
return () => document.removeEventListener('pointerlockchange', onLockChange, true); return () => document.removeEventListener('pointerlockchange', onLockChange, true);
}, []); }, []);
// Повторный ESC (toggle закрытие) теперь обрабатывает движок через // Повторный ESC (когда меню уже открыто) закрыть меню и вернуть
// setOnExitRequest _onEscMenu(false). Отдельный React-обработчик ESC // мышь в игру.
// УБРАН он слушал тот же ESC, что и движок, и создавал гонку: useEffect(() => {
// меню открывалось поверх себя, а _uiCursorMode застревал в true if (!topMenuOpen) return;
// (orbit-камера по ПКМ переставала работать после закрытия меню). const onEsc = (e) => {
if (e.key !== 'Escape') return;
const s = sceneRef.current;
if (!s || !s._isPlaying) return;
setTopMenuOpen(false);
s.player?.setUiCursorMode?.(false);
};
window.addEventListener('keydown', onEsc, true);
return () => window.removeEventListener('keydown', onEsc, true);
}, [topMenuOpen]);
// Горячая клавиша T открыть/закрыть чат. Игнорируем когда: // Горячая клавиша T открыть/закрыть чат. Игнорируем когда:
// уже введён текст в <input>/<textarea>/contenteditable (юзер печатает) // уже введён текст в <input>/<textarea>/contenteditable (юзер печатает)
@ -1620,10 +1619,9 @@ const KubikonPlayer = () => {
visible={topMenuOpen} visible={topMenuOpen}
onClose={() => { onClose={() => {
setTopMenuOpen(false); setTopMenuOpen(false);
// Синхронизируем движок (_playerMenuOpen) И возвращаем мышь // Возвращаем мышь в pointer-lock игры (как делал
// в игру одним вызовом. Без этого следующий ESC решит, что // старый ESC-handler выше).
// меню «ещё открыто», и не откроет его. try { sceneRef.current?.player?.setUiCursorMode?.(false); } catch {}
try { sceneRef.current?.setPlayerMenuOpen?.(false); } catch {}
}} }}
onExit={() => exitPlayer(id)} onExit={() => exitPlayer(id)}
onRespawn={() => respawnPlayer()} onRespawn={() => respawnPlayer()}

View File

@ -5332,7 +5332,6 @@ export class BabylonScene {
this._isPlaying = true; this._isPlaying = true;
// Сброс состояния касаний — каждый прогон начинается «не касаясь». // Сброс состояния касаний — каждый прогон начинается «не касаясь».
if (this._touchState) this._touchState.clear(); if (this._touchState) this._touchState.clear();
this._playerMenuOpen = false; // меню-оверлей закрыт на старте Play
// По умолчанию стандартный HUD видим в Play. // По умолчанию стандартный HUD видим в Play.
// Скрипт может скрыть через game.hud.setVisible(false). // Скрипт может скрыть через game.hud.setVisible(false).
this._setStdHudVisible(true); this._setStdHudVisible(true);
@ -5393,24 +5392,14 @@ export class BabylonScene {
this.modalManager.close(); this.modalManager.close();
return; return;
} }
// ESC в плеере = TOGGLE меню-оверлея поверх ЖИВОЙ игры (как в Roblox). // ESC в плеере = открыть меню-оверлей поверх ЖИВОЙ игры (как в Roblox).
// Единый источник истины — _playerMenuOpen в движке. Раньше состояние // Раньше тут был exitPlayMode() + _onPlayChange(false), из-за чего
// меню держал React, а ESC слушали ДВА обработчика (движок + React) → // KubikonPlayer заново звал enterPlayMode → игра перезапускалась
// гонка: меню открывалось поверх меню, а _uiCursorMode застревал в true // (респавн + перезапуск скриптов). Теперь только UI-курсор + сигнал
// → orbit-камера по ПКМ переставала работать после закрытия меню. // открыть меню. Play продолжает идти под меню.
// Теперь движок сам решает open/close и шлёт это в _onEscMenu(open).
if (typeof this._onEscMenu === 'function') { if (typeof this._onEscMenu === 'function') {
if (this._playerMenuOpen) { this.player?.setUiCursorMode?.(true);
// Меню открыто → ESC закрывает: вернуть мышь в игру. this._onEscMenu();
this._playerMenuOpen = false;
this.player?.setUiCursorMode?.(false);
this._onEscMenu(false);
} else {
// Меню закрыто → ESC открывает: освободить курсор.
this._playerMenuOpen = true;
this.player?.setUiCursorMode?.(true);
this._onEscMenu(true);
}
return; return;
} }
// Фолбэк (если меню не подписано, напр. в студии) — старое поведение. // Фолбэк (если меню не подписано, напр. в студии) — старое поведение.
@ -6007,22 +5996,6 @@ export class BabylonScene {
this._onEscMenu = cb; this._onEscMenu = cb;
} }
/**
* Синхронизация состояния меню-оверлея из UI (React). Когда меню закрывают
* НЕ через ESC (кнопка «Продолжить»/крестик/клик по фону), UI обязан сообщить
* движку иначе _playerMenuOpen рассинхронизируется и следующий ESC решит,
* что меню «открыто», и не откроет его. open=false также возвращает мышь в игру.
*/
setPlayerMenuOpen(open) {
const v = !!open;
if (this._playerMenuOpen === v) return;
this._playerMenuOpen = v;
if (!v) {
// меню закрыли из UI → вернуть управление камерой/мышью
try { this.player?.setUiCursorMode?.(false); } catch (e) { /* ignore */ }
}
}
/** /**
* Колбэк изменения сцены (любая модификация блоков/моделей). * Колбэк изменения сцены (любая модификация блоков/моделей).
* Используется KubikonEditor для dirty-tracking auto-save. * Используется KubikonEditor для dirty-tracking auto-save.

View File

@ -580,10 +580,6 @@ export class PlayerController {
setUiCursorMode(enabled) { setUiCursorMode(enabled) {
this._uiCursorMode = !!enabled; this._uiCursorMode = !!enabled;
if (enabled) { if (enabled) {
// Открываем UI (меню/курсор) → сбрасываем удержание ПКМ. Иначе если
// меню открыли при зажатой ПКМ, _rmbHeld застревает в true и orbit-
// камера после закрытия меню «думает», что ПКМ всё ещё активна.
this._rmbHeld = false;
// Освобождаем мышь // Освобождаем мышь
if (document.pointerLockElement === this.canvas) { if (document.pointerLockElement === this.canvas) {
try { document.exitPointerLock(); } catch (e) { /* ignore */ } try { document.exitPointerLock(); } catch (e) { /* ignore */ }

View File

@ -799,14 +799,7 @@ export class PrimitiveManager {
const newMesh = this._createMeshForType(typeDef, data.id, data.sx, data.sy, data.sz, data.material, data.studDensity); const newMesh = this._createMeshForType(typeDef, data.id, data.sx, data.sy, data.sz, data.material, data.studDensity);
newMesh.position = oldPos; newMesh.position = oldPos;
if (oldRot) newMesh.rotation = oldRot; if (oldRot) newMesh.rotation = oldRot;
// studs — материал пересоздаём заново (свежий faceUV/тайлинг). Иначе перенос. newMesh.material = oldMat;
if (data.material === 'studs') {
newMesh._studsDims = { type: data.type, sx: data.sx, sy: data.sy, sz: data.sz, density: data.studDensity };
this._applyMaterial(newMesh, typeDef, data.color, data.material);
try { oldMat?.dispose(); } catch (e) { /* ignore */ }
} else {
newMesh.material = oldMat;
}
newMesh.isPickable = true; newMesh.isPickable = true;
newMesh.metadata = { ...oldMesh.metadata }; newMesh.metadata = { ...oldMesh.metadata };
newMesh.setEnabled(data.visible); newMesh.setEnabled(data.visible);
@ -816,7 +809,24 @@ export class PrimitiveManager {
catch (e) { /* ignore */ } catch (e) { /* ignore */ }
data.mesh = newMesh; data.mesh = newMesh;
// _studsDims и материал studs уже выставлены выше. newMesh._studsDims = { type: data.type, sx: data.sx, sy: data.sy, sz: data.sz, density: data.studDensity };
// studs-материал: пересчитать тайлинг под новый размер меша.
// Куб уже пересоздан с новым faceUV (тайлинг в геометрии) — uScale=1.
// Для остальных форм пересчитываем uScale/vScale по размеру.
if (data.material === 'studs' && oldMat && oldMat.diffuseTexture) {
if (data.type === 'cube' || data.type === 'trigger') {
oldMat.diffuseTexture.uScale = oldMat.diffuseTexture.vScale = 1;
if (oldMat.bumpTexture) oldMat.bumpTexture.uScale = oldMat.bumpTexture.vScale = 1;
} else {
const tile = _studsTiling(data.type, data.sx, data.sy, data.sz, data.studDensity);
oldMat.diffuseTexture.uScale = tile.u;
oldMat.diffuseTexture.vScale = tile.v;
if (oldMat.bumpTexture) {
oldMat.bumpTexture.uScale = tile.u;
oldMat.bumpTexture.vScale = tile.v;
}
}
}
} }
/** Удалить инстанс. */ /** Удалить инстанс. */