diff --git a/src/KubikonPlayer/KubikonPlayer.jsx b/src/KubikonPlayer/KubikonPlayer.jsx index 9aa72f4..6993de9 100644 --- a/src/KubikonPlayer/KubikonPlayer.jsx +++ b/src/KubikonPlayer/KubikonPlayer.jsx @@ -311,36 +311,26 @@ const KubikonPlayer = () => { return () => { active = false; }; }, [projectId]); - // Перехват Ctrl-комбинаций которые ломают игру (Ctrl+W = закрыть вкладку, - // Ctrl+R = reload, Ctrl+T/N — мешают). Большинство браузеров блокирует - // отмену системных шорткатов, но beforeunload даёт пользователю шанс - // подтвердить выход. Также превентим preventDefault на keydown для - // случаев когда фокус НЕ на window-уровне (Chrome иногда позволяет). + // Перехват Ctrl-комбинаций которые ломают игру (Ctrl+W, Ctrl+R, Ctrl+T, + // Ctrl+N). Большинство браузеров блокирует отмену системных шорткатов, + // но мы пробуем preventDefault — иногда срабатывает. + // + // 2026-06-14: beforeunload-подтверждение убрано по решению UX — системное + // окно браузера невозможно стилизовать (с 2017 Chrome игнорирует кастомный + // текст), а уродливое модальное мешает. Случайное закрытие вкладки теперь + // просто закрывает игру без вопроса. useEffect(() => { const onKey = (e) => { if (!e.ctrlKey && !e.metaKey) return; - // Список «опасных» в игре сочетаний — превентим const dangerousCodes = ['KeyW', 'KeyR', 'KeyT', 'KeyN']; if (dangerousCodes.includes(e.code)) { e.preventDefault(); e.stopPropagation(); } }; - const onBeforeUnload = (e) => { - // Если юзер сам нажал «Покинуть» в меню — пропускаем без - // подтверждения. Флаг ставит exitPlayer(). - if (window.__rubloxExplicitExit) return undefined; - // Случайное закрытие вкладки (Ctrl+W, X-кнопка) — показываем - // подтверждение чтобы не потерять прогресс игры. - e.preventDefault(); - e.returnValue = ''; - return ''; - }; window.addEventListener('keydown', onKey, { capture: true }); - window.addEventListener('beforeunload', onBeforeUnload); return () => { window.removeEventListener('keydown', onKey, { capture: true }); - window.removeEventListener('beforeunload', onBeforeUnload); }; }, []); @@ -592,29 +582,39 @@ const KubikonPlayer = () => { // Этот же skinFolder уйдёт в мультиплеер как modelType, // чтобы соперники видели наш реальный скин. // - // LOCAL DEV (localhost): сначала пробуем localStorage - // ('rublox_selected_skin' — тот же ключ что в rublox-site), - // чтобы юзер мог тестить выбор скинов без записи в прод-БД. + // LOCAL DEV (localhost): берём скин из URL #skin= + // (передаётся сайтом 3000 при нажатии «Играть»), потом из + // localStorage самого плеера, потом дефолт. В БД НЕ лезем — + // прод-БД хранит легаси скины (skin_sigma-labubu и др.), + // которые мы не хотим грузить локально. + // + // PROD: только БД (rublox_equipped_skin). let mySkin = 'skin_y-bot'; const isLocalDev = (typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')); if (isLocalDev) { try { - const localPick = localStorage.getItem('rublox_selected_skin'); - if (localPick && typeof localPick === 'string') { - mySkin = localPick; - console.log('[KubikonPlayer] local-dev skin:', mySkin); + // 1) hash-параметр #skin= (от сайта при play-ticket) + const m = window.location.hash.match(/[#&]skin=([\w-]+)/); + if (m && m[1]) { + mySkin = m[1]; + console.log('[KubikonPlayer] local-dev skin (URL):', mySkin); + } else { + // 2) localStorage самого плеера + const localPick = localStorage.getItem('rublox_selected_skin'); + if (localPick && typeof localPick === 'string') { + mySkin = localPick; + console.log('[KubikonPlayer] local-dev skin (LS):', mySkin); + } } } catch (e) {} - } - if (mySkin === 'skin_y-bot' && userId) { + } else if (userId) { try { const skinRes = await Kubikon3DApi.getEquippedSkin(userId); const sf = skinRes?.data?.skin_folder; if (sf && typeof sf === 'string') mySkin = sf; } catch (e) { - // Сеть/ошибка — играем с дефолтным скином, не блокируем. console.warn('[KubikonPlayer] equipped-skin load failed', e); } }