diff --git a/src/Game.js b/src/Game.js --- a/src/Game.js +++ b/src/Game.js @@ -28,9 +28,12 @@ function Game({ avatarUrl, gender }) { // 2) реф для группы «города» const cityGroupRef = useRef(null); // 3) реф для группы «интерьера» const interiorGroupRef = useRef(null); - const cleanupTimerRef = useRef(null); + const cleanupTimerRef = useRef(null); + // Глобальный менеджер прогресса загрузки (используем в GLTFLoader) + const loadingManagerRef = useRef(null); + // камеры const orthoCamRef = useRef(null); const fpCamRef = useRef(null); const cameraRef = useRef(null); const rendererRef = useRef(null); @@ -347,6 +350,7 @@ function Game({ avatarUrl, gender }) { })); }, []); //Телефон + const scene = new THREE.Scene(); const playerRef = useRef(null); const cityMeshesRef = useRef([]); const cityObjectsDataRef = useRef([]); const loadedCityObjectsRef = useRef({}); @@ -744,6 +748,59 @@ function Game({ avatarUrl, gender }) { if (!mount) { console.log('[DEBUG] mountRef.current не определён!'); return; } + // ───────────────────────────────────────────── + // Красивый загрузочный оверлей + LoadingManager + // ───────────────────────────────────────────── + let overlayEl = null, barEl = null, textEl = null; + function createLoadingOverlay() { + if (overlayEl) return; + overlayEl = document.createElement('div'); + Object.assign(overlayEl.style, { + position: 'fixed', inset: '0', zIndex: 2000, + display: 'flex', flexDirection: 'column', + alignItems: 'center', justifyContent: 'center', + background: 'linear-gradient(135deg,#0f172a,#1e293b)', + color: '#fff', fontFamily: 'system-ui, Arial, sans-serif' + }); + textEl = document.createElement('div'); + Object.assign(textEl.style, { + fontSize: '24px', fontWeight: 700, opacity: 0.9, marginBottom: '16px' + }); + textEl.textContent = 'Загрузка ресурсов...'; + overlayEl.appendChild(textEl); + const barWrap = document.createElement('div'); + Object.assign(barWrap.style, { + width: '320px', height: '10px', + background: 'rgba(255,255,255,0.15)', + borderRadius: '999px', overflow: 'hidden', + boxShadow: '0 6px 20px rgba(0,0,0,0.35)' + }); + barEl = document.createElement('div'); + Object.assign(barEl.style, { + width: '0%', height: '100%', + transition: 'width .15s ease', + background: 'linear-gradient(90deg,#22d3ee,#38bdf8,#60a5fa)' + }); + barWrap.appendChild(barEl); + overlayEl.appendChild(barWrap); + const pct = document.createElement('div'); + Object.assign(pct.style, { marginTop: '12px', fontSize: '14px', opacity: 0.8 }); + pct.id = 'loadingPct'; + pct.textContent = '0%'; + overlayEl.appendChild(pct); + document.body.appendChild(overlayEl); + } + function updateLoadingOverlay(percent, text) { + if (!overlayEl) return; + const p = Math.max(0, Math.min(100, Math.round(percent || 0))); + if (barEl) barEl.style.width = p + '%'; + const pct = overlayEl.querySelector('#loadingPct'); + if (pct) pct.textContent = p + '%'; + if (text && textEl) textEl.textContent = text; + } + function removeLoadingOverlay() { + if (!overlayEl) return; + overlayEl.style.transition = 'opacity .2s ease'; + overlayEl.style.opacity = '0'; + setTimeout(() => { + overlayEl && overlayEl.remove(); + overlayEl = barEl = textEl = null; + }, 220); + } + // Общий менеджер загрузки (для GLTF/Texture и т.п.) + const loadingManager = new THREE.LoadingManager(); + loadingManagerRef.current = loadingManager; + loadingManager.onStart = (_url, loaded, total) => { + createLoadingOverlay(); + updateLoadingOverlay(total ? (loaded / total) * 100 : 5, 'Загрузка ресурсов...'); + }; + loadingManager.onProgress = (_url, loaded, total) => { + updateLoadingOverlay(total ? (loaded / total) * 100 : 50); + }; + loadingManager.onLoad = () => { + updateLoadingOverlay(100, 'Инициализация сцены...'); + setTimeout(removeLoadingOverlay, 150); + }; + console.log('–– useEffect начало'); const baseOffset = new THREE.Vector3(-200, 150, -200); const planarDist = Math.hypot(baseOffset.x, baseOffset.z); const radius = Math.hypot(planarDist, baseOffset.y); @@ -825,8 +882,9 @@ function Game({ avatarUrl, gender }) { socket.on('economy:inventory', setInventory); socket.on('gameTime:update', ({ time }) => setGameTime(time)); - const gltfLoader = new GLTFLoader(); - const animLoader = new GLTFLoader(); + // Лоадеры, учитывающиеся в прогрессе через loadingManagerRef + const gltfLoader = new GLTFLoader(loadingManagerRef.current || undefined); + const animLoader = new GLTFLoader(loadingManagerRef.current || undefined); async function loadPlayerModel(avatarUrl) { return new Promise((resolve, reject) => { gltfLoader.load(avatarUrl, (gltf) => { if (!gltf.scene) return reject('GLTF.scene отсутствует'); @@ -1168,6 +1226,18 @@ function Game({ avatarUrl, gender }) { setSelectedHouse(null); } + // Мини-лоадер при загрузке интерьеров (обёртка поверх loadInteriorScene) + const _origLoadInteriorScene = loadInteriorScene; + loadInteriorScene = async (interiorId) => { + try { + // показываем мини-оверлей на время подзагрузки интерьера + createLoadingOverlay(); + updateLoadingOverlay(30, 'Загрузка интерьера...'); + await _origLoadInteriorScene(interiorId); + } finally { + setTimeout(removeLoadingOverlay, 120); + } + }; + function onMouseWheel(e) { e.preventDefault(); const delta = -e.deltaY * 0.001; if (e.ctrlKey) {