From f5a96fbec0ca7ed55cae6f31bb1daa0cf590bef4 Mon Sep 17 00:00:00 2001 From: min Date: Sun, 7 Jun 2026 19:46:20 +0300 Subject: [PATCH] =?UTF-8?q?fix(player):=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= =?UTF-8?q?=D0=B0=2005=20=E2=80=94=20=D0=BA=D1=80=D0=B0=D1=81=D0=B8=D0=B2?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B8=20=D0=98=D0=93=D0=A0=D0=AB?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20=D0=B2=D1=85=D0=BE=D0=B4=D0=B5=20(?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B5=20=D0=B2=20=D1=81=D1=82=D1=83=D0=B4=D0=B8?= =?UTF-8?q?=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Главное по задаче 05: переделан React loading-оверлей в KubikonPlayer (тот, что игрок видит после клика «Играть» пока грузится игра). Новый компонент GameLoadingScreen: Ken Burns фон + карточка-витрина + название места + автор + verified-галочка + прогресс-бар (реальный 0→100%) + спиннер. Данные: project_data.scene.loadingScreen (настройки автора из студии) → мета игры (title/thumbnail/автор) → дефолт. 0 ошибок, проверено headless. Co-Authored-By: Claude Opus 4.8 --- src/KubikonPlayer/GameLoadingScreen.jsx | 198 ++++++++++++++++++++++++ src/KubikonPlayer/KubikonPlayer.jsx | 62 +++----- 2 files changed, 221 insertions(+), 39 deletions(-) create mode 100644 src/KubikonPlayer/GameLoadingScreen.jsx diff --git a/src/KubikonPlayer/GameLoadingScreen.jsx b/src/KubikonPlayer/GameLoadingScreen.jsx new file mode 100644 index 0000000..63ade54 --- /dev/null +++ b/src/KubikonPlayer/GameLoadingScreen.jsx @@ -0,0 +1,198 @@ +/** + * GameLoadingScreen — красивый экран загрузки игры в плеере (задача 05). + * + * Показывается пока грузится игра (после клика «Играть» на странице игры → + * открытие плеера). Композиция как в Roblox: + * - размытый фон-обложка игры с медленным Ken Burns (pan + zoom); + * - карточка-витрина по центру (обложка игры); + * - крупное название места; + * - автор + verified-галочка; + * - прогресс-бар + спиннер «ЗАГРУЗКА». + * + * Данные берёт из меты игры (title/thumbnail/автор) и, если автор настроил в + * студии вкладку «Стартовый экран» — из project_data.scene.loadingScreen + * (placeName / studioName / style / verified / background / cover). + */ +import React, { useEffect, useRef, useState } from 'react'; + +// Один раз вставляем CSS-keyframes (нельзя инлайнить в style). +let _cssInjected = false; +function injectCss() { + if (_cssInjected || typeof document === 'undefined') return; + _cssInjected = true; + const s = document.createElement('style'); + s.id = 'kbn-game-loading-css'; + s.textContent = + '@keyframes kbnGlsKen{0%{transform:scale(1.05) translate3d(0,0,0)}50%{transform:scale(1.15) translate3d(-3%,-2%,0)}100%{transform:scale(1.05) translate3d(-6%,0,0)}}' + + '.kbnGlsKen{animation:kbnGlsKen 22s ease-in-out infinite}' + + '@keyframes kbnGlsSpin{to{transform:rotate(360deg)}}' + + '.kbnGlsSpin{animation:kbnGlsSpin 0.85s linear infinite}' + + '@keyframes kbnGlsRise{0%{transform:translateY(0) scale(1);opacity:0}12%{opacity:.9}88%{opacity:.6}100%{transform:translateY(-110vh) scale(1.4);opacity:0}}' + + '.kbnGlsP{animation:kbnGlsRise linear infinite}' + + '@keyframes kbnGlsGlow{0%,100%{box-shadow:0 18px 60px rgba(0,0,0,.6),0 0 0 rgba(120,160,255,0)}50%{box-shadow:0 18px 60px rgba(0,0,0,.6),0 0 44px rgba(120,160,255,.4)}}' + + '.kbnGlsCard{animation:kbnGlsGlow 4s ease-in-out infinite}' + + '@keyframes kbnGlsBar{0%{transform:translateX(-100%)}100%{transform:translateX(250%)}}' + + '.kbnGlsBarRun{animation:kbnGlsBar 1.2s ease-in-out infinite}' + + '@media (prefers-reduced-motion:reduce){.kbnGlsKen,.kbnGlsP,.kbnGlsCard,.kbnGlsBarRun{animation:none}}'; + document.head.appendChild(s); +} + +function VerifiedBadge() { + return ( + + + + + ); +} + +/** + * props: + * meta — ответ getProjectForPlay (title, thumbnail, author_username/username, ...) + * loadingScreen — project_data.scene.loadingScreen (опц., настройки автора) + * progress — 0..1 (если null — «бегущая» полоса без процента) + */ +export default function GameLoadingScreen({ meta, loadingScreen, progress }) { + injectCss(); + const ls = loadingScreen || {}; + const [fade, setFade] = useState(0); + const rootRef = useRef(null); + + useEffect(() => { const t = setTimeout(() => setFade(1), 20); return () => clearTimeout(t); }, []); + + // Источники данных: настройки автора → мета игры → дефолт. + const bg = ls.background || meta?.thumbnail || null; + const cover = ls.cover || meta?.thumbnail || null; + const placeName = ls.placeName || meta?.title || 'Загрузка игры'; + const studioName = ls.studioName + || meta?.author_username || meta?.username || meta?.author || ''; + const verified = ls.verified != null ? !!ls.verified + : !!(meta?.author_verified || meta?.is_verified); + const style = ls.style || 'ken-burns'; + const accent = ls.accentColor || '#5fd0ff'; + const hasProgress = typeof progress === 'number' && progress >= 0; + const pct = hasProgress ? Math.round(Math.max(0, Math.min(1, progress)) * 100) : null; + + // parallax по мыши + const bgRef = useRef(null); + useEffect(() => { + if (style !== 'parallax' || !bgRef.current) return; + const h = (e) => { + const cx = (e.clientX / window.innerWidth - 0.5) * 26; + const cy = (e.clientY / window.innerHeight - 0.5) * 16; + if (bgRef.current) bgRef.current.style.transform = `translate3d(${-cx}px,${-cy}px,0) scale(1.1)`; + }; + window.addEventListener('mousemove', h); + return () => window.removeEventListener('mousemove', h); + }, [style]); + + const particles = style === 'particles' + ? Array.from({ length: 24 }, (_, i) => { + const size = 2 + (i % 4); + const dur = 7 + (i % 7); + return ( + + ); + }) : null; + + return ( +
+ {/* Фоновый слой (Ken Burns / parallax / static) */} + {bg && ( +
+ )} + {/* particles */} + {particles &&
{particles}
} + + {/* Контент */} +
+ {/* Карточка-витрина */} +
+ {!cover && ( + РУБЛОКС • 3D + )} +
+ + {/* Название места */} +
{placeName}
+ + {/* Автор + verified */} + {studioName && ( +
+ {studioName} + {verified && } +
+ )} + + {/* Прогресс-бар */} +
+ {hasProgress ? ( +
+ ) : ( +
+ )} +
+ + {/* Спиннер + статус */} +
+ + {pct != null ? `${pct}%` : 'ЗАГРУЗКА'} +
+
+
+ ); +} diff --git a/src/KubikonPlayer/KubikonPlayer.jsx b/src/KubikonPlayer/KubikonPlayer.jsx index 9debc37..f3ebee5 100644 --- a/src/KubikonPlayer/KubikonPlayer.jsx +++ b/src/KubikonPlayer/KubikonPlayer.jsx @@ -22,6 +22,7 @@ import { useAuth } from '../auth/PlayerAuth'; import RublocsLogo from '../components/RublocsLogo/RublocsLogo'; import useDeviceType from '../hooks/useDeviceType'; import KubikonMobileControls from './KubikonMobileControls'; +import GameLoadingScreen from './GameLoadingScreen'; // Плеер живёт на player.rublox.pro — он не знает SPA-роутов Майнкрафтии // (/kubikon, /login, /auth). Поэтому вместо navigate(...) делаем @@ -216,6 +217,9 @@ const KubikonPlayer = () => { const [forbidden, setForbidden] = useState(false); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); + // Задача 05: конфиг красивого экрана загрузки (из project_data.scene.loadingScreen). + const [loadingScreenCfg, setLoadingScreenCfg] = useState(null); + const [loadProgress, setLoadProgress] = useState(0); // Раньше была стартовая заглушка «тапни чтобы начать» — убрали по // фидбэку, она бесила. Теперь fullscreen опционально через кнопку // в углу. Этот state остался для совместимости с handleMobileStart. @@ -551,11 +555,18 @@ const KubikonPlayer = () => { setMeta(data); setLikesCount(data.likes_count || 0); setDislikesCount(data.dislikes_count || 0); + setLoadProgress(0.3); if (data.project_data) { const parsed = JSON.parse(data.project_data); initialStateRef.current = parsed; + // Задача 05: красивый экран загрузки — конфиг автора (если задан в студии). + try { + const lsc = parsed?.scene?.loadingScreen; + if (lsc && typeof lsc === 'object' && lsc.enabled !== false) setLoadingScreenCfg(lsc); + } catch (e) { /* ignore */ } await scene.loadFromState(parsed); + setLoadProgress(0.7); } // Ждём пока Babylon реально загрузит и скомпилит все @@ -592,6 +603,7 @@ const KubikonPlayer = () => { skinFolderRef.current = mySkin; try { scene.setPlayerModelType?.(mySkin); } catch (e) {} + setLoadProgress(1); setLoading(false); // Засчитываем плей. Передаём user_id (если залогинен) — // это активирует self-cooldown (автор не накручивает себе) @@ -970,6 +982,11 @@ const KubikonPlayer = () => { // Очищаем ref'ы — иначе следующий connectMultiplayer выйдет // на if (mpSyncRef.current || roomRef.current) return. try { sync.stop?.(); } catch (e) {} + // ВАЖНО: dispose() сносит ВСЕ старые меши remote-игроков со + // сцены. Без этого при auto-reconnect (Colyseus rejoin) новый + // MultiplayerSync видит пустую Map и при +remote создаёт + // дубль-меш на каждый кадр (см. фикс 2026-06-05). + try { sync.dispose?.(); } catch (e) {} mpSyncRef.current = null; roomRef.current = null; // Code 1000 / 1001 — нормальное закрытие. Code >= 4000 — наш @@ -1133,46 +1150,13 @@ const KubikonPlayer = () => { outline: 'none', }} /> - {/* Loading-оверлей */} + {/* Задача 05: красивый экран загрузки игры (Ken Burns + название места + автор). */} {loading && ( -
-
-
- -
-
-
- Загрузка игры… -
-
- Рублокс • 3D -
-
+ )} {/* Игровой UI (HUD, GUI, hotbar, прицел в первом лице) */}