From dbdd61b4d637ba8c97994e7687d17692d500de74 Mon Sep 17 00:00:00 2001 From: min Date: Sun, 14 Jun 2026 11:02:47 +0300 Subject: [PATCH] =?UTF-8?q?feat(player):=20dev-skin=20=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=20URL=20#skin=3D=20+=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=20beforeunload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LOCAL DEV: при запуске на localhost плеер берёт скин из: 1) hash-параметра #skin= (сайт 3000 передаёт его при play-ticket через buildPlayerUrl(gameId, ticket, selectedSkin)) 2) localStorage самого плеера (rublox_selected_skin) 3) дефолт skin_y-bot Это нужно потому что: - localStorage на :5173 (плеер) и :3000 (сайт) — РАЗНЫЕ хранилища - В прод-БД пока legacy-скины (skin_sigma-labubu и др.), пока feature-flag RUBLOX_NEW_SKINS_AVAILABLE=false плеер не должен в неё лезть локально PROD: только БД (rublox_equipped_skin) — поведение не меняется. Также убран beforeunload-prompt: системное окно браузера невозможно стилизовать (Chrome игнорирует кастомный текст с 2017), а уродливое окно мешало. Случайное закрытие вкладки теперь без подтверждения. --- src/KubikonPlayer/KubikonPlayer.jsx | 56 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) 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); } }