restore: Mixamo + dev-skin через URL #skin= #28

Merged
min merged 2 commits from restore/mixamo-skin-2026-06-14 into main 2026-06-14 08:11:28 +00:00
Showing only changes of commit dbdd61b4d6 - Show all commits

View File

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