diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b028e32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +backup/ +backup_v*/ +*.bak* +node_modules/ diff --git a/Dockerfile b/Dockerfile index 1e8ecff..cdc5e28 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,11 @@ COPY nginx.conf /etc/nginx/conf.d/default.conf COPY index.html /usr/share/nginx/html/index.html COPY style.css /usr/share/nginx/html/style.css COPY game.js /usr/share/nginx/html/game.js +COPY three147.min.js /usr/share/nginx/html/three147.min.js +COPY game3d.js /usr/share/nginx/html/game3d.js +COPY index-v2.html /usr/share/nginx/html/v2.html +COPY manifest.json /usr/share/nginx/html/manifest.json +COPY sw.js /usr/share/nginx/html/sw.js COPY src/ /usr/share/nginx/html/src/ EXPOSE 80 diff --git a/backup/20260103_082922/Dockerfile b/backup/20260103_082922/Dockerfile deleted file mode 100644 index fd37297..0000000 --- a/backup/20260103_082922/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM nginx:alpine - -# Копируем файлы игры в директорию nginx -COPY index.html /usr/share/nginx/html/index.html -COPY style.css /usr/share/nginx/html/style.css -COPY game.js /usr/share/nginx/html/game.js - -# Используем конфигурацию nginx по умолчанию -EXPOSE 80 - -CMD ["nginx", "-g", "daemon off;"] diff --git a/backup/20260103_082922/Minegrechka.html b/backup/20260103_082922/Minegrechka.html deleted file mode 100644 index 3638896..0000000 --- a/backup/20260103_082922/Minegrechka.html +++ /dev/null @@ -1,2688 +0,0 @@ - - - - - -GrechkaCraft: Multiplayer - - - - - - -
- - -
-
-
❤️ 100   🍗 100
-
🫁 100
-
📍 X:0 Y:0
-
🕒 День
-
🌐 default
- -
- -
🏃
-
💾
-
🔄
-
🔨
- -
-
-
Крафт
- -
-
-
- -
-
-
Инвентарь
- -
-
-
- -
-
ВЫ ПОГИБЛИ
- -
- -
-
🎒
-
- - - -
💬
-
- -
-
←
- -
↑
-
- - - - diff --git a/backup/20260103_082922/Minegrechka_Telegram.html b/backup/20260103_082922/Minegrechka_Telegram.html deleted file mode 100644 index aac7653..0000000 --- a/backup/20260103_082922/Minegrechka_Telegram.html +++ /dev/null @@ -1,2623 +0,0 @@ - - - - - -GrechkaCraft: Multiplayer - - - - - - - - -
- - -
-
-
❤️ 100   🍗 100
-
🫁 100
-
📍 X:0 Y:0
-
🕒 День
-
🌐 default
- -
- -
🏃
-
💾
-
🔨
- -
-
-
Крафт
- -
-
-
- -
-
-
Инвентарь
- -
-
-
- -
-
ВЫ ПОГИБЛИ
- -
- -
-
🎒
-
- - - -
💬
-
- -
-
←
- -
↑
-
- - - - diff --git a/backup/20260103_082922/backup/20260103_082922/Dockerfile b/backup/20260103_082922/backup/20260103_082922/Dockerfile deleted file mode 100644 index fd37297..0000000 --- a/backup/20260103_082922/backup/20260103_082922/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM nginx:alpine - -# Копируем файлы игры в директорию nginx -COPY index.html /usr/share/nginx/html/index.html -COPY style.css /usr/share/nginx/html/style.css -COPY game.js /usr/share/nginx/html/game.js - -# Используем конфигурацию nginx по умолчанию -EXPOSE 80 - -CMD ["nginx", "-g", "daemon off;"] diff --git a/backup/20260103_082922/backup/20260103_082922/Minegrechka.html b/backup/20260103_082922/backup/20260103_082922/Minegrechka.html deleted file mode 100644 index 3638896..0000000 --- a/backup/20260103_082922/backup/20260103_082922/Minegrechka.html +++ /dev/null @@ -1,2688 +0,0 @@ - - - - - -GrechkaCraft: Multiplayer - - - - - - -
- - -
-
-
❤️ 100   🍗 100
-
🫁 100
-
📍 X:0 Y:0
-
🕒 День
-
🌐 default
- -
- -
🏃
-
💾
-
🔄
-
🔨
- -
-
-
Крафт
- -
-
-
- -
-
-
Инвентарь
- -
-
-
- -
-
ВЫ ПОГИБЛИ
- -
- -
-
🎒
-
- - - -
💬
-
- -
-
←
- -
↑
-
- - - - diff --git a/backup/20260103_082922/backup/20260103_082922/Minegrechka_Telegram.html b/backup/20260103_082922/backup/20260103_082922/Minegrechka_Telegram.html deleted file mode 100644 index aac7653..0000000 --- a/backup/20260103_082922/backup/20260103_082922/Minegrechka_Telegram.html +++ /dev/null @@ -1,2623 +0,0 @@ - - - - - -GrechkaCraft: Multiplayer - - - - - - - - -
- - -
-
-
❤️ 100   🍗 100
-
🫁 100
-
📍 X:0 Y:0
-
🕒 День
-
🌐 default
- -
- -
🏃
-
💾
-
🔨
- -
-
-
Крафт
- -
-
-
- -
-
-
Инвентарь
- -
-
-
- -
-
ВЫ ПОГИБЛИ
- -
- -
-
🎒
-
- - - -
💬
-
- -
-
←
- -
↑
-
- - - - diff --git a/backup/20260103_082922/backup/20260103_082922/docker-compose.yml b/backup/20260103_082922/backup/20260103_082922/docker-compose.yml deleted file mode 100644 index 3de5ddb..0000000 --- a/backup/20260103_082922/backup/20260103_082922/docker-compose.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: grechka-game -services: - grechka-game: - build: - context: . - dockerfile: Dockerfile - container_name: grechka-game - restart: unless-stopped - networks: - - shared_network - labels: - # Включаем Traefik для этого сервиса - - "traefik.enable=true" - - "traefik.docker.network=shared_network" - - # HTTPS роутер - - "traefik.http.routers.grechka-game.entrypoints=websecure" - - "traefik.http.routers.grechka-game.rule=Host(`grechka.mkn8n.ru`)" - - "traefik.http.routers.grechka-game.tls=true" - - "traefik.http.routers.grechka-game.tls.certresolver=mytlschallenge" - - # Сервис - - "traefik.http.services.grechka-game.loadbalancer.server.port=80" - -networks: - shared_network: - external: true diff --git a/backup/20260103_082922/backup/20260103_082922/game.js b/backup/20260103_082922/backup/20260103_082922/game.js deleted file mode 100644 index 49d8e40..0000000 --- a/backup/20260103_082922/backup/20260103_082922/game.js +++ /dev/null @@ -1,2614 +0,0 @@ -(() => { - // ==================== КОНФИГУРАЦИЯ СЕРВЕРА ==================== - // Возможность переопределить сервер через query string - const urlParams = new URLSearchParams(window.location.search); - const SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru'; - const TELEGRAM_BOT_USERNAME = 'Grechkacraft_bot'; // Имя бота для share-ссылки - const TELEGRAM_APP_SHORT_NAME = 'minegrechka'; // Короткое имя Mini App - - // Защита от mixed content - if (location.protocol === 'https:' && SERVER_URL.startsWith('http://')) { - console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP'); - alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.'); - } - - // ==================== WORLD ID И ИГРОКА ==================== - let worldId = null; - let playerName = localStorage.getItem('minegrechka_playerName') || null; - - // Запрашиваем имя игрока, если его нет - if (!playerName) { - playerName = prompt('Введите ваше имя для игры:') || 'Игрок'; - localStorage.setItem('minegrechka_playerName', playerName); - console.log('Player name set:', playerName); - } - - // Берём worldId из URL или генерируем новый - console.log('Current URL:', window.location.href); - const worldParam = urlParams.get('world'); - console.log('world param:', worldParam); - - // Проверяем на null, undefined или пустую строку - worldId = (worldParam && worldParam.trim() !== '') ? worldParam : null; - - console.log('worldId after params:', worldId, 'type:', typeof worldId); - - // Если worldId отсутствует - генерируем новый и записываем в URL - if (!worldId) { - worldId = Math.random().toString(36).substring(2, 10); - console.log('Generated worldId:', worldId); - - try { - const newUrl = new URL(window.location.href); - newUrl.searchParams.set('world', worldId); - const newUrlString = newUrl.toString(); - console.log('New URL to set:', newUrlString); - - // Проверяем, поддерживается ли history API - if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') { - window.history.replaceState(null, '', newUrlString); - console.log('URL after replaceState:', window.location.href); - console.log('URL after replaceState (direct check):', window.location.search); - } else { - console.error('History API not supported!'); - } - } catch (e) { - console.error('Error updating URL:', e); - } - - console.log('Generated new worldId for browser:', worldId); - } - - console.log('Final worldId:', worldId, 'Player name:', playerName); - - console.log(`Server URL: ${SERVER_URL}, World ID: ${worldId}`); - - // Обработчик клика на worldId для копирования ссылки - document.getElementById('worldId').onclick = () => { - const shareUrl = new URL(window.location.href); - shareUrl.searchParams.set('world', worldId); - const shareUrlString = shareUrl.toString(); - - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(shareUrlString).then(() => { - alert('Ссылка скопирована!'); - }).catch(() => { - alert('Ссылка на мир:\n' + shareUrlString); - }); - } else { - alert('Ссылка на мир:\n' + shareUrlString); - } - }; - - // ==================== SOCKET.IO КЛИЕНТ ==================== - let socket = null; - let isMultiplayer = false; // Флаг для мультиплеерного режима - const otherPlayers = new Map(); // socket_id -> {x, y, color} - let mySocketId = null; - - // Throttle для отправки позиции (10-20 раз в секунду) - let lastMoveSendTime = 0; - const MOVE_SEND_INTERVAL = 0.05; // 50ms = 20 раз в секунду - let lastSentX = 0, lastSentY = 0; - - function initSocket() { - try { - socket = io(SERVER_URL, { - path: '/socket.io/', - transports: ['websocket', 'polling'] - }); - - socket.on('connect', () => { - console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${worldId}`); - mySocketId = socket.id; - isMultiplayer = true; - - // Присоединяемся к миру - socket.emit('join_world', { world_id: worldId, player_name: playerName }); - - // Показываем в UI - worldIdEl.textContent = worldId; - multiplayerStatus.style.display = 'block'; - }); - - socket.on('connect_error', (error) => { - console.error('Socket connection error:', error); - isMultiplayer = false; - }); - - socket.on('disconnect', () => { - console.log('Disconnected from server'); - isMultiplayer = false; - otherPlayers.clear(); - multiplayerStatus.style.display = 'none'; - }); - - // Обработка world_state - socket.on('world_state', (data) => { - console.log('Received world_state:', data); - - // Устанавливаем seed и перегенерируем мир если он изменился - if (data.seed !== undefined && data.seed !== worldSeed) { - const oldSeed = worldSeed; - worldSeed = data.seed; - console.log('World seed changed from', oldSeed, 'to', worldSeed); - - // Очищаем и перегенерируем мир с новым seed - generated.clear(); - grid.clear(); - blocks.length = 0; - placedBlocks = []; - removedBlocks = []; - console.log('World regenerated with new seed:', worldSeed); - } - - // Применяем блоки - if (data.blocks && Array.isArray(data.blocks)) { - for (const block of data.blocks) { - if (block.op === 'set') { - setBlock(block.gx, block.gy, block.t, false); - } else if (block.op === 'remove') { - removeBlock(block.gx, block.gy); - } - } - } - - // Устанавливаем время - if (data.time !== undefined) { - worldTime = data.time; - isNightTime = worldTime > 0.5; - } - - // Если есть spawnPoint от сервера - используем его и генерируем эту позицию - if (data.spawnPoint) { - spawnPoint.x = data.spawnPoint.x; - spawnPoint.y = data.spawnPoint.y; - // Генерируем колонну в точке спавна - const spawnGX = Math.floor(spawnPoint.x / TILE); - genColumn(spawnGX); - console.log('Server spawnPoint received and column generated:', spawnPoint); - } else { - // Если spawnPoint не пришёл от сервера - генерируем безопасную позицию - const startGX = 6; - genColumn(startGX); - const surfaceY = surfaceGyAt(startGX); - spawnPoint.x = startGX * TILE; - spawnPoint.y = (surfaceY - 1) * TILE; - console.log('Generated safe spawn point:', spawnPoint, 'surfaceY:', surfaceY); - } - - // Устанавливаем игрока в точку спавна - player.x = spawnPoint.x; - player.y = spawnPoint.y; - player.vx = 0; - player.vy = 0; - player.fallStartY = player.y; - console.log('Player moved to spawn point:', player.x, player.y); - - // Обновляем список игроков - if (data.players && Array.isArray(data.players)) { - otherPlayers.clear(); - for (const p of data.players) { - if (p.socket_id !== mySocketId) { - otherPlayers.set(p.socket_id, { - x: p.x, - y: p.y, - color: getRandomPlayerColor(p.socket_id), - name: p.player_name || 'Игрок' - }); - } - } - // Обновляем счётчик игроков - playerCountEl.textContent = data.players.length; - } - }); - - // Игрок присоединился - socket.on('player_joined', (data) => { - console.log('Player joined:', data.socket_id); - if (data.socket_id !== mySocketId) { - // Генерируем безопасную позицию для нового игрока - const spawnGX = 6; - genColumn(spawnGX); - const surfaceY = surfaceGyAt(spawnGX); - const safeSpawnX = spawnGX * TILE; - const safeSpawnY = (surfaceY - 1) * TILE; - - otherPlayers.set(data.socket_id, { - x: safeSpawnX, - y: safeSpawnY, - color: getRandomPlayerColor(data.socket_id), - name: data.player_name || 'Игрок' - }); - addChatMessage('Система', `Игрок присоединился`); - // Обновляем видимость кнопки сохранения - updateSaveButtonVisibility(); - } - }); - - // Игрок переместился - socket.on('player_moved', (data) => { - if (data.socket_id !== mySocketId && otherPlayers.has(data.socket_id)) { - const p = otherPlayers.get(data.socket_id); - p.x = data.x; - p.y = data.y; - // Обновляем имя, если оно пришло - if (data.player_name) { - p.name = data.player_name; - } - } - }); - - // Игрок покинул - socket.on('player_left', (data) => { - console.log('Player left:', data.socket_id); - otherPlayers.delete(data.socket_id); - addChatMessage('Система', `Игрок покинул игру`); - // Обновляем видимость кнопки сохранения - updateSaveButtonVisibility(); - }); - - // Блок изменён - socket.on('block_changed', (data) => { - if (data.op === 'set') { - setBlock(data.gx, data.gy, data.t, false); - } else if (data.op === 'remove') { - removeBlock(data.gx, data.gy); - } - }); - - // Сообщение в чат - socket.on('chat_message', (data) => { - const senderName = data.socket_id === mySocketId ? 'Вы' : `Игрок ${data.socket_id.substring(0, 6)}`; - addChatMessage(senderName, data.message); - }); - - // Обновление времени - socket.on('time_update', (data) => { - if (data.time !== undefined) { - worldTime = data.time; - isNightTime = worldTime > 0.5; - } - }); - - } catch (e) { - console.error('Error initializing socket:', e); - isMultiplayer = false; - } - } - - // Генерация случайного цвета для игрока на основе socket_id - function getRandomPlayerColor(socketId) { - const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e91e63', '#00bcd4']; - let hash = 0; - for (let i = 0; i < socketId.length; i++) { - hash = ((hash << 5) - hash) + socketId.charCodeAt(i); - hash = hash & hash; - } - return colors[Math.abs(hash) % colors.length]; - } - - // Отправка позиции игрока (с throttle) - function sendPlayerPosition() { - if (!isMultiplayer || !socket || !socket.connected) return; - - const now = performance.now() / 1000; - if (now - lastMoveSendTime < MOVE_SEND_INTERVAL) return; - - // Отправляем только если позиция изменилась - const dx = Math.abs(player.x - lastSentX); - const dy = Math.abs(player.y - lastSentY); - if (dx < 1 && dy < 1) return; - - lastMoveSendTime = now; - lastSentX = player.x; - lastSentY = player.y; - - socket.emit('player_move', { x: player.x, y: player.y, player_name: playerName }); - } - - // Отправка изменения блока - function sendBlockChange(gx, gy, t, op) { - if (!isMultiplayer || !socket || !socket.connected) return; - - socket.emit('block_change', { gx, gy, t, op }); - } - - // ==================== ЧАТ ==================== - const chatMessages = []; - const MAX_CHAT_MESSAGES = 20; - - function addChatMessage(sender, message) { - const time = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); - chatMessages.push({ sender, message, time }); - if (chatMessages.length > MAX_CHAT_MESSAGES) { - chatMessages.shift(); - } - renderChatMessages(); - } - - function renderChatMessages() { - const chatMessagesEl = document.getElementById('chatMessages'); - if (!chatMessagesEl) return; - - chatMessagesEl.innerHTML = chatMessages.map(m => - `
${m.time} ${m.sender}: ${m.message}
` - ).join(''); - - // Прокручиваем вниз - chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight; - } - - function sendChatMessage(message) { - if (!message || message.trim() === '') return; - - if (isMultiplayer && socket && socket.connected) { - socket.emit('chat_message', { message: message.trim() }); - } else { - addChatMessage('Вы', message.trim()); - } - } - - // ==================== ПОДЕЛИТЬСЯ МИРОМ ==================== - function shareWorld() { - const shareUrl = new URL(window.location.href); - shareUrl.searchParams.set('world', worldId); - const shareUrlString = shareUrl.toString(); - - // Копируем в буфер обмена - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(shareUrlString).then(() => { - alert('Ссылка скопирована!'); - }).catch(() => { - alert('Ссылка на мир:\n' + shareUrlString); - }); - } else { - alert('Ссылка на мир:\n' + shareUrlString); - } - } - - // ==================== ИНИЦИАЛИЗАЦИЯ UI ==================== - let chatOpen = false; - - document.getElementById('chatToggle').onclick = () => { - playSound('click'); - chatOpen = !chatOpen; - document.getElementById('chatPanel').style.display = chatOpen ? 'block' : 'none'; - if (chatOpen) { - document.getElementById('chatInput').focus(); - } - }; - - document.getElementById('chatClose').onclick = () => { - playSound('click'); - chatOpen = false; - document.getElementById('chatPanel').style.display = 'none'; - }; - - document.getElementById('chatSend').onclick = () => { - const input = document.getElementById('chatInput'); - sendChatMessage(input.value); - input.value = ''; - }; - - document.getElementById('chatInput').addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - sendChatMessage(e.target.value); - e.target.value = ''; - } - }); - - // ==================== ИНИЦИАЛИЗАЦИЯ СОКЕТА ==================== - // Инициализируем socket - initSocket(); - - // ==================== ЗВУКОВОЙ ДВИЖОК ==================== - const sounds = {}; - function loadSound(id, src) { - const audio = new Audio(); - audio.src = src; - audio.volume = 0.3; - sounds[id] = audio; - } - - // Загрузка звуков - loadSound('splash', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3'); - loadSound('sand1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3'); - loadSound('snow1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3'); - loadSound('stone1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3'); - loadSound('wood1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3'); - loadSound('cloth1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3'); - loadSound('fire', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3'); - loadSound('hit1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3'); - loadSound('attack', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3'); - loadSound('hurt_chicken', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3'); - loadSound('stone_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3'); - loadSound('wood_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3'); - loadSound('click', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3'); - loadSound('explode1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3'); - loadSound('glass1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3'); - loadSound('eat1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3'); - loadSound('step', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3'); - - function playSound(id) { - if(sounds[id]) { - sounds[id].currentTime = 0; - sounds[id].play().catch(e => console.error('Sound error:', e)); - } - } - - // Играем звук при прыжке - const gameEl = document.getElementById('game'); - const canvas = document.getElementById('c'); - const ctx = canvas.getContext('2d'); - - // offscreen light map (не вставляем в DOM) - const lightC = document.createElement('canvas'); - const lightCtx = lightC.getContext('2d'); - - const dpr = Math.max(1, window.devicePixelRatio || 1); - let W=0, H=0; - - const TILE = 40; - - // Мир - const SEA_GY = 14; // уровень воды (gy) - уменьшил для меньших водоёмов - const BEDROCK_GY = 140; // глубина бедрока (gy), чем больше - тем глубже - const GEN_MARGIN_X = 26; // запас генерации по X (в тайлах) - - const heroImg = new Image(); - heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png'; - - // Состояние инвентаря - let showFullInventory = false; - let recentItems = []; // Последние 5 выбранных предметов - - const BLOCKS = { - air: { n:'Воздух', solid:false }, - grass: { n:'Трава', c:'#7cfc00', solid:true }, - dirt: { n:'Грязь', c:'#8b4513', solid:true }, - stone: { n:'Камень', c:'#7f8c8d', solid:true }, - sand: { n:'Песок', c:'#f4d06f', solid:true }, - gravel: { n:'Гравий', c:'#95a5a6', solid:true }, - clay: { n:'Глина', c:'#74b9ff', solid:true }, - wood: { n:'Дерево', c:'#d35400', solid:true }, - planks: { n:'Доски', c:'#e67e22', solid:true }, - ladder: { n:'Лестница',c:'#d35400', solid:false, climbable:true }, - leaves: { n:'Листва', c:'#2ecc71', solid:true }, - glass: { n:'Стекло', c:'rgba(200,240,255,0.25)', solid:true, alpha:0.55 }, - water: { n:'Вода', c:'rgba(52,152,219,0.55)', solid:false, fluid:true }, - coal: { n:'Уголь', c:'#2c3e50', solid:true }, - copper_ore:{ n:'Медь', c:'#e17055', solid:true }, - iron_ore: { n:'Железо', c:'#dcdde1', solid:true }, - gold_ore: { n:'Золото', c:'#f1c40f', solid:true }, - diamond_ore:{n:'Алмаз', c:'#00a8ff', solid:true }, - brick: { n:'Кирпич', c:'#c0392b', solid:true }, - tnt: { n:'TNT', c:'#e74c3c', solid:true, explosive:true }, - campfire: { n:'Костёр', c:'#e67e22', solid:true, lightRadius:190 }, - torch: { n:'Факел', c:'#f9ca24', solid:true, lightRadius:140 }, - bedrock: { n:'Бедрок', c:'#2d3436', solid:true, unbreakable:true }, - flower: { n:'Цветок', c:'#ff4757', solid:false, decor:true }, - bed: { n:'Кровать', c:'#e91e63', solid:true, bed:true }, - boat: { n:'Лодка', c:'#8B4513', solid:false } - }; - - const ITEMS = { - meat: { n:'Сырое мясо', icon:'🥩', food:15 }, - cooked: { n:'Жареное мясо', icon:'🍖', food:45 } - }; - - // Seed мира для детерминированной генерации - // Инициализируем случайным seed, но он будет перезаписан сервером в мультиплеере - let worldSeed = Math.floor(Math.random() * 1000000); - - // Отслеживание изменений мира (для оптимизированного сохранения) - let placedBlocks = []; // [{gx, gy, t}] - блоки, установленные игроком - let removedBlocks = []; // [{gx, gy}] - блоки, удалённые игроком - - // Инструменты - const TOOLS = { - wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', durability: 60, miningPower: 1, craft: { wood: 3, planks: 2 } }, - stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', durability: 130, miningPower: 2, craft: { wood: 1, stone: 3 } }, - iron_pickaxe: { n:'Железная кирка', icon:'⛏️', durability: 250, miningPower: 3, craft: { wood: 1, iron_ore: 2 } }, - wood_sword: { n:'Деревянный меч', icon:'⚔️', durability: 40, damage: 5, craft: { wood: 2, planks: 1 } }, - stone_sword: { n:'Каменный меч', icon:'⚔️', durability: 100, damage: 8, craft: { wood: 1, stone: 2 } }, - iron_sword: { n:'Железный меч', icon:'⚔️', durability: 200, damage: 12, craft: { wood: 1, iron_ore: 1 } } - }; - - // Текстуры блоков (простые) - const tex = {}; - function makeTex(type) { - const t = BLOCKS[type]; - const c = document.createElement('canvas'); - c.width = 32; c.height = 32; - const g = c.getContext('2d'); - - if (type === 'tnt') { - g.fillStyle='#c0392b'; g.fillRect(0,0,32,32); - g.fillStyle='#fff'; g.fillRect(0,12,32,8); - g.fillStyle='#000'; g.font='bold 10px sans-serif'; g.fillText('TNT',6,20); - return c; - } - if (type === 'campfire') { - g.fillStyle='#5d4037'; g.fillRect(4,26,24,6); - g.fillStyle='#3e2723'; g.fillRect(7,23,18,4); - return c; - } - if (type === 'torch') { - g.fillStyle='#6d4c41'; g.fillRect(14,10,4,18); - g.fillStyle='#f39c12'; g.fillRect(12,6,8,8); - return c; - } - if (type === 'glass') { - g.fillStyle='rgba(200,240,255,0.25)'; g.fillRect(0,0,32,32); - g.strokeStyle='rgba(255,255,255,0.65)'; g.strokeRect(2,2,28,28); - g.beginPath(); g.moveTo(5,27); g.lineTo(27,5); g.stroke(); - return c; - } - if (type === 'water') { - g.fillStyle = t.c; g.fillRect(0,0,32,32); - g.fillStyle = 'rgba(255,255,255,0.08)'; - g.fillRect(0,6,32,2); - return c; - } - if (type === 'bed') { - // Основание кровати - g.fillStyle = '#e91e63'; - g.fillRect(0, 0, 32, 32); - // Подушка - g.fillStyle = '#f8bbd0'; - g.fillRect(2, 2, 14, 14); - // Одеяло - g.fillStyle = '#c2185b'; - g.fillRect(16, 4, 14, 24); - // Детали одеяла - g.fillStyle = '#e91e63'; - g.fillRect(18, 6, 10, 20); - return c; - } - if (type === 'flower') { - g.fillStyle='#2ecc71'; g.fillRect(14,14,4,18); - g.fillStyle=t.c; g.beginPath(); g.arc(16,12,6,0,6.28); g.fill(); - return c; - } - if (type === 'boat') { - // Корпус лодки - g.fillStyle = '#8B4513'; - g.fillRect(2, 12, 28, 8); - // Борта - g.fillStyle = '#A0522D'; - g.fillRect(0, 10, 32, 12); - // Внутренность - g.fillStyle = '#DEB887'; - g.fillRect(4, 14, 24, 4); - // Дно - g.fillStyle = '#654321'; - g.fillRect(2, 20, 28, 4); - return c; - } - if (type === 'ladder') { - // Боковые стойки лестницы - g.fillStyle = '#8B4513'; - g.fillRect(4, 0, 4, 32); - g.fillRect(24, 0, 4, 32); - // Ступени - g.fillStyle = '#A0522D'; - g.fillRect(4, 4, 24, 3); - g.fillRect(4, 12, 24, 3); - g.fillRect(4, 20, 24, 3); - g.fillRect(4, 28, 24, 3); - return c; - } - - g.fillStyle = t.c || '#000'; - g.fillRect(0,0,32,32); - - g.fillStyle = 'rgba(0,0,0,0.10)'; - for (let i=0;i<6;i++) g.fillRect((Math.random()*28)|0, (Math.random()*28)|0, 4,4); - - if (type.endsWith('_ore') || type==='coal') { - g.fillStyle = 'rgba(0,0,0,0.35)'; - for (let i=0;i<4;i++) g.fillRect((Math.random()*24)|0, (Math.random()*24)|0, 6,6); - } - return c; - } - Object.keys(BLOCKS).forEach(k => tex[k] = makeTex(k)); - - // Мир-хранилище - const grid = new Map(); // key "gx,gy" => {gx,gy,t, ...} - const blocks = []; // для рендера/перебора видимых - function k(gx,gy){ return gx+','+gy; } - function getBlock(gx,gy){ return grid.get(k(gx,gy)); } - function hasBlock(gx,gy){ return grid.has(k(gx,gy)); } - function isSolid(gx,gy){ - const b = getBlock(gx,gy); - if(!b || b.dead) return false; - const def = BLOCKS[b.t]; - return !!def.solid && !def.fluid && !def.decor; - } - function setBlock(gx,gy,t, isPlayerPlaced = false){ - const key = k(gx,gy); - if(grid.has(key)) return false; - const b = { gx, gy, t, dead:false, active:false, fuse:0 }; - grid.set(key, b); - blocks.push(b); - - // Отслеживаем блоки, установленные игроком - if(isPlayerPlaced){ - placedBlocks.push({gx, gy, t}); - } - - return true; - } - function removeBlock(gx,gy){ - const key = k(gx,gy); - const b = grid.get(key); - if(!b) return null; - if(BLOCKS[b.t].unbreakable) return null; - grid.delete(key); - b.dead = true; - - // Отслеживаем удалённые блоки - const wasPlayerPlaced = placedBlocks.some(pb => pb.gx === gx && pb.gy === gy); - if(wasPlayerPlaced){ - // Удаляем из placedBlocks - placedBlocks = placedBlocks.filter(pb => !(pb.gx === gx && pb.gy === gy)); - } else { - // Это природный блок - добавляем в removedBlocks - removedBlocks.push({gx, gy}); - } - - return b; - } - - // Физика жидкости - const waterUpdateQueue = new Set(); - let waterUpdateTimer = 0; - const WATER_UPDATE_INTERVAL = 0.05; // Обновляем воду каждые 0.05 секунды - - function updateWaterPhysics(dt){ - waterUpdateTimer += dt; - if(waterUpdateTimer < WATER_UPDATE_INTERVAL) return; - waterUpdateTimer = 0; - - // Ограничиваем количество водных блоков для обработки (оптимизация) - const MAX_WATER_BLOCKS_PER_UPDATE = 50; - let processedCount = 0; - - // Собираем только видимые водные блоки в очередь (оптимизация) - waterUpdateQueue.clear(); - const minGX = Math.floor(camX/TILE) - 10; - const maxGX = Math.floor((camX+W)/TILE) + 10; - const minGY = Math.floor(camY/TILE) - 10; - const maxGY = Math.floor((camY+H)/TILE) + 10; - - for(const b of blocks){ - if(processedCount >= MAX_WATER_BLOCKS_PER_UPDATE) break; - if(!b.dead && b.t === 'water' && - b.gx >= minGX && b.gx <= maxGX && - b.gy >= minGY && b.gy <= maxGY){ - waterUpdateQueue.add(k(b.gx, b.gy)); - processedCount++; - } - } - - // Обновляем воду с ограничением глубины распространения - const processed = new Set(); - const toAdd = []; - const MAX_WATER_DEPTH = 20; // Максимальная глубина распространения воды - - for(const key of waterUpdateQueue){ - if(processed.has(key)) continue; - const b = grid.get(key); - if(!b || b.dead) continue; - processed.add(key); - - const gx = b.gx; - const gy = b.gy; - - // Проверяем глубину - не распространяем воду слишком глубоко - if(gy > SEA_GY + MAX_WATER_DEPTH) continue; - - // Проверяем, можно ли воде упасть вниз - const belowKey = k(gx, gy + 1); - const below = grid.get(belowKey); - - // Внизу пусто - вода создаёт новый блок внизу (но не удаляется сверху) - if(!below || below.dead){ - // Ограничиваем создание новых водных блоков - if(toAdd.length < 20){ // Максимум 20 новых блоков за обновление - toAdd.push({gx, gy: gy + 1, t: 'water'}); - processed.add(belowKey); - } - continue; - } - - // Если внизу не вода и не твёрдый блок - вода может течь вниз - if(!isSolid(gx, gy + 1) && below && below.t !== 'water'){ - if(toAdd.length < 20){ - toAdd.push({gx, gy: gy + 1, t: 'water'}); - processed.add(belowKey); - } - continue; - } - - // Если внизу твёрдый блок или вода - вода растекается горизонтально - // Проверяем левую сторону - const leftKey = k(gx - 1, gy); - const left = grid.get(leftKey); - if(!left || left.dead){ - if(toAdd.length < 20){ - toAdd.push({gx: gx - 1, gy, t: 'water'}); - processed.add(leftKey); - } - continue; - } - - // Проверяем правую сторону - const rightKey = k(gx + 1, gy); - const right = grid.get(rightKey); - if(!right || right.dead){ - if(toAdd.length < 20){ - toAdd.push({gx: gx + 1, gy, t: 'water'}); - processed.add(rightKey); - } - continue; - } - } - - // Применяем изменения (только добавляем новые блоки) - for(const newData of toAdd){ - const key = k(newData.gx, newData.gy); - if(!grid.has(key)){ - const b = { - gx: newData.gx, - gy: newData.gy, - t: newData.t, - dead: false, - active: false, - fuse: 0 - }; - grid.set(key, b); - blocks.push(b); - } - } - - // Очищаем мёртвые блоки из массива - for(let i = blocks.length - 1; i >= 0; i--){ - if(blocks[i].dead){ - blocks.splice(i, 1); - } - } - } - - // Инвентарь - const inv = { - dirt:6, stone:0, sand:0, gravel:0, clay:0, - wood:0, planks:0, ladder:0, leaves:0, coal:0, - copper_ore:0, iron_ore:0, gold_ore:0, diamond_ore:0, - brick:0, glass:0, - tnt:1, campfire:0, torch:0, - meat:0, cooked:0, - wood_pickaxe:0, stone_pickaxe:0, iron_pickaxe:0, - wood_sword:0, stone_sword:0, iron_sword:0, - bed:0, boat:0 - }; - let selected = 'dirt'; - - const RECIPES = [ - { out:'planks', qty:4, cost:{ wood:1 } }, - { out:'ladder', qty:3, cost:{ planks:7 } }, - { out:'torch', qty:2, cost:{ coal:1, planks:1 } }, - { out:'glass', qty:1, cost:{ sand:3 } }, - { out:'brick', qty:1, cost:{ stone:2, clay:1 } }, - { out:'campfire', qty:1, cost:{ wood:1, coal:1 } }, - { out:'tnt', qty:1, cost:{ sand:2, coal:1 } }, - { out:'bed', qty:1, cost:{ wood: 3, planks: 3 } }, - { out:'boat', qty:1, cost:{ wood: 5 } }, - { out:'wood_pickaxe', qty:1, cost:{ wood: 3, planks: 2 } }, - { out:'stone_pickaxe', qty:1, cost:{ wood: 1, stone: 3 } }, - { out:'iron_pickaxe', qty:1, cost:{ wood: 1, iron_ore: 2 } }, - { out:'wood_sword', qty:1, cost:{ wood: 2, planks: 1 } }, - { out:'stone_sword', qty:1, cost:{ wood: 1, stone: 2 } }, - { out:'iron_sword', qty:1, cost:{ wood: 1, iron_ore: 1 } } - ]; - - // UI - const hpEl = document.getElementById('hp'); - const foodEl = document.getElementById('food'); - const sxEl = document.getElementById('sx'); - const syEl = document.getElementById('sy'); - const todEl = document.getElementById('tod'); - const worldIdEl = document.getElementById('worldId'); - const playerCountEl = document.getElementById('playerCount'); - const hotbarEl = document.getElementById('hotbar'); - const craftPanel = document.getElementById('craftPanel'); - const recipesEl = document.getElementById('recipes'); - const deathEl = document.getElementById('death'); - const inventoryPanel = document.getElementById('inventoryPanel'); - const inventoryGrid = document.getElementById('inventoryGrid'); - - // Клик на часы для включения ночи - todEl.style.cursor = 'pointer'; - todEl.onclick = () => { - playSound('click'); - worldTime = 0.6; // Устанавливаем ночь - isNightTime = true; - }; - - function rebuildHotbar(){ - hotbarEl.innerHTML=''; - - // Показываем последние 5 выбранных предметов (если они есть в инвентаре) - const items = recentItems.filter(id => inv[id] > 0).slice(0, 5); - - for(const id of items){ - const s = document.createElement('div'); - s.className = 'slot'+(id===selected?' sel':''); - if(BLOCKS[id]) { - s.style.backgroundImage = `url(${tex[id].toDataURL()})`; - s.style.backgroundSize = 'cover'; - } else if(ITEMS[id]) { - s.textContent = ITEMS[id].icon; - } else if(TOOLS[id]) { - s.textContent = TOOLS[id].icon; - } - const c = document.createElement('div'); - c.className='count'; - c.textContent = inv[id]; - s.appendChild(c); - s.onclick = () => { - playSound('click'); // Звук клика по инвентарю - selected=id; - // Обновляем список последних предметов - recentItems = recentItems.filter(item => item !== id); // Удаляем если уже есть - recentItems.unshift(id); // Добавляем в начало - recentItems = recentItems.slice(0, 5); // Оставляем только 5 - rebuildHotbar(); - }; - hotbarEl.appendChild(s); - } - } - - function renderInventory() { - inventoryGrid.innerHTML = ''; - - // Создаём сетку инвентаря 7x3 - const items = Object.keys(inv).filter(id => inv[id] > 0); - - // Добавляем пустые слоты для полной сетки - for(let i = 0; i < 21; i++) { - const slot = document.createElement('div'); - slot.className = 'inv-slot' + (i < items.length && items[i] === selected ? ' sel' : ''); - - if(i < items.length) { - const id = items[i]; - if(BLOCKS[id]) { - slot.style.backgroundImage = `url(${tex[id].toDataURL()})`; - slot.style.backgroundSize = 'cover'; - } else if(ITEMS[id]) { - slot.textContent = ITEMS[id].icon; - } else if(TOOLS[id]) { - slot.textContent = TOOLS[id].icon; - } - - const count = document.createElement('div'); - count.className = 'inv-count'; - count.textContent = inv[id]; - slot.appendChild(count); - - slot.onclick = () => { - playSound('click'); // Звук клика по инвентарю - selected = id; - // Обновляем список последних предметов - recentItems = recentItems.filter(item => item !== id); // Удаляем если уже есть - recentItems.unshift(id); // Добавляем в начало - recentItems = recentItems.slice(0, 5); // Оставляем только 5 - rebuildHotbar(); - renderInventory(); - }; - } - - inventoryGrid.appendChild(slot); - } - } - - function canCraft(r){ - for(const res in r.cost){ - if((inv[res]||0) < r.cost[res]) return false; - } - return true; - } - function renderCraft(){ - recipesEl.innerHTML=''; - for(const r of RECIPES){ - const row = document.createElement('div'); - row.className='recipe'; - const icon = document.createElement('div'); - icon.className='ricon'; - icon.style.backgroundImage = `url(${tex[r.out].toDataURL()})`; - const info = document.createElement('div'); - info.className='rinfo'; - const nm = document.createElement('div'); - nm.className='rname'; - nm.textContent = `${BLOCKS[r.out].n} x${r.qty}`; - const cs = document.createElement('div'); - cs.className='rcost'; - cs.textContent = Object.keys(r.cost).map(x => `${BLOCKS[x].n}: ${(inv[x]||0)}/${r.cost[x]}`).join(' '); - info.appendChild(nm); info.appendChild(cs); - const btn = document.createElement('button'); - btn.className='rcraft'; - btn.textContent='Создать'; - btn.disabled = !canCraft(r); - btn.onclick = () => { - if(!canCraft(r)) return; - playSound('click'); // Звук клика по кнопке крафта - for(const res in r.cost) inv[res]-=r.cost[res]; - inv[r.out] = (inv[r.out]||0) + r.qty; - rebuildHotbar(); - renderCraft(); - }; - row.appendChild(icon); row.appendChild(info); row.appendChild(btn); - recipesEl.appendChild(row); - } - } - - let craftOpen=false; - let inventoryOpen = false; - - document.getElementById('craftBtn').onclick = () => { - playSound('click'); // Звук клика по кнопке - craftOpen = !craftOpen; - craftPanel.style.display = craftOpen ? 'block' : 'none'; - if(craftOpen) { - renderCraft(); - // Закрываем инвентарь если открыт крафт - inventoryOpen = false; - inventoryPanel.style.display = 'none'; - } - }; - document.getElementById('craftClose').onclick = () => { - playSound('click'); // Звук клика по кнопке - craftOpen = false; - craftPanel.style.display = 'none'; - }; - - // Кнопка открытия инвентаря - document.getElementById('invToggle').onclick = () => { - playSound('click'); // Звук клика по кнопке - inventoryOpen = true; - inventoryPanel.style.display = 'block'; - renderInventory(); - // Закрываем крафт если открыт инвентарь - craftOpen = false; - craftPanel.style.display = 'none'; - }; - - document.getElementById('inventoryClose').onclick = () => { - playSound('click'); // Звук клика по кнопке - inventoryOpen = false; - inventoryPanel.style.display = 'none'; - }; - - // Кнопка сохранения игры (только для одиночного режима) - const saveBtn = document.getElementById('saveBtn'); - saveBtn.onclick = () => { - playSound('click'); - saveGame(); - alert('Игра сохранена!'); - }; - - // Кнопка сброса игры (удаление сохранения и создание нового мира) - const resetBtn = document.getElementById('resetBtn'); - resetBtn.onclick = () => { - if (confirm('Вы уверены, что хотите удалить сохранение и начать новую игру?')) { - playSound('click'); - - // Удаляем сохранение из localStorage - try { - localStorage.removeItem(SAVE_KEY); - console.log('Сохранение удалено из localStorage'); - } catch (e) { - console.warn('Ошибка удаления сохранения:', e); - } - - // Сбрасываем in-memory сохранение - inMemorySave = null; - - // Генерируем новый worldId - worldId = Math.random().toString(36).substring(2, 10); - console.log('Новый worldId после сброса:', worldId); - - // Обновляем URL - try { - const newUrl = new URL(window.location.href); - newUrl.searchParams.set('world', worldId); - const newUrlString = newUrl.toString(); - - if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') { - window.history.replaceState(null, '', newUrlString); - console.log('URL обновлён:', newUrlString); - } - } catch (e) { - console.error('Ошибка обновления URL:', e); - } - - // Перезагружаем страницу - location.reload(); - } - }; - - // Показываем кнопку сохранения только если играем одни - function updateSaveButtonVisibility() { - if (isMultiplayer && otherPlayers.size > 0) { - saveBtn.style.display = 'none'; - } else { - saveBtn.style.display = 'flex'; - } - } - - // Режимы - const MODES = [{id:'move',icon:'🏃'},{id:'mine',icon:'⛏️'},{id:'build',icon:'🧱'}]; - let modeIdx=0; - const modeBtn = document.getElementById('modeBtn'); - function mode(){ return MODES[modeIdx].id; } - modeBtn.onclick = () => { - playSound('click'); // Звук клика по кнопке режима - modeIdx=(modeIdx+1)%MODES.length; modeBtn.textContent=MODES[modeIdx].icon; - }; - - // День/ночь (автоматический цикл) - let isNightTime = false; - - // Управление - const inp = { l:false, r:false, j:false }; - function bindHold(el, key){ - const down=(e)=>{ e.preventDefault(); inp[key]=true; }; - const up=(e)=>{ e.preventDefault(); inp[key]=false; }; - el.addEventListener('pointerdown', down); - el.addEventListener('pointerup', up); - el.addEventListener('pointerleave', up); - } - bindHold(document.getElementById('left'),'l'); - bindHold(document.getElementById('right'),'r'); - bindHold(document.getElementById('jump'),'j'); - - window.addEventListener('keydown', (e)=>{ - if(e.code==='KeyA'||e.code==='ArrowLeft') inp.l=true; - if(e.code==='KeyD'||e.code==='ArrowRight') inp.r=true; - if(e.code==='Space'||e.code==='KeyW'||e.code==='ArrowUp') inp.j=true; - }); - window.addEventListener('keyup', (e)=>{ - if(e.code==='KeyA'||e.code==='ArrowLeft') inp.l=false; - if(e.code==='KeyD'||e.code==='ArrowRight') inp.r=false; - if(e.code==='Space'||e.code==='KeyW'||e.code==='ArrowUp') inp.j=false; - }); - - // Лодка - const boat = { - x: 0, y: 0, - w: 34, h: 34, - vx: 0, vy: 0, - active: false, - inWater: false - }; - - // Игрок - const player = { - x: 6*TILE, y: 0*TILE, - w: 34, h: 34, - vx: 0, vy: 0, - grounded: false, - inWater: false, - headInWater: false, - hp: 100, - hunger: 100, - o2: 100, - invuln: 0, - fallStartY: 0, - lastStepTime: 0, - sleeping: false, - inBoat: false - }; - - // Сохраняем начальную позицию для возрождения - const spawnPoint = { x: 6*TILE, y: 0*TILE }; - - // Система сохранения игры (localStorage + in-memory fallback) - const SAVE_KEY = 'minegrechka_save'; - let db = null; // Оставляем для совместимости, но не используем - let inMemorySave = null; // Запасное сохранение в памяти - - // Инициализация (localStorage + in-memory fallback) - function initDB(){ - return new Promise((resolve) => { - console.log('Используем localStorage для сохранений (sandbox режим)'); - resolve(null); - }); - } - - // Детерминированный генератор псевдослучайных чисел на основе seed - function seededRandom(gx, gy){ - const n = Math.sin(gx * 12.9898 + gy * 78.233 + worldSeed * 0.1) * 43758.5453; - return n - Math.floor(n); - } - - function saveGame(){ - const saveData = { - version: 2, - worldSeed: worldSeed, - player: { - x: player.x, - y: player.y, - hp: player.hp, - hunger: player.hunger, - o2: player.o2 - }, - inventory: inv, - time: worldTime, - isNight: isNightTime, - // Сохраняем только изменения - placedBlocks: placedBlocks.slice(), - removedBlocks: removedBlocks.slice() - }; - - const saveSize = JSON.stringify(saveData).length; - console.log('Сохранение: player HP:', player.hp, 'hunger:', player.hunger, 'o2:', player.o2); - - // Пробуем сохранить в localStorage (основной метод для персистентности) - try { - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - console.log(`Игра сохранена в localStorage (размер: ${saveSize} байт)`); - } catch(e){ - console.warn('Ошибка сохранения в localStorage, используем только in-memory:', e); - - // Если localStorage недоступен, используем in-memory fallback - inMemorySave = saveData; - console.log(`Игра сохранена в памяти (размер: ${saveSize} байт)`); - } - } - - function loadGame(){ - return new Promise((resolve, reject) => { - // Пробуем localStorage - try { - const localSave = localStorage.getItem(SAVE_KEY); - if(localSave){ - const parsed = JSON.parse(localSave); - console.log('Загружено из localStorage, player HP:', parsed.player?.hp); - resolve(parsed); - return; - } - } catch(e){ - console.warn('Ошибка доступа к localStorage:', e); - } - - // Если localStorage недоступен, используем in-memory сохранение - if(inMemorySave){ - console.log('Загружено из in-memory сохранения, player HP:', inMemorySave.player?.hp); - resolve(inMemorySave); - return; - } - - console.log('Сохранение не найдено'); - resolve(null); - }); - } - - // Миграция с версии 1 на версию 2 - function migrateV1toV2(saveData){ - console.log('Миграция сохранения с версии 1 на версию 2...'); - - // Сохраняем seed из текущей игры (так как v1 его не хранил) - saveData.worldSeed = worldSeed; - - // Инициализируем массивы изменений - saveData.placedBlocks = []; - saveData.removedBlocks = []; - - // Для v1 нам нужно сравнить сгенерированные блоки с тем, что должно быть по seed - // Это сложно сделать без полной перегенерации, поэтому для v1 сохраняем только seed - // и при загрузке просто перегенерируем мир - - // Удаляем старые данные - delete saveData.generatedBlocks; - - saveData.version = 2; - console.log('Миграция завершена'); - } - - async function applySave(saveData){ - if(!saveData) return; - - console.log('=== applySave START ==='); - console.log('player HP before applySave:', player.hp); - console.log('saveData.player.hp:', saveData.player?.hp); - - // Миграция версий - if(saveData.version === 1){ - migrateV1toV2(saveData); - } - - // Восстанавливаем seed - if(saveData.worldSeed !== undefined){ - worldSeed = saveData.worldSeed; - } - - // Восстанавливаем игрока - if(saveData.player){ - player.x = saveData.player.x; - player.y = saveData.player.y; - player.hunger = saveData.player.hunger; - player.o2 = saveData.player.o2; - - // Обновляем spawnPoint на позицию из сохранения - spawnPoint.x = player.x; - spawnPoint.y = player.y; - - // Проверяем HP из сохранения - если <= 0, устанавливаем 100 - const savedHP = saveData.player.hp; - console.log('Saved HP from file:', savedHP); - if(savedHP <= 0){ - console.log('WARNING: Saved HP is <= 0, setting to 100!'); - player.hp = 100; - } else { - player.hp = savedHP; - } - console.log('player HP after restore:', player.hp); - console.log('spawnPoint обновлён из сохранения: x=', spawnPoint.x, 'y=', spawnPoint.y); - } else { - console.log('No player data in save, setting default HP: 100'); - player.hp = 100; - } - - console.log('=== applySave END ==='); - - // Восстанавливаем инвентарь - if(saveData.inventory){ - for(const key in saveData.inventory){ - inv[key] = saveData.inventory[key]; - } - } - - // Восстанавливаем время - if(saveData.time !== undefined){ - worldTime = saveData.time; - } - - // Восстанавливаем день/ночь - if(saveData.isNight !== undefined){ - isNightTime = saveData.isNight; - } - - // Перегенерируем мир по seed - regenerateVisibleChunks(); - - // Применяем изменения (только для v2) - if(saveData.version === 2){ - // Применяем блоки, установленные игроком - for(const block of saveData.placedBlocks){ - setBlock(block.gx, block.gy, block.t, true); - } - - // Применяем удалённые блоки - for(const block of saveData.removedBlocks){ - removeBlock(block.gx, block.gy); - } - - // Восстанавливаем массивы изменений - placedBlocks = saveData.placedBlocks || []; - removedBlocks = saveData.removedBlocks || []; - } - - rebuildHotbar(); - console.log('Игра загружена'); - } - - // Камера (двухосевая) - let camX=0, camY=0; - - // День/ночь - let worldTime=0; - const DAY_LEN=360; // замедлил смену дня/ночи в 3 раза - - // Облака - const clouds = Array.from({length:10}, ()=>({ - x: Math.random()*2000, - y: -200 - Math.random()*260, // выше экрана, потому что мир по Y теперь двигается - w: 80+Math.random()*120, - s: 12+Math.random()*20 - })); - - // Частицы (взрыв) - const parts = []; - function spawnExplosion(x,y, power){ - const n = Math.floor(16 + power*10); - for(let i=0;i 100){ - playSound('splash'); - } - } - - function resolveY(e){ - // Всегда пересчитываем grounded (не держим "липким") - e.grounded = false; - - const x1 = e.x + 2; - const x2 = e.x + e.w - 2; - - // Проверяем, находится ли игрок на лестнице - const cx = e.x + e.w/2; - const cy = e.y + e.h/2; - const gx = Math.floor(cx / TILE); - const gy = Math.floor(cy / TILE); - const b = getBlock(gx, gy); - const onLadder = b && BLOCKS[b.t] && BLOCKS[b.t].climbable; - - // Если на лестнице - можно двигаться вверх/вниз - if(onLadder && e === player){ - e.grounded = true; - e.vy *= 0.8; // замедляем падение на лестнице - - // Если нажимаем прыжок на лестнице - поднимаемся - if(inp.j && e.vy > -200){ - e.vy = -200; - } - // Если не нажимаем прыжок - медленно спускаемся - else if(!inp.j && e.vy > 0){ - e.vy = Math.min(e.vy, 100); - } - return; - } - - // 1) Если движемся вниз ИЛИ стоим (vy >= 0) — проверяем опору под ногами - // Берём точку на 1px ниже стопы, чтобы не зависеть от ровного попадания в границу тайла. - if(e.vy >= 0){ - const probeY = e.y + e.h + 1; - const gy = Math.floor(probeY / TILE); - const gxA = Math.floor(x1 / TILE); - const gxB = Math.floor(x2 / TILE); - - if(isSolid(gxA, gy) || isSolid(gxB, gy)){ - e.y = gy * TILE - e.h; // прижимаем к полу - e.vy = 0; - e.grounded = true; - - // урон от падения — только игроку и только не в воде - if(e === player && !player.inWater){ - const fallTiles = (e.y - e.fallStartY) / TILE; - if(fallTiles > 6) player.hp -= (fallTiles - 6) * 10; - } - if(e === player) e.fallStartY = e.y; - } - } - - // 2) Проверка на возможность запрыгнуть на блок, если игрок рядом с ним - if(e.vy < 0 && e === player){ - const gy = Math.floor(e.y / TILE); - const gxA = Math.floor(x1 / TILE); - const gxB = Math.floor(x2 / TILE); - - // Проверяем, есть ли блок рядом с игроком - if((isSolid(gxA, gy) || isSolid(gxB, gy)) && !isSolid(gxA, gy - 1) && !isSolid(gxB, gy - 1)){ - e.y = (gy + 1) * TILE; - e.vy = 0; - e.grounded = true; - if(e === player) e.fallStartY = e.y; - console.log("Jumped onto block!"); - } - } - - // 2) Если движемся вверх — проверяем потолок - if(e.vy < 0){ - const gy = Math.floor(e.y / TILE); - const gxA = Math.floor(x1 / TILE); - const gxB = Math.floor(x2 / TILE); - if(isSolid(gxA, gy) || isSolid(gxB, gy)){ - e.y = (gy + 1) * TILE; - e.vy = 0; - } - } - } - - function resolveX(e){ - const y1 = e.y + 2; - const y2 = e.y + e.h - 2; - - if(e.vx > 0){ - const gx = Math.floor((e.x + e.w)/TILE); - const gyA = Math.floor(y1/TILE); - const gyB = Math.floor(y2/TILE); - if(isSolid(gx, gyA) || isSolid(gx, gyB)){ - e.x = gx*TILE - e.w; - e.vx = 0; - } - } else if(e.vx < 0){ - const gx = Math.floor(e.x/TILE); - const gyA = Math.floor(y1/TILE); - const gyB = Math.floor(y2/TILE); - if(isSolid(gx, gyA) || isSolid(gx, gyB)){ - e.x = (gx+1)*TILE; - e.vx = 0; - } - } - } - - // TNT логика: цепь + усиление - const activeTNT = new Set(); // хранит key - function activateTNT(b, fuse=3.2){ - if(b.dead) return; - if(b.active) return; - b.active=true; - b.fuse=fuse; - activeTNT.add(k(b.gx,b.gy)); - } - - function explodeAt(gx,gy){ - const center = getBlock(gx,gy); - if(!center) return; - - // усиление: считаем сколько TNT рядом (в радиусе 2) и активируем их «почти сразу» - let bonus = 0; - for(let x=gx-2; x<=gx+2; x++){ - for(let y=gy-2; y<=gy+2; y++){ - const b = getBlock(x,y); - if(b && !b.dead && b.t==='tnt' && !(x===gx && y===gy)){ - bonus += 0.8; - activateTNT(b, 0.12); // цепь - } - } - } - - const power = 1 + bonus; // условная мощность - const radius = 3.2 + bonus*0.7; // радиус разрушения в тайлах - const dmgR = 150 + bonus*60; // радиус урона в пикселях - - removeBlock(gx,gy); - activeTNT.delete(k(gx,gy)); - playSound('explode1'); // Звук взрыва - spawnExplosion(gx*TILE + TILE/2, gy*TILE + TILE/2, power); - - for(let x=Math.floor(gx-radius); x<=Math.ceil(gx+radius); x++){ - for(let y=Math.floor(gy-radius); y<=Math.ceil(gy+radius); y++){ - const d = Math.hypot(x-gx, y-gy); - if(d > radius) continue; - const b = getBlock(x,y); - if(!b || b.dead) continue; - if(BLOCKS[b.t].fluid) continue; - if(BLOCKS[b.t].unbreakable) continue; - if(b.t==='tnt') { activateTNT(b, 0.12); continue; } - removeBlock(x,y); - if(inv[b.t] !== undefined && Math.random()<0.20) inv[b.t]++; // немного дропа - } - } - rebuildHotbar(); - - // урон - const hurt = (e)=>{ - const dx = (e.x+e.w/2) - (gx*TILE+TILE/2); - const dy = (e.y+e.h/2) - (gy*TILE+TILE/2); - const dist = Math.hypot(dx,dy); - if(dist < dmgR){ - const dmg = (dmgR - dist) * 0.06 * power; - if(e === player) player.hp -= dmg; - else e.hp -= dmg; - e.vx += (dx/dist || 0) * 600; - e.vy -= 320; - } - }; - hurt(player); - mobs.forEach(hurt); - } - - // Взаимодействие мышь/тап - const mouse = { x:null, y:null }; - canvas.addEventListener('pointermove', (e)=>{ - const r = canvas.getBoundingClientRect(); - mouse.x = e.clientX - r.left; - mouse.y = e.clientY - r.top; - }); - - canvas.addEventListener('pointerdown', (e)=>{ - if(craftOpen) return; - if(player.hp<=0) return; - - const r = canvas.getBoundingClientRect(); - const sx = e.clientX - r.left; - const sy = e.clientY - r.top; - - const wx = sx + camX; - const wy = sy + camY; - - const gx = Math.floor(wx / TILE); - const gy = Math.floor(wy / TILE); - - // Пробуждение: клик по любой кровати когда спишь - const b = getBlock(gx,gy); - if(player.sleeping && b && b.t==='bed'){ - player.sleeping = false; - return; - } - - if(player.sleeping) return; // Нельзя взаимодействовать во время сна - - // клик по мобу (в режиме mine) - if(mode()==='mine'){ - for(let i=mobs.length-1;i>=0;i--){ - const m = mobs[i]; - if(wx>=m.x && wx<=m.x+m.w && wy>=m.y && wy<=m.y+m.h){ - m.hp -= 1; - m.vx += (m.x - player.x) * 2; - m.vy -= 200; - playSound('attack'); // Звук атаки игрока - if(m.hp<=0){ - // дроп еды - if(m.kind === 'chicken') playSound('hurt_chicken'); // Звук при убийстве курицы - inv.meat += (m.kind==='chicken' ? 1 : 2); - mobs.splice(i,1); - rebuildHotbar(); - } - return; - } - } - } - - // еда (предмет) - if(ITEMS[selected] && inv[selected]>0){ - const it = ITEMS[selected]; - if(player.hp < 100 || player.hunger < 100){ - playSound('eat1'); // Звук употребления еды - player.hunger = Math.min(100, player.hunger + it.food); - player.hp = Math.min(100, player.hp + 15); - inv[selected]--; - rebuildHotbar(); - } - return; - } - - // жарка на костре: выбран meat + клик по campfire - if(b && b.t==='campfire' && selected==='meat' && inv.meat>0){ - playSound('fire'); // Звук при жарке на костре - inv.meat--; inv.cooked++; - rebuildHotbar(); - return; - } - - // Сон на кровати: клик по bed - if(b && b.t==='bed' && isNight()){ - player.sleeping = true; - saveGame(); // Сохраняем при отходе ко сну - return; - } - - if(mode()==='mine'){ - if(!b) return; - if(BLOCKS[b.t].fluid || BLOCKS[b.t].decor) return; - - if(b.t==='tnt'){ activateTNT(b, 3.2); return; } // не взрывается сразу - - const removed = removeBlock(gx,gy); - if(removed){ - inv[removed.t] = (inv[removed.t]||0) + 1; - - // Отправляем изменение блока на сервер - sendBlockChange(gx, gy, removed.t, 'remove'); - - // Звуки при добыче блоков - if(removed.t === 'glass') playSound('glass1'); - else if(removed.t === 'sand') playSound('sand1'); - else if(removed.t === 'snow') playSound('snow1'); - else if(removed.t === 'stone' || removed.t.endsWith('_ore')) playSound('stone1'); - else if(removed.t === 'wood') playSound('wood1'); - else playSound('cloth1'); - - rebuildHotbar(); - } - return; - } - - if(mode()==='build'){ - if(inv[selected] <= 0) return; - if(!BLOCKS[selected]) return; - if(b) return; // занято - - // Проверяем, ставим ли лодку - if(selected === 'boat'){ - // Лодку можно ставить только на воду - const waterBelow = getBlock(gx, gy+1); - if(!waterBelow || waterBelow.t !== 'water'){ - return; - } - - // Создаём лодку - boat.x = gx * TILE; - boat.y = gy * TILE; - boat.vx = 0; - boat.vy = 0; - boat.active = true; - boat.inWater = true; - - // Сажаем игрока в лодку - player.inBoat = true; - player.x = boat.x; - player.y = boat.y; - player.vx = 0; - player.vy = 0; - - playSound('splash'); - inv[selected]--; - rebuildHotbar(); - return; - } - - // запрет ставить в игрока - const bx = gx*TILE, by = gy*TILE; - const overlap = !(bx >= player.x+player.w || bx+TILE <= player.x || by >= player.y+player.h || by+TILE <= player.y); - if(overlap) return; - - setBlock(gx,gy,selected, true); // true = блок установлен игроком - inv[selected]--; - - // Отправляем изменение блока на сервер - sendBlockChange(gx, gy, selected, 'set'); - - // Звук при строительстве - if(selected === 'stone' || selected === 'brick') playSound('stone_build'); - else if(selected === 'wood' || selected === 'planks') playSound('wood_build'); - else if(selected === 'glass') playSound('glass1'); - else if(selected === 'sand') playSound('sand1'); - else if(selected === 'snow') playSound('snow1'); - else if(selected === 'dirt' || selected === 'grass') playSound('cloth1'); - - rebuildHotbar(); - return; - } - }); - - // Генерация (по X, на всю глубину до bedrock) - const generated = new Set(); // gx already generated - function surfaceGyAt(gx){ - // базовая поверхность выше уровня воды с вариациями + "горы" - // Используем seed для детерминированной генерации - // Увеличили амплитуду и добавили больше частот для разнообразия - const n1 = Math.sin(gx*0.025 + worldSeed*0.001)*8; // крупные горы - const n2 = Math.sin(gx*0.012 + worldSeed*0.002)*12; // средние горы - const n3 = Math.sin(gx*0.006 + worldSeed*0.003)*6; // мелкие холмы - const n4 = Math.sin(gx*0.045 + worldSeed*0.004)*4; // детали - const n5 = Math.cos(gx*0.018 + worldSeed*0.005)*5; // дополнительные вариации - const h = Math.floor(SEA_GY - 8 + n1 + n2 + n3 + n4 + n5); // чем меньше gy - тем выше - return h; - } - - function genColumn(gx){ - if(generated.has(gx)) return; - generated.add(gx); - - const sgy = surfaceGyAt(gx); - - // вода (если поверхность ниже уровня моря => sgy > SEA_GY) - if(sgy > SEA_GY){ - for(let gy=SEA_GY; gy SEA_GY && gy === sgy+1 && seededRandom(gx, gy) < 0.25) t='clay'; - if(gy > sgy+6 && seededRandom(gx, gy) < 0.07) t='gravel'; - - // руды: чем глубже, тем интереснее - const depth = gy - sgy; - const r = seededRandom(gx, gy); - if(t==='stone'){ - if(r < 0.06) t='coal'; - else if(r < 0.10) t='copper_ore'; - else if(r < 0.13) t='iron_ore'; - else if(depth > 40 && r < 0.145) t='gold_ore'; - else if(depth > 70 && r < 0.152) t='diamond_ore'; - } - - setBlock(gx,gy,t); - } - - // Деревья и цветы (только на траве, и не в воде) - const top = getBlock(gx, sgy); - if(top && top.t==='grass'){ - if(seededRandom(gx, sgy-1) < 0.10){ - setBlock(gx, sgy-1,'flower'); - } - if(seededRandom(gx, sgy-2) < 0.12){ - // простое дерево - setBlock(gx, sgy-1, 'wood'); - setBlock(gx, sgy-2, 'wood'); - setBlock(gx, sgy-3, 'leaves'); - setBlock(gx-1, sgy-3,'leaves'); - setBlock(gx+1, sgy-3,'leaves'); - } - } - } - - // Перегенерация видимых чанков (используется при загрузке сохранения) - function regenerateVisibleChunks(){ - const gx0 = Math.floor(camX/TILE); - for(let gx=gx0-GEN_MARGIN_X; gx<=gx0+GEN_MARGIN_X; gx++){ - // Принудительно перегенерируем колонну - generated.delete(gx); - genColumn(gx); - } - } - - function ensureGenAroundCamera(){ - const gx0 = Math.floor(camX/TILE); - for(let gx=gx0-GEN_MARGIN_X; gx<=gx0+GEN_MARGIN_X; gx++){ - genColumn(gx); - } - } - - // Лут с дерева/листвы: дерево -> wood; листья -> leaves - // (уже в mine добавляется inv[type] автоматически) - - // Рисование костра: огонь поверх текстуры - function drawFire(wx,wy,now){ - const baseX = wx; - const baseY = wy; - const flick = 6 + (Math.sin(now/90)+1)*4; - ctx.fillStyle = 'rgba(255,140,0,0.85)'; - ctx.beginPath(); - ctx.moveTo(baseX+10, baseY+30); - ctx.lineTo(baseX+20, baseY+30-flick); - ctx.lineTo(baseX+30, baseY+30); - ctx.fill(); - - ctx.fillStyle = 'rgba(255,230,150,0.75)'; - ctx.beginPath(); - ctx.moveTo(baseX+14, baseY+30); - ctx.lineTo(baseX+20, baseY+30-(flick*0.7)); - ctx.lineTo(baseX+26, baseY+30); - ctx.fill(); - } - - // Моб AI - function mobAI(m, dt){ - updateWaterFlag(m); - - if(m.kind==='zombie'){ - // активность ночью - const night = isNight(); - if(!night){ m.hp=0; return; } - const dir = Math.sign((player.x) - m.x); - m.vx = dir * m.speed; - if(m.inWater && Math.random()<0.06) m.vy = -260; - // атака - if(Math.abs((m.x+ m.w/2) - (player.x+player.w/2)) < 28 && - Math.abs((m.y+ m.h/2) - (player.y+player.h/2)) < 40 && - player.invuln <= 0){ - player.hp -= 15; - player.invuln = 0.8; - player.vx += dir*420; - player.vy -= 260; - playSound('hit1'); // Звук при атаке зомби - } - } else if(m.kind==='creeper'){ - // активность ночью - const night = isNight(); - if(!night){ m.hp=0; return; } - const dir = Math.sign((player.x) - m.x); - const dist = Math.hypot((player.x+player.w/2) - (m.x+m.w/2), (player.y+player.h/2) - (m.y+m.h/2)); - - // Движение к игроку - m.vx = dir * m.speed; - if(m.inWater && Math.random()<0.06) m.vy = -260; - - // Взрыв если близко к игроку - if(dist < 60){ - m.fuse -= dt; - if(m.fuse <= 0){ - explodeAt(Math.floor((m.x+m.w/2)/TILE), Math.floor((m.y+m.h/2)/TILE)); - m.hp = 0; - } - } else { - // Поджигаем если очень близко - if(dist < 40){ - m.fuse = 0.5; // Быстрый взрыв - } - } - } else if(m.kind==='skeleton'){ - // активность ночью - const night = isNight(); - if(!night){ m.hp=0; return; } - const dir = Math.sign((player.x) - m.x); - const dist = Math.hypot((player.x+player.w/2) - (m.x+m.w/2), (player.y+player.h/2) - (m.y+m.h/2)); - - // Движение к игроку - m.vx = dir * m.speed; - if(m.inWater && Math.random()<0.06) m.vy = -260; - - // Стрельба стрелами с проверкой препятствий - m.shootCooldown -= dt; - if(dist < 200 && m.shootCooldown <= 0){ - m.shootCooldown = 1.5; - // Создаём стрелу (упрощённо - просто урон) - const dx = (player.x+player.w/2) - (m.x+m.w/2); - const dy = (player.y+player.h/2) - (m.y+m.h/2); - const angle = Math.atan2(dy, dx); - - // Проверяем препятствия (до 5 блоков) - let blocked = false; - const checkSteps = 5; - const stepSize = dist / checkSteps; - for(let i = 1; i <= checkSteps; i++){ - const checkX = m.x + m.w/2 + Math.cos(angle) * stepSize * i; - const checkY = m.y + m.h/2 + Math.sin(angle) * stepSize * i; - const checkGX = Math.floor(checkX / TILE); - const checkGY = Math.floor(checkY / TILE); - if(isSolid(checkGX, checkGY)){ - blocked = true; - break; - } - } - - // Урон игроку если попали и нет препятствий - if(!blocked && dist < 150 && player.invuln <= 0){ - player.hp -= 8; - player.invuln = 0.5; - player.vx += Math.cos(angle) * 300; - player.vy -= 200; - playSound('hit1'); - } - } - } else { - // животные - m.aiT -= dt; - if(m.aiT <= 0){ - m.aiT = 1.8 + Math.random()*2.5; - m.dir = Math.random()<0.5 ? -1 : 1; - if(Math.random()<0.25) m.dir = 0; - } - m.vx = m.dir * (m.kind==='chicken' ? 55 : 40); - if(m.inWater) m.vy = -120; - } - - // физика моба - const g = m.inWater ? GRAV_WATER : GRAV; - m.vy += g*dt; - - m.y += m.vy*dt; m.grounded=false; resolveY(m); - m.x += m.vx*dt; resolveX(m); - } - - function isNight(){ - // Автоматический цикл: ночь когда worldTime > 0.5 - return worldTime > 0.5; - } - - // Respawn - document.getElementById('respawnBtn').onclick = async () => { - playSound('click'); // Звук клика по кнопке - - console.log('=== RESPAWN CLICKED ==='); - console.log('isMultiplayer:', isMultiplayer); - console.log('otherPlayers.size:', otherPlayers.size); - console.log('player.hp before respawn:', player.hp); - - // В мультиплеере не загружаем сохранение, а возрождаемся в начальной точке - if (isMultiplayer && otherPlayers.size > 0) { - console.log('Мультиплеер режим - возрождение в начальной точке'); - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.vx = player.vy = 0; - player.invuln = 0; - player.x = spawnPoint.x; - player.y = spawnPoint.y; - player.fallStartY = player.y; - console.log('Возрождение в начальной точке, HP:', player.hp); - } else { - console.log('Одиночный режим - загружаем последнее сохранение'); - // Одиночный режим - загружаем последнее сохранение - const loadedSave = await loadGame(); - if(loadedSave){ - await applySave(loadedSave); - console.log('Загружено последнее сохранение после смерти, final HP:', player.hp); - } else { - // Если сохранения нет, возрождаемся в начальной точке - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.vx = player.vy = 0; - player.invuln = 0; - player.x = spawnPoint.x; - player.y = spawnPoint.y; - player.fallStartY = player.y; - console.log('Возрождение в начальной точке, HP:', player.hp); - } - } - - console.log('player.hp after respawn logic:', player.hp); - console.log('Hiding death screen...'); - deathEl.style.display='none'; - console.log('=== RESPAWN END ==='); - }; - - // Resize - function resize(){ - W = gameEl.clientWidth; - H = gameEl.clientHeight; - canvas.width = W*dpr; - canvas.height = H*dpr; - lightC.width = W*dpr; - lightC.height = H*dpr; - ctx.setTransform(dpr,0,0,dpr,0,0); - } - window.addEventListener('resize', resize); - - // init - resize(); - rebuildHotbar(); - - // Инициализируем и загружаем сохранение - initDB().then(async () => { - // Пытаемся загрузить сохранённую игру - const loadedSave = await loadGame(); - if(loadedSave){ - await applySave(loadedSave); - console.log('Загружено сохранение, HP:', player.hp); - - // Проверяем HP после загрузки - если <= 0, возрождаемся - if (player.hp <= 0) { - console.log('WARNING: HP <= 0 после загрузки, возрождаемся'); - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.x = spawnPoint.x; - player.y = spawnPoint.y; - player.vx = player.vy = 0; - player.invuln = 0; - player.fallStartY = player.y; - } - } else { - console.log('Сохранение не найдено, начинаем новую игру'); - - // Инициализируем игрока для новой игры - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.vx = player.vy = 0; - player.invuln = 0; - - // старт — на поверхности (ровно на 1 тайл выше поверхности) - const startGX = 6; - genColumn(startGX); - const surfaceY = surfaceGyAt(startGX); - player.y = (surfaceY - 1) * TILE; - player.x = startGX * TILE; - player.fallStartY = player.y; - - // Обновляем spawnPoint, чтобы возрождение было на поверхности - spawnPoint.x = player.x; - spawnPoint.y = player.y; - - console.log('Новая игра: startGX=', startGX, 'surfaceY=', surfaceY, 'player.y=', player.y, 'player.hp=', player.hp); - console.log('spawnPoint обновлён: x=', spawnPoint.x, 'y=', spawnPoint.y); - - // Генерируем карту вокруг стартовой позиции при инициализации - for(let gx = startGX - 50; gx <= startGX + 50; gx++){ - genColumn(gx); - } - } - - // Автосейв при скрытии страницы (защита от потери прогресса) - document.addEventListener('visibilitychange', () => { - if(document.hidden){ - saveGame(); - } - }); - - // Автосейв перед закрытием страницы (защита от потери прогресса) - window.addEventListener('beforeunload', () => { - saveGame(); - }); - }).catch(err => { - console.error('Ошибка инициализации:', err); - // При ошибке начинаем новую игру - const startGX = 6; - genColumn(startGX); - player.y = (surfaceGyAt(startGX)-1)*TILE; - player.fallStartY = player.y; - - for(let gx = startGX - 50; gx <= startGX + 50; gx++){ - genColumn(gx); - } - }); - - // main loop - let last = performance.now(); - let prevJump = false; - function loop(now){ - const dt = Math.min(0.05, (now-last)/1000); - last = now; - - const jumpPressed = inp.j && !prevJump; - prevJump = inp.j; - - // Ускорение времени во время сна - if(player.sleeping && isNight()){ - worldTime += dt * 8 / DAY_LEN; // В 8 раз быстрее - // Восстанавливаем здоровье во время сна - player.hp = Math.min(100, player.hp + dt * 20); - // Автоматическое пробуждение когда наступает день - if(!isNight()){ - player.sleeping = false; - } - } else { - worldTime += dt / DAY_LEN; - } - if(worldTime >= 1) worldTime -= 1; - - // камера следует за игроком по X/Y - camX = Math.floor((player.x + player.w/2) - W/2); - camY = Math.floor((player.y + player.h/2) - H/2); - - ensureGenAroundCamera(); - - // clouds parallax - for(const c of clouds){ - c.x -= c.s * dt; - if(c.x + c.w < camX - 400) c.x = camX + W + Math.random()*700; - } - - // player - updateWaterFlag(player); - - // кислород/утопление: тратим O2 только если голова под водой, иначе восстанавливаем [web:223] - if(player.headInWater){ - player.o2 = Math.max(0, player.o2 - 6*dt); // замедлил в 3.7 раза - if(player.o2 === 0){ - player.hp -= 4*dt; // уменьшил урон от утопления - } - } else { - player.o2 = Math.min(100, player.o2 + 10*dt); // замедлил восстановление в 4 раза - } - - // голод убывает, но HP не отнимает (как просили) - player.hunger = Math.max(0, player.hunger - dt*0.2); // замедлил в 4 раза - - // Игрок не может двигаться во время сна - if(player.sleeping){ - player.vx = 0; - player.vy = 0; - } else { - const dir = (inp.r?1:0) - (inp.l?1:0); - if(dir) player.vx = dir*MOVE; - else player.vx *= 0.82; - } - - // Звук шагов при движении по земле - if(player.grounded && !player.inWater && Math.abs(player.vx) > 50){ - const stepInterval = 0.35; // Интервал между шагами в секундах - if(now/1000 - player.lastStepTime > stepInterval){ - playSound('step'); - player.lastStepTime = now/1000; - } - } - - // прыжок/плавание (новая логика) - if(player.inBoat){ - // Игрок в лодке - лодка следует за игроком - const dir = (inp.r?1:0) - (inp.l?1:0); - if(dir) boat.vx = dir * MOVE; - else boat.vx *= 0.95; - - // Лодка плавает на воде - boat.vy = 0; - - // Игрок следует за лодкой (сидит внутри неё) - player.x = boat.x + 2; // Игрок по центру лодки - player.y = boat.y - 4; // Игрок выше лодки (сидит внутри) - player.vx = boat.vx; - player.vy = boat.vy; - player.grounded = true; - player.inWater = false; // Игрок не в воде когда в лодке - - // Прыжок из лодки (высадка) - if(jumpPressed){ - // Возвращаем лодку в инвентарь - inv.boat = (inv.boat || 0) + 1; - - player.inBoat = false; - boat.active = false; - player.y += TILE; // Прыгаем из лодки - player.vy = -JUMP * 0.5; - playSound('splash'); - } - - } else if(player.inWater){ - // сопротивление в воде - player.vx *= 0.90; - player.vy *= 0.92; - - // Если не нажимаем прыжок - тонем (гравитация в воде) - if(!jumpPressed && !inp.j){ - // Применяем гравитацию в воде - игрок тонет - player.vy += GRAV_WATER * dt; - } else { - // Если нажимаем прыжок - поднимаемся на поверхность - if(jumpPressed){ - player.vy = Math.min(player.vy, -520); // рывок вверх - } else if(inp.j){ - // если держим — мягкое всплытие - player.vy = Math.min(player.vy, -260); - } - } - - } else { - // обычный прыжок (только по нажатию) - if(jumpPressed && player.grounded && !player.sleeping){ - player.vy = -JUMP; - player.grounded = false; - player.fallStartY = player.y; - } - } - - // Гравитация применяется только вне воды и вне лодки - if(!player.inWater && !player.inBoat){ - player.vy += GRAV*dt; - } - - // Обновляем позицию лодки - if(boat.active){ - boat.x += boat.vx * dt; - boat.y += boat.vy * dt; - - // Лодка не выходит за пределы воды - const boatGX = Math.floor(boat.x / TILE); - const boatGY = Math.floor(boat.y / TILE); - const below = getBlock(boatGX, boatGY + 1); - - if(!below || below.t !== 'water'){ - // Если лодка вышла из воды - выкидываем игрока - inv.boat = (inv.boat || 0) + 1; - player.inBoat = false; - boat.active = false; - player.y += TILE; - player.vy = -200; - playSound('splash'); - } - } - - // Проверяем, не доплыл ли игрок из лодки - if(player.inBoat && !boat.active){ - inv.boat = (inv.boat || 0) + 1; - player.inBoat = false; - player.y += TILE; - player.vy = -200; - playSound('splash'); - } - - player.y += player.vy*dt; - resolveY(player); - player.x += player.vx*dt; resolveX(player); - - // Отправляем позицию на сервер (мультиплеер) - sendPlayerPosition(); - - // Обновляем физику воды - updateWaterPhysics(dt); - - player.invuln = Math.max(0, player.invuln - dt); - - // TNT tick - for(const key of Array.from(activeTNT)){ - const b = grid.get(key); - if(!b || b.dead){ activeTNT.delete(key); continue; } - b.fuse -= dt; - if(b.fuse <= 0){ - explodeAt(b.gx,b.gy); - } - } - - // mobs spawn (с обеих сторон камеры) - spawnT += dt; - if(spawnT > 1.8 && mobs.length < 30){ - spawnT = 0; - - // Выбираем сторону спавна (левая или правая) - const spawnLeft = Math.random() < 0.5; - const gx = spawnLeft - ? Math.floor((camX - 200)/TILE) - : Math.floor((camX + W + 200)/TILE); - - genColumn(gx); - const sgy = surfaceGyAt(gx); - const wx = gx*TILE + 4; - const wy = (sgy-2)*TILE; - - // не спавнить в воде - const top = getBlock(gx, sgy); - if(top && top.t==='water') { - // skip - } else { - if(isNight()){ - // Ночью спавним больше враждебных мобов - const rand = Math.random(); - if(rand < 0.35){ - mobs.push(new Zombie(wx, wy)); - } else if(rand < 0.55){ - mobs.push(new Creeper(wx, wy)); - } else { - mobs.push(new Skeleton(wx, wy)); - } - } else { - // Днём только животные - mobs.push(Math.random()<0.5 ? new Pig(wx, wy) : new Chicken(wx, wy)); - } - } - } - - // mobs update - for(let i=mobs.length-1;i>=0;i--){ - const m = mobs[i]; - mobAI(m, dt); - if(m.hp<=0) mobs.splice(i,1); - } - - // particles - for(let i=parts.length-1;i>=0;i--){ - const p = parts[i]; - p.t -= dt; - p.x += p.vx*dt; - p.y += p.vy*dt; - p.vy += GRAV*dt; - if(p.t <= 0) parts.splice(i,1); - } - - // death - if(player.hp <= 0){ - deathEl.style.display='flex'; - } else if(deathEl.style.display === 'flex') { - // Если HP > 0 но экран смерти всё ещё показан - скрываем его - deathEl.style.display='none'; - } - - // render - const night = isNight(); - - // sky - ctx.fillStyle = night ? '#070816' : '#87CEEB'; - ctx.fillRect(0,0,W,H); - - // clouds (parallax x/y) - ctx.save(); - ctx.translate(-camX*0.5, -camY*0.15); - ctx.fillStyle = 'rgba(255,255,255,0.65)'; - for(const c of clouds){ - ctx.fillRect(c.x, c.y, c.w, 26); - ctx.fillRect(c.x+20, c.y-10, c.w*0.6, 22); - } - ctx.restore(); - - // world - ctx.save(); - ctx.translate(-camX, -camY); - - const minGX = Math.floor(camX/TILE)-2; - const maxGX = Math.floor((camX+W)/TILE)+2; - const minGY = Math.floor(camY/TILE)-6; - const maxGY = Math.floor((camY+H)/TILE)+6; - - // draw blocks (по массиву, но фильтруем диапазоном) - for(const b of blocks){ - if(b.dead) continue; - if(b.gx < minGX || b.gx > maxGX || b.gy < minGY || b.gy > maxGY) continue; - - const def = BLOCKS[b.t]; - if(def.alpha){ - ctx.save(); - ctx.globalAlpha = def.alpha; - ctx.drawImage(tex[b.t], b.gx*TILE, b.gy*TILE, TILE, TILE); - ctx.restore(); - } else { - ctx.drawImage(tex[b.t], b.gx*TILE, b.gy*TILE, TILE, TILE); - } - - // TNT мигает, если активирован - if(b.t==='tnt' && b.active && Math.sin(now/60)>0){ - ctx.fillStyle='rgba(255,255,255,0.45)'; - ctx.fillRect(b.gx*TILE, b.gy*TILE, TILE, TILE); - } - - // огонь костра - if(b.t==='campfire'){ - drawFire(b.gx*TILE, b.gy*TILE, now); - } - } - - // mobs - for(const m of mobs){ - if(m.kind==='zombie'){ - ctx.fillStyle = '#2ecc71'; - ctx.fillRect(m.x, m.y, m.w, m.h); - ctx.fillStyle = '#c0392b'; - ctx.fillRect(m.x+6, m.y+12, 6,6); - ctx.fillRect(m.x+22, m.y+12, 6,6); - } else if(m.kind==='pig'){ - ctx.fillStyle = '#ffb6c1'; - ctx.fillRect(m.x, m.y, m.w, m.h); - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+22, m.y+5, 3,3); - ctx.fillStyle = '#ff69b4'; - ctx.fillRect(m.x+28, m.y+12, 6,6); - } else if(m.kind==='chicken'){ - // chicken - ctx.fillStyle = '#ecf0f1'; - ctx.fillRect(m.x, m.y, m.w, m.h); - ctx.fillStyle = '#f39c12'; - ctx.fillRect(m.x+18, m.y+10, 6,4); - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+8, m.y+6, 3,3); - } else if(m.kind==='creeper'){ - // creeper - ctx.fillStyle = '#4CAF50'; - ctx.fillRect(m.x, m.y, m.w, m.h); - // Глаза - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+8, m.y+8, 4,4); - ctx.fillRect(m.x+22, m.y+8, 4,4); - // Рот - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+12, m.y+20, 10,4); - // Ноги - ctx.fillStyle = '#4CAF50'; - ctx.fillRect(m.x+4, m.y+30, 6,20); - ctx.fillRect(m.x+24, m.y+30, 6,20); - } else if(m.kind==='skeleton'){ - // skeleton - детализированный - // Тело - ctx.fillStyle = '#ECEFF1'; - ctx.fillRect(m.x+10, m.y+20, 14, 12); - // Череп - ctx.fillRect(m.x+8, m.y+0, 18, 18); - // Глазницы - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+10, m.y+6, 4,4); - ctx.fillRect(m.x+20, m.y+6, 4,4); - // Нос - ctx.fillRect(m.x+15, m.y+12, 4,2); - // Руки - ctx.fillStyle = '#ECEFF1'; - ctx.fillRect(m.x+2, m.y+20, 6,14); - ctx.fillRect(m.x+26, m.y+20, 6,14); - // Ноги - ctx.fillRect(m.x+10, m.y+32, 6, 18); - ctx.fillRect(m.x+18, m.y+32, 6, 18); - } - } - - // boat (рисуем первой, чтобы игрок был внутри неё) - if(boat.active){ - ctx.drawImage(tex['boat'], boat.x - (TILE-boat.w)/2, boat.y - (TILE-boat.h)/2, TILE, TILE); - } - - // other players (multiplayer) - for(const [socketId, p] of otherPlayers){ - if(heroImg.complete){ - ctx.drawImage(heroImg, p.x - (TILE-player.w)/2, p.y - (TILE-player.h)/2, TILE, TILE); - } else { - ctx.fillStyle = p.color; - ctx.fillRect(p.x, p.y, 34, 34); - } - // Имя игрока (мелко над персонажем) - ctx.fillStyle = '#fff'; - ctx.font = '12px system-ui'; - ctx.textAlign = 'center'; - ctx.fillText(p.name, p.x + 17, p.y - 8); - } - - // player - if(heroImg.complete){ - ctx.drawImage(heroImg, player.x - (TILE-player.w)/2, player.y - (TILE-player.h)/2, TILE, TILE); - } else { - ctx.fillStyle='#fff'; - ctx.fillRect(player.x, player.y, player.w, player.h); - } - - // particles - for(const p of parts){ - ctx.fillStyle = p.c; - ctx.fillRect(p.x-2, p.y-2, 4, 4); - } - - // Стрелы скелета - for(const m of mobs){ - if(m.kind==='skeleton' && m.shootCooldown > 0.5){ - // Рисуем стрелу - const arrowX = m.x + m.w/2; - const arrowY = m.y + 15; - const targetX = player.x + player.w/2; - const targetY = player.y + player.h/2; - const angle = Math.atan2(targetY - arrowY, targetX - arrowX); - const speed = 400; - - // Проверяем, попала ли стрела - const dx = targetX - arrowX; - const dy = targetY - arrowY; - const dist = Math.hypot(dx, dy); - - // Рисуем стрелу - ctx.save(); - ctx.translate(arrowX, arrowY); - ctx.rotate(angle); - ctx.fillStyle = '#ECEFF1'; - ctx.fillRect(0, -1, 16, 2); - ctx.restore(); - - // Урон игроку если попали - if(dist < 150 && player.invuln <= 0){ - player.hp -= 8; - player.invuln = 0.5; - player.vx += Math.cos(angle) * 300; - player.vy -= 200; - playSound('hit1'); - } - } - } - - // build ghost - if(mode()==='build' && mouse.x!==null && !craftOpen && player.hp>0){ - const wx = mouse.x + camX; - const wy = mouse.y + camY; - const gx = Math.floor(wx/TILE); - const gy = Math.floor(wy/TILE); - ctx.strokeStyle = 'rgba(255,255,255,0.9)'; - ctx.lineWidth = 2; - ctx.strokeRect(gx*TILE, gy*TILE, TILE, TILE); - } - - ctx.restore(); - - // lighting overlay at night - if(night){ - // Рисуем полупрозрачный тёмный оверлей - ctx.save(); - ctx.fillStyle = 'rgba(0,0,0,0.50)'; - ctx.fillRect(0, 0, W, H); - ctx.restore(); - - // Освещение от факелов и костров - ctx.save(); - ctx.globalCompositeOperation = 'lighter'; - - // Функция для рисования света - function drawLight(x, y, radius, intensity) { - const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius); - gradient.addColorStop(0, `rgba(255, 255, 200, ${intensity})`); - gradient.addColorStop(1, `rgba(255, 255, 200, 0)`); - ctx.fillStyle = gradient; - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fill(); - } - - // Освещение от факелов (1/3 яркости) - for(const b of blocks){ - if(b.dead) continue; - if(b.gx < minGX || b.gx > maxGX || b.gy < minGY || b.gy > maxGY) continue; - const def = BLOCKS[b.t]; - if(def.lightRadius){ - const wx = b.gx*TILE + TILE/2 - camX; - const wy = b.gy*TILE + TILE/2 - camY; - drawLight(wx, wy, def.lightRadius, 0.33); - } - } - - ctx.restore(); - } - - // UI tick - if(Math.random()<0.25){ - hpEl.textContent = Math.max(0, Math.ceil(player.hp)); - foodEl.textContent = Math.ceil(player.hunger); - document.getElementById('o2').textContent = Math.ceil(player.o2); - sxEl.textContent = Math.floor(player.x/TILE); - syEl.textContent = Math.floor(player.y/TILE); - todEl.textContent = night ? 'Ночь' : 'День'; - worldIdEl.textContent = worldId; - if(isMultiplayer){ - document.getElementById('multiplayerStatus').style.display = 'flex'; - playerCountEl.textContent = otherPlayers.size + 1; // +1 = мы сами - } else { - document.getElementById('multiplayerStatus').style.display = 'none'; - } - } - - // Индикатор сна - if(player.sleeping){ - ctx.fillStyle = 'rgba(0,0,0,0.7)'; - ctx.fillRect(0, 0, W, H); - ctx.fillStyle = '#fff'; - ctx.font = 'bold 32px system-ui'; - ctx.textAlign = 'center'; - ctx.fillText('💤 Спим...', W/2, H/2); - ctx.font = '18px system-ui'; - ctx.fillText('Нажмите на кровать чтобы проснуться', W/2, H/2 + 40); - } - - requestAnimationFrame(loop); - } - - requestAnimationFrame(loop); -})(); diff --git a/backup/20260103_082922/backup/20260103_082922/index.html b/backup/20260103_082922/backup/20260103_082922/index.html deleted file mode 100644 index 3313464..0000000 --- a/backup/20260103_082922/backup/20260103_082922/index.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - -GrechkaCraft: Multiplayer - - - - - - -
- - -
-
-
❤️ 100   🍗 100
-
🫁 100
-
📍 X:0 Y:0
-
🕒 День
-
🌐 default
- -
- -
🏃
-
💾
-
🔨
-
🔄
- -
-
-
Крафт
- -
-
-
- -
-
-
Инвентарь
- -
-
-
- -
-
ВЫ ПОГИБЛИ
- -
- -
-
🎒
-
- - - -
💬
-
- -
-
←
- -
↑
-
- - - - diff --git a/backup/20260103_082922/docker-compose.yml b/backup/20260103_082922/docker-compose.yml deleted file mode 100644 index 3de5ddb..0000000 --- a/backup/20260103_082922/docker-compose.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: grechka-game -services: - grechka-game: - build: - context: . - dockerfile: Dockerfile - container_name: grechka-game - restart: unless-stopped - networks: - - shared_network - labels: - # Включаем Traefik для этого сервиса - - "traefik.enable=true" - - "traefik.docker.network=shared_network" - - # HTTPS роутер - - "traefik.http.routers.grechka-game.entrypoints=websecure" - - "traefik.http.routers.grechka-game.rule=Host(`grechka.mkn8n.ru`)" - - "traefik.http.routers.grechka-game.tls=true" - - "traefik.http.routers.grechka-game.tls.certresolver=mytlschallenge" - - # Сервис - - "traefik.http.services.grechka-game.loadbalancer.server.port=80" - -networks: - shared_network: - external: true diff --git a/backup/20260103_082922/game.js b/backup/20260103_082922/game.js deleted file mode 100644 index 49d8e40..0000000 --- a/backup/20260103_082922/game.js +++ /dev/null @@ -1,2614 +0,0 @@ -(() => { - // ==================== КОНФИГУРАЦИЯ СЕРВЕРА ==================== - // Возможность переопределить сервер через query string - const urlParams = new URLSearchParams(window.location.search); - const SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru'; - const TELEGRAM_BOT_USERNAME = 'Grechkacraft_bot'; // Имя бота для share-ссылки - const TELEGRAM_APP_SHORT_NAME = 'minegrechka'; // Короткое имя Mini App - - // Защита от mixed content - if (location.protocol === 'https:' && SERVER_URL.startsWith('http://')) { - console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP'); - alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.'); - } - - // ==================== WORLD ID И ИГРОКА ==================== - let worldId = null; - let playerName = localStorage.getItem('minegrechka_playerName') || null; - - // Запрашиваем имя игрока, если его нет - if (!playerName) { - playerName = prompt('Введите ваше имя для игры:') || 'Игрок'; - localStorage.setItem('minegrechka_playerName', playerName); - console.log('Player name set:', playerName); - } - - // Берём worldId из URL или генерируем новый - console.log('Current URL:', window.location.href); - const worldParam = urlParams.get('world'); - console.log('world param:', worldParam); - - // Проверяем на null, undefined или пустую строку - worldId = (worldParam && worldParam.trim() !== '') ? worldParam : null; - - console.log('worldId after params:', worldId, 'type:', typeof worldId); - - // Если worldId отсутствует - генерируем новый и записываем в URL - if (!worldId) { - worldId = Math.random().toString(36).substring(2, 10); - console.log('Generated worldId:', worldId); - - try { - const newUrl = new URL(window.location.href); - newUrl.searchParams.set('world', worldId); - const newUrlString = newUrl.toString(); - console.log('New URL to set:', newUrlString); - - // Проверяем, поддерживается ли history API - if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') { - window.history.replaceState(null, '', newUrlString); - console.log('URL after replaceState:', window.location.href); - console.log('URL after replaceState (direct check):', window.location.search); - } else { - console.error('History API not supported!'); - } - } catch (e) { - console.error('Error updating URL:', e); - } - - console.log('Generated new worldId for browser:', worldId); - } - - console.log('Final worldId:', worldId, 'Player name:', playerName); - - console.log(`Server URL: ${SERVER_URL}, World ID: ${worldId}`); - - // Обработчик клика на worldId для копирования ссылки - document.getElementById('worldId').onclick = () => { - const shareUrl = new URL(window.location.href); - shareUrl.searchParams.set('world', worldId); - const shareUrlString = shareUrl.toString(); - - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(shareUrlString).then(() => { - alert('Ссылка скопирована!'); - }).catch(() => { - alert('Ссылка на мир:\n' + shareUrlString); - }); - } else { - alert('Ссылка на мир:\n' + shareUrlString); - } - }; - - // ==================== SOCKET.IO КЛИЕНТ ==================== - let socket = null; - let isMultiplayer = false; // Флаг для мультиплеерного режима - const otherPlayers = new Map(); // socket_id -> {x, y, color} - let mySocketId = null; - - // Throttle для отправки позиции (10-20 раз в секунду) - let lastMoveSendTime = 0; - const MOVE_SEND_INTERVAL = 0.05; // 50ms = 20 раз в секунду - let lastSentX = 0, lastSentY = 0; - - function initSocket() { - try { - socket = io(SERVER_URL, { - path: '/socket.io/', - transports: ['websocket', 'polling'] - }); - - socket.on('connect', () => { - console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${worldId}`); - mySocketId = socket.id; - isMultiplayer = true; - - // Присоединяемся к миру - socket.emit('join_world', { world_id: worldId, player_name: playerName }); - - // Показываем в UI - worldIdEl.textContent = worldId; - multiplayerStatus.style.display = 'block'; - }); - - socket.on('connect_error', (error) => { - console.error('Socket connection error:', error); - isMultiplayer = false; - }); - - socket.on('disconnect', () => { - console.log('Disconnected from server'); - isMultiplayer = false; - otherPlayers.clear(); - multiplayerStatus.style.display = 'none'; - }); - - // Обработка world_state - socket.on('world_state', (data) => { - console.log('Received world_state:', data); - - // Устанавливаем seed и перегенерируем мир если он изменился - if (data.seed !== undefined && data.seed !== worldSeed) { - const oldSeed = worldSeed; - worldSeed = data.seed; - console.log('World seed changed from', oldSeed, 'to', worldSeed); - - // Очищаем и перегенерируем мир с новым seed - generated.clear(); - grid.clear(); - blocks.length = 0; - placedBlocks = []; - removedBlocks = []; - console.log('World regenerated with new seed:', worldSeed); - } - - // Применяем блоки - if (data.blocks && Array.isArray(data.blocks)) { - for (const block of data.blocks) { - if (block.op === 'set') { - setBlock(block.gx, block.gy, block.t, false); - } else if (block.op === 'remove') { - removeBlock(block.gx, block.gy); - } - } - } - - // Устанавливаем время - if (data.time !== undefined) { - worldTime = data.time; - isNightTime = worldTime > 0.5; - } - - // Если есть spawnPoint от сервера - используем его и генерируем эту позицию - if (data.spawnPoint) { - spawnPoint.x = data.spawnPoint.x; - spawnPoint.y = data.spawnPoint.y; - // Генерируем колонну в точке спавна - const spawnGX = Math.floor(spawnPoint.x / TILE); - genColumn(spawnGX); - console.log('Server spawnPoint received and column generated:', spawnPoint); - } else { - // Если spawnPoint не пришёл от сервера - генерируем безопасную позицию - const startGX = 6; - genColumn(startGX); - const surfaceY = surfaceGyAt(startGX); - spawnPoint.x = startGX * TILE; - spawnPoint.y = (surfaceY - 1) * TILE; - console.log('Generated safe spawn point:', spawnPoint, 'surfaceY:', surfaceY); - } - - // Устанавливаем игрока в точку спавна - player.x = spawnPoint.x; - player.y = spawnPoint.y; - player.vx = 0; - player.vy = 0; - player.fallStartY = player.y; - console.log('Player moved to spawn point:', player.x, player.y); - - // Обновляем список игроков - if (data.players && Array.isArray(data.players)) { - otherPlayers.clear(); - for (const p of data.players) { - if (p.socket_id !== mySocketId) { - otherPlayers.set(p.socket_id, { - x: p.x, - y: p.y, - color: getRandomPlayerColor(p.socket_id), - name: p.player_name || 'Игрок' - }); - } - } - // Обновляем счётчик игроков - playerCountEl.textContent = data.players.length; - } - }); - - // Игрок присоединился - socket.on('player_joined', (data) => { - console.log('Player joined:', data.socket_id); - if (data.socket_id !== mySocketId) { - // Генерируем безопасную позицию для нового игрока - const spawnGX = 6; - genColumn(spawnGX); - const surfaceY = surfaceGyAt(spawnGX); - const safeSpawnX = spawnGX * TILE; - const safeSpawnY = (surfaceY - 1) * TILE; - - otherPlayers.set(data.socket_id, { - x: safeSpawnX, - y: safeSpawnY, - color: getRandomPlayerColor(data.socket_id), - name: data.player_name || 'Игрок' - }); - addChatMessage('Система', `Игрок присоединился`); - // Обновляем видимость кнопки сохранения - updateSaveButtonVisibility(); - } - }); - - // Игрок переместился - socket.on('player_moved', (data) => { - if (data.socket_id !== mySocketId && otherPlayers.has(data.socket_id)) { - const p = otherPlayers.get(data.socket_id); - p.x = data.x; - p.y = data.y; - // Обновляем имя, если оно пришло - if (data.player_name) { - p.name = data.player_name; - } - } - }); - - // Игрок покинул - socket.on('player_left', (data) => { - console.log('Player left:', data.socket_id); - otherPlayers.delete(data.socket_id); - addChatMessage('Система', `Игрок покинул игру`); - // Обновляем видимость кнопки сохранения - updateSaveButtonVisibility(); - }); - - // Блок изменён - socket.on('block_changed', (data) => { - if (data.op === 'set') { - setBlock(data.gx, data.gy, data.t, false); - } else if (data.op === 'remove') { - removeBlock(data.gx, data.gy); - } - }); - - // Сообщение в чат - socket.on('chat_message', (data) => { - const senderName = data.socket_id === mySocketId ? 'Вы' : `Игрок ${data.socket_id.substring(0, 6)}`; - addChatMessage(senderName, data.message); - }); - - // Обновление времени - socket.on('time_update', (data) => { - if (data.time !== undefined) { - worldTime = data.time; - isNightTime = worldTime > 0.5; - } - }); - - } catch (e) { - console.error('Error initializing socket:', e); - isMultiplayer = false; - } - } - - // Генерация случайного цвета для игрока на основе socket_id - function getRandomPlayerColor(socketId) { - const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e91e63', '#00bcd4']; - let hash = 0; - for (let i = 0; i < socketId.length; i++) { - hash = ((hash << 5) - hash) + socketId.charCodeAt(i); - hash = hash & hash; - } - return colors[Math.abs(hash) % colors.length]; - } - - // Отправка позиции игрока (с throttle) - function sendPlayerPosition() { - if (!isMultiplayer || !socket || !socket.connected) return; - - const now = performance.now() / 1000; - if (now - lastMoveSendTime < MOVE_SEND_INTERVAL) return; - - // Отправляем только если позиция изменилась - const dx = Math.abs(player.x - lastSentX); - const dy = Math.abs(player.y - lastSentY); - if (dx < 1 && dy < 1) return; - - lastMoveSendTime = now; - lastSentX = player.x; - lastSentY = player.y; - - socket.emit('player_move', { x: player.x, y: player.y, player_name: playerName }); - } - - // Отправка изменения блока - function sendBlockChange(gx, gy, t, op) { - if (!isMultiplayer || !socket || !socket.connected) return; - - socket.emit('block_change', { gx, gy, t, op }); - } - - // ==================== ЧАТ ==================== - const chatMessages = []; - const MAX_CHAT_MESSAGES = 20; - - function addChatMessage(sender, message) { - const time = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); - chatMessages.push({ sender, message, time }); - if (chatMessages.length > MAX_CHAT_MESSAGES) { - chatMessages.shift(); - } - renderChatMessages(); - } - - function renderChatMessages() { - const chatMessagesEl = document.getElementById('chatMessages'); - if (!chatMessagesEl) return; - - chatMessagesEl.innerHTML = chatMessages.map(m => - `
${m.time} ${m.sender}: ${m.message}
` - ).join(''); - - // Прокручиваем вниз - chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight; - } - - function sendChatMessage(message) { - if (!message || message.trim() === '') return; - - if (isMultiplayer && socket && socket.connected) { - socket.emit('chat_message', { message: message.trim() }); - } else { - addChatMessage('Вы', message.trim()); - } - } - - // ==================== ПОДЕЛИТЬСЯ МИРОМ ==================== - function shareWorld() { - const shareUrl = new URL(window.location.href); - shareUrl.searchParams.set('world', worldId); - const shareUrlString = shareUrl.toString(); - - // Копируем в буфер обмена - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(shareUrlString).then(() => { - alert('Ссылка скопирована!'); - }).catch(() => { - alert('Ссылка на мир:\n' + shareUrlString); - }); - } else { - alert('Ссылка на мир:\n' + shareUrlString); - } - } - - // ==================== ИНИЦИАЛИЗАЦИЯ UI ==================== - let chatOpen = false; - - document.getElementById('chatToggle').onclick = () => { - playSound('click'); - chatOpen = !chatOpen; - document.getElementById('chatPanel').style.display = chatOpen ? 'block' : 'none'; - if (chatOpen) { - document.getElementById('chatInput').focus(); - } - }; - - document.getElementById('chatClose').onclick = () => { - playSound('click'); - chatOpen = false; - document.getElementById('chatPanel').style.display = 'none'; - }; - - document.getElementById('chatSend').onclick = () => { - const input = document.getElementById('chatInput'); - sendChatMessage(input.value); - input.value = ''; - }; - - document.getElementById('chatInput').addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - sendChatMessage(e.target.value); - e.target.value = ''; - } - }); - - // ==================== ИНИЦИАЛИЗАЦИЯ СОКЕТА ==================== - // Инициализируем socket - initSocket(); - - // ==================== ЗВУКОВОЙ ДВИЖОК ==================== - const sounds = {}; - function loadSound(id, src) { - const audio = new Audio(); - audio.src = src; - audio.volume = 0.3; - sounds[id] = audio; - } - - // Загрузка звуков - loadSound('splash', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3'); - loadSound('sand1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3'); - loadSound('snow1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3'); - loadSound('stone1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3'); - loadSound('wood1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3'); - loadSound('cloth1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3'); - loadSound('fire', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3'); - loadSound('hit1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3'); - loadSound('attack', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3'); - loadSound('hurt_chicken', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3'); - loadSound('stone_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3'); - loadSound('wood_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3'); - loadSound('click', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3'); - loadSound('explode1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3'); - loadSound('glass1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3'); - loadSound('eat1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3'); - loadSound('step', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3'); - - function playSound(id) { - if(sounds[id]) { - sounds[id].currentTime = 0; - sounds[id].play().catch(e => console.error('Sound error:', e)); - } - } - - // Играем звук при прыжке - const gameEl = document.getElementById('game'); - const canvas = document.getElementById('c'); - const ctx = canvas.getContext('2d'); - - // offscreen light map (не вставляем в DOM) - const lightC = document.createElement('canvas'); - const lightCtx = lightC.getContext('2d'); - - const dpr = Math.max(1, window.devicePixelRatio || 1); - let W=0, H=0; - - const TILE = 40; - - // Мир - const SEA_GY = 14; // уровень воды (gy) - уменьшил для меньших водоёмов - const BEDROCK_GY = 140; // глубина бедрока (gy), чем больше - тем глубже - const GEN_MARGIN_X = 26; // запас генерации по X (в тайлах) - - const heroImg = new Image(); - heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png'; - - // Состояние инвентаря - let showFullInventory = false; - let recentItems = []; // Последние 5 выбранных предметов - - const BLOCKS = { - air: { n:'Воздух', solid:false }, - grass: { n:'Трава', c:'#7cfc00', solid:true }, - dirt: { n:'Грязь', c:'#8b4513', solid:true }, - stone: { n:'Камень', c:'#7f8c8d', solid:true }, - sand: { n:'Песок', c:'#f4d06f', solid:true }, - gravel: { n:'Гравий', c:'#95a5a6', solid:true }, - clay: { n:'Глина', c:'#74b9ff', solid:true }, - wood: { n:'Дерево', c:'#d35400', solid:true }, - planks: { n:'Доски', c:'#e67e22', solid:true }, - ladder: { n:'Лестница',c:'#d35400', solid:false, climbable:true }, - leaves: { n:'Листва', c:'#2ecc71', solid:true }, - glass: { n:'Стекло', c:'rgba(200,240,255,0.25)', solid:true, alpha:0.55 }, - water: { n:'Вода', c:'rgba(52,152,219,0.55)', solid:false, fluid:true }, - coal: { n:'Уголь', c:'#2c3e50', solid:true }, - copper_ore:{ n:'Медь', c:'#e17055', solid:true }, - iron_ore: { n:'Железо', c:'#dcdde1', solid:true }, - gold_ore: { n:'Золото', c:'#f1c40f', solid:true }, - diamond_ore:{n:'Алмаз', c:'#00a8ff', solid:true }, - brick: { n:'Кирпич', c:'#c0392b', solid:true }, - tnt: { n:'TNT', c:'#e74c3c', solid:true, explosive:true }, - campfire: { n:'Костёр', c:'#e67e22', solid:true, lightRadius:190 }, - torch: { n:'Факел', c:'#f9ca24', solid:true, lightRadius:140 }, - bedrock: { n:'Бедрок', c:'#2d3436', solid:true, unbreakable:true }, - flower: { n:'Цветок', c:'#ff4757', solid:false, decor:true }, - bed: { n:'Кровать', c:'#e91e63', solid:true, bed:true }, - boat: { n:'Лодка', c:'#8B4513', solid:false } - }; - - const ITEMS = { - meat: { n:'Сырое мясо', icon:'🥩', food:15 }, - cooked: { n:'Жареное мясо', icon:'🍖', food:45 } - }; - - // Seed мира для детерминированной генерации - // Инициализируем случайным seed, но он будет перезаписан сервером в мультиплеере - let worldSeed = Math.floor(Math.random() * 1000000); - - // Отслеживание изменений мира (для оптимизированного сохранения) - let placedBlocks = []; // [{gx, gy, t}] - блоки, установленные игроком - let removedBlocks = []; // [{gx, gy}] - блоки, удалённые игроком - - // Инструменты - const TOOLS = { - wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', durability: 60, miningPower: 1, craft: { wood: 3, planks: 2 } }, - stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', durability: 130, miningPower: 2, craft: { wood: 1, stone: 3 } }, - iron_pickaxe: { n:'Железная кирка', icon:'⛏️', durability: 250, miningPower: 3, craft: { wood: 1, iron_ore: 2 } }, - wood_sword: { n:'Деревянный меч', icon:'⚔️', durability: 40, damage: 5, craft: { wood: 2, planks: 1 } }, - stone_sword: { n:'Каменный меч', icon:'⚔️', durability: 100, damage: 8, craft: { wood: 1, stone: 2 } }, - iron_sword: { n:'Железный меч', icon:'⚔️', durability: 200, damage: 12, craft: { wood: 1, iron_ore: 1 } } - }; - - // Текстуры блоков (простые) - const tex = {}; - function makeTex(type) { - const t = BLOCKS[type]; - const c = document.createElement('canvas'); - c.width = 32; c.height = 32; - const g = c.getContext('2d'); - - if (type === 'tnt') { - g.fillStyle='#c0392b'; g.fillRect(0,0,32,32); - g.fillStyle='#fff'; g.fillRect(0,12,32,8); - g.fillStyle='#000'; g.font='bold 10px sans-serif'; g.fillText('TNT',6,20); - return c; - } - if (type === 'campfire') { - g.fillStyle='#5d4037'; g.fillRect(4,26,24,6); - g.fillStyle='#3e2723'; g.fillRect(7,23,18,4); - return c; - } - if (type === 'torch') { - g.fillStyle='#6d4c41'; g.fillRect(14,10,4,18); - g.fillStyle='#f39c12'; g.fillRect(12,6,8,8); - return c; - } - if (type === 'glass') { - g.fillStyle='rgba(200,240,255,0.25)'; g.fillRect(0,0,32,32); - g.strokeStyle='rgba(255,255,255,0.65)'; g.strokeRect(2,2,28,28); - g.beginPath(); g.moveTo(5,27); g.lineTo(27,5); g.stroke(); - return c; - } - if (type === 'water') { - g.fillStyle = t.c; g.fillRect(0,0,32,32); - g.fillStyle = 'rgba(255,255,255,0.08)'; - g.fillRect(0,6,32,2); - return c; - } - if (type === 'bed') { - // Основание кровати - g.fillStyle = '#e91e63'; - g.fillRect(0, 0, 32, 32); - // Подушка - g.fillStyle = '#f8bbd0'; - g.fillRect(2, 2, 14, 14); - // Одеяло - g.fillStyle = '#c2185b'; - g.fillRect(16, 4, 14, 24); - // Детали одеяла - g.fillStyle = '#e91e63'; - g.fillRect(18, 6, 10, 20); - return c; - } - if (type === 'flower') { - g.fillStyle='#2ecc71'; g.fillRect(14,14,4,18); - g.fillStyle=t.c; g.beginPath(); g.arc(16,12,6,0,6.28); g.fill(); - return c; - } - if (type === 'boat') { - // Корпус лодки - g.fillStyle = '#8B4513'; - g.fillRect(2, 12, 28, 8); - // Борта - g.fillStyle = '#A0522D'; - g.fillRect(0, 10, 32, 12); - // Внутренность - g.fillStyle = '#DEB887'; - g.fillRect(4, 14, 24, 4); - // Дно - g.fillStyle = '#654321'; - g.fillRect(2, 20, 28, 4); - return c; - } - if (type === 'ladder') { - // Боковые стойки лестницы - g.fillStyle = '#8B4513'; - g.fillRect(4, 0, 4, 32); - g.fillRect(24, 0, 4, 32); - // Ступени - g.fillStyle = '#A0522D'; - g.fillRect(4, 4, 24, 3); - g.fillRect(4, 12, 24, 3); - g.fillRect(4, 20, 24, 3); - g.fillRect(4, 28, 24, 3); - return c; - } - - g.fillStyle = t.c || '#000'; - g.fillRect(0,0,32,32); - - g.fillStyle = 'rgba(0,0,0,0.10)'; - for (let i=0;i<6;i++) g.fillRect((Math.random()*28)|0, (Math.random()*28)|0, 4,4); - - if (type.endsWith('_ore') || type==='coal') { - g.fillStyle = 'rgba(0,0,0,0.35)'; - for (let i=0;i<4;i++) g.fillRect((Math.random()*24)|0, (Math.random()*24)|0, 6,6); - } - return c; - } - Object.keys(BLOCKS).forEach(k => tex[k] = makeTex(k)); - - // Мир-хранилище - const grid = new Map(); // key "gx,gy" => {gx,gy,t, ...} - const blocks = []; // для рендера/перебора видимых - function k(gx,gy){ return gx+','+gy; } - function getBlock(gx,gy){ return grid.get(k(gx,gy)); } - function hasBlock(gx,gy){ return grid.has(k(gx,gy)); } - function isSolid(gx,gy){ - const b = getBlock(gx,gy); - if(!b || b.dead) return false; - const def = BLOCKS[b.t]; - return !!def.solid && !def.fluid && !def.decor; - } - function setBlock(gx,gy,t, isPlayerPlaced = false){ - const key = k(gx,gy); - if(grid.has(key)) return false; - const b = { gx, gy, t, dead:false, active:false, fuse:0 }; - grid.set(key, b); - blocks.push(b); - - // Отслеживаем блоки, установленные игроком - if(isPlayerPlaced){ - placedBlocks.push({gx, gy, t}); - } - - return true; - } - function removeBlock(gx,gy){ - const key = k(gx,gy); - const b = grid.get(key); - if(!b) return null; - if(BLOCKS[b.t].unbreakable) return null; - grid.delete(key); - b.dead = true; - - // Отслеживаем удалённые блоки - const wasPlayerPlaced = placedBlocks.some(pb => pb.gx === gx && pb.gy === gy); - if(wasPlayerPlaced){ - // Удаляем из placedBlocks - placedBlocks = placedBlocks.filter(pb => !(pb.gx === gx && pb.gy === gy)); - } else { - // Это природный блок - добавляем в removedBlocks - removedBlocks.push({gx, gy}); - } - - return b; - } - - // Физика жидкости - const waterUpdateQueue = new Set(); - let waterUpdateTimer = 0; - const WATER_UPDATE_INTERVAL = 0.05; // Обновляем воду каждые 0.05 секунды - - function updateWaterPhysics(dt){ - waterUpdateTimer += dt; - if(waterUpdateTimer < WATER_UPDATE_INTERVAL) return; - waterUpdateTimer = 0; - - // Ограничиваем количество водных блоков для обработки (оптимизация) - const MAX_WATER_BLOCKS_PER_UPDATE = 50; - let processedCount = 0; - - // Собираем только видимые водные блоки в очередь (оптимизация) - waterUpdateQueue.clear(); - const minGX = Math.floor(camX/TILE) - 10; - const maxGX = Math.floor((camX+W)/TILE) + 10; - const minGY = Math.floor(camY/TILE) - 10; - const maxGY = Math.floor((camY+H)/TILE) + 10; - - for(const b of blocks){ - if(processedCount >= MAX_WATER_BLOCKS_PER_UPDATE) break; - if(!b.dead && b.t === 'water' && - b.gx >= minGX && b.gx <= maxGX && - b.gy >= minGY && b.gy <= maxGY){ - waterUpdateQueue.add(k(b.gx, b.gy)); - processedCount++; - } - } - - // Обновляем воду с ограничением глубины распространения - const processed = new Set(); - const toAdd = []; - const MAX_WATER_DEPTH = 20; // Максимальная глубина распространения воды - - for(const key of waterUpdateQueue){ - if(processed.has(key)) continue; - const b = grid.get(key); - if(!b || b.dead) continue; - processed.add(key); - - const gx = b.gx; - const gy = b.gy; - - // Проверяем глубину - не распространяем воду слишком глубоко - if(gy > SEA_GY + MAX_WATER_DEPTH) continue; - - // Проверяем, можно ли воде упасть вниз - const belowKey = k(gx, gy + 1); - const below = grid.get(belowKey); - - // Внизу пусто - вода создаёт новый блок внизу (но не удаляется сверху) - if(!below || below.dead){ - // Ограничиваем создание новых водных блоков - if(toAdd.length < 20){ // Максимум 20 новых блоков за обновление - toAdd.push({gx, gy: gy + 1, t: 'water'}); - processed.add(belowKey); - } - continue; - } - - // Если внизу не вода и не твёрдый блок - вода может течь вниз - if(!isSolid(gx, gy + 1) && below && below.t !== 'water'){ - if(toAdd.length < 20){ - toAdd.push({gx, gy: gy + 1, t: 'water'}); - processed.add(belowKey); - } - continue; - } - - // Если внизу твёрдый блок или вода - вода растекается горизонтально - // Проверяем левую сторону - const leftKey = k(gx - 1, gy); - const left = grid.get(leftKey); - if(!left || left.dead){ - if(toAdd.length < 20){ - toAdd.push({gx: gx - 1, gy, t: 'water'}); - processed.add(leftKey); - } - continue; - } - - // Проверяем правую сторону - const rightKey = k(gx + 1, gy); - const right = grid.get(rightKey); - if(!right || right.dead){ - if(toAdd.length < 20){ - toAdd.push({gx: gx + 1, gy, t: 'water'}); - processed.add(rightKey); - } - continue; - } - } - - // Применяем изменения (только добавляем новые блоки) - for(const newData of toAdd){ - const key = k(newData.gx, newData.gy); - if(!grid.has(key)){ - const b = { - gx: newData.gx, - gy: newData.gy, - t: newData.t, - dead: false, - active: false, - fuse: 0 - }; - grid.set(key, b); - blocks.push(b); - } - } - - // Очищаем мёртвые блоки из массива - for(let i = blocks.length - 1; i >= 0; i--){ - if(blocks[i].dead){ - blocks.splice(i, 1); - } - } - } - - // Инвентарь - const inv = { - dirt:6, stone:0, sand:0, gravel:0, clay:0, - wood:0, planks:0, ladder:0, leaves:0, coal:0, - copper_ore:0, iron_ore:0, gold_ore:0, diamond_ore:0, - brick:0, glass:0, - tnt:1, campfire:0, torch:0, - meat:0, cooked:0, - wood_pickaxe:0, stone_pickaxe:0, iron_pickaxe:0, - wood_sword:0, stone_sword:0, iron_sword:0, - bed:0, boat:0 - }; - let selected = 'dirt'; - - const RECIPES = [ - { out:'planks', qty:4, cost:{ wood:1 } }, - { out:'ladder', qty:3, cost:{ planks:7 } }, - { out:'torch', qty:2, cost:{ coal:1, planks:1 } }, - { out:'glass', qty:1, cost:{ sand:3 } }, - { out:'brick', qty:1, cost:{ stone:2, clay:1 } }, - { out:'campfire', qty:1, cost:{ wood:1, coal:1 } }, - { out:'tnt', qty:1, cost:{ sand:2, coal:1 } }, - { out:'bed', qty:1, cost:{ wood: 3, planks: 3 } }, - { out:'boat', qty:1, cost:{ wood: 5 } }, - { out:'wood_pickaxe', qty:1, cost:{ wood: 3, planks: 2 } }, - { out:'stone_pickaxe', qty:1, cost:{ wood: 1, stone: 3 } }, - { out:'iron_pickaxe', qty:1, cost:{ wood: 1, iron_ore: 2 } }, - { out:'wood_sword', qty:1, cost:{ wood: 2, planks: 1 } }, - { out:'stone_sword', qty:1, cost:{ wood: 1, stone: 2 } }, - { out:'iron_sword', qty:1, cost:{ wood: 1, iron_ore: 1 } } - ]; - - // UI - const hpEl = document.getElementById('hp'); - const foodEl = document.getElementById('food'); - const sxEl = document.getElementById('sx'); - const syEl = document.getElementById('sy'); - const todEl = document.getElementById('tod'); - const worldIdEl = document.getElementById('worldId'); - const playerCountEl = document.getElementById('playerCount'); - const hotbarEl = document.getElementById('hotbar'); - const craftPanel = document.getElementById('craftPanel'); - const recipesEl = document.getElementById('recipes'); - const deathEl = document.getElementById('death'); - const inventoryPanel = document.getElementById('inventoryPanel'); - const inventoryGrid = document.getElementById('inventoryGrid'); - - // Клик на часы для включения ночи - todEl.style.cursor = 'pointer'; - todEl.onclick = () => { - playSound('click'); - worldTime = 0.6; // Устанавливаем ночь - isNightTime = true; - }; - - function rebuildHotbar(){ - hotbarEl.innerHTML=''; - - // Показываем последние 5 выбранных предметов (если они есть в инвентаре) - const items = recentItems.filter(id => inv[id] > 0).slice(0, 5); - - for(const id of items){ - const s = document.createElement('div'); - s.className = 'slot'+(id===selected?' sel':''); - if(BLOCKS[id]) { - s.style.backgroundImage = `url(${tex[id].toDataURL()})`; - s.style.backgroundSize = 'cover'; - } else if(ITEMS[id]) { - s.textContent = ITEMS[id].icon; - } else if(TOOLS[id]) { - s.textContent = TOOLS[id].icon; - } - const c = document.createElement('div'); - c.className='count'; - c.textContent = inv[id]; - s.appendChild(c); - s.onclick = () => { - playSound('click'); // Звук клика по инвентарю - selected=id; - // Обновляем список последних предметов - recentItems = recentItems.filter(item => item !== id); // Удаляем если уже есть - recentItems.unshift(id); // Добавляем в начало - recentItems = recentItems.slice(0, 5); // Оставляем только 5 - rebuildHotbar(); - }; - hotbarEl.appendChild(s); - } - } - - function renderInventory() { - inventoryGrid.innerHTML = ''; - - // Создаём сетку инвентаря 7x3 - const items = Object.keys(inv).filter(id => inv[id] > 0); - - // Добавляем пустые слоты для полной сетки - for(let i = 0; i < 21; i++) { - const slot = document.createElement('div'); - slot.className = 'inv-slot' + (i < items.length && items[i] === selected ? ' sel' : ''); - - if(i < items.length) { - const id = items[i]; - if(BLOCKS[id]) { - slot.style.backgroundImage = `url(${tex[id].toDataURL()})`; - slot.style.backgroundSize = 'cover'; - } else if(ITEMS[id]) { - slot.textContent = ITEMS[id].icon; - } else if(TOOLS[id]) { - slot.textContent = TOOLS[id].icon; - } - - const count = document.createElement('div'); - count.className = 'inv-count'; - count.textContent = inv[id]; - slot.appendChild(count); - - slot.onclick = () => { - playSound('click'); // Звук клика по инвентарю - selected = id; - // Обновляем список последних предметов - recentItems = recentItems.filter(item => item !== id); // Удаляем если уже есть - recentItems.unshift(id); // Добавляем в начало - recentItems = recentItems.slice(0, 5); // Оставляем только 5 - rebuildHotbar(); - renderInventory(); - }; - } - - inventoryGrid.appendChild(slot); - } - } - - function canCraft(r){ - for(const res in r.cost){ - if((inv[res]||0) < r.cost[res]) return false; - } - return true; - } - function renderCraft(){ - recipesEl.innerHTML=''; - for(const r of RECIPES){ - const row = document.createElement('div'); - row.className='recipe'; - const icon = document.createElement('div'); - icon.className='ricon'; - icon.style.backgroundImage = `url(${tex[r.out].toDataURL()})`; - const info = document.createElement('div'); - info.className='rinfo'; - const nm = document.createElement('div'); - nm.className='rname'; - nm.textContent = `${BLOCKS[r.out].n} x${r.qty}`; - const cs = document.createElement('div'); - cs.className='rcost'; - cs.textContent = Object.keys(r.cost).map(x => `${BLOCKS[x].n}: ${(inv[x]||0)}/${r.cost[x]}`).join(' '); - info.appendChild(nm); info.appendChild(cs); - const btn = document.createElement('button'); - btn.className='rcraft'; - btn.textContent='Создать'; - btn.disabled = !canCraft(r); - btn.onclick = () => { - if(!canCraft(r)) return; - playSound('click'); // Звук клика по кнопке крафта - for(const res in r.cost) inv[res]-=r.cost[res]; - inv[r.out] = (inv[r.out]||0) + r.qty; - rebuildHotbar(); - renderCraft(); - }; - row.appendChild(icon); row.appendChild(info); row.appendChild(btn); - recipesEl.appendChild(row); - } - } - - let craftOpen=false; - let inventoryOpen = false; - - document.getElementById('craftBtn').onclick = () => { - playSound('click'); // Звук клика по кнопке - craftOpen = !craftOpen; - craftPanel.style.display = craftOpen ? 'block' : 'none'; - if(craftOpen) { - renderCraft(); - // Закрываем инвентарь если открыт крафт - inventoryOpen = false; - inventoryPanel.style.display = 'none'; - } - }; - document.getElementById('craftClose').onclick = () => { - playSound('click'); // Звук клика по кнопке - craftOpen = false; - craftPanel.style.display = 'none'; - }; - - // Кнопка открытия инвентаря - document.getElementById('invToggle').onclick = () => { - playSound('click'); // Звук клика по кнопке - inventoryOpen = true; - inventoryPanel.style.display = 'block'; - renderInventory(); - // Закрываем крафт если открыт инвентарь - craftOpen = false; - craftPanel.style.display = 'none'; - }; - - document.getElementById('inventoryClose').onclick = () => { - playSound('click'); // Звук клика по кнопке - inventoryOpen = false; - inventoryPanel.style.display = 'none'; - }; - - // Кнопка сохранения игры (только для одиночного режима) - const saveBtn = document.getElementById('saveBtn'); - saveBtn.onclick = () => { - playSound('click'); - saveGame(); - alert('Игра сохранена!'); - }; - - // Кнопка сброса игры (удаление сохранения и создание нового мира) - const resetBtn = document.getElementById('resetBtn'); - resetBtn.onclick = () => { - if (confirm('Вы уверены, что хотите удалить сохранение и начать новую игру?')) { - playSound('click'); - - // Удаляем сохранение из localStorage - try { - localStorage.removeItem(SAVE_KEY); - console.log('Сохранение удалено из localStorage'); - } catch (e) { - console.warn('Ошибка удаления сохранения:', e); - } - - // Сбрасываем in-memory сохранение - inMemorySave = null; - - // Генерируем новый worldId - worldId = Math.random().toString(36).substring(2, 10); - console.log('Новый worldId после сброса:', worldId); - - // Обновляем URL - try { - const newUrl = new URL(window.location.href); - newUrl.searchParams.set('world', worldId); - const newUrlString = newUrl.toString(); - - if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') { - window.history.replaceState(null, '', newUrlString); - console.log('URL обновлён:', newUrlString); - } - } catch (e) { - console.error('Ошибка обновления URL:', e); - } - - // Перезагружаем страницу - location.reload(); - } - }; - - // Показываем кнопку сохранения только если играем одни - function updateSaveButtonVisibility() { - if (isMultiplayer && otherPlayers.size > 0) { - saveBtn.style.display = 'none'; - } else { - saveBtn.style.display = 'flex'; - } - } - - // Режимы - const MODES = [{id:'move',icon:'🏃'},{id:'mine',icon:'⛏️'},{id:'build',icon:'🧱'}]; - let modeIdx=0; - const modeBtn = document.getElementById('modeBtn'); - function mode(){ return MODES[modeIdx].id; } - modeBtn.onclick = () => { - playSound('click'); // Звук клика по кнопке режима - modeIdx=(modeIdx+1)%MODES.length; modeBtn.textContent=MODES[modeIdx].icon; - }; - - // День/ночь (автоматический цикл) - let isNightTime = false; - - // Управление - const inp = { l:false, r:false, j:false }; - function bindHold(el, key){ - const down=(e)=>{ e.preventDefault(); inp[key]=true; }; - const up=(e)=>{ e.preventDefault(); inp[key]=false; }; - el.addEventListener('pointerdown', down); - el.addEventListener('pointerup', up); - el.addEventListener('pointerleave', up); - } - bindHold(document.getElementById('left'),'l'); - bindHold(document.getElementById('right'),'r'); - bindHold(document.getElementById('jump'),'j'); - - window.addEventListener('keydown', (e)=>{ - if(e.code==='KeyA'||e.code==='ArrowLeft') inp.l=true; - if(e.code==='KeyD'||e.code==='ArrowRight') inp.r=true; - if(e.code==='Space'||e.code==='KeyW'||e.code==='ArrowUp') inp.j=true; - }); - window.addEventListener('keyup', (e)=>{ - if(e.code==='KeyA'||e.code==='ArrowLeft') inp.l=false; - if(e.code==='KeyD'||e.code==='ArrowRight') inp.r=false; - if(e.code==='Space'||e.code==='KeyW'||e.code==='ArrowUp') inp.j=false; - }); - - // Лодка - const boat = { - x: 0, y: 0, - w: 34, h: 34, - vx: 0, vy: 0, - active: false, - inWater: false - }; - - // Игрок - const player = { - x: 6*TILE, y: 0*TILE, - w: 34, h: 34, - vx: 0, vy: 0, - grounded: false, - inWater: false, - headInWater: false, - hp: 100, - hunger: 100, - o2: 100, - invuln: 0, - fallStartY: 0, - lastStepTime: 0, - sleeping: false, - inBoat: false - }; - - // Сохраняем начальную позицию для возрождения - const spawnPoint = { x: 6*TILE, y: 0*TILE }; - - // Система сохранения игры (localStorage + in-memory fallback) - const SAVE_KEY = 'minegrechka_save'; - let db = null; // Оставляем для совместимости, но не используем - let inMemorySave = null; // Запасное сохранение в памяти - - // Инициализация (localStorage + in-memory fallback) - function initDB(){ - return new Promise((resolve) => { - console.log('Используем localStorage для сохранений (sandbox режим)'); - resolve(null); - }); - } - - // Детерминированный генератор псевдослучайных чисел на основе seed - function seededRandom(gx, gy){ - const n = Math.sin(gx * 12.9898 + gy * 78.233 + worldSeed * 0.1) * 43758.5453; - return n - Math.floor(n); - } - - function saveGame(){ - const saveData = { - version: 2, - worldSeed: worldSeed, - player: { - x: player.x, - y: player.y, - hp: player.hp, - hunger: player.hunger, - o2: player.o2 - }, - inventory: inv, - time: worldTime, - isNight: isNightTime, - // Сохраняем только изменения - placedBlocks: placedBlocks.slice(), - removedBlocks: removedBlocks.slice() - }; - - const saveSize = JSON.stringify(saveData).length; - console.log('Сохранение: player HP:', player.hp, 'hunger:', player.hunger, 'o2:', player.o2); - - // Пробуем сохранить в localStorage (основной метод для персистентности) - try { - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - console.log(`Игра сохранена в localStorage (размер: ${saveSize} байт)`); - } catch(e){ - console.warn('Ошибка сохранения в localStorage, используем только in-memory:', e); - - // Если localStorage недоступен, используем in-memory fallback - inMemorySave = saveData; - console.log(`Игра сохранена в памяти (размер: ${saveSize} байт)`); - } - } - - function loadGame(){ - return new Promise((resolve, reject) => { - // Пробуем localStorage - try { - const localSave = localStorage.getItem(SAVE_KEY); - if(localSave){ - const parsed = JSON.parse(localSave); - console.log('Загружено из localStorage, player HP:', parsed.player?.hp); - resolve(parsed); - return; - } - } catch(e){ - console.warn('Ошибка доступа к localStorage:', e); - } - - // Если localStorage недоступен, используем in-memory сохранение - if(inMemorySave){ - console.log('Загружено из in-memory сохранения, player HP:', inMemorySave.player?.hp); - resolve(inMemorySave); - return; - } - - console.log('Сохранение не найдено'); - resolve(null); - }); - } - - // Миграция с версии 1 на версию 2 - function migrateV1toV2(saveData){ - console.log('Миграция сохранения с версии 1 на версию 2...'); - - // Сохраняем seed из текущей игры (так как v1 его не хранил) - saveData.worldSeed = worldSeed; - - // Инициализируем массивы изменений - saveData.placedBlocks = []; - saveData.removedBlocks = []; - - // Для v1 нам нужно сравнить сгенерированные блоки с тем, что должно быть по seed - // Это сложно сделать без полной перегенерации, поэтому для v1 сохраняем только seed - // и при загрузке просто перегенерируем мир - - // Удаляем старые данные - delete saveData.generatedBlocks; - - saveData.version = 2; - console.log('Миграция завершена'); - } - - async function applySave(saveData){ - if(!saveData) return; - - console.log('=== applySave START ==='); - console.log('player HP before applySave:', player.hp); - console.log('saveData.player.hp:', saveData.player?.hp); - - // Миграция версий - if(saveData.version === 1){ - migrateV1toV2(saveData); - } - - // Восстанавливаем seed - if(saveData.worldSeed !== undefined){ - worldSeed = saveData.worldSeed; - } - - // Восстанавливаем игрока - if(saveData.player){ - player.x = saveData.player.x; - player.y = saveData.player.y; - player.hunger = saveData.player.hunger; - player.o2 = saveData.player.o2; - - // Обновляем spawnPoint на позицию из сохранения - spawnPoint.x = player.x; - spawnPoint.y = player.y; - - // Проверяем HP из сохранения - если <= 0, устанавливаем 100 - const savedHP = saveData.player.hp; - console.log('Saved HP from file:', savedHP); - if(savedHP <= 0){ - console.log('WARNING: Saved HP is <= 0, setting to 100!'); - player.hp = 100; - } else { - player.hp = savedHP; - } - console.log('player HP after restore:', player.hp); - console.log('spawnPoint обновлён из сохранения: x=', spawnPoint.x, 'y=', spawnPoint.y); - } else { - console.log('No player data in save, setting default HP: 100'); - player.hp = 100; - } - - console.log('=== applySave END ==='); - - // Восстанавливаем инвентарь - if(saveData.inventory){ - for(const key in saveData.inventory){ - inv[key] = saveData.inventory[key]; - } - } - - // Восстанавливаем время - if(saveData.time !== undefined){ - worldTime = saveData.time; - } - - // Восстанавливаем день/ночь - if(saveData.isNight !== undefined){ - isNightTime = saveData.isNight; - } - - // Перегенерируем мир по seed - regenerateVisibleChunks(); - - // Применяем изменения (только для v2) - if(saveData.version === 2){ - // Применяем блоки, установленные игроком - for(const block of saveData.placedBlocks){ - setBlock(block.gx, block.gy, block.t, true); - } - - // Применяем удалённые блоки - for(const block of saveData.removedBlocks){ - removeBlock(block.gx, block.gy); - } - - // Восстанавливаем массивы изменений - placedBlocks = saveData.placedBlocks || []; - removedBlocks = saveData.removedBlocks || []; - } - - rebuildHotbar(); - console.log('Игра загружена'); - } - - // Камера (двухосевая) - let camX=0, camY=0; - - // День/ночь - let worldTime=0; - const DAY_LEN=360; // замедлил смену дня/ночи в 3 раза - - // Облака - const clouds = Array.from({length:10}, ()=>({ - x: Math.random()*2000, - y: -200 - Math.random()*260, // выше экрана, потому что мир по Y теперь двигается - w: 80+Math.random()*120, - s: 12+Math.random()*20 - })); - - // Частицы (взрыв) - const parts = []; - function spawnExplosion(x,y, power){ - const n = Math.floor(16 + power*10); - for(let i=0;i 100){ - playSound('splash'); - } - } - - function resolveY(e){ - // Всегда пересчитываем grounded (не держим "липким") - e.grounded = false; - - const x1 = e.x + 2; - const x2 = e.x + e.w - 2; - - // Проверяем, находится ли игрок на лестнице - const cx = e.x + e.w/2; - const cy = e.y + e.h/2; - const gx = Math.floor(cx / TILE); - const gy = Math.floor(cy / TILE); - const b = getBlock(gx, gy); - const onLadder = b && BLOCKS[b.t] && BLOCKS[b.t].climbable; - - // Если на лестнице - можно двигаться вверх/вниз - if(onLadder && e === player){ - e.grounded = true; - e.vy *= 0.8; // замедляем падение на лестнице - - // Если нажимаем прыжок на лестнице - поднимаемся - if(inp.j && e.vy > -200){ - e.vy = -200; - } - // Если не нажимаем прыжок - медленно спускаемся - else if(!inp.j && e.vy > 0){ - e.vy = Math.min(e.vy, 100); - } - return; - } - - // 1) Если движемся вниз ИЛИ стоим (vy >= 0) — проверяем опору под ногами - // Берём точку на 1px ниже стопы, чтобы не зависеть от ровного попадания в границу тайла. - if(e.vy >= 0){ - const probeY = e.y + e.h + 1; - const gy = Math.floor(probeY / TILE); - const gxA = Math.floor(x1 / TILE); - const gxB = Math.floor(x2 / TILE); - - if(isSolid(gxA, gy) || isSolid(gxB, gy)){ - e.y = gy * TILE - e.h; // прижимаем к полу - e.vy = 0; - e.grounded = true; - - // урон от падения — только игроку и только не в воде - if(e === player && !player.inWater){ - const fallTiles = (e.y - e.fallStartY) / TILE; - if(fallTiles > 6) player.hp -= (fallTiles - 6) * 10; - } - if(e === player) e.fallStartY = e.y; - } - } - - // 2) Проверка на возможность запрыгнуть на блок, если игрок рядом с ним - if(e.vy < 0 && e === player){ - const gy = Math.floor(e.y / TILE); - const gxA = Math.floor(x1 / TILE); - const gxB = Math.floor(x2 / TILE); - - // Проверяем, есть ли блок рядом с игроком - if((isSolid(gxA, gy) || isSolid(gxB, gy)) && !isSolid(gxA, gy - 1) && !isSolid(gxB, gy - 1)){ - e.y = (gy + 1) * TILE; - e.vy = 0; - e.grounded = true; - if(e === player) e.fallStartY = e.y; - console.log("Jumped onto block!"); - } - } - - // 2) Если движемся вверх — проверяем потолок - if(e.vy < 0){ - const gy = Math.floor(e.y / TILE); - const gxA = Math.floor(x1 / TILE); - const gxB = Math.floor(x2 / TILE); - if(isSolid(gxA, gy) || isSolid(gxB, gy)){ - e.y = (gy + 1) * TILE; - e.vy = 0; - } - } - } - - function resolveX(e){ - const y1 = e.y + 2; - const y2 = e.y + e.h - 2; - - if(e.vx > 0){ - const gx = Math.floor((e.x + e.w)/TILE); - const gyA = Math.floor(y1/TILE); - const gyB = Math.floor(y2/TILE); - if(isSolid(gx, gyA) || isSolid(gx, gyB)){ - e.x = gx*TILE - e.w; - e.vx = 0; - } - } else if(e.vx < 0){ - const gx = Math.floor(e.x/TILE); - const gyA = Math.floor(y1/TILE); - const gyB = Math.floor(y2/TILE); - if(isSolid(gx, gyA) || isSolid(gx, gyB)){ - e.x = (gx+1)*TILE; - e.vx = 0; - } - } - } - - // TNT логика: цепь + усиление - const activeTNT = new Set(); // хранит key - function activateTNT(b, fuse=3.2){ - if(b.dead) return; - if(b.active) return; - b.active=true; - b.fuse=fuse; - activeTNT.add(k(b.gx,b.gy)); - } - - function explodeAt(gx,gy){ - const center = getBlock(gx,gy); - if(!center) return; - - // усиление: считаем сколько TNT рядом (в радиусе 2) и активируем их «почти сразу» - let bonus = 0; - for(let x=gx-2; x<=gx+2; x++){ - for(let y=gy-2; y<=gy+2; y++){ - const b = getBlock(x,y); - if(b && !b.dead && b.t==='tnt' && !(x===gx && y===gy)){ - bonus += 0.8; - activateTNT(b, 0.12); // цепь - } - } - } - - const power = 1 + bonus; // условная мощность - const radius = 3.2 + bonus*0.7; // радиус разрушения в тайлах - const dmgR = 150 + bonus*60; // радиус урона в пикселях - - removeBlock(gx,gy); - activeTNT.delete(k(gx,gy)); - playSound('explode1'); // Звук взрыва - spawnExplosion(gx*TILE + TILE/2, gy*TILE + TILE/2, power); - - for(let x=Math.floor(gx-radius); x<=Math.ceil(gx+radius); x++){ - for(let y=Math.floor(gy-radius); y<=Math.ceil(gy+radius); y++){ - const d = Math.hypot(x-gx, y-gy); - if(d > radius) continue; - const b = getBlock(x,y); - if(!b || b.dead) continue; - if(BLOCKS[b.t].fluid) continue; - if(BLOCKS[b.t].unbreakable) continue; - if(b.t==='tnt') { activateTNT(b, 0.12); continue; } - removeBlock(x,y); - if(inv[b.t] !== undefined && Math.random()<0.20) inv[b.t]++; // немного дропа - } - } - rebuildHotbar(); - - // урон - const hurt = (e)=>{ - const dx = (e.x+e.w/2) - (gx*TILE+TILE/2); - const dy = (e.y+e.h/2) - (gy*TILE+TILE/2); - const dist = Math.hypot(dx,dy); - if(dist < dmgR){ - const dmg = (dmgR - dist) * 0.06 * power; - if(e === player) player.hp -= dmg; - else e.hp -= dmg; - e.vx += (dx/dist || 0) * 600; - e.vy -= 320; - } - }; - hurt(player); - mobs.forEach(hurt); - } - - // Взаимодействие мышь/тап - const mouse = { x:null, y:null }; - canvas.addEventListener('pointermove', (e)=>{ - const r = canvas.getBoundingClientRect(); - mouse.x = e.clientX - r.left; - mouse.y = e.clientY - r.top; - }); - - canvas.addEventListener('pointerdown', (e)=>{ - if(craftOpen) return; - if(player.hp<=0) return; - - const r = canvas.getBoundingClientRect(); - const sx = e.clientX - r.left; - const sy = e.clientY - r.top; - - const wx = sx + camX; - const wy = sy + camY; - - const gx = Math.floor(wx / TILE); - const gy = Math.floor(wy / TILE); - - // Пробуждение: клик по любой кровати когда спишь - const b = getBlock(gx,gy); - if(player.sleeping && b && b.t==='bed'){ - player.sleeping = false; - return; - } - - if(player.sleeping) return; // Нельзя взаимодействовать во время сна - - // клик по мобу (в режиме mine) - if(mode()==='mine'){ - for(let i=mobs.length-1;i>=0;i--){ - const m = mobs[i]; - if(wx>=m.x && wx<=m.x+m.w && wy>=m.y && wy<=m.y+m.h){ - m.hp -= 1; - m.vx += (m.x - player.x) * 2; - m.vy -= 200; - playSound('attack'); // Звук атаки игрока - if(m.hp<=0){ - // дроп еды - if(m.kind === 'chicken') playSound('hurt_chicken'); // Звук при убийстве курицы - inv.meat += (m.kind==='chicken' ? 1 : 2); - mobs.splice(i,1); - rebuildHotbar(); - } - return; - } - } - } - - // еда (предмет) - if(ITEMS[selected] && inv[selected]>0){ - const it = ITEMS[selected]; - if(player.hp < 100 || player.hunger < 100){ - playSound('eat1'); // Звук употребления еды - player.hunger = Math.min(100, player.hunger + it.food); - player.hp = Math.min(100, player.hp + 15); - inv[selected]--; - rebuildHotbar(); - } - return; - } - - // жарка на костре: выбран meat + клик по campfire - if(b && b.t==='campfire' && selected==='meat' && inv.meat>0){ - playSound('fire'); // Звук при жарке на костре - inv.meat--; inv.cooked++; - rebuildHotbar(); - return; - } - - // Сон на кровати: клик по bed - if(b && b.t==='bed' && isNight()){ - player.sleeping = true; - saveGame(); // Сохраняем при отходе ко сну - return; - } - - if(mode()==='mine'){ - if(!b) return; - if(BLOCKS[b.t].fluid || BLOCKS[b.t].decor) return; - - if(b.t==='tnt'){ activateTNT(b, 3.2); return; } // не взрывается сразу - - const removed = removeBlock(gx,gy); - if(removed){ - inv[removed.t] = (inv[removed.t]||0) + 1; - - // Отправляем изменение блока на сервер - sendBlockChange(gx, gy, removed.t, 'remove'); - - // Звуки при добыче блоков - if(removed.t === 'glass') playSound('glass1'); - else if(removed.t === 'sand') playSound('sand1'); - else if(removed.t === 'snow') playSound('snow1'); - else if(removed.t === 'stone' || removed.t.endsWith('_ore')) playSound('stone1'); - else if(removed.t === 'wood') playSound('wood1'); - else playSound('cloth1'); - - rebuildHotbar(); - } - return; - } - - if(mode()==='build'){ - if(inv[selected] <= 0) return; - if(!BLOCKS[selected]) return; - if(b) return; // занято - - // Проверяем, ставим ли лодку - if(selected === 'boat'){ - // Лодку можно ставить только на воду - const waterBelow = getBlock(gx, gy+1); - if(!waterBelow || waterBelow.t !== 'water'){ - return; - } - - // Создаём лодку - boat.x = gx * TILE; - boat.y = gy * TILE; - boat.vx = 0; - boat.vy = 0; - boat.active = true; - boat.inWater = true; - - // Сажаем игрока в лодку - player.inBoat = true; - player.x = boat.x; - player.y = boat.y; - player.vx = 0; - player.vy = 0; - - playSound('splash'); - inv[selected]--; - rebuildHotbar(); - return; - } - - // запрет ставить в игрока - const bx = gx*TILE, by = gy*TILE; - const overlap = !(bx >= player.x+player.w || bx+TILE <= player.x || by >= player.y+player.h || by+TILE <= player.y); - if(overlap) return; - - setBlock(gx,gy,selected, true); // true = блок установлен игроком - inv[selected]--; - - // Отправляем изменение блока на сервер - sendBlockChange(gx, gy, selected, 'set'); - - // Звук при строительстве - if(selected === 'stone' || selected === 'brick') playSound('stone_build'); - else if(selected === 'wood' || selected === 'planks') playSound('wood_build'); - else if(selected === 'glass') playSound('glass1'); - else if(selected === 'sand') playSound('sand1'); - else if(selected === 'snow') playSound('snow1'); - else if(selected === 'dirt' || selected === 'grass') playSound('cloth1'); - - rebuildHotbar(); - return; - } - }); - - // Генерация (по X, на всю глубину до bedrock) - const generated = new Set(); // gx already generated - function surfaceGyAt(gx){ - // базовая поверхность выше уровня воды с вариациями + "горы" - // Используем seed для детерминированной генерации - // Увеличили амплитуду и добавили больше частот для разнообразия - const n1 = Math.sin(gx*0.025 + worldSeed*0.001)*8; // крупные горы - const n2 = Math.sin(gx*0.012 + worldSeed*0.002)*12; // средние горы - const n3 = Math.sin(gx*0.006 + worldSeed*0.003)*6; // мелкие холмы - const n4 = Math.sin(gx*0.045 + worldSeed*0.004)*4; // детали - const n5 = Math.cos(gx*0.018 + worldSeed*0.005)*5; // дополнительные вариации - const h = Math.floor(SEA_GY - 8 + n1 + n2 + n3 + n4 + n5); // чем меньше gy - тем выше - return h; - } - - function genColumn(gx){ - if(generated.has(gx)) return; - generated.add(gx); - - const sgy = surfaceGyAt(gx); - - // вода (если поверхность ниже уровня моря => sgy > SEA_GY) - if(sgy > SEA_GY){ - for(let gy=SEA_GY; gy SEA_GY && gy === sgy+1 && seededRandom(gx, gy) < 0.25) t='clay'; - if(gy > sgy+6 && seededRandom(gx, gy) < 0.07) t='gravel'; - - // руды: чем глубже, тем интереснее - const depth = gy - sgy; - const r = seededRandom(gx, gy); - if(t==='stone'){ - if(r < 0.06) t='coal'; - else if(r < 0.10) t='copper_ore'; - else if(r < 0.13) t='iron_ore'; - else if(depth > 40 && r < 0.145) t='gold_ore'; - else if(depth > 70 && r < 0.152) t='diamond_ore'; - } - - setBlock(gx,gy,t); - } - - // Деревья и цветы (только на траве, и не в воде) - const top = getBlock(gx, sgy); - if(top && top.t==='grass'){ - if(seededRandom(gx, sgy-1) < 0.10){ - setBlock(gx, sgy-1,'flower'); - } - if(seededRandom(gx, sgy-2) < 0.12){ - // простое дерево - setBlock(gx, sgy-1, 'wood'); - setBlock(gx, sgy-2, 'wood'); - setBlock(gx, sgy-3, 'leaves'); - setBlock(gx-1, sgy-3,'leaves'); - setBlock(gx+1, sgy-3,'leaves'); - } - } - } - - // Перегенерация видимых чанков (используется при загрузке сохранения) - function regenerateVisibleChunks(){ - const gx0 = Math.floor(camX/TILE); - for(let gx=gx0-GEN_MARGIN_X; gx<=gx0+GEN_MARGIN_X; gx++){ - // Принудительно перегенерируем колонну - generated.delete(gx); - genColumn(gx); - } - } - - function ensureGenAroundCamera(){ - const gx0 = Math.floor(camX/TILE); - for(let gx=gx0-GEN_MARGIN_X; gx<=gx0+GEN_MARGIN_X; gx++){ - genColumn(gx); - } - } - - // Лут с дерева/листвы: дерево -> wood; листья -> leaves - // (уже в mine добавляется inv[type] автоматически) - - // Рисование костра: огонь поверх текстуры - function drawFire(wx,wy,now){ - const baseX = wx; - const baseY = wy; - const flick = 6 + (Math.sin(now/90)+1)*4; - ctx.fillStyle = 'rgba(255,140,0,0.85)'; - ctx.beginPath(); - ctx.moveTo(baseX+10, baseY+30); - ctx.lineTo(baseX+20, baseY+30-flick); - ctx.lineTo(baseX+30, baseY+30); - ctx.fill(); - - ctx.fillStyle = 'rgba(255,230,150,0.75)'; - ctx.beginPath(); - ctx.moveTo(baseX+14, baseY+30); - ctx.lineTo(baseX+20, baseY+30-(flick*0.7)); - ctx.lineTo(baseX+26, baseY+30); - ctx.fill(); - } - - // Моб AI - function mobAI(m, dt){ - updateWaterFlag(m); - - if(m.kind==='zombie'){ - // активность ночью - const night = isNight(); - if(!night){ m.hp=0; return; } - const dir = Math.sign((player.x) - m.x); - m.vx = dir * m.speed; - if(m.inWater && Math.random()<0.06) m.vy = -260; - // атака - if(Math.abs((m.x+ m.w/2) - (player.x+player.w/2)) < 28 && - Math.abs((m.y+ m.h/2) - (player.y+player.h/2)) < 40 && - player.invuln <= 0){ - player.hp -= 15; - player.invuln = 0.8; - player.vx += dir*420; - player.vy -= 260; - playSound('hit1'); // Звук при атаке зомби - } - } else if(m.kind==='creeper'){ - // активность ночью - const night = isNight(); - if(!night){ m.hp=0; return; } - const dir = Math.sign((player.x) - m.x); - const dist = Math.hypot((player.x+player.w/2) - (m.x+m.w/2), (player.y+player.h/2) - (m.y+m.h/2)); - - // Движение к игроку - m.vx = dir * m.speed; - if(m.inWater && Math.random()<0.06) m.vy = -260; - - // Взрыв если близко к игроку - if(dist < 60){ - m.fuse -= dt; - if(m.fuse <= 0){ - explodeAt(Math.floor((m.x+m.w/2)/TILE), Math.floor((m.y+m.h/2)/TILE)); - m.hp = 0; - } - } else { - // Поджигаем если очень близко - if(dist < 40){ - m.fuse = 0.5; // Быстрый взрыв - } - } - } else if(m.kind==='skeleton'){ - // активность ночью - const night = isNight(); - if(!night){ m.hp=0; return; } - const dir = Math.sign((player.x) - m.x); - const dist = Math.hypot((player.x+player.w/2) - (m.x+m.w/2), (player.y+player.h/2) - (m.y+m.h/2)); - - // Движение к игроку - m.vx = dir * m.speed; - if(m.inWater && Math.random()<0.06) m.vy = -260; - - // Стрельба стрелами с проверкой препятствий - m.shootCooldown -= dt; - if(dist < 200 && m.shootCooldown <= 0){ - m.shootCooldown = 1.5; - // Создаём стрелу (упрощённо - просто урон) - const dx = (player.x+player.w/2) - (m.x+m.w/2); - const dy = (player.y+player.h/2) - (m.y+m.h/2); - const angle = Math.atan2(dy, dx); - - // Проверяем препятствия (до 5 блоков) - let blocked = false; - const checkSteps = 5; - const stepSize = dist / checkSteps; - for(let i = 1; i <= checkSteps; i++){ - const checkX = m.x + m.w/2 + Math.cos(angle) * stepSize * i; - const checkY = m.y + m.h/2 + Math.sin(angle) * stepSize * i; - const checkGX = Math.floor(checkX / TILE); - const checkGY = Math.floor(checkY / TILE); - if(isSolid(checkGX, checkGY)){ - blocked = true; - break; - } - } - - // Урон игроку если попали и нет препятствий - if(!blocked && dist < 150 && player.invuln <= 0){ - player.hp -= 8; - player.invuln = 0.5; - player.vx += Math.cos(angle) * 300; - player.vy -= 200; - playSound('hit1'); - } - } - } else { - // животные - m.aiT -= dt; - if(m.aiT <= 0){ - m.aiT = 1.8 + Math.random()*2.5; - m.dir = Math.random()<0.5 ? -1 : 1; - if(Math.random()<0.25) m.dir = 0; - } - m.vx = m.dir * (m.kind==='chicken' ? 55 : 40); - if(m.inWater) m.vy = -120; - } - - // физика моба - const g = m.inWater ? GRAV_WATER : GRAV; - m.vy += g*dt; - - m.y += m.vy*dt; m.grounded=false; resolveY(m); - m.x += m.vx*dt; resolveX(m); - } - - function isNight(){ - // Автоматический цикл: ночь когда worldTime > 0.5 - return worldTime > 0.5; - } - - // Respawn - document.getElementById('respawnBtn').onclick = async () => { - playSound('click'); // Звук клика по кнопке - - console.log('=== RESPAWN CLICKED ==='); - console.log('isMultiplayer:', isMultiplayer); - console.log('otherPlayers.size:', otherPlayers.size); - console.log('player.hp before respawn:', player.hp); - - // В мультиплеере не загружаем сохранение, а возрождаемся в начальной точке - if (isMultiplayer && otherPlayers.size > 0) { - console.log('Мультиплеер режим - возрождение в начальной точке'); - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.vx = player.vy = 0; - player.invuln = 0; - player.x = spawnPoint.x; - player.y = spawnPoint.y; - player.fallStartY = player.y; - console.log('Возрождение в начальной точке, HP:', player.hp); - } else { - console.log('Одиночный режим - загружаем последнее сохранение'); - // Одиночный режим - загружаем последнее сохранение - const loadedSave = await loadGame(); - if(loadedSave){ - await applySave(loadedSave); - console.log('Загружено последнее сохранение после смерти, final HP:', player.hp); - } else { - // Если сохранения нет, возрождаемся в начальной точке - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.vx = player.vy = 0; - player.invuln = 0; - player.x = spawnPoint.x; - player.y = spawnPoint.y; - player.fallStartY = player.y; - console.log('Возрождение в начальной точке, HP:', player.hp); - } - } - - console.log('player.hp after respawn logic:', player.hp); - console.log('Hiding death screen...'); - deathEl.style.display='none'; - console.log('=== RESPAWN END ==='); - }; - - // Resize - function resize(){ - W = gameEl.clientWidth; - H = gameEl.clientHeight; - canvas.width = W*dpr; - canvas.height = H*dpr; - lightC.width = W*dpr; - lightC.height = H*dpr; - ctx.setTransform(dpr,0,0,dpr,0,0); - } - window.addEventListener('resize', resize); - - // init - resize(); - rebuildHotbar(); - - // Инициализируем и загружаем сохранение - initDB().then(async () => { - // Пытаемся загрузить сохранённую игру - const loadedSave = await loadGame(); - if(loadedSave){ - await applySave(loadedSave); - console.log('Загружено сохранение, HP:', player.hp); - - // Проверяем HP после загрузки - если <= 0, возрождаемся - if (player.hp <= 0) { - console.log('WARNING: HP <= 0 после загрузки, возрождаемся'); - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.x = spawnPoint.x; - player.y = spawnPoint.y; - player.vx = player.vy = 0; - player.invuln = 0; - player.fallStartY = player.y; - } - } else { - console.log('Сохранение не найдено, начинаем новую игру'); - - // Инициализируем игрока для новой игры - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.vx = player.vy = 0; - player.invuln = 0; - - // старт — на поверхности (ровно на 1 тайл выше поверхности) - const startGX = 6; - genColumn(startGX); - const surfaceY = surfaceGyAt(startGX); - player.y = (surfaceY - 1) * TILE; - player.x = startGX * TILE; - player.fallStartY = player.y; - - // Обновляем spawnPoint, чтобы возрождение было на поверхности - spawnPoint.x = player.x; - spawnPoint.y = player.y; - - console.log('Новая игра: startGX=', startGX, 'surfaceY=', surfaceY, 'player.y=', player.y, 'player.hp=', player.hp); - console.log('spawnPoint обновлён: x=', spawnPoint.x, 'y=', spawnPoint.y); - - // Генерируем карту вокруг стартовой позиции при инициализации - for(let gx = startGX - 50; gx <= startGX + 50; gx++){ - genColumn(gx); - } - } - - // Автосейв при скрытии страницы (защита от потери прогресса) - document.addEventListener('visibilitychange', () => { - if(document.hidden){ - saveGame(); - } - }); - - // Автосейв перед закрытием страницы (защита от потери прогресса) - window.addEventListener('beforeunload', () => { - saveGame(); - }); - }).catch(err => { - console.error('Ошибка инициализации:', err); - // При ошибке начинаем новую игру - const startGX = 6; - genColumn(startGX); - player.y = (surfaceGyAt(startGX)-1)*TILE; - player.fallStartY = player.y; - - for(let gx = startGX - 50; gx <= startGX + 50; gx++){ - genColumn(gx); - } - }); - - // main loop - let last = performance.now(); - let prevJump = false; - function loop(now){ - const dt = Math.min(0.05, (now-last)/1000); - last = now; - - const jumpPressed = inp.j && !prevJump; - prevJump = inp.j; - - // Ускорение времени во время сна - if(player.sleeping && isNight()){ - worldTime += dt * 8 / DAY_LEN; // В 8 раз быстрее - // Восстанавливаем здоровье во время сна - player.hp = Math.min(100, player.hp + dt * 20); - // Автоматическое пробуждение когда наступает день - if(!isNight()){ - player.sleeping = false; - } - } else { - worldTime += dt / DAY_LEN; - } - if(worldTime >= 1) worldTime -= 1; - - // камера следует за игроком по X/Y - camX = Math.floor((player.x + player.w/2) - W/2); - camY = Math.floor((player.y + player.h/2) - H/2); - - ensureGenAroundCamera(); - - // clouds parallax - for(const c of clouds){ - c.x -= c.s * dt; - if(c.x + c.w < camX - 400) c.x = camX + W + Math.random()*700; - } - - // player - updateWaterFlag(player); - - // кислород/утопление: тратим O2 только если голова под водой, иначе восстанавливаем [web:223] - if(player.headInWater){ - player.o2 = Math.max(0, player.o2 - 6*dt); // замедлил в 3.7 раза - if(player.o2 === 0){ - player.hp -= 4*dt; // уменьшил урон от утопления - } - } else { - player.o2 = Math.min(100, player.o2 + 10*dt); // замедлил восстановление в 4 раза - } - - // голод убывает, но HP не отнимает (как просили) - player.hunger = Math.max(0, player.hunger - dt*0.2); // замедлил в 4 раза - - // Игрок не может двигаться во время сна - if(player.sleeping){ - player.vx = 0; - player.vy = 0; - } else { - const dir = (inp.r?1:0) - (inp.l?1:0); - if(dir) player.vx = dir*MOVE; - else player.vx *= 0.82; - } - - // Звук шагов при движении по земле - if(player.grounded && !player.inWater && Math.abs(player.vx) > 50){ - const stepInterval = 0.35; // Интервал между шагами в секундах - if(now/1000 - player.lastStepTime > stepInterval){ - playSound('step'); - player.lastStepTime = now/1000; - } - } - - // прыжок/плавание (новая логика) - if(player.inBoat){ - // Игрок в лодке - лодка следует за игроком - const dir = (inp.r?1:0) - (inp.l?1:0); - if(dir) boat.vx = dir * MOVE; - else boat.vx *= 0.95; - - // Лодка плавает на воде - boat.vy = 0; - - // Игрок следует за лодкой (сидит внутри неё) - player.x = boat.x + 2; // Игрок по центру лодки - player.y = boat.y - 4; // Игрок выше лодки (сидит внутри) - player.vx = boat.vx; - player.vy = boat.vy; - player.grounded = true; - player.inWater = false; // Игрок не в воде когда в лодке - - // Прыжок из лодки (высадка) - if(jumpPressed){ - // Возвращаем лодку в инвентарь - inv.boat = (inv.boat || 0) + 1; - - player.inBoat = false; - boat.active = false; - player.y += TILE; // Прыгаем из лодки - player.vy = -JUMP * 0.5; - playSound('splash'); - } - - } else if(player.inWater){ - // сопротивление в воде - player.vx *= 0.90; - player.vy *= 0.92; - - // Если не нажимаем прыжок - тонем (гравитация в воде) - if(!jumpPressed && !inp.j){ - // Применяем гравитацию в воде - игрок тонет - player.vy += GRAV_WATER * dt; - } else { - // Если нажимаем прыжок - поднимаемся на поверхность - if(jumpPressed){ - player.vy = Math.min(player.vy, -520); // рывок вверх - } else if(inp.j){ - // если держим — мягкое всплытие - player.vy = Math.min(player.vy, -260); - } - } - - } else { - // обычный прыжок (только по нажатию) - if(jumpPressed && player.grounded && !player.sleeping){ - player.vy = -JUMP; - player.grounded = false; - player.fallStartY = player.y; - } - } - - // Гравитация применяется только вне воды и вне лодки - if(!player.inWater && !player.inBoat){ - player.vy += GRAV*dt; - } - - // Обновляем позицию лодки - if(boat.active){ - boat.x += boat.vx * dt; - boat.y += boat.vy * dt; - - // Лодка не выходит за пределы воды - const boatGX = Math.floor(boat.x / TILE); - const boatGY = Math.floor(boat.y / TILE); - const below = getBlock(boatGX, boatGY + 1); - - if(!below || below.t !== 'water'){ - // Если лодка вышла из воды - выкидываем игрока - inv.boat = (inv.boat || 0) + 1; - player.inBoat = false; - boat.active = false; - player.y += TILE; - player.vy = -200; - playSound('splash'); - } - } - - // Проверяем, не доплыл ли игрок из лодки - if(player.inBoat && !boat.active){ - inv.boat = (inv.boat || 0) + 1; - player.inBoat = false; - player.y += TILE; - player.vy = -200; - playSound('splash'); - } - - player.y += player.vy*dt; - resolveY(player); - player.x += player.vx*dt; resolveX(player); - - // Отправляем позицию на сервер (мультиплеер) - sendPlayerPosition(); - - // Обновляем физику воды - updateWaterPhysics(dt); - - player.invuln = Math.max(0, player.invuln - dt); - - // TNT tick - for(const key of Array.from(activeTNT)){ - const b = grid.get(key); - if(!b || b.dead){ activeTNT.delete(key); continue; } - b.fuse -= dt; - if(b.fuse <= 0){ - explodeAt(b.gx,b.gy); - } - } - - // mobs spawn (с обеих сторон камеры) - spawnT += dt; - if(spawnT > 1.8 && mobs.length < 30){ - spawnT = 0; - - // Выбираем сторону спавна (левая или правая) - const spawnLeft = Math.random() < 0.5; - const gx = spawnLeft - ? Math.floor((camX - 200)/TILE) - : Math.floor((camX + W + 200)/TILE); - - genColumn(gx); - const sgy = surfaceGyAt(gx); - const wx = gx*TILE + 4; - const wy = (sgy-2)*TILE; - - // не спавнить в воде - const top = getBlock(gx, sgy); - if(top && top.t==='water') { - // skip - } else { - if(isNight()){ - // Ночью спавним больше враждебных мобов - const rand = Math.random(); - if(rand < 0.35){ - mobs.push(new Zombie(wx, wy)); - } else if(rand < 0.55){ - mobs.push(new Creeper(wx, wy)); - } else { - mobs.push(new Skeleton(wx, wy)); - } - } else { - // Днём только животные - mobs.push(Math.random()<0.5 ? new Pig(wx, wy) : new Chicken(wx, wy)); - } - } - } - - // mobs update - for(let i=mobs.length-1;i>=0;i--){ - const m = mobs[i]; - mobAI(m, dt); - if(m.hp<=0) mobs.splice(i,1); - } - - // particles - for(let i=parts.length-1;i>=0;i--){ - const p = parts[i]; - p.t -= dt; - p.x += p.vx*dt; - p.y += p.vy*dt; - p.vy += GRAV*dt; - if(p.t <= 0) parts.splice(i,1); - } - - // death - if(player.hp <= 0){ - deathEl.style.display='flex'; - } else if(deathEl.style.display === 'flex') { - // Если HP > 0 но экран смерти всё ещё показан - скрываем его - deathEl.style.display='none'; - } - - // render - const night = isNight(); - - // sky - ctx.fillStyle = night ? '#070816' : '#87CEEB'; - ctx.fillRect(0,0,W,H); - - // clouds (parallax x/y) - ctx.save(); - ctx.translate(-camX*0.5, -camY*0.15); - ctx.fillStyle = 'rgba(255,255,255,0.65)'; - for(const c of clouds){ - ctx.fillRect(c.x, c.y, c.w, 26); - ctx.fillRect(c.x+20, c.y-10, c.w*0.6, 22); - } - ctx.restore(); - - // world - ctx.save(); - ctx.translate(-camX, -camY); - - const minGX = Math.floor(camX/TILE)-2; - const maxGX = Math.floor((camX+W)/TILE)+2; - const minGY = Math.floor(camY/TILE)-6; - const maxGY = Math.floor((camY+H)/TILE)+6; - - // draw blocks (по массиву, но фильтруем диапазоном) - for(const b of blocks){ - if(b.dead) continue; - if(b.gx < minGX || b.gx > maxGX || b.gy < minGY || b.gy > maxGY) continue; - - const def = BLOCKS[b.t]; - if(def.alpha){ - ctx.save(); - ctx.globalAlpha = def.alpha; - ctx.drawImage(tex[b.t], b.gx*TILE, b.gy*TILE, TILE, TILE); - ctx.restore(); - } else { - ctx.drawImage(tex[b.t], b.gx*TILE, b.gy*TILE, TILE, TILE); - } - - // TNT мигает, если активирован - if(b.t==='tnt' && b.active && Math.sin(now/60)>0){ - ctx.fillStyle='rgba(255,255,255,0.45)'; - ctx.fillRect(b.gx*TILE, b.gy*TILE, TILE, TILE); - } - - // огонь костра - if(b.t==='campfire'){ - drawFire(b.gx*TILE, b.gy*TILE, now); - } - } - - // mobs - for(const m of mobs){ - if(m.kind==='zombie'){ - ctx.fillStyle = '#2ecc71'; - ctx.fillRect(m.x, m.y, m.w, m.h); - ctx.fillStyle = '#c0392b'; - ctx.fillRect(m.x+6, m.y+12, 6,6); - ctx.fillRect(m.x+22, m.y+12, 6,6); - } else if(m.kind==='pig'){ - ctx.fillStyle = '#ffb6c1'; - ctx.fillRect(m.x, m.y, m.w, m.h); - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+22, m.y+5, 3,3); - ctx.fillStyle = '#ff69b4'; - ctx.fillRect(m.x+28, m.y+12, 6,6); - } else if(m.kind==='chicken'){ - // chicken - ctx.fillStyle = '#ecf0f1'; - ctx.fillRect(m.x, m.y, m.w, m.h); - ctx.fillStyle = '#f39c12'; - ctx.fillRect(m.x+18, m.y+10, 6,4); - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+8, m.y+6, 3,3); - } else if(m.kind==='creeper'){ - // creeper - ctx.fillStyle = '#4CAF50'; - ctx.fillRect(m.x, m.y, m.w, m.h); - // Глаза - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+8, m.y+8, 4,4); - ctx.fillRect(m.x+22, m.y+8, 4,4); - // Рот - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+12, m.y+20, 10,4); - // Ноги - ctx.fillStyle = '#4CAF50'; - ctx.fillRect(m.x+4, m.y+30, 6,20); - ctx.fillRect(m.x+24, m.y+30, 6,20); - } else if(m.kind==='skeleton'){ - // skeleton - детализированный - // Тело - ctx.fillStyle = '#ECEFF1'; - ctx.fillRect(m.x+10, m.y+20, 14, 12); - // Череп - ctx.fillRect(m.x+8, m.y+0, 18, 18); - // Глазницы - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+10, m.y+6, 4,4); - ctx.fillRect(m.x+20, m.y+6, 4,4); - // Нос - ctx.fillRect(m.x+15, m.y+12, 4,2); - // Руки - ctx.fillStyle = '#ECEFF1'; - ctx.fillRect(m.x+2, m.y+20, 6,14); - ctx.fillRect(m.x+26, m.y+20, 6,14); - // Ноги - ctx.fillRect(m.x+10, m.y+32, 6, 18); - ctx.fillRect(m.x+18, m.y+32, 6, 18); - } - } - - // boat (рисуем первой, чтобы игрок был внутри неё) - if(boat.active){ - ctx.drawImage(tex['boat'], boat.x - (TILE-boat.w)/2, boat.y - (TILE-boat.h)/2, TILE, TILE); - } - - // other players (multiplayer) - for(const [socketId, p] of otherPlayers){ - if(heroImg.complete){ - ctx.drawImage(heroImg, p.x - (TILE-player.w)/2, p.y - (TILE-player.h)/2, TILE, TILE); - } else { - ctx.fillStyle = p.color; - ctx.fillRect(p.x, p.y, 34, 34); - } - // Имя игрока (мелко над персонажем) - ctx.fillStyle = '#fff'; - ctx.font = '12px system-ui'; - ctx.textAlign = 'center'; - ctx.fillText(p.name, p.x + 17, p.y - 8); - } - - // player - if(heroImg.complete){ - ctx.drawImage(heroImg, player.x - (TILE-player.w)/2, player.y - (TILE-player.h)/2, TILE, TILE); - } else { - ctx.fillStyle='#fff'; - ctx.fillRect(player.x, player.y, player.w, player.h); - } - - // particles - for(const p of parts){ - ctx.fillStyle = p.c; - ctx.fillRect(p.x-2, p.y-2, 4, 4); - } - - // Стрелы скелета - for(const m of mobs){ - if(m.kind==='skeleton' && m.shootCooldown > 0.5){ - // Рисуем стрелу - const arrowX = m.x + m.w/2; - const arrowY = m.y + 15; - const targetX = player.x + player.w/2; - const targetY = player.y + player.h/2; - const angle = Math.atan2(targetY - arrowY, targetX - arrowX); - const speed = 400; - - // Проверяем, попала ли стрела - const dx = targetX - arrowX; - const dy = targetY - arrowY; - const dist = Math.hypot(dx, dy); - - // Рисуем стрелу - ctx.save(); - ctx.translate(arrowX, arrowY); - ctx.rotate(angle); - ctx.fillStyle = '#ECEFF1'; - ctx.fillRect(0, -1, 16, 2); - ctx.restore(); - - // Урон игроку если попали - if(dist < 150 && player.invuln <= 0){ - player.hp -= 8; - player.invuln = 0.5; - player.vx += Math.cos(angle) * 300; - player.vy -= 200; - playSound('hit1'); - } - } - } - - // build ghost - if(mode()==='build' && mouse.x!==null && !craftOpen && player.hp>0){ - const wx = mouse.x + camX; - const wy = mouse.y + camY; - const gx = Math.floor(wx/TILE); - const gy = Math.floor(wy/TILE); - ctx.strokeStyle = 'rgba(255,255,255,0.9)'; - ctx.lineWidth = 2; - ctx.strokeRect(gx*TILE, gy*TILE, TILE, TILE); - } - - ctx.restore(); - - // lighting overlay at night - if(night){ - // Рисуем полупрозрачный тёмный оверлей - ctx.save(); - ctx.fillStyle = 'rgba(0,0,0,0.50)'; - ctx.fillRect(0, 0, W, H); - ctx.restore(); - - // Освещение от факелов и костров - ctx.save(); - ctx.globalCompositeOperation = 'lighter'; - - // Функция для рисования света - function drawLight(x, y, radius, intensity) { - const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius); - gradient.addColorStop(0, `rgba(255, 255, 200, ${intensity})`); - gradient.addColorStop(1, `rgba(255, 255, 200, 0)`); - ctx.fillStyle = gradient; - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fill(); - } - - // Освещение от факелов (1/3 яркости) - for(const b of blocks){ - if(b.dead) continue; - if(b.gx < minGX || b.gx > maxGX || b.gy < minGY || b.gy > maxGY) continue; - const def = BLOCKS[b.t]; - if(def.lightRadius){ - const wx = b.gx*TILE + TILE/2 - camX; - const wy = b.gy*TILE + TILE/2 - camY; - drawLight(wx, wy, def.lightRadius, 0.33); - } - } - - ctx.restore(); - } - - // UI tick - if(Math.random()<0.25){ - hpEl.textContent = Math.max(0, Math.ceil(player.hp)); - foodEl.textContent = Math.ceil(player.hunger); - document.getElementById('o2').textContent = Math.ceil(player.o2); - sxEl.textContent = Math.floor(player.x/TILE); - syEl.textContent = Math.floor(player.y/TILE); - todEl.textContent = night ? 'Ночь' : 'День'; - worldIdEl.textContent = worldId; - if(isMultiplayer){ - document.getElementById('multiplayerStatus').style.display = 'flex'; - playerCountEl.textContent = otherPlayers.size + 1; // +1 = мы сами - } else { - document.getElementById('multiplayerStatus').style.display = 'none'; - } - } - - // Индикатор сна - if(player.sleeping){ - ctx.fillStyle = 'rgba(0,0,0,0.7)'; - ctx.fillRect(0, 0, W, H); - ctx.fillStyle = '#fff'; - ctx.font = 'bold 32px system-ui'; - ctx.textAlign = 'center'; - ctx.fillText('💤 Спим...', W/2, H/2); - ctx.font = '18px system-ui'; - ctx.fillText('Нажмите на кровать чтобы проснуться', W/2, H/2 + 40); - } - - requestAnimationFrame(loop); - } - - requestAnimationFrame(loop); -})(); diff --git a/backup/20260103_082922/index.html b/backup/20260103_082922/index.html deleted file mode 100644 index 3313464..0000000 --- a/backup/20260103_082922/index.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - -GrechkaCraft: Multiplayer - - - - - - -
- - -
-
-
❤️ 100   🍗 100
-
🫁 100
-
📍 X:0 Y:0
-
🕒 День
-
🌐 default
- -
- -
🏃
-
💾
-
🔨
-
🔄
- -
-
-
Крафт
- -
-
-
- -
-
-
Инвентарь
- -
-
-
- -
-
ВЫ ПОГИБЛИ
- -
- -
-
🎒
-
- - - -
💬
-
- -
-
←
- -
↑
-
- - - - diff --git a/backup/20260103_082922/style.css b/backup/20260103_082922/style.css deleted file mode 100644 index af8325b..0000000 --- a/backup/20260103_082922/style.css +++ /dev/null @@ -1,72 +0,0 @@ -/* Minegrechka Game Styles - v1.2 */ -html, body { margin:0; padding:0; width:100%; height:100%; overflow:hidden; background:#111; font-family: system-ui, sans-serif; user-select:none; -webkit-user-select:none; touch-action:none; } -#game { position:absolute; top:0; left:0; right:0; bottom:140px; background:#87CEEB; overflow:hidden; } -canvas { display:block; width:100%; height:100%; image-rendering:pixelated; } -#controls { position:absolute; left:0; right:0; bottom:0; height:140px; background:#222; border-top:4px solid #444; z-index:10; } - -.ui { position:absolute; inset:0; pointer-events:none; z-index:20; } -.pe { pointer-events:auto; } - -#stats { position:absolute; left:10px; top:10px; color:#fff; font-weight:800; font-size:14px; - background: rgba(0,0,0,0.55); padding:8px; border-radius:10px; text-shadow:1px 1px 0 #000; } -#stats .row{ display:flex; gap:10px; align-items:center; } - -.rbtn { position:absolute; right:10px; width:52px; height:52px; border-radius:12px; - display:flex; align-items:center; justify-content:center; border:2px solid rgba(255,255,255,0.9); - font-size:24px; cursor:pointer; pointer-events:auto; box-shadow:0 4px 0 rgba(0,0,0,0.5); } -.rbtn:active { transform: translateY(4px); box-shadow:none; } -#modeBtn { top:10px; background:#f39c12; } -#saveBtn { top:10px; right:70px !important; background:#27ae60; } -#resetBtn { top:10px; right:130px !important; background:#e74c3c; } -#craftBtn { top:74px; right:10px !important; background:#9b59b6; } -#chatToggle { top:10px; right:190px !important; background:#9b59b6; } - -#hotbar { position:absolute; left:50%; transform:translateX(-50%); bottom:10px; display:flex; gap:6px; - background: rgba(0,0,0,0.60); padding:6px; border-radius:12px; pointer-events:auto; - overflow-x: auto; overflow-y: hidden; max-width: 80%; } -.slot { width:38px; height:38px; border:2px solid rgba(255,255,255,0.22); border-radius:10px; - position:relative; overflow:hidden; cursor:pointer; background: rgba(255,255,255,0.07); - display:flex; align-items:center; justify-content:center; font-size:18px; } -.slot.sel { border-color:#f1c40f; box-shadow: 0 0 0 2px rgba(241,196,15,0.18) inset; } -.count { position:absolute; right:3px; bottom:1px; font-size:10px; color:#fff; font-weight:900; text-shadow:1px 1px 0 #000; } - -/* Craft modal */ -#craftPanel { display:none; position:absolute; left:14px; right:14px; top:14px; bottom:14px; - background: rgba(10,10,12,0.92); border:2px solid rgba(255,255,255,0.85); border-radius:14px; - pointer-events:auto; padding:12px; overflow:auto; } - -/* Inventory modal */ -#inventoryPanel { display:none; position:absolute; left:50%; top:50%; transform: translate(-50%, -50%); - width: 420px; max-width: 90%; background: rgba(10,10,12,0.92); border:2px solid rgba(255,255,255,0.85); - border-radius:14px; pointer-events:auto; padding:12px; z-index: 100; } -#inventoryPanel .top { display:flex; justify-content:space-between; align-items:center; color:#fff; margin-bottom:10px; } -#inventoryGrid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin-top: 10px; } -.inv-slot { width: 48px; height: 48px; border: 2px solid rgba(255,255,255,0.22); border-radius: 8px; - position: relative; overflow: hidden; cursor: pointer; background: rgba(255,255,255,0.07); - display: flex; align-items: center; justify-content: center; font-size: 24px; } -.inv-slot.sel { border-color: #f1c40f; box-shadow: 0 0 0 2px rgba(241,196,15,0.18) inset; } -.inv-count { position: absolute; right: 3px; bottom: 1px; font-size: 12px; color: #fff; - font-weight: 900; text-shadow: 1px 1px 0 #000; background: rgba(0,0,0,0.5); padding: 1px 3px; border-radius: 4px; } -#craftPanel .top { display:flex; justify-content:space-between; align-items:center; color:#fff; margin-bottom:10px; } -#craftPanel .close { background:#c0392b; border:none; color:#fff; font-weight:900; padding:8px 10px; border-radius:10px; cursor:pointer; } -.recipe { display:flex; align-items:center; gap:10px; padding:10px; border-radius:12px; - background: rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.10); margin-bottom:8px; } -.ricon { width:32px; height:32px; border-radius:8px; background-size:cover; image-rendering:pixelated; } -.rinfo { flex:1; } -.rname { color:#fff; font-weight:900; font-size:14px; } -.rcost { color:#bbb; font-size:11px; line-height:1.25; } -.rcraft { background:#2ecc71; border:none; color:#fff; font-weight:900; padding:8px 10px; border-radius:10px; cursor:pointer; } -.rcraft:disabled { background:#444; color:#888; cursor:not-allowed; } - -/* Controls buttons */ -.cbtn { position:absolute; top:50%; transform:translateY(-50%); width:74px; height:74px; border-radius:14px; - background:#333; border:3px solid #555; color:#fff; font-size:32px; display:flex; align-items:center; justify-content:center; - box-shadow:0 7px 0 #111; pointer-events:auto; } -.cbtn:active { transform:translateY(-46%); box-shadow:0 3px 0 #111; background:#444; } -#left { left:18px; } -#right { left:102px; } -#jump { right:18px; background:#d35400; border-color:#e67e22; } - -#death { display:none; position:absolute; inset:0; background: rgba(60,0,0,0.88); - z-index:200; color:#fff; pointer-events:auto; align-items:center; justify-content:center; flex-direction:column; gap:12px; } -#death button { padding:12px 18px; font-size:18px; font-weight:900; border:none; border-radius:12px; cursor:pointer; } diff --git a/build3d.js b/build3d.js new file mode 100644 index 0000000..1f84169 --- /dev/null +++ b/build3d.js @@ -0,0 +1,107 @@ +#!/usr/bin/env node +/** + * Simple ES-module → IIFE bundler for GrechkaCraft + * Resolves import/export, wraps in single IIFE, no external deps. + * Usage: node build.js > game.js + */ +const fs = require('fs'); +const path = require('path'); + +const SRC = path.join(__dirname, 'srcv3'); +const ENTRY = 'main3d.js'; + +const visited = new Set(); +const chunks = []; + +const fileExports = {}; +const fileImports = {}; + +function resolveImportPath(fromPath, importerDir) { + let resolved = path.resolve(importerDir, fromPath); + if (!fs.existsSync(resolved) && fs.existsSync(resolved + '.js')) { + resolved += '.js'; + } + return resolved; +} + +function collectExports(content) { + const exports = []; + const re1 = /export\s*\{([^}]+)\}/g; + let m; + while ((m = re1.exec(content)) !== null) { + const names = m[1].split(',').map(s => { + const parts = s.trim().split(/\s+as\s+/); + return { local: parts[0].trim(), exported: (parts[1] || parts[0]).trim() }; + }); + exports.push(...names); + } + const re2 = /export\s+(?:async\s+)?(?:const|let|var|function|class)\s+(\w+)/g; + while ((m = re2.exec(content)) !== null) { + exports.push({ local: m[1], exported: m[1] }); + } + if (/export\s+default\s+/.test(content)) { + exports.push({ local: '__default__', exported: 'default' }); + } + return exports; +} + +function collectImports(content) { + const imports = []; + const re = /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g; + let m; + while ((m = re.exec(content)) !== null) { + const names = m[1].split(',').map(s => { + const parts = s.trim().split(/\s+as\s+/); + return { local: (parts[1] || parts[0]).trim(), imported: parts[0].trim() }; + }); + imports.push({ names, from: m[2] }); + } + const re2 = /import\s+(\w+)\s+from\s*['"]([^'"]+)['"]/g; + while ((m = re2.exec(content)) !== null) { + imports.push({ names: [{ local: m[1], imported: 'default' }], from: m[2] }); + } + return imports; +} + +function processFile(filePath) { + const relPath = path.relative(SRC, filePath); + if (visited.has(relPath)) return; + visited.add(relPath); + + let content = fs.readFileSync(filePath, 'utf8'); + const dir = path.dirname(filePath); + + fileExports[relPath] = collectExports(content); + fileImports[relPath] = collectImports(content); + + for (const imp of fileImports[relPath]) { + const depPath = resolveImportPath(imp.from, dir); + processFile(depPath); + } + + // Strip import statements + content = content.replace(/import\s*\{[^}]+\}\s*from\s*['"][^'"]+['"];?/g, ''); + content = content.replace(/import\s+\w+\s+from\s*['"][^'"]+['"];?/g, ''); + // Strip export keyword but keep declarations + content = content.replace(/export\s+default\s+/, 'const __default__ = '); + content = content.replace(/export\s+(?:async\s+)?(const|let|var|function|class)\s/g, '$1 '); + content = content.replace(/export\s*\{[^}]*\};?/g, ''); + + chunks.push({ relPath, content }); +} + +processFile(path.join(SRC, ENTRY)); + +let output = '// GrechkaCraft — auto-bundled from ES modules\n'; +output += '(function() {\n'; +output += '"use strict";\n\n'; + +for (const chunk of chunks) { + output += `// === ${chunk.relPath} ===\n`; + output += chunk.content.trim(); + output += '\n\n'; +} + +output += '})();\n'; + +process.stdout.write(output); \ No newline at end of file diff --git a/game.js b/game.js index 7f1bd07..1cda237 100644 --- a/game.js +++ b/game.js @@ -1,1560 +1,1865 @@ (() => { - // ==================== КОНФИГУРАЦИЯ СЕРВЕРА ==================== -// === Custom modal functions === -function customAlert(msg) { - const overlay = document.createElement("div"); - overlay.className = "custom-modal-overlay"; - const box = document.createElement("div"); - box.className = "custom-modal-box"; - const text = document.createElement("div"); - text.textContent = msg; - text.style.marginBottom = "16px"; - const btn = document.createElement("button"); - btn.className = "btn-ok"; - btn.textContent = "OK"; - btn.onclick = () => overlay.remove(); - box.appendChild(text); - box.appendChild(btn); - overlay.appendChild(box); - document.querySelector("#game").appendChild(overlay); -} -function customConfirm(msg, onYes) { - const overlay = document.createElement("div"); - overlay.className = "custom-modal-overlay"; - const box = document.createElement("div"); - box.className = "custom-modal-box"; - const text = document.createElement("div"); - text.textContent = msg; - text.style.marginBottom = "16px"; - const btns = document.createElement("div"); - btns.className = "modal-btns"; - const yesBtn = document.createElement("button"); - yesBtn.className = "btn-yes"; - yesBtn.textContent = "Да"; - yesBtn.onclick = () => { overlay.remove(); onYes(); }; - const noBtn = document.createElement("button"); - noBtn.className = "btn-no"; - noBtn.textContent = "Отмена"; - noBtn.onclick = () => overlay.remove(); - btns.appendChild(yesBtn); - btns.appendChild(noBtn); - box.appendChild(text); - box.appendChild(btns); - overlay.appendChild(box); - document.querySelector("#game").appendChild(overlay); -} - // Возможность переопределить сервер через query string - const urlParams = new URLSearchParams(window.location.search); - const SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru'; - const TELEGRAM_BOT_USERNAME = 'Grechkacraft_bot'; // Имя бота для share-ссылки - const TELEGRAM_APP_SHORT_NAME = 'minegrechka'; // Короткое имя Mini App - - // Защита от mixed content - if (location.protocol === 'https:' && SERVER_URL.startsWith('http://')) { - console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP'); - alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.'); - } - - // ==================== TELEGRAM MINI APP ==================== - const isTgWebApp = !!(window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData); - let tgUser = null; - let tgSaveLoaded = false; - - if (isTgWebApp) { - try { - Telegram.WebApp.ready(); - Telegram.WebApp.expand(); - tgUser = Telegram.WebApp.initDataUnsafe?.user || null; - console.log('[TG] Mini App detected, user:', tgUser?.id, tgUser?.username); - // Set player name from TG - if (tgUser && tgUser.first_name) { - const tgName = tgUser.username || tgUser.first_name; - localStorage.setItem('minegrechka_playerName', tgName); - } - } catch(e) { - console.warn('[TG] WebApp init error:', e); - } - } + // src/core/constants.js + var TILE = 40; + var SEA_GY = 14; + var BEDROCK_GY = 140; + var GEN_MARGIN_X = 40; + var GRAV = 2200; + var GRAV_WATER = 550; + var MOVE = 320; + var JUMP = 760; + var DAY_LEN = 360; + var SAVE_KEY = "minegrechka_save"; + var MOVE_SEND_INTERVAL = 0.05; + var MAX_RAINDROPS = 200; + var MAX_CHAT_MESSAGES = 50; + var VOICE_SERVER = "wss://voicegrech.mkn8n.ru"; - // ==================== WORLD ID И ИГРОКА ==================== - let worldId = null; - let playerName = localStorage.getItem('minegrechka_playerName') || null; - - // Priority: URL param > TG saved world > new world - const worldParam = urlParams.get('world'); - - // ==================== START MENU ==================== - let startMenuResolved = false; - - function generateShortWorldCode() { - // Generate readable 6-char code: consonants + digits (easy to copy/share) - const c = 'bcdfghjkmnpqrstvwxyz'; - const d = '23456789'; - let code = ''; - for (let i = 0; i < 3; i++) code += c[Math.floor(Math.random() * c.length)]; - for (let i = 0; i < 3; i++) code += d[Math.floor(Math.random() * d.length)]; - return code; - } - - function showStartMenu() { - return new Promise((resolve) => { - // If world is in URL — skip menu, auto-join - if (worldParam && worldParam.trim() !== '') { - worldId = worldParam.trim(); - console.log('[Menu] Joining world from URL:', worldId); - resolve(worldId); - return; - } - - // ===== Minecraft-style CSS ===== - const mcCSS = ` - .mc-overlay { position:fixed;top:0;left:0;width:100%;height:100%;z-index:99999; - display:flex;align-items:center;justify-content:center;font-family:'Courier New',monospace; - image-rendering:pixelated; } - .mc-bg { position:absolute;inset:0;background:#000;opacity:0.75; } - .mc-dirt { position:absolute;bottom:0;left:0;right:0;height:40%; - background:repeating-linear-gradient(0deg,#8B6914 0px,#8B6914 2px,#7A5B12 2px,#7A5B12 4px);opacity:0.3; } - .mc-box { position:relative;z-index:1;background:#C6C6C6;border:4px solid #000; - padding:0;max-width:420px;width:92%;box-shadow:4px 4px 0 #000; } - .mc-titlebar { background:#1a1a2e;padding:12px 16px;text-align:center; - border-bottom:4px solid #000; } - .mc-title { font-size:24px;font-weight:bold;color:#FFAA00;text-shadow:2px 2px 0 #000; - letter-spacing:2px; } - .mc-subtitle { font-size:13px;color:#AAA;margin-top:4px; } - .mc-body { padding:16px;background:#C6C6C6; } - .mc-btn { display:block;width:100%;padding:10px 16px;margin-bottom:8px; - font-family:'Courier New',monospace;font-size:16px;font-weight:bold; - cursor:pointer;text-align:center;border:3px solid #000; - image-rendering:pixelated;box-shadow:3px 3px 0 #000; - transition:transform 0.05s,box-shadow 0.05s; } - .mc-btn:active { transform:translate(2px,2px);box-shadow:1px 1px 0 #000; } - .mc-btn-green { background:#5A8F3C;color:#E0E0E0;border-color:#3A5F1C;text-shadow:1px 1px 0 #222; } - .mc-btn-green:hover { background:#6DAF4E; } - .mc-btn-blue { background:#3C5F9E;color:#E0E0E0;border-color:#2A4070;text-shadow:1px 1px 0 #222; } - .mc-btn-blue:hover { background:#4B73BE; } - .mc-btn-purple { background:#7B3FA0;color:#E0E0E0;border-color:#5A2C78;text-shadow:1px 1px 0 #222; } - .mc-btn-purple:hover { background:#9555BE; } - .mc-btn-orange { background:#B86E00;color:#E0E0E0;border-color:#8A5200;text-shadow:1px 1px 0 #222; } - .mc-btn-orange:hover { background:#DA8500; } - .mc-btn-red { background:#9E2B2B;color:#E0E0E0;border-color:#6E1B1B;text-shadow:1px 1px 0 #222; } - .mc-btn-red:hover { background:#BE3B3B; } - .mc-btn-gray { background:#555;color:#AAA;border-color:#333;text-shadow:1px 1px 0 #111; } - .mc-btn-gray:hover { background:#666;color:#DDD; } - .mc-btn-outline { background:transparent;color:#555;border-color:#555; - text-shadow:none;box-shadow:2px 2px 0 #333; } - .mc-btn-outline:hover { background:#DDD;color:#222; } - .mc-input { width:100%;padding:8px 12px;font-family:'Courier New',monospace; - font-size:16px;background:#FFF;border:3px solid #000;color:#222; - box-shadow:inset 2px 2px 0 #999;margin:4px 0; } - .mc-input:focus { outline:none;border-color:#FFAA00; } - .mc-label { font-size:13px;color:#444;font-weight:bold;margin:8px 0 4px; } - .mc-divider { border:none;border-top:2px solid #999;margin:12px 0; } - .mc-friend-row { display:flex;justify-content:space-between;align-items:center; - padding:6px 8px;margin-bottom:4px;background:#E8E8E8;border:2px solid #999; - box-shadow:2px 2px 0 #999; } - .mc-friend-btn { padding:4px 10px;font-family:'Courier New',monospace; - font-size:13px;font-weight:bold;cursor:pointer;border:2px solid #000; - box-shadow:1px 1px 0 #000; } - .mc-friend-btn:active { transform:translate(1px,1px);box-shadow:none; } - .mc-friend-btn-green { background:#5A8F3C;color:#E0E0E0; } - .mc-friend-btn-red { background:#9E2B2B;color:#E0E0E0; } - .mc-checkbox-row { display:flex;align-items:center;gap:8px;font-size:13px;color:#444;cursor:pointer; } - .mc-checkbox { width:18px;height:18px;accent-color:#5A8F3C; } - `; - - const styleEl = document.createElement('style'); - styleEl.textContent = mcCSS; - document.head.appendChild(styleEl); - - const overlay = document.createElement('div'); - overlay.className = 'mc-overlay'; - overlay.id = 'startMenu'; - overlay.innerHTML = '
'; - - const box = document.createElement('div'); - box.className = 'mc-box'; - - // Title bar - const titleBar = document.createElement('div'); - titleBar.className = 'mc-titlebar'; - titleBar.innerHTML = '
⛏️ GrechkaCraft
' + - '
' + (isTgWebApp ? ('Привет, ' + (tgUser?.first_name || 'Игрок') + '!') : 'Voxel-мир в браузере') + '
'; - box.appendChild(titleBar); - - const body = document.createElement('div'); - body.className = 'mc-body'; - - // === Buttons === - // New World - const btnNew = document.createElement('button'); - btnNew.className = 'mc-btn mc-btn-green'; - btnNew.textContent = '🌍 Новый мир'; - btnNew.onclick = () => { - worldId = generateShortWorldCode(); - overlay.remove(); - styleEl.remove(); - resolve(worldId); - }; - body.appendChild(btnNew); - - // Join by code - const btnJoin = document.createElement('button'); - btnJoin.className = 'mc-btn mc-btn-blue'; - btnJoin.textContent = '🔗 Вступить по коду'; - btnJoin.onclick = () => { - body.innerHTML = ''; - const lbl = document.createElement('div'); - lbl.className = 'mc-label'; - lbl.textContent = 'Код мира:'; - body.appendChild(lbl); - const codeInput = document.createElement('input'); - codeInput.className = 'mc-input'; - codeInput.type = 'text'; - codeInput.placeholder = 'bkh237'; - codeInput.maxLength = 12; - body.appendChild(codeInput); - const btnGo = document.createElement('button'); - btnGo.className = 'mc-btn mc-btn-blue'; - btnGo.textContent = 'Войти →'; - btnGo.onclick = () => { - const code = codeInput.value.trim().toLowerCase(); - if (code.length >= 3) { - worldId = code; - overlay.remove(); - styleEl.remove(); - resolve(worldId); - } - }; - body.appendChild(btnGo); - const btnBack = document.createElement('button'); - btnBack.className = 'mc-btn mc-btn-gray'; - btnBack.textContent = '← Назад'; - btnBack.onclick = () => { overlay.remove(); styleEl.remove(); showStartMenu().then(resolve); }; - body.appendChild(btnBack); - codeInput.focus(); - codeInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') btnGo.click(); }); - }; - body.appendChild(btnJoin); - - // Continue saved game - const hasLocalSave = !!localStorage.getItem('minegrechka_save'); - if (isTgWebApp || hasLocalSave) { - const btnHistory = document.createElement('button'); - btnHistory.className = 'mc-btn mc-btn-purple'; - btnHistory.textContent = '💾 Продолжить игру'; - btnHistory.onclick = async () => { - if (isTgWebApp && tgUser) { - try { - const resp = await fetch(SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id); - const data = await resp.json(); - if (data.ok && data.world_id) { - worldId = data.world_id; - tgSaveLoaded = true; - overlay.remove(); - styleEl.remove(); - resolve(worldId); - return; - } - } catch(e) { console.warn('[TG] Load save failed:', e); } - } - if (hasLocalSave) { - worldId = generateShortWorldCode(); - overlay.remove(); - styleEl.remove(); - resolve(worldId); - return; - } - customAlert('Сохранений не найдено'); - }; - body.appendChild(btnHistory); - } - - // === TG-only buttons === - if (isTgWebApp && window.Telegram && Telegram.WebApp.switchInlineQuery) { - // Invite friends — with margin - const btnInvite = document.createElement('button'); - btnInvite.className = 'mc-btn mc-btn-outline'; - btnInvite.style.marginTop = '16px'; - btnInvite.textContent = '📤 Пригласить друзей'; - btnInvite.onclick = () => { - const code = worldId || generateShortWorldCode(); - const url = 'https://t.me/Grechkacraft_bot/app?startapp=' + encodeURIComponent(code); - if (Telegram.WebApp && Telegram.WebApp.openTelegramLink) { - Telegram.WebApp.openTelegramLink('https://t.me/share/url?url=' + encodeURIComponent(url) + '&text=' + encodeURIComponent('🏗️ Заходи в GrechkaCraft! Мир: ' + code)); - } else { - navigator.clipboard.writeText(url).then(() => showToast('📋 Скопировано!')).catch(() => showToast(url)); - } - }; - body.appendChild(btnInvite); - - // Friends — with margin - const btnFriends = document.createElement('button'); - btnFriends.className = 'mc-btn mc-btn-orange'; - btnFriends.style.marginTop = '8px'; - btnFriends.textContent = '👥 Друзья'; - btnFriends.onclick = async () => { - try { - const resp = await fetch(SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id); - const data = await resp.json(); - body.innerHTML = ''; - - // Back button at top - const btnBackTop = document.createElement('button'); - btnBackTop.className = 'mc-btn mc-btn-gray'; - btnBackTop.textContent = '← Назад'; - btnBackTop.style.marginBottom = '12px'; - btnBackTop.onclick = () => { overlay.remove(); styleEl.remove(); showStartMenu().then(resolve); }; - body.appendChild(btnBackTop); - - // Nickname section - if (data.nickname) { - const nickDiv = document.createElement('div'); - nickDiv.className = 'mc-friend-row'; - nickDiv.innerHTML = '🔗 Твой ник: ' + data.nickname + ''; - nickDiv.style.background = '#D5E8C5'; - body.appendChild(nickDiv); - } else { - const nickLabel = document.createElement('div'); - nickLabel.className = 'mc-label'; - nickLabel.style.color = '#9E2B2B'; - nickLabel.textContent = '⚡ Задай ник, чтобы друзья могли тебя найти:'; - body.appendChild(nickLabel); - const nickRow = document.createElement('div'); - nickRow.style.display = 'flex'; - nickRow.style.gap = '4px'; - nickRow.style.marginBottom = '8px'; - const nickInput = document.createElement('input'); - nickInput.className = 'mc-input'; - nickInput.style.flex = '1'; - nickInput.type = 'text'; - nickInput.maxLength = 16; - nickInput.placeholder = 'Твой ник'; - const nickBtn = document.createElement('button'); - nickBtn.className = 'mc-btn mc-btn-green'; - nickBtn.style.width = 'auto'; - nickBtn.style.marginBottom = '0'; - nickBtn.style.padding = '8px 16px'; - nickBtn.textContent = '✓'; - nickBtn.onclick = async () => { - const n = nickInput.value.trim().toLowerCase(); - if (!n || n.length < 2) { showToast('❌ Минимум 2 символа'); return; } - const check = await fetch(SERVER_URL + '/api/nick/check?nick=' + encodeURIComponent(n)); - const cd = await check.json(); - if (!cd.available) { showToast('❌ Ник занят'); return; } - const set = await fetch(SERVER_URL + '/api/nick/set', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,nickname:n})}); - const sd = await set.json(); - if (sd.ok) { showToast('✅ Ник: ' + sd.nickname); btnFriends.onclick(); } else { showToast('❌ Ошибка'); } - }; - nickRow.appendChild(nickInput); - nickRow.appendChild(nickBtn); - body.appendChild(nickRow); - } - - // Friends list - if (data.friends && data.friends.length) { - const listLabel = document.createElement('div'); - listLabel.className = 'mc-label'; - listLabel.textContent = '👥 Друзья:'; - body.appendChild(listLabel); - for (const f of data.friends) { - const fDiv = document.createElement('div'); - fDiv.className = 'mc-friend-row'; - const statusDot = f.online ? '🟢' : '⚫'; - let info = statusDot + ' ' + f.nickname + ''; - if (f.online && f.world_id) info += ' — мир ' + f.world_id; - const fInfo = document.createElement('div'); - fInfo.innerHTML = info; - fDiv.appendChild(fInfo); - if (f.online && f.world_id) { - const joinBtn = document.createElement('button'); - joinBtn.className = 'mc-friend-btn mc-friend-btn-green'; - joinBtn.textContent = '🎮 Вступить'; - joinBtn.onclick = () => { worldId = f.world_id; overlay.remove(); styleEl.remove(); resolve(worldId); }; - fDiv.appendChild(joinBtn); - } - body.appendChild(fDiv); - } - } else if (data.friends && !data.friends.length) { - const noFriends = document.createElement('div'); - noFriends.className = 'mc-label'; - noFriends.style.color = '#888'; - noFriends.textContent = 'Пока нет друзей'; - body.appendChild(noFriends); - } - - // Pending requests - if (data.pending && data.pending.length) { - const pLabel = document.createElement('div'); - pLabel.className = 'mc-label'; - pLabel.style.color = '#B86E00'; - pLabel.textContent = '📨 Запросы в друзья:'; - body.appendChild(pLabel); - for (const p of data.pending) { - const pItem = document.createElement('div'); - pItem.className = 'mc-friend-row'; - const span = document.createElement('span'); - span.textContent = p.from; - pItem.appendChild(span); - const btns = document.createElement('div'); - btns.style.display = 'flex'; - btns.style.gap = '4px'; - const acceptBtn = document.createElement('button'); - acceptBtn.className = 'mc-friend-btn mc-friend-btn-green'; - acceptBtn.textContent = '✓'; - acceptBtn.onclick = async () => { - const r = await fetch(SERVER_URL + '/api/friends/accept', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,request_id:p.id})}); - const d = await r.json(); - if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.onclick(); } else { showToast('❌ Ошибка'); } - }; - const declineBtn = document.createElement('button'); - declineBtn.className = 'mc-friend-btn mc-friend-btn-red'; - declineBtn.textContent = '✕'; - declineBtn.onclick = async () => { - await fetch(SERVER_URL + '/api/friends/decline', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,request_id:p.id})}); - showToast('Отклонено'); - btnFriends.onclick(); - }; - btns.appendChild(acceptBtn); - btns.appendChild(declineBtn); - pItem.appendChild(btns); - body.appendChild(pItem); - } - } - - // Add friend - body.appendChild(document.createElement('hr')); - body.className = 'mc-label'; - const addLabel = document.createElement('div'); - addLabel.className = 'mc-label'; - addLabel.textContent = '➕ Добавить друга по нику:'; - body.appendChild(addLabel); - const addRow = document.createElement('div'); - addRow.style.display = 'flex'; - addRow.style.gap = '4px'; - const addInput = document.createElement('input'); - addInput.className = 'mc-input'; - addInput.style.flex = '1'; - addInput.type = 'text'; - addInput.maxLength = 16; - addInput.placeholder = 'Ник друга'; - const addBtn = document.createElement('button'); - addBtn.className = 'mc-btn mc-btn-blue'; - addBtn.style.width = 'auto'; - addBtn.style.marginBottom = '0'; - addBtn.style.padding = '8px 16px'; - addBtn.textContent = '+'; - addBtn.onclick = async () => { - const nick = addInput.value.trim().toLowerCase(); - if (!nick || nick.length < 2) { showToast('❌ Минимум 2 символа'); return; } - const r = await fetch(SERVER_URL + '/api/friends/request', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,nickname:nick})}); - const d = await r.json(); - if (d.ok) { showToast(d.message === 'Already friends' ? '✅ Уже друзья!' : '📨 Запрос отправлен!'); } - else { showToast('❌ ' + (d.error || 'Ошибка')); } - }; - addRow.appendChild(addInput); - addRow.appendChild(addBtn); - body.appendChild(addRow); - - // Privacy checkboxes - const settings = data.settings || { allow_requests: 1, notify_online: 1 }; - const settingsDiv = document.createElement('div'); - settingsDiv.style.marginTop = '12px'; - const reqLabel = document.createElement('label'); - reqLabel.className = 'mc-checkbox-row'; - const reqCb = document.createElement('input'); - reqCb.className = 'mc-checkbox'; - reqCb.type = 'checkbox'; - reqCb.checked = settings.allow_requests; - reqCb.onchange = async () => { - await fetch(SERVER_URL + '/api/friends/settings', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,allow_requests:reqCb.checked?1:0,notify_online:settings.notify_online})}); - }; - reqLabel.appendChild(reqCb); - reqLabel.appendChild(document.createTextNode(' Запросы в друзья')); - settingsDiv.appendChild(reqLabel); - body.appendChild(settingsDiv); - } catch(e) { showToast('❌ Ошибка загрузки друзей'); console.error(e); } - }; - body.appendChild(btnFriends); - } - - // Player name input - if (!playerName) { - const nameDivider = document.createElement('hr'); - nameDivider.className = 'mc-divider'; - body.appendChild(nameDivider); - const nameLabel = document.createElement('div'); - nameLabel.className = 'mc-label'; - nameLabel.textContent = 'Твоё имя:'; - body.appendChild(nameLabel); - const nameInput = document.createElement('input'); - nameInput.className = 'mc-input'; - nameInput.type = 'text'; - nameInput.placeholder = 'Ваше имя'; - nameInput.value = tgUser ? (tgUser.username || tgUser.first_name || '') : ''; - nameInput.maxLength = 20; - nameInput.addEventListener('input', () => { - if (nameInput.value.trim()) { - playerName = nameInput.value.trim(); - localStorage.setItem('minegrechka_playerName', playerName); - } - }); - body.appendChild(nameInput); - } - - box.appendChild(body); - overlay.appendChild(box); - document.body.appendChild(overlay); - - // Auto-load TG save - if (isTgWebApp && tgUser && !worldParam) { - fetch(SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id) - .then(r => r.json()) - .then(data => { - if (data.ok && data.world_id) { - // could update UI but menu is already shown - } - }) - .catch(() => {}); - } - }); - } - - // ==================== GAME LAUNCH ==================== - // Override the old auto-join logic - let gameLaunched = false; - - async function launchGame() { - if (gameLaunched) return; - gameLaunched = true; - - if (!playerName) { - playerName = tgUser ? (tgUser.username || tgUser.first_name || 'Игрок') : 'Игрок'; - localStorage.setItem('minegrechka_playerName', playerName); - } - - // Validate TG initData if in TG context - if (isTgWebApp && Telegram.WebApp.initData) { - try { - const resp = await fetch(SERVER_URL + '/api/tg/auth', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ initData: Telegram.WebApp.initData }) - }); - const data = await resp.json(); - if (data.ok && data.tg_id) { - console.log('[TG] Authenticated, tg_id:', data.tg_id); - tgUser = tgUser || {}; - tgUser.id = data.tg_id; - } - } catch(e) { console.warn('[TG] Auth failed:', e); } - } - - // Update URL with world code - try { - const newUrl = new URL(window.location.href); - newUrl.searchParams.set('world', worldId); - if (typeof history !== 'undefined' && history.replaceState) { - history.replaceState(null, '', newUrl.toString()); - } - } catch(e) {} - - console.log('[Launch] worldId:', worldId, 'player:', playerName, 'tg:', !!tgUser); - - // The rest of the game init will pick up worldId and playerName - // Signal that menu is done - startMenuResolved = true; - window.dispatchEvent(new Event('startMenuDone')); - } - - // ==================== SOCKET.IO КЛИЕНТ ==================== - let socket = null; - let isMultiplayer = false; // Флаг для мультиплеерного режима - const otherPlayers = new Map(); // socket_id -> {x, y, color} - const serverMobs = new Map(); // id -> mob (server-spawned, client-authoritative physics) - - // Helper to get all mobs (local + server-spawned in MP) - function getAllMobs() { - return isMultiplayer ? mobs.concat(Array.from(serverMobs.values())) : mobs; - } - - // Create a client-side mob object from server spawn data with correct properties matching client constructors - function createMobFromServer(data) { - const kindProps = { - zombie: { w: 34, h: 50, hp: 4, speed: 80 + Math.random() * 40, hostile: true, fuse: 0, shootCooldown: 2 }, - creeper: { w: 34, h: 50, hp: 4, speed: 60 + Math.random() * 30, hostile: true, fuse: 3.2, shootCooldown: 2 }, - skeleton: { w: 34, h: 50, hp: 4, speed: 70 + Math.random() * 30, hostile: true, fuse: 0, shootCooldown: 0 }, - pig: { w: 34, h: 24, hp: 2, speed: 40, hostile: false, fuse: 0, shootCooldown: 2 }, - chicken: { w: 26, h: 22, hp: 1, speed: 55, hostile: false, fuse: 0, shootCooldown: 2 }, - scorpion: { w: 26, h: 26, hp: 3, speed: 90, hostile: true, fuse: 0, shootCooldown: 0, biome:'desert' }, - polar_bear:{ w: 40, h: 34, hp: 8, speed: 50, hostile: false, fuse: 0, shootCooldown: 2, biome:'tundra' }, - slime: { w: 24, h: 24, hp: 2, speed: 30, hostile: true, fuse: 0, shootCooldown: 2, biome:'swamp' }, - eagle: { w: 30, h: 22, hp: 3, speed: 120, hostile: true, fuse: 0, shootCooldown: 0, biome:'mountains' } - }; - const props = kindProps[data.kind] || kindProps['pig']; // fallback - return { - id: data.id, - kind: data.kind, - x: data.x, - y: data.y, - w: props.w, - h: props.h, - hp: data.hp || props.hp, - maxHp: data.maxHp || data.hp || props.hp, - speed: props.speed, - hostile: props.hostile, + // src/core/state.js + var state = { + // Константы (для доступа из модулей) + TILE, + SEA_GY, + BEDROCK_GY, + GEN_MARGIN_X, + GRAV, + GRAV_WATER, + MOVE, + JUMP, + DAY_LEN, + SAVE_KEY, + MOVE_SEND_INTERVAL, + MAX_RAINDROPS, + MAX_CHAT_MESSAGES, + VOICE_SERVER, + // Камера + camX: 0, + camY: 0, + W: 0, + H: 0, + dpr: 1, + // Canvas/DOM (заполняется в main.js) + gameEl: null, + canvas: null, + ctx: null, + lightC: null, + lightCtx: null, + // День/ночь + worldTime: 0, + isNightTime: false, + // Мультиплеер + isMultiplayer: false, + mySocketId: null, + socket: null, + voiceSocket: null, + voicePosT: 0, + // Сервер + SERVER_URL: "", + TELEGRAM_BOT_USERNAME: "", + TELEGRAM_APP_SHORT_NAME: "", + // Игрок + playerName: null, + worldId: null, + // Инвентарь/UI + selected: 0, + showFullInventory: false, + craftOpen: false, + inventoryOpen: false, + chatOpen: false, + minimapOpen: false, + modeIdx: 0, + currentFurnaceKey: null, + // Мир + worldSeed: Math.floor(Math.random() * 1e6), + // Погода + isRaining: false, + rainIntensity: 0, + weatherTimer: 0, + weatherChangeInterval: 60 + Math.random() * 120, + // Мобы/спавн + spawnT: 0, + // Цикл + last: 0, + prevJump: false, + // Сеть — throttle отправки позиции + lastMoveSendTime: 0, + lastSentX: 0, + lastSentY: 0, + // Mining progress + mining: { gx: null, gy: null, progress: 0, hardness: 0 }, + // Trade/Market + myTgId: null, + tradePanel: null, + tradeContent: null, + marketPanel: null, + marketContent: null, + // DOM-ссылки (заполняются в main.js) + hpEl: null, + foodEl: null, + sxEl: null, + syEl: null, + todEl: null, + worldIdEl: null, + playerCountEl: null, + deathEl: null, + hotbarEl: null, + craftPanel: null, + recipesEl: null, + inventoryPanel: null, + inventoryGrid: null, + multiplayerStatus: null, + // Игрок + player: { + x: 6 * TILE, + y: 0 * TILE, + w: 34, + h: 34, vx: 0, vy: 0, grounded: false, inWater: false, - aiT: 0, - dir: data.dir || 1, - dead: false, - fuse: props.fuse, - shootCooldown: props.shootCooldown - }; + headInWater: false, + hp: 100, + hunger: 100, + o2: 100, + invuln: 0, + fallStartY: 0, + lastStepTime: 0, + sleeping: false, + inBoat: false, + armor: 0, + equippedArmor: null + }, + // Точка спавна + spawnPoint: { x: 6 * TILE, y: 0 * TILE }, + // Инвентарь + inv: { + dirt: 6, + stone: 0, + sand: 0, + gravel: 0, + clay: 0, + wood: 0, + planks: 0, + ladder: 0, + leaves: 0, + coal: 0, + copper_ore: 0, + iron_ore: 0, + gold_ore: 0, + diamond_ore: 0, + brick: 0, + glass: 0, + tnt: 1, + campfire: 0, + torch: 0, + meat: 0, + cooked: 0, + arrow: 0, + wood_pickaxe: 0, + stone_pickaxe: 0, + iron_pickaxe: 0, + wood_sword: 0, + stone_sword: 0, + iron_sword: 0, + wood_axe: 0, + stone_axe: 0, + iron_axe: 0, + wood_shovel: 0, + stone_shovel: 0, + iron_shovel: 0, + iron_armor: 0, + bow: 0, + furnace: 0, + bed: 0, + boat: 0, + iron_ingot: 0, + gold_ingot: 0, + copper_ingot: 0, + cobblestone: 0 + }, + // Лодка + boat: { + x: 0, + y: 0, + w: 34, + h: 34, + vx: 0, + vy: 0, + active: false, + inWater: false + }, + // Ввод + inp: { up: false, down: false, left: false, right: false, jump: false, mine: false, build: false, bow: false, l: false, r: false, j: false, s: false }, + // Мышь + mouse: { x: 0, y: 0 }, + // Другие игроки (MP) + otherPlayers: /* @__PURE__ */ new Map(), + // Серверные мобы (MP) + serverMobs: /* @__PURE__ */ new Map(), + // Мобы + mobs: [], + // Снаряды + projectiles: [], + // Отслеживание изменений мира + placedBlocks: [], + removedBlocks: [], + // Серверные изменения + serverOverrides: /* @__PURE__ */ new Map(), + // Чат + chatMessages: [], + // Погода — капли + raindrops: [], + // Облака + clouds: Array.from({ length: 10 }, () => ({ + x: Math.random() * 2e3, + y: -200 - Math.random() * 260, + w: 80 + Math.random() * 120, + s: 12 + Math.random() * 20 + })), + // Частицы + parts: [], + // Активный TNT + activeTNT: /* @__PURE__ */ new Set(), + // Прочность инструментов + toolDurability: /* @__PURE__ */ new Map(), + // Последние выбранные предметы + recentItems: [], + // Активные печи + activeFurnaces: /* @__PURE__ */ new Map(), + // Сгенерированные колонны + generated: /* @__PURE__ */ new Set(), + // Изображение героя + heroImg: null, + // Сетка мира + grid: /* @__PURE__ */ new Map(), + blocks: [], + // In-memory save fallback + inMemorySave: null + }; + + // src/core/canvas.js + function initCanvas() { + const gameEl = document.getElementById("game"); + const canvas = document.getElementById("c"); + const ctx = canvas.getContext("2d"); + const lightC = document.createElement("canvas"); + const lightCtx = lightC.getContext("2d"); + const dpr = Math.max(1, window.devicePixelRatio || 1); + function resize() { + state.W = gameEl.clientWidth; + state.H = gameEl.clientHeight; + canvas.width = state.W * dpr; + canvas.height = state.H * dpr; + lightC.width = state.W * dpr; + lightC.height = state.H * dpr; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + } + window.addEventListener("resize", resize); + resize(); + state.gameEl = gameEl; + state.canvas = canvas; + state.ctx = ctx; + state.lightC = lightC; + state.lightCtx = lightCtx; + state.dpr = dpr; } - let mySocketId = null; - - // Throttle для отправки позиции (10-20 раз в секунду) - let lastMoveSendTime = 0; - const MOVE_SEND_INTERVAL = 0.05; // 50ms = 20 раз в секунду - let lastSentX = 0, lastSentY = 0; - - function initSocket() { - try { - socket = io(SERVER_URL, { - path: '/socket.io/', - transports: ['websocket', 'polling'] - }); - - socket.on('connect', () => { - console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${worldId}`); - mySocketId = socket.id; - isMultiplayer = true; - - // Присоединяемся к миру - socket.emit('join_world', { world_id: worldId, player_name: playerName, tg_id: (tgUser && tgUser.id) ? tgUser.id : null }); - - // TG: отправляем tg_auth + онлайн статус для уведомления друзей - if (tgUser && tgUser.id) { - socket.emit('tg_auth', { tg_id: tgUser.id }); - try { - fetch(SERVER_URL + '/api/tg/online', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,world_id:worldId,player_name:playerName||'Игрок'})}); - } catch(e) { console.warn('[TG] online notify error:', e); } - } - - // Показываем в UI - worldIdEl.textContent = worldId; - // XP/Level display - const lvXpNext = xpForLevel(player.level + 1); - const lvXpCur = xpForLevel(player.level); - const xpInLevel = player.xp - lvXpCur; - const xpNeeded = lvXpNext - lvXpCur; - document.getElementById('xplevel').textContent = player.level; - document.getElementById('xpbar').textContent = xpInLevel + '/' + xpNeeded; - multiplayerStatus.style.display = 'block'; - }); - - socket.on('connect_error', (error) => { - console.error('Socket connection error:', error); - isMultiplayer = false; - }); - - socket.on('disconnect', () => { - console.log('Disconnected from server'); - isMultiplayer = false; - otherPlayers.clear(); - multiplayerStatus.style.display = 'none'; - // TG: убираем онлайн статус - if (tgUser && tgUser.id) { - try { fetch(SERVER_URL + '/api/tg/offline', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id})}); } catch(e) {} - } - }); - - // Обработка world_state - socket.on('world_state', (data) => { - console.log('Received world_state:', data); - - // Устанавливаем seed и перегенерируем мир если он изменился - if (data.seed !== undefined && data.seed !== worldSeed) { - const oldSeed = worldSeed; - worldSeed = data.seed; - console.log('World seed set from', oldSeed, 'to', worldSeed); - // Full regeneration with correct seed - grid.clear(); - blocks.length = 0; - placedBlocks = []; - removedBlocks = []; - generated.clear(); - for (let gx = spawnPoint.gx - 25; gx <= spawnPoint.gx + 25; gx++) { - genColumn(gx); - decorations(gx); - } - console.log('World regenerated with seed:', worldSeed); + + // src/config.js + var urlParams = new URLSearchParams(window.location.search); + var SERVER_URL = urlParams.get("server") || "https://apigrech.mkn8n.ru"; + if (location.protocol === "https:" && SERVER_URL.startsWith("http://")) { + console.warn("\u26A0\uFE0F Mixed content warning: page is HTTPS but server URL is HTTP"); + alert("\u26A0\uFE0F \u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435: \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u0430 \u043F\u043E HTTPS, \u043D\u043E \u0441\u0435\u0440\u0432\u0435\u0440 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442 HTTP. \u042D\u0442\u043E \u043C\u043E\u0436\u0435\u0442 \u0432\u044B\u0437\u0432\u0430\u0442\u044C \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u044B."); + } + + // src/data/blocks.js + var BLOCKS = { + air: { n: "\u0412\u043E\u0437\u0434\u0443\u0445", solid: false, hardness: 0 }, + grass: { n: "\u0422\u0440\u0430\u0432\u0430", c: "#7cfc00", solid: true, hardness: 3, toolType: "shovel" }, + dirt: { n: "\u0413\u0440\u044F\u0437\u044C", c: "#8b4513", solid: true, hardness: 3, toolType: "shovel" }, + stone: { n: "\u041A\u0430\u043C\u0435\u043D\u044C", c: "#7f8c8d", solid: true, hardness: 6, toolType: "pickaxe" }, + sand: { n: "\u041F\u0435\u0441\u043E\u043A", c: "#f4d06f", solid: true, hardness: 2, toolType: "shovel" }, + gravel: { n: "\u0413\u0440\u0430\u0432\u0438\u0439", c: "#95a5a6", solid: true, hardness: 3, toolType: "shovel" }, + clay: { n: "\u0413\u043B\u0438\u043D\u0430", c: "#74b9ff", solid: true, hardness: 3, toolType: "shovel" }, + wood: { n: "\u0414\u0435\u0440\u0435\u0432\u043E", c: "#d35400", solid: true, hardness: 4, toolType: "axe" }, + planks: { n: "\u0414\u043E\u0441\u043A\u0438", c: "#e67e22", solid: true, hardness: 3, toolType: "axe" }, + ladder: { n: "\u041B\u0435\u0441\u0442\u043D\u0438\u0446\u0430", c: "#d35400", solid: false, climbable: true, hardness: 1, toolType: null }, + leaves: { n: "\u041B\u0438\u0441\u0442\u0432\u0430", c: "#2ecc71", solid: true, hardness: 1, toolType: "axe", minTool: null }, + glass: { n: "\u0421\u0442\u0435\u043A\u043B\u043E", c: "rgba(200,240,255,0.25)", solid: true, alpha: 0.55, hardness: 1, toolType: null }, + water: { n: "\u0412\u043E\u0434\u0430", c: "rgba(52,152,219,0.55)", solid: false, fluid: true, hardness: 0 }, + coal: { n: "\u0423\u0433\u043E\u043B\u044C", c: "#2c3e50", solid: true, hardness: 8, toolType: "pickaxe", minTool: "wood" }, + copper_ore: { n: "\u041C\u0435\u0434\u044C", c: "#e17055", solid: true, hardness: 10, toolType: "pickaxe", minTool: "wood" }, + iron_ore: { n: "\u0416\u0435\u043B\u0435\u0437\u043E", c: "#dcdde1", solid: true, hardness: 12, toolType: "pickaxe", minTool: "stone" }, + iron_armor: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u0430\u044F \u0431\u0440\u043E\u043D\u044F", c: "#95a5a6", solid: false, armor: 0.5, hardness: 0 }, + gold_ore: { n: "\u0417\u043E\u043B\u043E\u0442\u043E", c: "#f1c40f", solid: true, hardness: 15, toolType: "pickaxe", minTool: "stone" }, + diamond_ore: { n: "\u0410\u043B\u043C\u0430\u0437", c: "#00a8ff", solid: true, hardness: 20, toolType: "pickaxe", minTool: "iron" }, + brick: { n: "\u041A\u0438\u0440\u043F\u0438\u0447", c: "#c0392b", solid: true, hardness: 8, toolType: "pickaxe" }, + tnt: { n: "TNT", c: "#e74c3c", solid: true, explosive: true, hardness: 1, toolType: null }, + campfire: { n: "\u041A\u043E\u0441\u0442\u0451\u0440", c: "#e67e22", solid: true, lightRadius: 190, hardness: 2, toolType: null }, + torch: { n: "\u0424\u0430\u043A\u0435\u043B", c: "#f9ca24", solid: true, lightRadius: 140, hardness: 1, toolType: null }, + bedrock: { n: "\u0411\u0435\u0434\u0440\u043E\u043A", c: "#2d3436", solid: true, unbreakable: true, hardness: Infinity }, + flower: { n: "\u0426\u0432\u0435\u0442\u043E\u043A", c: "#ff4757", solid: false, decor: true, hardness: 0 }, + bed: { n: "\u041A\u0440\u043E\u0432\u0430\u0442\u044C", c: "#e91e63", solid: true, bed: true, hardness: 2, toolType: "axe" }, + boat: { n: "\u041B\u043E\u0434\u043A\u0430", c: "#8B4513", solid: false, hardness: 0 }, + furnace: { n: "\u041F\u0435\u0447\u044C", c: "#696969", solid: true, smelting: true, hardness: 8, toolType: "pickaxe", minTool: "wood" }, + cobblestone: { n: "\u0411\u0443\u043B\u044B\u0436\u043D\u0438\u043A", c: "#7f8c8d", solid: true, hardness: 6, toolType: "pickaxe" } + }; + + // src/world/world-storage.js + var grid = state.grid; + var blocks = state.blocks; + function k(gx, gy) { + return gx + "," + gy; + } + function getBlock(gx, gy) { + return grid.get(k(gx, gy)); + } + function isSolid(gx, gy) { + const b = getBlock(gx, gy); + if (!b || b.dead) return false; + const def = BLOCKS[b.t]; + return !!def.solid && !def.fluid && !def.decor; + } + function setBlock(gx, gy, t, isPlayerPlaced = false) { + const key = k(gx, gy); + if (grid.has(key)) return false; + const b = { gx, gy, t, dead: false, active: false, fuse: 0 }; + grid.set(key, b); + blocks.push(b); + if (isPlayerPlaced) { + state.placedBlocks.push({ gx, gy, t }); + } + return true; + } + function removeBlock(gx, gy) { + const key = k(gx, gy); + const b = grid.get(key); + if (!b) return null; + if (BLOCKS[b.t].unbreakable) return null; + grid.delete(key); + b.dead = true; + const wasPlayerPlaced = state.placedBlocks.some((pb) => pb.gx === gx && pb.gy === gy); + if (wasPlayerPlaced) { + state.placedBlocks = state.placedBlocks.filter((pb) => !(pb.gx === gx && pb.gy === gy)); + } else { + state.removedBlocks.push({ gx, gy }); + } + return b; + } + + // src/world/generation.js + var generated = state.generated; + function surfaceGyAt(gx) { + const n1 = Math.sin(gx * 0.025 + state.worldSeed * 1e-3) * 8; + const n2 = Math.sin(gx * 0.012 + state.worldSeed * 2e-3) * 12; + const n3 = Math.sin(gx * 6e-3 + state.worldSeed * 3e-3) * 6; + const n4 = Math.sin(gx * 0.045 + state.worldSeed * 4e-3) * 4; + const n5 = Math.cos(gx * 0.018 + state.worldSeed * 5e-3) * 5; + const h = Math.floor(SEA_GY - 8 + n1 + n2 + n3 + n4 + n5); + return h; + } + function genColumn(gx) { + if (generated.has(gx)) return; + generated.add(gx); + const sgy = surfaceGyAt(gx); + if (sgy > SEA_GY) { + for (let gy = SEA_GY; gy < sgy; gy++) { + setBlock(gx, gy, "water"); + } + setBlock(gx, sgy, "sand"); + } else { + if (sgy < SEA_GY - 10) setBlock(gx, sgy, "stone"); + else setBlock(gx, sgy, "grass"); + } + for (let gy = sgy + 1; gy <= BEDROCK_GY; gy++) { + if (gy === BEDROCK_GY) { + setBlock(gx, gy, "bedrock"); + continue; + } + let t = "stone"; + if (gy <= sgy + 3) t = "dirt"; + if (sgy > SEA_GY && gy === sgy + 1 && seededRandom(gx, gy) < 0.25) t = "clay"; + if (gy > sgy + 6 && seededRandom(gx, gy) < 0.07) t = "gravel"; + const depth = gy - sgy; + const r = seededRandom(gx, gy); + if (t === "stone") { + if (r < 0.06) t = "coal"; + else if (r < 0.1) t = "copper_ore"; + else if (r < 0.13) t = "iron_ore"; + else if (depth > 40 && r < 0.145) t = "gold_ore"; + else if (depth > 70 && r < 0.152) t = "diamond_ore"; + } + setBlock(gx, gy, t); + } + const top = getBlock(gx, sgy); + if (top && top.t === "grass") { + if (seededRandom(gx, sgy - 1) < 0.1) { + setBlock(gx, sgy - 1, "flower"); + } + if (seededRandom(gx, sgy - 2) < 0.12) { + setBlock(gx, sgy - 1, "wood"); + setBlock(gx, sgy - 2, "wood"); + setBlock(gx, sgy - 3, "leaves"); + setBlock(gx - 1, sgy - 3, "leaves"); + setBlock(gx + 1, sgy - 3, "leaves"); + } + } + const colPrefix = gx + ","; + for (const [key, ov] of state.serverOverrides) { + if (!key.startsWith(colPrefix)) continue; + if (ov.op === "remove") { + const b = grid.get(key); + if (b) { + grid.delete(key); + b.dead = true; } - - // Применяем блоки — сохраняем в serverOverrides для применения после genColumn - if (data.blocks && Array.isArray(data.blocks)) { - for (const block of data.blocks) { - const key = k(block.gx, block.gy); - serverOverrides.set(key, { op: block.op, t: block.t }); - // Также пробуем применить сразу (если колонна уже сгенерирована) - if (block.op === 'set') { - setBlock(block.gx, block.gy, block.t, false); - } else if (block.op === 'remove') { - removeBlock(block.gx, block.gy); - } - } - } - - // Устанавливаем время - if (data.time !== undefined) { - worldTime = data.time; - isNightTime = worldTime > 0.5; - } - - // Всегда считаем spawnPoint на клиенте по той же формуле что и surfaceGyAt - // Это гарантирует совпадение с terrain generation - { - const startGX = 6; - // Генерируем колонну и соседние для безопасного спавна - for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx); - const surfaceY = surfaceGyAt(startGX); - // Ищем ближайшую небудущую позицию сверху вниз от поверхности - let safeGY = surfaceY - 1; - // Проверяем что над поверхностью воздух (не в воде) - const aboveBlock = getBlock(startGX, surfaceY - 1); - if (aboveBlock && aboveBlock.t === 'water') { - // Если в воде — ищем поверхность выше уровня моря - for (let gy = SEA_GY - 1; gy >= 0; gy--) { - const b = getBlock(startGX, gy); - if (!b || b.dead || b.t === 'air' || b.t === 'water') continue; - safeGY = gy - 1; - break; - } - } - spawnPoint.x = startGX * TILE; - spawnPoint.y = safeGY * TILE; - console.log('Client-side spawn point:', spawnPoint, 'surfaceY:', surfaceY, 'safeGY:', safeGY); + } else if (ov.op === "set") { + if (!grid.has(key)) { + const gy = parseInt(key.split(",")[1]); + const nb = { gx, gy, t: ov.t, dead: false, active: false, fuse: 0 }; + grid.set(key, nb); + blocks.push(nb); } - - // Устанавливаем игрока в точку спавна - player.x = spawnPoint.x; - player.y = spawnPoint.y; - player.vx = 0; - player.vy = 0; - player.fallStartY = player.y; - console.log('Player moved to spawn point:', player.x, player.y); - - // Устанавливаем HP на 100% при каждом подключении к миру - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.invuln = 0; - console.log('[MULTIPLAYER CONNECT] Player HP set to 100% on connect'); - - // Обновляем список игроков - if (data.players && Array.isArray(data.players)) { - otherPlayers.clear(); - for (const p of data.players) { - if (p.socket_id !== mySocketId) { - otherPlayers.set(p.socket_id, { - x: p.x, - y: p.y, - color: getRandomPlayerColor(p.socket_id), - name: p.player_name || 'Игрок' - }); - } - } - // Обновляем счётчик игроков - playerCountEl.textContent = data.players.length; - } - // Server mobs — client-authoritative: create with full client-side properties - if (data.mobs && Array.isArray(data.mobs)) { - serverMobs.clear(); - for (const m of data.mobs) { - const sm = createMobFromServer(m); - // Пересчитываем Y по клиентской surfaceGyAt - const spawnGX = Math.floor(sm.x / TILE); - genColumn(spawnGX - 1); genColumn(spawnGX); genColumn(spawnGX + 1); - const clientSurfaceY = surfaceGyAt(spawnGX); - sm.y = (clientSurfaceY - 1) * TILE; - sm.vy = 0; - serverMobs.set(m.id, sm); - } - } - }); - - // Игрок присоединился - socket.on('player_joined', (data) => { - console.log('Player joined:', data.socket_id); - if (data.socket_id !== mySocketId) { - // Генерируем безопасную позицию для нового игрока - const spawnGX = 6; - genColumn(spawnGX); - const surfaceY = surfaceGyAt(spawnGX); - const safeSpawnX = spawnGX * TILE; - const safeSpawnY = (surfaceY - 1) * TILE; - - otherPlayers.set(data.socket_id, { - x: safeSpawnX, - y: safeSpawnY, - tx: safeSpawnX, - ty: safeSpawnY, - color: getRandomPlayerColor(data.socket_id), - name: data.player_name || 'Игрок' - }); - addChatMessage('Система', `Игрок присоединился`); - // Обновляем видимость кнопки сохранения - updateSaveButtonVisibility(); - } - }); - - // Игрок переместился - socket.on('player_moved', (data) => { - if (data.socket_id !== mySocketId && otherPlayers.has(data.socket_id)) { - const p = otherPlayers.get(data.socket_id); - // Lerp: запоминаем целевую позицию, рисуем в рендере с интерполяцией - p.tx = data.x; - p.ty = data.y; - // Обновляем имя, если оно пришло - if (data.player_name) { - p.name = data.player_name; - } - } - }); - - // Игрок покинул - socket.on('player_left', (data) => { - console.log('Player left:', data.socket_id); - otherPlayers.delete(data.socket_id); - addChatMessage('Система', `Игрок покинул игру`); - // Обновляем видимость кнопки сохранения - updateSaveButtonVisibility(); - }); - - // === MOB SYNC (multiplayer) === - - socket.on('mob_spawned', (data) => { - const sm = createMobFromServer(data); - // Пересчитываем Y по клиентской surfaceGyAt — сервер может ошибаться - const spawnGX = Math.floor(sm.x / TILE); - // Убеждаемся что чанк сгенерирован (иначе getBlock=null → моб падает сквозь землю) - genColumn(spawnGX - 1); genColumn(spawnGX); genColumn(spawnGX + 1); - const clientSurfaceY = surfaceGyAt(spawnGX); - sm.y = (clientSurfaceY - 1) * TILE; // 1 блок выше поверхности (под ногами) - sm.vy = 0; - sm.grounded = false; // resolveY в mobAI поставит grounded=true - serverMobs.set(data.id, sm); - }); - - socket.on('mob_positions', (arr) => { - for (const u of arr) { - let sm = serverMobs.get(u.id); - if (!sm) { - sm = createMobFromServer({ id: u.id, kind: u.kind || 'zombie', x: u.x, y: u.y, - hp: u.hp || 1, maxHp: u.hp || 1, dir: u.dir || 1 }); - serverMobs.set(u.id, sm); - } - sm.x += (u.x - sm.x) * 0.35; - sm.y += (u.y - sm.y) * 0.35; - sm.dir = u.dir != null ? u.dir : sm.dir; - sm.hp = u.hp != null ? u.hp : sm.hp; - sm.fuse = u.fuse != null ? u.fuse : sm.fuse; - sm.grounded = !!u.grounded; - } - }); - - socket.on('mob_despawned', (data) => { serverMobs.delete(data.id); }); - - socket.on('mob_died', (data) => { - const sm = serverMobs.get(data.id); - if (sm && data.killer === mySocketId) { - // Give loot to the killer - if (sm.kind === 'chicken') playSound('hurt_chicken'); - spawnDrops(sm.x, sm.y, sm.kind); - rebuildHotbar(); - } - serverMobs.delete(data.id); - }); - - socket.on('mob_hurt_ack', (data) => { - const sm = serverMobs.get(data.id); - if (sm) sm.hp = data.hp; - }); - - socket.on('mob_explode', (data) => { - explodeAt(data.gx, data.gy); - serverMobs.delete(data.id); - }); - - socket.on('mob_shoot', (data) => { - projectiles.push({ - x: data.x, y: data.y, vx: data.vx, vy: data.vy, - dmg: data.dmg, owner: 'mob', life: data.life - }); - }); - - // Блок изменён - socket.on('block_changed', (data) => { - const key = k(data.gx, data.gy); - serverOverrides.set(key, { op: data.op, t: data.t }); - if (data.op === 'set') { - setBlock(data.gx, data.gy, data.t, false); - } else if (data.op === 'remove') { - removeBlock(data.gx, data.gy); - } - }); - - // Сообщение в чат - socket.on('chat_message', (data) => { - const senderName = data.socket_id === mySocketId ? 'Вы' : `Игрок ${data.socket_id.substring(0, 6)}`; - addChatMessage(senderName, data.message); - }); - - // Обновление времени - socket.on('time_update', (data) => { - if (data.time !== undefined) { - worldTime = data.time; - isNightTime = worldTime > 0.5; - } - }); - - } catch (e) { - console.error('Error initializing socket:', e); - isMultiplayer = false; - } - } - - // Генерация случайного цвета для игрока на основе socket_id - function getRandomPlayerColor(socketId) { - const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e91e63', '#00bcd4']; - let hash = 0; - for (let i = 0; i < socketId.length; i++) { - hash = ((hash << 5) - hash) + socketId.charCodeAt(i); - hash = hash & hash; - } - return colors[Math.abs(hash) % colors.length]; - } - - // Отправка позиции игрока (с throttle) - function sendPlayerPosition() { - if (!isMultiplayer || !socket || !socket.connected) return; - - const now = performance.now() / 1000; - if (now - lastMoveSendTime < MOVE_SEND_INTERVAL) return; - - // Отправляем только если позиция изменилась - const dx = Math.abs(player.x - lastSentX); - const dy = Math.abs(player.y - lastSentY); - if (dx < 1 && dy < 1) return; - - lastMoveSendTime = now; - lastSentX = player.x; - lastSentY = player.y; - - socket.emit('player_move', { x: player.x, y: player.y, player_name: playerName }); - } - - // Отправка изменения блока - function sendBlockChange(gx, gy, t, op) { - if (!isMultiplayer || !socket || !socket.connected) return; - - socket.emit('block_change', { gx, gy, t, op }); - } - - // ==================== ЧАТ ==================== - const chatMessages = []; - const MAX_CHAT_MESSAGES = 20; - - function addChatMessage(sender, message) { - const time = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); - chatMessages.push({ sender, message, time }); - if (chatMessages.length > MAX_CHAT_MESSAGES) { - chatMessages.shift(); - } - renderChatMessages(); - } - - function renderChatMessages() { - const chatMessagesEl = document.getElementById('chatMessages'); - if (!chatMessagesEl) return; - - chatMessagesEl.innerHTML = chatMessages.map(m => - `
${m.time} ${m.sender}: ${m.message}
` - ).join(''); - - // Прокручиваем вниз - chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight; - } - - function sendChatMessage(message) { - if (!message || message.trim() === '') return; - - if (isMultiplayer && socket && socket.connected) { - socket.emit('chat_message', { message: message.trim() }); - } else { - addChatMessage('Вы', message.trim()); - } - } - - // ==================== ПОДЕЛИТЬСЯ МИРОМ ==================== - function shareWorld() { - const shareUrl = new URL(window.location.href); - shareUrl.searchParams.set('world', worldId); - const shareUrlString = shareUrl.toString(); - - // Копируем в буфер обмена - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(shareUrlString).then(() => { - alert('Ссылка скопирована!'); - }).catch(() => { - alert('Ссылка на мир:\n' + shareUrlString); - }); - } else { - alert('Ссылка на мир:\n' + shareUrlString); - } - } - - // ==================== ИНИЦИАЛИЗАЦИЯ UI ==================== - let chatOpen = false; - - document.getElementById('chatToggle').onclick = () => { - playSound('click'); - chatOpen = !chatOpen; - document.getElementById('chatPanel').style.display = chatOpen ? 'block' : 'none'; - if (chatOpen) { - document.getElementById('chatInput').focus(); - } - }; - - document.getElementById('chatClose').onclick = () => { - playSound('click'); - chatOpen = false; - document.getElementById('chatPanel').style.display = 'none'; - }; - - document.getElementById('chatSend').onclick = () => { - const input = document.getElementById('chatInput'); - sendChatMessage(input.value); - input.value = ''; - }; - - document.getElementById('chatInput').addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - sendChatMessage(e.target.value); - e.target.value = ''; - } - }); - - // ==================== ИНИЦИАЛИЗАЦИЯ СОКЕТА ==================== - // Show start menu first, then init socket - showStartMenu().then((wid) => { - worldId = wid; - // Send tg_id if TG user - initSocket(); - launchGame(); - }); - - // ==================== ЗВУКОВОЙ ДВИЖОК ==================== - const sounds = {}; + } + } + } + function regenerateVisibleChunks() { + const gx0 = Math.floor(state.camX / TILE); + for (let gx = gx0 - GEN_MARGIN_X; gx <= gx0 + GEN_MARGIN_X; gx++) { + generated.delete(gx); + genColumn(gx); + } + } + function ensureGenAroundCamera() { + const gx0 = Math.floor(state.camX / TILE); + for (let gx = gx0 - GEN_MARGIN_X; gx <= gx0 + GEN_MARGIN_X; gx++) { + genColumn(gx); + } + } + function seededRandom(gx, gy) { + const n = Math.sin(gx * 12.9898 + gy * 78.233 + state.worldSeed * 0.1) * 43758.5453; + return n - Math.floor(n); + } + + // src/audio/sound-engine.js + var sounds = {}; function loadSound(id, src) { const audio = new Audio(); audio.src = src; audio.volume = 0.3; sounds[id] = audio; } - - // Загрузка звуков - loadSound('splash', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3'); - loadSound('sand1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3'); - loadSound('snow1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3'); - loadSound('stone1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3'); - loadSound('wood1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3'); - loadSound('cloth1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3'); - loadSound('fire', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3'); - loadSound('hit1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3'); - loadSound('attack', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3'); - loadSound('hurt_chicken', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3'); - loadSound('stone_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3'); - loadSound('wood_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3'); - loadSound('click', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3'); - loadSound('explode1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3'); - loadSound('glass1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3'); - loadSound('eat1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3'); - loadSound('step', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3'); - + loadSound("splash", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3"); + loadSound("sand1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3"); + loadSound("snow1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3"); + loadSound("stone1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3"); + loadSound("wood1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3"); + loadSound("cloth1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3"); + loadSound("fire", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3"); + loadSound("hit1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3"); + loadSound("attack", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3"); + loadSound("hurt_chicken", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3"); + loadSound("stone_build", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3"); + loadSound("wood_build", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3"); + loadSound("click", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3"); + loadSound("explode1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3"); + loadSound("glass1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3"); + loadSound("eat1", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3"); + loadSound("step", "https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3"); function playSound(id) { - if(sounds[id]) { + if (sounds[id]) { sounds[id].currentTime = 0; - sounds[id].play().catch(e => console.error('Sound error:', e)); + sounds[id].play().catch((e) => console.error("Sound error:", e)); } } - - // Играем звук при прыжке - const gameEl = document.getElementById('game'); - const canvas = document.getElementById('c'); - const ctx = canvas.getContext('2d'); - - // offscreen light map (не вставляем в DOM) - const lightC = document.createElement('canvas'); - const lightCtx = lightC.getContext('2d'); - - const dpr = Math.max(1, window.devicePixelRatio || 1); - let W=0, H=0; - - const TILE = 40; - - // Мир - const SEA_GY = 14; // уровень воды (gy) - уменьшил для меньших водоёмов - const BEDROCK_GY = 140; // глубина бедрока (gy), чем больше - тем глубже - const GEN_MARGIN_X = 40; // запас генерации по X (в тайлах) — увеличен для плавной прогрузки - - const heroImg = new Image(); - heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png'; - - // Состояние инвентаря - let showFullInventory = false; - let recentItems = []; // Последние 5 выбранных предметов - - const BLOCKS = { - air: { n:'Воздух', solid:false }, - grass: { n:'Трава', c:'#7cfc00', solid:true }, - dirt: { n:'Грязь', c:'#8b4513', solid:true }, - stone: { n:'Камень', c:'#7f8c8d', solid:true }, - sand: { n:'Песок', c:'#f4d06f', solid:true }, - gravel: { n:'Гравий', c:'#95a5a6', solid:true }, - clay: { n:'Глина', c:'#74b9ff', solid:true }, - wood: { n:'Дерево', c:'#d35400', solid:true }, - planks: { n:'Доски', c:'#e67e22', solid:true }, - ladder: { n:'Лестница',c:'#d35400', solid:false, climbable:true }, - leaves: { n:'Листва', c:'#2ecc71', solid:true }, - glass: { n:'Стекло', c:'rgba(200,240,255,0.25)', solid:true, alpha:0.55 }, - water: { n:'Вода', c:'rgba(52,152,219,0.55)', solid:false, fluid:true }, - coal: { n:'Уголь', c:'#2c3e50', solid:true }, - copper_ore:{ n:'Медь', c:'#e17055', solid:true }, - iron_ore: { n:'Железо', c:'#dcdde1', solid:true }, - iron_armor:{ n:'Железная броня', c:'#95a5a6', solid:false, armor:0.5 }, - gold_ore: { n:'Золото', c:'#f1c40f', solid:true }, - diamond_ore:{n:'Алмаз', c:'#00a8ff', solid:true }, - brick: { n:'Кирпич', c:'#c0392b', solid:true }, - tnt: { n:'TNT', c:'#e74c3c', solid:true, explosive:true }, - campfire: { n:'Костёр', c:'#e67e22', solid:true, lightRadius:280 }, - torch: { n:'Факел', c:'#f9ca24', solid:true, lightRadius:220 }, - bedrock: { n:'Бедрок', c:'#2d3436', solid:true, unbreakable:true }, - flower: { n:'Цветок', c:'#ff4757', solid:false, decor:true }, - bed: { n:'Кровать', c:'#e91e63', solid:true, bed:true }, - boat: { n:'Лодка', c:'#8B4513', solid:false }, - furnace: { n:'Печь', c:'#696969', solid:true, smelting:true }, - // === BIOME BLOCKS === - snow: { n:'Снег', c:'#ecf0f1', solid:true }, - ice: { n:'Лёд', c:'#74b9ff', solid:true, slip:true }, - cactus: { n:'Кактус', c:'#27ae60', solid:true, hurt:true }, - mushroom: { n:'Гриб', c:'#e74c3c', solid:false, decor:true }, - moss: { n:'Мох', c:'#0a6640', solid:true }, - swamp_water:{ n:'Болотная вода', c:'rgba(100,120,40,0.6)', solid:false, fluid:true, poison:true }, - farmland: { n:'Грядка', c:'#8B6914', solid:true, farmable:true }, - dead_bush: { n:'Сухой куст', c:'#b2bec3', solid:false, decor:true }, - spruce_leaves:{ n:'Ель', c:'#0a6640', solid:true }, - // === CROP STAGES === - wheat_stage0:{ n:'Росток пшеницы', c:'#a8e6a0', solid:false, decor:true, growthStage:0, growthTime:30, growsTo:'wheat_stage1' }, - wheat_stage1:{ n:'Пшеница', c:'#7dcea0', solid:false, decor:true, growthStage:1, growthTime:30, growsTo:'wheat_stage2' }, - wheat_stage2:{ n:'Пшеница', c:'#b8d730', solid:false, decor:true, growthStage:2, growthTime:30, growsTo:'wheat_stage3' }, - wheat_stage3:{ n:'Пшеница', c:'#f1c40f', solid:false, decor:true, harvestable:true, harvestItem:'wheat', harvestQty:2 }, - carrot_stage0:{ n:'Росток моркови', c:'#a8e6a0', solid:false, decor:true, growthStage:0, growthTime:30, growsTo:'carrot_stage1' }, - carrot_stage1:{ n:'Морковь', c:'#f0c27a', solid:false, decor:true, growthStage:1, growthTime:30, growsTo:'carrot_stage2' }, - carrot_stage2:{ n:'Морковь', c:'#e8a040', solid:false, decor:true, growthStage:2, growthTime:30, growsTo:'carrot_stage3' }, - carrot_stage3:{ n:'Морковь', c:'#e67e22', solid:false, decor:true, harvestable:true, harvestItem:'carrot', harvestQty:3 }, - potato_stage0:{ n:'Росток картофеля', c:'#a8e6a0', solid:false, decor:true, growthStage:0, growthTime:30, growsTo:'potato_stage1' }, - potato_stage1:{ n:'Картофель', c:'#c8b888', solid:false, decor:true, growthStage:1, growthTime:30, growsTo:'potato_stage2' }, - potato_stage2:{ n:'Картофель', c:'#bfaa78', solid:false, decor:true, growthStage:2, growthTime:30, growsTo:'potato_stage3' }, - potato_stage3:{ n:'Картофель', c:'#dfe6e9', solid:false, decor:true, harvestable:true, harvestItem:'potato', harvestQty:2 } - }; - - const ITEMS = { - meat: { n:'Сырое мясо', icon:'🥩', food:15 }, - cooked: { n:'Жареное мясо', icon:'🍖', food:45 }, - arrow: { n:'Стрела', icon:'➡️', stack:64 }, - chicken_meat: { n:'Курица', icon:'🍗', food:12, stack:64 }, - feather: { n:'Перо', icon:'🪶', stack:64 }, - bone: { n:'Кость', icon:'🦴', stack:64 }, - gunpowder: { n:'Порох', icon:'💥', stack:64 }, - // === FARMING ITEMS === - wheat: { n:'Пшеница', icon:'🌾', stack:64 }, - bread: { n:'Хлеб', icon:'🍞', food:30 }, - carrot: { n:'Морковь', icon:'🥕', food:8, stack:64 }, - potato: { n:'Картофель', icon:'🥔', stack:64 }, - baked_potato:{ n:'Печёная картошка', icon:'🥔', food:25 }, - mushroom_stew:{ n:'Грибной суп', icon:'🍲', food:40 }, - // === MOB DROP ITEMS === - scorpion_stinger:{ n:'Жало скорпиона', icon:'🔺', stack:64 }, - polar_fur: { n:'Шкура медведя', icon:'🧥', stack:64 }, - slime_ball: { n:'Слизь', icon:'🟢', stack:64 }, - eagle_feather:{ n:'Перо орла', icon:'🪶', stack:64 }, - // === NEW ARMOR & TOOLS === - gold_armor: { n:'Золотая броня', icon:'🛡️', stack:1, armor:0.65 } - }; - - // Seed мира для детерминированной генерации - // Seed: null in MP until server sends it; SP uses random seed - let worldSeed = isMultiplayer ? null : Math.floor(Math.random() * 1000000); - - // Отслеживание изменений мира (для оптимизированного сохранения) - let placedBlocks = []; // [{gx, gy, t}] - блоки, установленные игроком - let removedBlocks = []; // [{gx, gy}] - блоки, удалённые игроком - // Серверные изменения — применяются после genColumn чтобы не перезатирались - const serverOverrides = new Map(); // key "gx,gy" => {op:'set'|'remove', t?:string} - - // Инструменты - const TOOLS = { - wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', durability: 60, miningPower: 1, craft: { wood: 3, planks: 2 }, requiredLevel: 1 }, - stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', durability: 130, miningPower: 2, craft: { wood: 1, stone: 3 }, requiredLevel: 2 }, - iron_pickaxe: { n:'Железная кирка', icon:'⛏️', durability: 250, miningPower: 3, craft: { wood: 1, iron_ore: 2 }, requiredLevel: 3 }, - wood_sword: { n:'Деревянный меч', icon:'⚔️', durability: 40, damage: 5, craft: { wood: 2, planks: 1 }, requiredLevel: 1 }, - stone_sword: { n:'Каменный меч', icon:'⚔️', durability: 100, damage: 8, craft: { wood: 1, stone: 2 }, requiredLevel: 2 }, - iron_sword: { n:'Железный меч', icon:'⚔️', durability: 200, damage: 12, craft: { wood: 1, iron_ore: 1 }, requiredLevel: 3 }, - bow: { n:'Лук', icon:'🏹', durability: 150, craft: { wood: 3, planks: 2 }, requiredLevel: 4 }, - diamond_pickaxe: { n:'Алмазная кирка', icon:'⛏️', durability: 500, miningPower: 5, craft: { wood: 1, diamond_ore: 2 }, requiredLevel: 7 }, - diamond_sword: { n:'Алмазный меч', icon:'⚔️', durability: 400, damage: 18, craft: { wood: 1, diamond_ore: 2 }, requiredLevel: 7 }, - hoe: { n:'Мотыга', icon:'🔨', durability: 80, tillTo: 'farmland', craft: { wood: 2, planks: 1 }, requiredLevel: 1 } + // src/ui/chat.js + var chatMessages = []; + var MAX_CHAT_MESSAGES2 = 20; + function addChatMessage(sender, message) { + const time = (/* @__PURE__ */ new Date()).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" }); + chatMessages.push({ sender, message, time }); + if (chatMessages.length > MAX_CHAT_MESSAGES2) { + chatMessages.shift(); + } + renderChatMessages(); + } + function renderChatMessages() { + const chatMessagesEl = document.getElementById("chatMessages"); + if (!chatMessagesEl) return; + chatMessagesEl.innerHTML = chatMessages.map( + (m) => `
${m.time} ${m.sender}: ${m.message}
` + ).join(""); + chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight; + } + function sendChatMessage(message) { + if (!message || message.trim() === "") return; + if (state.isMultiplayer && state.socket && state.socket.connected) { + state.socket.emit("chat_message", { message: message.trim() }); + } else { + addChatMessage("\u0412\u044B", message.trim()); + } + } + function initChat() { + document.getElementById("chatToggle").onclick = () => { + playSound("click"); + state.chatOpen = !state.chatOpen; + document.getElementById("chatPanel").style.display = state.chatOpen ? "block" : "none"; + if (state.chatOpen) { + document.getElementById("chatInput").focus(); + } + }; + document.getElementById("chatClose").onclick = () => { + playSound("click"); + state.chatOpen = false; + document.getElementById("chatPanel").style.display = "none"; + }; + document.getElementById("chatSend").onclick = () => { + const input = document.getElementById("chatInput"); + sendChatMessage(input.value); + input.value = ""; + }; + document.getElementById("chatInput").addEventListener("keypress", (e) => { + if (e.key === "Enter") { + sendChatMessage(e.target.value); + e.target.value = ""; + } + }); + } + + // src/entities/player.js + function calculateDamage(baseDamage) { + const reduction = state.player.armor; + const actualDamage = baseDamage * (1 - reduction); + console.log("[DAMAGE] Base:", baseDamage, "- Armor:", reduction, "- Actual:", actualDamage.toFixed(1)); + return actualDamage; + } + + // src/data/items.js + var ITEMS = { + meat: { n: "\u0421\u044B\u0440\u043E\u0435 \u043C\u044F\u0441\u043E", icon: "\u{1F969}", food: 15 }, + cooked: { n: "\u0416\u0430\u0440\u0435\u043D\u043E\u0435 \u043C\u044F\u0441\u043E", icon: "\u{1F356}", food: 45 }, + arrow: { n: "\u0421\u0442\u0440\u0435\u043B\u0430", icon: "\u27A1\uFE0F", stack: 64 } }; - - // Текстуры блоков (простые) - const tex = {}; + ITEMS.iron_ingot = { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u044B\u0439 \u0441\u043B\u0438\u0442\u043E\u043A", icon: "\u{1F529}" }; + ITEMS.gold_ingot = { n: "\u0417\u043E\u043B\u043E\u0442\u043E\u0439 \u0441\u043B\u0438\u0442\u043E\u043A", icon: "\u{1FA99}" }; + ITEMS.copper_ingot = { n: "\u041C\u0435\u0434\u043D\u044B\u0439 \u0441\u043B\u0438\u0442\u043E\u043A", icon: "\u{1F7E4}" }; + ITEMS.wood_axe = { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u044B\u0439 \u0442\u043E\u043F\u043E\u0440", icon: "\u{1FA93}" }; + ITEMS.stone_axe = { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u044B\u0439 \u0442\u043E\u043F\u043E\u0440", icon: "\u{1FA93}" }; + ITEMS.iron_axe = { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u044B\u0439 \u0442\u043E\u043F\u043E\u0440", icon: "\u{1FA93}" }; + ITEMS.wood_shovel = { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u0430\u044F \u043B\u043E\u043F\u0430\u0442\u0430", icon: "\u{1F531}" }; + ITEMS.stone_shovel = { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u0430\u044F \u043B\u043E\u043F\u0430\u0442\u0430", icon: "\u{1F531}" }; + ITEMS.iron_shovel = { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u0430\u044F \u043B\u043E\u043F\u0430\u0442\u0430", icon: "\u{1F531}" }; + + // src/data/tools.js + var TOOLS = { + wood_pickaxe: { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u0430\u044F \u043A\u0438\u0440\u043A\u0430", icon: "\u26CF\uFE0F", damage: 1, durability: 60, toolType: "pickaxe", toolTier: "wood", speed: 2 }, + stone_pickaxe: { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u0430\u044F \u043A\u0438\u0440\u043A\u0430", icon: "\u26CF\uFE0F", damage: 2, durability: 132, toolType: "pickaxe", toolTier: "stone", speed: 4 }, + iron_pickaxe: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u0430\u044F \u043A\u0438\u0440\u043A\u0430", icon: "\u26CF\uFE0F", damage: 3, durability: 250, toolType: "pickaxe", toolTier: "iron", speed: 6 }, + wood_sword: { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u044B\u0439 \u043C\u0435\u0447", icon: "\u{1F5E1}\uFE0F", damage: 4, durability: 60 }, + stone_sword: { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u044B\u0439 \u043C\u0435\u0447", icon: "\u{1F5E1}\uFE0F", damage: 5, durability: 132 }, + iron_sword: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u044B\u0439 \u043C\u0435\u0447", icon: "\u{1F5E1}\uFE0F", damage: 7, durability: 250 }, + wood_axe: { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u044B\u0439 \u0442\u043E\u043F\u043E\u0440", icon: "\u{1FA93}", damage: 3, durability: 60, toolType: "axe", toolTier: "wood", speed: 2 }, + stone_axe: { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u044B\u0439 \u0442\u043E\u043F\u043E\u0440", icon: "\u{1FA93}", damage: 4, durability: 132, toolType: "axe", toolTier: "stone", speed: 4 }, + iron_axe: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u044B\u0439 \u0442\u043E\u043F\u043E\u0440", icon: "\u{1FA93}", damage: 5, durability: 250, toolType: "axe", toolTier: "iron", speed: 6 }, + wood_shovel: { n: "\u0414\u0435\u0440\u0435\u0432\u044F\u043D\u043D\u0430\u044F \u043B\u043E\u043F\u0430\u0442\u0430", icon: "\u{1F531}", damage: 1, durability: 60, toolType: "shovel", toolTier: "wood", speed: 2 }, + stone_shovel: { n: "\u041A\u0430\u043C\u0435\u043D\u043D\u0430\u044F \u043B\u043E\u043F\u0430\u0442\u0430", icon: "\u{1F531}", damage: 2, durability: 132, toolType: "shovel", toolTier: "stone", speed: 4 }, + iron_shovel: { n: "\u0416\u0435\u043B\u0435\u0437\u043D\u0430\u044F \u043B\u043E\u043F\u0430\u0442\u0430", icon: "\u{1F531}", damage: 3, durability: 250, toolType: "shovel", toolTier: "iron", speed: 6 }, + bow: { n: "\u041B\u0443\u043A", icon: "\u{1F3F9}", damage: 10, durability: 200 } + }; + function addTool(type) { + const maxDur = TOOLS[type]?.durability || 60; + const key = `${type}_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`; + state.toolDurability.set(key, { type, current: maxDur, max: maxDur }); + } + function useTool(type) { + for (const [tid, dur] of state.toolDurability) { + if (dur.type === type && dur.current > 0) { + dur.current--; + if (dur.current <= 0) { + state.toolDurability.delete(tid); + state.inv[type] = Math.max(0, (state.inv[type] || 0) - 1); + return true; + } + return false; + } + } + return false; + } + function getBestTool(blockType) { + const block = BLOCKS[blockType]; + if (!block || !block.toolType) return null; + const toolType = block.toolType; + for (const tier of ["iron", "stone", "wood"]) { + const toolId = `${tier}_${toolType}`; + if (TOOLS[toolId] && state.inv[toolId] > 0) return toolId; + } + return null; + } + function toolTierLessThan(toolTier, minTier) { + const tiers = { wood: 1, stone: 2, iron: 3 }; + return (tiers[toolTier] || 0) < (tiers[minTier] || 0); + } + function getMiningSpeed(blockType) { + const block = BLOCKS[blockType]; + if (!block) return 1; + const toolId = getBestTool(blockType); + if (toolId) { + const tool = TOOLS[toolId]; + if (tool.toolType === block.toolType) return tool.speed; + } + return 1; + } + + // src/render/textures.js + var tex = {}; + var itemTex = {}; + function drawArrow(ctx, color) { + ctx.fillStyle = "#8B4513"; + ctx.fillRect(8, 14, 16, 3); + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(24, 15); + ctx.lineTo(18, 10); + ctx.lineTo(18, 20); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = "#e74c3c"; + ctx.beginPath(); + ctx.moveTo(8, 15); + ctx.lineTo(4, 10); + ctx.lineTo(10, 14); + ctx.closePath(); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(8, 16); + ctx.lineTo(4, 21); + ctx.lineTo(10, 17); + ctx.closePath(); + ctx.fill(); + } + function drawPickaxe(ctx, headColor, handleColor) { + ctx.fillStyle = handleColor || "#8B4513"; + ctx.save(); + ctx.translate(16, 16); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-2, 0, 4, 18); + ctx.fillStyle = headColor; + ctx.fillRect(-8, -3, 16, 5); + ctx.fillRect(-8, -3, 3, 8); + ctx.fillRect(5, -3, 3, 8); + ctx.restore(); + } + function drawAxe(ctx, headColor, handleColor) { + ctx.fillStyle = handleColor || "#8B4513"; + ctx.save(); + ctx.translate(16, 16); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-2, 0, 4, 18); + ctx.fillStyle = headColor; + ctx.fillRect(-2, -8, 10, 8); + ctx.fillRect(-2, -8, 4, 12); + ctx.restore(); + } + function drawShovel(ctx, headColor, handleColor) { + ctx.fillStyle = handleColor || "#8B4513"; + ctx.save(); + ctx.translate(16, 16); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-2, 0, 4, 18); + ctx.fillStyle = headColor; + ctx.fillRect(-6, -8, 12, 8); + ctx.fillRect(-4, -12, 8, 4); + ctx.restore(); + } + function drawSword(ctx, bladeColor, guardColor) { + ctx.fillStyle = "#8B4513"; + ctx.fillRect(14, 18, 4, 10); + ctx.fillStyle = guardColor || "#f1c40f"; + ctx.fillRect(10, 16, 12, 3); + ctx.fillStyle = bladeColor; + ctx.beginPath(); + ctx.moveTo(16, 2); + ctx.lineTo(12, 16); + ctx.lineTo(20, 16); + ctx.closePath(); + ctx.fill(); + } + function drawBow(ctx) { + ctx.strokeStyle = "#8B4513"; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(12, 16, 12, -Math.PI * 0.7, Math.PI * 0.7); + ctx.stroke(); + ctx.strokeStyle = "#ddd"; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(12, 4); + ctx.lineTo(12, 28); + ctx.stroke(); + } + function drawIngot(ctx, color) { + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(6, 12); + ctx.lineTo(26, 12); + ctx.lineTo(24, 24); + ctx.lineTo(8, 24); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = "rgba(255,255,255,0.3)"; + ctx.fillRect(8, 8, 16, 4); + } + function drawMeat(ctx, cooked) { + ctx.fillStyle = cooked ? "#8B4513" : "#e74c3c"; + ctx.beginPath(); + ctx.ellipse(16, 18, 10, 8, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = "#f5f5dc"; + ctx.fillRect(8, 6, 3, 12); + ctx.fillRect(14, 4, 3, 8); + } + function makeItemTex(id) { + const c = document.createElement("canvas"); + c.width = 32; + c.height = 32; + const g = c.getContext("2d"); + const tierColors = { + wood: { head: "#a0522d", blade: "#a0522d" }, + stone: { head: "#7f8c8d", blade: "#95a5a6" }, + iron: { head: "#bdc3c7", blade: "#ecf0f1" } + }; + switch (id) { + case "arrow": + drawArrow(g, "#7f8c8d"); + break; + case "bow": + drawBow(g); + break; + case "wood_pickaxe": + drawPickaxe(g, tierColors.wood.head, tierColors.wood.blade); + break; + case "stone_pickaxe": + drawPickaxe(g, tierColors.stone.head); + break; + case "iron_pickaxe": + drawPickaxe(g, tierColors.iron.head); + break; + case "wood_axe": + drawAxe(g, tierColors.wood.head); + break; + case "stone_axe": + drawAxe(g, tierColors.stone.head); + break; + case "iron_axe": + drawAxe(g, tierColors.iron.head); + break; + case "wood_shovel": + drawShovel(g, tierColors.wood.head); + break; + case "stone_shovel": + drawShovel(g, tierColors.stone.head); + break; + case "iron_shovel": + drawShovel(g, tierColors.iron.head); + break; + case "wood_sword": + drawSword(g, tierColors.wood.blade, "#a0522d"); + break; + case "stone_sword": + drawSword(g, tierColors.stone.blade, "#7f8c8d"); + break; + case "iron_sword": + drawSword(g, tierColors.iron.blade, "#bdc3c7"); + break; + case "iron_ingot": + drawIngot(g, "#bdc3c7"); + break; + case "gold_ingot": + drawIngot(g, "#f1c40f"); + break; + case "copper_ingot": + drawIngot(g, "#e67e22"); + break; + case "meat": + drawMeat(g, false); + break; + case "cooked": + drawMeat(g, true); + break; + default: + g.fillStyle = "#7f8c8d"; + g.fillRect(4, 4, 24, 24); + g.fillStyle = "rgba(255,255,255,0.2)"; + g.fillRect(4, 4, 24, 4); + } + return c; + } function makeTex(type) { const t = BLOCKS[type]; - const c = document.createElement('canvas'); - c.width = 32; c.height = 32; - const g = c.getContext('2d'); - - if (type === 'tnt') { - g.fillStyle='#c0392b'; g.fillRect(0,0,32,32); - g.fillStyle='#fff'; g.fillRect(0,12,32,8); - g.fillStyle='#000'; g.font='bold 10px sans-serif'; g.fillText('TNT',6,20); - return c; - } - if (type === 'campfire') { - g.fillStyle='#5d4037'; g.fillRect(4,26,24,6); - g.fillStyle='#3e2723'; g.fillRect(7,23,18,4); - return c; - } - if (type === 'torch') { - g.fillStyle='#6d4c41'; g.fillRect(14,10,4,18); - g.fillStyle='#f39c12'; g.fillRect(12,6,8,8); - return c; - } - if (type === 'glass') { - g.fillStyle='rgba(200,240,255,0.25)'; g.fillRect(0,0,32,32); - g.strokeStyle='rgba(255,255,255,0.65)'; g.strokeRect(2,2,28,28); - g.beginPath(); g.moveTo(5,27); g.lineTo(27,5); g.stroke(); - return c; - } - if (type === 'water') { - g.fillStyle = t.c; g.fillRect(0,0,32,32); - g.fillStyle = 'rgba(255,255,255,0.08)'; - g.fillRect(0,6,32,2); - return c; - } - if (type === 'bed') { - // Основание кровати - g.fillStyle = '#e91e63'; + const c = document.createElement("canvas"); + c.width = 32; + c.height = 32; + const g = c.getContext("2d"); + if (type === "tnt") { + g.fillStyle = "#c0392b"; g.fillRect(0, 0, 32, 32); - // Подушка - g.fillStyle = '#f8bbd0'; + g.fillStyle = "#fff"; + g.fillRect(0, 12, 32, 8); + g.fillStyle = "#000"; + g.font = "bold 10px sans-serif"; + g.fillText("TNT", 6, 20); + return c; + } + if (type === "campfire") { + g.fillStyle = "#5d4037"; + g.fillRect(4, 26, 24, 6); + g.fillStyle = "#3e2723"; + g.fillRect(7, 23, 18, 4); + return c; + } + if (type === "torch") { + g.fillStyle = "#6d4c41"; + g.fillRect(14, 10, 4, 18); + g.fillStyle = "#f39c12"; + g.fillRect(12, 6, 8, 8); + return c; + } + if (type === "glass") { + g.fillStyle = "rgba(200,240,255,0.25)"; + g.fillRect(0, 0, 32, 32); + g.strokeStyle = "rgba(255,255,255,0.65)"; + g.strokeRect(2, 2, 28, 28); + g.beginPath(); + g.moveTo(5, 27); + g.lineTo(27, 5); + g.stroke(); + return c; + } + if (type === "water") { + g.fillStyle = t.c; + g.fillRect(0, 0, 32, 32); + g.fillStyle = "rgba(255,255,255,0.08)"; + g.fillRect(0, 6, 32, 2); + return c; + } + if (type === "bed") { + g.fillStyle = "#e91e63"; + g.fillRect(0, 0, 32, 32); + g.fillStyle = "#f8bbd0"; g.fillRect(2, 2, 14, 14); - // Одеяло - g.fillStyle = '#c2185b'; + g.fillStyle = "#c2185b"; g.fillRect(16, 4, 14, 24); - // Детали одеяла - g.fillStyle = '#e91e63'; + g.fillStyle = "#e91e63"; g.fillRect(18, 6, 10, 20); return c; } - if (type === 'flower') { - g.fillStyle='#2ecc71'; g.fillRect(14,14,4,18); - g.fillStyle=t.c; g.beginPath(); g.arc(16,12,6,0,6.28); g.fill(); + if (type === "flower") { + g.fillStyle = "#2ecc71"; + g.fillRect(14, 14, 4, 18); + g.fillStyle = t.c; + g.beginPath(); + g.arc(16, 12, 6, 0, 6.28); + g.fill(); return c; } - if (type === 'boat') { - // Корпус лодки - g.fillStyle = '#8B4513'; + if (type === "boat") { + g.fillStyle = "#8B4513"; g.fillRect(2, 12, 28, 8); - // Борта - g.fillStyle = '#A0522D'; + g.fillStyle = "#A0522D"; g.fillRect(0, 10, 32, 12); - // Внутренность - g.fillStyle = '#DEB887'; + g.fillStyle = "#DEB887"; g.fillRect(4, 14, 24, 4); - // Дно - g.fillStyle = '#654321'; + g.fillStyle = "#654321"; g.fillRect(2, 20, 28, 4); return c; } - if (type === 'ladder') { - // Боковые стойки лестницы - g.fillStyle = '#8B4513'; + if (type === "ladder") { + g.fillStyle = "#8B4513"; g.fillRect(4, 0, 4, 32); g.fillRect(24, 0, 4, 32); - // Ступени - g.fillStyle = '#A0522D'; + g.fillStyle = "#A0522D"; g.fillRect(4, 4, 24, 3); g.fillRect(4, 12, 24, 3); g.fillRect(4, 20, 24, 3); g.fillRect(4, 28, 24, 3); return c; } - // === BIOME BLOCK TEXTURES === - if (type === 'snow') { - g.fillStyle = '#ecf0f1'; - g.fillRect(0, 0, 32, 32); - g.fillStyle = '#dfe6e9'; - for (let i = 0; i < 4; i++) g.fillRect((Math.random()*26)|0, (Math.random()*26)|0, 5, 3); - return c; - } - if (type === 'ice') { - g.fillStyle = '#74b9ff'; - g.fillRect(0, 0, 32, 32); - g.strokeStyle = 'rgba(255,255,255,0.4)'; - g.beginPath(); g.moveTo(4,10); g.lineTo(20,16); g.lineTo(10,28); g.stroke(); - g.beginPath(); g.moveTo(18,4); g.lineTo(28,12); g.stroke(); - return c; - } - if (type === 'cactus') { - g.fillStyle = '#27ae60'; - g.fillRect(6, 2, 20, 28); - g.fillStyle = '#2ecc71'; - g.fillRect(2, 8, 6, 4); - g.fillRect(24, 14, 6, 4); - g.fillStyle = '#1e8449'; - g.fillRect(12, 0, 2, 30); - g.fillRect(18, 0, 2, 30); - return c; - } - if (type === 'mushroom') { - g.fillStyle = '#f5e6cc'; g.fillRect(14, 20, 4, 12); - g.fillStyle = '#e74c3c'; g.beginPath(); g.arc(16, 14, 10, Math.PI, 0); g.fill(); - g.fillStyle = '#fff'; g.fillRect(10, 10, 3, 3); g.fillRect(17, 8, 4, 3); - return c; - } - if (type === 'moss') { - g.fillStyle = '#0a6640'; - g.fillRect(0, 0, 32, 32); - g.fillStyle = '#1e8449'; - for (let i = 0; i < 6; i++) g.fillRect((Math.random()*26)|0, (Math.random()*26)|0, 4, 3); - return c; - } - if (type === 'swamp_water') { - g.fillStyle = 'rgba(100,120,40,0.6)'; - g.fillRect(0, 0, 32, 32); - g.fillStyle = 'rgba(80,100,20,0.3)'; - g.fillRect(0, 10, 32, 2); - g.fillRect(0, 22, 32, 2); - return c; - } - if (type === 'farmland') { - g.fillStyle = '#8B6914'; - g.fillRect(0, 0, 32, 32); - g.fillStyle = '#7a5c10'; - for (let i = 0; i < 4; i++) g.fillRect(0, 6+i*8, 32, 2); - g.fillStyle = '#6B4E0A'; - g.fillRect(8, 2, 2, 28); - g.fillRect(18, 2, 2, 28); - return c; - } - if (type === 'dead_bush') { - g.fillStyle = '#b2bec3'; - g.fillRect(12, 16, 2, 16); - g.fillRect(8, 14, 8, 2); - g.fillRect(16, 12, 8, 2); - g.fillRect(6, 18, 4, 2); - return c; - } - if (type === 'spruce_leaves') { - g.fillStyle = '#0a6640'; - g.fillRect(0, 0, 32, 32); - g.fillStyle = '#0d7a4d'; - for (let i = 0; i < 5; i++) g.fillRect((Math.random()*24)|0, (Math.random()*24)|0, 6, 4); - return c; - } - // CROP STAGES - if (type.startsWith('wheat_stage') || type.startsWith('carrot_stage') || type.startsWith('potato_stage')) { - const st = parseInt(type.charAt(type.length-1)); - const colors = type.startsWith('wheat') ? ['#a8e6a0','#7dcea0','#b8d730','#f1c40f'] : - type.startsWith('carrot') ? ['#a8e6a0','#f0c27a','#e8a040','#e67e22'] : - ['#a8e6a0','#c8b888','#bfaa78','#dfe6e9']; - g.fillStyle = '#5d4037'; - g.fillRect(15, 18, 2, 14); - if (st >= 1) { - g.fillStyle = colors[st]; - g.fillRect(10, 8 + (3-st)*3, 12, 10); - } else { - g.fillStyle = '#a8e6a0'; - g.fillRect(14, 22, 4, 4); - } - return c; - } - - g.fillStyle = t.c || '#000'; - g.fillRect(0,0,32,32); - - g.fillStyle = 'rgba(0,0,0,0.10)'; - for (let i=0;i<6;i++) g.fillRect((Math.random()*28)|0, (Math.random()*28)|0, 4,4); - - if (type.endsWith('_ore') || type==='coal') { - g.fillStyle = 'rgba(0,0,0,0.35)'; - for (let i=0;i<4;i++) g.fillRect((Math.random()*24)|0, (Math.random()*24)|0, 6,6); + g.fillStyle = t.c || "#000"; + g.fillRect(0, 0, 32, 32); + g.fillStyle = "rgba(0,0,0,0.10)"; + for (let i = 0; i < 6; i++) g.fillRect(Math.random() * 28 | 0, Math.random() * 28 | 0, 4, 4); + if (type.endsWith("_ore") || type === "coal") { + g.fillStyle = "rgba(0,0,0,0.35)"; + for (let i = 0; i < 4; i++) g.fillRect(Math.random() * 24 | 0, Math.random() * 24 | 0, 6, 6); } return c; } - Object.keys(BLOCKS).forEach(k => tex[k] = makeTex(k)); - - // Мир-хранилище - const grid = new Map(); // key "gx,gy" => {gx,gy,t, ...} - const blocks = []; // для рендера/перебора видимых - function k(gx,gy){ return gx+','+gy; } - function getBlock(gx,gy){ return grid.get(k(gx,gy)); } - function hasBlock(gx,gy){ return grid.has(k(gx,gy)); } - function isSolid(gx,gy){ - const b = getBlock(gx,gy); - if(!b || b.dead) return false; - const def = BLOCKS[b.t]; - return !!def.solid && !def.fluid && !def.decor; + function initTextures() { + Object.keys(BLOCKS).forEach((k2) => tex[k2] = makeTex(k2)); + const itemIds = [ + "arrow", + "bow", + "wood_pickaxe", + "stone_pickaxe", + "iron_pickaxe", + "wood_axe", + "stone_axe", + "iron_axe", + "wood_shovel", + "stone_shovel", + "iron_shovel", + "wood_sword", + "stone_sword", + "iron_sword", + "iron_ingot", + "gold_ingot", + "copper_ingot", + "meat", + "cooked" + ]; + itemIds.forEach((id) => { + itemTex[id] = makeItemTex(id); + }); } - function setBlock(gx,gy,t, isPlayerPlaced = false){ - const key = k(gx,gy); - if(grid.has(key)) return false; - const b = { gx, gy, t, dead:false, active:false, fuse:0 }; - grid.set(key, b); - blocks.push(b); - - // Отслеживаем блоки, установленные игроком - if(isPlayerPlaced){ - placedBlocks.push({gx, gy, t}); + + // src/ui/hotbar.js + function rebuildHotbar() { + const hotbarEl = state.hotbarEl; + const inv = state.inv; + const selected = state.selected; + const recentItems = state.recentItems; + const toolDurability = state.toolDurability; + hotbarEl.innerHTML = ""; + const items = recentItems.filter((id) => inv[id] > 0).slice(0, 5); + for (const id of items) { + const s = document.createElement("div"); + s.className = "slot" + (id === selected ? " sel" : ""); + if (BLOCKS[id] && tex[id]) { + s.style.backgroundImage = `url(${tex[id].toDataURL()})`; + s.style.backgroundSize = "cover"; + } else if (itemTex[id]) { + s.style.backgroundImage = `url(${itemTex[id].toDataURL()})`; + s.style.backgroundSize = "cover"; + } else if (ITEMS[id]) { + s.textContent = ITEMS[id].icon || "\u{1F4E6}"; + } else if (TOOLS[id]) { + s.textContent = TOOLS[id].icon || "\u{1F527}"; + } else if (id === "iron_armor") { + s.textContent = "\u{1F6E1}\uFE0F"; + s.style.background = "linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)"; + } + const c = document.createElement("div"); + c.className = "count"; + c.textContent = inv[id]; + s.appendChild(c); + s.onclick = () => { + playSound("click"); + state.selected = id; + state.recentItems = state.recentItems.filter((item) => item !== id); + state.recentItems.unshift(id); + state.recentItems = state.recentItems.slice(0, 5); + rebuildHotbar(); + }; + if (id === "iron_armor" && state.player.equippedArmor === "iron_armor") { + const equipped = document.createElement("div"); + equipped.className = "equipped-indicator"; + equipped.textContent = "\u2713"; + s.appendChild(equipped); + } + if (TOOLS[id] && inv[id] > 0) { + let curDur = 0, maxDur = TOOLS[id].durability; + for (const [tid, dur] of toolDurability) { + if (dur.type === id) { + curDur = dur.current; + maxDur = dur.max; + break; + } + } + if (maxDur > 0) { + const bar = document.createElement("div"); + bar.style.cssText = `position:absolute;bottom:0;left:0;right:0;height:3px;background:#333;border-radius:0 0 6px 6px;overflow:hidden;`; + const fill = document.createElement("div"); + const pct = curDur / maxDur; + const color = pct > 0.5 ? "#2ecc71" : pct > 0.25 ? "#f39c12" : "#e74c3c"; + fill.style.cssText = `width:${pct * 100}%;height:100%;background:${color};`; + bar.appendChild(fill); + s.appendChild(bar); + } + } + hotbarEl.appendChild(s); } - - return true; } - function removeBlock(gx,gy){ - const key = k(gx,gy); - const b = grid.get(key); - if(!b) return null; - if(BLOCKS[b.t].unbreakable) return null; - grid.delete(key); - b.dead = true; - - // Отслеживаем удалённые блоки - const wasPlayerPlaced = placedBlocks.some(pb => pb.gx === gx && pb.gy === gy); - if(wasPlayerPlaced){ - // Удаляем из placedBlocks - placedBlocks = placedBlocks.filter(pb => !(pb.gx === gx && pb.gy === gy)); + + // src/render/particles.js + var parts = []; + function spawnExplosion(x, y, power) { + const n = Math.floor(16 + power * 10); + for (let i = 0; i < n; i++) { + parts.push({ + x, + y, + vx: (Math.random() - 0.5) * (300 + power * 200), + vy: (Math.random() - 0.5) * (300 + power * 200), + t: 0.7, + c: "#ffa500" + }); + } + } + + // src/world/tnt.js + function activateTNT(b, fuse = 3.2) { + if (b.dead) return; + if (b.active) return; + b.active = true; + b.fuse = fuse; + state.activeTNT.add(k(b.gx, b.gy)); + } + function explodeAt(gx, gy) { + const center = getBlock(gx, gy); + if (!center) return; + let bonus = 0; + for (let x = gx - 2; x <= gx + 2; x++) { + for (let y = gy - 2; y <= gy + 2; y++) { + const b = getBlock(x, y); + if (b && !b.dead && b.t === "tnt" && !(x === gx && y === gy)) { + bonus += 0.8; + activateTNT(b, 0.12); + } + } + } + const power = 1 + bonus; + const radius = 3.2 + bonus * 0.7; + const dmgR = 150 + bonus * 60; + removeBlock(gx, gy); + state.activeTNT.delete(k(gx, gy)); + playSound("explode1"); + spawnExplosion(gx * TILE + TILE / 2, gy * TILE + TILE / 2, power); + for (let x = Math.floor(gx - radius); x <= Math.ceil(gx + radius); x++) { + for (let y = Math.floor(gy - radius); y <= Math.ceil(gy + radius); y++) { + const d = Math.hypot(x - gx, y - gy); + if (d > radius) continue; + const b = getBlock(x, y); + if (!b || b.dead) continue; + if (BLOCKS[b.t].fluid) continue; + if (BLOCKS[b.t].unbreakable) continue; + if (b.t === "tnt") { + activateTNT(b, 0.12); + continue; + } + removeBlock(x, y); + if (state.inv[b.t] !== void 0 && Math.random() < 0.2) state.inv[b.t]++; + } + } + rebuildHotbar(); + const hurt = (e) => { + const dx = e.x + e.w / 2 - (gx * TILE + TILE / 2); + const dy = e.y + e.h / 2 - (gy * TILE + TILE / 2); + const dist = Math.hypot(dx, dy); + if (dist < dmgR) { + const dmg = (dmgR - dist) * 0.06 * power; + if (e === state.player) { + const actualDamage = calculateDamage(dmg); + state.player.hp -= actualDamage; + } else { + e.hp -= dmg; + } + e.vx += (dx / dist || 0) * 600; + e.vy -= 320; + } + }; + hurt(state.player); + state.mobs.forEach(hurt); + } + + // src/game/save.js + var SAVE_KEY2 = state.SAVE_KEY; + function initDB() { + return new Promise((resolve) => { + console.log("\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C localStorage \u0434\u043B\u044F \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0439 (sandbox \u0440\u0435\u0436\u0438\u043C)"); + resolve(null); + }); + } + function saveGame() { + const saveData = { + version: 2, + worldSeed: state.worldSeed, + player: { + x: state.player.x, + y: state.player.y, + hp: state.player.hp, + hunger: state.player.hunger, + o2: state.player.o2 + }, + inventory: state.inv, + time: state.worldTime, + isNight: state.isNightTime, + // Сохраняем только изменения + placedBlocks: state.placedBlocks.slice(), + removedBlocks: state.removedBlocks.slice() + }; + const saveSize = JSON.stringify(saveData).length; + console.log("\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435: player HP:", state.player.hp, "hunger:", state.player.hunger, "o2:", state.player.o2); + try { + localStorage.setItem(SAVE_KEY2, JSON.stringify(saveData)); + console.log(`\u0418\u0433\u0440\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430 \u0432 localStorage (\u0440\u0430\u0437\u043C\u0435\u0440: ${saveSize} \u0431\u0430\u0439\u0442)`); + } catch (e) { + console.warn("\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F \u0432 localStorage, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C \u0442\u043E\u043B\u044C\u043A\u043E in-memory:", e); + state.inMemorySave = saveData; + console.log(`\u0418\u0433\u0440\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430 \u0432 \u043F\u0430\u043C\u044F\u0442\u0438 (\u0440\u0430\u0437\u043C\u0435\u0440: ${saveSize} \u0431\u0430\u0439\u0442)`); + } + } + function loadGame() { + return new Promise((resolve, reject) => { + try { + const localSave = localStorage.getItem(SAVE_KEY2); + if (localSave) { + const parsed = JSON.parse(localSave); + console.log("\u0417\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E \u0438\u0437 localStorage, player HP:", parsed.player?.hp); + resolve(parsed); + return; + } + } catch (e) { + console.warn("\u041E\u0448\u0438\u0431\u043A\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A localStorage:", e); + } + if (state.inMemorySave) { + console.log("\u0417\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E \u0438\u0437 in-memory \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F, player HP:", state.inMemorySave.player?.hp); + resolve(state.inMemorySave); + return; + } + console.log("\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E"); + resolve(null); + }); + } + function migrateV1toV2(saveData) { + console.log("\u041C\u0438\u0433\u0440\u0430\u0446\u0438\u044F \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F \u0441 \u0432\u0435\u0440\u0441\u0438\u0438 1 \u043D\u0430 \u0432\u0435\u0440\u0441\u0438\u044E 2..."); + saveData.worldSeed = state.worldSeed; + saveData.placedBlocks = []; + saveData.removedBlocks = []; + delete saveData.generatedBlocks; + saveData.version = 2; + console.log("\u041C\u0438\u0433\u0440\u0430\u0446\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0430"); + } + async function applySave(saveData) { + if (!saveData) return; + console.log("=== applySave START ==="); + console.log("player HP before applySave:", state.player.hp); + console.log("saveData.player.hp:", saveData.player?.hp); + if (saveData.version === 1) { + migrateV1toV2(saveData); + } + if (saveData.worldSeed !== void 0) { + state.worldSeed = saveData.worldSeed; + } + if (saveData.player) { + state.player.x = saveData.player.x; + state.player.y = saveData.player.y; + state.player.hunger = saveData.player.hunger; + state.player.o2 = saveData.player.o2; + state.spawnPoint.x = state.player.x; + state.spawnPoint.y = state.player.y; + const savedHP = saveData.player.hp; + console.log("Saved HP from file:", savedHP); + if (savedHP <= 0) { + console.log("WARNING: Saved HP is <= 0, setting to 100!"); + state.player.hp = 100; + } else { + state.player.hp = savedHP; + } + console.log("player HP after restore:", state.player.hp); + console.log("spawnPoint \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D \u0438\u0437 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F: x=", state.spawnPoint.x, "y=", state.spawnPoint.y); } else { - // Это природный блок - добавляем в removedBlocks - removedBlocks.push({gx, gy}); + console.log("No player data in save, setting default HP: 100"); + state.player.hp = 100; } - - return b; + console.log("=== applySave END ==="); + if (saveData.inventory) { + for (const key in saveData.inventory) { + state.inv[key] = saveData.inventory[key]; + } + } + if (saveData.time !== void 0) { + state.worldTime = saveData.time; + } + if (saveData.isNight !== void 0) { + state.isNightTime = saveData.isNight; + } + regenerateVisibleChunks(); + if (saveData.version === 2) { + for (const block of saveData.placedBlocks) { + setBlock(block.gx, block.gy, block.t, true); + } + for (const block of saveData.removedBlocks) { + removeBlock(block.gx, block.gy); + } + state.placedBlocks = saveData.placedBlocks || []; + state.removedBlocks = saveData.removedBlocks || []; + } + rebuildHotbar(); + console.log("\u0418\u0433\u0440\u0430 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u0430"); } - - // Физика жидкости - const waterUpdateQueue = new Set(); - let waterUpdateTimer = 0; - const WATER_UPDATE_INTERVAL = 0.05; // Обновляем воду каждые 0.05 секунды - - function updateWaterPhysics(dt){ + + // src/ui/save-controls.js + var dropdownOpen = false; + function initSaveControls() { + const menuBtn = document.getElementById("menuBtn"); + const menuDropdown = document.getElementById("menuDropdown"); + const saveItem = document.getElementById("saveItem"); + const resetItem = document.getElementById("resetItem"); + const marketItem = document.getElementById("marketItem"); + menuBtn.onclick = null; + menuBtn.addEventListener("click", (e) => { + e.stopPropagation(); + dropdownOpen = !dropdownOpen; + menuDropdown.classList.toggle("open", dropdownOpen); + }); + document.addEventListener("click", () => { + if (dropdownOpen) { + dropdownOpen = false; + menuDropdown.classList.remove("open"); + } + }); + menuDropdown.addEventListener("click", (e) => { + e.stopPropagation(); + }); + saveItem.addEventListener("click", () => { + playSound("click"); + saveGame(); + dropdownOpen = false; + menuDropdown.classList.remove("open"); + alert("\u0418\u0433\u0440\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0430!"); + }); + resetItem.addEventListener("click", () => { + if (confirm("\u0412\u044B \u0443\u0432\u0435\u0440\u0435\u043D\u044B, \u0447\u0442\u043E \u0445\u043E\u0442\u0438\u0442\u0435 \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u0438 \u043D\u0430\u0447\u0430\u0442\u044C \u043D\u043E\u0432\u0443\u044E \u0438\u0433\u0440\u0443?")) { + playSound("click"); + try { + localStorage.removeItem(state.SAVE_KEY); + console.log("\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u0443\u0434\u0430\u043B\u0435\u043D\u043E \u0438\u0437 localStorage"); + } catch (e) { + console.warn("\u041E\u0448\u0438\u0431\u043A\u0430 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u044F \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F:", e); + } + state.inMemorySave = null; + state.worldId = Math.random().toString(36).substring(2, 10); + console.log("\u041D\u043E\u0432\u044B\u0439 worldId \u043F\u043E\u0441\u043B\u0435 \u0441\u0431\u0440\u043E\u0441\u0430:", state.worldId); + try { + const newUrl = new URL(window.location.href); + newUrl.searchParams.set("world", state.worldId); + if (typeof window.history !== "undefined" && typeof window.history.replaceState === "function") { + window.history.replaceState(null, "", newUrl.toString()); + console.log("URL \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D:", newUrl.toString()); + } + } catch (e) { + console.error("\u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F URL:", e); + } + location.reload(); + } else { + dropdownOpen = false; + menuDropdown.classList.remove("open"); + } + }); + marketItem.addEventListener("click", () => { + dropdownOpen = false; + menuDropdown.classList.remove("open"); + document.dispatchEvent(new CustomEvent("toggleMarket")); + }); + } + function updateSaveButtonVisibility() { + const saveItem = document.getElementById("saveItem"); + if (!saveItem) return; + if (state.isMultiplayer && state.otherPlayers.size > 0) { + saveItem.style.display = "none"; + } else { + saveItem.style.display = "block"; + } + } + + // src/multiplayer/socket-helpers.js + function getRandomPlayerColor(socketId) { + const colors = ["#e74c3c", "#3498db", "#2ecc71", "#f39c12", "#9b59b6", "#1abc9c", "#e91e63", "#00bcd4"]; + let hash = 0; + for (let i = 0; i < socketId.length; i++) { + hash = (hash << 5) - hash + socketId.charCodeAt(i); + hash = hash & hash; + } + return colors[Math.abs(hash) % colors.length]; + } + var lastMoveSendTime = 0; + var MOVE_SEND_INTERVAL2 = 0.05; + var lastSentX = 0; + var lastSentY = 0; + function sendPlayerPosition() { + if (!state.isMultiplayer || !state.socket || !state.socket.connected) return; + const now = performance.now() / 1e3; + if (now - lastMoveSendTime < MOVE_SEND_INTERVAL2) return; + const dx = Math.abs(state.player.x - lastSentX); + const dy = Math.abs(state.player.y - lastSentY); + if (dx < 1 && dy < 1) return; + lastMoveSendTime = now; + lastSentX = state.player.x; + lastSentY = state.player.y; + state.socket.emit("player_move", { x: state.player.x, y: state.player.y, player_name: state.playerName }); + } + function sendBlockChange(gx, gy, t, op) { + if (!state.isMultiplayer || !state.socket || !state.socket.connected) return; + state.socket.emit("block_change", { gx, gy, t, op }); + } + + // src/ui/trade.js + function itemName(id) { + return BLOCKS[id]?.n || TOOLS[id]?.n || ITEMS[id]?.n || id; + } + function itemIcon(id) { + if (BLOCKS[id]) return "\u{1F7EB}"; + if (TOOLS[id]) return TOOLS[id].icon || "\u{1F527}"; + if (ITEMS[id]) return ITEMS[id].icon || "\u{1F4E6}"; + return "\u{1F4E6}"; + } + var tradeOpen = false; + var currentTradeId = null; + var myOffer = {}; + var partnerOffer = {}; + var iConfirmed = false; + function initTradeSocket(socket2) { + socket2.on("trade_request", (data) => { + playSound("click"); + const overlay = document.createElement("div"); + overlay.id = "tradeRequestOverlay"; + overlay.className = "custom-modal-overlay"; + overlay.innerHTML = ` +
+
\u{1F91D} ${data.from_name} \u0445\u043E\u0447\u0435\u0442 \u0442\u043E\u0440\u0433\u043E\u0432\u0430\u0442\u044C
+ +
`; + document.getElementById("game").appendChild(overlay); + document.getElementById("tradeAcceptBtn").addEventListener("click", () => { + overlay.remove(); + socket2.emit("trade_accept", { from_socket_id: data.from }); + }); + document.getElementById("tradeDeclineBtn").addEventListener("click", () => { + overlay.remove(); + socket2.emit("trade_decline", { from_socket_id: data.from }); + }); + }); + socket2.on("trade_start", (data) => { + currentTradeId = data.trade_id; + myOffer = {}; + partnerOffer = {}; + iConfirmed = false; + tradeOpen = true; + renderTradeUI(data.partner_name); + state.tradePanel.style.display = "block"; + }); + socket2.on("trade_update", (data) => { + partnerOffer = data.player1Offer || data.player2Offer || {}; + const isP1 = state.socket && state.socket.id === data.player1Offer; + if (state.mySocketId) { + } + renderTradeOffers(); + }); + socket2.on("trade_status", (data) => { + const confirmBtn = document.getElementById("tradeConfirmBtn"); + if (confirmBtn) { + const amIPlayer1 = true; + if (iConfirmed) { + confirmBtn.textContent = "\u2705 \u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435..."; + confirmBtn.disabled = true; + confirmBtn.style.background = "#555"; + } + } + const partnerStatus = document.getElementById("partnerConfirmStatus"); + if (partnerStatus) { + const partnerConfirmed = data.player1Confirm || data.player2Confirm; + partnerStatus.textContent = partnerConfirmed ? "\u2705 \u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B" : "\u23F3 \u0415\u0449\u0451 \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B"; + } + }); + socket2.on("trade_complete", (data) => { + const received = data.received || {}; + for (const [item, qty] of Object.entries(received)) { + if (qty > 0) { + state.inv[item] = (state.inv[item] || 0) + qty; + } + } + const sent = data.sent || {}; + for (const [item, qty] of Object.entries(sent)) { + if (qty > 0) { + state.inv[item] = (state.inv[item] || 0) - qty; + if (state.inv[item] < 0) state.inv[item] = 0; + } + } + addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", "\u{1F91D} \u0421\u0434\u0435\u043B\u043A\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0430!"); + closeTrade(); + }); + socket2.on("trade_declined", () => { + addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", "\u0422\u043E\u0440\u0433\u043E\u0432\u043B\u044F \u043E\u0442\u043A\u043B\u043E\u043D\u0435\u043D\u0430"); + closeTrade(); + }); + socket2.on("trade_cancelled", () => { + addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", "\u0422\u043E\u0440\u0433\u043E\u0432\u043B\u044F \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430"); + closeTrade(); + }); + socket2.on("trade_error", (data) => { + addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", "\u274C " + data.msg); + }); + } + function renderTradeUI(partnerName) { + const panel = state.tradePanel; + const content = state.tradeContent; + content.innerHTML = ` +
\u{1F91D} \u0422\u043E\u0440\u0433\u043E\u0432\u043B\u044F \u0441 ${partnerName}
+
+
+
\u0412\u044B \u043F\u0440\u0435\u0434\u043B\u0430\u0433\u0430\u0435\u0442\u0435:
+
+ +
+
\u21C4
+
+
\u041F\u0430\u0440\u0442\u043D\u0451\u0440 \u043F\u0440\u0435\u0434\u043B\u0430\u0433\u0430\u0435\u0442:
+
+ \u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435... +
+
+
+
\u23F3 \u041F\u0430\u0440\u0442\u043D\u0451\u0440 \u0435\u0449\u0451 \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B
+
+ + +
`; + document.getElementById("tradeAddItemBtn")?.addEventListener("click", showAddItemDialog); + document.getElementById("tradeConfirmBtn")?.addEventListener("click", confirmTrade); + document.getElementById("tradeCancelBtn")?.addEventListener("click", cancelTrade); + } + function renderTradeOffers() { + const myItems = document.getElementById("myOfferItems"); + if (myItems) { + const keys = Object.keys(myOffer).filter((k2) => myOffer[k2] > 0); + if (keys.length === 0) { + myItems.innerHTML = '\u041D\u0438\u0447\u0435\u0433\u043E'; + } else { + myItems.innerHTML = keys.map((k2) => `
${itemIcon(k2)} ${itemName(k2)} x${myOffer[k2]}
`).join(""); + } + } + } + function showAddItemDialog() { + const inv = state.inv; + const items = Object.keys(inv).filter((id) => inv[id] > 0); + if (items.length === 0) { + addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", "\u0418\u043D\u0432\u0435\u043D\u0442\u0430\u0440\u044C \u043F\u0443\u0441\u0442!"); + return; + } + const overlay = document.createElement("div"); + overlay.id = "tradeItemPicker"; + overlay.className = "custom-modal-overlay"; + let html = '
\u0427\u0442\u043E \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u0442\u044C?
'; + for (const id of items) { + const offered = myOffer[id] || 0; + const available = inv[id] - offered; + if (available <= 0) continue; + html += `
+ ${itemIcon(id)} + ${itemName(id)} + x${available} +
`; + } + html += "
"; + overlay.innerHTML = html; + document.getElementById("game").appendChild(overlay); + overlay.querySelectorAll(".trade-pick-item").forEach((el) => { + el.addEventListener("click", () => { + const itemId = el.dataset.item; + const available = inv[itemId] - (myOffer[itemId] || 0); + const qty = Math.min(available, available > 1 ? promptForQty(itemId, available) : 1); + if (qty > 0) { + myOffer[itemId] = (myOffer[itemId] || 0) + qty; + renderTradeOffers(); + iConfirmed = false; + const btn = document.getElementById("tradeConfirmBtn"); + if (btn) { + btn.textContent = "\u2705 \u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C"; + btn.disabled = false; + btn.style.background = "#2ecc71"; + } + } + overlay.remove(); + }); + }); + } + function promptForQty(itemId, max) { + const val = parseInt(prompt(`\u0421\u043A\u043E\u043B\u044C\u043A\u043E ${itemName(itemId)}? (\u043C\u0430\u043A\u0441: ${max})`, "1")); + return isNaN(val) || val < 1 ? 1 : Math.min(val, max); + } + function confirmTrade() { + if (!currentTradeId || !state.socket) return; + for (const [item, qty] of Object.entries(myOffer)) { + if (qty > 0) { + if (state.inv[item] < qty) { + addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", `\u041D\u0435\u0434\u043E\u0441\u0442\u0430\u0442\u043E\u0447\u043D\u043E ${itemName(item)}!`); + return; + } + } + } + state.socket.emit("trade_offer", { trade_id: currentTradeId, offer: myOffer }); + setTimeout(() => { + state.socket.emit("trade_confirm", { trade_id: currentTradeId }); + iConfirmed = true; + const btn = document.getElementById("tradeConfirmBtn"); + if (btn) { + btn.textContent = "\u2705 \u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435..."; + btn.disabled = true; + btn.style.background = "#555"; + } + }, 200); + } + function cancelTrade() { + if (!currentTradeId || !state.socket) return; + state.socket.emit("trade_cancel", { trade_id: currentTradeId }); + closeTrade(); + } + function closeTrade() { + tradeOpen = false; + currentTradeId = null; + myOffer = {}; + partnerOffer = {}; + iConfirmed = false; + if (state.tradePanel) state.tradePanel.style.display = "none"; + document.getElementById("tradeItemPicker")?.remove(); + } + + // src/multiplayer/socket.js + var socket = null; + function initSocket() { + try { + socket = io(SERVER_URL, { + path: "/socket.io/", + transports: ["websocket", "polling"] + }); + state.socket = socket; + socket.on("connect", () => { + console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${state.worldId}`); + state.mySocketId = socket.id; + state.isMultiplayer = true; + socket.emit("join_world", { world_id: state.worldId, player_name: state.playerName }); + if (state.worldIdEl) state.worldIdEl.textContent = state.worldId; + if (state.multiplayerStatus) state.multiplayerStatus.style.display = "block"; + initTradeSocket(socket); + }); + socket.on("connect_error", (error) => { + console.error("Socket connection error:", error); + state.isMultiplayer = false; + }); + socket.on("disconnect", () => { + console.log("Disconnected from server"); + state.isMultiplayer = false; + state.otherPlayers.clear(); + if (state.multiplayerStatus) state.multiplayerStatus.style.display = "none"; + }); + socket.on("world_state", (data) => { + console.log("Received world_state:", data); + if (data.seed !== void 0 && data.seed !== state.worldSeed) { + const oldSeed = state.worldSeed; + state.worldSeed = data.seed; + console.log("World seed changed from", oldSeed, "to", state.worldSeed); + state.generated.clear(); + grid.clear(); + blocks.length = 0; + state.placedBlocks = []; + state.removedBlocks = []; + console.log("World regenerated with new seed:", state.worldSeed); + } + if (data.blocks && Array.isArray(data.blocks)) { + for (const block of data.blocks) { + const key = k(block.gx, block.gy); + state.serverOverrides.set(key, { op: block.op, t: block.t }); + if (block.op === "set") { + setBlock(block.gx, block.gy, block.t, false); + } else if (block.op === "remove") { + removeBlock(block.gx, block.gy); + } + } + } + if (data.time !== void 0) { + state.worldTime = data.time; + state.isNightTime = state.worldTime > 0.5; + } + { + const startGX = 6; + for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx); + const surfaceY = surfaceGyAt(startGX); + let safeGY = surfaceY - 1; + const aboveBlock = getBlock(startGX, surfaceY - 1); + if (aboveBlock && aboveBlock.t === "water") { + for (let gy = SEA_GY - 1; gy >= 0; gy--) { + const b = getBlock(startGX, gy); + if (!b || b.dead || b.t === "air" || b.t === "water") continue; + safeGY = gy - 1; + break; + } + } + state.spawnPoint.x = startGX * TILE; + state.spawnPoint.y = safeGY * TILE; + console.log("Client-side spawn point:", state.spawnPoint, "surfaceY:", surfaceY, "safeGY:", safeGY); + } + state.player.x = state.spawnPoint.x; + state.player.y = state.spawnPoint.y; + state.player.vx = 0; + state.player.vy = 0; + state.player.fallStartY = state.player.y; + console.log("Player moved to spawn point:", state.player.x, state.player.y); + state.player.hp = 100; + state.player.hunger = 100; + state.player.o2 = 100; + state.player.invuln = 0; + console.log("[MULTIPLAYER CONNECT] Player HP set to 100% on connect"); + if (data.players && Array.isArray(data.players)) { + state.otherPlayers.clear(); + for (const p of data.players) { + if (p.socket_id !== state.mySocketId) { + state.otherPlayers.set(p.socket_id, { + x: p.x, + y: p.y, + color: getRandomPlayerColor(p.socket_id), + name: p.player_name || "\u0418\u0433\u0440\u043E\u043A" + }); + } + } + state.playerCountEl.textContent = data.players.length; + } + if (data.mobs && Array.isArray(data.mobs)) { + state.serverMobs.clear(); + for (const m of data.mobs) { + const sm = { ...m, maxHp: m.maxHp || m.hp, vx: m.vx || 0, vy: m.vy || 0, grounded: false, inWater: false, aiT: 0, dir: m.dir || 1, dead: false, fuse: m.fuse || 0, shootCooldown: 2, speed: m.speed || 80 }; + state.serverMobs.set(m.id, sm); + } + } + if (data.tg_id) state.myTgId = data.tg_id; + }); + socket.on("player_joined", (data) => { + console.log("Player joined:", data.socket_id); + if (data.socket_id !== state.mySocketId) { + const spawnGX = 6; + genColumn(spawnGX); + const surfaceY = surfaceGyAt(spawnGX); + const safeSpawnX = spawnGX * TILE; + const safeSpawnY = (surfaceY - 1) * TILE; + state.otherPlayers.set(data.socket_id, { + x: safeSpawnX, + y: safeSpawnY, + color: getRandomPlayerColor(data.socket_id), + name: data.player_name || "\u0418\u0433\u0440\u043E\u043A" + }); + addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", `\u0418\u0433\u0440\u043E\u043A \u043F\u0440\u0438\u0441\u043E\u0435\u0434\u0438\u043D\u0438\u043B\u0441\u044F`); + updateSaveButtonVisibility(); + } + }); + socket.on("player_moved", (data) => { + if (data.socket_id !== state.mySocketId && state.otherPlayers.has(data.socket_id)) { + const p = state.otherPlayers.get(data.socket_id); + p.x = data.x; + p.y = data.y; + if (data.player_name) { + p.name = data.player_name; + } + } + }); + socket.on("player_left", (data) => { + console.log("Player left:", data.socket_id); + state.otherPlayers.delete(data.socket_id); + addChatMessage("\u0421\u0438\u0441\u0442\u0435\u043C\u0430", `\u0418\u0433\u0440\u043E\u043A \u043F\u043E\u043A\u0438\u043D\u0443\u043B \u0438\u0433\u0440\u0443`); + updateSaveButtonVisibility(); + }); + socket.on("mob_spawned", (data) => { + const sm = { ...data, maxHp: data.maxHp || data.hp, vx: data.vx || 0, vy: data.vy || 0, grounded: false, inWater: false, aiT: 0, dir: data.dir || 1, dead: false, fuse: data.fuse || 0, shootCooldown: 2, speed: data.speed || 80 }; + state.serverMobs.set(data.id, sm); + }); + socket.on("mob_positions", (arr) => { + for (const u of arr) { + const sm = state.serverMobs.get(u.id); + if (sm) { + sm.x = u.x; + sm.y = u.y; + sm.vx = u.vx; + sm.vy = u.vy; + sm.dir = u.dir; + sm.hp = u.hp; + sm.fuse = u.fuse || 0; + } + } + }); + socket.on("mob_despawned", (data) => { + state.serverMobs.delete(data.id); + }); + socket.on("mob_died", (data) => { + const sm = state.serverMobs.get(data.id); + if (sm && data.killer === state.mySocketId) { + if (sm.kind === "chicken") playSound("hurt_chicken"); + state.inv.meat += sm.kind === "chicken" ? 1 : 2; + if (sm.kind === "skeleton") { + state.inv.arrow += 2 + Math.floor(Math.random() * 3); + if (Math.random() < 0.15) state.inv.bow = (state.inv.bow || 0) + 1; + } + rebuildHotbar(); + } + state.serverMobs.delete(data.id); + }); + socket.on("mob_hurt_ack", (data) => { + const sm = state.serverMobs.get(data.id); + if (sm) sm.hp = data.hp; + }); + socket.on("mob_explode", (data) => { + explodeAt(data.gx, data.gy); + state.serverMobs.delete(data.id); + }); + socket.on("mob_shoot", (data) => { + state.projectiles.push({ + x: data.x, + y: data.y, + vx: data.vx, + vy: data.vy, + dmg: data.dmg, + owner: "mob", + life: data.life + }); + }); + socket.on("block_changed", (data) => { + const key = k(data.gx, data.gy); + state.serverOverrides.set(key, { op: data.op, t: data.t }); + if (data.op === "set") { + setBlock(data.gx, data.gy, data.t, false); + } else if (data.op === "remove") { + removeBlock(data.gx, data.gy); + } + }); + socket.on("chat_message", (data) => { + const senderName = data.socket_id === state.mySocketId ? "\u0412\u044B" : `\u0418\u0433\u0440\u043E\u043A ${data.socket_id.substring(0, 6)}`; + addChatMessage(senderName, data.message); + }); + socket.on("time_update", (data) => { + if (data.time !== void 0) { + state.worldTime = data.time; + state.isNightTime = state.worldTime > 0.5; + } + }); + } catch (e) { + console.error("Error initializing socket:", e); + state.isMultiplayer = false; + } + } + + // src/data/recipes.js + var RECIPES = [ + { out: "planks", qty: 4, cost: { wood: 1 } }, + { out: "ladder", qty: 3, cost: { planks: 7 } }, + { out: "torch", qty: 2, cost: { coal: 1, planks: 1 } }, + { out: "glass", qty: 1, cost: { sand: 3 } }, + { out: "brick", qty: 1, cost: { stone: 2, clay: 1 } }, + { out: "campfire", qty: 1, cost: { wood: 1, coal: 1 } }, + { out: "tnt", qty: 1, cost: { sand: 2, coal: 1 } }, + { out: "bed", qty: 1, cost: { wood: 3, planks: 3 } }, + { out: "boat", qty: 1, cost: { wood: 5 } }, + { out: "wood_pickaxe", qty: 1, cost: { wood: 3, planks: 2 } }, + { out: "stone_pickaxe", qty: 1, cost: { wood: 1, stone: 3 } }, + { out: "iron_pickaxe", qty: 1, cost: { wood: 1, iron_ore: 2 } }, + { out: "wood_sword", qty: 1, cost: { wood: 2, planks: 1 } }, + { out: "stone_sword", qty: 1, cost: { wood: 1, stone: 2 } }, + { out: "iron_sword", qty: 1, cost: { wood: 1, iron_ore: 1 } }, + { out: "wood_axe", qty: 1, cost: { wood: 3, planks: 2 } }, + { out: "stone_axe", qty: 1, cost: { wood: 1, stone: 3 } }, + { out: "iron_axe", qty: 1, cost: { wood: 1, iron_ore: 2 } }, + { out: "wood_shovel", qty: 1, cost: { wood: 1, planks: 1 } }, + { out: "stone_shovel", qty: 1, cost: { wood: 1, stone: 2 } }, + { out: "iron_shovel", qty: 1, cost: { wood: 1, iron_ore: 1 } }, + { out: "iron_armor", qty: 1, cost: { iron_ore: 5 } }, + { out: "furnace", qty: 1, cost: { stone: 8 } }, + { out: "bow", qty: 1, cost: { wood: 3, planks: 2 } }, + { out: "arrow", qty: 4, cost: { stone: 1, wood: 1 } } + ]; + var SMELTING_RECIPES = [ + { in: "sand", qty: 1, out: "glass", outQty: 1, time: 3 }, + { in: "clay", qty: 1, out: "brick", outQty: 1, time: 3 }, + { in: "iron_ore", qty: 1, out: "iron_ingot", outQty: 1, time: 5 }, + { in: "gold_ore", qty: 1, out: "gold_ingot", outQty: 1, time: 5 }, + { in: "copper_ore", qty: 1, out: "copper_ingot", outQty: 1, time: 4 }, + { in: "meat", qty: 1, out: "cooked", outQty: 1, time: 2 }, + { in: "cobblestone", qty: 1, out: "stone", outQty: 1, time: 2 } + ]; + + // src/world/water.js + var waterUpdateQueue = /* @__PURE__ */ new Set(); + var waterUpdateTimer = 0; + var WATER_UPDATE_INTERVAL = 0.05; + function updateWaterPhysics(dt) { waterUpdateTimer += dt; - if(waterUpdateTimer < WATER_UPDATE_INTERVAL) return; + if (waterUpdateTimer < WATER_UPDATE_INTERVAL) return; waterUpdateTimer = 0; - - // Ограничиваем количество водных блоков для обработки (оптимизация) const MAX_WATER_BLOCKS_PER_UPDATE = 50; let processedCount = 0; - - // Собираем только видимые водные блоки в очередь (оптимизация) waterUpdateQueue.clear(); - const minGX = Math.floor(camX/TILE) - 10; - const maxGX = Math.floor((camX+W)/TILE) + 10; - const minGY = Math.floor(camY/TILE) - 10; - const maxGY = Math.floor((camY+H)/TILE) + 10; - - for(const b of blocks){ - if(processedCount >= MAX_WATER_BLOCKS_PER_UPDATE) break; - if(!b.dead && b.t === 'water' && - b.gx >= minGX && b.gx <= maxGX && - b.gy >= minGY && b.gy <= maxGY){ + const W = state.W; + const H = state.H; + const minGX = Math.floor(state.camX / TILE) - 10; + const maxGX = Math.floor((state.camX + W) / TILE) + 10; + const minGY = Math.floor(state.camY / TILE) - 10; + const maxGY = Math.floor((state.camY + H) / TILE) + 10; + for (const b of blocks) { + if (processedCount >= MAX_WATER_BLOCKS_PER_UPDATE) break; + if (!b.dead && b.t === "water" && b.gx >= minGX && b.gx <= maxGX && b.gy >= minGY && b.gy <= maxGY) { waterUpdateQueue.add(k(b.gx, b.gy)); processedCount++; } } - - // Обновляем воду с ограничением глубины распространения - const processed = new Set(); + const processed = /* @__PURE__ */ new Set(); const toAdd = []; - const MAX_WATER_DEPTH = 20; // Максимальная глубина распространения воды - - for(const key of waterUpdateQueue){ - if(processed.has(key)) continue; + const MAX_WATER_DEPTH = 20; + for (const key of waterUpdateQueue) { + if (processed.has(key)) continue; const b = grid.get(key); - if(!b || b.dead) continue; + if (!b || b.dead) continue; processed.add(key); - const gx = b.gx; const gy = b.gy; - - // Проверяем глубину - не распространяем воду слишком глубоко - if(gy > SEA_GY + MAX_WATER_DEPTH) continue; - - // Проверяем, можно ли воде упасть вниз + if (gy > SEA_GY + MAX_WATER_DEPTH) continue; const belowKey = k(gx, gy + 1); const below = grid.get(belowKey); - - // Внизу пусто - вода создаёт новый блок внизу (но не удаляется сверху) - if(!below || below.dead){ - // Ограничиваем создание новых водных блоков - if(toAdd.length < 20){ // Максимум 20 новых блоков за обновление - toAdd.push({gx, gy: gy + 1, t: 'water'}); + if (!below || below.dead) { + if (toAdd.length < 20) { + toAdd.push({ gx, gy: gy + 1, t: "water" }); processed.add(belowKey); } continue; } - - // Если внизу не вода и не твёрдый блок - вода может течь вниз - if(!isSolid(gx, gy + 1) && below && below.t !== 'water'){ - if(toAdd.length < 20){ - toAdd.push({gx, gy: gy + 1, t: 'water'}); + if (!isSolid(gx, gy + 1) && below && below.t !== "water") { + if (toAdd.length < 20) { + toAdd.push({ gx, gy: gy + 1, t: "water" }); processed.add(belowKey); } continue; } - - // Если внизу твёрдый блок или вода - вода растекается горизонтально - // Проверяем левую сторону const leftKey = k(gx - 1, gy); const left = grid.get(leftKey); - if(!left || left.dead){ - if(toAdd.length < 20){ - toAdd.push({gx: gx - 1, gy, t: 'water'}); + if (!left || left.dead) { + if (toAdd.length < 20) { + toAdd.push({ gx: gx - 1, gy, t: "water" }); processed.add(leftKey); } continue; } - - // Проверяем правую сторону const rightKey = k(gx + 1, gy); const right = grid.get(rightKey); - if(!right || right.dead){ - if(toAdd.length < 20){ - toAdd.push({gx: gx + 1, gy, t: 'water'}); + if (!right || right.dead) { + if (toAdd.length < 20) { + toAdd.push({ gx: gx + 1, gy, t: "water" }); processed.add(rightKey); } continue; } } - - // Применяем изменения (только добавляем новые блоки) - for(const newData of toAdd){ + for (const newData of toAdd) { const key = k(newData.gx, newData.gy); - if(!grid.has(key)){ + if (!grid.has(key)) { const b = { gx: newData.gx, gy: newData.gy, @@ -1567,239 +1872,500 @@ function customConfirm(msg, onYes) { blocks.push(b); } } - - // Очищаем мёртвые блоки из массива - for(let i = blocks.length - 1; i >= 0; i--){ - if(blocks[i].dead){ + for (let i = blocks.length - 1; i >= 0; i--) { + if (blocks[i].dead) { blocks.splice(i, 1); } } } - - // Инвентарь - const inv = { - dirt:6, stone:0, sand:0, gravel:0, clay:0, - wood:0, planks:0, ladder:0, leaves:0, coal:0, - copper_ore:0, iron_ore:0, gold_ore:0, diamond_ore:0, - brick:0, glass:0, - tnt:1, campfire:0, torch:0, - meat:0, cooked:0, arrow:0, - wood_pickaxe:0, stone_pickaxe:0, iron_pickaxe:0, - wood_sword:0, stone_sword:0, iron_sword:0, - iron_armor:0, gold_armor:0, - bow:0, furnace:0, - bed:0, boat:0, - iron_ingot:0, gold_ingot:0, copper_ingot:0, - diamond_pickaxe:0, diamond_sword:0, hoe:0, - wheat:0, bread:0, carrot:0, potato:0, baked_potato:0, - scorpion_stinger:0, polar_fur:0, slime_ball:0, eagle_feather:0, - snow:0, ice:0, cactus:0, mushroom:0, moss:0, farmland:0, - spruce_leaves:0, dead_bush:0, - wheat_stage0:0, carrot_stage0:0, potato_stage0:0 - }; - let selected = 'dirt'; - - // Прочность инструментов: Map<"tooltype_id", {current, max}> - // При крафте инструмента создаём запись с max durability - const toolDurability = new Map(); - - function addTool(type) { - const def = TOOLS[type]; - if (!def) return; - const id = `${type}_${Date.now()}_${Math.random().toString(36).slice(2,6)}`; - toolDurability.set(id, { type, current: def.durability, max: def.durability }); - return id; + + // src/physics/water-detect.js + function isWaterAt(px, py) { + const gx = Math.floor(px / TILE); + const gy = Math.floor(py / TILE); + const b = getBlock(gx, gy); + return !!(b && b.t === "water"); } - - function getToolDurability(id) { - return toolDurability.get(id); + function updateWaterFlag(e) { + const cx = e.x + e.w / 2; + const wasInWater = e.inWater; + const mid = isWaterAt(cx, e.y + e.h / 2); + const feet = isWaterAt(cx, e.y + e.h - 2); + e.inWater = mid || feet; + e.headInWater = isWaterAt(cx, e.y + 4); + if (e === state.player && !wasInWater && e.inWater && e.vy > 100) { + playSound("splash"); + } } - - // Найти лучший инструмент данного типа в инвентаре - function findBestTool(toolType) { - if (inv[toolType] <= 0) return null; - // Возвращаем первый попавшийся — упрощённо - return toolType; - } - - // Использовать инструмент (уменьшить прочность). Возвращает true если сломался - function useTool(toolType) { - // Ищем любой инструмент этого типа с прочностью - for (const [id, dur] of toolDurability) { - if (dur.type === toolType) { - dur.current--; - if (dur.current <= 0) { - toolDurability.delete(id); - inv[toolType]--; - rebuildHotbar(); - return true; // сломался + + // src/physics/collision.js + function resolveY(e) { + e.grounded = false; + const x1 = e.x + 2; + const x2 = e.x + e.w - 2; + const cx = e.x + e.w / 2; + const cy = e.y + e.h / 2; + const gx = Math.floor(cx / TILE); + const gy = Math.floor(cy / TILE); + const b = getBlock(gx, gy); + const onLadder = b && BLOCKS[b.t] && BLOCKS[b.t].climbable; + if (onLadder) { + e.grounded = true; + if (state.inp.jump) { + e.vy = -200; + } else if (state.inp.down) { + e.vy = 100; + } else { + e.vy = 0; + } + return; + } + const leftGX = Math.floor((e.x - 4) / TILE); + const rightGX = Math.floor((e.x + e.w + 4) / TILE); + const playerGY = Math.floor((e.y + e.h / 2) / TILE); + const leftBlock = getBlock(leftGX, playerGY); + const rightBlock = getBlock(rightGX, playerGY); + const leftLadder = leftBlock && BLOCKS[leftBlock.t] && BLOCKS[leftBlock.t].climbable; + const rightLadder = rightBlock && BLOCKS[rightBlock.t] && BLOCKS[rightBlock.t].climbable; + if ((leftLadder || rightLadder) && state.inp.jump && e.vy < 0) { + if (leftLadder && e.x > leftGX * TILE + TILE / 2) { + e.x = leftGX * TILE + TILE / 2 - e.w / 2; + } else if (rightLadder && e.x < rightGX * TILE + TILE / 2) { + e.x = rightGX * TILE + TILE / 2 - e.w / 2; + } + e.grounded = true; + e.vy = -150; + return; + } + if (e.vy >= 0) { + const probeY = e.y + e.h + 1; + const gy2 = Math.floor(probeY / TILE); + const gxA = Math.floor(x1 / TILE); + const gxB = Math.floor(x2 / TILE); + if (isSolid(gxA, gy2) || isSolid(gxB, gy2)) { + e.y = gy2 * TILE - e.h; + e.vy = 0; + e.grounded = true; + if (e === state.player && !state.player.inWater) { + const fallTiles = (e.y - e.fallStartY) / TILE; + if (fallTiles > 6) { + const damage = calculateDamage((fallTiles - 6) * 10); + state.player.hp -= damage; + } } - return false; + if (e === state.player) e.fallStartY = e.y; } } - return false; - } - - const RECIPES = [ - { out:'planks', qty:4, cost:{ wood:1 }, requiredLevel:1 }, - { out:'ladder', qty:3, cost:{ planks:7 }, requiredLevel:1 }, - { out:'torch', qty:2, cost:{ coal:1, planks:1 }, requiredLevel:1 }, - { out:'glass', qty:1, cost:{ sand:3 }, requiredLevel:1 }, - { out:'brick', qty:1, cost:{ stone:2, clay:1 }, requiredLevel:1 }, - { out:'campfire', qty:1, cost:{ wood:1, coal:1 }, requiredLevel:1 }, - { out:'tnt', qty:1, cost:{ sand:2, coal:1 }, requiredLevel:8 }, - { out:'bed', qty:1, cost:{ wood: 3, planks: 3 }, requiredLevel:1 }, - { out:'boat', qty:1, cost:{ wood: 5 }, requiredLevel:2 }, - { out:'wood_pickaxe', qty:1, cost:{ wood: 3, planks: 2 }, requiredLevel:1 }, - { out:'stone_pickaxe', qty:1, cost:{ wood: 1, stone: 3 }, requiredLevel:2 }, - { out:'iron_pickaxe', qty:1, cost:{ wood: 1, iron_ore: 2 }, requiredLevel:3 }, - { out:'wood_sword', qty:1, cost:{ wood: 2, planks: 1 }, requiredLevel:1 }, - { out:'stone_sword', qty:1, cost:{ wood: 1, stone: 2 }, requiredLevel:2 }, - { out:'iron_sword', qty:1, cost:{ wood: 1, iron_ore: 1 }, requiredLevel:3 }, - { out:'iron_armor', qty:1, cost:{ iron_ore: 5 }, requiredLevel:5 }, - { out:'gold_armor', qty:1, cost:{ gold_ore: 8 }, requiredLevel:6 }, - { out:'furnace', qty:1, cost:{ stone: 8 }, requiredLevel:3 }, - { out:'bow', qty:1, cost:{ wood: 3, planks: 2 }, requiredLevel:4 }, - { out:'arrow', qty:4, cost:{ stone: 1, wood: 1 }, requiredLevel:1 }, - // === NEW RECIPES === - { out:'bread', qty:1, cost:{ wheat: 3 }, requiredLevel:1 }, - { out:'diamond_pickaxe', qty:1, cost:{ wood: 1, diamond_ore: 2 }, requiredLevel:7 }, - { out:'diamond_sword', qty:1, cost:{ wood: 1, diamond_ore: 2 }, requiredLevel:7 }, - { out:'hoe', qty:1, cost:{ wood: 2, planks: 1 }, requiredLevel:1 } - ]; - - // Рецепты печи (обжиг) - const SMELTING_RECIPES = [ - { in:'sand', qty:1, out:'glass', outQty:1, time:3 }, // песок → стекло - { in:'clay', qty:1, out:'brick', outQty:1, time:3 }, // глина → кирпич - { in:'iron_ore', qty:1, out:'iron_ingot', outQty:1, time:5 }, // железо → слиток - { in:'gold_ore', qty:1, out:'gold_ingot', outQty:1, time:5 }, // золото → слиток - { in:'copper_ore', qty:1, out:'copper_ingot', outQty:1, time:4 }, // медь → слиток - { in:'meat', qty:1, out:'cooked', outQty:1, time:2 }, // сырое мясо → жареное - { in:'cobblestone', qty:1, out:'stone', outQty:1, time:2 }, // булыжник → камень - { in:'potato', qty:1, out:'baked_potato', outQty:1, time:2 } // картофель → печёная картошка - ]; - - // Новые предметы от обжига - ITEMS.iron_ingot = { n:'Железный слиток', icon:'🔩' }; - ITEMS.gold_ingot = { n:'Золотой слиток', icon:'🪙' }; - ITEMS.copper_ingot = { n:'Медный слиток', icon:'🟤' }; - ITEMS.diamond_pickaxe = { n:'Алмазная кирка', icon:'⛏️', durability:500, miningPower:5 }; - ITEMS.diamond_sword = { n:'Алмазный меч', icon:'⚔️', durability:400, damage:18 }; - ITEMS.hoe = { n:'Мотыга', icon:'🔨', durability:80, tillTo:'farmland' }; - - // Активные печи: Map ключа блока → { recipe, progress, totalTime } - const activeFurnaces = new Map(); - - // UI - const hpEl = document.getElementById('hp'); - const foodEl = document.getElementById('food'); - const tempEl = document.getElementById("temp"); - const sxEl = document.getElementById('sx'); - const syEl = document.getElementById('sy'); - const todEl = document.getElementById('tod'); - const worldIdEl = document.getElementById('worldId'); - // Toast уведомление - function showToast(msg, duration) { - duration = duration || 1800; - const t = document.createElement('div'); - t.textContent = msg; - t.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#2ecc71;padding:10px 24px;border-radius:10px;font:bold 16px Arial,sans-serif;z-index:99999;pointer-events:none;transition:opacity 0.4s;'; - document.body.appendChild(t); - setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 400); }, duration); - } - // Клик на worldId → копировать код мира + toast - worldIdEl.addEventListener('click', () => { - const code = worldId || ''; - if (!code) return; - const text = code; // копируем только код, не URL - if (navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(text).then(() => showToast('📋 Скопировано: ' + code)).catch(() => showToast(code)); - } else { - // fallback - const ta = document.createElement('textarea'); - ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; - document.body.appendChild(ta); ta.select(); - try { document.execCommand('copy'); showToast('📋 Скопировано: ' + code); } catch(e) { showToast(code); } - ta.remove(); + if (e.vy < 0 && e === state.player) { + const gy2 = Math.floor(e.y / TILE); + const gxA = Math.floor(x1 / TILE); + const gxB = Math.floor(x2 / TILE); + if ((isSolid(gxA, gy2) || isSolid(gxB, gy2)) && !isSolid(gxA, gy2 - 1) && !isSolid(gxB, gy2 - 1)) { + e.y = (gy2 + 1) * TILE; + e.vy = 0; + e.grounded = true; + if (e === state.player) e.fallStartY = e.y; + console.log("Jumped onto block!"); + } } - }); - const playerCountEl = document.getElementById('playerCount'); - const hotbarEl = document.getElementById('hotbar'); - const craftPanel = document.getElementById('craftPanel'); - const recipesEl = document.getElementById('recipes'); - const deathEl = document.getElementById('death'); - const inventoryPanel = document.getElementById('inventoryPanel'); - const inventoryGrid = document.getElementById('inventoryGrid'); - - // ==================== МИНИКАРТА ==================== - const minimapWrap = document.getElementById('minimapWrap'); - const minimapCanvas = document.getElementById('minimap'); - const minimapCtx = minimapCanvas.getContext('2d'); - let minimapOpen = false; - - document.getElementById('mapToggle').onclick = () => { - playSound('click'); - minimapOpen = !minimapOpen; - minimapWrap.style.display = minimapOpen ? 'block' : 'none'; + if (e.vy < 0) { + const gy2 = Math.floor(e.y / TILE); + const gxA = Math.floor(x1 / TILE); + const gxB = Math.floor(x2 / TILE); + if (isSolid(gxA, gy2) || isSolid(gxB, gy2)) { + e.y = (gy2 + 1) * TILE; + e.vy = 0; + } + } + } + function resolveX(e) { + const y1 = e.y + 2; + const y2 = e.y + e.h - 2; + const cx = e.x + e.w / 2; + const cy = e.y + e.h / 2; + const gx = Math.floor(cx / TILE); + const gy = Math.floor(cy / TILE); + const b = getBlock(gx, gy); + const onLadder = b && BLOCKS[b.t] && BLOCKS[b.t].climbable; + if (e.vx > 0) { + const gx2 = Math.floor((e.x + e.w) / TILE); + const gyA = Math.floor(y1 / TILE); + const gyB = Math.floor(y2 / TILE); + const solidA = isSolid(gx2, gyA); + const solidB = isSolid(gx2, gyB); + if (solidA || solidB) { + e.x = gx2 * TILE - e.w; + e.vx = 0; + } + } else if (e.vx < 0) { + const gx2 = Math.floor(e.x / TILE); + const gyA = Math.floor(y1 / TILE); + const gyB = Math.floor(y2 / TILE); + const solidA = isSolid(gx2, gyA); + const solidB = isSolid(gx2, gyB); + if (solidA || solidB) { + e.x = (gx2 + 1) * TILE; + e.vx = 0; + } + } + } + + // src/entities/mob-ai.js + function mobAI(m, dt) { + updateWaterFlag(m); + if (m.kind === "zombie") { + const night = isNight(); + if (!night) { + m.hp -= 30 * dt; + m.vx *= 0.5; + return; + } + const dir = Math.sign(state.player.x - m.x); + m.vx = dir * m.speed; + if (m.inWater && Math.random() < 0.06) m.vy = -260; + if (Math.abs(m.x + m.w / 2 - (state.player.x + state.player.w / 2)) < 28 && Math.abs(m.y + m.h / 2 - (state.player.y + state.player.h / 2)) < 40 && state.player.invuln <= 0) { + const damage = calculateDamage(15); + state.player.hp -= damage; + state.player.invuln = 0.8; + state.player.vx += dir * 420; + state.player.vy -= 260; + playSound("hit1"); + } + } else if (m.kind === "creeper") { + const night = isNight(); + if (!night) { + m.hp -= 30 * dt; + m.vx *= 0.5; + return; + } + const dir = Math.sign(state.player.x - m.x); + const dist = Math.hypot(state.player.x + state.player.w / 2 - (m.x + m.w / 2), state.player.y + state.player.h / 2 - (m.y + m.h / 2)); + m.vx = dir * m.speed; + if (m.inWater && Math.random() < 0.06) m.vy = -260; + if (dist < 60) { + m.fuse -= dt; + if (m.fuse <= 0) { + explodeAt(Math.floor((m.x + m.w / 2) / TILE), Math.floor((m.y + m.h / 2) / TILE)); + m.hp = 0; + } + } else { + if (dist < 40) { + m.fuse = 0.5; + } + } + } else if (m.kind === "skeleton") { + const night = isNight(); + if (!night) { + m.hp -= 30 * dt; + m.vx *= 0.5; + return; + } + const dir = Math.sign(state.player.x - m.x); + const dist = Math.hypot(state.player.x + state.player.w / 2 - (m.x + m.w / 2), state.player.y + state.player.h / 2 - (m.y + m.h / 2)); + m.vx = dir * m.speed; + if (m.inWater && Math.random() < 0.06) m.vy = -260; + m.shootCooldown -= dt; + if (dist < 300 && m.shootCooldown <= 0) { + m.shootCooldown = 2; + const dx = state.player.x + state.player.w / 2 - (m.x + m.w / 2); + const dy = state.player.y + state.player.h / 2 - (m.y + m.h / 2); + const angle = Math.atan2(dy, dx); + const speed = 450; + state.projectiles.push({ + x: m.x + m.w / 2, + y: m.y + m.h / 3, + vx: Math.cos(angle) * speed, + vy: Math.sin(angle) * speed, + dmg: 6, + owner: "mob", + life: 3 + }); + } + } else { + m.aiT -= dt; + if (m.aiT <= 0) { + m.aiT = 1.8 + Math.random() * 2.5; + m.dir = Math.random() < 0.5 ? -1 : 1; + if (Math.random() < 0.25) m.dir = 0; + } + m.vx = m.dir * (m.kind === "chicken" ? 55 : 40); + if (m.inWater) m.vy = -120; + } + const g = m.inWater ? GRAV_WATER : GRAV; + m.vy += g * dt; + m.y += m.vy * dt; + m.grounded = false; + resolveY(m); + m.x += m.vx * dt; + resolveX(m); + } + function isNight() { + return state.worldTime > 0.5; + } + + // src/ui/furnace.js + var furnacePanel = document.getElementById("furnacePanel"); + var furnaceContent = document.getElementById("furnaceContent"); + function initFurnace() { + document.getElementById("furnaceClose").onclick = () => { + furnacePanel.style.display = "none"; + state.currentFurnaceKey = null; + }; + } + function openFurnaceUI(gx, gy) { + state.currentFurnaceKey = `${gx},${gy}`; + furnacePanel.style.display = "block"; + renderFurnaceUI(); + } + function renderFurnaceUI() { + if (!state.currentFurnaceKey) return; + const [fgx, fgy] = state.currentFurnaceKey.split(",").map(Number); + const fb = getBlock(fgx, fgy); + if (!fb || fb.t !== "furnace") { + furnacePanel.style.display = "none"; + state.currentFurnaceKey = null; + return; + } + const active = state.activeFurnaces.get(state.currentFurnaceKey); + let html = '
'; + for (let i = 0; i < SMELTING_RECIPES.length; i++) { + const recipe = SMELTING_RECIPES[i]; + const haveCount = state.inv[recipe.in] || 0; + const canSmelt = haveCount >= recipe.qty; + const outDef = BLOCKS[recipe.out]; + const outItem = ITEMS[recipe.out]; + const iconStr = outItem ? outItem.icon : outDef ? "\u{1F9F1}" : "\u2753"; + const nameStr = outItem ? outItem.n : outDef ? outDef.n : recipe.out; + const inItem = ITEMS[recipe.in]; + const inDef = BLOCKS[recipe.in]; + const inName = inItem ? inItem.n : inDef ? inDef.n : recipe.in; + html += `
`; + html += `
${iconStr}
`; + html += `
`; + html += `
${nameStr}
`; + html += `
${inName} x${recipe.qty} (\u0435\u0441\u0442\u044C: ${haveCount}) \u2022 ${recipe.time}\u0441
`; + html += `
`; + html += ``; + html += `
`; + } + if (active) { + const pct = Math.min(100, Math.floor(active.progress / active.recipe.time * 100)); + html += `
`; + html += `
\u{1F525} \u041E\u0431\u0436\u0438\u0433: ${pct}%
`; + html += `
`; + html += `
`; + html += `
`; + } + html += "
"; + furnaceContent.innerHTML = html; + } + window._smelt = (recipeIdx) => { + if (!state.currentFurnaceKey) return; + const recipe = SMELTING_RECIPES[recipeIdx]; + if ((state.inv[recipe.in] || 0) < recipe.qty) return; + if (state.activeFurnaces.has(state.currentFurnaceKey)) return; + state.inv[recipe.in] -= recipe.qty; + state.activeFurnaces.set(state.currentFurnaceKey, { + recipe, + progress: 0 + }); + playSound("fire"); + rebuildHotbar(); + renderFurnaceUI(); }; - - // Цвета блоков для миникарты (по 1 пикселю на блок) - const MINIMAP_COLORS = { - grass: '#4a8c2a', dirt: '#6b3410', stone: '#6a6a6a', sand: '#d4b84a', - gravel: '#888', clay: '#5a9ad4', wood: '#a63d00', planks: '#c46a10', - leaves: '#2a8a3a', glass: '#aaddee', water: '#2980b9', coal: '#1a1a2a', - copper_ore: '#a05535', iron_ore: '#b0b0b0', gold_ore: '#d4a017', - diamond_ore: '#0090d0', brick: '#8a2010', tnt: '#c02020', - campfire: '#d08020', torch: '#d0a020', bedrock: '#1a1a1a', - flower: '#d03050', bed: '#c03060', ladder: '#a04000', boat: '#6b3410', - snow: '#ecf0f1', ice: '#74b9ff', cactus: '#27ae60', mushroom: '#e74c3c', - moss: '#0a6640', swamp_water: '#687828', farmland: '#8B6914', - dead_bush: '#b2bec3', spruce_leaves: '#0a6640', - wheat_stage0: '#a8e6a0', wheat_stage1: '#7dcea0', wheat_stage2: '#b8d730', wheat_stage3: '#f1c40f', - carrot_stage0: '#a8e6a0', carrot_stage1: '#f0c27a', carrot_stage2: '#e8a040', carrot_stage3: '#e67e22', - potato_stage0: '#a8e6a0', potato_stage1: '#c8b888', potato_stage2: '#bfaa78', potato_stage3: '#dfe6e9' + function tickFurnaces(dt) { + for (const [key, furnace] of state.activeFurnaces) { + furnace.progress += dt; + if (furnace.progress >= furnace.recipe.time) { + const outItem = furnace.recipe.out; + if (ITEMS[outItem]) { + state.inv[outItem] = (state.inv[outItem] || 0) + furnace.recipe.outQty; + } else if (BLOCKS[outItem]) { + state.inv[outItem] = (state.inv[outItem] || 0) + furnace.recipe.outQty; + } + playSound("stone_build"); + state.activeFurnaces.delete(key); + if (key === state.currentFurnaceKey) { + renderFurnaceUI(); + } + } + } + } + + // src/world/weather.js + function updateWeather(dt) { + state.weatherTimer += dt; + if (state.weatherTimer >= state.weatherChangeInterval) { + state.weatherTimer = 0; + state.weatherChangeInterval = 60 + Math.random() * 120; + const nightChance = isNight() ? 0.25 : 0.4; + state.isRaining = Math.random() < nightChance; + } + const target = state.isRaining ? 0.4 + Math.random() * 0.01 : 0; + state.rainIntensity += (target - state.rainIntensity) * dt * 0.5; + if (state.rainIntensity < 0.01) state.rainIntensity = 0; + } + function updateRain(dt) { + if (!state.isRaining || state.rainIntensity < 0.01) { + state.raindrops.length = 0; + return; + } + const spawnRate = Math.floor(state.rainIntensity * 60 * dt); + for (let i = 0; i < spawnRate && state.raindrops.length < state.MAX_RAINDROPS; i++) { + state.raindrops.push({ + x: state.camX + Math.random() * state.W, + y: state.camY - 20, + vy: 400 + Math.random() * 200, + len: 8 + Math.random() * 12 + }); + } + for (let i = state.raindrops.length - 1; i >= 0; i--) { + const d = state.raindrops[i]; + d.y += d.vy * dt; + d.x -= 30 * dt; + if (d.y > state.camY + state.H + 20) { + state.raindrops.splice(i, 1); + } + } + } + function drawRain() { + if (state.raindrops.length === 0) return; + const ctx = state.ctx; + ctx.save(); + ctx.strokeStyle = "rgba(174,194,224,0.5)"; + ctx.lineWidth = 1.5; + ctx.beginPath(); + for (const d of state.raindrops) { + ctx.moveTo(d.x, d.y); + ctx.lineTo(d.x - 3, d.y + d.len); + } + ctx.stroke(); + ctx.restore(); + } + + // src/entities/mobs.js + var Entity = class { + constructor(x, y, w, h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + this.vx = 0; + this.vy = 0; + this.hp = 3; + this.grounded = false; + this.inWater = false; + this.aiT = 0; + this.dir = 1; + } }; - + var Pig = class extends Entity { + constructor(x, y) { + super(x, y, 34, 24); + this.kind = "pig"; + this.hp = 2; + } + }; + var Chicken = class extends Entity { + constructor(x, y) { + super(x, y, 26, 22); + this.kind = "chicken"; + this.hp = 1; + } + }; + var Zombie = class extends Entity { + constructor(x, y) { + super(x, y, 34, 50); + this.kind = "zombie"; + this.hp = 4; + this.speed = 80 + Math.random() * 40; + } + }; + var Creeper = class extends Entity { + constructor(x, y) { + super(x, y, 34, 50); + this.kind = "creeper"; + this.hp = 4; + this.speed = 60 + Math.random() * 30; + this.fuse = 3.2; + } + }; + var Skeleton = class extends Entity { + constructor(x, y) { + super(x, y, 34, 50); + this.kind = "skeleton"; + this.hp = 4; + this.speed = 70 + Math.random() * 30; + this.shootCooldown = 0; + } + }; + + // src/ui/minimap.js + var minimapWrap = document.getElementById("minimapWrap"); + var minimapCanvas = document.getElementById("minimap"); + var minimapCtx = minimapCanvas.getContext("2d"); + var MINIMAP_COLORS = { + grass: "#4a8c2a", + dirt: "#6b3410", + stone: "#6a6a6a", + sand: "#d4b84a", + gravel: "#888", + clay: "#5a9ad4", + wood: "#a63d00", + planks: "#c46a10", + leaves: "#2a8a3a", + glass: "#aaddee", + water: "#2980b9", + coal: "#1a1a2a", + copper_ore: "#a05535", + iron_ore: "#b0b0b0", + gold_ore: "#d4a017", + diamond_ore: "#0090d0", + brick: "#8a2010", + tnt: "#c02020", + campfire: "#d08020", + torch: "#d0a020", + bedrock: "#1a1a1a", + flower: "#d03050", + bed: "#c03060", + ladder: "#a04000", + boat: "#6b3410" + }; + function initMinimap() { + document.getElementById("mapToggle").onclick = () => { + playSound("click"); + state.minimapOpen = !state.minimapOpen; + minimapWrap.style.display = state.minimapOpen ? "block" : "none"; + }; + } function renderMinimap() { - if (!minimapOpen) return; + if (!state.minimapOpen) return; const mW = minimapCanvas.width; const mH = minimapCanvas.height; - const scale = 2; // пикселей на блок - - // Область карты — центрирована на игроке - const pGX = Math.floor(player.x / TILE); - const biome = getCachedBiome(pGX); - const pGY = Math.floor(player.y / TILE); + const scale = 2; + const TILE2 = state.TILE; + const player = state.player; + const pGX = Math.floor(player.x / TILE2); + const pGY = Math.floor(player.y / TILE2); const viewW = Math.floor(mW / scale); const viewH = Math.floor(mH / scale); const startGX = pGX - Math.floor(viewW / 2); const startGY = pGY - Math.floor(viewH / 2); - - // Очищаем - minimapCtx.fillStyle = isNight() ? '#070816' : '#87CEEB'; + minimapCtx.fillStyle = isNight() ? "#070816" : "#87CEEB"; minimapCtx.fillRect(0, 0, mW, mH); - - // Рисуем блоки const imgData = minimapCtx.createImageData(mW, mH); const data = imgData.data; - for (let dx = 0; dx < viewW; dx++) { for (let dy = 0; dy < viewH; dy++) { const gx = startGX + dx; const gy = startGY + dy; const b = getBlock(gx, gy); - if (!b || b.dead || b.t === 'air') continue; - + if (!b || b.dead || b.t === "air") continue; const color = MINIMAP_COLORS[b.t]; if (!color) continue; - - // Парсим hex цвет - const r = parseInt(color.slice(1,3), 16); - const g = parseInt(color.slice(3,5), 16); - const bl = parseInt(color.slice(5,7), 16); - - // Заполняем scale x scale пикселей + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const bl = parseInt(color.slice(5, 7), 16); for (let sx = 0; sx < scale; sx++) { for (let sy = 0; sy < scale; sy++) { const px = dx * scale + sx; @@ -1807,3876 +2373,1564 @@ function customConfirm(msg, onYes) { if (px >= mW || py >= mH) continue; const idx = (py * mW + px) * 4; data[idx] = r; - data[idx+1] = g; - data[idx+2] = bl; - data[idx+3] = 255; + data[idx + 1] = g; + data[idx + 2] = bl; + data[idx + 3] = 255; } } } } - minimapCtx.putImageData(imgData, 0, 0); - - // Игрок — белый пиксель по центру - minimapCtx.fillStyle = '#fff'; - minimapCtx.fillRect(Math.floor(mW/2) - 2, Math.floor(mH/2) - 2, 4, 4); - - // Другие игроки — жёлтые точки - for (const [sid, p] of otherPlayers) { - const dx = Math.floor(p.x / TILE) - startGX; - const dy = Math.floor(p.y / TILE) - startGY; + minimapCtx.fillStyle = "#fff"; + minimapCtx.fillRect(Math.floor(mW / 2) - 2, Math.floor(mH / 2) - 2, 4, 4); + for (const [sid, p] of state.otherPlayers) { + const dx = Math.floor(p.x / TILE2) - startGX; + const dy = Math.floor(p.y / TILE2) - startGY; if (dx >= 0 && dx < viewW && dy >= 0 && dy < viewH) { - minimapCtx.fillStyle = '#f1c40f'; + minimapCtx.fillStyle = "#f1c40f"; minimapCtx.fillRect(dx * scale - 1, dy * scale - 1, 3, 3); } } - - // Мобы — красные (враждебные) / зелёные (животные) - const allMobsForMap = getAllMobs(); + const allMobsForMap = state.isMultiplayer ? Array.from(state.serverMobs.values()) : state.mobs; for (const m of allMobsForMap) { - const dx = Math.floor(m.x / TILE) - startGX; - const dy = Math.floor(m.y / TILE) - startGY; + const dx = Math.floor(m.x / TILE2) - startGX; + const dy = Math.floor(m.y / TILE2) - startGY; if (dx >= 0 && dx < viewW && dy >= 0 && dy < viewH) { - const hostile = m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton'; - minimapCtx.fillStyle = hostile ? '#e74c3c' : '#2ecc71'; + const hostile = m.kind === "zombie" || m.kind === "creeper" || m.kind === "skeleton"; + minimapCtx.fillStyle = hostile ? "#e74c3c" : "#2ecc71"; minimapCtx.fillRect(dx * scale, dy * scale, 2, 2); } } } - - // ==================== ПЕЧЬ (ОБЖИГ) ==================== - const furnacePanel = document.getElementById('furnacePanel'); - const furnaceContent = document.getElementById('furnaceContent'); - let currentFurnaceKey = null; // "gx,gy" текущей открытой печи - - document.getElementById('furnaceClose').onclick = () => { - furnacePanel.style.display = 'none'; - currentFurnaceKey = null; - }; - - function openFurnaceUI(gx, gy) { - currentFurnaceKey = `${gx},${gy}`; - furnacePanel.style.display = 'block'; - renderFurnaceUI(); - } - - function renderFurnaceUI() { - if (!currentFurnaceKey) return; - - // Проверяем что печь всё ещё существует - const [fgx, fgy] = currentFurnaceKey.split(',').map(Number); - const fb = getBlock(fgx, fgy); - if (!fb || fb.t !== 'furnace') { - furnacePanel.style.display = 'none'; - currentFurnaceKey = null; - return; - } - - // Текущий процесс обжига - const active = activeFurnaces.get(currentFurnaceKey); - - let html = '
'; - - // Доступные рецепты — показываем только те, для которых есть ресурсы - for (let i = 0; i < SMELTING_RECIPES.length; i++) { - const recipe = SMELTING_RECIPES[i]; - const haveCount = inv[recipe.in] || 0; - const canSmelt = haveCount >= recipe.qty; - - // Иконка результата - const outDef = BLOCKS[recipe.out]; - const outItem = ITEMS[recipe.out]; - const iconStr = outItem ? outItem.icon : (outDef ? '🧱' : '❓'); - const nameStr = outItem ? outItem.n : (outDef ? outDef.n : recipe.out); - const inItem = ITEMS[recipe.in]; - const inDef = BLOCKS[recipe.in]; - const inName = inItem ? inItem.n : (inDef ? inDef.n : recipe.in); - - html += `
`; - html += `
${iconStr}
`; - html += `
`; - html += `
${nameStr}
`; - html += `
${inName} x${recipe.qty} (есть: ${haveCount}) • ${recipe.time}с
`; - html += `
`; - html += ``; - html += `
`; - } - - // Текущий прогресс - if (active) { - const pct = Math.min(100, Math.floor((active.progress / active.recipe.time) * 100)); - html += `
`; - html += `
🔥 Обжиг: ${pct}%
`; - html += `
`; - html += `
`; - html += `
`; - } - - html += '
'; - furnaceContent.innerHTML = html; - } - - // Глобальная функция для кнопки обжига - window._smelt = (recipeIdx) => { - if (!currentFurnaceKey) return; - const recipe = SMELTING_RECIPES[recipeIdx]; - if ((inv[recipe.in] || 0) < recipe.qty) return; - - // Уже обжигаем в этой печи? - if (activeFurnaces.has(currentFurnaceKey)) return; - - // Забираем ресурсы - inv[recipe.in] -= recipe.qty; - - // Запускаем обжиг - activeFurnaces.set(currentFurnaceKey, { - recipe: recipe, - progress: 0 - }); - - playSound('fire'); - rebuildHotbar(); - renderFurnaceUI(); - }; - - // Тик печей — вызывается в главном цикле - function tickFurnaces(dt) { - for (const [key, furnace] of activeFurnaces) { - furnace.progress += dt; - if (furnace.progress >= furnace.recipe.time) { - // Обжиг завершён — выдаём результат - const outItem = furnace.recipe.out; - if (ITEMS[outItem]) { - inv[outItem] = (inv[outItem] || 0) + furnace.recipe.outQty; - } else if (BLOCKS[outItem]) { - inv[outItem] = (inv[outItem] || 0) + furnace.recipe.outQty; - } - playSound('stone_build'); - activeFurnaces.delete(key); - - // Если эта печь открыта — обновляем UI - if (key === currentFurnaceKey) { - renderFurnaceUI(); - } - } - } - } - - - // ==================== ГОЛОСОВОЙ ЧАТ v3 ==================== - // ==================== ГОЛОСОВОЙ ЧАТ v3 ==================== -// Per-speaker architecture, AudioWorklet, VAD, Opus/PCM, spatial audio -// Replaces lines 1449-1665 in game.js - - let voiceSocket = null; - let voiceStream = null; - let audioCtx = null; - let voiceActive = false; - let voiceMode = 'near'; - const VOICE_SERVER = 'https://voicegrech.mkn8n.ru'; - - // Voice config - const VC = { - sampleRate: 16000, // 16kHz — sufficient for voice, saves 33% bandwidth - frameMs: 20, // 20ms frames = 320 samples @ 16kHz - samplesPerFrame: 320, // 16000 * 0.02 - vadThreshold: 0.0005, // RMS threshold for voice detection - vadHangover: 5, // 100ms hangover after speech ends - jbufTargetMs: 80, // Target jitter: 80ms (was 200ms) - jbufMinMs: 40, - jbufMaxMs: 200, - maxSpeakers: 6, - voiceRadius: 600, - opusBitrate: 16000, - posUpdateMs: 200, // Update position every 200ms (was 500ms) - }; - - // Codec state - let voiceCodec = 'pcm'; // 'opus' or 'pcm' - let voiceEncoder = null; // WebCodecs AudioEncoder - let voiceSeq = 0; - let voiceTimestamp = 0; - let wasSpeaking = false; - let silenceFrames = 0; - let captureNode = null; - let playbackNode = null; - - // Per-speaker map - const remoteSpeakers = new Map(); // socketId → { jitterBuf, lastFrame, gain, panner, lowpass, decoder, codec, x, y, name, mode, speaking, lastActive } - - // ==================== WORKLET CODE (inline strings) ==================== - - const voiceCaptureWorkletCode = ` -class VoiceCaptureProcessor extends AudioWorkletProcessor { - constructor() { - super(); - this._buf = new Float32Array(320); // 20ms @ 16kHz - this._pos = 0; - this._speaking = false; - this._silenceFrames = 0; - this._hangover = 5; // 100ms - this._vadThreshold = 0.0005; - this._lastRms = 0; + // src/game/modes.js + var MODES = [{ id: "mine", icon: "\u26CF\uFE0F" }, { id: "build", icon: "\u{1F9F1}" }]; + function mode() { + return MODES[state.modeIdx].id; } - process(inputs) { - const input = inputs[0]; - if (!input || !input[0]) return true; - const ch = input[0]; - let i = 0; - while (i < ch.length) { - const rem = this._buf.length - this._pos; - const n = Math.min(rem, ch.length - i); - this._buf.set(ch.subarray(i, i + n), this._pos); - this._pos += n; - i += n; - if (this._pos >= this._buf.length) { - this._processFrame(); - this._pos = 0; - } - } - return true; - } - _processFrame() { - let sum = 0; - for (let i = 0; i < this._buf.length; i++) sum += this._buf[i] * this._buf[i]; - const rms = Math.sqrt(sum / this._buf.length); - this._lastRms = rms; - - if (rms > this._vadThreshold) { - this._speaking = true; - this._silenceFrames = 0; - } else { - this._silenceFrames++; - if (this._silenceFrames > this._hangover) this._speaking = false; - } - - this.port.postMessage({ - type: 'frame', - samples: this._buf.slice(), - speaking: this._speaking, - rms: rms - }); - } -} -registerProcessor('voice-capture', VoiceCaptureProcessor); -`; - - const voicePlaybackWorkletCode = ` -class VoicePlaybackProcessor extends AudioWorkletProcessor { - constructor() { - super(); - this.speakers = new Map(); // id → { ringBuf, readPos, writePos, ready, gain, pan, lastSample, active } - this.RING_SIZE = 48000; // 3 seconds @ 16kHz - this.port.onmessage = (e) => { - const d = e.data; - if (d.type === 'addSpeaker') { - this.speakers.set(d.id, { - ringBuf: new Float32Array(this.RING_SIZE), - readPos: 0, writePos: 0, ready: 0, - gain: d.gain || 1, pan: d.pan || 0, - lastSample: 0, active: false, - jbufTarget: 320 * 4, // ~80ms in samples - fadeOut: 0 - }); - } else if (d.type === 'removeSpeaker') { - this.speakers.delete(d.id); - } else if (d.type === 'pushFrames') { - const sp = this.speakers.get(d.id); - if (!sp) return; - const frames = d.samples; // Float32Array - for (let i = 0; i < frames.length; i++) { - sp.ringBuf[sp.writePos] = frames[i]; - sp.writePos = (sp.writePos + 1) % this.RING_SIZE; - if (sp.ready < this.RING_SIZE) sp.ready++; - } - sp.active = true; - sp.fadeOut = 0; - } else if (d.type === 'updateSpatial') { - const sp = this.speakers.get(d.id); - if (sp) { - sp.gain = d.gain; - sp.pan = d.pan; - } - } + function initModes() { + const modeBtn = document.getElementById("modeBtn"); + modeBtn.onclick = () => { + playSound("click"); + state.modeIdx = (state.modeIdx + 1) % MODES.length; + modeBtn.textContent = MODES[state.modeIdx].icon; }; } - process(inputs, outputs) { - const output = outputs[0]; - if (!output || !output[0]) return true; - const left = output[0]; - const right = output[1] || output[0]; // mono fallback - - // Clear output - for (let i = 0; i < left.length; i++) { left[i] = 0; right[i] = 0; } - - for (const [id, sp] of this.speakers) { - if (sp.ready < 1 && sp.active) { - // Fade out over ~20ms (128 samples) - sp.fadeOut++; - if (sp.fadeOut > 128) { sp.active = false; continue; } - const fade = 1 - (sp.fadeOut / 128); - const g = sp.gain * fade; - const lg = g * Math.cos((sp.pan + 1) * Math.PI / 4); - const rg = g * Math.sin((sp.pan + 1) * Math.PI / 4); - // Repeat last sample with fade - for (let i = 0; i < left.length; i++) { - left[i] += sp.lastSample * lg; - right[i] += sp.lastSample * rg; - } - continue; - } - if (!sp.active && sp.ready < sp.jbufTarget) continue; // Wait for jitter buffer to fill - if (!sp.active && sp.ready >= sp.jbufTarget) sp.active = true; - if (!sp.active) continue; - - sp.fadeOut = 0; - const g = sp.gain; - // Constant-power pan law - const lg = g * Math.cos((sp.pan + 1) * Math.PI / 4); - const rg = g * Math.sin((sp.pan + 1) * Math.PI / 4); - - for (let i = 0; i < left.length; i++) { - if (sp.ready > 0) { - const s = sp.ringBuf[sp.readPos]; - sp.lastSample = s; - left[i] += s * lg; - right[i] += s * rg; - sp.readPos = (sp.readPos + 1) % this.RING_SIZE; - sp.ready--; - } else { - // Underrun — fade from last sample - sp.lastSample *= 0.85; - left[i] += sp.lastSample * lg; - right[i] += sp.lastSample * rg; - } - } - } - return true; - } -} -registerProcessor('voice-playback', VoicePlaybackProcessor); -`; - // ==================== Opus ENCODE/DECODE ==================== - - async function initVoiceEncoder() { - if (false && 'AudioEncoder' in window) { - try { - // Check if Opus is supported - const support = await AudioEncoder.isConfigSupported({ - codec: 'opus', - sampleRate: VC.sampleRate, - numberOfChannels: 1, - bitrate: VC.opusBitrate - }); - if (!support.supported) throw new Error('Opus not supported'); - - voiceEncoder = new AudioEncoder({ - output: (chunk) => { - // chunk: EncodedAudioChunk with Opus data - const data = new Uint8Array(chunk.byteLength); - chunk.copyTo(data); - // Send as binary frame via Socket.IO - console.log('[voice] TX Opus chunk, seq:', voiceSeq, 'size:', data.buffer.byteLength); - voiceSocket.emit('voice_data', { - codec: 'opus', - data: data.buffer, - seq: voiceSeq++, - ts: chunk.timestamp, - speaking: true - }); - }, - error: (e) => { - console.error('[voice] encoder error:', e); - voiceCodec = 'pcm'; - voiceEncoder = null; - } - }); - - await voiceEncoder.configure({ - codec: 'opus', - sampleRate: VC.sampleRate, - numberOfChannels: 1, - bitrate: VC.opusBitrate - }); - - voiceCodec = 'opus'; - console.log('[voice] WebCodecs Opus encoder initialized @', VC.opusBitrate, 'bps'); + // src/input/mouse-handler.js + var mouse = { x: null, y: null }; + var miningHeld = false; + var miningInterval = null; + function initMouseHandlers() { + const canvas = state.canvas; + canvas.addEventListener("pointermove", (e) => { + const r = canvas.getBoundingClientRect(); + mouse.x = e.clientX - r.left; + mouse.y = e.clientY - r.top; + }); + canvas.addEventListener("pointerdown", (e) => { + if (state.craftOpen) return; + if (state.player.hp <= 0) return; + const r = canvas.getBoundingClientRect(); + const sx = e.clientX - r.left; + const sy = e.clientY - r.top; + const wx = sx + state.camX; + const wy = sy + state.camY; + const gx = Math.floor(wx / state.TILE); + const gy = Math.floor(wy / state.TILE); + const b = getBlock(gx, gy); + if (state.player.sleeping && b && b.t === "bed") { + state.player.sleeping = false; return; - } catch (e) { - console.warn('[voice] WebCodecs Opus not available, PCM fallback:', e.message); } - } - voiceCodec = 'pcm'; - console.log('[voice] Using PCM @', VC.sampleRate, 'Hz'); - } - - async function initDecoderForSpeaker(speakerId, codec) { - const sp = remoteSpeakers.get(speakerId); - if (!sp) return; - - if (codec === 'opus' && 'AudioDecoder' in window) { - try { - const decoder = new AudioDecoder({ - output: (audioData) => { - const samples = new Float32Array(audioData.numberOfFrames); - audioData.copyTo(samples, { planeIndex: 0 }); - audioData.close(); - // Push decoded PCM to worklet - if (playbackNode) { - playbackNode.port.postMessage({ - type: 'pushFrames', - id: speakerId, - samples: samples - }); + if (state.player.sleeping) return; + if (b && b.t === "furnace" && mode() === "mine") { + openFurnaceUI(gx, gy); + return; + } + if (mode() === "mine") { + if (state.isMultiplayer) { + for (const [id, sm] of state.serverMobs) { + if (sm.dead) continue; + if (wx >= sm.x && wx <= sm.x + sm.w && wy >= sm.y && wy <= sm.y + sm.h) { + let dmg = 1; + const swordTypes = ["iron_sword", "stone_sword", "wood_sword"]; + for (const st of swordTypes) { + if (state.inv[st] > 0) { + dmg = TOOLS[st].damage || 3; + useTool(st); + break; + } + } + state.socket.emit("mob_hurt", { id: sm.id, dmg }); + playSound("attack"); + return; } - }, - error: (e) => { - console.error('[voice] decoder error for', speakerId, e); - sp.codec = 'pcm'; // Fallback to PCM - } - }); - - await decoder.configure({ - codec: 'opus', - sampleRate: VC.sampleRate, - numberOfChannels: 1 - }); - - sp.decoder = decoder; - sp.codec = 'opus'; - console.log('[voice] Opus decoder initialized for', speakerId); - } catch (e) { - console.warn('[voice] Opus decoder failed, PCM fallback:', e.message); - sp.codec = 'pcm'; - } - } else { - sp.codec = 'pcm'; - } - } - - // ==================== RemoteSpeaker helper ==================== - - function createRemoteSpeaker(socketId, name, codec) { - const sp = { - id: socketId, - name: name || '???', - codec: codec || 'pcm', - decoder: null, - x: 0, y: 0, - mode: 'near', - speaking: false, - lastActive: Date.now(), - // Audio nodes per speaker (managed on main thread for smooth ramps) - gainNode: audioCtx.createGain(), - pannerNode: audioCtx.createStereoPanner(), - lowpassNode: audioCtx.createBiquadFilter(), - }; - // lowpass for distance muffling - sp.lowpassNode.type = 'lowpass'; - sp.lowpassNode.frequency.value = 4000; - sp.lowpassNode.Q.value = 0.7; - // Connect: lowpass → panner → gain → (connected to mixer later) - sp.lowpassNode.connect(sp.pannerNode); - sp.pannerNode.connect(sp.gainNode); - sp.gainNode.gain.value = 1.0; // Start with volume (worklet needs non-zero gain) - - remoteSpeakers.set(socketId, sp); - // Register speaker in playback worklet - if (playbackNode) { - playbackNode.port.postMessage({ type: 'addSpeaker', id: socketId, gain: 1.0, pan: 0 }); - console.log('[voice] addSpeaker posted to worklet, id:', socketId?.substring(0,8), 'gain:', sp.gainNode.gain.value); - } - initDecoderForSpeaker(socketId, codec); - return sp; - } - - function removeRemoteSpeaker(socketId) { - const sp = remoteSpeakers.get(socketId); - if (!sp) return; - try { sp.gainNode.disconnect(); } catch(e) {} - try { sp.pannerNode.disconnect(); } catch(e) {} - try { sp.lowpassNode.disconnect(); } catch(e) {} - if (sp.decoder) { try { sp.decoder.close(); } catch(e) {} } - remoteSpeakers.delete(socketId); - if (playbackNode) { - playbackNode.port.postMessage({ type: 'removeSpeaker', id: socketId }); - } - } - - function updateSpeakerSpatial(sp) { - if (!audioCtx) return; - // Calculate distance-based audio - const dx = sp.x - player.x; - const dy = sp.y - player.y; - const dist = Math.sqrt(dx * dx + dy * dy); - - const now = audioCtx.currentTime; - const rampTime = 0.05; // 50ms smooth ramp - - if (sp.mode === 'world' && voiceMode === 'world') { - // World mode: full volume, no panning, no distance filter - sp.gainNode.gain.cancelScheduledValues(now); - sp.gainNode.gain.linearRampToValueAtTime(1.0, now + rampTime); - sp.pannerNode.pan.cancelScheduledValues(now); - sp.pannerNode.pan.linearRampToValueAtTime(0, now + rampTime); - sp.lowpassNode.frequency.cancelScheduledValues(now); - sp.lowpassNode.frequency.linearRampToValueAtTime(4000, now + rampTime); - } else if (dist > VC.voiceRadius) { - // Out of range - sp.gainNode.gain.cancelScheduledValues(now); - sp.gainNode.gain.linearRampToValueAtTime(0, now + rampTime); - } else { - // Near mode with spatial audio - const volume = 1 / (1 + dist / 150); // Perceptual falloff - const pan = Math.max(-1, Math.min(1, dx / 300)); // Stereo pan based on X - const cutoff = 4000 - (3800 * (dist / VC.voiceRadius)); // Muffle at distance - - sp.gainNode.gain.cancelScheduledValues(now); - sp.gainNode.gain.linearRampToValueAtTime(volume, now + rampTime); - sp.pannerNode.pan.cancelScheduledValues(now); - sp.pannerNode.pan.linearRampToValueAtTime(pan, now + rampTime); - sp.lowpassNode.frequency.cancelScheduledValues(now); - sp.lowpassNode.frequency.linearRampToValueAtTime(Math.max(200, cutoff), now + rampTime); - } - - // Update worklet spatial params - if (playbackNode) { - const currentGain = sp.gainNode.gain.value; - const currentPan = sp.pannerNode.pan.value; - if (currentGain > 0.01) console.log('[voice] updateSpatial, id:', sp.id?.substring(0,8), 'gain:', currentGain.toFixed(3), 'pan:', currentPan.toFixed(3)); - playbackNode.port.postMessage({ - type: 'updateSpatial', - id: sp.id, - gain: currentGain, - pan: currentPan - }); - } - } - - // ==================== UI ==================== - - const voiceBtn = document.createElement('div'); - voiceBtn.innerHTML = '🎤/'; - voiceBtn.title = 'Голосовой чат (выкл)'; - voiceBtn.style.cssText = 'position:absolute;top:74px;right:130px;width:52px;height:52px;border-radius:12px;background:#555;z-index:200;display:flex;align-items:center;justify-content:center;font-size:24px;cursor:pointer;border:2px solid rgba(255,255,255,0.9);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;'; - document.querySelector('.ui').appendChild(voiceBtn); - - const voiceModeBtn = document.createElement('div'); - voiceModeBtn.innerHTML = '📢'; - voiceModeBtn.title = 'Режим: рядом (600px)'; - voiceModeBtn.style.cssText = 'position:absolute;top:74px;right:190px;width:48px;height:52px;border-radius:12px;background:#3498db;z-index:200;display:flex;align-items:center;justify-content:center;font-size:20px;cursor:pointer;border:2px solid rgba(255,255,255,0.7);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;color:#fff;font-weight:bold;'; - document.querySelector('.ui').appendChild(voiceModeBtn); - voiceModeBtn.onclick = () => { - if (voiceMode === 'near') { - voiceMode = 'world'; - voiceModeBtn.innerHTML = '🌍'; - voiceModeBtn.title = 'Режим: весь мир'; - voiceModeBtn.style.background = '#e67e22'; - } else { - voiceMode = 'near'; - voiceModeBtn.innerHTML = '📢'; - voiceModeBtn.title = 'Режим: рядом (600px)'; - voiceModeBtn.style.background = '#3498db'; - } - if (voiceSocket && voiceSocket.connected) { - voiceSocket.emit('voice_mode', { mode: voiceMode }); - } - // Update spatial for all speakers - for (const [id, sp] of remoteSpeakers) updateSpeakerSpatial(sp); - }; - - // Speaking indicators per players - const speakingIndicator = document.createElement('div'); - speakingIndicator.style.cssText = 'position:absolute;top:130px;right:10px;z-index:200;display:none;background:rgba(0,0,0,0.7);color:#2ecc71;padding:4px 8px;border-radius:6px;font-size:12px;max-width:200px;line-height:1.4;'; - document.querySelector('.ui').appendChild(speakingIndicator); - let speakingTimeouts = new Map(); // socketId → timeout - - // Codec indicator - const codecIndicator = document.createElement('div'); - codecIndicator.style.cssText = 'position:absolute;top:180px;right:10px;z-index:200;font-size:10px;color:rgba(255,255,255,0.5);pointer-events:none;'; - codecIndicator.textContent = ''; - document.querySelector('.ui').appendChild(codecIndicator); - - // ==================== VOICE ON/OFF ==================== - - voiceBtn.onclick = async () => { - if (voiceActive) { - // === DISABLE === - voiceActive = false; - voiceBtn.innerHTML = '🎤/'; - voiceBtn.style.background = '#555'; - codecIndicator.textContent = ''; - - // Clean up capture - if (captureNode) { captureNode.disconnect(); captureNode = null; } - if (voiceStream) { voiceStream.getTracks().forEach(t => t.stop()); voiceStream = null; } - - // Clean up playback - if (playbackNode) { playbackNode.disconnect(); playbackNode = null; } - - // Clean up per-speaker nodes - for (const [id, sp] of remoteSpeakers) { - try { sp.gainNode.disconnect(); } catch(e) {} - try { sp.pannerNode.disconnect(); } catch(e) {} - try { sp.lowpassNode.disconnect(); } catch(e) {} - if (sp.decoder) { try { sp.decoder.close(); } catch(e) {} } - } - remoteSpeakers.clear(); - - // Clean up encoder - if (voiceEncoder) { try { voiceEncoder.close(); } catch(e) {} voiceEncoder = null; } - - if (audioCtx) { audioCtx.close(); audioCtx = null; } - if (voiceSocket) { voiceSocket.disconnect(); voiceSocket = null; } - speakingIndicator.style.display = 'none'; - return; - } - - // === ENABLE === - try { - voiceStream = await navigator.mediaDevices.getUserMedia({ - audio: { - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - echoCancellationType: 'browser', - noiseSuppressionType: 'browser', - sampleRate: { ideal: VC.sampleRate }, - channelCount: { ideal: 1 } - } - }); - - audioCtx = new AudioContext({ sampleRate: VC.sampleRate }); - if (audioCtx.state === 'suspended') await audioCtx.resume(); - console.log('[voice] AudioContext:', audioCtx.state, 'sampleRate:', audioCtx.sampleRate); - - // Register worklets via Blob URLs - const captureURL = URL.createObjectURL(new Blob([voiceCaptureWorkletCode], { type: 'application/javascript' })); - const playbackURL = URL.createObjectURL(new Blob([voicePlaybackWorkletCode], { type: 'application/javascript' })); - - await audioCtx.audioWorklet.addModule(captureURL); - await audioCtx.audioWorklet.addModule(playbackURL); - URL.revokeObjectURL(captureURL); - URL.revokeObjectURL(playbackURL); - console.log('[voice] AudioWorklets registered'); - - // Connect mic → capture worklet - const micSource = audioCtx.createMediaStreamSource(voiceStream); - captureNode = new AudioWorkletNode(audioCtx, 'voice-capture'); - micSource.connect(captureNode); - // Do NOT connect captureNode to destination — we don't hear ourselves - - // Connect playback worklet → per-speaker gain nodes → destination - playbackNode = new AudioWorkletNode(audioCtx, 'voice-playback', { numberOfOutputs: 1, outputChannelCount: [2] }); - playbackNode.connect(audioCtx.destination); - // Per-speaker nodes will connect between playback worklet output and destination - // Actually: playback worklet mixes internally → stereo output → destination - // Per-speaker Web Audio nodes (lowpass, pan, gain) used for spatial UPDATES only (values sent to worklet via messages) - // The actual mixing happens inside the worklet - - // Connect to voice server FIRST so capture frames have a socket - voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] }); - - // Handle capture frames - voiceSeq = 0; - voiceTimestamp = 0; - wasSpeaking = false; - voiceActive = true; // Enable capture BEFORE onmessage handler - let _frameCount = 0; - captureNode.port.onmessage = (e) => { - const { type, samples, speaking, rms } = e.data; - if (type !== 'frame') return; - _frameCount++; - if (_frameCount <= 5 || _frameCount % 100 === 0) { - console.log('[voice] capture frame #', _frameCount, 'rms:', rms?.toFixed(4), 'speaking:', speaking, 'voiceActive:', voiceActive, 'socket:', !!voiceSocket, 'connected:', voiceSocket?.connected, 'codec:', voiceCodec); - } - if (!voiceActive || !voiceSocket || !voiceSocket.connected) { - - return; - } - - voiceTimestamp += VC.samplesPerFrame; // 320 samples * (1/16000) = 20ms per frame - - if (!speaking && !wasSpeaking) { - // Complete silence — don't transmit - silenceFrames++; - return; - } - - if (voiceCodec === 'opus' && voiceEncoder) { - // Encode with Opus via WebCodecs - try { - const audioData = new AudioData({ - format: 'float32-planar', - sampleRate: VC.sampleRate, - numberOfFrames: samples.length, - numberOfChannels: 1, - timestamp: voiceTimestamp * (1000000 / VC.sampleRate), // microseconds - data: samples - }); - voiceEncoder.encode(audioData); - audioData.close(); - } catch (err) { - // Fallback: send as PCM - sendPCMFrame(samples, speaking, wasSpeaking); - } - } else { - sendPCMFrame(samples, speaking, wasSpeaking); - } - - wasSpeaking = speaking; - silenceFrames = 0; - }; - - function sendPCMFrame(samples, isSpeaking, wasSp) { - const int16 = new Int16Array(samples.length); - for (let i = 0; i < samples.length; i++) { - const s = Math.max(-1, Math.min(1, samples[i])); - int16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; - } - voiceSocket.emit('voice_data', { - codec: 'pcm', - data: int16.buffer, - seq: voiceSeq++, - ts: performance.now(), - speaking: isSpeaking || wasSp - }); - } - - // Initialize encoder - await initVoiceEncoder(); - codecIndicator.textContent = voiceCodec === 'opus' ? '🔊 Opus' : '🔊 PCM'; - - // Voice socket already created above - - voiceSocket.on('connect', () => { - console.log('[voice] Connected, id:', voiceSocket.id, 'codec:', voiceCodec); - voiceSocket.emit('voice_join', { - world_id: worldId, - x: player.x, y: player.y, - name: playerName || 'Игрок', - mode: voiceMode, - codec: voiceCodec - }); - }); - - voiceSocket.on('connect_error', (err) => { - console.error('[voice] Connect error:', err.message); - }); - - voiceSocket.on('voice_in', (payload) => { - console.log('[voice] RX voice_in, codec:', payload.codec, 'dataSize:', payload.data?.byteLength || payload.data?.length || 'N/A', 'from:', payload.meta?.from?.substring(0,8)); - const { data, meta } = payload; - if (!audioCtx || audioCtx.state === 'closed') { console.warn('[voice] audioCtx missing/closed'); return; } - - let sp = remoteSpeakers.get(meta.from); - if (!sp) { - // New speaker — create per-speaker audio nodes - sp = createRemoteSpeaker(meta.from, meta.name, meta.codec || 'pcm'); - } - - // Update position - sp.x = meta.x || 0; - sp.y = meta.y || 0; - sp.mode = meta.mode || 'near'; - sp.lastActive = Date.now(); - updateSpeakerSpatial(sp); - - // Decode and push to worklet - if ((meta.codec || 'pcm') === 'opus' && sp.decoder) { - // Opus decode - try { - const uint8 = new Uint8Array(data); - const chunk = new EncodedAudioChunk({ - type: 'key', // Opus frames are self-decodable - timestamp: performance.now() * 1000, // microseconds - data: uint8 - }); - sp.decoder.decode(chunk); - } catch (err) { - // Opus decode failed, try PCM fallback - decodeAndPushPCM(sp.id, data); - } - } else { - decodeAndPushPCM(sp.id, data); - } - - // Update speaking indicator - sp.speaking = true; - speakingIndicator.style.display = 'block'; - speakingIndicator.textContent = '🔊 ' + sp.name; - clearTimeout(speakingTimeouts.get(meta.from)); - speakingTimeouts.set(meta.from, setTimeout(() => { - sp.speaking = false; - // Check if anyone is still speaking - let anyoneSpeaking = false; - for (const [, s] of remoteSpeakers) { if (s.speaking) { anyoneSpeaking = true; break; } } - if (!anyoneSpeaking) speakingIndicator.style.display = 'none'; - speakingTimeouts.delete(meta.from); - }, 1500)); - }); - - voiceSocket.on('voice_leave', (data) => { - removeRemoteSpeaker(data.id); - }); - - voiceSocket.on('disconnect', () => { - console.log('[voice] Disconnected'); - // Clear all speakers on disconnect - for (const [id] of remoteSpeakers) removeRemoteSpeaker(id); - }); - - // voiceActive already set to true above - voiceBtn.textContent = '🎤'; - voiceBtn.style.background = '#2ecc71'; - console.log('[voice] Voice chat ACTIVE, codec:', voiceCodec); - - } catch(e) { - console.error('[voice] Error:', e); - voiceBtn.style.background = '#e74c3c'; - } - }; - - // ==================== PCM decode helper ==================== - - function decodeAndPushPCM(speakerId, buffer) { - console.log('[voice] decodeAndPushPCM, id:', speakerId?.substring(0,8), 'bufSize:', buffer?.byteLength || buffer?.length || 'N/A'); - const int16 = new Int16Array(buffer); - const float32 = new Float32Array(int16.length); - for (let i = 0; i < int16.length; i++) { - float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF); - } - if (playbackNode) { - playbackNode.port.postMessage({ - type: 'pushFrames', - id: speakerId, - samples: float32 - }); - } - } - - // ==================== Voice position update ==================== - - let voicePosT = 0; - - // Клик на часы для включения ночи - todEl.style.cursor = 'pointer'; - todEl.onclick = () => { - playSound('click'); - worldTime = 0.6; // Устанавливаем ночь - isNightTime = true; - }; - - function rebuildHotbar(){ - hotbarEl.innerHTML=''; - - // Показываем последние 5 выбранных предметов (если они есть в инвентаре) - const items = recentItems.filter(id => inv[id] > 0).slice(0, 5); - - for(const id of items){ - const s = document.createElement('div'); - s.className = 'slot'+(id===selected?' sel':''); - if(BLOCKS[id]) { - s.style.backgroundImage = `url(${tex[id].toDataURL()})`; - s.style.backgroundSize = 'cover'; - } else if(ITEMS[id]) { - s.textContent = ITEMS[id].icon; - } else if(TOOLS[id]) { - s.textContent = TOOLS[id].icon; - } else if(id === 'iron_armor') { - s.textContent = '🛡️'; - s.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)'; - } - const c = document.createElement('div'); - c.className='count'; - c.textContent = inv[id]; - s.appendChild(c); - s.onclick = () => { - playSound('click'); // Звук клика по инвентарю - if(selected === id) { - // Повторный клик — снимаем выбор, возвращаем к первому блоку - selected = recentItems.find(rid => BLOCKS[rid] && rid !== id) || recentItems[0] || 'dirt'; - } else { - selected = id; - // Обновляем список последних предметов - recentItems = recentItems.filter(item => item !== id); // Удаляем если уже есть - recentItems.unshift(id); // Добавляем в начало - recentItems = recentItems.slice(0, 5); // Оставляем только 5 - } - rebuildHotbar(); - }; - - // Показываем индикатор надетой брони - if(id === 'iron_armor' && player.equippedArmor === 'iron_armor') { - const equipped = document.createElement('div'); - equipped.className = 'equipped-indicator'; - equipped.textContent = '✓'; - s.appendChild(equipped); - } - - // Durability bar для инструментов - if(TOOLS[id] && inv[id] > 0) { - // Находим текущую прочность - let curDur = 0, maxDur = TOOLS[id].durability; - for (const [tid, dur] of toolDurability) { - if (dur.type === id) { - curDur = dur.current; - maxDur = dur.max; - break; } } - if (maxDur > 0) { - const bar = document.createElement('div'); - bar.style.cssText = `position:absolute;bottom:0;left:0;right:0;height:3px;background:#333;border-radius:0 0 6px 6px;overflow:hidden;`; - const fill = document.createElement('div'); - const pct = curDur / maxDur; - const color = pct > 0.5 ? '#2ecc71' : pct > 0.25 ? '#f39c12' : '#e74c3c'; - fill.style.cssText = `width:${pct*100}%;height:100%;background:${color};`; - bar.appendChild(fill); - s.appendChild(bar); - } - } - hotbarEl.appendChild(s); - } - } - - function renderInventory() { - inventoryGrid.innerHTML = ''; - - // Создаём сетку инвентаря 7x3 - const items = Object.keys(inv).filter(id => inv[id] > 0); - - // Добавляем пустые слоты для полной сетки - for(let i = 0; i < 21; i++) { - const slot = document.createElement('div'); - slot.className = 'inv-slot' + (i < items.length && items[i] === selected ? ' sel' : ''); - - if(i < items.length) { - const id = items[i]; - if(BLOCKS[id]) { - slot.style.backgroundImage = `url(${tex[id].toDataURL()})`; - slot.style.backgroundSize = 'cover'; - } else if(ITEMS[id]) { - slot.textContent = ITEMS[id].icon; - } else if(TOOLS[id]) { - slot.textContent = TOOLS[id].icon; - } else if(id === 'iron_armor') { - slot.textContent = '🛡️'; - slot.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)'; - } - - const count = document.createElement('div'); - count.className = 'inv-count'; - count.textContent = inv[id]; - slot.appendChild(count); - - slot.onclick = () => { - playSound('click'); // Звук клика по инвентарю - if(selected === id) { - selected = recentItems.find(rid => BLOCKS[rid] && rid !== id) || recentItems[0] || 'dirt'; - } else { - selected = id; - recentItems = recentItems.filter(item => item !== id); - recentItems.unshift(id); - recentItems = recentItems.slice(0, 5); - } - rebuildHotbar(); - renderInventory(); - }; - - // Двойной клик для надевания брони - slot.ondblclick = () => { - if(id === 'iron_armor' && inv.iron_armor > 0) { - // Если уже надета броня - снимаем её - if(player.equippedArmor === 'iron_armor') { - player.equippedArmor = null; - player.armor = 0; - console.log('[ARMOR] Iron armor unequipped'); - } else { - // Надеваем броню - player.equippedArmor = 'iron_armor'; - player.armor = BLOCKS['iron_armor'].armor; - console.log('[ARMOR] Iron armor equipped - armor:', player.armor); + for (let i = state.mobs.length - 1; i >= 0; i--) { + const m = state.mobs[i]; + if (wx >= m.x && wx <= m.x + m.w && wy >= m.y && wy <= m.y + m.h) { + let dmg = 1; + const swordTypes = ["iron_sword", "stone_sword", "wood_sword"]; + for (const st of swordTypes) { + if (state.inv[st] > 0) { + dmg = TOOLS[st].damage || 3; + useTool(st); + break; + } } - playSound('click'); - renderInventory(); - } - if(id === 'gold_armor' && inv.gold_armor > 0) { - if(player.equippedArmor === 'gold_armor') { - player.equippedArmor = null; - player.armor = 0; - } else { - player.equippedArmor = 'gold_armor'; - player.armor = ITEMS['gold_armor'].armor; + m.hp -= dmg; + m.vx += (m.x - state.player.x) * 2; + m.vy -= 200; + playSound("attack"); + if (m.hp <= 0) { + if (m.kind === "chicken") playSound("hurt_chicken"); + state.inv.meat += m.kind === "chicken" ? 1 : 2; + if (m.kind === "skeleton") { + state.inv.arrow += 2 + Math.floor(Math.random() * 3); + if (Math.random() < 0.15) state.inv.bow = (state.inv.bow || 0) + 1; + } + state.mobs.splice(i, 1); + rebuildHotbar(); } - playSound('click'); - renderInventory(); - } - }; - } - - inventoryGrid.appendChild(slot); - } - } - - function canCraft(r){ - console.log('[CRAFT] Checking recipe for:', r.out, '- cost:', r.cost); - for(const res in r.cost){ - const have = inv[res] || 0; - const need = r.cost[res]; - console.log('[CRAFT] Resource:', res, '- have:', have, '- need:', need, '- can craft:', have >= need); - if(have < need) return false; - } - return true; - } - function renderCraft(){ - recipesEl.innerHTML=''; - for(const r of RECIPES){ - const row = document.createElement('div'); - row.className='recipe'; - const reqLv = r.requiredLevel || 1; - const locked = player.level < reqLv; - const icon = document.createElement('div'); - icon.className='ricon'; - // Иконка — блок, инструмент или предмет - if(tex[r.out]){ - icon.style.backgroundImage = `url(${tex[r.out].toDataURL()})`; - } else if(TOOLS[r.out]){ - icon.textContent = TOOLS[r.out].icon; - icon.style.fontSize = '24px'; - icon.style.display = 'flex'; - icon.style.alignItems = 'center'; - icon.style.justifyContent = 'center'; - } else if(ITEMS[r.out]){ - icon.textContent = ITEMS[r.out].icon; - icon.style.fontSize = '24px'; - icon.style.display = 'flex'; - icon.style.alignItems = 'center'; - icon.style.justifyContent = 'center'; - } - const info = document.createElement('div'); - info.className='rinfo'; - const nm = document.createElement('div'); - nm.className='rname'; - const itemName = BLOCKS[r.out]?.n || TOOLS[r.out]?.n || ITEMS[r.out]?.n || r.out; - nm.textContent = `${itemName} x${r.qty}` + (locked ? ` (Lv.${reqLv})` : ''); - if(locked) { nm.style.color = '#888'; nm.style.textDecoration = 'line-through'; } - const cs = document.createElement('div'); - cs.className='rcost'; - cs.textContent = Object.keys(r.cost).map(x => { - const cn = BLOCKS[x]?.n || TOOLS[x]?.n || ITEMS[x]?.n || x; - return `${cn}: ${(inv[x]||0)}/${r.cost[x]}`; - }).join(' '); - if(locked) cs.style.color = '#666'; - info.appendChild(nm); info.appendChild(cs); - const btn = document.createElement('button'); - btn.className='rcraft'; - btn.textContent='Создать'; - btn.disabled = !canCraft(r) || locked; - if(locked) btn.title = `Требуется уровень ${reqLv}`; - btn.onclick = () => { - if(!canCraft(r) || locked) return; - playSound('click'); - for(const res in r.cost) inv[res]-=r.cost[res]; - inv[r.out] = (inv[r.out]||0) + r.qty; - if(TOOLS[r.out]) addTool(r.out); - rebuildHotbar(); - renderCraft(); - }; - row.appendChild(icon); row.appendChild(info); row.appendChild(btn); - recipesEl.appendChild(row); - } - } - - let craftOpen=false; - let inventoryOpen = false; - - document.getElementById('craftBtn').onclick = () => { - playSound('click'); // Звук клика по кнопке - craftOpen = !craftOpen; - craftPanel.style.display = craftOpen ? 'block' : 'none'; - if(craftOpen) { - renderCraft(); - // Закрываем инвентарь если открыт крафт - inventoryOpen = false; - inventoryPanel.style.display = 'none'; - } - }; - document.getElementById('craftClose').onclick = () => { - playSound('click'); // Звук клика по кнопке - craftOpen = false; - craftPanel.style.display = 'none'; - }; - - // Кнопка открытия инвентаря - document.getElementById('invToggle').onclick = () => { - playSound('click'); // Звук клика по кнопке - inventoryOpen = !inventoryOpen; - inventoryPanel.style.display = inventoryOpen ? 'block' : 'none'; - if(inventoryOpen) { - renderInventory(); - // Закрываем крафт если открыт инвентарь - craftOpen = false; - craftPanel.style.display = 'none'; - } - }; - - document.getElementById('inventoryClose').onclick = () => { - playSound('click'); // Звук клика по кнопке - inventoryOpen = false; - inventoryPanel.style.display = 'none'; - }; - - // Кнопка сохранения игры (только для одиночного режима) - const saveBtn = document.getElementById('saveBtn'); - saveBtn.onclick = () => { - playSound('click'); - saveGame(); - customAlert('Игра сохранена!'); - }; - - // Кнопка сброса игры (удаление сохранения и создание нового мира) - const resetBtn = document.getElementById('resetBtn'); - resetBtn.onclick = () => { - customConfirm('Вы уверены, что хотите удалить сохранение и начать новую игру?', () => { - playSound('click'); - - // Удаляем сохранение из localStorage - try { - localStorage.removeItem(SAVE_KEY); - console.log('Сохранение удалено из localStorage'); - } catch (e) { - console.warn('Ошибка удаления сохранения:', e); - } - - // Сбрасываем in-memory сохранение - inMemorySave = null; - - // Генерируем новый worldId - worldId = Math.random().toString(36).substring(2, 10); - console.log('Новый worldId после сброса:', worldId); - - // Обновляем URL - try { - const newUrl = new URL(window.location.href); - newUrl.searchParams.set('world', worldId); - const newUrlString = newUrl.toString(); - - if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') { - window.history.replaceState(null, '', newUrlString); - console.log('URL обновлён:', newUrlString); - } - } catch (e) { - console.error('Ошибка обновления URL:', e); - } - - // Перезагружаем страницу - location.reload(); - }); - }; - - // Показываем кнопку сохранения только если играем одни - function updateSaveButtonVisibility() { - if (isMultiplayer && otherPlayers.size > 0) { - saveBtn.style.display = 'none'; - } else { - saveBtn.style.display = 'flex'; - } - } - - // Режимы - const MODES = [{id:'mine',icon:'⛏️'},{id:'build',icon:'🧱'}]; - let modeIdx=0; - const modeBtn = document.getElementById('modeBtn'); - function mode(){ return MODES[modeIdx].id; } - modeBtn.onclick = () => { - playSound('click'); // Звук клика по кнопке режима - modeIdx=(modeIdx+1)%MODES.length; modeBtn.textContent=MODES[modeIdx].icon; - }; - - // День/ночь (автоматический цикл) - let isNightTime = false; - - // Управление - const inp = { l:false, r:false, j:false, s:false }; - function bindHold(el, key){ - const down=(e)=>{ e.preventDefault(); inp[key]=true; }; - const up=(e)=>{ e.preventDefault(); inp[key]=false; }; - el.addEventListener('pointerdown', down); - el.addEventListener('pointerup', up); - el.addEventListener('pointerleave', up); - } - const leftBtn = document.getElementById('left'); - const rightBtn = document.getElementById('right'); - const jumpBtn = document.getElementById('jump'); - const downBtn = document.getElementById('down'); - - if(leftBtn) bindHold(leftBtn,'l'); - if(rightBtn) bindHold(rightBtn,'r'); - if(jumpBtn) bindHold(jumpBtn,'j'); - if(downBtn) bindHold(downBtn,'s'); - - window.addEventListener('keydown', (e)=>{ - if(e.code==='KeyA'||e.code==='ArrowLeft') inp.l=true; - if(e.code==='KeyD'||e.code==='ArrowRight') inp.r=true; - if(e.code==='Space'||e.code==='KeyW'||e.code==='ArrowUp') inp.j=true; - if(e.code==='KeyS'||e.code==='ArrowDown') inp.s=true; - }); - window.addEventListener('keyup', (e)=>{ - if(e.code==='KeyA'||e.code==='ArrowLeft') inp.l=false; - if(e.code==='KeyD'||e.code==='ArrowRight') inp.r=false; - if(e.code==='Space'||e.code==='KeyW'||e.code==='ArrowUp') inp.j=false; - if(e.code==='KeyS'||e.code==='ArrowDown') inp.s=false; - }); - - // Лодка - const boat = { - x: 0, y: 0, - w: 34, h: 34, - vx: 0, vy: 0, - active: false, - inWater: false - }; - - // Функция для расчёта урона с учётом брони - function calculateDamage(baseDamage) { - // Броня снижает урон пропорционально - // armor: 0 = без брони (100% урона) - // armor: 0.5 = железная броня (50% урона) - const reduction = player.armor; - const actualDamage = baseDamage * (1 - reduction); - console.log('[DAMAGE] Base:', baseDamage, '- Armor:', reduction, '- Actual:', actualDamage.toFixed(1)); - return actualDamage; - } - - // Игрок - const player = { - x: 6*TILE, y: 0*TILE, - w: 34, h: 34, - vx: 0, vy: 0, - grounded: false, - inWater: false, - headInWater: false, - hp: 100, - hunger: 100, - o2: 100, - invuln: 0, - slowTimer: 0, // яд скорпиона — замедление - fallStartY: 0, - lastStepTime: 0, - sleeping: false, - inBoat: false, - armor: 0, // Текущий уровень защиты (0 = без брони, 0.5 = железная броня) - equippedArmor: null, // Тип надетой брони - xp: 0, - level: 1 - }; - - // Сохраняем начальную позицию для возрождения - const spawnPoint = { x: 6*TILE, y: 0*TILE }; - - // Система дропов с мобов - const drops = []; // {x, y, vy, item, qty, age} - let levelUpPopup = null; // {text, timer} - - function xpForLevel(lv) { - if (lv <= 1) return 0; - const thresholds = [0, 50, 150, 300, 500, 800, 1200, 1700, 2300, 3000]; - if (lv - 1 < thresholds.length) return thresholds[lv - 1]; - return Math.floor(3000 + (lv - 10) * (lv - 10) * 50 + (lv - 10) * 200); - } - - function getMobLoot(kind) { - const table = { - chicken: [{item:'chicken_meat',min:1,max:2,chance:1},{item:'feather',min:0,max:1,chance:0.5}], - pig: [{item:'meat',min:1,max:2,chance:1}], - zombie: [{item:'meat',min:0,max:1,chance:0.5},{item:'iron_ingot',min:0,max:1,chance:0.3}], - skeleton: [{item:'arrow',min:2,max:4,chance:1},{item:'bone',min:0,max:2,chance:0.6},{item:'bow',min:1,max:1,chance:0.15}], - creeper: [{item:'gunpowder',min:1,max:2,chance:1}], - scorpion: [{item:'scorpion_stinger',min:0,max:1,chance:0.4}], - polar_bear:[{item:'polar_fur',min:1,max:2,chance:0.8},{item:'meat',min:1,max:2,chance:1}], - slime: [{item:'slime_ball',min:1,max:2,chance:1}], - eagle: [{item:'eagle_feather',min:1,max:2,chance:0.7}] - }; - return table[kind] || []; - } - - function getMobXP(kind) { - const xpTable = { chicken:3, pig:5, zombie:10, skeleton:12, creeper:15, scorpion:8, polar_bear:20, slime:5, eagle:15 }; - return xpTable[kind] || 0; - } - - function spawnDrops(mx, my, kind) { - const loot = getMobLoot(kind); - for (const entry of loot) { - if (Math.random() > entry.chance) continue; - const qty = entry.min + Math.floor(Math.random() * (entry.max - entry.min + 1)); - if (qty <= 0) continue; - drops.push({ - x: mx + (Math.random() - 0.5) * 20, - y: my + (Math.random() - 0.5) * 10, - vy: -1 - Math.random() * 2, - item: entry.item, - qty: qty, - age: 0, xpValue: getMobXP(kind) - }); - } - } - - const LEVEL_UNLOCKS = { - 2: 'Каменные инструменты, Лодка', - 3: 'Железные инструменты, Печь', - 4: 'Лук и стрелы', - 5: 'Железная броня', - 6: 'Золотая броня', - 7: 'Алмазные инструменты', - 8: 'TNT' - }; - - function grantXP(amount) { - player.xp += amount; - while (player.xp >= xpForLevel(player.level + 1)) { - player.level++; - const unlock = LEVEL_UNLOCKS[player.level] || ''; - levelUpPopup = { text: '⭐ Уровень ' + player.level + '!' + (unlock ? ' ' + unlock : ''), timer: 240 }; - } - } - - function pickupDrops() { - for (let i = drops.length - 1; i >= 0; i--) { - const d = drops[i]; - const dx = player.x + player.w/2 - d.x; - const dy = player.y + player.h/2 - d.y; - if (dx*dx + dy*dy < 30*30) { - if (!inv[d.item]) inv[d.item] = 0; - inv[d.item] += d.qty; - if (d.xpValue) grantXP(Math.ceil(d.xpValue * 0.3)); - drops.splice(i, 1); - rebuildHotbar(); - } - } - } - - function drawDrops(ctx) { - for (let i = drops.length - 1; i >= 0; i--) { - const d = drops[i]; - d.age++; - if (d.age > 3600) { drops.splice(i, 1); continue; } // 60 sec at 60fps - // Bounce animation - const bounce = Math.abs(Math.sin(d.age * 0.05)) * 6; - const dy = d.y - bounce; - const sx = d.x - camX; - const sy = dy - camY; - // Skip if off screen - if (sx < -40 || sx > W + 40 || sy < -40 || sy > H + 40) continue; - // Glow effect - ctx.save(); - ctx.globalAlpha = 0.3; - ctx.fillStyle = '#ffff00'; - ctx.beginPath(); - ctx.arc(sx, sy, 14, 0, Math.PI*2); - ctx.fill(); - ctx.restore(); - // Item icon - ctx.save(); - ctx.font = '16px system-ui'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - const itemDef = ITEMS[d.item]; - const icon = itemDef ? itemDef.icon : '🎁'; - const label = d.qty > 1 ? icon + '×' + d.qty : icon; - ctx.fillText(label, sx, sy); - ctx.restore(); - } - } - - function drawLevelUpPopup(ctx) { - if (!levelUpPopup) return; - levelUpPopup.timer--; - if (levelUpPopup.timer <= 0) { levelUpPopup = null; return; } - const alpha = Math.min(1, levelUpPopup.timer / 60); - ctx.save(); - ctx.globalAlpha = alpha; - ctx.fillStyle = '#FFD700'; - ctx.font = 'bold 36px system-ui'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.strokeStyle = '#000'; - ctx.lineWidth = 3; - ctx.strokeText(levelUpPopup.text, W/2, H/2 - 60); - ctx.fillText(levelUpPopup.text, W/2, H/2 - 60); - ctx.restore(); - } - - // Система сохранения игры (localStorage + in-memory fallback) - const SAVE_KEY = 'minegrechka_save'; - let db = null; // Оставляем для совместимости, но не используем - let inMemorySave = null; // Запасное сохранение в памяти - - // Инициализация (localStorage + in-memory fallback) - function initDB(){ - return new Promise((resolve) => { - console.log('Используем localStorage для сохранений (sandbox режим)'); - resolve(null); - }); - } - - // Детерминированный генератор псевдослучайных чисел на основе seed - function seededRandom(gx, gy){ - const n = Math.sin(gx * 12.9898 + gy * 78.233 + worldSeed * 0.1) * 43758.5453; - return n - Math.floor(n); - } - - function saveGame(){ - const saveData = { - version: 2, - worldSeed: worldSeed, - player: { - x: player.x, - y: player.y, - hp: player.hp, - hunger: player.hunger, - o2: player.o2, - xp: player.xp, - level: player.level - }, - temperature: player.temperature, - inventory: inv, - time: worldTime, - isNight: isNightTime, - // Сохраняем только изменения - placedBlocks: placedBlocks.slice(), - removedBlocks: removedBlocks.slice() - }; - - const saveSize = JSON.stringify(saveData).length; - console.log('Сохранение: player HP:', player.hp, 'hunger:', player.hunger, 'o2:', player.o2); - - // Пробуем сохранить в localStorage (основной метод для персистентности) - try { - localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); - console.log(`Игра сохранена в localStorage (размер: ${saveSize} байт)`); - // TG: also save to server - if (tgUser && tgUser.id) { - try { - fetch(SERVER_URL + '/api/tg/save', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ tg_id: tgUser.id, save_data: JSON.stringify(saveData), world_id: worldId, player_name: playerName }) - }).catch(e2 => console.warn('[TG] Save to server failed:', e2)); - } catch(tgE) {} - } - } catch(e){ - console.warn('Ошибка сохранения в localStorage, используем только in-memory:', e); - - // Если localStorage недоступен, используем in-memory fallback - inMemorySave = saveData; - console.log(`Игра сохранена в памяти (размер: ${saveSize} байт)`); - } - } - - function loadGame(){ - return new Promise(async (resolve, reject) => { - // TG: try server save first - if (tgUser && tgUser.id && tgSaveLoaded) { - try { - const resp = await fetch(SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id); - const data = await resp.json(); - if (data.ok && data.save_data) { - const parsed = JSON.parse(data.save_data); - console.log('[TG] Loaded save from server, HP:', parsed.player?.hp); - tgSaveLoaded = parsed; // store for later use - resolve(parsed); return; } - } catch(e) { console.warn('[TG] Load from server failed:', e); } - } - // Пробуем localStorage - try { - const localSave = localStorage.getItem(SAVE_KEY); - if(localSave){ - const parsed = JSON.parse(localSave); - console.log('Загружено из localStorage, player HP:', parsed.player?.hp); - resolve(parsed); - return; } - } catch(e){ - console.warn('Ошибка доступа к localStorage:', e); } - - // Если localStorage недоступен, используем in-memory сохранение - if(inMemorySave){ - console.log('Загружено из in-memory сохранения, player HP:', inMemorySave.player?.hp); - resolve(inMemorySave); + if (state.selected === "bow" && state.inv.bow > 0 && state.inv.arrow > 0) { + const aimX = wx - state.player.x - state.player.w / 2; + const aimY = wy - state.player.y - state.player.h / 2; + const angle = Math.atan2(aimY, aimX); + state.projectiles.push({ + x: state.player.x + state.player.w / 2, + y: state.player.y + state.player.h / 3, + vx: Math.cos(angle) * 550, + vy: Math.sin(angle) * 550, + dmg: 10, + owner: "player", + life: 4 + }); + state.inv.arrow--; + useTool("bow"); + playSound("hit1"); + rebuildHotbar(); return; } - - console.log('Сохранение не найдено'); - resolve(null); - }); - } - - // Миграция с версии 1 на версию 2 - function migrateV1toV2(saveData){ - console.log('Миграция сохранения с версии 1 на версию 2...'); - - // Сохраняем seed из текущей игры (так как v1 его не хранил) - saveData.worldSeed = worldSeed; - - // Инициализируем массивы изменений - saveData.placedBlocks = []; - saveData.removedBlocks = []; - - // Для v1 нам нужно сравнить сгенерированные блоки с тем, что должно быть по seed - // Это сложно сделать без полной перегенерации, поэтому для v1 сохраняем только seed - // и при загрузке просто перегенерируем мир - - // Удаляем старые данные - delete saveData.generatedBlocks; - - saveData.version = 2; - console.log('Миграция завершена'); - } - - async function applySave(saveData){ - if(!saveData) return; - - console.log('=== applySave START ==='); - console.log('player HP before applySave:', player.hp); - console.log('saveData.player.hp:', saveData.player?.hp); - - // Миграция версий - if(saveData.version === 1){ - player.temperature = saveData.player.temperature !== undefined ? saveData.player.temperature : 15; - migrateV1toV2(saveData); - } - - // Восстанавливаем seed - if(saveData.worldSeed !== undefined){ - worldSeed = saveData.worldSeed; - } - - // Восстанавливаем игрока - if(saveData.player){ - player.x = saveData.player.x; - player.y = saveData.player.y; - player.hunger = saveData.player.hunger; - player.o2 = saveData.player.o2; - player.xp = saveData.player.xp || 0; - player.level = saveData.player.level || 1; - - // Обновляем spawnPoint на позицию из сохранения - spawnPoint.x = player.x; - spawnPoint.y = player.y; - - // Проверяем HP из сохранения - если <= 0, устанавливаем 100 - const savedHP = saveData.player.hp; - console.log('Saved HP from file:', savedHP); - if(savedHP <= 0){ - console.log('WARNING: Saved HP is <= 0, setting to 100!'); - player.hp = 100; - } else { - player.hp = savedHP; - } - console.log('player HP after restore:', player.hp); - console.log('spawnPoint обновлён из сохранения: x=', spawnPoint.x, 'y=', spawnPoint.y); - } else { - console.log('No player data in save, setting default HP: 100'); - player.hp = 100; - } - - console.log('=== applySave END ==='); - - // Восстанавливаем инвентарь - if(saveData.inventory){ - for(const key in saveData.inventory){ - inv[key] = saveData.inventory[key]; - } - } - - // Восстанавливаем время - if(saveData.time !== undefined){ - worldTime = saveData.time; - } - - // Восстанавливаем день/ночь - if(saveData.isNight !== undefined){ - isNightTime = saveData.isNight; - } - - // Перегенерируем мир по seed - regenerateVisibleChunks(); - - // Применяем изменения (только для v2) - if(saveData.version === 2){ - // Применяем блоки, установленные игроком - for(const block of saveData.placedBlocks){ - setBlock(block.gx, block.gy, block.t, true); - } - - // Применяем удалённые блоки - for(const block of saveData.removedBlocks){ - removeBlock(block.gx, block.gy); - } - - // Восстанавливаем массивы изменений - placedBlocks = saveData.placedBlocks || []; - removedBlocks = saveData.removedBlocks || []; - } - - rebuildHotbar(); - console.log('Игра загружена'); - } - - // Камера (двухосевая) - let camX=0, camY=0; - - // День/ночь - let worldTime=0; - const DAY_LEN=360; // замедлил смену дня/ночи в 3 раза - - // Облака - const clouds = Array.from({length:10}, ()=>({ - x: Math.random()*2000, - y: -200 - Math.random()*260, // выше экрана, потому что мир по Y теперь двигается - w: 80+Math.random()*120, - s: 12+Math.random()*20 - })); - - // Дождь - let isRaining = false; - let rainIntensity = 0; // 0..1 - const snowflakes = []; // снежинки для тундры - const MAX_SNOWFLAKES = 150; - const raindrops = []; - const MAX_RAINDROPS = 200; - - // ==================== РОСТ КУЛЬТУР ==================== - const growthTimers = {}; // ключ: "gx,gy" → { stage:0-3, growTimer:X } - - // Старая функция заменена — теперь погода через биомы (см. weatherState выше) - // updateWeather(dt) вызывается из основного цикла — биом-зависимая - - // Интеграция: определяем isRaining из weatherState для визуализации - function syncWeatherVisual() { - isRaining = (weatherState.type === 'rain' || weatherState.type === 'storm'); - if (weatherState.type === 'clear') { - rainIntensity *= 0.95; // плавное затухание - if (rainIntensity < 0.01) rainIntensity = 0; - } - } - - function updateRain(dt) { - syncWeatherVisual(); - // Дождь - if ((weatherState.type === 'rain' || weatherState.type === 'storm') && rainIntensity < weatherState.intensity * 0.6) { - rainIntensity += dt * 0.3; - } else if (weatherState.type === 'clear' || weatherState.type === 'snow' || weatherState.type === 'fog') { - rainIntensity = Math.max(0, rainIntensity - dt * 0.5); - } - if (rainIntensity < 0.01) { - raindrops.length = 0; - } else { - const spawnRate = Math.floor(rainIntensity * 80 * dt); - for (let i = 0; i < spawnRate && raindrops.length < MAX_RAINDROPS; i++) { - raindrops.push({ - x: camX + Math.random() * W, - y: camY - 20, - vy: 400 + Math.random() * 200, - len: 8 + Math.random() * 12 - }); - } - for (let i = raindrops.length - 1; i >= 0; i--) { - const d = raindrops[i]; - d.y += d.vy * dt; - d.x -= 30 * dt; - if (d.y > camY + H + 20) raindrops.splice(i, 1); - } - } - // Снег - if (weatherState.type === 'snow') { - const spawnRate = Math.floor(weatherState.intensity * 30 * dt); - for (let i = 0; i < spawnRate && snowflakes.length < MAX_SNOWFLAKES; i++) { - snowflakes.push({ - x: camX + Math.random() * W, - y: camY - 10, - vy: 40 + Math.random() * 60, - vx: (Math.random() - 0.5) * 30, - size: 2 + Math.random() * 3 - }); - } - } - for (let i = snowflakes.length - 1; i >= 0; i--) { - const s = snowflakes[i]; - s.y += s.vy * dt; - s.x += s.vx * dt + Math.sin(s.y * 0.02) * 10 * dt; - if (s.y > camY + H + 20 || weatherState.type !== 'snow') snowflakes.splice(i, 1); - } - } - - function drawRain() { - // Дождь - if (raindrops.length > 0) { - ctx.save(); - ctx.strokeStyle = (weatherState.type === 'storm') ? 'rgba(174,194,224,0.7)' : 'rgba(174,194,224,0.5)'; - ctx.lineWidth = 1.5; - ctx.beginPath(); - for (const d of raindrops) { - ctx.moveTo(d.x, d.y); - ctx.lineTo(d.x - 3, d.y + d.len); - } - ctx.stroke(); - ctx.restore(); - } - // Снег - if (snowflakes.length > 0) { - ctx.save(); - ctx.fillStyle = 'rgba(255,255,255,0.8)'; - for (const s of snowflakes) { - ctx.beginPath(); - ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2); - ctx.fill(); - } - ctx.restore(); - } - // Гроза — вспышка - if (weatherState.type === 'storm' && Math.random() < 0.003) { - ctx.save(); - ctx.fillStyle = 'rgba(255,255,255,0.3)'; - ctx.fillRect(camX, camY, W, H); - ctx.restore(); - } - // Туман — серый оверлей - if (weatherState.type === 'fog') { - ctx.save(); - ctx.fillStyle = 'rgba(200,200,200,0.4)'; - ctx.fillRect(camX, camY, W, H); - ctx.restore(); - } - } - - // Частицы (взрыв) - const parts = []; - function spawnExplosion(x,y, power){ - const n = Math.floor(16 + power*10); - for(let i=0;i 100){ - playSound('splash'); - } - } - - function resolveY(e){ - // Всегда пересчитываем grounded (не держим "липким") - e.grounded = false; - - const x1 = e.x + 2; - const x2 = e.x + e.w - 2; - - // Проверяем, находится ли игрок на лестнице (по центру) - const cx = e.x + e.w/2; - const cy = e.y + e.h/2; - const gx = Math.floor(cx / TILE); - const gy = Math.floor(cy / TILE); - const b = getBlock(gx, gy); - const onLadder = b && BLOCKS[b.t] && BLOCKS[b.t].climbable; - - // Если на лестнице - можно двигаться вверх/вниз - if(onLadder){ - e.grounded = true; - - // Если нажимаем прыжок на лестнице - поднимаемся - if(inp.j){ - e.vy = -200; - } - // Если нажимаем вниз - спускаемся - else if(inp.s){ - e.vy = 100; - } - // Иначе - остаёмся на месте (нет гравитации) - else { - e.vy = 0; - } - return; - } - - // Проверяем, можно ли запрыгнуть на лестницу рядом (слева или справа) - const leftGX = Math.floor((e.x - 4) / TILE); - const rightGX = Math.floor((e.x + e.w + 4) / TILE); - const playerGY = Math.floor((e.y + e.h/2) / TILE); - - const leftBlock = getBlock(leftGX, playerGY); - const rightBlock = getBlock(rightGX, playerGY); - const leftLadder = leftBlock && BLOCKS[leftBlock.t] && BLOCKS[leftBlock.t].climbable; - const rightLadder = rightBlock && BLOCKS[rightBlock.t] && BLOCKS[rightBlock.t].climbable; - - // Если рядом есть лестница и игрок прыгает - притягиваем к ней - if((leftLadder || rightLadder) && inp.j && e.vy < 0){ - // Перемещаем игрока к лестнице - if(leftLadder && e.x > leftGX * TILE + TILE/2){ - e.x = leftGX * TILE + TILE/2 - e.w/2; - } else if(rightLadder && e.x < rightGX * TILE + TILE/2){ - e.x = rightGX * TILE + TILE/2 - e.w/2; - } - e.grounded = true; - e.vy = -150; // меньший прыжок при запрыгивании на лестницу - return; - } - - // 1) Если движемся вниз ИЛИ стоим (vy >= 0) — проверяем опору под ногами - // Берём точку на 1px ниже стопы, чтобы не зависеть от ровного попадания в границу тайла. - if(e.vy >= 0){ - const probeY = e.y + e.h + 1; - const gy = Math.floor(probeY / TILE); - const gxA = Math.floor(x1 / TILE); - const gxB = Math.floor(x2 / TILE); - - if(isSolid(gxA, gy) || isSolid(gxB, gy)){ - e.y = gy * TILE - e.h; // прижимаем к полу - e.vy = 0; - e.grounded = true; - - // урон от падения — только игроку и только не в воде - if(e === player && !player.inWater){ - const fallTiles = (e.y - e.fallStartY) / TILE; - if(fallTiles > 6) { - const damage = calculateDamage((fallTiles - 6) * 10); - player.hp -= damage; - } + if (ITEMS[state.selected] && state.inv[state.selected] > 0) { + const it = ITEMS[state.selected]; + if (state.player.hp < 100 || state.player.hunger < 100) { + playSound("eat1"); + state.player.hunger = Math.min(100, state.player.hunger + it.food); + state.player.hp = Math.min(100, state.player.hp + 15); + state.inv[state.selected]--; + rebuildHotbar(); } - if(e === player) e.fallStartY = e.y; + return; } - } - - // 2) Проверка на возможность запрыгнуть на блок, если игрок рядом с ним - if(e.vy < 0 && e === player){ - const gy = Math.floor(e.y / TILE); - const gxA = Math.floor(x1 / TILE); - const gxB = Math.floor(x2 / TILE); - - // Проверяем, есть ли блок рядом с игроком - if((isSolid(gxA, gy) || isSolid(gxB, gy)) && !isSolid(gxA, gy - 1) && !isSolid(gxB, gy - 1)){ - e.y = (gy + 1) * TILE; - e.vy = 0; - e.grounded = true; - if(e === player) e.fallStartY = e.y; - console.log("Jumped onto block!"); + if (b && b.t === "campfire" && state.selected === "meat" && state.inv.meat > 0) { + playSound("fire"); + state.inv.meat--; + state.inv.cooked++; + rebuildHotbar(); + return; } - } - - // 2) Если движемся вверх — проверяем потолок - if(e.vy < 0){ - const gy = Math.floor(e.y / TILE); - const gxA = Math.floor(x1 / TILE); - const gxB = Math.floor(x2 / TILE); - if(isSolid(gxA, gy) || isSolid(gxB, gy)){ - e.y = (gy + 1) * TILE; - e.vy = 0; + if (b && b.t === "bed" && isNight()) { + state.player.sleeping = true; + saveGame(); + return; } - } - } - - function resolveX(e){ - const y1 = e.y + 2; - const y2 = e.y + e.h - 2; - - // Проверяем, находимся ли мы на лестнице - const cx = e.x + e.w/2; - const cy = e.y + e.h/2; - const gx = Math.floor(cx / TILE); - const gy = Math.floor(cy / TILE); - const b = getBlock(gx, gy); - const onLadder = b && BLOCKS[b.t] && BLOCKS[b.t].climbable; - - if(e.vx > 0){ - const gx = Math.floor((e.x + e.w)/TILE); - const gyA = Math.floor(y1/TILE); - const gyB = Math.floor(y2/TILE); - const solidA = isSolid(gx, gyA); - const solidB = isSolid(gx, gyB); - - if(solidA || solidB){ - e.x = gx*TILE - e.w; - e.vx = 0; - } - } else if(e.vx < 0){ - const gx = Math.floor(e.x/TILE); - const gyA = Math.floor(y1/TILE); - const gyB = Math.floor(y2/TILE); - const solidA = isSolid(gx, gyA); - const solidB = isSolid(gx, gyB); - - if(solidA || solidB){ - e.x = (gx+1)*TILE; - e.vx = 0; - } - } - } - - // TNT логика: цепь + усиление - const activeTNT = new Set(); // хранит key - function activateTNT(b, fuse=3.2){ - if(b.dead) return; - if(b.active) return; - b.active=true; - b.fuse=fuse; - activeTNT.add(k(b.gx,b.gy)); - } - - function explodeAt(gx,gy){ - const center = getBlock(gx,gy); - if(!center) return; - - // усиление: считаем сколько TNT рядом (в радиусе 2) и активируем их «почти сразу» - let bonus = 0; - for(let x=gx-2; x<=gx+2; x++){ - for(let y=gy-2; y<=gy+2; y++){ - const b = getBlock(x,y); - if(b && !b.dead && b.t==='tnt' && !(x===gx && y===gy)){ - bonus += 0.8; - activateTNT(b, 0.12); // цепь + if (mode() === "mine") { + if (!b) return; + if (BLOCKS[b.t].fluid || BLOCKS[b.t].decor) return; + if (b.t === "tnt") { + activateTNT(b, 3.2); + return; } - } - } - - const power = 1 + bonus; // условная мощность - const radius = 3.2 + bonus*0.7; // радиус разрушения в тайлах - const dmgR = 150 + bonus*60; // радиус урона в пикселях - - removeBlock(gx,gy); - activeTNT.delete(k(gx,gy)); - playSound('explode1'); // Звук взрыва - spawnExplosion(gx*TILE + TILE/2, gy*TILE + TILE/2, power); - - for(let x=Math.floor(gx-radius); x<=Math.ceil(gx+radius); x++){ - for(let y=Math.floor(gy-radius); y<=Math.ceil(gy+radius); y++){ - const d = Math.hypot(x-gx, y-gy); - if(d > radius) continue; - const b = getBlock(x,y); - if(!b || b.dead) continue; - if(BLOCKS[b.t].fluid) continue; - if(BLOCKS[b.t].unbreakable) continue; - if(b.t==='tnt') { activateTNT(b, 0.12); continue; } - removeBlock(x,y); - if(inv[b.t] !== undefined && Math.random()<0.20) inv[b.t]++; // немного дропа - } - } - rebuildHotbar(); - - // урон - const hurt = (e)=>{ - const dx = (e.x+e.w/2) - (gx*TILE+TILE/2); - const dy = (e.y+e.h/2) - (gy*TILE+TILE/2); - const dist = Math.hypot(dx,dy); - if(dist < dmgR){ - const dmg = (dmgR - dist) * 0.06 * power; - if(e === player) { - const actualDamage = calculateDamage(dmg); - player.hp -= actualDamage; - } else { - e.hp -= dmg; - } - e.vx += (dx/dist || 0) * 600; - e.vy -= 320; - } - }; - hurt(player); - mobs.forEach(hurt); - } - - // Взаимодействие мышь/тап - const mouse = { x:null, y:null }; - canvas.addEventListener('pointermove', (e)=>{ - const r = canvas.getBoundingClientRect(); - mouse.x = e.clientX - r.left; - mouse.y = e.clientY - r.top; - }); - - canvas.addEventListener('pointerdown', (e)=>{ - if(craftOpen) return; - if(player.hp<=0) return; - - const r = canvas.getBoundingClientRect(); - const sx = e.clientX - r.left; - const sy = e.clientY - r.top; - - const wx = sx + camX; - const wy = sy + camY; - - const gx = Math.floor(wx / TILE); - const gy = Math.floor(wy / TILE); - - // Пробуждение: клик по любой кровати когда спишь - const b = getBlock(gx,gy); - if(player.sleeping && b && b.t==='bed'){ - player.sleeping = false; - return; - } - - if(player.sleeping) return; // Нельзя взаимодействовать во время сна - - // Клик по печи — открываем панель обжига - if(b && b.t === 'furnace' && mode() === 'mine'){ - openFurnaceUI(gx, gy); - return; - } - - // клик по мобу (в режиме mine) - if(mode()==='mine'){ - // Check all mobs (local + server-spawned) using getAllMobs - const allClickMobs = getAllMobs(); - for(let i = allClickMobs.length - 1; i >= 0; i--){ - const m = allClickMobs[i]; - if(m.dead) continue; - if(wx>=m.x && wx<=m.x+m.w && wy>=m.y && wy<=m.y+m.h){ - let dmg = 1; - const swordTypes = ['iron_sword','stone_sword','wood_sword']; - for (const st of swordTypes) { - if (inv[st] > 0) { dmg = TOOLS[st].damage || 3; useTool(st); break; } - } - m.hp -= dmg; - m.vx += (m.x - player.x) * 2; - m.vy -= 200; - playSound('attack'); - // Server-spawned mob: emit hurt to server for relay, handle death locally - if(m.id !== undefined && isMultiplayer){ - socket.emit('mob_hurt', { id: m.id, dmg }); - if(m.hp <= 0){ - socket.emit('mob_died', { id: m.id }); - } - } - if(m.hp<=0){ - if(m.kind === 'chicken') playSound('hurt_chicken'); - spawnDrops(m.x + m.w/2, m.y + m.h/2, m.kind); - // Remove from the correct array - if(m.id !== undefined){ - serverMobs.delete(m.id); - } else { - const localIdx = mobs.indexOf(m); - if(localIdx >= 0) mobs.splice(localIdx, 1); - } + const hardness = BLOCKS[b.t].hardness || 1; + if (hardness === Infinity) return; + if (hardness === 0) { + const removed = removeBlock(gx, gy); + if (removed) { + state.inv[removed.t] = (state.inv[removed.t] || 0) + 1; + sendBlockChange(gx, gy, removed.t, "remove"); rebuildHotbar(); } return; } - } - } - - // Лук — стреляем стрелой - if(selected === 'bow' && inv.bow > 0 && inv.arrow > 0){ - const aimX = wx - player.x - player.w/2; - const aimY = wy - player.y - player.h/2; - const angle = Math.atan2(aimY, aimX); - projectiles.push({ - x: player.x + player.w/2, - y: player.y + player.h/3, - vx: Math.cos(angle) * 550, - vy: Math.sin(angle) * 550, - dmg: 10, - owner: 'player', - life: 4 - }); - inv.arrow--; - useTool('bow'); - playSound('hit1'); - rebuildHotbar(); - return; - } - - // еда (предмет) - if(ITEMS[selected] && inv[selected]>0){ - const it = ITEMS[selected]; - if(player.hp < 100 || player.hunger < 100){ - playSound('eat1'); - player.hunger = Math.min(100, player.hunger + it.food); - player.hp = Math.min(100, player.hp + 15); - inv[selected]--; - rebuildHotbar(); - } - return; - } - - //Посадка семян на грядку - if(b && b.t === 'farmland' && mode()==='build'){ - const seedMap = { wheat: 'wheat_stage0', carrot: 'carrot_stage0', potato: 'potato_stage0' }; - if(seedMap[selected] && inv[selected] > 0){ - inv[selected]--; - const cropType = seedMap[selected]; - setBlock(gx, gy-1, cropType); - growthTimers[gx+','+(gy-1)] = { stage:0, growTimer: 10+Math.random()*5 }; - sendBlockChange(gx, gy-1, cropType, 'add'); - playSound('cloth1'); - rebuildHotbar(); - return; - } - } - - // Мотыга — превращает grass/dirt в farmland (в любом режиме) - if(selected === 'hoe' && inv.hoe > 0 && b){ - if(b.t === 'grass' || b.t === 'dirt'){ - setBlock(gx, gy, 'farmland'); - sendBlockChange(gx, gy, b.t, 'remove'); - sendBlockChange(gx, gy, 'farmland', 'add'); - useTool('hoe'); - playSound('cloth1'); - rebuildHotbar(); - return; - } - } - - // жарка на костре: выбран meat + клик по campfire - if(b && b.t==='campfire' && selected==='meat' && inv.meat>0){ - playSound('fire'); // Звук при жарке на костре - inv.meat--; inv.cooked++; - rebuildHotbar(); - return; - } - - // Сон на кровати: клик по bed - if(b && b.t==='bed' && isNight()){ - player.sleeping = true; - saveGame(); // Сохраняем при отходе ко сну - return; - } - - if(mode()==='mine'){ - if(!b) return; - if(BLOCKS[b.t].fluid) return; - // Клик на урожайную культуру — сбор - if(BLOCKS[b.t].harvestable){ - const hInfo = BLOCKS[b.t]; - inv[hInfo.harvestItem] = (inv[hInfo.harvestItem]||0) + hInfo.harvestQty; - removeBlock(gx, gy); - sendBlockChange(gx, gy, b.t, 'remove'); - delete growthTimers[gx+','+gy]; - playSound('cloth1'); - rebuildHotbar(); - return; - } - if(BLOCKS[b.t].decor) return; - - if(b.t==='tnt'){ activateTNT(b, 3.2); return; } // не взрывается сразу - - const removed = removeBlock(gx,gy); - if(removed){ - inv[removed.t] = (inv[removed.t]||0) + 1; - - // Тратим прочность кирки (если есть в инвентаре) - const pickTypes = ['iron_pickaxe','stone_pickaxe','wood_pickaxe']; - for (const pt of pickTypes) { - if (inv[pt] > 0) { - const broke = useTool(pt); - if (broke) playSound('cloth1'); // звук поломки - break; - } + if (state.mining.gx === gx && state.mining.gy === gy) { + state.mining.progress += getMiningSpeed(b.t); + } else { + state.mining = { gx, gy, progress: getMiningSpeed(b.t), hardness }; } - - // Отправляем изменение блока на сервер - sendBlockChange(gx, gy, removed.t, 'remove'); - - // Звуки при добыче блоков - if(removed.t === 'glass') playSound('glass1'); - else if(removed.t === 'sand') playSound('sand1'); - else if(removed.t === 'snow') playSound('snow1'); - else if(removed.t === 'stone' || removed.t.endsWith('_ore')) playSound('stone1'); - else if(removed.t === 'wood') playSound('wood1'); - else playSound('cloth1'); - - rebuildHotbar(); + if (b.t === "glass") playSound("glass1"); + else if (b.t === "sand") playSound("sand1"); + else if (b.t === "stone" || b.t.endsWith("_ore")) playSound("stone1"); + else if (b.t === "wood" || b.t === "planks") playSound("wood1"); + else playSound("cloth1"); + if (state.mining.progress >= hardness) { + const minTool = BLOCKS[b.t].minTool; + let drops = true; + if (minTool) { + const toolId = getBestTool(b.t); + if (toolId) { + const tool = TOOLS[toolId]; + if (toolTierLessThan(tool.toolTier, minTool)) drops = false; + else useTool(toolId); + } else { + drops = false; + } + } else { + const toolId = getBestTool(b.t); + if (toolId) useTool(toolId); + } + const removed = removeBlock(gx, gy); + if (removed) { + if (drops) { + state.inv[removed.t] = (state.inv[removed.t] || 0) + 1; + } else { + if (BLOCKS[removed.t]?.toolType === "pickaxe") { + state.inv.cobblestone = (state.inv.cobblestone || 0) + 1; + } + } + sendBlockChange(gx, gy, removed.t, "remove"); + if (removed.t === "glass") playSound("glass1"); + else if (removed.t === "sand") playSound("sand1"); + else if (removed.t === "stone" || removed.t.endsWith("_ore")) playSound("stone1"); + else if (removed.t === "wood") playSound("wood1"); + else playSound("cloth1"); + rebuildHotbar(); + } + state.mining = { gx: null, gy: null, progress: 0, hardness: 0 }; + } + miningHeld = true; + if (miningInterval) clearInterval(miningInterval); + miningInterval = setInterval(() => { + if (!miningHeld) { + clearInterval(miningInterval); + miningInterval = null; + return; + } + const mb = getBlock(state.mining.gx, state.mining.gy); + if (!mb || mb.dead || mb.t !== b.t) { + miningHeld = false; + clearInterval(miningInterval); + miningInterval = null; + return; + } + if (state.mining.gx !== null && state.mining.hardness > 0) { + state.mining.progress += getMiningSpeed(mb.t) * 0.3; + if (state.mining.progress >= state.mining.hardness) { + const mgx = state.mining.gx; + const mgy = state.mining.gy; + const mMinTool = BLOCKS[mb.t].minTool; + let mDrops = true; + if (mMinTool) { + const mToolId = getBestTool(mb.t); + if (mToolId) { + const mTool = TOOLS[mToolId]; + if (toolTierLessThan(mTool.toolTier, mMinTool)) mDrops = false; + else useTool(mToolId); + } else { + mDrops = false; + } + } else { + const mToolId = getBestTool(mb.t); + if (mToolId) useTool(mToolId); + } + const removed = removeBlock(mgx, mgy); + if (removed) { + if (mDrops) { + state.inv[removed.t] = (state.inv[removed.t] || 0) + 1; + } else { + if (BLOCKS[removed.t]?.toolType === "pickaxe") { + state.inv.cobblestone = (state.inv.cobblestone || 0) + 1; + } + } + sendBlockChange(mgx, mgy, removed.t, "remove"); + rebuildHotbar(); + } + state.mining = { gx: null, gy: null, progress: 0, hardness: 0 }; + miningHeld = false; + clearInterval(miningInterval); + miningInterval = null; + } + } + }, 100); + return; } - return; - } - - if(mode()==='build'){ - if(inv[selected] <= 0) return; - if(!BLOCKS[selected]) return; - if(b) return; // занято - - // Проверяем, ставим ли лодку - if(selected === 'boat'){ - // Лодку можно ставить только на воду - const waterBelow = getBlock(gx, gy+1); - if(!waterBelow || waterBelow.t !== 'water'){ + if (mode() === "build") { + if (state.inv[state.selected] <= 0) return; + if (!BLOCKS[state.selected]) return; + if (b) return; + if (state.selected === "boat") { + const waterBelow = getBlock(gx, gy + 1); + if (!waterBelow || waterBelow.t !== "water") { + return; + } + state.boat.x = gx * state.TILE; + state.boat.y = gy * state.TILE; + state.boat.vx = 0; + state.boat.vy = 0; + state.boat.active = true; + state.boat.inWater = true; + state.player.inBoat = true; + state.player.x = state.boat.x; + state.player.y = state.boat.y; + state.player.vx = 0; + state.player.vy = 0; + playSound("splash"); + state.inv[state.selected]--; + rebuildHotbar(); return; } - - // Создаём лодку - boat.x = gx * TILE; - boat.y = gy * TILE; - boat.vx = 0; - boat.vy = 0; - boat.active = true; - boat.inWater = true; - - // Сажаем игрока в лодку - player.inBoat = true; - player.x = boat.x; - player.y = boat.y; - player.vx = 0; - player.vy = 0; - - playSound('splash'); - inv[selected]--; + const TILE2 = state.TILE; + const bx = gx * TILE2, by = gy * TILE2; + const overlap = !(bx >= state.player.x + state.player.w || bx + TILE2 <= state.player.x || by >= state.player.y + state.player.h || by + TILE2 <= state.player.y); + if (overlap) return; + setBlock(gx, gy, state.selected, true); + state.inv[state.selected]--; + sendBlockChange(gx, gy, state.selected, "set"); + if (state.selected === "stone" || state.selected === "brick") playSound("stone_build"); + else if (state.selected === "wood" || state.selected === "planks") playSound("wood_build"); + else if (state.selected === "glass") playSound("glass1"); + else if (state.selected === "sand") playSound("sand1"); + else if (state.selected === "dirt" || state.selected === "grass") playSound("cloth1"); rebuildHotbar(); return; } - - // запрет ставить в игрока - const bx = gx*TILE, by = gy*TILE; - const overlap = !(bx >= player.x+player.w || bx+TILE <= player.x || by >= player.y+player.h || by+TILE <= player.y); - if(overlap) return; - - setBlock(gx,gy,selected, true); // true = блок установлен игроком - inv[selected]--; - - // Отправляем изменение блока на сервер - sendBlockChange(gx, gy, selected, 'set'); - - // Звук при строительстве - if(selected === 'stone' || selected === 'brick') playSound('stone_build'); - else if(selected === 'wood' || selected === 'planks') playSound('wood_build'); - else if(selected === 'glass') playSound('glass1'); - else if(selected === 'sand') playSound('sand1'); - else if(selected === 'snow') playSound('snow1'); - else if(selected === 'dirt' || selected === 'grass') playSound('cloth1'); - - rebuildHotbar(); - return; - } - }); - - // ==================== БИОМЫ ==================== - const BIOMES = { - plains: { name:'Равнина', surface:'grass', subsurface:'dirt', trees:true, flowers:true, treeChance:0.12 }, - desert: { name:'Пустыня', surface:'sand', subsurface:'sand', trees:false, flowers:false, treeChance:0 }, - tundra: { name:'Тундра', surface:'snow', subsurface:'dirt', trees:true, flowers:false, treeChance:0.06 }, - swamp: { name:'Болото', surface:'moss', subsurface:'dirt', trees:true, flowers:false, treeChance:0.10 }, - mountains:{ name:'Горы', surface:'stone', subsurface:'stone', trees:false, flowers:false, treeChance:0 } - }; - - function getBiome(gx) { - const temp = Math.sin(gx*0.003 + worldSeed*0.01)*0.5 + Math.sin(gx*0.007 + worldSeed*0.02)*0.3 + 0.5; - const humid = Math.sin(gx*0.004 + worldSeed*0.015 + 1000)*0.5 + Math.cos(gx*0.006 + worldSeed*0.02 + 2000)*0.3 + 0.5; - const mtVal = Math.sin(gx*0.001 + worldSeed*0.005)*0.5 + 0.5; - if (temp > 0.7) return 'desert'; - if (temp < 0.3) return 'tundra'; - if (humid > 0.7 && temp >= 0.3 && temp <= 0.7) return 'swamp'; - if (temp >= 0.5 && temp <= 0.7 && mtVal > 0.75) return 'mountains'; - return 'plains'; + }); + canvas.addEventListener("pointerup", () => { + miningHeld = false; + }); + canvas.addEventListener("pointerleave", () => { + miningHeld = false; + }); } - const biomeCache = {}; - function getCachedBiome(gx) { - const chunk = Math.floor(gx / 8); // cache per 8-tile chunk for smoother biomes - if (biomeCache[chunk] === undefined) biomeCache[chunk] = getBiome(chunk * 8); - return biomeCache[chunk]; - } - - // ==================== ПОГОДА ==================== - const weatherState = { type: 'clear', intensity: 0, timer: 0, duration: 180, nextChange: 120 + Math.random()*180 }; - const BIOME_WEATHER = { - plains: { clear:0.50, rain:0.30, storm:0.10, snow:0, fog:0.10 }, - desert: { clear:0.80, rain:0.05, storm:0, snow:0, fog:0.15 }, - tundra: { clear:0.20, rain:0, storm:0.10, snow:0.60, fog:0.10 }, - swamp: { clear:0.20, rain:0.30, storm:0.10, snow:0, fog:0.40 }, - mountains:{ clear:0.40, rain:0.30, storm:0.10, snow:0.10, fog:0.10 } - }; - - function updateWeather(dt) { - weatherState.timer += dt; - if (weatherState.timer >= weatherState.nextChange) { - weatherState.timer = 0; - weatherState.nextChange = 60 + Math.random() * 240; - const biome = getCachedBiome(Math.floor(player.x / TILE)); - const probs = BIOME_WEATHER[biome] || BIOME_WEATHER.plains; - const r = Math.random(); - let cum = 0; - if ((cum += probs.clear) > r) { weatherState.type = 'clear'; } - else if ((cum += probs.rain) > r) { weatherState.type = 'rain'; } - else if ((cum += probs.storm) > r) { weatherState.type = 'storm'; } - else if ((cum += probs.snow) > r) { weatherState.type = 'snow'; } - else { weatherState.type = 'fog'; } - weatherState.duration = 60 + Math.random() * 300; - } - // Intensity interpolation - const target = (weatherState.type === 'clear') ? 0 : 1; - weatherState.intensity += (target - weatherState.intensity) * dt * 0.5; - } - - function getWeatherSpeedMultiplier() { - if (weatherState.type === 'rain') return 0.85; - if (weatherState.type === 'snow') return 0.7; - if (weatherState.type === 'storm') return 0.85; - return 1; - } - - function isOutdoorLight(lx, ly) { - // Check if position is outdoors (no block above) - const aboveGy = Math.floor(ly / TILE) - 1; - const aboveGx = Math.floor(lx / TILE); - const above = getBlock(aboveGx, aboveGy); - return !above || !BLOCKS[above.t]?.solid; - } - - // ==================== СТРУКТУРЫ МИРА ==================== - function placeStructure(startGx, startGy, pattern) { - for (let dy = 0; dy < pattern.length; dy++) { - for (let dx = 0; dx < pattern[dy].length; dx++) { - const bt = pattern[dy][dx]; - if (bt && bt !== 'air') { - setBlock(startGx + dx, startGy + dy, bt); - } - } - } - } - - // Пирамида в пустыне - const PYRAMID_PATTERN = [ - ['sand','sand','sand','sand','sand','sand','sand'], - ['sand','stone','stone','stone','stone','stone','sand'], - ['sand','stone','air','air','air','stone','sand'], - ['sand','stone','air','air','air','stone','sand'], - ['sand','stone','air','air','air','stone','sand'], - ['sand','sand','stone','stone','stone','sand','sand'] - ]; - - // Дом в равнине - const HOUSE_PATTERN = [ - ['air','planks','planks','planks','planks','air'], - ['planks','air','air','air','air','planks'], - ['planks','air','torch','air','air','planks'], - ['planks','air','air','air','air','planks'], - ['planks','planks','air','planks','planks','planks'] - ]; - - // Хижина в болоте - const HUT_PATTERN = [ - ['air','wood','wood','wood','air'], - ['wood','air','air','air','wood'], - ['wood','air','torch','air','wood'], - ['moss','moss','air','moss','moss'] - ]; - - // ==================== ГЕНЕРАЦИЯ ==================== - const generated = new Set(); // gx already generated - function surfaceGyAt(gx) { - const biome = getCachedBiome(gx); - // Base noise (same for all biomes) - const n1 = Math.sin(gx*0.025 + worldSeed*0.001)*8; - const n2 = Math.sin(gx*0.012 + worldSeed*0.002)*12; - const n3 = Math.sin(gx*0.006 + worldSeed*0.003)*6; - const n4 = Math.sin(gx*0.045 + worldSeed*0.004)*4; - const n5 = Math.cos(gx*0.018 + worldSeed*0.005)*5; - let h; - switch(biome) { - case 'desert': - h = Math.floor(SEA_GY - 4 + n3*0.3 + n4*0.5); // flatter, slightly higher - break; - case 'tundra': - h = Math.floor(SEA_GY - 6 + n2*0.5 + n3*0.4 + n5*0.3); // gentle rolling - break; - case 'swamp': - h = Math.floor(SEA_GY - 2 + n3*0.2 + n4*0.3); // very flat, near sea level - h = Math.max(h, SEA_GY - 3); // never too deep - break; - case 'mountains': - h = Math.floor(SEA_GY - 15 + n1*1.5 + n2*1.2 + n3); // tall peaks - break; - default: // plains - h = Math.floor(SEA_GY - 8 + n1 + n2 + n3 + n4 + n5); - } - return h; - } - - function genColumn(gx) { - if(generated.has(gx)) return; - if(worldSeed == null) return; // wait for server seed in multiplayer - generated.add(gx); - - const sgy = surfaceGyAt(gx); - const biome = getCachedBiome(gx); - - // === Вода и поверхность === - if(sgy > SEA_GY) { - // ниже уровня моря — заливаем водой - for(let gy = SEA_GY; gy < sgy; gy++) { - const blockType = (biome === 'swamp' && gy >= SEA_GY - 1) ? 'swamp_water' : 'water'; - setBlock(gx, gy, blockType); - } - setBlock(gx, sgy, 'sand'); - } else { - // поверхность - const b = BIOMES[biome]; - setBlock(gx, sgy, b.surface); - // болото: случайные лужи болотной воды - if(biome === 'swamp' && seededRandom(gx*3, sgy) < 0.15) { - setBlock(gx, sgy-1, 'swamp_water'); - } - // тундра: лёд на воде рядом - if(biome === 'tundra' && sgy === SEA_GY && seededRandom(gx, SEA_GY-1) < 0.3) { - setBlock(gx, SEA_GY-1, 'ice'); - } - } - - // === Подповерхностные слои === - for(let gy = sgy+1; gy <= BEDROCK_GY; gy++) { - if(gy === BEDROCK_GY) { setBlock(gx,gy,'bedrock'); continue; } - - let t = BIOMES[biome].subsurface; - - // глубже — камень - if(gy > sgy + 3) t = 'stone'; - - // пустыня: sand глубже - if(biome === 'desert' && gy <= sgy + 6) t = 'sand'; - - // болото: глина ближе к поверхности - if(biome === 'swamp' && gy <= sgy + 2 && seededRandom(gx, gy) < 0.3) t = 'clay'; - - // горы: gravel - if(biome === 'mountains' && gy > sgy + 4 && seededRandom(gx, gy) < 0.12) t = 'gravel'; - - // общие руды - const depth = gy - sgy; - const r = seededRandom(gx, gy); - if(t === 'stone') { - if(r < 0.06) t = 'coal'; - else if(r < 0.10) t = 'copper_ore'; - else if(r < 0.13) t = 'iron_ore'; - else if(depth > 40 && r < 0.145) t = 'gold_ore'; - else if(depth > 70 && r < 0.152) t = 'diamond_ore'; - } - - setBlock(gx, gy, t); - } - - // === Растительность и декор === - const b = BIOMES[biome]; - const top = getBlock(gx, sgy); - - // цветы (только plain) - if(biome === 'plains' && top && top.t === 'grass' && seededRandom(gx, sgy-1) < 0.10) { - setBlock(gx, sgy-1, 'flower'); - } - - // деревья - if(b.trees && seededRandom(gx*7, sgy-2) < b.treeChance) { - if(biome === 'tundra') { - // ёлки (треугольные, 3-5 высоты) - const th = 3 + Math.floor(seededRandom(gx, sgy) * 3); - for(let i = 0; i < th; i++) setBlock(gx, sgy-1-i, 'wood'); - // крона (треугольник) - for(let row = 0; row < th; row++) { - const w = Math.min(row + 1, 2); - for(let dx = -w; dx <= w; dx++) { - const ly = sgy-1-th+row; - if(ly >= 0) setBlock(gx+dx, ly, 'spruce_leaves'); - } - } - } else if(biome === 'swamp') { - // болотное дерево (короче, с мхом) - setBlock(gx, sgy-1, 'wood'); - setBlock(gx, sgy-2, 'moss'); - setBlock(gx-1, sgy-2, 'leaves'); - setBlock(gx+1, sgy-2, 'leaves'); - } else { - // обычное дерево (plains) - setBlock(gx, sgy-1, 'wood'); - setBlock(gx, sgy-2, 'wood'); - setBlock(gx, sgy-3, 'leaves'); - setBlock(gx-1, sgy-3, 'leaves'); - setBlock(gx+1, sgy-3, 'leaves'); - } - } - - // кактусы (пустыня) - if(biome === 'desert' && seededRandom(gx*11, sgy) < 0.04) { - const ch = 1 + Math.floor(seededRandom(gx, sgy+1) * 2); - for(let i = 0; i < ch; i++) setBlock(gx, sgy-1-i, 'cactus'); - } - - // грибы (болото) - if(biome === 'swamp' && seededRandom(gx*13, sgy) < 0.06) { - setBlock(gx, sgy-1, 'mushroom'); - } - - // сухие кусты (пустыня) - if(biome === 'desert' && seededRandom(gx*17, sgy) < 0.05) { - setBlock(gx, sgy-1, 'dead_bush'); - } - - // === Структуры мира === - // Пирамида в пустыне - if(biome === 'desert' && ((gx % 200 + 200) % 200) === 47 && sgy < SEA_GY && sgy > SEA_GY - 5) { - placeStructure(gx, sgy - 5, PYRAMID_PATTERN); - } - // Дом в равнине - if(biome === 'plains' && ((gx % 150 + 150) % 150) === 33 && sgy < SEA_GY) { - placeStructure(gx, sgy - 4, HOUSE_PATTERN); - } - // Хижина в болоте - if(biome === 'swamp' && ((gx % 180 + 180) % 180) === 55 && sgy < SEA_GY) { - placeStructure(gx, sgy - 3, HUT_PATTERN); - } - - // Применяем серверные оверрайды для этой колонны - const colPrefix = gx + ','; - for (const [key, ov] of serverOverrides) { - if (!key.startsWith(colPrefix)) continue; - if (ov.op === 'remove') { - const b = grid.get(key); - if (b) { grid.delete(key); b.dead = true; } - } else if (ov.op === 'set') { - if (!grid.has(key)) { - const gy = parseInt(key.split(',')[1]); - const nb = { gx, gy, t: ov.t, dead: false, active: false, fuse: 0 }; - grid.set(key, nb); - blocks.push(nb); - } - } - } - } - - // Перегенерация видимых чанков (используется при загрузке сохранения) - function regenerateVisibleChunks(){ - const gx0 = Math.floor(camX/TILE); - for(let gx=gx0-GEN_MARGIN_X; gx<=gx0+GEN_MARGIN_X; gx++){ - // Принудительно перегенерируем колонну - generated.delete(gx); - genColumn(gx); - } - } - - function ensureGenAroundCamera(){ - const gx0 = Math.floor(camX/TILE); - for(let gx=gx0-GEN_MARGIN_X; gx<=gx0+GEN_MARGIN_X; gx++){ - genColumn(gx); - } - } - - // Лут с дерева/листвы: дерево -> wood; листья -> leaves - // (уже в mine добавляется inv[type] автоматически) - - // Рисование костра: огонь поверх текстуры - function drawFire(wx,wy,now){ + // src/render/draw-fire.js + function drawFire(ctx, wx, wy, now) { const baseX = wx; const baseY = wy; - const flick = 1.5 + (Math.sin(now/200)+1)*1; - ctx.fillStyle = 'rgba(255,140,0,0.85)'; + const flick = 6 + (Math.sin(now / 90) + 1) * 4; + ctx.fillStyle = "rgba(255,140,0,0.85)"; ctx.beginPath(); - ctx.moveTo(baseX+10, baseY+30); - ctx.lineTo(baseX+20, baseY+30-flick); - ctx.lineTo(baseX+30, baseY+30); + ctx.moveTo(baseX + 10, baseY + 30); + ctx.lineTo(baseX + 20, baseY + 30 - flick); + ctx.lineTo(baseX + 30, baseY + 30); ctx.fill(); - - ctx.fillStyle = 'rgba(255,230,150,0.75)'; + ctx.fillStyle = "rgba(255,230,150,0.75)"; ctx.beginPath(); - ctx.moveTo(baseX+14, baseY+30); - ctx.lineTo(baseX+20, baseY+30-(flick*0.7)); - ctx.lineTo(baseX+26, baseY+30); + ctx.moveTo(baseX + 14, baseY + 30); + ctx.lineTo(baseX + 20, baseY + 30 - flick * 0.7); + ctx.lineTo(baseX + 26, baseY + 30); ctx.fill(); } - - // Моб AI - function mobAI(m, dt){ - updateWaterFlag(m); - - if(m.kind==='zombie'){ - // активность ночью - const night = isNight(); - if(!night){ m.hp -= 30*dt; m.vx *= 0.5; return; } - const dir = Math.sign((player.x) - m.x); - m.vx = dir * m.speed; - if(m.inWater && Math.random()<0.06) m.vy = -260; - // атака - if(Math.abs((m.x+ m.w/2) - (player.x+player.w/2)) < 28 && - Math.abs((m.y+ m.h/2) - (player.y+player.h/2)) < 40 && - player.invuln <= 0){ - const damage = calculateDamage(15); - player.hp -= damage; - player.invuln = 0.8; - player.vx += dir*420; - player.vy -= 260; - playSound('hit1'); // Звук при атаке зомби - } - } else if(m.kind==='creeper'){ - // активность ночью - const night = isNight(); - if(!night){ m.hp -= 30*dt; m.vx *= 0.5; return; } - const dir = Math.sign((player.x) - m.x); - const dist = Math.hypot((player.x+player.w/2) - (m.x+m.w/2), (player.y+player.h/2) - (m.y+m.h/2)); - - // Движение к игроку - m.vx = dir * m.speed; - if(m.inWater && Math.random()<0.06) m.vy = -260; - - // Взрыв если близко к игроку - if(dist < 60){ - m.fuse -= dt; - if(m.fuse <= 0){ - explodeAt(Math.floor((m.x+m.w/2)/TILE), Math.floor((m.y+m.h/2)/TILE)); - m.hp = 0; - } - } else { - // Поджигаем если очень близко - if(dist < 40){ - m.fuse = 0.5; // Быстрый взрыв - } - } - } else if(m.kind==='skeleton'){ - // активность ночью - const night = isNight(); - if(!night){ m.hp -= 30*dt; m.vx *= 0.5; return; } - const dir = Math.sign((player.x) - m.x); - const dist = Math.hypot((player.x+player.w/2) - (m.x+m.w/2), (player.y+player.h/2) - (m.y+m.h/2)); - - // Движение к игроку - m.vx = dir * m.speed; - if(m.inWater && Math.random()<0.06) m.vy = -260; - - // Стрельба стрелами - m.shootCooldown -= dt; - if(dist < 300 && m.shootCooldown <= 0){ - m.shootCooldown = 2.0; - const dx = (player.x+player.w/2) - (m.x+m.w/2); - const dy = (player.y+player.h/2) - (m.y+m.h/2); - const angle = Math.atan2(dy, dx); - const speed = 450; - projectiles.push({ - x: m.x + m.w/2, - y: m.y + m.h/3, - vx: Math.cos(angle) * speed, - vy: Math.sin(angle) * speed, - dmg: 6, - owner: 'mob', - life: 3 - }); - } - } else if(m.kind==='scorpion') { - // Скорпион — бежит к игроку, яд (замедление) - const dir = Math.sign((player.x) - m.x); - m.vx = dir * m.speed; - if(m.inWater && Math.random()<0.06) m.vy = -260; - // Яд при касании — замедление на 3 сек - if(Math.abs((m.x+ m.w/2) - (player.x+player.w/2)) < 28 && - Math.abs((m.y+ m.h/2) - (player.y+player.h/2)) < 40 && - player.invuln <= 0){ - const damage = calculateDamage(8); - player.hp -= damage; - player.invuln = 0.8; - player.slowTimer = 3; // замедление - player.vx += dir*300; - player.vy -= 200; - playSound('hit1'); - } - } else if(m.kind==='polar_bear') { - // Белый медведь — нейтрален, атакует если ударили (hostile пока нет, атакует через proximity) - m.aiT -= dt; - if(m.aiT <= 0){ - m.aiT = 2.0 + Math.random()*3; - m.dir = Math.random()<0.5 ? -1 : 1; - if(Math.random()<0.3) m.dir = 0; - } - m.vx = m.dir * m.speed; - if(m.inWater) m.vy = -120; - } else if(m.kind==='slime') { - // Слизь — прыгает к игроку - const dir = Math.sign((player.x+player.w/2) - (m.x+m.w/2)); - m.aiT -= dt; - if(m.aiT <= 0){ - m.aiT = 1.5 + Math.random()*1.5; - m.dir = dir; - // Прыжок - m.vy = -200; - } - m.vx = m.dir * m.speed; - } else if(m.kind==='eagle') { - // Орёл — летает, атакует пикированием - const dx = (player.x+player.w/2) - (m.x+m.w/2); - const dy = (player.y+player.h/2) - (m.y+m.h/2); - const dist = Math.hypot(dx, dy); - if(dist < 400) { - // Пикирует на игрока - m.vx = Math.sign(dx) * m.speed; - m.vy = dy > 0 ? 60 : -60; // летит вниз к игроку - } else { - // Патрулирует - m.aiT -= dt; - if(m.aiT <= 0){ - m.aiT = 2+Math.random()*3; - m.dir = Math.random()<0.5 ? -1 : 1; - } - m.vx = m.dir * m.speed * 0.5; - m.vy = Math.sin(performance.now()/1000) * 30; // мягкое покачивание - } - // Атака при касании - if(dist < 30 && player.invuln <= 0){ - const damage = calculateDamage(10); - player.hp -= damage; - player.invuln = 0.8; - player.vy -= 200; - playSound('hit1'); - } - // Орёл не падает — летающий моб - m.vy *= 0.5; - m.grounded = false; - } else { - // животные (pig, chicken) - m.aiT -= dt; - if(m.aiT <= 0){ - m.aiT = 1.8 + Math.random()*2.5; - m.dir = Math.random()<0.5 ? -1 : 1; - if(Math.random()<0.25) m.dir = 0; - } - m.vx = m.dir * (m.kind==='chicken' ? 55 : 40); - if(m.inWater) m.vy = -120; - } - - // физика моба - const g = m.inWater ? GRAV_WATER : GRAV; - m.vy += g*dt; - - m.y += m.vy*dt; m.grounded=false; resolveY(m); - m.x += m.vx*dt; resolveX(m); - } - - function isNight(){ - // Автоматический цикл: ночь когда worldTime > 0.5 - return worldTime > 0.5; - } - - // Respawn - document.getElementById('respawnBtn').onclick = async () => { - playSound('click'); // Звук клика по кнопке - - console.log('=== RESPAWN CLICKED ==='); - console.log('isMultiplayer:', isMultiplayer); - console.log('otherPlayers.size:', otherPlayers.size); - console.log('player.hp before respawn:', player.hp); - - // В мультиплеере не загружаем сохранение, а возрождаемся в начальной точке - if (isMultiplayer && otherPlayers.size > 0) { - console.log('Мультиплеер режим - возрождение в начальной точке'); - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.vx = player.vy = 0; - player.invuln = 0; - player.x = spawnPoint.x; - player.y = spawnPoint.y; - player.fallStartY = player.y; - console.log('Возрождение в начальной точке, HP:', player.hp); - } else { - console.log('Одиночный режим - загружаем последнее сохранение'); - // Одиночный режим - загружаем последнее сохранение - const loadedSave = await loadGame(); - if(loadedSave){ - await applySave(loadedSave); - console.log('Загружено последнее сохранение после смерти, final HP:', player.hp); - } else { - // Если сохранения нет, возрождаемся в начальной точке - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.vx = player.vy = 0; - player.invuln = 0; - player.x = spawnPoint.x; - player.y = spawnPoint.y; - player.fallStartY = player.y; - console.log('Возрождение в начальной точке, HP:', player.hp); - } - } - - console.log('player.hp after respawn logic:', player.hp); - console.log('Hiding death screen...'); - deathEl.style.display='none'; - console.log('=== RESPAWN END ==='); - }; - - // Resize - function resize(){ - W = gameEl.clientWidth; - H = gameEl.clientHeight; - canvas.width = W*dpr; - canvas.height = H*dpr; - lightC.width = W*dpr; - lightC.height = H*dpr; - ctx.setTransform(dpr,0,0,dpr,0,0); - } - window.addEventListener('resize', resize); - - // init - resize(); - rebuildHotbar(); - - // Инициализируем и загружаем сохранение - initDB().then(async () => { - // Пытаемся загрузить сохранённую игру - const loadedSave = await loadGame(); - if(loadedSave){ - await applySave(loadedSave); - console.log('Загружено сохранение, HP:', player.hp); - - // Проверяем HP после загрузки - если <= 0, возрождаемся - if (player.hp <= 0) { - console.log('WARNING: HP <= 0 после загрузки, возрождаемся'); - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.x = spawnPoint.x; - player.y = spawnPoint.y; - player.vx = player.vy = 0; - player.invuln = 0; - player.fallStartY = player.y; - } - } else { - console.log('Сохранение не найдено, начинаем новую игру'); - - // Инициализируем игрока для новой игры - player.hp = 100; - player.hunger = 100; - player.o2 = 100; - player.vx = player.vy = 0; - player.invuln = 0; - - // старт — на поверхности (используем ту же логику что и в world_state) - const startGX = 6; - for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx); - const surfaceY = surfaceGyAt(startGX); - let safeGY = surfaceY - 1; - const aboveBlock = getBlock(startGX, surfaceY - 1); - if (aboveBlock && aboveBlock.t === 'water') { - for (let gy = SEA_GY - 1; gy >= 0; gy--) { - const b = getBlock(startGX, gy); - if (!b || b.dead || b.t === 'air' || b.t === 'water') continue; - safeGY = gy - 1; - break; - } - } - player.y = safeGY * TILE; - player.x = startGX * TILE; - player.fallStartY = player.y; - - // Обновляем spawnPoint, чтобы возрождение было на поверхности - spawnPoint.x = player.x; - spawnPoint.y = player.y; - - console.log('Новая игра: startGX=', startGX, 'surfaceY=', surfaceY, 'player.y=', player.y, 'player.hp=', player.hp); - console.log('spawnPoint обновлён: x=', spawnPoint.x, 'y=', spawnPoint.y); - - // Генерируем карту вокруг стартовой позиции при инициализации - for(let gx = startGX - 50; gx <= startGX + 50; gx++){ - genColumn(gx); - } - } - - // Автосейв при скрытии страницы (защита от потери прогресса) - document.addEventListener('visibilitychange', () => { - if(document.hidden){ - saveGame(); - } - }); - - // Автосейв перед закрытием страницы (защита от потери прогресса) - window.addEventListener('beforeunload', () => { - saveGame(); - }); - }).catch(err => { - console.error('Ошибка инициализации:', err); - // При ошибке начинаем новую игру - const startGX = 6; - genColumn(startGX); - player.y = (surfaceGyAt(startGX)-1)*TILE; - player.fallStartY = player.y; - - for(let gx = startGX - 50; gx <= startGX + 50; gx++){ - genColumn(gx); - } - }); - - // main loop - let last = performance.now(); - let prevJump = false; - // При возврате на вкладку — сбрасываем last чтобы не было скачка dt - document.addEventListener('visibilitychange', () => { + + // src/game/loop.js + var last = performance.now(); + var prevJump = false; + document.addEventListener("visibilitychange", () => { if (!document.hidden) last = performance.now(); }); - function loop(now){ - const rawDt = Math.min(0.05, (now-last)/1000); + function loop(now) { + const rawDt = Math.min(0.05, (now - last) / 1e3); last = now; - // Sub-stepping: разбиваем физику на шаги ≤16ms чтобы не проваливаться сквозь блоки const PHYSICS_STEP = 0.016; const steps = Math.max(1, Math.ceil(rawDt / PHYSICS_STEP)); const dt = rawDt / steps; - - const jumpPressed = inp.j && !prevJump; - prevJump = inp.j; - - // Ускорение времени во время сна - if(player.sleeping && isNight()){ - worldTime += dt * 8 / DAY_LEN; // В 8 раз быстрее - // Восстанавливаем здоровье во время сна + const player = state.player; + const inv = state.inv; + const inp2 = state.inp; + const clouds = state.clouds; + const mobs = state.mobs; + const projectiles = state.projectiles; + const parts2 = state.parts; + const boat = state.boat; + const jumpPressed = inp2.j && !prevJump; + prevJump = inp2.j; + if (player.sleeping && isNight()) { + state.worldTime += dt * 8 / state.DAY_LEN; player.hp = Math.min(100, player.hp + dt * 20); - // Автоматическое пробуждение когда наступает день - if(!isNight()){ + if (!isNight()) { player.sleeping = false; } } else { - worldTime += dt / DAY_LEN; + state.worldTime += dt / state.DAY_LEN; } - if(worldTime >= 1) worldTime -= 1; - - // камера следует за игроком по X/Y - camX = Math.floor((player.x + player.w/2) - W/2); - camY = Math.floor((player.y + player.h/2) - H/2); - + if (state.worldTime >= 1) state.worldTime -= 1; + state.camX = Math.floor(player.x + player.w / 2 - state.W / 2); + state.camY = Math.floor(player.y + player.h / 2 - state.H / 2); ensureGenAroundCamera(); - - // clouds parallax - for(const c of clouds){ + for (const c of clouds) { c.x -= c.s * dt; - if(c.x + c.w < camX - 400) c.x = camX + W + Math.random()*700; + if (c.x + c.w < state.camX - 400) c.x = state.camX + state.W + Math.random() * 700; } - - // Погода (биом-зависимая) - updateWeather(dt); - - // player updateWaterFlag(player); - - // кислород/утопление: тратим O2 только если голова под водой, иначе восстанавливаем [web:223] - if(player.headInWater){ - player.o2 = Math.max(0, player.o2 - 6*dt); // замедлил в 3.7 раза - if(player.o2 === 0){ - const damage = calculateDamage(4*dt); + if (player.headInWater) { + player.o2 = Math.max(0, player.o2 - 6 * dt); + if (player.o2 === 0) { + const damage = calculateDamage(4 * dt); player.hp -= damage; } } else { - player.o2 = Math.min(100, player.o2 + 10*dt); // замедлил восстановление в 4 раза + player.o2 = Math.min(100, player.o2 + 10 * dt); } - - // голод убывает, но HP не отнимает (как просили) - player.hunger = Math.max(0, player.hunger - dt*0.2); // замедлил в 4 раза - - - // ========== ТЕМПЕРАТУРА ========== - if (player.temperature === undefined) player.temperature = 15; - const BIOME_TEMP = { tundra: -20, plains: 15, swamp: 20, desert: 45, mountains: 5 }; - const UNDERGROUND_TEMP = 15; - const COLD_THRESHOLD = -5; - const HEAT_THRESHOLD = 35; - const HEAT_RADIUS = 5; - const pGX = Math.floor(player.x / TILE); - const biome = getCachedBiome(pGX); - const pGY = Math.floor(player.y / TILE); - const sGY = surfaceGyAt(pGX); - const isUndergroundTemp = (pGY - sGY) > 2; // player deeper than 2 blocks below surface - let targetTemp = BIOME_TEMP[biome] || 15; - if (isUndergroundTemp) targetTemp = UNDERGROUND_TEMP; - if (isNight() && !isUndergroundTemp) targetTemp -= 10; - if ((weatherState.type === 'rain' || weatherState.type === 'storm') && !isUndergroundTemp) targetTemp -= 5; - player.temperature += (targetTemp - player.temperature) * dt * 0.3; - let nearHeat = false, nearCool = false; - for (let dx = -HEAT_RADIUS; dx <= HEAT_RADIUS; dx++) { - for (let dy = -HEAT_RADIUS; dy <= HEAT_RADIUS; dy++) { - const b = getBlock(pGX + dx, pGY + dy); - if (!b || b.dead) continue; - if (b.t === 'campfire' || b.t === 'torch') nearHeat = true; - if (b.t === 'water' && biome === 'desert') nearCool = true; - } - } - if (nearHeat) { - player.temperature += (30 - player.temperature) * dt * 2; - if (player.hp < 100) player.hp = Math.min(100, player.hp + 8 * dt); - } - if (nearCool) { - player.temperature += (15 - player.temperature) * dt * 1.5; - if (player.hp < 100 && player.temperature > HEAT_THRESHOLD) { - player.hp = Math.min(100, player.hp + 8 * dt); - } - } - if (isUndergroundTemp && !nearHeat) { - if (player.hp < 100 && player.temperature >= COLD_THRESHOLD && player.temperature <= HEAT_THRESHOLD) { - player.hp = Math.min(100, player.hp + 2 * dt); - } - } - const aboveBlk = getBlock(pGX, pGY - 1); - const inShade = aboveBlk && BLOCKS[aboveBlk.t] && BLOCKS[aboveBlk.t].solid && aboveBlk.t !== 'glass'; - if (inShade && !isUndergroundTemp && !nearCool) { - player.temperature -= 5 * dt; - if (player.temperature > HEAT_THRESHOLD - 2 && player.hp < 100) { - player.hp = Math.min(100, player.hp + 1 * dt); - } - } - if (player.temperature < COLD_THRESHOLD && !isUndergroundTemp) { - const severity = Math.abs(player.temperature - COLD_THRESHOLD) / 15; - player.hp -= 1 * severity * dt; - if (severity > 0.5) player.vx *= (1 - 0.3 * Math.min(1, severity) * dt); - } - if (player.temperature > HEAT_THRESHOLD && !isUndergroundTemp) { - const severity = (player.temperature - HEAT_THRESHOLD) / 15; - player.hp -= 1 * severity * dt; - if (severity > 0.5) player.hunger -= 0.5 * severity * dt; - } - // Игрок не может двигаться во время сна - if(player.sleeping){ + player.hunger = Math.max(0, player.hunger - dt * 0.2); + if (player.sleeping) { player.vx = 0; player.vy = 0; } else { - const dir = (inp.r?1:0) - (inp.l?1:0); - const speedMult = getWeatherSpeedMultiplier() * (player.slowTimer > 0 ? 0.4 : 1.0); - if(dir) player.vx = dir * MOVE * speedMult; + const dir = (inp2.r ? 1 : 0) - (inp2.l ? 1 : 0); + if (dir) player.vx = dir * state.MOVE; else player.vx *= 0.82; } - - // Звук шагов при движении по земле - if(player.grounded && !player.inWater && Math.abs(player.vx) > 50){ - const stepInterval = 0.35; // Интервал между шагами в секундах - if(now/1000 - player.lastStepTime > stepInterval){ - playSound('step'); - player.lastStepTime = now/1000; + if (player.grounded && !player.inWater && Math.abs(player.vx) > 50) { + const stepInterval = 0.35; + if (now / 1e3 - player.lastStepTime > stepInterval) { + playSound("step"); + player.lastStepTime = now / 1e3; } } - - // прыжок/плавание (новая логика) - if(player.inBoat){ - // Игрок в лодке - лодка следует за игроком - const dir = (inp.r?1:0) - (inp.l?1:0); - if(dir) boat.vx = dir * MOVE; + if (player.inBoat) { + const dir = (inp2.r ? 1 : 0) - (inp2.l ? 1 : 0); + if (dir) boat.vx = dir * state.MOVE; else boat.vx *= 0.95; - - // Лодка плавает на воде boat.vy = 0; - - // Игрок следует за лодкой (сидит внутри неё) - player.x = boat.x + 2; // Игрок по центру лодки - player.y = boat.y - 4; // Игрок выше лодки (сидит внутри) + player.x = boat.x + 2; + player.y = boat.y - 4; player.vx = boat.vx; player.vy = boat.vy; player.grounded = true; - player.inWater = false; // Игрок не в воде когда в лодке - - // Прыжок из лодки (высадка) - if(jumpPressed){ - // Возвращаем лодку в инвентарь + player.inWater = false; + if (jumpPressed) { inv.boat = (inv.boat || 0) + 1; - player.inBoat = false; boat.active = false; - player.y += TILE; // Прыгаем из лодки - player.vy = -JUMP * 0.5; - playSound('splash'); + player.y += state.TILE; + player.vy = -state.JUMP * 0.5; + playSound("splash"); } - - } else if(player.inWater){ - // сопротивление в воде - player.vx *= 0.90; + } else if (player.inWater) { + player.vx *= 0.9; player.vy *= 0.92; - - // Если не нажимаем прыжок - тонем (гравитация в воде) - if(!jumpPressed && !inp.j){ - // Применяем гравитацию в воде - игрок тонет - player.vy += GRAV_WATER * dt; + if (!jumpPressed && !inp2.j) { + player.vy += state.GRAV_WATER * dt; } else { - // Если нажимаем прыжок - поднимаемся на поверхность - if(jumpPressed){ - player.vy = Math.min(player.vy, -520); // рывок вверх - } else if(inp.j){ - // если держим — мягкое всплытие + if (jumpPressed) { + player.vy = Math.min(player.vy, -520); + } else if (inp2.j) { player.vy = Math.min(player.vy, -260); } } - } else { - // обычный прыжок (только по нажатию) - if(jumpPressed && player.grounded && !player.sleeping){ - player.vy = -JUMP; + if (jumpPressed && player.grounded && !player.sleeping) { + player.vy = -state.JUMP; player.grounded = false; player.fallStartY = player.y; } } - - // Гравитация применяется только вне воды и вне лодки - if(!player.inWater && !player.inBoat){ - player.vy += GRAV*dt; + if (!player.inWater && !player.inBoat) { + player.vy += state.GRAV * dt; } - - // Обновляем позицию лодки - if(boat.active){ + if (boat.active) { boat.x += boat.vx * dt; boat.y += boat.vy * dt; - - // Лодка не выходит за пределы воды - const boatGX = Math.floor(boat.x / TILE); - const boatGY = Math.floor(boat.y / TILE); + const boatGX = Math.floor(boat.x / state.TILE); + const boatGY = Math.floor(boat.y / state.TILE); const below = getBlock(boatGX, boatGY + 1); - - if(!below || below.t !== 'water'){ - // Если лодка вышла из воды - выкидываем игрока + if (!below || below.t !== "water") { inv.boat = (inv.boat || 0) + 1; player.inBoat = false; boat.active = false; - player.y += TILE; + player.y += state.TILE; player.vy = -200; - playSound('splash'); + playSound("splash"); } } - - // Проверяем, не доплыл ли игрок из лодки - if(player.inBoat && !boat.active){ + if (player.inBoat && !boat.active) { inv.boat = (inv.boat || 0) + 1; player.inBoat = false; - player.y += TILE; + player.y += state.TILE; player.vy = -200; - playSound('splash'); + playSound("splash"); } - - // Sub-stepped physics: применяем движение мелкими шагами for (let step = 0; step < steps; step++) { - player.y += player.vy*dt; + player.y += player.vy * dt; resolveY(player); - player.x += player.vx*dt; + player.x += player.vx * dt; resolveX(player); } - - // Отправляем позицию на сервер (мультиплеер) sendPlayerPosition(); - - // Обновляем физику воды updateWaterPhysics(dt); - - // Погода и дождь updateWeather(dt); updateRain(dt); - player.invuln = Math.max(0, player.invuln - dt); - if(player.slowTimer > 0) player.slowTimer = Math.max(0, player.slowTimer - dt); - - // Рост культур - for(const key of Object.keys(growthTimers)){ - const tile = growthTimers[key]; - if(tile.stage < 3){ - tile.growTimer -= dt; - if(tile.growTimer <= 0){ - tile.stage++; - tile.growTimer = 8 + Math.random()*6; - // Обновляем визуальный блок - const [gxStr, gyStr] = key.split(','); - const gx = parseInt(gxStr), gy = parseInt(gyStr); - const curBlock = getBlock(gx, gy); - if(curBlock && curBlock.t !== curBlock.t.replace(/_stage\d/, '_stage'+tile.stage)){ - // Вычисляем следующую стадию - const baseType = curBlock.t.replace(/_stage\d/, ''); - const nextType = baseType + '_stage' + tile.stage; - setBlock(gx, gy, nextType); - sendBlockChange(gx, gy, nextType, 'add'); - // Проверяем созрел ли - if(tile.stage >= 3) delete growthTimers[key]; - } - } - } + state.voicePosT += dt; + if (state.voicePosT > 0.5 && state.voiceSocket && state.voiceSocket.connected) { + state.voicePosT = 0; + state.voiceSocket.emit("voice_pos", { x: player.x, y: player.y }); } - - // Voice position update - voicePosT += dt; - if(voicePosT > 0.5 && voiceSocket && voiceSocket.connected){ - voicePosT = 0; - voiceSocket.emit('voice_pos', { x: player.x, y: player.y }); - } - - // Furnace tick tickFurnaces(dt); - - // Обновляем UI печи если открыта - if(currentFurnaceKey && Math.random() < 0.1){ + if (state.currentFurnaceKey && Math.random() < 0.1) { renderFurnaceUI(); } - - // Projectile tick (стрелы) - for(let i = projectiles.length-1; i>=0; i--){ + for (let i = projectiles.length - 1; i >= 0; i--) { const p = projectiles[i]; p.x += p.vx * dt; p.y += p.vy * dt; - p.vy += 400 * dt; // гравитация + p.vy += 400 * dt; p.life -= dt; - - // Столкновение с блоком - const gx = Math.floor(p.x / TILE); - const gy = Math.floor(p.y / TILE); + const gx = Math.floor(p.x / state.TILE); + const gy = Math.floor(p.y / state.TILE); const blk = getBlock(gx, gy); - if(blk && !blk.dead && BLOCKS[blk.t] && BLOCKS[blk.t].solid){ - // Врезался в стену - if(p.owner === 'player' && Math.random() < 0.5) inv.arrow++; // подбираем ~50% + if (blk && !blk.dead && BLOCKS[blk.t] && BLOCKS[blk.t].solid) { + if (p.owner === "player" && Math.random() < 0.5) inv.arrow++; projectiles.splice(i, 1); continue; } - - // Столкновение с сущностью - if(p.owner === 'mob'){ - // Попал в игрока - if(p.x > player.x && p.x < player.x+player.w && p.y > player.y && p.y < player.y+player.h){ - if(player.invuln <= 0){ + if (p.owner === "mob") { + if (p.x > player.x && p.x < player.x + player.w && p.y > player.y && p.y < player.y + player.h) { + if (player.invuln <= 0) { player.hp -= calculateDamage(p.dmg); player.invuln = 0.4; player.vx += p.vx * 0.3; player.vy -= 150; - playSound('hit1'); + playSound("hit1"); } projectiles.splice(i, 1); continue; } } else { - // Попал в моба — check all mobs (client-authoritative) - const allArrowMobs = getAllMobs(); - for(let j = allArrowMobs.length - 1; j >= 0; j--){ - const m = allArrowMobs[j]; - if(m.dead) continue; - if(p.x > m.x && p.x < m.x+m.w && p.y > m.y && p.y < m.y+m.h){ - m.hp -= p.dmg; - m.vx += p.vx * 0.2; - m.vy -= 200; - // Server-spawned mob: emit arrow hit to server for relay - if(m.id !== undefined && isMultiplayer){ - socket.emit('mob_arrow_hit', { id: m.id, dmg: p.dmg, vx: p.vx }); - if(m.hp <= 0){ - socket.emit('mob_died', { id: m.id }); - } + let hitMob = false; + if (state.isMultiplayer) { + for (const [id, sm] of state.serverMobs) { + if (sm.dead) continue; + if (p.x > sm.x && p.x < sm.x + sm.w && p.y > sm.y && p.y < sm.y + sm.h) { + state.socket.emit("mob_arrow_hit", { id: sm.id, dmg: p.dmg, vx: p.vx }); + projectiles.splice(i, 1); + hitMob = true; + break; } - if(m.hp <= 0){ - spawnDrops(m.x + m.w/2, m.y + m.h/2, m.kind); - // Remove from the correct array - if(m.id !== undefined){ - serverMobs.delete(m.id); - } else { - const localIdx = mobs.indexOf(m); - if(localIdx >= 0) mobs.splice(localIdx, 1); + } + } + if (!hitMob) { + for (let j = mobs.length - 1; j >= 0; j--) { + const m = mobs[j]; + if (p.x > m.x && p.x < m.x + m.w && p.y > m.y && p.y < m.y + m.h) { + m.hp -= p.dmg; + m.vx += p.vx * 0.2; + m.vy -= 200; + if (m.hp <= 0) { + inv.meat += m.kind === "chicken" ? 1 : 2; + if (m.kind === "skeleton") { + inv.arrow += 2 + Math.floor(Math.random() * 3); + if (Math.random() < 0.15) inv.bow = (inv.bow || 0) + 1; + } + mobs.splice(j, 1); + rebuildHotbar(); } - rebuildHotbar(); + projectiles.splice(i, 1); + break; } - projectiles.splice(i, 1); - break; } } } - - // Таймаут - if(p.life <= 0) projectiles.splice(i, 1); + if (p.life <= 0) projectiles.splice(i, 1); } - - // TNT tick - for(const key of Array.from(activeTNT)){ - const b = grid.get(key); - if(!b || b.dead){ activeTNT.delete(key); continue; } + for (const key of Array.from(state.activeTNT)) { + const b = state.grid.get(key); + if (!b || b.dead) { + state.activeTNT.delete(key); + continue; + } b.fuse -= dt; - if(b.fuse <= 0){ - explodeAt(b.gx,b.gy); + if (b.fuse <= 0) { + explodeAt(b.gx, b.gy); } } - - // mobs spawn (с обеих сторон камеры) — только в одиночном режиме (MP mobs come from server events) - spawnT += dt; - if(!isMultiplayer && spawnT > 1.8 && getAllMobs().length < 30){ - spawnT = 0; - - // Выбираем сторону спавна (левая или правая) + state.spawnT += dt; + if (!state.isMultiplayer && state.spawnT > 1.8 && mobs.length < 30) { + state.spawnT = 0; const spawnLeft = Math.random() < 0.5; - const gx = spawnLeft - ? Math.floor((camX - 200)/TILE) - : Math.floor((camX + W + 200)/TILE); - + const gx = spawnLeft ? Math.floor((state.camX - 200) / state.TILE) : Math.floor((state.camX + state.W + 200) / state.TILE); genColumn(gx); const sgy = surfaceGyAt(gx); - const wx = gx*TILE + 4; - const wy = (sgy-1)*TILE; // 1 блок выше поверхности - - // не спавнить в воде + const wx = gx * state.TILE + 4; + const wy = (sgy - 2) * state.TILE; const top = getBlock(gx, sgy); - if(top && top.t==='water') { - // skip + if (top && top.t === "water") { } else { - const night = isNight(); - if(night){ - // Ночью спавним враждебных мобов (максимум 12 хостайл) - const hostileCount = mobs.filter(m => m.kind==='zombie'||m.kind==='creeper'||m.kind==='skeleton').length; - if(hostileCount < 12){ + const night2 = isNight(); + if (night2) { + const hostileCount = mobs.filter((m) => m.kind === "zombie" || m.kind === "creeper" || m.kind === "skeleton").length; + if (hostileCount < 12) { const rand = Math.random(); - if(rand < 0.35){ + if (rand < 0.35) { mobs.push(new Zombie(wx, wy)); - } else if(rand < 0.55){ + } else if (rand < 0.55) { mobs.push(new Creeper(wx, wy)); } else { mobs.push(new Skeleton(wx, wy)); } } } - // Животные спавнятся и днём и ночью (с лимитом) - const animalCount = mobs.filter(m => m.kind==='pig'||m.kind==='chicken').length; - if(animalCount < 8){ - mobs.push(Math.random()<0.5 ? new Pig(wx, wy) : new Chicken(wx, wy)); + const animalCount = mobs.filter((m) => m.kind === "pig" || m.kind === "chicken").length; + if (animalCount < 8) { + mobs.push(Math.random() < 0.5 ? new Pig(wx, wy) : new Chicken(wx, wy)); } } } - - // mobs update — mobAI runs on ALL mobs (client-authoritative for server-spawned mobs too) - { - // Local mobs - for(let i=mobs.length-1;i>=0;i--){ + if (!state.isMultiplayer) { + for (let i = mobs.length - 1; i >= 0; i--) { const m = mobs[i]; mobAI(m, dt); - if(m.hp<=0) mobs.splice(i,1); - } - // Server-spawned mobs: server is authoritative for positions - // mobAI does NOT run on serverMobs — server handles all physics + AI - // Only remove dead mobs from local map - if(isMultiplayer){ - for (const [id, sm] of serverMobs) { - if(sm.hp <= 0){ - sm.dead = true; - } - } - for (const [id, sm] of serverMobs) { - if(sm.dead) serverMobs.delete(id); - } + if (m.hp <= 0) mobs.splice(i, 1); } } - - // particles - for(let i=parts.length-1;i>=0;i--){ - const p = parts[i]; + for (let i = parts2.length - 1; i >= 0; i--) { + const p = parts2[i]; p.t -= dt; - p.x += p.vx*dt; - p.y += p.vy*dt; - p.vy += GRAV*dt; - if(p.t <= 0) parts.splice(i,1); + p.x += p.vx * dt; + p.y += p.vy * dt; + p.vy += state.GRAV * dt; + if (p.t <= 0) parts2.splice(i, 1); } - - // death - if(player.hp <= 0){ - deathEl.style.display='flex'; - } else if(deathEl.style.display === 'flex') { - // Если HP > 0 но экран смерти всё ещё показан - скрываем его - deathEl.style.display='none'; + if (player.hp <= 0) { + state.deathEl.style.display = "flex"; + } else if (state.deathEl.style.display === "flex") { + state.deathEl.style.display = "none"; } - - // render + const ctx = state.ctx; + const W = state.W; + const H = state.H; + const camX = state.camX; + const camY = state.camY; + const TILE2 = state.TILE; + const tex2 = state.tex; const night = isNight(); - - // sky - ctx.fillStyle = night ? '#070816' : (isRaining ? '#6B7B8D' : '#87CEEB'); - ctx.fillRect(0,0,W,H); - - // clouds (parallax x/y) + ctx.fillStyle = night ? "#070816" : state.isRaining ? "#6B7B8D" : "#87CEEB"; + ctx.fillRect(0, 0, W, H); ctx.save(); - ctx.translate(-camX*0.5, -camY*0.15); - ctx.fillStyle = 'rgba(255,255,255,0.65)'; - for(const c of clouds){ + ctx.translate(-camX * 0.5, -camY * 0.15); + ctx.fillStyle = "rgba(255,255,255,0.65)"; + for (const c of clouds) { ctx.fillRect(c.x, c.y, c.w, 26); - ctx.fillRect(c.x+20, c.y-10, c.w*0.6, 22); + ctx.fillRect(c.x + 20, c.y - 10, c.w * 0.6, 22); } ctx.restore(); - - // world ctx.save(); ctx.translate(-camX, -camY); - - const minGX = Math.floor(camX/TILE)-2; - const maxGX = Math.floor((camX+W)/TILE)+2; - const minGY = Math.floor(camY/TILE)-6; - const maxGY = Math.floor((camY+H)/TILE)+6; - - // draw blocks (по массиву, но фильтруем диапазоном) - for(const b of blocks){ - if(b.dead) continue; - if(b.gx < minGX || b.gx > maxGX || b.gy < minGY || b.gy > maxGY) continue; - + const minGX = Math.floor(camX / TILE2) - 2; + const maxGX = Math.floor((camX + W) / TILE2) + 2; + const minGY = Math.floor(camY / TILE2) - 6; + const maxGY = Math.floor((camY + H) / TILE2) + 6; + const blocks2 = state.blocks; + for (const b of blocks2) { + if (b.dead) continue; + if (b.gx < minGX || b.gx > maxGX || b.gy < minGY || b.gy > maxGY) continue; const def = BLOCKS[b.t]; - if(def.alpha){ + if (def.alpha) { ctx.save(); ctx.globalAlpha = def.alpha; - ctx.drawImage(tex[b.t], b.gx*TILE, b.gy*TILE, TILE, TILE); + ctx.drawImage(tex2[b.t], b.gx * TILE2, b.gy * TILE2, TILE2, TILE2); ctx.restore(); } else { - ctx.drawImage(tex[b.t], b.gx*TILE, b.gy*TILE, TILE, TILE); + ctx.drawImage(tex2[b.t], b.gx * TILE2, b.gy * TILE2, TILE2, TILE2); } - - // TNT мигает, если активирован - if(b.t==='tnt' && b.active && Math.sin(now/60)>0){ - ctx.fillStyle='rgba(255,255,255,0.45)'; - ctx.fillRect(b.gx*TILE, b.gy*TILE, TILE, TILE); + if (b.t === "tnt" && b.active && Math.sin(now / 60) > 0) { + ctx.fillStyle = "rgba(255,255,255,0.45)"; + ctx.fillRect(b.gx * TILE2, b.gy * TILE2, TILE2, TILE2); } - - // огонь костра - if(b.t==='campfire'){ - drawFire(b.gx*TILE, b.gy*TILE, now); + if (b.t === "campfire") { + drawFire(ctx, b.gx * TILE2, b.gy * TILE2, now); } - // Печь — огонь когда обжигает - if(b.t==='furnace' && activeFurnaces.has(`${b.gx},${b.gy}`)){ - drawFire(b.gx*TILE + 8, b.gy*TILE + 5, now); + if (b.t === "furnace" && state.activeFurnaces.has(`${b.gx},${b.gy}`)) { + drawFire(ctx, b.gx * TILE2 + 8, b.gy * TILE2 + 5, now); } } - - // mobs - const allMobsRender = getAllMobs(); - for(const m of allMobsRender){ - if(m.kind==='zombie'){ - ctx.fillStyle = '#2ecc71'; + if (state.mining.gx !== null && state.mining.hardness > 0) { + const mg = state.mining; + const progress = Math.min(1, mg.progress / mg.hardness); + const cx = mg.gx * TILE2 - camX + camX; + const cy = mg.gy * TILE2 - camY + camY; + const wx = mg.gx * TILE2; + const wy = mg.gy * TILE2; + ctx.save(); + ctx.globalAlpha = Math.min(0.8, progress); + const numCracks = Math.floor(progress * 5); + ctx.strokeStyle = "#000"; + ctx.lineWidth = 2; + for (let i = 0; i < numCracks; i++) { + ctx.beginPath(); + ctx.moveTo(wx + TILE2 / 2, wy + TILE2 / 2); + const angle = (i * 72 + 36) * Math.PI / 180; + ctx.lineTo(wx + TILE2 / 2 + Math.cos(angle) * TILE2 * 0.4, wy + TILE2 / 2 + Math.sin(angle) * TILE2 * 0.4); + ctx.stroke(); + } + ctx.fillStyle = `rgba(0,0,0,${progress * 0.4})`; + ctx.fillRect(wx, wy, TILE2, TILE2); + ctx.globalAlpha = 1; + ctx.fillStyle = "#333"; + ctx.fillRect(wx, wy + TILE2 - 4, TILE2, 4); + ctx.fillStyle = "#2ecc71"; + ctx.fillRect(wx, wy + TILE2 - 4, TILE2 * progress, 4); + ctx.restore(); + } + const allMobsRender = state.isMultiplayer ? Array.from(state.serverMobs.values()) : mobs; + for (const m of allMobsRender) { + if (m.kind === "zombie") { + ctx.fillStyle = "#2ecc71"; ctx.fillRect(m.x, m.y, m.w, m.h); - ctx.fillStyle = '#c0392b'; - ctx.fillRect(m.x+6, m.y+12, 6,6); - ctx.fillRect(m.x+22, m.y+12, 6,6); - } else if(m.kind==='pig'){ - ctx.fillStyle = '#ffb6c1'; + ctx.fillStyle = "#c0392b"; + ctx.fillRect(m.x + 6, m.y + 12, 6, 6); + ctx.fillRect(m.x + 22, m.y + 12, 6, 6); + } else if (m.kind === "pig") { + ctx.fillStyle = "#ffb6c1"; ctx.fillRect(m.x, m.y, m.w, m.h); - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+22, m.y+5, 3,3); - ctx.fillStyle = '#ff69b4'; - ctx.fillRect(m.x+28, m.y+12, 6,6); - } else if(m.kind==='chicken'){ - // chicken - ctx.fillStyle = '#ecf0f1'; + ctx.fillStyle = "#000"; + ctx.fillRect(m.x + 22, m.y + 5, 3, 3); + ctx.fillStyle = "#ff69b4"; + ctx.fillRect(m.x + 28, m.y + 12, 6, 6); + } else if (m.kind === "chicken") { + ctx.fillStyle = "#ecf0f1"; ctx.fillRect(m.x, m.y, m.w, m.h); - ctx.fillStyle = '#f39c12'; - ctx.fillRect(m.x+18, m.y+10, 6,4); - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+8, m.y+6, 3,3); - } else if(m.kind==='creeper'){ - // creeper - ctx.fillStyle = '#4CAF50'; + ctx.fillStyle = "#f39c12"; + ctx.fillRect(m.x + 18, m.y + 10, 6, 4); + ctx.fillStyle = "#000"; + ctx.fillRect(m.x + 8, m.y + 6, 3, 3); + } else if (m.kind === "creeper") { + ctx.fillStyle = "#4CAF50"; ctx.fillRect(m.x, m.y, m.w, m.h); - // Глаза - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+8, m.y+8, 4,4); - ctx.fillRect(m.x+22, m.y+8, 4,4); - // Рот - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+12, m.y+20, 10,4); - // Ноги - ctx.fillStyle = '#4CAF50'; - ctx.fillRect(m.x+4, m.y+30, 6,20); - ctx.fillRect(m.x+24, m.y+30, 6,20); - } else if(m.kind==='skeleton'){ - // skeleton - детализированный - // Тело - ctx.fillStyle = '#ECEFF1'; - ctx.fillRect(m.x+10, m.y+20, 14, 12); - // Череп - ctx.fillRect(m.x+8, m.y+0, 18, 18); - // Глазницы - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+10, m.y+6, 4,4); - ctx.fillRect(m.x+20, m.y+6, 4,4); - // Нос - ctx.fillRect(m.x+15, m.y+12, 4,2); - // Руки - ctx.fillStyle = '#ECEFF1'; - ctx.fillRect(m.x+2, m.y+20, 6,14); - ctx.fillRect(m.x+26, m.y+20, 6,14); - // Ноги - ctx.fillRect(m.x+10, m.y+32, 6, 18); - ctx.fillRect(m.x+18, m.y+32, 6, 18); - // Лук в руке + ctx.fillStyle = "#000"; + ctx.fillRect(m.x + 8, m.y + 8, 4, 4); + ctx.fillRect(m.x + 22, m.y + 8, 4, 4); + ctx.fillRect(m.x + 12, m.y + 20, 10, 4); + ctx.fillStyle = "#4CAF50"; + ctx.fillRect(m.x + 4, m.y + 30, 6, 20); + ctx.fillRect(m.x + 24, m.y + 30, 6, 20); + } else if (m.kind === "skeleton") { + ctx.fillStyle = "#ECEFF1"; + ctx.fillRect(m.x + 10, m.y + 20, 14, 12); + ctx.fillRect(m.x + 8, m.y + 0, 18, 18); + ctx.fillStyle = "#000"; + ctx.fillRect(m.x + 10, m.y + 6, 4, 4); + ctx.fillRect(m.x + 20, m.y + 6, 4, 4); + ctx.fillRect(m.x + 15, m.y + 12, 4, 2); + ctx.fillStyle = "#ECEFF1"; + ctx.fillRect(m.x + 2, m.y + 20, 6, 14); + ctx.fillRect(m.x + 26, m.y + 20, 6, 14); + ctx.fillRect(m.x + 10, m.y + 32, 6, 18); + ctx.fillRect(m.x + 18, m.y + 32, 6, 18); ctx.save(); ctx.translate(m.x + 30, m.y + 22); - ctx.strokeStyle = '#8B4513'; + ctx.strokeStyle = "#8B4513"; ctx.lineWidth = 2; ctx.beginPath(); - ctx.arc(0, 0, 8, -Math.PI*0.7, Math.PI*0.7); + ctx.arc(0, 0, 8, -Math.PI * 0.7, Math.PI * 0.7); ctx.stroke(); - // Тетива - ctx.strokeStyle = '#ccc'; + ctx.strokeStyle = "#ccc"; ctx.lineWidth = 1; ctx.beginPath(); - ctx.moveTo(8*Math.cos(-Math.PI*0.7), 8*Math.sin(-Math.PI*0.7)); - ctx.lineTo(8*Math.cos(Math.PI*0.7), 8*Math.sin(Math.PI*0.7)); + ctx.moveTo(8 * Math.cos(-Math.PI * 0.7), 8 * Math.sin(-Math.PI * 0.7)); + ctx.lineTo(8 * Math.cos(Math.PI * 0.7), 8 * Math.sin(Math.PI * 0.7)); ctx.stroke(); ctx.restore(); - } else if(m.kind==='scorpion') { - // скорпион — оранжево-коричневый - ctx.fillStyle = '#d35400'; - ctx.fillRect(m.x+2, m.y+4, m.w-4, m.h-8); - ctx.fillStyle = '#c0392b'; - ctx.fillRect(m.x+m.w-4, m.y-6, 4, 10); - ctx.fillRect(m.x+m.w-2, m.y-10, 3, 5); - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+4, m.y+6, 3, 3); - ctx.fillRect(m.x+m.w-7, m.y+6, 3, 3); - ctx.fillStyle = '#d35400'; - ctx.fillRect(m.x-4, m.y+8, 6, 4); - ctx.fillRect(m.x+m.w-2, m.y+8, 6, 4); - } else if(m.kind==='polar_bear') { - ctx.fillStyle = '#ecf0f1'; - ctx.fillRect(m.x+2, m.y+4, m.w-4, m.h-4); - ctx.fillRect(m.x+8, m.y-2, m.w-16, 10); - ctx.fillStyle = '#bdc3c7'; - ctx.fillRect(m.x+m.w/2-3, m.y+2, 6, 4); - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+12, m.y+1, 3, 3); - ctx.fillRect(m.x+m.w-14, m.y+1, 3, 3); - ctx.fillStyle = '#ecf0f1'; - ctx.fillRect(m.x+8, m.y-4, 4, 4); - ctx.fillRect(m.x+m.w-12, m.y-4, 4, 4); - } else if(m.kind==='slime') { - const bounce = Math.sin(performance.now()/200)*3; - ctx.fillStyle = 'rgba(46,204,113,0.8)'; - ctx.fillRect(m.x-1, m.y-1+bounce, m.w+2, m.h+2); - ctx.fillStyle = 'rgba(39,174,96,0.9)'; - ctx.fillRect(m.x+1, m.y+1+bounce, m.w-2, m.h-2); - ctx.fillStyle = '#fff'; - ctx.fillRect(m.x+4, m.y+6+bounce, 6, 6); - ctx.fillRect(m.x+m.w-10, m.y+6+bounce, 6, 6); - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+6, m.y+8+bounce, 3, 3); - ctx.fillRect(m.x+m.w-8, m.y+8+bounce, 3, 3); - } else if(m.kind==='eagle') { - ctx.fillStyle = '#8B4513'; - ctx.fillRect(m.x+8, m.y+6, m.w-16, m.h-10); - const wingY = Math.sin(performance.now()/150)*4; - ctx.fillRect(m.x-6, m.y+4+wingY, 16, 6); - ctx.fillRect(m.x+m.w-10, m.y+4-wingY, 16, 6); - ctx.fillStyle = '#fff'; - ctx.fillRect(m.x+m.w/2-3, m.y+2, 6, 6); - ctx.fillStyle = '#f39c12'; - ctx.fillRect(m.x+m.w/2-1, m.y+6, 4, 3); - ctx.fillStyle = '#000'; - ctx.fillRect(m.x+m.w/2-1, m.y+3, 2, 2); } } - - // boat (рисуем первой, чтобы игрок был внутри неё) - if(boat.active){ - ctx.drawImage(tex['boat'], boat.x - (TILE-boat.w)/2, boat.y - (TILE-boat.h)/2, TILE, TILE); + if (boat.active) { + ctx.drawImage(tex2["boat"], boat.x - (TILE2 - boat.w) / 2, boat.y - (TILE2 - boat.h) / 2, TILE2, TILE2); } - - // Lerp other players towards target positions (smooth movement) - for(const [socketId, p] of otherPlayers){ - if (p.tx !== undefined) { - p.x += (p.tx - p.x) * 0.25; - p.y += (p.ty - p.y) * 0.25; - } - } - // other players (multiplayer) - for(const [socketId, p] of otherPlayers){ - if(heroImg.complete){ - ctx.drawImage(heroImg, p.x - (TILE-player.w)/2, p.y - (TILE-player.h)/2, TILE, TILE); + for (const [socketId, p] of state.otherPlayers) { + if (state.heroImg.complete) { + ctx.drawImage(state.heroImg, p.x - (TILE2 - player.w) / 2, p.y - (TILE2 - player.h) / 2, TILE2, TILE2); } else { ctx.fillStyle = p.color; ctx.fillRect(p.x, p.y, 34, 34); } - // Имя игрока (мелко над персонажем) - ctx.fillStyle = '#fff'; - ctx.font = '12px system-ui'; - ctx.textAlign = 'center'; + ctx.fillStyle = "#fff"; + ctx.font = "12px system-ui"; + ctx.textAlign = "center"; ctx.fillText(p.name, p.x + 17, p.y - 8); } - - // player - if(heroImg.complete){ - ctx.drawImage(heroImg, player.x - (TILE-player.w)/2, player.y - (TILE-player.h)/2, TILE, TILE); + if (state.heroImg.complete) { + ctx.drawImage(state.heroImg, player.x - (TILE2 - player.w) / 2, player.y - (TILE2 - player.h) / 2, TILE2, TILE2); } else { - ctx.fillStyle='#fff'; + ctx.fillStyle = "#fff"; ctx.fillRect(player.x, player.y, player.w, player.h); } - - // projectiles (стрелы) - for(const p of projectiles){ + for (const p of projectiles) { const angle = Math.atan2(p.vy, p.vx); ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(angle); - ctx.fillStyle = p.owner === 'mob' ? '#c0392b' : '#f1c40f'; + ctx.fillStyle = p.owner === "mob" ? "#c0392b" : "#f1c40f"; ctx.fillRect(-12, -1.5, 24, 3); - // наконечник ctx.beginPath(); ctx.moveTo(12, -4); ctx.lineTo(16, 0); ctx.lineTo(12, 4); ctx.closePath(); ctx.fill(); - // оперение - ctx.fillStyle = '#888'; + ctx.fillStyle = "#888"; ctx.fillRect(-12, -3, 4, 2); ctx.fillRect(-12, 1, 4, 2); ctx.restore(); } - - // particles - for(const p of parts){ + for (const p of parts2) { ctx.fillStyle = p.c; - ctx.fillRect(p.x-2, p.y-2, 4, 4); + ctx.fillRect(p.x - 2, p.y - 2, 4, 4); } - - // Стрелы скелета - for(const m of mobs){ - if(m.kind==='skeleton' && m.shootCooldown > 0.5){ - // Рисуем стрелу - const arrowX = m.x + m.w/2; + for (const m of mobs) { + if (m.kind === "skeleton" && m.shootCooldown > 0.5) { + const arrowX = m.x + m.w / 2; const arrowY = m.y + 15; - const targetX = player.x + player.w/2; - const targetY = player.y + player.h/2; + const targetX = player.x + player.w / 2; + const targetY = player.y + player.h / 2; const angle = Math.atan2(targetY - arrowY, targetX - arrowX); - const speed = 400; - - // Проверяем, попала ли стрела const dx = targetX - arrowX; const dy = targetY - arrowY; const dist = Math.hypot(dx, dy); - - // Рисуем стрелу ctx.save(); ctx.translate(arrowX, arrowY); ctx.rotate(angle); - ctx.fillStyle = '#ECEFF1'; + ctx.fillStyle = "#ECEFF1"; ctx.fillRect(0, -1, 16, 2); ctx.restore(); - - // Урон игроку если попали - if(dist < 150 && player.invuln <= 0){ + if (dist < 150 && player.invuln <= 0) { player.hp -= 8; player.invuln = 0.5; player.vx += Math.cos(angle) * 300; player.vy -= 200; - playSound('hit1'); + playSound("hit1"); } } } - - // build ghost - if(mode()==='build' && mouse.x!==null && !craftOpen && player.hp>0){ + if (mode() === "build" && mouse.x !== null && !state.craftOpen && player.hp > 0) { const wx = mouse.x + camX; const wy = mouse.y + camY; - const gx = Math.floor(wx/TILE); - const gy = Math.floor(wy/TILE); - ctx.strokeStyle = 'rgba(255,255,255,0.9)'; + const ggx = Math.floor(wx / TILE2); + const ggy = Math.floor(wy / TILE2); + ctx.strokeStyle = "rgba(255,255,255,0.9)"; ctx.lineWidth = 2; - ctx.strokeRect(gx*TILE, gy*TILE, TILE, TILE); + ctx.strokeRect(ggx * TILE2, ggy * TILE2, TILE2, TILE2); } - ctx.restore(); - - // lighting overlay at night (Minecraft-style: offscreen lightmap, wall occlusion, warm flicker) - if(night){ - // 1) Рисуем тёмный оверлей на offscreen canvas - lightC.width = W*dpr; - lightC.height = H*dpr; - lightCtx.setTransform(dpr,0,0,dpr,0,0); - lightCtx.fillStyle = 'rgba(0,0,12,0.82)'; - lightCtx.fillRect(0,0,W,H); - - // 2) Вырезаем «окна света» через destination-out — свет не проходит сквозь стены - lightCtx.globalCompositeOperation = 'destination-out'; - - // Функция: рисуем мягкий луч света с затуханием за стенами - function castLight(sx, sy, radius) { - const flick = 0.97 + Math.sin(now/300 + sx*0.01)*0.015 + Math.sin(now/470 + sy*0.02)*0.015; + if (night) { + let castLight = function(sx, sy, radius) { + const flick = 0.88 + Math.sin(now / 80 + sx * 0.01) * 0.06 + Math.sin(now / 130 + sy * 0.02) * 0.06; const r = radius * flick; - // 24 луча — мягкий круглый свет - const steps = 24; - // Собираем дистанции до стен по лучам - const dists = new Float32Array(steps); - for(let i=0; i TILE*0.3){ + for (let step = TILE2 * 0.5; step < r; step += TILE2 * 0.6) { + const lgx = Math.floor((sx + ddx * step) / TILE2); + const lgy = Math.floor((sy + ddy * step) / TILE2); + const blk = getBlock(lgx, lgy); + if (blk && !blk.dead && BLOCKS[blk.t] && BLOCKS[blk.t].solid && step > TILE2 * 0.3) { maxDist = step; break; } } dists[i] = maxDist; } - // Рисуем сглаженный полигон по dists - const cx = sx-camX, cy = sy-camY; - // Центр: яркая точка + const cx = sx - camX, cy = sy - camY; const maxR = Math.max(...dists); const grad = lightCtx.createRadialGradient(cx, cy, 0, cx, cy, maxR); - grad.addColorStop(0, 'rgba(255,255,255,1)'); - grad.addColorStop(0.4, 'rgba(255,255,255,0.8)'); - grad.addColorStop(1, 'rgba(255,255,255,0)'); + grad.addColorStop(0, "rgba(255,255,255,1)"); + grad.addColorStop(0.5, "rgba(255,255,255,0.65)"); + grad.addColorStop(1, "rgba(255,255,255,0)"); lightCtx.fillStyle = grad; - // Рисуем shape по dists (звездоподобный полигон) lightCtx.beginPath(); - for(let i=0; i<=steps; i++){ - const idx = i % steps; - const nextIdx = (i+1) % steps; - const avgD = (dists[idx] + dists[nextIdx]) / 2; - const angle = (idx/steps) * Math.PI * 2; - const px = cx + Math.cos(angle) * dists[idx]; - const py = cy + Math.sin(angle) * dists[idx]; - if(i===0) lightCtx.moveTo(px, py); + for (let i = 0; i <= steps2; i++) { + const idx = i % steps2; + const ang = idx / steps2 * Math.PI * 2; + const px = cx + Math.cos(ang) * dists[idx]; + const py = cy + Math.sin(ang) * dists[idx]; + if (i === 0) lightCtx.moveTo(px, py); else lightCtx.lineTo(px, py); } lightCtx.closePath(); lightCtx.fill(); - } - - // Источники света - for(const b of blocks){ - if(b.dead) continue; - if(b.gx < minGX-5 || b.gx > maxGX+5 || b.gy < minGY-5 || b.gy > maxGY+5) continue; + }; + const lightC = state.lightC; + const lightCtx = state.lightCtx; + lightC.width = W * state.dpr; + lightC.height = H * state.dpr; + lightCtx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0); + lightCtx.fillStyle = "rgba(0,0,12,0.82)"; + lightCtx.fillRect(0, 0, W, H); + lightCtx.globalCompositeOperation = "destination-out"; + for (const b of blocks2) { + if (b.dead) continue; + if (b.gx < minGX - 5 || b.gx > maxGX + 5 || b.gy < minGY - 5 || b.gy > maxGY + 5) continue; const def = BLOCKS[b.t]; - if(def.lightRadius){ - castLight(b.gx*TILE + TILE/2, b.gy*TILE + TILE/2, def.lightRadius); + if (def.lightRadius) { + castLight(b.gx * TILE2 + TILE2 / 2, b.gy * TILE2 + TILE2 / 2, def.lightRadius); } } - - // 3) Накладываем lightmap на основной canvas - lightCtx.globalCompositeOperation = 'source-over'; + lightCtx.globalCompositeOperation = "source-over"; ctx.drawImage(lightC, 0, 0, W, H); - - // 4) Тёплый оверлей от источников света (additive, мягкий) ctx.save(); - ctx.globalCompositeOperation = 'lighter'; - for(const b of blocks){ - if(b.dead) continue; - if(b.gx < minGX-3 || b.gx > maxGX+3 || b.gy < minGY-3 || b.gy > maxGY+3) continue; + ctx.globalCompositeOperation = "lighter"; + for (const b of blocks2) { + if (b.dead) continue; + if (b.gx < minGX - 3 || b.gx > maxGX + 3 || b.gy < minGY - 3 || b.gy > maxGY + 3) continue; const def = BLOCKS[b.t]; - if(def.lightRadius){ - const flick = 0.9 + Math.sin(now/350 + b.gx*3.7)*0.05 + Math.sin(now/530 + b.gy*2.3)*0.05; - const wx = b.gx*TILE + TILE/2 - camX; - const wy = b.gy*TILE + TILE/2 - camY; - const r = def.lightRadius * 0.85; - const grad = ctx.createRadialGradient(wx,wy, 0, wx,wy, r); - grad.addColorStop(0, `rgba(255,180,80,0.28)`); - grad.addColorStop(0.5, `rgba(255,140,40,0.13)`); - grad.addColorStop(1, 'rgba(255,100,20,0)'); + if (def.lightRadius) { + const flick = 0.7 + Math.sin(now / 90 + b.gx * 3.7) * 0.15 + Math.sin(now / 140 + b.gy * 2.3) * 0.15; + const wx = b.gx * TILE2 + TILE2 / 2 - camX; + const wy = b.gy * TILE2 + TILE2 / 2 - camY; + const r = def.lightRadius * 0.6 * flick; + const grad = ctx.createRadialGradient(wx, wy, 0, wx, wy, r); + grad.addColorStop(0, `rgba(255,180,80,${0.12 * flick})`); + grad.addColorStop(0.5, `rgba(255,140,40,${0.06 * flick})`); + grad.addColorStop(1, "rgba(255,100,20,0)"); ctx.fillStyle = grad; ctx.beginPath(); - ctx.arc(wx, wy, r, 0, Math.PI*2); + ctx.arc(wx, wy, r, 0, Math.PI * 2); ctx.fill(); } } ctx.restore(); } - - // Дождь (после ночного оверлея) drawRain(); - if(Math.random()<0.25){ - hpEl.textContent = Math.max(0, Math.ceil(player.hp)); - foodEl.textContent = Math.ceil(player.hunger); - document.getElementById('o2').textContent = Math.ceil(player.o2); - sxEl.textContent = Math.floor(player.x/TILE); - syEl.textContent = Math.floor(player.y/TILE); - todEl.textContent = night ? 'Ночь' : 'День'; - document.getElementById('xplevel').textContent = player.level; - const lvXpNext = xpForLevel(player.level + 1); - const lvXpCur = xpForLevel(player.level); - const xpInLevel = player.xp - lvXpCur; - const xpNeeded = lvXpNext - lvXpCur; - document.getElementById('xpbar').textContent = xpInLevel + '/' + xpNeeded; - tempEl.textContent = Math.round(player.temperature) + "°C"; - worldIdEl.textContent = worldId; - if(isMultiplayer){ - document.getElementById('multiplayerStatus').style.display = 'flex'; - playerCountEl.textContent = otherPlayers.size + 1; // +1 = мы сами + if (Math.random() < 0.25) { + state.hpEl.textContent = Math.max(0, Math.ceil(player.hp)); + state.foodEl.textContent = Math.ceil(player.hunger); + document.getElementById("o2").textContent = Math.ceil(player.o2); + state.sxEl.textContent = Math.floor(player.x / TILE2); + state.syEl.textContent = Math.floor(player.y / TILE2); + state.todEl.textContent = night ? "\u041D\u043E\u0447\u044C" : "\u0414\u0435\u043D\u044C"; + state.worldIdEl.textContent = state.worldId; + if (state.isMultiplayer) { + document.getElementById("multiplayerStatus").style.display = "flex"; + state.playerCountEl.textContent = state.otherPlayers.size + 1; } else { - document.getElementById('multiplayerStatus').style.display = 'none'; + document.getElementById("multiplayerStatus").style.display = "none"; } } - - // Индикатор сна - if(player.sleeping){ - ctx.fillStyle = 'rgba(0,0,0,0.7)'; + if (player.sleeping) { + ctx.fillStyle = "rgba(0,0,0,0.7)"; ctx.fillRect(0, 0, W, H); - ctx.fillStyle = '#fff'; - ctx.font = 'bold 32px system-ui'; - ctx.textAlign = 'center'; - ctx.fillText('💤 Спим...', W/2, H/2); - ctx.font = '18px system-ui'; - ctx.fillText('Нажмите на кровать чтобы проснуться', W/2, H/2 + 40); + ctx.fillStyle = "#fff"; + ctx.font = "bold 32px system-ui"; + ctx.textAlign = "center"; + ctx.fillText("\u{1F4A4} \u0421\u043F\u0438\u043C...", W / 2, H / 2); + ctx.font = "18px system-ui"; + ctx.fillText("\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u043D\u0430 \u043A\u0440\u043E\u0432\u0430\u0442\u044C \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0441\u043D\u0443\u0442\u044C\u0441\u044F", W / 2, H / 2 + 40); } - - // Рисуем дропы - // Температурный оверлей (заморозка/тепловой удар) - if (typeof player.temperature !== 'undefined' && (player.temperature < -5 || player.temperature > 35)) { - const severity = player.temperature < -5 - ? Math.abs(player.temperature + 5) / 20 - : (player.temperature - 35) / 20; - const alpha = Math.min(0.35, severity * 0.15); - if (player.temperature < -5) { - ctx.fillStyle = 'rgba(100,150,255,' + alpha + ')'; - } else { - ctx.fillStyle = 'rgba(255,100,50,' + alpha + ')'; - } - ctx.fillRect(0, 0, W, H); - } - drawDrops(ctx); - // Пикап дропов - pickupDrops(); - // Popup уровня - drawLevelUpPopup(ctx); - - // Миникарта (обновляем раз в ~4 кадра для оптимизации) - if(minimapOpen && Math.random() < 0.25){ + if (state.minimapOpen && Math.random() < 0.25) { renderMinimap(); } - requestAnimationFrame(loop); } - + + // src/ui/market.js + var marketPanel = document.getElementById("marketPanel"); + var marketClose = document.getElementById("marketClose"); + var marketContent = document.getElementById("marketContent"); + function initMarket() { + marketClose.addEventListener("click", () => closeMarket()); + document.addEventListener("toggleMarket", () => toggleMarket()); + } + function toggleMarket() { + if (marketPanel.style.display === "none" || !marketPanel.style.display) { + openMarket(); + } else { + closeMarket(); + } + } + function closeMarket() { + marketPanel.style.display = "none"; + } + function openMarket() { + marketPanel.style.display = "block"; + renderMarket(); + } + function renderMarket() { + if (!state.socket) { + marketContent.innerHTML = '

\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u043A \u0441\u0435\u0440\u0432\u0435\u0440\u0443...

'; + return; + } + state.socket.emit("market_list", {}, (orders) => { + if (!orders || !orders.length) { + marketContent.innerHTML = ` +

\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442

+
+ +
+ `; + const btn2 = document.getElementById("createOrderBtn"); + if (btn2) btn2.addEventListener("click", showCreateOrder); + return; + } + let html = '
'; + html += orders.map((o) => ` +
+
+ ${o.offer_item} \xD7${o.offer_qty} + \u21C4 + ${o.want_item} \xD7${o.want_qty} +
+
${o.seller || "\u0410\u043D\u043E\u043D\u0438\u043C"}
+
+ `).join(""); + marketContent.innerHTML = html; + const btn = document.getElementById("createOrderBtn"); + if (btn) btn.addEventListener("click", showCreateOrder); + }); + } + function showCreateOrder() { + const inv = state.player.inventory || []; + const items = inv.filter((slot) => slot && slot.id); + marketContent.innerHTML = ` +
+

\u0412\u044B\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u0435\u0434\u043C\u0435\u0442

+ + + + +
+ + +
+
+ `; + document.getElementById("cancelOrder").addEventListener("click", () => renderMarket()); + document.getElementById("submitOrder").addEventListener("click", () => { + const offerItem = document.getElementById("offerItem").value; + const wantItem = document.getElementById("wantItem").value.trim(); + if (!offerItem || !wantItem) { + alert("\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u0440\u0435\u0434\u043C\u0435\u0442 \u0438 \u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u0447\u0442\u043E \u0445\u043E\u0442\u0438\u0442\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C"); + return; + } + if (state.socket) { + state.socket.emit("market_create", { + offer_item: offerItem, + offer_qty: 1, + want_item: wantItem, + want_qty: 1 + }); + playSound("click"); + setTimeout(renderMarket, 500); + } + }); + } + + // src/ui/respawn.js + function initRespawn() { + document.getElementById("respawnBtn").onclick = async () => { + playSound("click"); + console.log("=== RESPAWN CLICKED ==="); + console.log("isMultiplayer:", state.isMultiplayer); + console.log("otherPlayers.size:", state.otherPlayers.size); + console.log("player.hp before respawn:", state.player.hp); + const player = state.player; + const deathEl = state.deathEl; + if (state.isMultiplayer && state.otherPlayers.size > 0) { + console.log("\u041C\u0443\u043B\u044C\u0442\u0438\u043F\u043B\u0435\u0435\u0440 \u0440\u0435\u0436\u0438\u043C - \u0432\u043E\u0437\u0440\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u0432 \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u043E\u0439 \u0442\u043E\u0447\u043A\u0435"); + player.hp = 100; + player.hunger = 100; + player.o2 = 100; + player.vx = player.vy = 0; + player.invuln = 0; + player.x = state.spawnPoint.x; + player.y = state.spawnPoint.y; + player.fallStartY = player.y; + console.log("\u0412\u043E\u0437\u0440\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u0432 \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u043E\u0439 \u0442\u043E\u0447\u043A\u0435, HP:", player.hp); + } else { + console.log("\u041E\u0434\u0438\u043D\u043E\u0447\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C - \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043C \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435"); + const loadedSave = await loadGame(); + if (loadedSave) { + await applySave(loadedSave); + console.log("\u0417\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u043F\u043E\u0441\u043B\u0435 \u0441\u043C\u0435\u0440\u0442\u0438, final HP:", player.hp); + } else { + player.hp = 100; + player.hunger = 100; + player.o2 = 100; + player.vx = player.vy = 0; + player.invuln = 0; + player.x = state.spawnPoint.x; + player.y = state.spawnPoint.y; + player.fallStartY = player.y; + console.log("\u0412\u043E\u0437\u0440\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u0432 \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u043E\u0439 \u0442\u043E\u0447\u043A\u0435, HP:", player.hp); + } + } + console.log("player.hp after respawn logic:", player.hp); + console.log("Hiding death screen..."); + deathEl.style.display = "none"; + console.log("=== RESPAWN END ==="); + }; + } + + // src/ui/share.js + function initShare() { + document.getElementById("worldId").onclick = () => { + const shareUrl = new URL(window.location.href); + shareUrl.searchParams.set("world", state.worldId); + const shareUrlString = shareUrl.toString(); + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(shareUrlString).then(() => { + alert("\u0421\u0441\u044B\u043B\u043A\u0430 \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u0430!"); + }).catch(() => { + alert("\u0421\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043C\u0438\u0440:\n" + shareUrlString); + }); + } else { + alert("\u0421\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043C\u0438\u0440:\n" + shareUrlString); + } + }; + } + + // src/input/controls.js + var inp = state.inp; + function bindHold(el, key) { + const down = (e) => { + e.preventDefault(); + state.inp[key] = true; + }; + const up = (e) => { + e.preventDefault(); + state.inp[key] = false; + }; + el.addEventListener("pointerdown", down); + el.addEventListener("pointerup", up); + el.addEventListener("pointerleave", up); + } + function initControls() { + const leftBtn = document.getElementById("left"); + const rightBtn = document.getElementById("right"); + const jumpBtn = document.getElementById("jump"); + const downBtn = document.getElementById("down"); + if (leftBtn) bindHold(leftBtn, "l"); + if (rightBtn) bindHold(rightBtn, "r"); + if (jumpBtn) bindHold(jumpBtn, "j"); + if (downBtn) bindHold(downBtn, "s"); + window.addEventListener("keydown", (e) => { + if (e.code === "KeyA" || e.code === "ArrowLeft") state.inp.l = true; + if (e.code === "KeyD" || e.code === "ArrowRight") state.inp.r = true; + if (e.code === "Space" || e.code === "KeyW" || e.code === "ArrowUp") state.inp.j = true; + if (e.code === "KeyS" || e.code === "ArrowDown") state.inp.s = true; + }); + window.addEventListener("keyup", (e) => { + if (e.code === "KeyA" || e.code === "ArrowLeft") state.inp.l = false; + if (e.code === "KeyD" || e.code === "ArrowRight") state.inp.r = false; + if (e.code === "Space" || e.code === "KeyW" || e.code === "ArrowUp") state.inp.j = false; + if (e.code === "KeyS" || e.code === "ArrowDown") state.inp.s = false; + }); + } + + // src/multiplayer/voice-chat.js + var voiceSocket = null; + var voiceStream = null; + var audioCtx = null; + var voiceProcessor = null; + var voiceActive = false; + var VOICE_SERVER2 = "https://voicegrech.mkn8n.ru"; + var voiceBtn = document.createElement("div"); + voiceBtn.innerHTML = '\u{1F3A4}/'; + voiceBtn.title = "\u0413\u043E\u043B\u043E\u0441\u043E\u0432\u043E\u0439 \u0447\u0430\u0442 (\u0432\u044B\u043A\u043B)"; + voiceBtn.style.cssText = "position:absolute;top:74px;right:130px;width:52px;height:52px;border-radius:12px;background:#555;z-index:200;display:flex;align-items:center;justify-content:center;font-size:24px;cursor:pointer;border:2px solid rgba(255,255,255,0.9);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;"; + document.querySelector(".ui").appendChild(voiceBtn); + var speakingIndicator = document.createElement("div"); + speakingIndicator.style.cssText = "position:absolute;top:130px;right:10px;z-index:200;display:none;background:rgba(0,0,0,0.7);color:#2ecc71;padding:4px 8px;border-radius:6px;font-size:12px;"; + speakingIndicator.textContent = "\u{1F50A}"; + document.querySelector(".ui").appendChild(speakingIndicator); + var speakingTimeout = null; + voiceBtn.onclick = async () => { + if (voiceActive) { + voiceActive = false; + voiceBtn.innerHTML = '\u{1F3A4}/'; + voiceBtn.style.background = "#555"; + if (voiceStream) { + voiceStream.getTracks().forEach((t) => t.stop()); + voiceStream = null; + } + if (voiceProcessor) { + voiceProcessor.disconnect(); + voiceProcessor = null; + } + if (audioCtx) { + audioCtx.close(); + audioCtx = null; + } + if (voiceSocket) { + voiceSocket.disconnect(); + voiceSocket = null; + } + return; + } + try { + voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 24e3 } }); + audioCtx = new AudioContext({ sampleRate: 24e3 }); + const source = audioCtx.createMediaStreamSource(voiceStream); + voiceProcessor = audioCtx.createScriptProcessor(4096, 1, 1); + voiceProcessor.onaudioprocess = (e) => { + if (!voiceActive || !voiceSocket || !voiceSocket.connected) return; + const pcm = e.inputBuffer.getChannelData(0); + const int16 = new Int16Array(pcm.length); + for (let i = 0; i < pcm.length; i++) { + const s = Math.max(-1, Math.min(1, pcm[i])); + int16[i] = s < 0 ? s * 32768 : s * 32767; + } + voiceSocket.emit("voice_data", int16.buffer); + }; + source.connect(voiceProcessor); + voiceProcessor.connect(audioCtx.createGain()); + voiceSocket = io(VOICE_SERVER2, { transports: ["websocket"] }); + voiceSocket.on("connect", () => { + voiceSocket.emit("voice_join", { world_id: state.worldId, x: state.player.x, y: state.player.y, name: state.playerName || "\u0418\u0433\u0440\u043E\u043A" }); + }); + voiceSocket.on("voice_in", (payload) => { + const { data, meta, volume } = payload; + if (!audioCtx || audioCtx.state === "closed") return; + const int16 = new Int16Array(data); + const float32 = new Float32Array(int16.length); + for (let i = 0; i < int16.length; i++) { + float32[i] = int16[i] / (int16[i] < 0 ? 32768 : 32767) * volume; + } + const buf = audioCtx.createBuffer(1, float32.length, 24e3); + buf.getChannelData(0).set(float32); + const src = audioCtx.createBufferSource(); + src.buffer = buf; + const gain = audioCtx.createGain(); + gain.gain.value = volume; + src.connect(gain).connect(audioCtx.destination); + src.start(); + speakingIndicator.style.display = "block"; + speakingIndicator.textContent = `\u{1F50A} ${meta.name}`; + clearTimeout(speakingTimeout); + speakingTimeout = setTimeout(() => { + speakingIndicator.style.display = "none"; + }, 500); + }); + voiceActive = true; + voiceBtn.textContent = "\u{1F3A4}"; + voiceBtn.style.background = "#2ecc71"; + } catch (e) { + console.error("Voice error:", e); + voiceBtn.style.background = "#e74c3c"; + } + }; + function initVoice() { + let voicePosT = 0; + return { + update(dt) { + voicePosT += dt; + if (voicePosT > 0.5 && voiceSocket && voiceSocket.connected) { + voicePosT = 0; + voiceSocket.emit("voice_pos", { x: state.player.x, y: state.player.y }); + } + } + }; + } + + // src/ui/inventory.js + function renderInventory() { + const inventoryGrid = state.inventoryGrid; + const inv = state.inv; + const selected = state.selected; + inventoryGrid.innerHTML = ""; + const items = Object.keys(inv).filter((id) => inv[id] > 0); + for (let i = 0; i < 21; i++) { + const slot = document.createElement("div"); + slot.className = "inv-slot" + (i < items.length && items[i] === selected ? " sel" : ""); + if (i < items.length) { + const id = items[i]; + if (BLOCKS[id] && tex[id]) { + slot.style.backgroundImage = `url(${tex[id].toDataURL()})`; + slot.style.backgroundSize = "cover"; + } else if (itemTex[id]) { + slot.style.backgroundImage = `url(${itemTex[id].toDataURL()})`; + slot.style.backgroundSize = "cover"; + } else if (ITEMS[id]) { + slot.textContent = ITEMS[id].icon || "\u{1F4E6}"; + } else if (TOOLS[id]) { + slot.textContent = TOOLS[id].icon || "\u{1F527}"; + } else if (id === "iron_armor") { + slot.textContent = "\u{1F6E1}\uFE0F"; + slot.style.background = "linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)"; + } + const count = document.createElement("div"); + count.className = "inv-count"; + count.textContent = inv[id]; + slot.appendChild(count); + slot.onclick = () => { + playSound("click"); + state.selected = id; + state.recentItems = state.recentItems.filter((item) => item !== id); + state.recentItems.unshift(id); + state.recentItems = state.recentItems.slice(0, 5); + rebuildHotbar(); + renderInventory(); + }; + slot.ondblclick = () => { + if (id === "iron_armor" && inv.iron_armor > 0) { + if (state.player.equippedArmor === "iron_armor") { + state.player.equippedArmor = null; + state.player.armor = 0; + } else { + state.player.equippedArmor = "iron_armor"; + state.player.armor = BLOCKS["iron_armor"].armor; + } + playSound("click"); + renderInventory(); + } + }; + } + inventoryGrid.appendChild(slot); + } + } + + // src/ui/craft.js + function canCraft(r) { + const inv = state.inv; + for (const res in r.cost) { + if ((inv[res] || 0) < r.cost[res]) return false; + } + return true; + } + function renderCraft() { + const inv = state.inv; + const recipesEl = state.recipesEl; + recipesEl.innerHTML = ""; + for (const r of RECIPES) { + const row = document.createElement("div"); + row.className = "recipe"; + const icon = document.createElement("div"); + icon.className = "ricon"; + if (tex[r.out]) { + icon.style.backgroundImage = `url(${tex[r.out].toDataURL()})`; + } else if (itemTex[r.out]) { + icon.style.backgroundImage = `url(${itemTex[r.out].toDataURL()})`; + } else if (TOOLS[r.out]) { + icon.textContent = TOOLS[r.out].icon || "\u{1F527}"; + icon.style.fontSize = "24px"; + icon.style.display = "flex"; + icon.style.alignItems = "center"; + icon.style.justifyContent = "center"; + } else if (ITEMS[r.out]) { + icon.textContent = ITEMS[r.out].icon || "\u{1F4E6}"; + icon.style.fontSize = "24px"; + icon.style.display = "flex"; + icon.style.alignItems = "center"; + icon.style.justifyContent = "center"; + } + const info = document.createElement("div"); + info.className = "rinfo"; + const nm = document.createElement("div"); + nm.className = "rname"; + const itemName2 = BLOCKS[r.out]?.n || TOOLS[r.out]?.n || ITEMS[r.out]?.n || r.out; + nm.textContent = `${itemName2} x${r.qty}`; + const cs = document.createElement("div"); + cs.className = "rcost"; + cs.textContent = Object.keys(r.cost).map((x) => { + const cn = BLOCKS[x]?.n || TOOLS[x]?.n || ITEMS[x]?.n || x; + return `${cn}: ${inv[x] || 0}/${r.cost[x]}`; + }).join(" "); + info.appendChild(nm); + info.appendChild(cs); + const btn = document.createElement("button"); + btn.className = "rcraft"; + btn.textContent = "\u0421\u043E\u0437\u0434\u0430\u0442\u044C"; + btn.disabled = !canCraft(r); + btn.onclick = () => { + if (!canCraft(r)) return; + playSound("click"); + for (const res in r.cost) inv[res] -= r.cost[res]; + inv[r.out] = (inv[r.out] || 0) + r.qty; + if (TOOLS[r.out]) addTool(r.out); + rebuildHotbar(); + renderCraft(); + }; + row.appendChild(icon); + row.appendChild(info); + row.appendChild(btn); + recipesEl.appendChild(row); + } + } + var craftOpen = false; + var inventoryOpen = false; + function toggleCraft() { + playSound("click"); + craftOpen = !craftOpen; + state.craftPanel.style.display = craftOpen ? "block" : "none"; + if (craftOpen) { + renderCraft(); + inventoryOpen = false; + state.inventoryPanel.style.display = "none"; + } + } + function closeCraft() { + playSound("click"); + craftOpen = false; + state.craftPanel.style.display = "none"; + } + function toggleInventory() { + playSound("click"); + inventoryOpen = true; + state.inventoryPanel.style.display = "block"; + renderInventory(); + craftOpen = false; + state.craftPanel.style.display = "none"; + } + function closeInventory() { + playSound("click"); + inventoryOpen = false; + state.inventoryPanel.style.display = "none"; + } + + // src/main.js + var urlParams2 = new URLSearchParams(window.location.search); + state.SERVER_URL = SERVER_URL; + state.TELEGRAM_BOT_USERNAME = "Grechkacraft_bot"; + state.TELEGRAM_APP_SHORT_NAME = "minegrechka"; + state.playerName = localStorage.getItem("minegrechka_playerName") || null; + if (!state.playerName) { + state.playerName = prompt("\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0432\u0430\u0448\u0435 \u0438\u043C\u044F \u0434\u043B\u044F \u0438\u0433\u0440\u044B:") || "\u0418\u0433\u0440\u043E\u043A"; + localStorage.setItem("minegrechka_playerName", state.playerName); + console.log("Player name set:", state.playerName); + } + console.log("Current URL:", window.location.href); + var worldParam = urlParams2.get("world"); + console.log("world param:", worldParam); + state.worldId = worldParam && worldParam.trim() !== "" ? worldParam : null; + console.log("worldId after params:", state.worldId, "type:", typeof state.worldId); + if (!state.worldId) { + state.worldId = Math.random().toString(36).substring(2, 10); + console.log("Generated worldId:", state.worldId); + try { + const newUrl = new URL(window.location.href); + newUrl.searchParams.set("world", state.worldId); + const newUrlString = newUrl.toString(); + console.log("New URL to set:", newUrlString); + if (typeof window.history !== "undefined" && typeof window.history.replaceState === "function") { + window.history.replaceState(null, "", newUrlString); + console.log("URL after replaceState:", window.location.href); + } else { + console.error("History API not supported!"); + } + } catch (e) { + console.error("Error updating URL:", e); + } + console.log("Generated new worldId for browser:", state.worldId); + } + console.log("Final worldId:", state.worldId, "Player name:", state.playerName); + initCanvas(); + state.hpEl = document.getElementById("hp"); + state.foodEl = document.getElementById("food"); + state.sxEl = document.getElementById("sx"); + state.syEl = document.getElementById("sy"); + state.todEl = document.getElementById("tod"); + state.worldIdEl = document.getElementById("worldId"); + state.playerCountEl = document.getElementById("playerCount"); + state.deathEl = document.getElementById("death"); + state.hotbarEl = document.getElementById("hotbar"); + state.craftPanel = document.getElementById("craftPanel"); + state.recipesEl = document.getElementById("recipes"); + state.inventoryPanel = document.getElementById("inventoryPanel"); + state.inventoryGrid = document.getElementById("inventoryGrid"); + state.marketPanel = document.getElementById("marketPanel"); + state.marketContent = document.getElementById("marketContent"); + state.tradePanel = document.getElementById("tradePanel"); + state.tradeContent = document.getElementById("tradeContent"); + state.multiplayerStatus = document.getElementById("multiplayerStatus"); + initTextures(); + state.tex = tex; + state.itemTex = itemTex; + var craftBtn = document.getElementById("craftBtn"); + var invToggle = document.getElementById("invToggle"); + var craftClose = document.getElementById("craftClose"); + var inventoryClose = document.getElementById("inventoryClose"); + if (craftBtn) craftBtn.onclick = () => toggleCraft(); + if (craftClose) craftClose.onclick = () => closeCraft(); + if (invToggle) invToggle.onclick = () => toggleInventory(); + if (inventoryClose) inventoryClose.onclick = () => closeInventory(); + initControls(); + initMouseHandlers(); + initChat(); + initFurnace(); + initMinimap(); + initSaveControls(); + initMarket(); + initRespawn(); + initShare(); + initModes(); + initSocket(); + initVoice(); + rebuildHotbar(); + initDB().then(async () => { + const loadedSave = await loadGame(); + if (loadedSave) { + await applySave(loadedSave); + console.log("\u0417\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435, HP:", state.player.hp); + if (state.player.hp <= 0) { + console.log("WARNING: HP <= 0 \u043F\u043E\u0441\u043B\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438, \u0432\u043E\u0437\u0440\u043E\u0436\u0434\u0430\u0435\u043C\u0441\u044F"); + state.player.hp = 100; + state.player.hunger = 100; + state.player.o2 = 100; + state.player.x = state.spawnPoint.x; + state.player.y = state.spawnPoint.y; + state.player.vx = state.player.vy = 0; + state.player.invuln = 0; + state.player.fallStartY = state.player.y; + } + } else { + console.log("\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E, \u043D\u0430\u0447\u0438\u043D\u0430\u0435\u043C \u043D\u043E\u0432\u0443\u044E \u0438\u0433\u0440\u0443"); + state.player.hp = 100; + state.player.hunger = 100; + state.player.o2 = 100; + state.player.vx = state.player.vy = 0; + state.player.invuln = 0; + const startGX = 6; + for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx); + const surfaceY = surfaceGyAt(startGX); + let safeGY = surfaceY - 1; + const aboveBlock = getBlock(startGX, surfaceY - 1); + if (aboveBlock && aboveBlock.t === "water") { + for (let gy = state.SEA_GY - 1; gy >= 0; gy--) { + const b = getBlock(startGX, gy); + if (!b || b.dead || b.t === "air" || b.t === "water") continue; + safeGY = gy - 1; + break; + } + } + state.player.y = safeGY * state.TILE; + state.player.x = startGX * state.TILE; + state.player.fallStartY = state.player.y; + state.spawnPoint.x = state.player.x; + state.spawnPoint.y = state.player.y; + console.log("\u041D\u043E\u0432\u0430\u044F \u0438\u0433\u0440\u0430: startGX=", startGX, "surfaceY=", surfaceY, "player.y=", state.player.y); + for (let gx = startGX - 50; gx <= startGX + 50; gx++) { + genColumn(gx); + } + } + document.addEventListener("visibilitychange", () => { + if (document.hidden) { + saveGame(); + } + }); + window.addEventListener("beforeunload", () => { + saveGame(); + }); + }).catch((err) => { + console.error("\u041E\u0448\u0438\u0431\u043A\u0430 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438:", err); + const startGX = 6; + genColumn(startGX); + state.player.y = (surfaceGyAt(startGX) - 1) * state.TILE; + state.player.fallStartY = state.player.y; + for (let gx = startGX - 50; gx <= startGX + 50; gx++) { + genColumn(gx); + } + }); requestAnimationFrame(loop); })(); diff --git a/game.js.bak.v30 b/game.js.bak.v30 new file mode 100644 index 0000000..8635c7e --- /dev/null +++ b/game.js.bak.v30 @@ -0,0 +1,5865 @@ +(() => { + // ==================== КОНФИГУРАЦИЯ СЕРВЕРА ==================== +// === Custom modal functions === +function customAlert(msg) { + const overlay = document.createElement("div"); + overlay.className = "custom-modal-overlay"; + const box = document.createElement("div"); + box.className = "custom-modal-box"; + const text = document.createElement("div"); + text.textContent = msg; + text.style.marginBottom = "16px"; + const btn = document.createElement("button"); + btn.className = "btn-ok"; + btn.textContent = "OK"; + btn.onclick = () => overlay.remove(); + box.appendChild(text); + box.appendChild(btn); + overlay.appendChild(box); + document.querySelector("#game").appendChild(overlay); +} +function customConfirm(msg, onYes) { + const overlay = document.createElement("div"); + overlay.className = "custom-modal-overlay"; + const box = document.createElement("div"); + box.className = "custom-modal-box"; + const text = document.createElement("div"); + text.textContent = msg; + text.style.marginBottom = "16px"; + const btns = document.createElement("div"); + btns.className = "modal-btns"; + const yesBtn = document.createElement("button"); + yesBtn.className = "btn-yes"; + yesBtn.textContent = "Да"; + yesBtn.onclick = () => { overlay.remove(); onYes(); }; + const noBtn = document.createElement("button"); + noBtn.className = "btn-no"; + noBtn.textContent = "Отмена"; + noBtn.onclick = () => overlay.remove(); + btns.appendChild(yesBtn); + btns.appendChild(noBtn); + box.appendChild(text); + box.appendChild(btns); + overlay.appendChild(box); + document.querySelector("#game").appendChild(overlay); +} + // Возможность переопределить сервер через query string + const urlParams = new URLSearchParams(window.location.search); + const SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru'; + const TELEGRAM_BOT_USERNAME = 'Grechkacraft_bot'; // Имя бота для share-ссылки + const TELEGRAM_APP_SHORT_NAME = 'minegrechka'; // Короткое имя Mini App + + // Защита от mixed content + if (location.protocol === 'https:' && SERVER_URL.startsWith('http://')) { + console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP'); + alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.'); + } + + // ==================== TELEGRAM MINI APP ==================== + const isTgWebApp = !!(window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData); + let tgUser = null; + let tgSaveLoaded = false; + + if (isTgWebApp) { + try { + Telegram.WebApp.ready(); + Telegram.WebApp.expand(); + tgUser = Telegram.WebApp.initDataUnsafe?.user || null; + console.log('[TG] Mini App detected, user:', tgUser?.id, tgUser?.username); + // Set player name from TG + if (tgUser && tgUser.first_name) { + const tgName = tgUser.username || tgUser.first_name; + localStorage.setItem('minegrechka_playerName', tgName); + } + } catch(e) { + console.warn('[TG] WebApp init error:', e); + } + } + + // ==================== WORLD ID И ИГРОКА ==================== + let worldId = null; + let playerName = localStorage.getItem('minegrechka_playerName') || null; + + // Priority: URL/startParam > TG saved world > new world + const worldParam = urlParams.get('world'); + + // TG Mini App startParam (deep link: t.me/bot?start=CODE) + const tgStartParam = (isTgWebApp && Telegram.WebApp.initDataUnsafe?.start_param) ? Telegram.WebApp.initDataUnsafe.start_param.trim() : null; + if (tgStartParam && !worldParam) { + console.log('[TG] startParam detected, joining world:', tgStartParam); + } + + // ==================== START MENU ==================== + let startMenuResolved = false; + + function generateShortWorldCode() { + // Generate readable 6-char code: consonants + digits (easy to copy/share) + const c = 'bcdfghjkmnpqrstvwxyz'; + const d = '23456789'; + let code = ''; + for (let i = 0; i < 3; i++) code += c[Math.floor(Math.random() * c.length)]; + for (let i = 0; i < 3; i++) code += d[Math.floor(Math.random() * d.length)]; + return code; + } + + function showStartMenu() { + return new Promise((resolve) => { + // If world is in URL — skip menu, auto-join + if ((worldParam && worldParam.trim() !== '') || tgStartParam) { + worldId = (worldParam && worldParam.trim()) || tgStartParam; + console.log('[Menu] Joining world from URL/startParam:', worldId); + resolve(worldId); + return; + } + + // ===== Minecraft-style CSS ===== + const mcCSS = ` + .mc-overlay { position:fixed;top:0;left:0;width:100%;height:100%;z-index:99999; + display:flex;align-items:center;justify-content:center;font-family:'Courier New',monospace; + image-rendering:pixelated; } + .mc-bg { position:absolute;inset:0;background:#000;opacity:0.75; } + .mc-dirt { position:absolute;bottom:0;left:0;right:0;height:40%; + background:repeating-linear-gradient(0deg,#8B6914 0px,#8B6914 2px,#7A5B12 2px,#7A5B12 4px);opacity:0.3; } + .mc-box { position:relative;z-index:1;background:#C6C6C6;border:4px solid #000; + padding:0;max-width:420px;width:92%;box-shadow:4px 4px 0 #000; } + .mc-titlebar { background:#1a1a2e;padding:12px 16px;text-align:center; + border-bottom:4px solid #000; } + .mc-title { font-size:24px;font-weight:bold;color:#FFAA00;text-shadow:2px 2px 0 #000; + letter-spacing:2px; } + .mc-subtitle { font-size:13px;color:#AAA;margin-top:4px; } + .mc-body { padding:16px;background:#C6C6C6; } + .mc-btn { display:block;width:100%;padding:10px 16px;margin-bottom:8px; + font-family:'Courier New',monospace;font-size:16px;font-weight:bold; + cursor:pointer;text-align:center;border:3px solid #000; + image-rendering:pixelated;box-shadow:3px 3px 0 #000; + transition:transform 0.05s,box-shadow 0.05s; } + .mc-btn:active { transform:translate(2px,2px);box-shadow:1px 1px 0 #000; } + .mc-btn-green { background:#5A8F3C;color:#E0E0E0;border-color:#3A5F1C;text-shadow:1px 1px 0 #222; } + .mc-btn-green:hover { background:#6DAF4E; } + .mc-btn-blue { background:#3C5F9E;color:#E0E0E0;border-color:#2A4070;text-shadow:1px 1px 0 #222; } + .mc-btn-blue:hover { background:#4B73BE; } + .mc-btn-purple { background:#7B3FA0;color:#E0E0E0;border-color:#5A2C78;text-shadow:1px 1px 0 #222; } + .mc-btn-purple:hover { background:#9555BE; } + .mc-btn-orange { background:#B86E00;color:#E0E0E0;border-color:#8A5200;text-shadow:1px 1px 0 #222; } + .mc-btn-orange:hover { background:#DA8500; } + .mc-btn-red { background:#9E2B2B;color:#E0E0E0;border-color:#6E1B1B;text-shadow:1px 1px 0 #222; } + .mc-btn-red:hover { background:#BE3B3B; } + .mc-btn-gray { background:#555;color:#AAA;border-color:#333;text-shadow:1px 1px 0 #111; } + .mc-btn-gray:hover { background:#666;color:#DDD; } + .mc-btn-outline { background:transparent;color:#555;border-color:#555; + text-shadow:none;box-shadow:2px 2px 0 #333; } + .mc-btn-outline:hover { background:#DDD;color:#222; } + .mc-input { width:100%;padding:8px 12px;font-family:'Courier New',monospace; + font-size:16px;background:#FFF;border:3px solid #000;color:#222; + box-shadow:inset 2px 2px 0 #999;margin:4px 0; } + .mc-input:focus { outline:none;border-color:#FFAA00; } + .mc-label { font-size:13px;color:#444;font-weight:bold;margin:8px 0 4px; } + .mc-divider { border:none;border-top:2px solid #999;margin:12px 0; } + .mc-friend-row { display:flex;justify-content:space-between;align-items:center; + padding:6px 8px;margin-bottom:4px;background:#E8E8E8;border:2px solid #999; + box-shadow:2px 2px 0 #999; } + .mc-friend-btn { padding:4px 10px;font-family:'Courier New',monospace; + font-size:13px;font-weight:bold;cursor:pointer;border:2px solid #000; + box-shadow:1px 1px 0 #000; } + .mc-friend-btn:active { transform:translate(1px,1px);box-shadow:none; } + .mc-friend-btn-green { background:#5A8F3C;color:#E0E0E0; } + .mc-friend-btn-red { background:#9E2B2B;color:#E0E0E0; } + .mc-checkbox-row { display:flex;align-items:center;gap:8px;font-size:13px;color:#444;cursor:pointer; } + .mc-checkbox { width:18px;height:18px;accent-color:#5A8F3C; } + `; + + const styleEl = document.createElement('style'); + styleEl.textContent = mcCSS; + document.head.appendChild(styleEl); + + const overlay = document.createElement('div'); + overlay.className = 'mc-overlay'; + overlay.id = 'startMenu'; + overlay.innerHTML = '
'; + + const box = document.createElement('div'); + box.className = 'mc-box'; + + // Title bar + const titleBar = document.createElement('div'); + titleBar.className = 'mc-titlebar'; + titleBar.innerHTML = '
⛏️ GrechkaCraft
' + + '
' + (isTgWebApp ? ('Привет, ' + (tgUser?.first_name || 'Игрок') + '!') : 'Voxel-мир в браузере') + '
'; + box.appendChild(titleBar); + + const body = document.createElement('div'); + body.className = 'mc-body'; + + // === Buttons === + // New World + const btnNew = document.createElement('button'); + btnNew.className = 'mc-btn mc-btn-green'; + btnNew.textContent = '🌍 Новый мир'; + btnNew.onclick = () => { + worldId = generateShortWorldCode(); + overlay.remove(); + styleEl.remove(); + resolve(worldId); + }; + body.appendChild(btnNew); + + // Join by code + const btnJoin = document.createElement('button'); + btnJoin.className = 'mc-btn mc-btn-blue'; + btnJoin.textContent = '🔗 Вступить по коду'; + btnJoin.onclick = () => { + body.innerHTML = ''; + const lbl = document.createElement('div'); + lbl.className = 'mc-label'; + lbl.textContent = 'Код мира:'; + body.appendChild(lbl); + const codeInput = document.createElement('input'); + codeInput.className = 'mc-input'; + codeInput.type = 'text'; + codeInput.placeholder = 'bkh237'; + codeInput.maxLength = 12; + body.appendChild(codeInput); + const btnGo = document.createElement('button'); + btnGo.className = 'mc-btn mc-btn-blue'; + btnGo.textContent = 'Войти →'; + btnGo.onclick = () => { + const code = codeInput.value.trim().toLowerCase(); + if (code.length >= 3) { + worldId = code; + overlay.remove(); + styleEl.remove(); + resolve(worldId); + } + }; + body.appendChild(btnGo); + const btnBack = document.createElement('button'); + btnBack.className = 'mc-btn mc-btn-gray'; + btnBack.textContent = '← Назад'; + btnBack.onclick = () => { overlay.remove(); styleEl.remove(); showStartMenu().then(resolve); }; + body.appendChild(btnBack); + codeInput.focus(); + codeInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') btnGo.click(); }); + }; + body.appendChild(btnJoin); + + // Continue saved game + const hasLocalSave = !!localStorage.getItem('minegrechka_save'); + if (isTgWebApp || hasLocalSave) { + const btnHistory = document.createElement('button'); + btnHistory.className = 'mc-btn mc-btn-purple'; + btnHistory.textContent = '💾 Продолжить игру'; + btnHistory.onclick = async () => { + if (isTgWebApp && tgUser) { + try { + const resp = await fetch(SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id); + const data = await resp.json(); + if (data.ok && data.world_id) { + worldId = data.world_id; + tgSaveLoaded = true; + overlay.remove(); + styleEl.remove(); + resolve(worldId); + return; + } + } catch(e) { console.warn('[TG] Load save failed:', e); } + } + if (hasLocalSave) { + worldId = generateShortWorldCode(); + overlay.remove(); + styleEl.remove(); + resolve(worldId); + return; + } + customAlert('Сохранений не найдено'); + }; + body.appendChild(btnHistory); + } + + // === TG-only buttons === + if (isTgWebApp && window.Telegram && Telegram.WebApp.switchInlineQuery) { + // Invite friends — with margin + const btnInvite = document.createElement('button'); + btnInvite.className = 'mc-btn mc-btn-outline'; + btnInvite.style.marginTop = '16px'; + btnInvite.textContent = '📤 Пригласить друзей'; + btnInvite.onclick = () => { + const code = worldId || generateShortWorldCode(); + const url = 'https://t.me/Grechkacraft_bot/app?startapp=' + encodeURIComponent(code); + if (Telegram.WebApp && Telegram.WebApp.openTelegramLink) { + Telegram.WebApp.openTelegramLink('https://t.me/share/url?url=' + encodeURIComponent(url) + '&text=' + encodeURIComponent('🏗️ Заходи в GrechkaCraft! Мир: ' + code)); + } else { + navigator.clipboard.writeText(url).then(() => showToast('📋 Скопировано!')).catch(() => showToast(url)); + } + }; + body.appendChild(btnInvite); + + // Friends — with margin + const btnFriends = document.createElement('button'); + btnFriends.className = 'mc-btn mc-btn-orange'; + btnFriends.style.marginTop = '8px'; + btnFriends.textContent = '👥 Друзья'; + btnFriends.onclick = async () => { + try { + const resp = await fetch(SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id); + const data = await resp.json(); + body.innerHTML = ''; + + // Back button at top + const btnBackTop = document.createElement('button'); + btnBackTop.className = 'mc-btn mc-btn-gray'; + btnBackTop.textContent = '← Назад'; + btnBackTop.style.marginBottom = '12px'; + btnBackTop.onclick = () => { overlay.remove(); styleEl.remove(); showStartMenu().then(resolve); }; + body.appendChild(btnBackTop); + + // Nickname section + if (data.nickname) { + const nickDiv = document.createElement('div'); + nickDiv.className = 'mc-friend-row'; + nickDiv.innerHTML = '🔗 Твой ник: ' + data.nickname + ''; + nickDiv.style.background = '#D5E8C5'; + body.appendChild(nickDiv); + } else { + const nickLabel = document.createElement('div'); + nickLabel.className = 'mc-label'; + nickLabel.style.color = '#9E2B2B'; + nickLabel.textContent = '⚡ Задай ник, чтобы друзья могли тебя найти:'; + body.appendChild(nickLabel); + const nickRow = document.createElement('div'); + nickRow.style.display = 'flex'; + nickRow.style.gap = '4px'; + nickRow.style.marginBottom = '8px'; + const nickInput = document.createElement('input'); + nickInput.className = 'mc-input'; + nickInput.style.flex = '1'; + nickInput.type = 'text'; + nickInput.maxLength = 16; + nickInput.placeholder = 'Твой ник'; + const nickBtn = document.createElement('button'); + nickBtn.className = 'mc-btn mc-btn-green'; + nickBtn.style.width = 'auto'; + nickBtn.style.marginBottom = '0'; + nickBtn.style.padding = '8px 16px'; + nickBtn.textContent = '✓'; + nickBtn.onclick = async () => { + const n = nickInput.value.trim().toLowerCase(); + if (!n || n.length < 2) { showToast('❌ Минимум 2 символа'); return; } + const check = await fetch(SERVER_URL + '/api/nick/check?nick=' + encodeURIComponent(n)); + const cd = await check.json(); + if (!cd.available) { showToast('❌ Ник занят'); return; } + const set = await fetch(SERVER_URL + '/api/nick/set', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,nickname:n})}); + const sd = await set.json(); + if (sd.ok) { showToast('✅ Ник: ' + sd.nickname); btnFriends.onclick(); } else { showToast('❌ Ошибка'); } + }; + nickRow.appendChild(nickInput); + nickRow.appendChild(nickBtn); + body.appendChild(nickRow); + } + + // Friends list + if (data.friends && data.friends.length) { + const listLabel = document.createElement('div'); + listLabel.className = 'mc-label'; + listLabel.textContent = '👥 Друзья:'; + body.appendChild(listLabel); + for (const f of data.friends) { + const fDiv = document.createElement('div'); + fDiv.className = 'mc-friend-row'; + const statusDot = f.online ? '🟢' : '⚫'; + let info = statusDot + ' ' + f.nickname + ''; + if (f.online && f.world_id) info += ' — мир ' + f.world_id; + const fInfo = document.createElement('div'); + fInfo.innerHTML = info; + fDiv.appendChild(fInfo); + if (f.online && f.world_id) { + const joinBtn = document.createElement('button'); + joinBtn.className = 'mc-friend-btn mc-friend-btn-green'; + joinBtn.textContent = '🎮 Вступить'; + joinBtn.onclick = () => { worldId = f.world_id; overlay.remove(); styleEl.remove(); resolve(worldId); }; + fDiv.appendChild(joinBtn); + } + body.appendChild(fDiv); + } + } else if (data.friends && !data.friends.length) { + const noFriends = document.createElement('div'); + noFriends.className = 'mc-label'; + noFriends.style.color = '#888'; + noFriends.textContent = 'Пока нет друзей'; + body.appendChild(noFriends); + } + + // Pending requests + if (data.pending && data.pending.length) { + const pLabel = document.createElement('div'); + pLabel.className = 'mc-label'; + pLabel.style.color = '#B86E00'; + pLabel.textContent = '📨 Запросы в друзья:'; + body.appendChild(pLabel); + for (const p of data.pending) { + const pItem = document.createElement('div'); + pItem.className = 'mc-friend-row'; + const span = document.createElement('span'); + span.textContent = p.from; + pItem.appendChild(span); + const btns = document.createElement('div'); + btns.style.display = 'flex'; + btns.style.gap = '4px'; + const acceptBtn = document.createElement('button'); + acceptBtn.className = 'mc-friend-btn mc-friend-btn-green'; + acceptBtn.textContent = '✓'; + acceptBtn.onclick = async () => { + const r = await fetch(SERVER_URL + '/api/friends/accept', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,request_id:p.id})}); + const d = await r.json(); + if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.onclick(); } else { showToast('❌ Ошибка'); } + }; + const declineBtn = document.createElement('button'); + declineBtn.className = 'mc-friend-btn mc-friend-btn-red'; + declineBtn.textContent = '✕'; + declineBtn.onclick = async () => { + await fetch(SERVER_URL + '/api/friends/decline', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,request_id:p.id})}); + showToast('Отклонено'); + btnFriends.onclick(); + }; + btns.appendChild(acceptBtn); + btns.appendChild(declineBtn); + pItem.appendChild(btns); + body.appendChild(pItem); + } + } + + // Add friend + body.appendChild(document.createElement('hr')); + body.className = 'mc-label'; + const addLabel = document.createElement('div'); + addLabel.className = 'mc-label'; + addLabel.textContent = '➕ Добавить друга по нику:'; + body.appendChild(addLabel); + const addRow = document.createElement('div'); + addRow.style.display = 'flex'; + addRow.style.gap = '4px'; + const addInput = document.createElement('input'); + addInput.className = 'mc-input'; + addInput.style.flex = '1'; + addInput.type = 'text'; + addInput.maxLength = 16; + addInput.placeholder = 'Ник друга'; + const addBtn = document.createElement('button'); + addBtn.className = 'mc-btn mc-btn-blue'; + addBtn.style.width = 'auto'; + addBtn.style.marginBottom = '0'; + addBtn.style.padding = '8px 16px'; + addBtn.textContent = '+'; + addBtn.onclick = async () => { + const nick = addInput.value.trim().toLowerCase(); + if (!nick || nick.length < 2) { showToast('❌ Минимум 2 символа'); return; } + const r = await fetch(SERVER_URL + '/api/friends/request', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,nickname:nick})}); + const d = await r.json(); + if (d.ok) { showToast(d.message === 'Already friends' ? '✅ Уже друзья!' : '📨 Запрос отправлен!'); } + else { showToast('❌ ' + (d.error || 'Ошибка')); } + }; + addRow.appendChild(addInput); + addRow.appendChild(addBtn); + body.appendChild(addRow); + + // Privacy checkboxes + const settings = data.settings || { allow_requests: 1, notify_online: 1 }; + const settingsDiv = document.createElement('div'); + settingsDiv.style.marginTop = '12px'; + const reqLabel = document.createElement('label'); + reqLabel.className = 'mc-checkbox-row'; + const reqCb = document.createElement('input'); + reqCb.className = 'mc-checkbox'; + reqCb.type = 'checkbox'; + reqCb.checked = settings.allow_requests; + reqCb.onchange = async () => { + await fetch(SERVER_URL + '/api/friends/settings', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,allow_requests:reqCb.checked?1:0,notify_online:settings.notify_online})}); + }; + reqLabel.appendChild(reqCb); + reqLabel.appendChild(document.createTextNode(' Запросы в друзья')); + settingsDiv.appendChild(reqLabel); + body.appendChild(settingsDiv); + } catch(e) { showToast('❌ Ошибка загрузки друзей'); console.error(e); } + }; + body.appendChild(btnFriends); + } + + // Player name input + if (!playerName) { + const nameDivider = document.createElement('hr'); + nameDivider.className = 'mc-divider'; + body.appendChild(nameDivider); + const nameLabel = document.createElement('div'); + nameLabel.className = 'mc-label'; + nameLabel.textContent = 'Твоё имя:'; + body.appendChild(nameLabel); + const nameInput = document.createElement('input'); + nameInput.className = 'mc-input'; + nameInput.type = 'text'; + nameInput.placeholder = 'Ваше имя'; + nameInput.value = tgUser ? (tgUser.username || tgUser.first_name || '') : ''; + nameInput.maxLength = 20; + nameInput.addEventListener('input', () => { + if (nameInput.value.trim()) { + playerName = nameInput.value.trim(); + localStorage.setItem('minegrechka_playerName', playerName); + } + }); + body.appendChild(nameInput); + } + + box.appendChild(body); + overlay.appendChild(box); + document.body.appendChild(overlay); + + // Auto-load TG save + if (isTgWebApp && tgUser && !worldParam) { + fetch(SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id) + .then(r => r.json()) + .then(data => { + if (data.ok && data.world_id) { + // could update UI but menu is already shown + } + }) + .catch(() => {}); + } + }); + } + + // ==================== GAME LAUNCH ==================== + // Override the old auto-join logic + let gameLaunched = false; + + async function launchGame() { + if (gameLaunched) return; + gameLaunched = true; + + if (!playerName) { + playerName = tgUser ? (tgUser.username || tgUser.first_name || 'Игрок') : 'Игрок'; + localStorage.setItem('minegrechka_playerName', playerName); + } + + // Validate TG initData if in TG context + if (isTgWebApp && Telegram.WebApp.initData) { + try { + const resp = await fetch(SERVER_URL + '/api/tg/auth', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ initData: Telegram.WebApp.initData }) + }); + const data = await resp.json(); + if (data.ok && data.tg_id) { + console.log('[TG] Authenticated, tg_id:', data.tg_id); + tgUser = tgUser || {}; + tgUser.id = data.tg_id; + } + } catch(e) { console.warn('[TG] Auth failed:', e); } + } + + // Update URL with world code + try { + const newUrl = new URL(window.location.href); + newUrl.searchParams.set('world', worldId); + if (typeof history !== 'undefined' && history.replaceState) { + history.replaceState(null, '', newUrl.toString()); + } + } catch(e) {} + + console.log('[Launch] worldId:', worldId, 'player:', playerName, 'tg:', !!tgUser); + + // The rest of the game init will pick up worldId and playerName + // Signal that menu is done + startMenuResolved = true; + window.dispatchEvent(new Event('startMenuDone')); + } + + // ==================== SOCKET.IO КЛИЕНТ ==================== + let socket = null; + let isMultiplayer = false; // Флаг для мультиплеерного режима + const otherPlayers = new Map(); // socket_id -> {x, y, color} + const serverMobs = new Map(); // id -> mob (server-spawned, client-authoritative physics) + + // Helper to get all mobs (local + server-spawned in MP) + function getAllMobs() { + return isMultiplayer ? mobs.concat(Array.from(serverMobs.values())) : mobs; + } + + // Create a client-side mob object from server spawn data with correct properties matching client constructors + function createMobFromServer(data) { + const kindProps = { + zombie: { w: 34, h: 50, hp: 4, speed: 80 + Math.random() * 40, hostile: true, fuse: 0, shootCooldown: 2 }, + creeper: { w: 34, h: 50, hp: 4, speed: 60 + Math.random() * 30, hostile: true, fuse: 3.2, shootCooldown: 2 }, + skeleton: { w: 34, h: 50, hp: 4, speed: 70 + Math.random() * 30, hostile: true, fuse: 0, shootCooldown: 0 }, + pig: { w: 34, h: 24, hp: 2, speed: 40, hostile: false, fuse: 0, shootCooldown: 2 }, + chicken: { w: 26, h: 22, hp: 1, speed: 55, hostile: false, fuse: 0, shootCooldown: 2 }, + scorpion: { w: 26, h: 26, hp: 3, speed: 90, hostile: true, fuse: 0, shootCooldown: 0, biome:'desert' }, + polar_bear:{ w: 40, h: 34, hp: 8, speed: 50, hostile: false, fuse: 0, shootCooldown: 2, biome:'tundra' }, + slime: { w: 24, h: 24, hp: 2, speed: 30, hostile: true, fuse: 0, shootCooldown: 2, biome:'swamp' }, + eagle: { w: 30, h: 22, hp: 3, speed: 120, hostile: true, fuse: 0, shootCooldown: 0, biome:'mountains' } + }; + const props = kindProps[data.kind] || kindProps['pig']; // fallback + return { + id: data.id, + kind: data.kind, + x: data.x, + y: data.y, + w: props.w, + h: props.h, + hp: data.hp || props.hp, + maxHp: data.maxHp || data.hp || props.hp, + speed: props.speed, + hostile: props.hostile, + vx: 0, + vy: 0, + grounded: false, + inWater: false, + aiT: 0, + dir: data.dir || 1, + dead: false, + fuse: props.fuse, + shootCooldown: props.shootCooldown + }; + } + let mySocketId = null; + + // Throttle для отправки позиции (10-20 раз в секунду) + let lastMoveSendTime = 0; + const MOVE_SEND_INTERVAL = 0.05; // 50ms = 20 раз в секунду + let lastSentX = 0, lastSentY = 0; + + function initSocket() { + try { + socket = io(SERVER_URL, { + path: '/socket.io/', + transports: ['websocket', 'polling'] + }); + + socket.on('connect', () => { + console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${worldId}`); + mySocketId = socket.id; + isMultiplayer = true; + + // Присоединяемся к миру + socket.emit('join_world', { world_id: worldId, player_name: playerName, tg_id: (tgUser && tgUser.id) ? tgUser.id : null }); + + // TG: отправляем tg_auth + онлайн статус для уведомления друзей + if (tgUser && tgUser.id) { + socket.emit('tg_auth', { tg_id: tgUser.id }); + try { + fetch(SERVER_URL + '/api/tg/online', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id,world_id:worldId,player_name:playerName||'Игрок'})}); + } catch(e) { console.warn('[TG] online notify error:', e); } + } + + // Показываем в UI + worldIdEl.textContent = worldId; + // XP/Level display + const lvXpNext = xpForLevel(player.level + 1); + const lvXpCur = xpForLevel(player.level); + const xpInLevel = player.xp - lvXpCur; + const xpNeeded = lvXpNext - lvXpCur; + document.getElementById('xplevel').textContent = player.level; + document.getElementById('xpbar').textContent = xpInLevel + '/' + xpNeeded; + multiplayerStatus.style.display = 'block'; + }); + + socket.on('connect_error', (error) => { + console.error('Socket connection error:', error); + isMultiplayer = false; + }); + + socket.on('disconnect', () => { + console.log('Disconnected from server'); + isMultiplayer = false; + otherPlayers.clear(); + multiplayerStatus.style.display = 'none'; + // TG: убираем онлайн статус + if (tgUser && tgUser.id) { + try { fetch(SERVER_URL + '/api/tg/offline', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tg_id:tgUser.id})}); } catch(e) {} + } + }); + + // Обработка world_state + socket.on('world_state', (data) => { + console.log('Received world_state:', data); + + // Устанавливаем seed и перегенерируем мир если он изменился + if (data.seed !== undefined && data.seed !== worldSeed) { + const oldSeed = worldSeed; + worldSeed = data.seed; + console.log('World seed set from', oldSeed, 'to', worldSeed); + // Full regeneration with correct seed + grid.clear(); + blocks.length = 0; + placedBlocks = []; + removedBlocks = []; + generated.clear(); + for (let gx = spawnPoint.gx - 25; gx <= spawnPoint.gx + 25; gx++) { + genColumn(gx); + decorations(gx); + } + console.log('World regenerated with seed:', worldSeed); + } + + // Применяем блоки — сохраняем в serverOverrides для применения после genColumn + if (data.blocks && Array.isArray(data.blocks)) { + for (const block of data.blocks) { + const key = k(block.gx, block.gy); + serverOverrides.set(key, { op: block.op, t: block.t }); + // Также пробуем применить сразу (если колонна уже сгенерирована) + if (block.op === 'set') { + setBlock(block.gx, block.gy, block.t, false); + } else if (block.op === 'remove') { + removeBlock(block.gx, block.gy); + } + } + } + + // Устанавливаем время + if (data.time !== undefined) { + worldTime = data.time; + isNightTime = worldTime > 0.5; + } + + // Всегда считаем spawnPoint на клиенте по той же формуле что и surfaceGyAt + // Это гарантирует совпадение с terrain generation + { + const startGX = 6; + // Генерируем колонну и соседние для безопасного спавна + for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx); + const surfaceY = surfaceGyAt(startGX); + // Ищем ближайшую небудущую позицию сверху вниз от поверхности + let safeGY = surfaceY - 1; + // Проверяем что над поверхностью воздух (не в воде) + const aboveBlock = getBlock(startGX, surfaceY - 1); + if (aboveBlock && aboveBlock.t === 'water') { + // Если в воде — ищем поверхность выше уровня моря + for (let gy = SEA_GY - 1; gy >= 0; gy--) { + const b = getBlock(startGX, gy); + if (!b || b.dead || b.t === 'air' || b.t === 'water') continue; + safeGY = gy - 1; + break; + } + } + spawnPoint.x = startGX * TILE; + spawnPoint.y = safeGY * TILE; + console.log('Client-side spawn point:', spawnPoint, 'surfaceY:', surfaceY, 'safeGY:', safeGY); + } + + // Устанавливаем игрока в точку спавна + player.x = spawnPoint.x; + player.y = spawnPoint.y; + player.vx = 0; + player.vy = 0; + player.fallStartY = player.y; + console.log('Player moved to spawn point:', player.x, player.y); + + // Устанавливаем HP на 100% при каждом подключении к миру + player.hp = 100; + player.hunger = 100; + player.o2 = 100; + player.invuln = 0; + console.log('[MULTIPLAYER CONNECT] Player HP set to 100% on connect'); + + // Обновляем список игроков + if (data.players && Array.isArray(data.players)) { + otherPlayers.clear(); + for (const p of data.players) { + if (p.socket_id !== mySocketId) { + otherPlayers.set(p.socket_id, { + x: p.x, + y: p.y, + color: getRandomPlayerColor(p.socket_id), + name: p.player_name || 'Игрок' + }); + } + } + // Обновляем счётчик игроков + playerCountEl.textContent = data.players.length; + } + // Server mobs — client-authoritative: create with full client-side properties + if (data.mobs && Array.isArray(data.mobs)) { + serverMobs.clear(); + for (const m of data.mobs) { + const sm = createMobFromServer(m); + // Пересчитываем Y по клиентской surfaceGyAt + const spawnGX = Math.floor(sm.x / TILE); + genColumn(spawnGX - 1); genColumn(spawnGX); genColumn(spawnGX + 1); + const clientSurfaceY = surfaceGyAt(spawnGX); + sm.y = (clientSurfaceY - 1) * TILE; + sm.vy = 0; + serverMobs.set(m.id, sm); + } + } + // Claims (приватные зоны) + if (data.claims && Array.isArray(data.claims)) { + claims = data.claims; + myClaimCount = claims.filter(function(c) { return c.owner_name === playerName; }).length; + console.log('[Claims] Loaded', claims.length, 'claims for world'); + } + }); + + // Игрок присоединился + socket.on('player_joined', (data) => { + console.log('Player joined:', data.socket_id); + if (data.socket_id !== mySocketId) { + // Генерируем безопасную позицию для нового игрока + const spawnGX = 6; + genColumn(spawnGX); + const surfaceY = surfaceGyAt(spawnGX); + const safeSpawnX = spawnGX * TILE; + const safeSpawnY = (surfaceY - 1) * TILE; + + otherPlayers.set(data.socket_id, { + x: safeSpawnX, + y: safeSpawnY, + tx: safeSpawnX, + ty: safeSpawnY, + color: getRandomPlayerColor(data.socket_id), + name: data.player_name || 'Игрок' + }); + addChatMessage('Система', `Игрок присоединился`); + // Обновляем видимость кнопки сохранения + updateSaveButtonVisibility(); + } + }); + + // Игрок переместился + socket.on('player_moved', (data) => { + if (data.socket_id !== mySocketId && otherPlayers.has(data.socket_id)) { + const p = otherPlayers.get(data.socket_id); + // Lerp: запоминаем целевую позицию, рисуем в рендере с интерполяцией + p.tx = data.x; + p.ty = data.y; + // Обновляем имя, если оно пришло + if (data.player_name) { + p.name = data.player_name; + } + } + }); + + // Игрок покинул + socket.on('player_left', (data) => { + console.log('Player left:', data.socket_id); + otherPlayers.delete(data.socket_id); + addChatMessage('Система', `Игрок покинул игру`); + // Обновляем видимость кнопки сохранения + updateSaveButtonVisibility(); + }); + + // === MOB SYNC (multiplayer) === + + socket.on('mob_spawned', (data) => { + const sm = createMobFromServer(data); + // Пересчитываем Y по клиентской surfaceGyAt — сервер может ошибаться + const spawnGX = Math.floor(sm.x / TILE); + // Убеждаемся что чанк сгенерирован (иначе getBlock=null → моб падает сквозь землю) + genColumn(spawnGX - 1); genColumn(spawnGX); genColumn(spawnGX + 1); + const clientSurfaceY = surfaceGyAt(spawnGX); + sm.y = (clientSurfaceY - 1) * TILE; // 1 блок выше поверхности (под ногами) + sm.vy = 0; + sm.grounded = false; // resolveY в mobAI поставит grounded=true + serverMobs.set(data.id, sm); + }); + + socket.on('mob_positions', (arr) => { + for (const u of arr) { + let sm = serverMobs.get(u.id); + if (!sm) { + sm = createMobFromServer({ id: u.id, kind: u.kind || 'zombie', x: u.x, y: u.y, + hp: u.hp || 1, maxHp: u.hp || 1, dir: u.dir || 1 }); + serverMobs.set(u.id, sm); + } + sm.x += (u.x - sm.x) * 0.35; + sm.y += (u.y - sm.y) * 0.35; + sm.dir = u.dir != null ? u.dir : sm.dir; + sm.hp = u.hp != null ? u.hp : sm.hp; + sm.fuse = u.fuse != null ? u.fuse : sm.fuse; + sm.grounded = !!u.grounded; + } + }); + + socket.on('mob_despawned', (data) => { serverMobs.delete(data.id); }); + + socket.on('mob_died', (data) => { + const sm = serverMobs.get(data.id); + if (sm && data.killer === mySocketId) { + // Give loot to the killer + if (sm.kind === 'chicken') playSound('hurt_chicken'); + spawnDrops(sm.x, sm.y, sm.kind); + rebuildHotbar(); + } + serverMobs.delete(data.id); + }); + + socket.on('mob_hurt_ack', (data) => { + const sm = serverMobs.get(data.id); + if (sm) sm.hp = data.hp; + }); + + socket.on('mob_explode', (data) => { + explodeAt(data.gx, data.gy); + serverMobs.delete(data.id); + }); + + socket.on('mob_shoot', (data) => { + projectiles.push({ + x: data.x, y: data.y, vx: data.vx, vy: data.vy, + dmg: data.dmg, owner: 'mob', life: data.life + }); + }); + + // Блок изменён + + // Claim events from server + socket.on('claim_created', (data) => { + playSound('click'); + showToast('🏴 Территория захвачена! Радиус: ' + data.radius + ' блоков', 3000); + }); + + socket.on('claim_removed', (data) => { + playSound('click'); + showToast('🏴 Флаг снят — территория свободна', 3000); + }); + + socket.on('claims_update', (data) => { + claims = data; + myClaimCount = claims.filter(c => c.owner_name === playerName).length; + }); + + socket.on('claim_denied', (data) => { + showToast('⛔ Территория ' + data.owner + ' — чужая зона!', 3000); + }); + + socket.on('claim_error', (data) => { + showToast('❌ ' + data.msg, 3000); + }); + + socket.on('block_changed', (data) => { + const key = k(data.gx, data.gy); + serverOverrides.set(key, { op: data.op, t: data.t }); + if (data.op === 'set') { + setBlock(data.gx, data.gy, data.t, false); + } else if (data.op === 'remove') { + removeBlock(data.gx, data.gy); + } + }); + + // Сообщение в чат + socket.on('chat_message', (data) => { + const senderName = data.socket_id === mySocketId ? 'Вы' : `Игрок ${data.socket_id.substring(0, 6)}`; + addChatMessage(senderName, data.message); + }); + + // Обновление времени + socket.on('time_update', (data) => { + if (data.time !== undefined) { + worldTime = data.time; + isNightTime = worldTime > 0.5; + } + }); + + } catch (e) { + console.error('Error initializing socket:', e); + isMultiplayer = false; + } + } + + // Генерация случайного цвета для игрока на основе socket_id + function getRandomPlayerColor(socketId) { + const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e91e63', '#00bcd4']; + let hash = 0; + for (let i = 0; i < socketId.length; i++) { + hash = ((hash << 5) - hash) + socketId.charCodeAt(i); + hash = hash & hash; + } + return colors[Math.abs(hash) % colors.length]; + } + + // Отправка позиции игрока (с throttle) + function sendPlayerPosition() { + if (!isMultiplayer || !socket || !socket.connected) return; + + const now = performance.now() / 1000; + if (now - lastMoveSendTime < MOVE_SEND_INTERVAL) return; + + // Отправляем только если позиция изменилась + const dx = Math.abs(player.x - lastSentX); + const dy = Math.abs(player.y - lastSentY); + if (dx < 1 && dy < 1) return; + + lastMoveSendTime = now; + lastSentX = player.x; + lastSentY = player.y; + + socket.emit('player_move', { x: player.x, y: player.y, player_name: playerName }); + } + + // Отправка изменения блока + function sendBlockChange(gx, gy, t, op) { + if (!isMultiplayer || !socket || !socket.connected) return; + + socket.emit('block_change', { gx, gy, t, op }); + } + + + // ==================== CLAIMS (ПРИВАТНЫЕ ЗОНЫ) ==================== + let claims = []; // [{gx, gy, radius, owner_name}] + let myClaimCount = 0; // How many flags player has placed + + // Claim border overlay colors + const CLAIM_COLORS = [ + 'rgba(231,76,60,0.3)', // red-ish + 'rgba(52,152,219,0.3)', // blue + 'rgba(46,204,113,0.3)', // green + 'rgba(155,89,182,0.3)', // purple + 'rgba(241,196,15,0.3)', // yellow + ]; + + // ==================== ЧАТ ==================== + const chatMessages = []; + const MAX_CHAT_MESSAGES = 20; + + function addChatMessage(sender, message) { + const time = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); + chatMessages.push({ sender, message, time }); + if (chatMessages.length > MAX_CHAT_MESSAGES) { + chatMessages.shift(); + } + renderChatMessages(); + } + + function renderChatMessages() { + const chatMessagesEl = document.getElementById('chatMessages'); + if (!chatMessagesEl) return; + + chatMessagesEl.innerHTML = chatMessages.map(m => + `
${m.time} ${m.sender}: ${m.message}
` + ).join(''); + + // Прокручиваем вниз + chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight; + } + + function sendChatMessage(message) { + if (!message || message.trim() === '') return; + + if (isMultiplayer && socket && socket.connected) { + socket.emit('chat_message', { message: message.trim() }); + } else { + addChatMessage('Вы', message.trim()); + } + } + + // ==================== ПОДЕЛИТЬСЯ МИРОМ ==================== + function shareWorld() { + const shareUrl = new URL(window.location.href); + shareUrl.searchParams.set('world', worldId); + const shareUrlString = shareUrl.toString(); + + // Копируем в буфер обмена + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(shareUrlString).then(() => { + alert('Ссылка скопирована!'); + }).catch(() => { + alert('Ссылка на мир:\n' + shareUrlString); + }); + } else { + alert('Ссылка на мир:\n' + shareUrlString); + } + } + + // ==================== ИНИЦИАЛИЗАЦИЯ UI ==================== + let chatOpen = false; + + document.getElementById('chatToggle').onclick = () => { + playSound('click'); + chatOpen = !chatOpen; + document.getElementById('chatPanel').style.display = chatOpen ? 'block' : 'none'; + if (chatOpen) { + document.getElementById('chatInput').focus(); + } + }; + + document.getElementById('chatClose').onclick = () => { + playSound('click'); + chatOpen = false; + document.getElementById('chatPanel').style.display = 'none'; + }; + + document.getElementById('chatSend').onclick = () => { + const input = document.getElementById('chatInput'); + sendChatMessage(input.value); + input.value = ''; + }; + + document.getElementById('chatInput').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + sendChatMessage(e.target.value); + e.target.value = ''; + } + }); + + // ==================== ИНИЦИАЛИЗАЦИЯ СОКЕТА ==================== + // Show start menu first, then init socket + showStartMenu().then((wid) => { + worldId = wid; + // Send tg_id if TG user + initSocket(); + launchGame(); + }); + + // ==================== ЗВУКОВОЙ ДВИЖОК ==================== + const sounds = {}; + function loadSound(id, src) { + const audio = new Audio(); + audio.src = src; + audio.volume = 0.3; + sounds[id] = audio; + } + + // Загрузка звуков + loadSound('splash', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3'); + loadSound('sand1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3'); + loadSound('snow1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3'); + loadSound('stone1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3'); + loadSound('wood1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3'); + loadSound('cloth1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3'); + loadSound('fire', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3'); + loadSound('hit1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3'); + loadSound('attack', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3'); + loadSound('hurt_chicken', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3'); + loadSound('stone_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3'); + loadSound('wood_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3'); + loadSound('click', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3'); + loadSound('explode1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3'); + loadSound('glass1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3'); + loadSound('eat1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3'); + loadSound('step', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3'); + + function playSound(id) { + if(sounds[id]) { + sounds[id].currentTime = 0; + sounds[id].play().catch(e => console.error('Sound error:', e)); + } + } + + // Играем звук при прыжке + const gameEl = document.getElementById('game'); + const canvas = document.getElementById('c'); + const ctx = canvas.getContext('2d'); + + // offscreen light map (не вставляем в DOM) + const lightC = document.createElement('canvas'); + const lightCtx = lightC.getContext('2d'); + + const dpr = Math.max(1, window.devicePixelRatio || 1); + let W=0, H=0; + + const TILE = 40; + + // Мир + const SEA_GY = 14; // уровень воды (gy) - уменьшил для меньших водоёмов + const BEDROCK_GY = 140; // глубина бедрока (gy), чем больше - тем глубже + const GEN_MARGIN_X = 40; // запас генерации по X (в тайлах) — увеличен для плавной прогрузки + + const heroImg = new Image(); + heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png'; + + // Состояние инвентаря + let showFullInventory = false; + let recentItems = []; // Последние 5 выбранных предметов + + const BLOCKS = { + air: { n:'Воздух', solid:false }, + grass: { n:'Трава', c:'#7cfc00', solid:true }, + dirt: { n:'Грязь', c:'#8b4513', solid:true }, + stone: { n:'Камень', c:'#7f8c8d', solid:true }, + sand: { n:'Песок', c:'#f4d06f', solid:true }, + gravel: { n:'Гравий', c:'#95a5a6', solid:true }, + clay: { n:'Глина', c:'#74b9ff', solid:true }, + wood: { n:'Дерево', c:'#d35400', solid:true }, + planks: { n:'Доски', c:'#e67e22', solid:true }, + ladder: { n:'Лестница',c:'#d35400', solid:false, climbable:true }, + leaves: { n:'Листва', c:'#2ecc71', solid:true }, + glass: { n:'Стекло', c:'rgba(200,240,255,0.25)', solid:true, alpha:0.55 }, + water: { n:'Вода', c:'rgba(52,152,219,0.55)', solid:false, fluid:true }, + coal: { n:'Уголь', c:'#2c3e50', solid:true }, + copper_ore:{ n:'Медь', c:'#e17055', solid:true }, + iron_ore: { n:'Железо', c:'#dcdde1', solid:true }, + iron_armor:{ n:'Железная броня', c:'#95a5a6', solid:false, armor:0.5 }, + gold_ore: { n:'Золото', c:'#f1c40f', solid:true }, + diamond_ore:{n:'Алмаз', c:'#00a8ff', solid:true }, + brick: { n:'Кирпич', c:'#c0392b', solid:true }, + tnt: { n:'TNT', c:'#e74c3c', solid:true, explosive:true }, + campfire: { n:'Костёр', c:'#e67e22', solid:true, lightRadius:280 }, + torch: { n:'Факел', c:'#f9ca24', solid:true, lightRadius:220 }, + bedrock: { n:'Бедрок', c:'#2d3436', solid:true, unbreakable:true }, + flower: { n:'Цветок', c:'#ff4757', solid:false, decor:true }, + bed: { n:'Кровать', c:'#e91e63', solid:true, bed:true }, + boat: { n:'Лодка', c:'#8B4513', solid:false }, + furnace: { n:'Печь', c:'#696969', solid:true, smelting:true }, + flag: { n:'Флаг', c:'#e74c3c', solid:true, claimRadius:15 }, + // === BIOME BLOCKS === + snow: { n:'Снег', c:'#ecf0f1', solid:true }, + ice: { n:'Лёд', c:'#74b9ff', solid:true, slip:true }, + cactus: { n:'Кактус', c:'#27ae60', solid:true, hurt:true }, + mushroom: { n:'Гриб', c:'#e74c3c', solid:false, decor:true }, + moss: { n:'Мох', c:'#0a6640', solid:true }, + swamp_water:{ n:'Болотная вода', c:'rgba(100,120,40,0.6)', solid:false, fluid:true, poison:true }, + farmland: { n:'Грядка', c:'#8B6914', solid:true, farmable:true }, + dead_bush: { n:'Сухой куст', c:'#b2bec3', solid:false, decor:true }, + spruce_leaves:{ n:'Ель', c:'#0a6640', solid:true }, + // === CROP STAGES === + wheat_stage0:{ n:'Росток пшеницы', c:'#a8e6a0', solid:false, decor:true, growthStage:0, growthTime:30, growsTo:'wheat_stage1' }, + wheat_stage1:{ n:'Пшеница', c:'#7dcea0', solid:false, decor:true, growthStage:1, growthTime:30, growsTo:'wheat_stage2' }, + wheat_stage2:{ n:'Пшеница', c:'#b8d730', solid:false, decor:true, growthStage:2, growthTime:30, growsTo:'wheat_stage3' }, + wheat_stage3:{ n:'Пшеница', c:'#f1c40f', solid:false, decor:true, harvestable:true, harvestItem:'wheat', harvestQty:2 }, + carrot_stage0:{ n:'Росток моркови', c:'#a8e6a0', solid:false, decor:true, growthStage:0, growthTime:30, growsTo:'carrot_stage1' }, + carrot_stage1:{ n:'Морковь', c:'#f0c27a', solid:false, decor:true, growthStage:1, growthTime:30, growsTo:'carrot_stage2' }, + carrot_stage2:{ n:'Морковь', c:'#e8a040', solid:false, decor:true, growthStage:2, growthTime:30, growsTo:'carrot_stage3' }, + carrot_stage3:{ n:'Морковь', c:'#e67e22', solid:false, decor:true, harvestable:true, harvestItem:'carrot', harvestQty:3 }, + potato_stage0:{ n:'Росток картофеля', c:'#a8e6a0', solid:false, decor:true, growthStage:0, growthTime:30, growsTo:'potato_stage1' }, + potato_stage1:{ n:'Картофель', c:'#c8b888', solid:false, decor:true, growthStage:1, growthTime:30, growsTo:'potato_stage2' }, + potato_stage2:{ n:'Картофель', c:'#bfaa78', solid:false, decor:true, growthStage:2, growthTime:30, growsTo:'potato_stage3' }, + potato_stage3:{ n:'Картофель', c:'#dfe6e9', solid:false, decor:true, harvestable:true, harvestItem:'potato', harvestQty:2 } + }; + + const ITEMS = { + meat: { n:'Сырое мясо', icon:'🥩', food:15 }, + cooked: { n:'Жареное мясо', icon:'🍖', food:45 }, + arrow: { n:'Стрела', icon:'➡️', stack:64 }, + chicken_meat: { n:'Курица', icon:'🍗', food:12, stack:64 }, + feather: { n:'Перо', icon:'🪶', stack:64 }, + bone: { n:'Кость', icon:'🦴', stack:64 }, + gunpowder: { n:'Порох', icon:'💥', stack:64 }, + // === FARMING ITEMS === + wheat: { n:'Пшеница', icon:'🌾', stack:64 }, + bread: { n:'Хлеб', icon:'🍞', food:30 }, + carrot: { n:'Морковь', icon:'🥕', food:8, stack:64 }, + potato: { n:'Картофель', icon:'🥔', stack:64 }, + baked_potato:{ n:'Печёная картошка', icon:'🥔', food:25 }, + mushroom_stew:{ n:'Грибной суп', icon:'🍲', food:40 }, + flag: { n:'Флаг', icon:'🏴', stack:5, craft: { planks: 3, iron_ore: 1 } }, + // === MOB DROP ITEMS === + scorpion_stinger:{ n:'Жало скорпиона', icon:'🔺', stack:64 }, + polar_fur: { n:'Шкура медведя', icon:'🧥', stack:64 }, + slime_ball: { n:'Слизь', icon:'🟢', stack:64 }, + eagle_feather:{ n:'Перо орла', icon:'🪶', stack:64 }, + // === NEW ARMOR & TOOLS === + gold_armor: { n:'Золотая броня', icon:'🛡️', stack:1, armor:0.65 } + }; + + // Seed мира для детерминированной генерации + // Seed: null in MP until server sends it; SP uses random seed + let worldSeed = isMultiplayer ? null : Math.floor(Math.random() * 1000000); + + // Отслеживание изменений мира (для оптимизированного сохранения) + let placedBlocks = []; // [{gx, gy, t}] - блоки, установленные игроком + let removedBlocks = []; // [{gx, gy}] - блоки, удалённые игроком + + // Серверные изменения — применяются после genColumn чтобы не перезатирались + const serverOverrides = new Map(); // key "gx,gy" => {op:'set'|'remove', t?:string} + + // Инструменты + const TOOLS = { + wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', durability: 60, miningPower: 1, craft: { wood: 3, planks: 2 }, requiredLevel: 1 }, + stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', durability: 130, miningPower: 2, craft: { wood: 1, stone: 3 }, requiredLevel: 2 }, + iron_pickaxe: { n:'Железная кирка', icon:'⛏️', durability: 250, miningPower: 3, craft: { wood: 1, iron_ore: 2 }, requiredLevel: 3 }, + wood_sword: { n:'Деревянный меч', icon:'⚔️', durability: 40, damage: 5, craft: { wood: 2, planks: 1 }, requiredLevel: 1 }, + stone_sword: { n:'Каменный меч', icon:'⚔️', durability: 100, damage: 8, craft: { wood: 1, stone: 2 }, requiredLevel: 2 }, + iron_sword: { n:'Железный меч', icon:'⚔️', durability: 200, damage: 12, craft: { wood: 1, iron_ore: 1 }, requiredLevel: 3 }, + bow: { n:'Лук', icon:'🏹', durability: 150, craft: { wood: 3, planks: 2 }, requiredLevel: 4 }, + diamond_pickaxe: { n:'Алмазная кирка', icon:'⛏️', durability: 500, miningPower: 5, craft: { wood: 1, diamond_ore: 2 }, requiredLevel: 7 }, + diamond_sword: { n:'Алмазный меч', icon:'⚔️', durability: 400, damage: 18, craft: { wood: 1, diamond_ore: 2 }, requiredLevel: 7 }, + hoe: { n:'Мотыга', icon:'🔨', durability: 80, tillTo: 'farmland', craft: { wood: 2, planks: 1 }, requiredLevel: 1 } + }; + + // Текстуры блоков (простые) + const tex = {}; + function makeTex(type) { + const t = BLOCKS[type]; + const c = document.createElement('canvas'); + c.width = 32; c.height = 32; + const g = c.getContext('2d'); + + if (type === 'tnt') { + g.fillStyle='#c0392b'; g.fillRect(0,0,32,32); + g.fillStyle='#fff'; g.fillRect(0,12,32,8); + g.fillStyle='#000'; g.font='bold 10px sans-serif'; g.fillText('TNT',6,20); + return c; + } + if (type === 'campfire') { + g.fillStyle='#5d4037'; g.fillRect(4,26,24,6); + g.fillStyle='#3e2723'; g.fillRect(7,23,18,4); + return c; + } + if (type === 'torch') { + g.fillStyle='#6d4c41'; g.fillRect(14,10,4,18); + g.fillStyle='#f39c12'; g.fillRect(12,6,8,8); + return c; + } + if (type === 'glass') { + g.fillStyle='rgba(200,240,255,0.25)'; g.fillRect(0,0,32,32); + g.strokeStyle='rgba(255,255,255,0.65)'; g.strokeRect(2,2,28,28); + g.beginPath(); g.moveTo(5,27); g.lineTo(27,5); g.stroke(); + return c; + } + if (type === 'water') { + g.fillStyle = t.c; g.fillRect(0,0,32,32); + g.fillStyle = 'rgba(255,255,255,0.08)'; + g.fillRect(0,6,32,2); + return c; + } + if (type === 'bed') { + // Основание кровати + g.fillStyle = '#e91e63'; + g.fillRect(0, 0, 32, 32); + // Подушка + g.fillStyle = '#f8bbd0'; + g.fillRect(2, 2, 14, 14); + // Одеяло + g.fillStyle = '#c2185b'; + g.fillRect(16, 4, 14, 24); + // Детали одеяла + g.fillStyle = '#e91e63'; + g.fillRect(18, 6, 10, 20); + return c; + } + if (type === 'flower') { + g.fillStyle='#2ecc71'; g.fillRect(14,14,4,18); + g.fillStyle=t.c; g.beginPath(); g.arc(16,12,6,0,6.28); g.fill(); + return c; + } + if (type === 'boat') { + // Корпус лодки + g.fillStyle = '#8B4513'; + g.fillRect(2, 12, 28, 8); + // Борта + g.fillStyle = '#A0522D'; + g.fillRect(0, 10, 32, 12); + // Внутренность + g.fillStyle = '#DEB887'; + g.fillRect(4, 14, 24, 4); + // Дно + g.fillStyle = '#654321'; + g.fillRect(2, 20, 28, 4); + return c; + } + if (type === 'ladder') { + // Боковые стойки лестницы + g.fillStyle = '#8B4513'; + g.fillRect(4, 0, 4, 32); + g.fillRect(24, 0, 4, 32); + // Ступени + g.fillStyle = '#A0522D'; + g.fillRect(4, 4, 24, 3); + g.fillRect(4, 12, 24, 3); + g.fillRect(4, 20, 24, 3); + g.fillRect(4, 28, 24, 3); + return c; + } + // === BIOME BLOCK TEXTURES === + if (type === 'snow') { + g.fillStyle = '#ecf0f1'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#dfe6e9'; + for (let i = 0; i < 4; i++) g.fillRect((Math.random()*26)|0, (Math.random()*26)|0, 5, 3); + return c; + } + if (type === 'ice') { + g.fillStyle = '#74b9ff'; + g.fillRect(0, 0, 32, 32); + g.strokeStyle = 'rgba(255,255,255,0.4)'; + g.beginPath(); g.moveTo(4,10); g.lineTo(20,16); g.lineTo(10,28); g.stroke(); + g.beginPath(); g.moveTo(18,4); g.lineTo(28,12); g.stroke(); + return c; + } + if (type === 'cactus') { + g.fillStyle = '#27ae60'; + g.fillRect(6, 2, 20, 28); + g.fillStyle = '#2ecc71'; + g.fillRect(2, 8, 6, 4); + g.fillRect(24, 14, 6, 4); + g.fillStyle = '#1e8449'; + g.fillRect(12, 0, 2, 30); + g.fillRect(18, 0, 2, 30); + return c; + } + if (type === 'mushroom') { + g.fillStyle = '#f5e6cc'; g.fillRect(14, 20, 4, 12); + g.fillStyle = '#e74c3c'; g.beginPath(); g.arc(16, 14, 10, Math.PI, 0); g.fill(); + g.fillStyle = '#fff'; g.fillRect(10, 10, 3, 3); g.fillRect(17, 8, 4, 3); + return c; + } + if (type === 'moss') { + g.fillStyle = '#0a6640'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#1e8449'; + for (let i = 0; i < 6; i++) g.fillRect((Math.random()*26)|0, (Math.random()*26)|0, 4, 3); + return c; + } + if (type === 'swamp_water') { + g.fillStyle = 'rgba(100,120,40,0.6)'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = 'rgba(80,100,20,0.3)'; + g.fillRect(0, 10, 32, 2); + g.fillRect(0, 22, 32, 2); + return c; + } + if (type === 'farmland') { + g.fillStyle = '#8B6914'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#7a5c10'; + for (let i = 0; i < 4; i++) g.fillRect(0, 6+i*8, 32, 2); + g.fillStyle = '#6B4E0A'; + g.fillRect(8, 2, 2, 28); + g.fillRect(18, 2, 2, 28); + return c; + } + if (type === 'dead_bush') { + g.fillStyle = '#b2bec3'; + g.fillRect(12, 16, 2, 16); + g.fillRect(8, 14, 8, 2); + g.fillRect(16, 12, 8, 2); + g.fillRect(6, 18, 4, 2); + return c; + } + if (type === 'spruce_leaves') { + g.fillStyle = '#0a6640'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#0d7a4d'; + for (let i = 0; i < 5; i++) g.fillRect((Math.random()*24)|0, (Math.random()*24)|0, 6, 4); + return c; + } + // CROP STAGES + if (type.startsWith('wheat_stage') || type.startsWith('carrot_stage') || type.startsWith('potato_stage')) { + const st = parseInt(type.charAt(type.length-1)); + const colors = type.startsWith('wheat') ? ['#a8e6a0','#7dcea0','#b8d730','#f1c40f'] : + type.startsWith('carrot') ? ['#a8e6a0','#f0c27a','#e8a040','#e67e22'] : + ['#a8e6a0','#c8b888','#bfaa78','#dfe6e9']; + g.fillStyle = '#5d4037'; + g.fillRect(15, 18, 2, 14); + if (st >= 1) { + g.fillStyle = colors[st]; + g.fillRect(10, 8 + (3-st)*3, 12, 10); + } else { + g.fillStyle = '#a8e6a0'; + g.fillRect(14, 22, 4, 4); + } + return c; + } + + g.fillStyle = t.c || '#000'; + g.fillRect(0,0,32,32); + + g.fillStyle = 'rgba(0,0,0,0.10)'; + for (let i=0;i<6;i++) g.fillRect((Math.random()*28)|0, (Math.random()*28)|0, 4,4); + + if (type.endsWith('_ore') || type==='coal') { + g.fillStyle = 'rgba(0,0,0,0.35)'; + for (let i=0;i<4;i++) g.fillRect((Math.random()*24)|0, (Math.random()*24)|0, 6,6); + } + return c; + } + Object.keys(BLOCKS).forEach(k => tex[k] = makeTex(k)); + + // Мир-хранилище + const grid = new Map(); // key "gx,gy" => {gx,gy,t, ...} + const blocks = []; // для рендера/перебора видимых + function k(gx,gy){ return gx+','+gy; } + function getBlock(gx,gy){ return grid.get(k(gx,gy)); } + function hasBlock(gx,gy){ return grid.has(k(gx,gy)); } + function isSolid(gx,gy){ + const b = getBlock(gx,gy); + if(!b || b.dead) return false; + const def = BLOCKS[b.t]; + return !!def.solid && !def.fluid && !def.decor; + } + function setBlock(gx,gy,t, isPlayerPlaced = false){ + const key = k(gx,gy); + if(grid.has(key)) return false; + const b = { gx, gy, t, dead:false, active:false, fuse:0 }; + grid.set(key, b); + blocks.push(b); + + // Отслеживаем блоки, установленные игроком + if(isPlayerPlaced){ + placedBlocks.push({gx, gy, t}); + } + + return true; + } + function removeBlock(gx,gy){ + const key = k(gx,gy); + const b = grid.get(key); + if(!b) return null; + if(BLOCKS[b.t].unbreakable) return null; + grid.delete(key); + b.dead = true; + + // Отслеживаем удалённые блоки + const wasPlayerPlaced = placedBlocks.some(pb => pb.gx === gx && pb.gy === gy); + if(wasPlayerPlaced){ + // Удаляем из placedBlocks + placedBlocks = placedBlocks.filter(pb => !(pb.gx === gx && pb.gy === gy)); + } else { + // Это природный блок - добавляем в removedBlocks + removedBlocks.push({gx, gy}); + } + + return b; + } + + // Физика жидкости + const waterUpdateQueue = new Set(); + let waterUpdateTimer = 0; + const WATER_UPDATE_INTERVAL = 0.05; // Обновляем воду каждые 0.05 секунды + + function updateWaterPhysics(dt){ + waterUpdateTimer += dt; + if(waterUpdateTimer < WATER_UPDATE_INTERVAL) return; + waterUpdateTimer = 0; + + // Ограничиваем количество водных блоков для обработки (оптимизация) + const MAX_WATER_BLOCKS_PER_UPDATE = 50; + let processedCount = 0; + + // Собираем только видимые водные блоки в очередь (оптимизация) + waterUpdateQueue.clear(); + const minGX = Math.floor(camX/TILE) - 10; + const maxGX = Math.floor((camX+W)/TILE) + 10; + const minGY = Math.floor(camY/TILE) - 10; + const maxGY = Math.floor((camY+H)/TILE) + 10; + + for(const b of blocks){ + if(processedCount >= MAX_WATER_BLOCKS_PER_UPDATE) break; + if(!b.dead && b.t === 'water' && + b.gx >= minGX && b.gx <= maxGX && + b.gy >= minGY && b.gy <= maxGY){ + waterUpdateQueue.add(k(b.gx, b.gy)); + processedCount++; + } + } + + // Обновляем воду с ограничением глубины распространения + const processed = new Set(); + const toAdd = []; + const MAX_WATER_DEPTH = 20; // Максимальная глубина распространения воды + + for(const key of waterUpdateQueue){ + if(processed.has(key)) continue; + const b = grid.get(key); + if(!b || b.dead) continue; + processed.add(key); + + const gx = b.gx; + const gy = b.gy; + + // Проверяем глубину - не распространяем воду слишком глубоко + if(gy > SEA_GY + MAX_WATER_DEPTH) continue; + + // Проверяем, можно ли воде упасть вниз + const belowKey = k(gx, gy + 1); + const below = grid.get(belowKey); + + // Внизу пусто - вода создаёт новый блок внизу (но не удаляется сверху) + if(!below || below.dead){ + // Ограничиваем создание новых водных блоков + if(toAdd.length < 20){ // Максимум 20 новых блоков за обновление + toAdd.push({gx, gy: gy + 1, t: 'water'}); + processed.add(belowKey); + } + continue; + } + + // Если внизу не вода и не твёрдый блок - вода может течь вниз + if(!isSolid(gx, gy + 1) && below && below.t !== 'water'){ + if(toAdd.length < 20){ + toAdd.push({gx, gy: gy + 1, t: 'water'}); + processed.add(belowKey); + } + continue; + } + + // Если внизу твёрдый блок или вода - вода растекается горизонтально + // Проверяем левую сторону + const leftKey = k(gx - 1, gy); + const left = grid.get(leftKey); + if(!left || left.dead){ + if(toAdd.length < 20){ + toAdd.push({gx: gx - 1, gy, t: 'water'}); + processed.add(leftKey); + } + continue; + } + + // Проверяем правую сторону + const rightKey = k(gx + 1, gy); + const right = grid.get(rightKey); + if(!right || right.dead){ + if(toAdd.length < 20){ + toAdd.push({gx: gx + 1, gy, t: 'water'}); + processed.add(rightKey); + } + continue; + } + } + + // Применяем изменения (только добавляем новые блоки) + for(const newData of toAdd){ + const key = k(newData.gx, newData.gy); + if(!grid.has(key)){ + const b = { + gx: newData.gx, + gy: newData.gy, + t: newData.t, + dead: false, + active: false, + fuse: 0 + }; + grid.set(key, b); + blocks.push(b); + } + } + + // Очищаем мёртвые блоки из массива + for(let i = blocks.length - 1; i >= 0; i--){ + if(blocks[i].dead){ + blocks.splice(i, 1); + } + } + } + + // Инвентарь + const inv = { + dirt:6, stone:0, sand:0, gravel:0, clay:0, + wood:0, planks:0, ladder:0, leaves:0, coal:0, + copper_ore:0, iron_ore:0, gold_ore:0, diamond_ore:0, + brick:0, glass:0, + tnt:1, campfire:0, torch:0, + meat:0, cooked:0, arrow:0, + wood_pickaxe:0, stone_pickaxe:0, iron_pickaxe:0, + wood_sword:0, stone_sword:0, iron_sword:0, + iron_armor:0, gold_armor:0, + bow:0, furnace:0, + bed:0, boat:0, + iron_ingot:0, gold_ingot:0, copper_ingot:0, + diamond_pickaxe:0, diamond_sword:0, hoe:0, + wheat:0, bread:0, carrot:0, potato:0, baked_potato:0, + scorpion_stinger:0, polar_fur:0, slime_ball:0, eagle_feather:0, + snow:0, ice:0, cactus:0, mushroom:0, moss:0, farmland:0, + spruce_leaves:0, dead_bush:0, + wheat_stage0:0, carrot_stage0:0, potato_stage0:0 + }; + let selected = 'dirt'; + + // Прочность инструментов: Map<"tooltype_id", {current, max}> + // При крафте инструмента создаём запись с max durability + const toolDurability = new Map(); + + function addTool(type) { + const def = TOOLS[type]; + if (!def) return; + const id = `${type}_${Date.now()}_${Math.random().toString(36).slice(2,6)}`; + toolDurability.set(id, { type, current: def.durability, max: def.durability }); + return id; + } + + function getToolDurability(id) { + return toolDurability.get(id); + } + + // Найти лучший инструмент данного типа в инвентаре + function findBestTool(toolType) { + if (inv[toolType] <= 0) return null; + // Возвращаем первый попавшийся — упрощённо + return toolType; + } + + // Использовать инструмент (уменьшить прочность). Возвращает true если сломался + function useTool(toolType) { + // Ищем любой инструмент этого типа с прочностью + for (const [id, dur] of toolDurability) { + if (dur.type === toolType) { + dur.current--; + if (dur.current <= 0) { + toolDurability.delete(id); + inv[toolType]--; + rebuildHotbar(); + return true; // сломался + } + return false; + } + } + return false; + } + + const RECIPES = [ + { out:'planks', qty:4, cost:{ wood:1 }, requiredLevel:1 }, + { out:'ladder', qty:3, cost:{ planks:7 }, requiredLevel:1 }, + { out:'torch', qty:2, cost:{ coal:1, planks:1 }, requiredLevel:1 }, + { out:'glass', qty:1, cost:{ sand:3 }, requiredLevel:1 }, + { out:'brick', qty:1, cost:{ stone:2, clay:1 }, requiredLevel:1 }, + { out:'campfire', qty:1, cost:{ wood:1, coal:1 }, requiredLevel:1 }, + { out:'tnt', qty:1, cost:{ sand:2, coal:1 }, requiredLevel:8 }, + { out:'bed', qty:1, cost:{ wood: 3, planks: 3 }, requiredLevel:1 }, + { out:'boat', qty:1, cost:{ wood: 5 }, requiredLevel:2 }, + { out:'wood_pickaxe', qty:1, cost:{ wood: 3, planks: 2 }, requiredLevel:1 }, + { out:'stone_pickaxe', qty:1, cost:{ wood: 1, stone: 3 }, requiredLevel:2 }, + { out:'iron_pickaxe', qty:1, cost:{ wood: 1, iron_ore: 2 }, requiredLevel:3 }, + { out:'wood_sword', qty:1, cost:{ wood: 2, planks: 1 }, requiredLevel:1 }, + { out:'stone_sword', qty:1, cost:{ wood: 1, stone: 2 }, requiredLevel:2 }, + { out:'iron_sword', qty:1, cost:{ wood: 1, iron_ore: 1 }, requiredLevel:3 }, + { out:'iron_armor', qty:1, cost:{ iron_ore: 5 }, requiredLevel:5 }, + { out:'gold_armor', qty:1, cost:{ gold_ore: 8 }, requiredLevel:6 }, + { out:'furnace', qty:1, cost:{ stone: 8 }, requiredLevel:3 }, + { out:'bow', qty:1, cost:{ wood: 3, planks: 2 }, requiredLevel:4 }, + { out:'arrow', qty:4, cost:{ stone: 1, wood: 1 }, requiredLevel:1 }, + // === NEW RECIPES === + { out:'bread', qty:1, cost:{ wheat: 3 }, requiredLevel:1 }, + { out:'diamond_pickaxe', qty:1, cost:{ wood: 1, diamond_ore: 2 }, requiredLevel:7 }, + { out:'diamond_sword', qty:1, cost:{ wood: 1, diamond_ore: 2 }, requiredLevel:7 }, + { out:'hoe', qty:1, cost:{ wood: 2, planks: 1 }, requiredLevel:1 }, + { out:'flag', qty:1, cost:{ planks: 5, iron_ore: 1 }, requiredLevel:2 } + ]; + + // Рецепты печи (обжиг) + const SMELTING_RECIPES = [ + { in:'sand', qty:1, out:'glass', outQty:1, time:3 }, // песок → стекло + { in:'clay', qty:1, out:'brick', outQty:1, time:3 }, // глина → кирпич + { in:'iron_ore', qty:1, out:'iron_ingot', outQty:1, time:5 }, // железо → слиток + { in:'gold_ore', qty:1, out:'gold_ingot', outQty:1, time:5 }, // золото → слиток + { in:'copper_ore', qty:1, out:'copper_ingot', outQty:1, time:4 }, // медь → слиток + { in:'meat', qty:1, out:'cooked', outQty:1, time:2 }, // сырое мясо → жареное + { in:'cobblestone', qty:1, out:'stone', outQty:1, time:2 }, // булыжник → камень + { in:'potato', qty:1, out:'baked_potato', outQty:1, time:2 } // картофель → печёная картошка + ]; + + // Новые предметы от обжига + ITEMS.iron_ingot = { n:'Железный слиток', icon:'🔩' }; + ITEMS.gold_ingot = { n:'Золотой слиток', icon:'🪙' }; + ITEMS.copper_ingot = { n:'Медный слиток', icon:'🟤' }; + ITEMS.diamond_pickaxe = { n:'Алмазная кирка', icon:'⛏️', durability:500, miningPower:5 }; + ITEMS.diamond_sword = { n:'Алмазный меч', icon:'⚔️', durability:400, damage:18 }; + ITEMS.hoe = { n:'Мотыга', icon:'🔨', durability:80, tillTo:'farmland' }; + + // Активные печи: Map ключа блока → { recipe, progress, totalTime } + const activeFurnaces = new Map(); + + // UI + const hpEl = document.getElementById('hp'); + const foodEl = document.getElementById('food'); + const tempEl = document.getElementById("temp"); + const sxEl = document.getElementById('sx'); + const syEl = document.getElementById('sy'); + const todEl = document.getElementById('tod'); + const worldIdEl = document.getElementById('worldId'); + // Toast уведомление + function showToast(msg, duration) { + duration = duration || 1800; + const t = document.createElement('div'); + t.textContent = msg; + t.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#2ecc71;padding:10px 24px;border-radius:10px;font:bold 16px Arial,sans-serif;z-index:99999;pointer-events:none;transition:opacity 0.4s;'; + document.body.appendChild(t); + setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 400); }, duration); + } + // Клик на worldId → копировать код мира + toast + worldIdEl.addEventListener('click', () => { + const code = worldId || ''; + if (!code) return; + const text = code; // копируем только код, не URL + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(() => showToast('📋 Скопировано: ' + code)).catch(() => showToast(code)); + } else { + // fallback + const ta = document.createElement('textarea'); + ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); ta.select(); + try { document.execCommand('copy'); showToast('📋 Скопировано: ' + code); } catch(e) { showToast(code); } + ta.remove(); + } + }); + const playerCountEl = document.getElementById('playerCount'); + const hotbarEl = document.getElementById('hotbar'); + const craftPanel = document.getElementById('craftPanel'); + const recipesEl = document.getElementById('recipes'); + const deathEl = document.getElementById('death'); + const inventoryPanel = document.getElementById('inventoryPanel'); + const inventoryGrid = document.getElementById('inventoryGrid'); + + // ==================== МИНИКАРТА ==================== + const minimapWrap = document.getElementById('minimapWrap'); + const minimapCanvas = document.getElementById('minimap'); + const minimapCtx = minimapCanvas.getContext('2d'); + let minimapOpen = false; + + document.getElementById('mapToggle').onclick = () => { + playSound('click'); + minimapOpen = !minimapOpen; + minimapWrap.style.display = minimapOpen ? 'block' : 'none'; + }; + + // Цвета блоков для миникарты (по 1 пикселю на блок) + const MINIMAP_COLORS = { + grass: '#4a8c2a', dirt: '#6b3410', stone: '#6a6a6a', sand: '#d4b84a', + gravel: '#888', clay: '#5a9ad4', wood: '#a63d00', planks: '#c46a10', + leaves: '#2a8a3a', glass: '#aaddee', water: '#2980b9', coal: '#1a1a2a', + copper_ore: '#a05535', iron_ore: '#b0b0b0', gold_ore: '#d4a017', + diamond_ore: '#0090d0', brick: '#8a2010', tnt: '#c02020', + campfire: '#d08020', torch: '#d0a020', bedrock: '#1a1a1a', + flower: '#d03050', bed: '#c03060', ladder: '#a04000', boat: '#6b3410', + snow: '#ecf0f1', ice: '#74b9ff', cactus: '#27ae60', mushroom: '#e74c3c', + moss: '#0a6640', swamp_water: '#687828', farmland: '#8B6914', + dead_bush: '#b2bec3', spruce_leaves: '#0a6640', + wheat_stage0: '#a8e6a0', wheat_stage1: '#7dcea0', wheat_stage2: '#b8d730', wheat_stage3: '#f1c40f', + carrot_stage0: '#a8e6a0', carrot_stage1: '#f0c27a', carrot_stage2: '#e8a040', carrot_stage3: '#e67e22', + potato_stage0: '#a8e6a0', potato_stage1: '#c8b888', potato_stage2: '#bfaa78', potato_stage3: '#dfe6e9' + }; + + function renderMinimap() { + if (!minimapOpen) return; + const mW = minimapCanvas.width; + const mH = minimapCanvas.height; + const scale = 2; // пикселей на блок + + // Область карты — центрирована на игроке + const pGX = Math.floor(player.x / TILE); + const biome = getCachedBiome(pGX); + const pGY = Math.floor(player.y / TILE); + const viewW = Math.floor(mW / scale); + const viewH = Math.floor(mH / scale); + const startGX = pGX - Math.floor(viewW / 2); + const startGY = pGY - Math.floor(viewH / 2); + + // Очищаем + minimapCtx.fillStyle = isNight() ? '#070816' : '#87CEEB'; + minimapCtx.fillRect(0, 0, mW, mH); + + // Рисуем блоки + const imgData = minimapCtx.createImageData(mW, mH); + const data = imgData.data; + + for (let dx = 0; dx < viewW; dx++) { + for (let dy = 0; dy < viewH; dy++) { + const gx = startGX + dx; + const gy = startGY + dy; + const b = getBlock(gx, gy); + if (!b || b.dead || b.t === 'air') continue; + + const color = MINIMAP_COLORS[b.t]; + if (!color) continue; + + // Парсим hex цвет + const r = parseInt(color.slice(1,3), 16); + const g = parseInt(color.slice(3,5), 16); + const bl = parseInt(color.slice(5,7), 16); + + // Заполняем scale x scale пикселей + for (let sx = 0; sx < scale; sx++) { + for (let sy = 0; sy < scale; sy++) { + const px = dx * scale + sx; + const py = dy * scale + sy; + if (px >= mW || py >= mH) continue; + const idx = (py * mW + px) * 4; + data[idx] = r; + data[idx+1] = g; + data[idx+2] = bl; + data[idx+3] = 255; + } + } + } + } + + minimapCtx.putImageData(imgData, 0, 0); + + // Игрок — белый пиксель по центру + minimapCtx.fillStyle = '#fff'; + minimapCtx.fillRect(Math.floor(mW/2) - 2, Math.floor(mH/2) - 2, 4, 4); + + // Другие игроки — жёлтые точки + for (const [sid, p] of otherPlayers) { + const dx = Math.floor(p.x / TILE) - startGX; + const dy = Math.floor(p.y / TILE) - startGY; + if (dx >= 0 && dx < viewW && dy >= 0 && dy < viewH) { + minimapCtx.fillStyle = '#f1c40f'; + minimapCtx.fillRect(dx * scale - 1, dy * scale - 1, 3, 3); + } + } + + // Мобы — красные (враждебные) / зелёные (животные) + const allMobsForMap = getAllMobs(); + for (const m of allMobsForMap) { + const dx = Math.floor(m.x / TILE) - startGX; + const dy = Math.floor(m.y / TILE) - startGY; + if (dx >= 0 && dx < viewW && dy >= 0 && dy < viewH) { + const hostile = m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton'; + minimapCtx.fillStyle = hostile ? '#e74c3c' : '#2ecc71'; + minimapCtx.fillRect(dx * scale, dy * scale, 2, 2); + } + } + } + + // ==================== ПЕЧЬ (ОБЖИГ) ==================== + const furnacePanel = document.getElementById('furnacePanel'); + const furnaceContent = document.getElementById('furnaceContent'); + let currentFurnaceKey = null; // "gx,gy" текущей открытой печи + + document.getElementById('furnaceClose').onclick = () => { + furnacePanel.style.display = 'none'; + currentFurnaceKey = null; + }; + + function openFurnaceUI(gx, gy) { + currentFurnaceKey = `${gx},${gy}`; + furnacePanel.style.display = 'block'; + renderFurnaceUI(); + } + + function renderFurnaceUI() { + if (!currentFurnaceKey) return; + + // Проверяем что печь всё ещё существует + const [fgx, fgy] = currentFurnaceKey.split(',').map(Number); + const fb = getBlock(fgx, fgy); + if (!fb || fb.t !== 'furnace') { + furnacePanel.style.display = 'none'; + currentFurnaceKey = null; + return; + } + + // Текущий процесс обжига + const active = activeFurnaces.get(currentFurnaceKey); + + let html = '
'; + + // Доступные рецепты — показываем только те, для которых есть ресурсы + for (let i = 0; i < SMELTING_RECIPES.length; i++) { + const recipe = SMELTING_RECIPES[i]; + const haveCount = inv[recipe.in] || 0; + const canSmelt = haveCount >= recipe.qty; + + // Иконка результата + const outDef = BLOCKS[recipe.out]; + const outItem = ITEMS[recipe.out]; + const iconStr = outItem ? outItem.icon : (outDef ? '🧱' : '❓'); + const nameStr = outItem ? outItem.n : (outDef ? outDef.n : recipe.out); + const inItem = ITEMS[recipe.in]; + const inDef = BLOCKS[recipe.in]; + const inName = inItem ? inItem.n : (inDef ? inDef.n : recipe.in); + + html += `
`; + html += `
${iconStr}
`; + html += `
`; + html += `
${nameStr}
`; + html += `
${inName} x${recipe.qty} (есть: ${haveCount}) • ${recipe.time}с
`; + html += `
`; + html += ``; + html += `
`; + } + + // Текущий прогресс + if (active) { + const pct = Math.min(100, Math.floor((active.progress / active.recipe.time) * 100)); + html += `
`; + html += `
🔥 Обжиг: ${pct}%
`; + html += `
`; + html += `
`; + html += `
`; + } + + html += '
'; + furnaceContent.innerHTML = html; + } + + // Глобальная функция для кнопки обжига + window._smelt = (recipeIdx) => { + if (!currentFurnaceKey) return; + const recipe = SMELTING_RECIPES[recipeIdx]; + if ((inv[recipe.in] || 0) < recipe.qty) return; + + // Уже обжигаем в этой печи? + if (activeFurnaces.has(currentFurnaceKey)) return; + + // Забираем ресурсы + inv[recipe.in] -= recipe.qty; + + // Запускаем обжиг + activeFurnaces.set(currentFurnaceKey, { + recipe: recipe, + progress: 0 + }); + + playSound('fire'); + rebuildHotbar(); + renderFurnaceUI(); + }; + + // Тик печей — вызывается в главном цикле + function tickFurnaces(dt) { + for (const [key, furnace] of activeFurnaces) { + furnace.progress += dt; + if (furnace.progress >= furnace.recipe.time) { + // Обжиг завершён — выдаём результат + const outItem = furnace.recipe.out; + if (ITEMS[outItem]) { + inv[outItem] = (inv[outItem] || 0) + furnace.recipe.outQty; + } else if (BLOCKS[outItem]) { + inv[outItem] = (inv[outItem] || 0) + furnace.recipe.outQty; + } + playSound('stone_build'); + activeFurnaces.delete(key); + + // Если эта печь открыта — обновляем UI + if (key === currentFurnaceKey) { + renderFurnaceUI(); + } + } + } + } + + + + // ==================== ГОЛОСОВОЙ ЧАТ v3 ==================== + // ==================== ГОЛОСОВОЙ ЧАТ v3 ==================== +// Per-speaker architecture, AudioWorklet, VAD, Opus/PCM, spatial audio +// Replaces lines 1449-1665 in game.js + + let voiceSocket = null; + let voiceStream = null; + let audioCtx = null; + let voiceActive = false; + let voiceMode = 'near'; + const VOICE_SERVER = 'https://voicegrech.mkn8n.ru'; + + // Voice config + const VC = { + sampleRate: 16000, // 16kHz — sufficient for voice, saves 33% bandwidth + frameMs: 20, // 20ms frames = 320 samples @ 16kHz + samplesPerFrame: 320, // 16000 * 0.02 + vadThreshold: 0.0005, // RMS threshold for voice detection + vadHangover: 5, // 100ms hangover after speech ends + jbufTargetMs: 80, // Target jitter: 80ms (was 200ms) + jbufMinMs: 40, + jbufMaxMs: 200, + maxSpeakers: 6, + voiceRadius: 600, + opusBitrate: 16000, + posUpdateMs: 200, // Update position every 200ms (was 500ms) + }; + + // Codec state + let voiceCodec = 'pcm'; // 'opus' or 'pcm' + let voiceEncoder = null; // WebCodecs AudioEncoder + let voiceSeq = 0; + let voiceTimestamp = 0; + let wasSpeaking = false; + let silenceFrames = 0; + let captureNode = null; + let playbackNode = null; + + // Per-speaker map + const remoteSpeakers = new Map(); // socketId → { jitterBuf, lastFrame, gain, panner, lowpass, decoder, codec, x, y, name, mode, speaking, lastActive } + + // ==================== WORKLET CODE (inline strings) ==================== + + const voiceCaptureWorkletCode = ` +class VoiceCaptureProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this._buf = new Float32Array(320); // 20ms @ 16kHz + this._pos = 0; + this._speaking = false; + this._silenceFrames = 0; + this._hangover = 5; // 100ms + this._vadThreshold = 0.0005; + this._lastRms = 0; + } + process(inputs) { + const input = inputs[0]; + if (!input || !input[0]) return true; + const ch = input[0]; + let i = 0; + while (i < ch.length) { + const rem = this._buf.length - this._pos; + const n = Math.min(rem, ch.length - i); + this._buf.set(ch.subarray(i, i + n), this._pos); + this._pos += n; + i += n; + if (this._pos >= this._buf.length) { + this._processFrame(); + this._pos = 0; + } + } + return true; + } + _processFrame() { + let sum = 0; + for (let i = 0; i < this._buf.length; i++) sum += this._buf[i] * this._buf[i]; + const rms = Math.sqrt(sum / this._buf.length); + this._lastRms = rms; + + if (rms > this._vadThreshold) { + this._speaking = true; + this._silenceFrames = 0; + } else { + this._silenceFrames++; + if (this._silenceFrames > this._hangover) this._speaking = false; + } + + this.port.postMessage({ + type: 'frame', + samples: this._buf.slice(), + speaking: this._speaking, + rms: rms + }); + } +} +registerProcessor('voice-capture', VoiceCaptureProcessor); +`; + + const voicePlaybackWorkletCode = ` +class VoicePlaybackProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.speakers = new Map(); // id → { ringBuf, readPos, writePos, ready, gain, pan, lastSample, active } + this.RING_SIZE = 48000; // 3 seconds @ 16kHz + this.port.onmessage = (e) => { + const d = e.data; + if (d.type === 'addSpeaker') { + this.speakers.set(d.id, { + ringBuf: new Float32Array(this.RING_SIZE), + readPos: 0, writePos: 0, ready: 0, + gain: d.gain || 1, pan: d.pan || 0, + lastSample: 0, active: false, + jbufTarget: 320 * 4, // ~80ms in samples + fadeOut: 0 + }); + } else if (d.type === 'removeSpeaker') { + this.speakers.delete(d.id); + } else if (d.type === 'pushFrames') { + const sp = this.speakers.get(d.id); + if (!sp) return; + const frames = d.samples; // Float32Array + for (let i = 0; i < frames.length; i++) { + sp.ringBuf[sp.writePos] = frames[i]; + sp.writePos = (sp.writePos + 1) % this.RING_SIZE; + if (sp.ready < this.RING_SIZE) sp.ready++; + } + sp.active = true; + sp.fadeOut = 0; + } else if (d.type === 'updateSpatial') { + const sp = this.speakers.get(d.id); + if (sp) { + sp.gain = d.gain; + sp.pan = d.pan; + } + } + }; + } + process(inputs, outputs) { + const output = outputs[0]; + if (!output || !output[0]) return true; + const left = output[0]; + const right = output[1] || output[0]; // mono fallback + + // Clear output + for (let i = 0; i < left.length; i++) { left[i] = 0; right[i] = 0; } + + for (const [id, sp] of this.speakers) { + if (sp.ready < 1 && sp.active) { + // Fade out over ~20ms (128 samples) + sp.fadeOut++; + if (sp.fadeOut > 128) { sp.active = false; continue; } + const fade = 1 - (sp.fadeOut / 128); + const g = sp.gain * fade; + const lg = g * Math.cos((sp.pan + 1) * Math.PI / 4); + const rg = g * Math.sin((sp.pan + 1) * Math.PI / 4); + // Repeat last sample with fade + for (let i = 0; i < left.length; i++) { + left[i] += sp.lastSample * lg; + right[i] += sp.lastSample * rg; + } + continue; + } + if (!sp.active && sp.ready < sp.jbufTarget) continue; // Wait for jitter buffer to fill + if (!sp.active && sp.ready >= sp.jbufTarget) sp.active = true; + if (!sp.active) continue; + + sp.fadeOut = 0; + const g = sp.gain; + // Constant-power pan law + const lg = g * Math.cos((sp.pan + 1) * Math.PI / 4); + const rg = g * Math.sin((sp.pan + 1) * Math.PI / 4); + + for (let i = 0; i < left.length; i++) { + if (sp.ready > 0) { + const s = sp.ringBuf[sp.readPos]; + sp.lastSample = s; + left[i] += s * lg; + right[i] += s * rg; + sp.readPos = (sp.readPos + 1) % this.RING_SIZE; + sp.ready--; + } else { + // Underrun — fade from last sample + sp.lastSample *= 0.85; + left[i] += sp.lastSample * lg; + right[i] += sp.lastSample * rg; + } + } + } + return true; + } +} +registerProcessor('voice-playback', VoicePlaybackProcessor); +`; + + // ==================== Opus ENCODE/DECODE ==================== + + async function initVoiceEncoder() { + if (false && 'AudioEncoder' in window) { + try { + // Check if Opus is supported + const support = await AudioEncoder.isConfigSupported({ + codec: 'opus', + sampleRate: VC.sampleRate, + numberOfChannels: 1, + bitrate: VC.opusBitrate + }); + if (!support.supported) throw new Error('Opus not supported'); + + voiceEncoder = new AudioEncoder({ + output: (chunk) => { + // chunk: EncodedAudioChunk with Opus data + const data = new Uint8Array(chunk.byteLength); + chunk.copyTo(data); + // Send as binary frame via Socket.IO + console.log('[voice] TX Opus chunk, seq:', voiceSeq, 'size:', data.buffer.byteLength); + voiceSocket.emit('voice_data', { + codec: 'opus', + data: data.buffer, + seq: voiceSeq++, + ts: chunk.timestamp, + speaking: true + }); + }, + error: (e) => { + console.error('[voice] encoder error:', e); + voiceCodec = 'pcm'; + voiceEncoder = null; + } + }); + + await voiceEncoder.configure({ + codec: 'opus', + sampleRate: VC.sampleRate, + numberOfChannels: 1, + bitrate: VC.opusBitrate + }); + + voiceCodec = 'opus'; + console.log('[voice] WebCodecs Opus encoder initialized @', VC.opusBitrate, 'bps'); + return; + } catch (e) { + console.warn('[voice] WebCodecs Opus not available, PCM fallback:', e.message); + } + } + voiceCodec = 'pcm'; + console.log('[voice] Using PCM @', VC.sampleRate, 'Hz'); + } + + async function initDecoderForSpeaker(speakerId, codec) { + const sp = remoteSpeakers.get(speakerId); + if (!sp) return; + + if (codec === 'opus' && 'AudioDecoder' in window) { + try { + const decoder = new AudioDecoder({ + output: (audioData) => { + const samples = new Float32Array(audioData.numberOfFrames); + audioData.copyTo(samples, { planeIndex: 0 }); + audioData.close(); + // Push decoded PCM to worklet + if (playbackNode) { + playbackNode.port.postMessage({ + type: 'pushFrames', + id: speakerId, + samples: samples + }); + } + }, + error: (e) => { + console.error('[voice] decoder error for', speakerId, e); + sp.codec = 'pcm'; // Fallback to PCM + } + }); + + await decoder.configure({ + codec: 'opus', + sampleRate: VC.sampleRate, + numberOfChannels: 1 + }); + + sp.decoder = decoder; + sp.codec = 'opus'; + console.log('[voice] Opus decoder initialized for', speakerId); + } catch (e) { + console.warn('[voice] Opus decoder failed, PCM fallback:', e.message); + sp.codec = 'pcm'; + } + } else { + sp.codec = 'pcm'; + } + } + + // ==================== RemoteSpeaker helper ==================== + + function createRemoteSpeaker(socketId, name, codec) { + const sp = { + id: socketId, + name: name || '???', + codec: codec || 'pcm', + decoder: null, + x: 0, y: 0, + mode: 'near', + speaking: false, + lastActive: Date.now(), + // Audio nodes per speaker (managed on main thread for smooth ramps) + gainNode: audioCtx.createGain(), + pannerNode: audioCtx.createStereoPanner(), + lowpassNode: audioCtx.createBiquadFilter(), + }; + // lowpass for distance muffling + sp.lowpassNode.type = 'lowpass'; + sp.lowpassNode.frequency.value = 4000; + sp.lowpassNode.Q.value = 0.7; + // Connect: lowpass → panner → gain → (connected to mixer later) + sp.lowpassNode.connect(sp.pannerNode); + sp.pannerNode.connect(sp.gainNode); + sp.gainNode.gain.value = 1.0; // Start with volume (worklet needs non-zero gain) + + remoteSpeakers.set(socketId, sp); + // Register speaker in playback worklet + if (playbackNode) { + playbackNode.port.postMessage({ type: 'addSpeaker', id: socketId, gain: 1.0, pan: 0 }); + console.log('[voice] addSpeaker posted to worklet, id:', socketId?.substring(0,8), 'gain:', sp.gainNode.gain.value); + } + initDecoderForSpeaker(socketId, codec); + return sp; + } + + function removeRemoteSpeaker(socketId) { + const sp = remoteSpeakers.get(socketId); + if (!sp) return; + try { sp.gainNode.disconnect(); } catch(e) {} + try { sp.pannerNode.disconnect(); } catch(e) {} + try { sp.lowpassNode.disconnect(); } catch(e) {} + if (sp.decoder) { try { sp.decoder.close(); } catch(e) {} } + remoteSpeakers.delete(socketId); + if (playbackNode) { + playbackNode.port.postMessage({ type: 'removeSpeaker', id: socketId }); + } + } + + function updateSpeakerSpatial(sp) { + if (!audioCtx) return; + // Calculate distance-based audio + const dx = sp.x - player.x; + const dy = sp.y - player.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + const now = audioCtx.currentTime; + const rampTime = 0.05; // 50ms smooth ramp + + if (sp.mode === 'world' && voiceMode === 'world') { + // World mode: full volume, no panning, no distance filter + sp.gainNode.gain.cancelScheduledValues(now); + sp.gainNode.gain.linearRampToValueAtTime(1.0, now + rampTime); + sp.pannerNode.pan.cancelScheduledValues(now); + sp.pannerNode.pan.linearRampToValueAtTime(0, now + rampTime); + sp.lowpassNode.frequency.cancelScheduledValues(now); + sp.lowpassNode.frequency.linearRampToValueAtTime(4000, now + rampTime); + } else if (dist > VC.voiceRadius) { + // Out of range + sp.gainNode.gain.cancelScheduledValues(now); + sp.gainNode.gain.linearRampToValueAtTime(0, now + rampTime); + } else { + // Near mode with spatial audio + const volume = 1 / (1 + dist / 150); // Perceptual falloff + const pan = Math.max(-1, Math.min(1, dx / 300)); // Stereo pan based on X + const cutoff = 4000 - (3800 * (dist / VC.voiceRadius)); // Muffle at distance + + sp.gainNode.gain.cancelScheduledValues(now); + sp.gainNode.gain.linearRampToValueAtTime(volume, now + rampTime); + sp.pannerNode.pan.cancelScheduledValues(now); + sp.pannerNode.pan.linearRampToValueAtTime(pan, now + rampTime); + sp.lowpassNode.frequency.cancelScheduledValues(now); + sp.lowpassNode.frequency.linearRampToValueAtTime(Math.max(200, cutoff), now + rampTime); + } + + // Update worklet spatial params + if (playbackNode) { + const currentGain = sp.gainNode.gain.value; + const currentPan = sp.pannerNode.pan.value; + if (currentGain > 0.01) console.log('[voice] updateSpatial, id:', sp.id?.substring(0,8), 'gain:', currentGain.toFixed(3), 'pan:', currentPan.toFixed(3)); + playbackNode.port.postMessage({ + type: 'updateSpatial', + id: sp.id, + gain: currentGain, + pan: currentPan + }); + } + } + + // ==================== UI ==================== + + const voiceBtn = document.createElement('div'); + voiceBtn.innerHTML = '🎤/'; + voiceBtn.title = 'Голосовой чат (выкл)'; + voiceBtn.style.cssText = 'position:absolute;top:74px;right:130px;width:52px;height:52px;border-radius:12px;background:#555;z-index:200;display:flex;align-items:center;justify-content:center;font-size:24px;cursor:pointer;border:2px solid rgba(255,255,255,0.9);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;'; + document.querySelector('.ui').appendChild(voiceBtn); + + const voiceModeBtn = document.createElement('div'); + voiceModeBtn.innerHTML = '📢'; + voiceModeBtn.title = 'Режим: рядом (600px)'; + voiceModeBtn.style.cssText = 'position:absolute;top:74px;right:190px;width:48px;height:52px;border-radius:12px;background:#3498db;z-index:200;display:flex;align-items:center;justify-content:center;font-size:20px;cursor:pointer;border:2px solid rgba(255,255,255,0.7);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;color:#fff;font-weight:bold;'; + document.querySelector('.ui').appendChild(voiceModeBtn); + voiceModeBtn.onclick = () => { + if (voiceMode === 'near') { + voiceMode = 'world'; + voiceModeBtn.innerHTML = '🌍'; + voiceModeBtn.title = 'Режим: весь мир'; + voiceModeBtn.style.background = '#e67e22'; + } else { + voiceMode = 'near'; + voiceModeBtn.innerHTML = '📢'; + voiceModeBtn.title = 'Режим: рядом (600px)'; + voiceModeBtn.style.background = '#3498db'; + } + if (voiceSocket && voiceSocket.connected) { + voiceSocket.emit('voice_mode', { mode: voiceMode }); + } + // Update spatial for all speakers + for (const [id, sp] of remoteSpeakers) updateSpeakerSpatial(sp); + }; + + // Speaking indicators per players + const speakingIndicator = document.createElement('div'); + speakingIndicator.style.cssText = 'position:absolute;top:130px;right:10px;z-index:200;display:none;background:rgba(0,0,0,0.7);color:#2ecc71;padding:4px 8px;border-radius:6px;font-size:12px;max-width:200px;line-height:1.4;'; + document.querySelector('.ui').appendChild(speakingIndicator); + let speakingTimeouts = new Map(); // socketId → timeout + + // Codec indicator + const codecIndicator = document.createElement('div'); + codecIndicator.style.cssText = 'position:absolute;top:180px;right:10px;z-index:200;font-size:10px;color:rgba(255,255,255,0.5);pointer-events:none;'; + codecIndicator.textContent = ''; + document.querySelector('.ui').appendChild(codecIndicator); + + // ==================== VOICE ON/OFF ==================== + + voiceBtn.onclick = async () => { + if (voiceActive) { + // === DISABLE === + voiceActive = false; + voiceBtn.innerHTML = '🎤/'; + voiceBtn.style.background = '#555'; + codecIndicator.textContent = ''; + + // Clean up capture + if (captureNode) { captureNode.disconnect(); captureNode = null; } + if (voiceStream) { voiceStream.getTracks().forEach(t => t.stop()); voiceStream = null; } + + // Clean up playback + if (playbackNode) { playbackNode.disconnect(); playbackNode = null; } + + // Clean up per-speaker nodes + for (const [id, sp] of remoteSpeakers) { + try { sp.gainNode.disconnect(); } catch(e) {} + try { sp.pannerNode.disconnect(); } catch(e) {} + try { sp.lowpassNode.disconnect(); } catch(e) {} + if (sp.decoder) { try { sp.decoder.close(); } catch(e) {} } + } + remoteSpeakers.clear(); + + // Clean up encoder + if (voiceEncoder) { try { voiceEncoder.close(); } catch(e) {} voiceEncoder = null; } + + if (audioCtx) { audioCtx.close(); audioCtx = null; } + if (voiceSocket) { voiceSocket.disconnect(); voiceSocket = null; } + speakingIndicator.style.display = 'none'; + return; + } + + // === ENABLE === + try { + voiceStream = await navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + echoCancellationType: 'browser', + noiseSuppressionType: 'browser', + sampleRate: { ideal: VC.sampleRate }, + channelCount: { ideal: 1 } + } + }); + + audioCtx = new AudioContext({ sampleRate: VC.sampleRate }); + if (audioCtx.state === 'suspended') await audioCtx.resume(); + console.log('[voice] AudioContext:', audioCtx.state, 'sampleRate:', audioCtx.sampleRate); + + // Register worklets via Blob URLs + const captureURL = URL.createObjectURL(new Blob([voiceCaptureWorkletCode], { type: 'application/javascript' })); + const playbackURL = URL.createObjectURL(new Blob([voicePlaybackWorkletCode], { type: 'application/javascript' })); + + await audioCtx.audioWorklet.addModule(captureURL); + await audioCtx.audioWorklet.addModule(playbackURL); + URL.revokeObjectURL(captureURL); + URL.revokeObjectURL(playbackURL); + console.log('[voice] AudioWorklets registered'); + + // Connect mic → capture worklet + const micSource = audioCtx.createMediaStreamSource(voiceStream); + captureNode = new AudioWorkletNode(audioCtx, 'voice-capture'); + micSource.connect(captureNode); + // Do NOT connect captureNode to destination — we don't hear ourselves + + // Connect playback worklet → per-speaker gain nodes → destination + playbackNode = new AudioWorkletNode(audioCtx, 'voice-playback', { numberOfOutputs: 1, outputChannelCount: [2] }); + playbackNode.connect(audioCtx.destination); + // Per-speaker nodes will connect between playback worklet output and destination + // Actually: playback worklet mixes internally → stereo output → destination + // Per-speaker Web Audio nodes (lowpass, pan, gain) used for spatial UPDATES only (values sent to worklet via messages) + // The actual mixing happens inside the worklet + + // Connect to voice server FIRST so capture frames have a socket + voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] }); + + // Handle capture frames + voiceSeq = 0; + voiceTimestamp = 0; + wasSpeaking = false; + voiceActive = true; // Enable capture BEFORE onmessage handler + let _frameCount = 0; + captureNode.port.onmessage = (e) => { + const { type, samples, speaking, rms } = e.data; + if (type !== 'frame') return; + _frameCount++; + if (_frameCount <= 5 || _frameCount % 100 === 0) { + console.log('[voice] capture frame #', _frameCount, 'rms:', rms?.toFixed(4), 'speaking:', speaking, 'voiceActive:', voiceActive, 'socket:', !!voiceSocket, 'connected:', voiceSocket?.connected, 'codec:', voiceCodec); + } + if (!voiceActive || !voiceSocket || !voiceSocket.connected) { + + return; + } + + voiceTimestamp += VC.samplesPerFrame; // 320 samples * (1/16000) = 20ms per frame + + if (!speaking && !wasSpeaking) { + // Complete silence — don't transmit + silenceFrames++; + return; + } + + if (voiceCodec === 'opus' && voiceEncoder) { + // Encode with Opus via WebCodecs + try { + const audioData = new AudioData({ + format: 'float32-planar', + sampleRate: VC.sampleRate, + numberOfFrames: samples.length, + numberOfChannels: 1, + timestamp: voiceTimestamp * (1000000 / VC.sampleRate), // microseconds + data: samples + }); + voiceEncoder.encode(audioData); + audioData.close(); + } catch (err) { + // Fallback: send as PCM + sendPCMFrame(samples, speaking, wasSpeaking); + } + } else { + sendPCMFrame(samples, speaking, wasSpeaking); + } + + wasSpeaking = speaking; + silenceFrames = 0; + }; + + function sendPCMFrame(samples, isSpeaking, wasSp) { + const int16 = new Int16Array(samples.length); + for (let i = 0; i < samples.length; i++) { + const s = Math.max(-1, Math.min(1, samples[i])); + int16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; + } + voiceSocket.emit('voice_data', { + codec: 'pcm', + data: int16.buffer, + seq: voiceSeq++, + ts: performance.now(), + speaking: isSpeaking || wasSp + }); + } + + // Initialize encoder + await initVoiceEncoder(); + codecIndicator.textContent = voiceCodec === 'opus' ? '🔊 Opus' : '🔊 PCM'; + + // Voice socket already created above + + voiceSocket.on('connect', () => { + console.log('[voice] Connected, id:', voiceSocket.id, 'codec:', voiceCodec); + voiceSocket.emit('voice_join', { + world_id: worldId, + x: player.x, y: player.y, + name: playerName || 'Игрок', + mode: voiceMode, + codec: voiceCodec + }); + }); + + voiceSocket.on('connect_error', (err) => { + console.error('[voice] Connect error:', err.message); + }); + + voiceSocket.on('voice_in', (payload) => { + console.log('[voice] RX voice_in, codec:', payload.codec, 'dataSize:', payload.data?.byteLength || payload.data?.length || 'N/A', 'from:', payload.meta?.from?.substring(0,8)); + const { data, meta } = payload; + if (!audioCtx || audioCtx.state === 'closed') { console.warn('[voice] audioCtx missing/closed'); return; } + + let sp = remoteSpeakers.get(meta.from); + if (!sp) { + // New speaker — create per-speaker audio nodes + sp = createRemoteSpeaker(meta.from, meta.name, meta.codec || 'pcm'); + } + + // Update position + sp.x = meta.x || 0; + sp.y = meta.y || 0; + sp.mode = meta.mode || 'near'; + sp.lastActive = Date.now(); + updateSpeakerSpatial(sp); + + // Decode and push to worklet + if ((meta.codec || 'pcm') === 'opus' && sp.decoder) { + // Opus decode + try { + const uint8 = new Uint8Array(data); + const chunk = new EncodedAudioChunk({ + type: 'key', // Opus frames are self-decodable + timestamp: performance.now() * 1000, // microseconds + data: uint8 + }); + sp.decoder.decode(chunk); + } catch (err) { + // Opus decode failed, try PCM fallback + decodeAndPushPCM(sp.id, data); + } + } else { + decodeAndPushPCM(sp.id, data); + } + + // Update speaking indicator + sp.speaking = true; + speakingIndicator.style.display = 'block'; + speakingIndicator.textContent = '🔊 ' + sp.name; + clearTimeout(speakingTimeouts.get(meta.from)); + speakingTimeouts.set(meta.from, setTimeout(() => { + sp.speaking = false; + // Check if anyone is still speaking + let anyoneSpeaking = false; + for (const [, s] of remoteSpeakers) { if (s.speaking) { anyoneSpeaking = true; break; } } + if (!anyoneSpeaking) speakingIndicator.style.display = 'none'; + speakingTimeouts.delete(meta.from); + }, 1500)); + }); + + voiceSocket.on('voice_leave', (data) => { + removeRemoteSpeaker(data.id); + }); + + voiceSocket.on('disconnect', () => { + console.log('[voice] Disconnected'); + // Clear all speakers on disconnect + for (const [id] of remoteSpeakers) removeRemoteSpeaker(id); + }); + + // voiceActive already set to true above + voiceBtn.textContent = '🎤'; + voiceBtn.style.background = '#2ecc71'; + console.log('[voice] Voice chat ACTIVE, codec:', voiceCodec); + + } catch(e) { + console.error('[voice] Error:', e); + voiceBtn.style.background = '#e74c3c'; + } + }; + + // ==================== PCM decode helper ==================== + + function decodeAndPushPCM(speakerId, buffer) { + console.log('[voice] decodeAndPushPCM, id:', speakerId?.substring(0,8), 'bufSize:', buffer?.byteLength || buffer?.length || 'N/A'); + const int16 = new Int16Array(buffer); + const float32 = new Float32Array(int16.length); + for (let i = 0; i < int16.length; i++) { + float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF); + } + if (playbackNode) { + playbackNode.port.postMessage({ + type: 'pushFrames', + id: speakerId, + samples: float32 + }); + } + } + + // ==================== Voice position update ==================== + + let voicePosT = 0; + + // Клик на часы для включения ночи + todEl.style.cursor = 'pointer'; + todEl.onclick = () => { + playSound('click'); + worldTime = 0.6; // Устанавливаем ночь + isNightTime = true; + }; + + function rebuildHotbar(){ + hotbarEl.innerHTML=''; + + // Показываем последние 5 выбранных предметов (если они есть в инвентаре) + const items = recentItems.filter(id => inv[id] > 0).slice(0, 5); + + for(const id of items){ + const s = document.createElement('div'); + s.className = 'slot'+(id===selected?' sel':''); + if(BLOCKS[id]) { + s.style.backgroundImage = `url(${tex[id].toDataURL()})`; + s.style.backgroundSize = 'cover'; + } else if(ITEMS[id]) { + s.textContent = ITEMS[id].icon; + } else if(TOOLS[id]) { + s.textContent = TOOLS[id].icon; + } else if(id === 'iron_armor') { + s.textContent = '🛡️'; + s.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)'; + } + const c = document.createElement('div'); + c.className='count'; + c.textContent = inv[id]; + s.appendChild(c); + s.onclick = () => { + playSound('click'); // Звук клика по инвентарю + if(selected === id) { + // Повторный клик — снимаем выбор, возвращаем к первому блоку + selected = recentItems.find(rid => BLOCKS[rid] && rid !== id) || recentItems[0] || 'dirt'; + } else { + selected = id; + // Обновляем список последних предметов + recentItems = recentItems.filter(item => item !== id); // Удаляем если уже есть + recentItems.unshift(id); // Добавляем в начало + recentItems = recentItems.slice(0, 5); // Оставляем только 5 + } + rebuildHotbar(); + }; + + // Показываем индикатор надетой брони + if(id === 'iron_armor' && player.equippedArmor === 'iron_armor') { + const equipped = document.createElement('div'); + equipped.className = 'equipped-indicator'; + equipped.textContent = '✓'; + s.appendChild(equipped); + } + + // Durability bar для инструментов + if(TOOLS[id] && inv[id] > 0) { + // Находим текущую прочность + let curDur = 0, maxDur = TOOLS[id].durability; + for (const [tid, dur] of toolDurability) { + if (dur.type === id) { + curDur = dur.current; + maxDur = dur.max; + break; + } + } + if (maxDur > 0) { + const bar = document.createElement('div'); + bar.style.cssText = `position:absolute;bottom:0;left:0;right:0;height:3px;background:#333;border-radius:0 0 6px 6px;overflow:hidden;`; + const fill = document.createElement('div'); + const pct = curDur / maxDur; + const color = pct > 0.5 ? '#2ecc71' : pct > 0.25 ? '#f39c12' : '#e74c3c'; + fill.style.cssText = `width:${pct*100}%;height:100%;background:${color};`; + bar.appendChild(fill); + s.appendChild(bar); + } + } + hotbarEl.appendChild(s); + } + } + + function renderInventory() { + inventoryGrid.innerHTML = ''; + + // Создаём сетку инвентаря 7x3 + const items = Object.keys(inv).filter(id => inv[id] > 0); + + // Добавляем пустые слоты для полной сетки + for(let i = 0; i < 21; i++) { + const slot = document.createElement('div'); + slot.className = 'inv-slot' + (i < items.length && items[i] === selected ? ' sel' : ''); + + if(i < items.length) { + const id = items[i]; + if(BLOCKS[id]) { + slot.style.backgroundImage = `url(${tex[id].toDataURL()})`; + slot.style.backgroundSize = 'cover'; + } else if(ITEMS[id]) { + slot.textContent = ITEMS[id].icon; + } else if(TOOLS[id]) { + slot.textContent = TOOLS[id].icon; + } else if(id === 'iron_armor') { + slot.textContent = '🛡️'; + slot.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)'; + } + + const count = document.createElement('div'); + count.className = 'inv-count'; + count.textContent = inv[id]; + slot.appendChild(count); + + slot.onclick = () => { + playSound('click'); // Звук клика по инвентарю + if(selected === id) { + selected = recentItems.find(rid => BLOCKS[rid] && rid !== id) || recentItems[0] || 'dirt'; + } else { + selected = id; + recentItems = recentItems.filter(item => item !== id); + recentItems.unshift(id); + recentItems = recentItems.slice(0, 5); + } + rebuildHotbar(); + renderInventory(); + }; + + // Двойной клик для надевания брони + slot.ondblclick = () => { + if(id === 'iron_armor' && inv.iron_armor > 0) { + // Если уже надета броня - снимаем её + if(player.equippedArmor === 'iron_armor') { + player.equippedArmor = null; + player.armor = 0; + console.log('[ARMOR] Iron armor unequipped'); + } else { + // Надеваем броню + player.equippedArmor = 'iron_armor'; + player.armor = BLOCKS['iron_armor'].armor; + console.log('[ARMOR] Iron armor equipped - armor:', player.armor); + } + playSound('click'); + renderInventory(); + } + if(id === 'gold_armor' && inv.gold_armor > 0) { + if(player.equippedArmor === 'gold_armor') { + player.equippedArmor = null; + player.armor = 0; + } else { + player.equippedArmor = 'gold_armor'; + player.armor = ITEMS['gold_armor'].armor; + } + playSound('click'); + renderInventory(); + } + }; + } + + inventoryGrid.appendChild(slot); + } + } + + function canCraft(r){ + console.log('[CRAFT] Checking recipe for:', r.out, '- cost:', r.cost); + for(const res in r.cost){ + const have = inv[res] || 0; + const need = r.cost[res]; + console.log('[CRAFT] Resource:', res, '- have:', have, '- need:', need, '- can craft:', have >= need); + if(have < need) return false; + } + return true; + } + function renderCraft(){ + recipesEl.innerHTML=''; + for(const r of RECIPES){ + const row = document.createElement('div'); + row.className='recipe'; + const reqLv = r.requiredLevel || 1; + const locked = player.level < reqLv; + const icon = document.createElement('div'); + icon.className='ricon'; + // Иконка — блок, инструмент или предмет + if(tex[r.out]){ + icon.style.backgroundImage = `url(${tex[r.out].toDataURL()})`; + } else if(TOOLS[r.out]){ + icon.textContent = TOOLS[r.out].icon; + icon.style.fontSize = '24px'; + icon.style.display = 'flex'; + icon.style.alignItems = 'center'; + icon.style.justifyContent = 'center'; + } else if(ITEMS[r.out]){ + icon.textContent = ITEMS[r.out].icon; + icon.style.fontSize = '24px'; + icon.style.display = 'flex'; + icon.style.alignItems = 'center'; + icon.style.justifyContent = 'center'; + } + const info = document.createElement('div'); + info.className='rinfo'; + const nm = document.createElement('div'); + nm.className='rname'; + const itemName = BLOCKS[r.out]?.n || TOOLS[r.out]?.n || ITEMS[r.out]?.n || r.out; + nm.textContent = `${itemName} x${r.qty}` + (locked ? ` (Lv.${reqLv})` : ''); + if(locked) { nm.style.color = '#888'; nm.style.textDecoration = 'line-through'; } + const cs = document.createElement('div'); + cs.className='rcost'; + cs.textContent = Object.keys(r.cost).map(x => { + const cn = BLOCKS[x]?.n || TOOLS[x]?.n || ITEMS[x]?.n || x; + return `${cn}: ${(inv[x]||0)}/${r.cost[x]}`; + }).join(' '); + if(locked) cs.style.color = '#666'; + info.appendChild(nm); info.appendChild(cs); + const btn = document.createElement('button'); + btn.className='rcraft'; + btn.textContent='Создать'; + btn.disabled = !canCraft(r) || locked; + if(locked) btn.title = `Требуется уровень ${reqLv}`; + btn.onclick = () => { + if(!canCraft(r) || locked) return; + playSound('click'); + for(const res in r.cost) inv[res]-=r.cost[res]; + inv[r.out] = (inv[r.out]||0) + r.qty; + if(TOOLS[r.out]) addTool(r.out); + rebuildHotbar(); + renderCraft(); + }; + row.appendChild(icon); row.appendChild(info); row.appendChild(btn); + recipesEl.appendChild(row); + } + } + + let craftOpen=false; + let inventoryOpen = false; + + document.getElementById('craftBtn').onclick = () => { + playSound('click'); // Звук клика по кнопке + craftOpen = !craftOpen; + craftPanel.style.display = craftOpen ? 'block' : 'none'; + if(craftOpen) { + renderCraft(); + // Закрываем инвентарь если открыт крафт + inventoryOpen = false; + inventoryPanel.style.display = 'none'; + } + }; + document.getElementById('craftClose').onclick = () => { + playSound('click'); // Звук клика по кнопке + craftOpen = false; + craftPanel.style.display = 'none'; + }; + + // Кнопка открытия инвентаря + document.getElementById('invToggle').onclick = () => { + playSound('click'); // Звук клика по кнопке + inventoryOpen = !inventoryOpen; + inventoryPanel.style.display = inventoryOpen ? 'block' : 'none'; + if(inventoryOpen) { + renderInventory(); + // Закрываем крафт если открыт инвентарь + craftOpen = false; + craftPanel.style.display = 'none'; + } + }; + + document.getElementById('inventoryClose').onclick = () => { + playSound('click'); // Звук клика по кнопке + inventoryOpen = false; + inventoryPanel.style.display = 'none'; + }; + // Меню ⋯ (Сохранить / Новый мир / Рынок) + let dropdownOpen = false; + const menuBtn = document.getElementById('menuBtn'); + const menuDropdown = document.getElementById('menuDropdown'); + const saveItem = document.getElementById('saveItem'); + const resetItem = document.getElementById('resetItem'); + const marketItem = document.getElementById('marketItem'); + + // Убить старый onclick (предотвращает race condition) + menuBtn.onclick = null; + + // Toggle dropdown + menuBtn.addEventListener('click', (e) => { + e.stopPropagation(); + dropdownOpen = !dropdownOpen; + menuDropdown.classList.toggle('open', dropdownOpen); + }); + + // Закрытие по клику вне + document.addEventListener('click', () => { + if (dropdownOpen) { + dropdownOpen = false; + menuDropdown.classList.remove('open'); + } + }); + + // Предотвращаем закрытие при клике внутри дропдауна + menuDropdown.addEventListener('click', (e) => { + e.stopPropagation(); + }); + + // 💾 Сохранить + saveItem.addEventListener('click', () => { + playSound('click'); + saveGame(); + dropdownOpen = false; + menuDropdown.classList.remove('open'); + customAlert('Игра сохранена!'); + }); + + // 🔄 Новый мир + resetItem.addEventListener('click', () => { + dropdownOpen = false; + menuDropdown.classList.remove('open'); + customConfirm('Вы уверены, что хотите удалить сохранение и начать новую игру?', () => { + playSound('click'); + + try { + localStorage.removeItem(SAVE_KEY); + console.log('Сохранение удалено из localStorage'); + } catch (e) { + console.warn('Ошибка удаления сохранения:', e); + } + + inMemorySave = null; + worldId = Math.random().toString(36).substring(2, 10); + console.log('Новый worldId после сброса:', worldId); + + try { + const newUrl = new URL(window.location.href); + newUrl.searchParams.set('world', worldId); + if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') { + window.history.replaceState(null, '', newUrl.toString()); + console.log('URL обновлён:', newUrl.toString()); + } + } catch (e) { + console.error('Ошибка обновления URL:', e); + } + + location.reload(); + }); + }); + + // 🏪 Рынок + marketItem.addEventListener('click', () => { + dropdownOpen = false; + menuDropdown.classList.remove('open'); + toggleMarket(); + }); + + // Показываем пункт сохранения только если играем одни + function updateSaveButtonVisibility() { + if (isMultiplayer && otherPlayers.size > 0) { + saveItem.style.display = 'none'; + } else { + saveItem.style.display = 'block'; + } + } + + // ==================== РЫНОК ПРЕДЛОЖЕНИЙ ==================== + const marketPanel = document.getElementById('marketPanel'); + const marketClose = document.getElementById('marketClose'); + const marketContent = document.getElementById('marketContent'); + let marketOpen = false; + + function toggleMarket() { + marketOpen = !marketOpen; + marketPanel.style.display = marketOpen ? 'block' : 'none'; + if (marketOpen) renderMarket(); + } + + function closeMarket() { + marketOpen = false; + marketPanel.style.display = 'none'; + } + + function renderMarket() { + if (!socket) { + marketContent.innerHTML = '

Подключение...

'; + return; + } + // Запрос ордеров с сервера + socket.emit('market_list', {}, (orders) => { + if (!orders || !orders.length) { + marketContent.innerHTML = '

Предложений пока нет

' + + '
'; + const btn = document.getElementById('createOrderBtn'); + if (btn) btn.onclick = showCreateOrder; + return; + } + let html = '
'; + html += orders.map(o => '
' + o.offer_item + ' ×' + o.offer_qty + ' ⇄ ' + o.want_item + ' ×' + o.want_qty + '' + (o.seller||'Аноним') + '
').join(''); + marketContent.innerHTML = html; + const btn = document.getElementById('createOrderBtn'); + if (btn) btn.onclick = showCreateOrder; + }); + } + + function showCreateOrder() { + const inv = playerInventory || []; + const items = inv.filter(s => s && s.id); + marketContent.innerHTML = '

Выставить предмет

' + + '' + + '' + + '' + + '' + + '
' + + '' + + '' + + '
'; + document.getElementById('cancelOrder').onclick = () => renderMarket(); + document.getElementById('submitOrder').onclick = () => { + const offerItem = document.getElementById('offerItem').value; + const wantItem = document.getElementById('wantItem').value.trim(); + if (!offerItem || !wantItem) { customAlert('Выберите предмет и укажите что хотите'); return; } + if (socket) { socket.emit('market_create', { offer_item: offerItem, offer_qty: 1, want_item: wantItem, want_qty: 1 }); playSound('click'); setTimeout(renderMarket, 500); } + }; + } + + marketClose.onclick = () => closeMarket(); + +// Режимы + const MODES = [{id:'mine',icon:'⛏️'},{id:'build',icon:'🧱'}]; + let modeIdx=0; + const modeBtn = document.getElementById('modeBtn'); + function mode(){ return MODES[modeIdx].id; } + modeBtn.onclick = () => { + playSound('click'); // Звук клика по кнопке режима + modeIdx=(modeIdx+1)%MODES.length; modeBtn.textContent=MODES[modeIdx].icon; + }; + + // День/ночь (автоматический цикл) + let isNightTime = false; + + // Управление + const inp = { l:false, r:false, j:false, s:false }; + function bindHold(el, key){ + const down=(e)=>{ e.preventDefault(); inp[key]=true; }; + const up=(e)=>{ e.preventDefault(); inp[key]=false; }; + el.addEventListener('pointerdown', down); + el.addEventListener('pointerup', up); + el.addEventListener('pointerleave', up); + } + const leftBtn = document.getElementById('left'); + const rightBtn = document.getElementById('right'); + const jumpBtn = document.getElementById('jump'); + const downBtn = document.getElementById('down'); + + if(leftBtn) bindHold(leftBtn,'l'); + if(rightBtn) bindHold(rightBtn,'r'); + if(jumpBtn) bindHold(jumpBtn,'j'); + if(downBtn) bindHold(downBtn,'s'); + + window.addEventListener('keydown', (e)=>{ + if(e.code==='KeyA'||e.code==='ArrowLeft') inp.l=true; + if(e.code==='KeyD'||e.code==='ArrowRight') inp.r=true; + if(e.code==='Space'||e.code==='KeyW'||e.code==='ArrowUp') inp.j=true; + if(e.code==='KeyS'||e.code==='ArrowDown') inp.s=true; + }); + window.addEventListener('keyup', (e)=>{ + if(e.code==='KeyA'||e.code==='ArrowLeft') inp.l=false; + if(e.code==='KeyD'||e.code==='ArrowRight') inp.r=false; + if(e.code==='Space'||e.code==='KeyW'||e.code==='ArrowUp') inp.j=false; + if(e.code==='KeyS'||e.code==='ArrowDown') inp.s=false; + }); + + // Лодка + const boat = { + x: 0, y: 0, + w: 34, h: 34, + vx: 0, vy: 0, + active: false, + inWater: false + }; + + // Функция для расчёта урона с учётом брони + function calculateDamage(baseDamage) { + // Броня снижает урон пропорционально + // armor: 0 = без брони (100% урона) + // armor: 0.5 = железная броня (50% урона) + const reduction = player.armor; + const actualDamage = baseDamage * (1 - reduction); + console.log('[DAMAGE] Base:', baseDamage, '- Armor:', reduction, '- Actual:', actualDamage.toFixed(1)); + return actualDamage; + } + + // Игрок + const player = { + x: 6*TILE, y: 0*TILE, + w: 34, h: 34, + vx: 0, vy: 0, + grounded: false, + inWater: false, + headInWater: false, + hp: 100, + hunger: 100, + o2: 100, + invuln: 0, + slowTimer: 0, // яд скорпиона — замедление + fallStartY: 0, + lastStepTime: 0, + sleeping: false, + inBoat: false, + armor: 0, // Текущий уровень защиты (0 = без брони, 0.5 = железная броня) + equippedArmor: null, // Тип надетой брони + xp: 0, + level: 1 + }; + + // Сохраняем начальную позицию для возрождения + const spawnPoint = { x: 6*TILE, y: 0*TILE }; + + // Система дропов с мобов + const drops = []; // {x, y, vy, item, qty, age} + let levelUpPopup = null; // {text, timer} + + function xpForLevel(lv) { + if (lv <= 1) return 0; + const thresholds = [0, 50, 150, 300, 500, 800, 1200, 1700, 2300, 3000]; + if (lv - 1 < thresholds.length) return thresholds[lv - 1]; + return Math.floor(3000 + (lv - 10) * (lv - 10) * 50 + (lv - 10) * 200); + } + + function getMobLoot(kind) { + const table = { + chicken: [{item:'chicken_meat',min:1,max:2,chance:1},{item:'feather',min:0,max:1,chance:0.5}], + pig: [{item:'meat',min:1,max:2,chance:1}], + zombie: [{item:'meat',min:0,max:1,chance:0.5},{item:'iron_ingot',min:0,max:1,chance:0.3}], + skeleton: [{item:'arrow',min:2,max:4,chance:1},{item:'bone',min:0,max:2,chance:0.6},{item:'bow',min:1,max:1,chance:0.15}], + creeper: [{item:'gunpowder',min:1,max:2,chance:1}], + scorpion: [{item:'scorpion_stinger',min:0,max:1,chance:0.4}], + polar_bear:[{item:'polar_fur',min:1,max:2,chance:0.8},{item:'meat',min:1,max:2,chance:1}], + slime: [{item:'slime_ball',min:1,max:2,chance:1}], + eagle: [{item:'eagle_feather',min:1,max:2,chance:0.7}] + }; + return table[kind] || []; + } + + function getMobXP(kind) { + const xpTable = { chicken:3, pig:5, zombie:10, skeleton:12, creeper:15, scorpion:8, polar_bear:20, slime:5, eagle:15 }; + return xpTable[kind] || 0; + } + + function spawnDrops(mx, my, kind) { + const loot = getMobLoot(kind); + for (const entry of loot) { + if (Math.random() > entry.chance) continue; + const qty = entry.min + Math.floor(Math.random() * (entry.max - entry.min + 1)); + if (qty <= 0) continue; + drops.push({ + x: mx + (Math.random() - 0.5) * 20, + y: my + (Math.random() - 0.5) * 10, + vy: -1 - Math.random() * 2, + item: entry.item, + qty: qty, + age: 0, xpValue: getMobXP(kind) + }); + } + } + + const LEVEL_UNLOCKS = { + 2: 'Каменные инструменты, Лодка', + 3: 'Железные инструменты, Печь', + 4: 'Лук и стрелы', + 5: 'Железная броня', + 6: 'Золотая броня', + 7: 'Алмазные инструменты', + 8: 'TNT' + }; + + function grantXP(amount) { + player.xp += amount; + while (player.xp >= xpForLevel(player.level + 1)) { + player.level++; + const unlock = LEVEL_UNLOCKS[player.level] || ''; + levelUpPopup = { text: '⭐ Уровень ' + player.level + '!' + (unlock ? ' ' + unlock : ''), timer: 240 }; + } + } + + function pickupDrops() { + for (let i = drops.length - 1; i >= 0; i--) { + const d = drops[i]; + const dx = player.x + player.w/2 - d.x; + const dy = player.y + player.h/2 - d.y; + if (dx*dx + dy*dy < 30*30) { + if (!inv[d.item]) inv[d.item] = 0; + inv[d.item] += d.qty; + if (d.xpValue) grantXP(Math.ceil(d.xpValue * 0.3)); + drops.splice(i, 1); + rebuildHotbar(); + } + } + } + + function drawDrops(ctx) { + for (let i = drops.length - 1; i >= 0; i--) { + const d = drops[i]; + d.age++; + if (d.age > 3600) { drops.splice(i, 1); continue; } // 60 sec at 60fps + // Bounce animation + const bounce = Math.abs(Math.sin(d.age * 0.05)) * 6; + const dy = d.y - bounce; + const sx = d.x - camX; + const sy = dy - camY; + // Skip if off screen + if (sx < -40 || sx > W + 40 || sy < -40 || sy > H + 40) continue; + // Glow effect + ctx.save(); + ctx.globalAlpha = 0.3; + ctx.fillStyle = '#ffff00'; + ctx.beginPath(); + ctx.arc(sx, sy, 14, 0, Math.PI*2); + ctx.fill(); + ctx.restore(); + // Item icon + ctx.save(); + ctx.font = '16px system-ui'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + const itemDef = ITEMS[d.item]; + const icon = itemDef ? itemDef.icon : '🎁'; + const label = d.qty > 1 ? icon + '×' + d.qty : icon; + ctx.fillText(label, sx, sy); + ctx.restore(); + } + } + + function drawLevelUpPopup(ctx) { + if (!levelUpPopup) return; + levelUpPopup.timer--; + if (levelUpPopup.timer <= 0) { levelUpPopup = null; return; } + const alpha = Math.min(1, levelUpPopup.timer / 60); + ctx.save(); + ctx.globalAlpha = alpha; + ctx.fillStyle = '#FFD700'; + ctx.font = 'bold 36px system-ui'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.strokeStyle = '#000'; + ctx.lineWidth = 3; + ctx.strokeText(levelUpPopup.text, W/2, H/2 - 60); + ctx.fillText(levelUpPopup.text, W/2, H/2 - 60); + ctx.restore(); + } + + // Система сохранения игры (localStorage + in-memory fallback) + const SAVE_KEY = 'minegrechka_save'; + let db = null; // Оставляем для совместимости, но не используем + let inMemorySave = null; // Запасное сохранение в памяти + + // Инициализация (localStorage + in-memory fallback) + function initDB(){ + return new Promise((resolve) => { + console.log('Используем localStorage для сохранений (sandbox режим)'); + resolve(null); + }); + } + + // Детерминированный генератор псевдослучайных чисел на основе seed + function seededRandom(gx, gy){ + const n = Math.sin(gx * 12.9898 + gy * 78.233 + worldSeed * 0.1) * 43758.5453; + return n - Math.floor(n); + } + + function saveGame(){ + const saveData = { + version: 2, + worldSeed: worldSeed, + player: { + x: player.x, + y: player.y, + hp: player.hp, + hunger: player.hunger, + o2: player.o2, + xp: player.xp, + level: player.level + }, + temperature: player.temperature, + inventory: inv, + time: worldTime, + isNight: isNightTime, + // Сохраняем только изменения + placedBlocks: placedBlocks.slice(), + removedBlocks: removedBlocks.slice() + }; + + const saveSize = JSON.stringify(saveData).length; + console.log('Сохранение: player HP:', player.hp, 'hunger:', player.hunger, 'o2:', player.o2); + + // Пробуем сохранить в localStorage (основной метод для персистентности) + try { + localStorage.setItem(SAVE_KEY, JSON.stringify(saveData)); + console.log(`Игра сохранена в localStorage (размер: ${saveSize} байт)`); + // TG: also save to server + if (tgUser && tgUser.id) { + try { + fetch(SERVER_URL + '/api/tg/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tg_id: tgUser.id, save_data: JSON.stringify(saveData), world_id: worldId, player_name: playerName }) + }).catch(e2 => console.warn('[TG] Save to server failed:', e2)); + } catch(tgE) {} + } + } catch(e){ + console.warn('Ошибка сохранения в localStorage, используем только in-memory:', e); + + // Если localStorage недоступен, используем in-memory fallback + inMemorySave = saveData; + console.log(`Игра сохранена в памяти (размер: ${saveSize} байт)`); + } + } + + function loadGame(){ + return new Promise(async (resolve, reject) => { + // TG: try server save first + if (tgUser && tgUser.id && tgSaveLoaded) { + try { + const resp = await fetch(SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id); + const data = await resp.json(); + if (data.ok && data.save_data) { + const parsed = JSON.parse(data.save_data); + console.log('[TG] Loaded save from server, HP:', parsed.player?.hp); + tgSaveLoaded = parsed; // store for later use + resolve(parsed); + return; + } + } catch(e) { console.warn('[TG] Load from server failed:', e); } + } + // Пробуем localStorage + try { + const localSave = localStorage.getItem(SAVE_KEY); + if(localSave){ + const parsed = JSON.parse(localSave); + console.log('Загружено из localStorage, player HP:', parsed.player?.hp); + resolve(parsed); + return; + } + } catch(e){ + console.warn('Ошибка доступа к localStorage:', e); + } + + // Если localStorage недоступен, используем in-memory сохранение + if(inMemorySave){ + console.log('Загружено из in-memory сохранения, player HP:', inMemorySave.player?.hp); + resolve(inMemorySave); + return; + } + + console.log('Сохранение не найдено'); + resolve(null); + }); + } + + // Миграция с версии 1 на версию 2 + function migrateV1toV2(saveData){ + console.log('Миграция сохранения с версии 1 на версию 2...'); + + // Сохраняем seed из текущей игры (так как v1 его не хранил) + saveData.worldSeed = worldSeed; + + // Инициализируем массивы изменений + saveData.placedBlocks = []; + saveData.removedBlocks = []; + + // Для v1 нам нужно сравнить сгенерированные блоки с тем, что должно быть по seed + // Это сложно сделать без полной перегенерации, поэтому для v1 сохраняем только seed + // и при загрузке просто перегенерируем мир + + // Удаляем старые данные + delete saveData.generatedBlocks; + + saveData.version = 2; + console.log('Миграция завершена'); + } + + async function applySave(saveData){ + if(!saveData) return; + + console.log('=== applySave START ==='); + console.log('player HP before applySave:', player.hp); + console.log('saveData.player.hp:', saveData.player?.hp); + + // Миграция версий + if(saveData.version === 1){ + player.temperature = saveData.player.temperature !== undefined ? saveData.player.temperature : 15; + migrateV1toV2(saveData); + } + + // Восстанавливаем seed + if(saveData.worldSeed !== undefined){ + worldSeed = saveData.worldSeed; + } + + // Восстанавливаем игрока + if(saveData.player){ + player.x = saveData.player.x; + player.y = saveData.player.y; + player.hunger = saveData.player.hunger; + player.o2 = saveData.player.o2; + player.xp = saveData.player.xp || 0; + player.level = saveData.player.level || 1; + + // Обновляем spawnPoint на позицию из сохранения + spawnPoint.x = player.x; + spawnPoint.y = player.y; + + // Проверяем HP из сохранения - если <= 0, устанавливаем 100 + const savedHP = saveData.player.hp; + console.log('Saved HP from file:', savedHP); + if(savedHP <= 0){ + console.log('WARNING: Saved HP is <= 0, setting to 100!'); + player.hp = 100; + } else { + player.hp = savedHP; + } + console.log('player HP after restore:', player.hp); + console.log('spawnPoint обновлён из сохранения: x=', spawnPoint.x, 'y=', spawnPoint.y); + } else { + console.log('No player data in save, setting default HP: 100'); + player.hp = 100; + } + + console.log('=== applySave END ==='); + + // Восстанавливаем инвентарь + if(saveData.inventory){ + for(const key in saveData.inventory){ + inv[key] = saveData.inventory[key]; + } + } + + // Восстанавливаем время + if(saveData.time !== undefined){ + worldTime = saveData.time; + } + + // Восстанавливаем день/ночь + if(saveData.isNight !== undefined){ + isNightTime = saveData.isNight; + } + + // Перегенерируем мир по seed + regenerateVisibleChunks(); + + // Применяем изменения (только для v2) + if(saveData.version === 2){ + // Применяем блоки, установленные игроком + for(const block of saveData.placedBlocks){ + setBlock(block.gx, block.gy, block.t, true); + } + + // Применяем удалённые блоки + for(const block of saveData.removedBlocks){ + removeBlock(block.gx, block.gy); + } + + // Восстанавливаем массивы изменений + placedBlocks = saveData.placedBlocks || []; + removedBlocks = saveData.removedBlocks || []; + } + + rebuildHotbar(); + console.log('Игра загружена'); + } + + // Камера (двухосевая) + let camX=0, camY=0; + + // День/ночь + let worldTime=0; + const DAY_LEN=360; // замедлил смену дня/ночи в 3 раза + + // Облака + const clouds = Array.from({length:10}, ()=>({ + x: Math.random()*2000, + y: -200 - Math.random()*260, // выше экрана, потому что мир по Y теперь двигается + w: 80+Math.random()*120, + s: 12+Math.random()*20 + })); + + // Дождь + let isRaining = false; + let rainIntensity = 0; // 0..1 + const snowflakes = []; // снежинки для тундры + const MAX_SNOWFLAKES = 150; + const raindrops = []; + const MAX_RAINDROPS = 200; + + // ==================== РОСТ КУЛЬТУР ==================== + const growthTimers = {}; // ключ: "gx,gy" → { stage:0-3, growTimer:X } + + // Старая функция заменена — теперь погода через биомы (см. weatherState выше) + // updateWeather(dt) вызывается из основного цикла — биом-зависимая + + // Интеграция: определяем isRaining из weatherState для визуализации + function syncWeatherVisual() { + isRaining = (weatherState.type === 'rain' || weatherState.type === 'storm'); + if (weatherState.type === 'clear') { + rainIntensity *= 0.95; // плавное затухание + if (rainIntensity < 0.01) rainIntensity = 0; + } + } + + function updateRain(dt) { + syncWeatherVisual(); + // Дождь + if ((weatherState.type === 'rain' || weatherState.type === 'storm') && rainIntensity < weatherState.intensity * 0.6) { + rainIntensity += dt * 0.3; + } else if (weatherState.type === 'clear' || weatherState.type === 'snow' || weatherState.type === 'fog') { + rainIntensity = Math.max(0, rainIntensity - dt * 0.5); + } + if (rainIntensity < 0.01) { + raindrops.length = 0; + } else { + const spawnRate = Math.floor(rainIntensity * 80 * dt); + for (let i = 0; i < spawnRate && raindrops.length < MAX_RAINDROPS; i++) { + raindrops.push({ + x: camX + Math.random() * W, + y: camY - 20, + vy: 400 + Math.random() * 200, + len: 8 + Math.random() * 12 + }); + } + for (let i = raindrops.length - 1; i >= 0; i--) { + const d = raindrops[i]; + d.y += d.vy * dt; + d.x -= 30 * dt; + if (d.y > camY + H + 20) raindrops.splice(i, 1); + } + } + // Снег + if (weatherState.type === 'snow') { + const spawnRate = Math.floor(weatherState.intensity * 30 * dt); + for (let i = 0; i < spawnRate && snowflakes.length < MAX_SNOWFLAKES; i++) { + snowflakes.push({ + x: camX + Math.random() * W, + y: camY - 10, + vy: 40 + Math.random() * 60, + vx: (Math.random() - 0.5) * 30, + size: 2 + Math.random() * 3 + }); + } + } + for (let i = snowflakes.length - 1; i >= 0; i--) { + const s = snowflakes[i]; + s.y += s.vy * dt; + s.x += s.vx * dt + Math.sin(s.y * 0.02) * 10 * dt; + if (s.y > camY + H + 20 || weatherState.type !== 'snow') snowflakes.splice(i, 1); + } + } + + function drawRain() { + // Дождь + if (raindrops.length > 0) { + ctx.save(); + ctx.strokeStyle = (weatherState.type === 'storm') ? 'rgba(174,194,224,0.7)' : 'rgba(174,194,224,0.5)'; + ctx.lineWidth = 1.5; + ctx.beginPath(); + for (const d of raindrops) { + ctx.moveTo(d.x, d.y); + ctx.lineTo(d.x - 3, d.y + d.len); + } + ctx.stroke(); + ctx.restore(); + } + // Снег + if (snowflakes.length > 0) { + ctx.save(); + ctx.fillStyle = 'rgba(255,255,255,0.8)'; + for (const s of snowflakes) { + ctx.beginPath(); + ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2); + ctx.fill(); + } + ctx.restore(); + } + // Гроза — вспышка + if (weatherState.type === 'storm' && Math.random() < 0.003) { + ctx.save(); + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(camX, camY, W, H); + ctx.restore(); + } + // Туман — серый оверлей + if (weatherState.type === 'fog') { + ctx.save(); + ctx.fillStyle = 'rgba(200,200,200,0.4)'; + ctx.fillRect(camX, camY, W, H); + ctx.restore(); + } + } + + // Частицы (взрыв) + const parts = []; + function spawnExplosion(x,y, power){ + const n = Math.floor(16 + power*10); + for(let i=0;i 100){ + playSound('splash'); + } + } + + function resolveY(e){ + // Всегда пересчитываем grounded (не держим "липким") + e.grounded = false; + + const x1 = e.x + 2; + const x2 = e.x + e.w - 2; + + // Проверяем, находится ли игрок на лестнице (по центру) + const cx = e.x + e.w/2; + const cy = e.y + e.h/2; + const gx = Math.floor(cx / TILE); + const gy = Math.floor(cy / TILE); + const b = getBlock(gx, gy); + const onLadder = b && BLOCKS[b.t] && BLOCKS[b.t].climbable; + + // Если на лестнице - можно двигаться вверх/вниз + if(onLadder){ + e.grounded = true; + + // Если нажимаем прыжок на лестнице - поднимаемся + if(inp.j){ + e.vy = -200; + } + // Если нажимаем вниз - спускаемся + else if(inp.s){ + e.vy = 100; + } + // Иначе - остаёмся на месте (нет гравитации) + else { + e.vy = 0; + } + return; + } + + // Проверяем, можно ли запрыгнуть на лестницу рядом (слева или справа) + const leftGX = Math.floor((e.x - 4) / TILE); + const rightGX = Math.floor((e.x + e.w + 4) / TILE); + const playerGY = Math.floor((e.y + e.h/2) / TILE); + + const leftBlock = getBlock(leftGX, playerGY); + const rightBlock = getBlock(rightGX, playerGY); + const leftLadder = leftBlock && BLOCKS[leftBlock.t] && BLOCKS[leftBlock.t].climbable; + const rightLadder = rightBlock && BLOCKS[rightBlock.t] && BLOCKS[rightBlock.t].climbable; + + // Если рядом есть лестница и игрок прыгает - притягиваем к ней + if((leftLadder || rightLadder) && inp.j && e.vy < 0){ + // Перемещаем игрока к лестнице + if(leftLadder && e.x > leftGX * TILE + TILE/2){ + e.x = leftGX * TILE + TILE/2 - e.w/2; + } else if(rightLadder && e.x < rightGX * TILE + TILE/2){ + e.x = rightGX * TILE + TILE/2 - e.w/2; + } + e.grounded = true; + e.vy = -150; // меньший прыжок при запрыгивании на лестницу + return; + } + + // 1) Если движемся вниз ИЛИ стоим (vy >= 0) — проверяем опору под ногами + // Берём точку на 1px ниже стопы, чтобы не зависеть от ровного попадания в границу тайла. + if(e.vy >= 0){ + const probeY = e.y + e.h + 1; + const gy = Math.floor(probeY / TILE); + const gxA = Math.floor(x1 / TILE); + const gxB = Math.floor(x2 / TILE); + + if(isSolid(gxA, gy) || isSolid(gxB, gy)){ + e.y = gy * TILE - e.h; // прижимаем к полу + e.vy = 0; + e.grounded = true; + + // урон от падения — только игроку и только не в воде + if(e === player && !player.inWater){ + const fallTiles = (e.y - e.fallStartY) / TILE; + if(fallTiles > 6) { + const damage = calculateDamage((fallTiles - 6) * 10); + player.hp -= damage; + } + } + if(e === player) e.fallStartY = e.y; + } + } + + // 2) Проверка на возможность запрыгнуть на блок, если игрок рядом с ним + if(e.vy < 0 && e === player){ + const gy = Math.floor(e.y / TILE); + const gxA = Math.floor(x1 / TILE); + const gxB = Math.floor(x2 / TILE); + + // Проверяем, есть ли блок рядом с игроком + if((isSolid(gxA, gy) || isSolid(gxB, gy)) && !isSolid(gxA, gy - 1) && !isSolid(gxB, gy - 1)){ + e.y = (gy + 1) * TILE; + e.vy = 0; + e.grounded = true; + if(e === player) e.fallStartY = e.y; + console.log("Jumped onto block!"); + } + } + + // 2) Если движемся вверх — проверяем потолок + if(e.vy < 0){ + const gy = Math.floor(e.y / TILE); + const gxA = Math.floor(x1 / TILE); + const gxB = Math.floor(x2 / TILE); + if(isSolid(gxA, gy) || isSolid(gxB, gy)){ + e.y = (gy + 1) * TILE; + e.vy = 0; + } + } + } + + function resolveX(e){ + const y1 = e.y + 2; + const y2 = e.y + e.h - 2; + + // Проверяем, находимся ли мы на лестнице + const cx = e.x + e.w/2; + const cy = e.y + e.h/2; + const gx = Math.floor(cx / TILE); + const gy = Math.floor(cy / TILE); + const b = getBlock(gx, gy); + const onLadder = b && BLOCKS[b.t] && BLOCKS[b.t].climbable; + + if(e.vx > 0){ + const gx = Math.floor((e.x + e.w)/TILE); + const gyA = Math.floor(y1/TILE); + const gyB = Math.floor(y2/TILE); + const solidA = isSolid(gx, gyA); + const solidB = isSolid(gx, gyB); + + if(solidA || solidB){ + e.x = gx*TILE - e.w; + e.vx = 0; + } + } else if(e.vx < 0){ + const gx = Math.floor(e.x/TILE); + const gyA = Math.floor(y1/TILE); + const gyB = Math.floor(y2/TILE); + const solidA = isSolid(gx, gyA); + const solidB = isSolid(gx, gyB); + + if(solidA || solidB){ + e.x = (gx+1)*TILE; + e.vx = 0; + } + } + } + + // TNT логика: цепь + усиление + const activeTNT = new Set(); // хранит key + function activateTNT(b, fuse=3.2){ + if(b.dead) return; + if(b.active) return; + b.active=true; + b.fuse=fuse; + activeTNT.add(k(b.gx,b.gy)); + } + + function explodeAt(gx,gy){ + const center = getBlock(gx,gy); + if(!center) return; + + // усиление: считаем сколько TNT рядом (в радиусе 2) и активируем их «почти сразу» + let bonus = 0; + for(let x=gx-2; x<=gx+2; x++){ + for(let y=gy-2; y<=gy+2; y++){ + const b = getBlock(x,y); + if(b && !b.dead && b.t==='tnt' && !(x===gx && y===gy)){ + bonus += 0.8; + activateTNT(b, 0.12); // цепь + } + } + } + + const power = 1 + bonus; // условная мощность + const radius = 3.2 + bonus*0.7; // радиус разрушения в тайлах + const dmgR = 150 + bonus*60; // радиус урона в пикселях + + removeBlock(gx,gy); + activeTNT.delete(k(gx,gy)); + playSound('explode1'); // Звук взрыва + spawnExplosion(gx*TILE + TILE/2, gy*TILE + TILE/2, power); + + for(let x=Math.floor(gx-radius); x<=Math.ceil(gx+radius); x++){ + for(let y=Math.floor(gy-radius); y<=Math.ceil(gy+radius); y++){ + const d = Math.hypot(x-gx, y-gy); + if(d > radius) continue; + const b = getBlock(x,y); + if(!b || b.dead) continue; + if(BLOCKS[b.t].fluid) continue; + if(BLOCKS[b.t].unbreakable) continue; + if(b.t==='tnt') { activateTNT(b, 0.12); continue; } + removeBlock(x,y); + if(inv[b.t] !== undefined && Math.random()<0.20) inv[b.t]++; // немного дропа + } + } + rebuildHotbar(); + + // урон + const hurt = (e)=>{ + const dx = (e.x+e.w/2) - (gx*TILE+TILE/2); + const dy = (e.y+e.h/2) - (gy*TILE+TILE/2); + const dist = Math.hypot(dx,dy); + if(dist < dmgR){ + const dmg = (dmgR - dist) * 0.06 * power; + if(e === player) { + const actualDamage = calculateDamage(dmg); + player.hp -= actualDamage; + } else { + e.hp -= dmg; + } + e.vx += (dx/dist || 0) * 600; + e.vy -= 320; + } + }; + hurt(player); + mobs.forEach(hurt); + } + + // Взаимодействие мышь/тап + const mouse = { x:null, y:null }; + canvas.addEventListener('pointermove', (e)=>{ + const r = canvas.getBoundingClientRect(); + mouse.x = e.clientX - r.left; + mouse.y = e.clientY - r.top; + }); + + canvas.addEventListener('pointerdown', (e)=>{ + if(craftOpen) return; + if(player.hp<=0) return; + + const r = canvas.getBoundingClientRect(); + const sx = e.clientX - r.left; + const sy = e.clientY - r.top; + + const wx = sx + camX; + const wy = sy + camY; + + const gx = Math.floor(wx / TILE); + const gy = Math.floor(wy / TILE); + + // Пробуждение: клик по любой кровати когда спишь + const b = getBlock(gx,gy); + if(player.sleeping && b && b.t==='bed'){ + player.sleeping = false; + return; + } + + if(player.sleeping) return; // Нельзя взаимодействовать во время сна + + // Клик по печи — открываем панель обжига + if(b && b.t === 'furnace' && mode() === 'mine'){ + openFurnaceUI(gx, gy); + return; + } + + // клик по мобу (в режиме mine) + if(mode()==='mine'){ + // Check all mobs (local + server-spawned) using getAllMobs + const allClickMobs = getAllMobs(); + for(let i = allClickMobs.length - 1; i >= 0; i--){ + const m = allClickMobs[i]; + if(m.dead) continue; + if(wx>=m.x && wx<=m.x+m.w && wy>=m.y && wy<=m.y+m.h){ + let dmg = 1; + const swordTypes = ['iron_sword','stone_sword','wood_sword']; + for (const st of swordTypes) { + if (inv[st] > 0) { dmg = TOOLS[st].damage || 3; useTool(st); break; } + } + m.hp -= dmg; + m.vx += (m.x - player.x) * 2; + m.vy -= 200; + playSound('attack'); + // Server-spawned mob: emit hurt to server for relay, handle death locally + if(m.id !== undefined && isMultiplayer){ + socket.emit('mob_hurt', { id: m.id, dmg }); + if(m.hp <= 0){ + socket.emit('mob_died', { id: m.id }); + } + } + if(m.hp<=0){ + if(m.kind === 'chicken') playSound('hurt_chicken'); + spawnDrops(m.x + m.w/2, m.y + m.h/2, m.kind); + // Remove from the correct array + if(m.id !== undefined){ + serverMobs.delete(m.id); + } else { + const localIdx = mobs.indexOf(m); + if(localIdx >= 0) mobs.splice(localIdx, 1); + } + rebuildHotbar(); + } + return; + } + } + } + + // Лук — стреляем стрелой + if(selected === 'bow' && inv.bow > 0 && inv.arrow > 0){ + const aimX = wx - player.x - player.w/2; + const aimY = wy - player.y - player.h/2; + const angle = Math.atan2(aimY, aimX); + projectiles.push({ + x: player.x + player.w/2, + y: player.y + player.h/3, + vx: Math.cos(angle) * 550, + vy: Math.sin(angle) * 550, + dmg: 10, + owner: 'player', + life: 4 + }); + inv.arrow--; + useTool('bow'); + playSound('hit1'); + rebuildHotbar(); + return; + } + + // еда (предмет) + if(ITEMS[selected] && inv[selected]>0){ + const it = ITEMS[selected]; + if(player.hp < 100 || player.hunger < 100){ + playSound('eat1'); + player.hunger = Math.min(100, player.hunger + it.food); + player.hp = Math.min(100, player.hp + 15); + inv[selected]--; + rebuildHotbar(); + } + return; + } + + //Посадка семян на грядку + if(b && b.t === 'farmland' && mode()==='build'){ + const seedMap = { wheat: 'wheat_stage0', carrot: 'carrot_stage0', potato: 'potato_stage0' }; + if(seedMap[selected] && inv[selected] > 0){ + inv[selected]--; + const cropType = seedMap[selected]; + setBlock(gx, gy-1, cropType); + growthTimers[gx+','+(gy-1)] = { stage:0, growTimer: 10+Math.random()*5 }; + sendBlockChange(gx, gy-1, cropType, 'add'); + playSound('cloth1'); + rebuildHotbar(); + return; + } + } + + // Мотыга — превращает grass/dirt в farmland (в любом режиме) + if(selected === 'hoe' && inv.hoe > 0 && b){ + if(b.t === 'grass' || b.t === 'dirt'){ + setBlock(gx, gy, 'farmland'); + sendBlockChange(gx, gy, b.t, 'remove'); + sendBlockChange(gx, gy, 'farmland', 'add'); + useTool('hoe'); + playSound('cloth1'); + rebuildHotbar(); + return; + } + } + + // жарка на костре: выбран meat + клик по campfire + if(b && b.t==='campfire' && selected==='meat' && inv.meat>0){ + playSound('fire'); // Звук при жарке на костре + inv.meat--; inv.cooked++; + rebuildHotbar(); + return; + } + + // Сон на кровати: клик по bed + if(b && b.t==='bed' && isNight()){ + player.sleeping = true; + saveGame(); // Сохраняем при отходе ко сну + return; + } + + if(mode()==='mine'){ + if(!b) return; + if(BLOCKS[b.t].fluid) return; + // Клик на урожайную культуру — сбор + if(BLOCKS[b.t].harvestable){ + const hInfo = BLOCKS[b.t]; + inv[hInfo.harvestItem] = (inv[hInfo.harvestItem]||0) + hInfo.harvestQty; + removeBlock(gx, gy); + sendBlockChange(gx, gy, b.t, 'remove'); + delete growthTimers[gx+','+gy]; + playSound('cloth1'); + rebuildHotbar(); + return; + } + if(BLOCKS[b.t].decor) return; + + if(b.t==='tnt'){ activateTNT(b, 3.2); return; } // не взрывается сразу + + const removed = removeBlock(gx,gy); + if(removed){ + inv[removed.t] = (inv[removed.t]||0) + 1; + + // Тратим прочность кирки (если есть в инвентаре) + const pickTypes = ['iron_pickaxe','stone_pickaxe','wood_pickaxe']; + for (const pt of pickTypes) { + if (inv[pt] > 0) { + const broke = useTool(pt); + if (broke) playSound('cloth1'); // звук поломки + break; + } + } + + // Отправляем изменение блока на сервер + sendBlockChange(gx, gy, removed.t, 'remove'); + + // Звуки при добыче блоков + if(removed.t === 'glass') playSound('glass1'); + else if(removed.t === 'sand') playSound('sand1'); + else if(removed.t === 'snow') playSound('snow1'); + else if(removed.t === 'stone' || removed.t.endsWith('_ore')) playSound('stone1'); + else if(removed.t === 'wood') playSound('wood1'); + else playSound('cloth1'); + + rebuildHotbar(); + } + return; + } + + if(mode()==='build'){ + if(inv[selected] <= 0) return; + if(!BLOCKS[selected]) return; + if(b) return; // занято + + // Проверяем, ставим ли лодку + if(selected === 'boat'){ + // Лодку можно ставить только на воду + const waterBelow = getBlock(gx, gy+1); + if(!waterBelow || waterBelow.t !== 'water'){ + return; + } + + // Создаём лодку + boat.x = gx * TILE; + boat.y = gy * TILE; + boat.vx = 0; + boat.vy = 0; + boat.active = true; + boat.inWater = true; + + // Сажаем игрока в лодку + player.inBoat = true; + player.x = boat.x; + player.y = boat.y; + player.vx = 0; + player.vy = 0; + + playSound('splash'); + inv[selected]--; + rebuildHotbar(); + return; + } + + // запрет ставить в игрока + const bx = gx*TILE, by = gy*TILE; + const overlap = !(bx >= player.x+player.w || bx+TILE <= player.x || by >= player.y+player.h || by+TILE <= player.y); + if(overlap) return; + + setBlock(gx,gy,selected, true); // true = блок установлен игроком + inv[selected]--; + + // Отправляем изменение блока на сервер + sendBlockChange(gx, gy, selected, 'set'); + + // Звук при строительстве + if(selected === 'stone' || selected === 'brick') playSound('stone_build'); + else if(selected === 'wood' || selected === 'planks') playSound('wood_build'); + else if(selected === 'glass') playSound('glass1'); + else if(selected === 'sand') playSound('sand1'); + else if(selected === 'snow') playSound('snow1'); + else if(selected === 'dirt' || selected === 'grass') playSound('cloth1'); + + rebuildHotbar(); + return; + } + }); + + // ==================== БИОМЫ ==================== + const BIOMES = { + plains: { name:'Равнина', surface:'grass', subsurface:'dirt', trees:true, flowers:true, treeChance:0.12 }, + desert: { name:'Пустыня', surface:'sand', subsurface:'sand', trees:false, flowers:false, treeChance:0 }, + tundra: { name:'Тундра', surface:'snow', subsurface:'dirt', trees:true, flowers:false, treeChance:0.06 }, + swamp: { name:'Болото', surface:'moss', subsurface:'dirt', trees:true, flowers:false, treeChance:0.10 }, + mountains:{ name:'Горы', surface:'stone', subsurface:'stone', trees:false, flowers:false, treeChance:0 } + }; + + function getBiome(gx) { + const temp = Math.sin(gx*0.003 + worldSeed*0.01)*0.5 + Math.sin(gx*0.007 + worldSeed*0.02)*0.3 + 0.5; + const humid = Math.sin(gx*0.004 + worldSeed*0.015 + 1000)*0.5 + Math.cos(gx*0.006 + worldSeed*0.02 + 2000)*0.3 + 0.5; + const mtVal = Math.sin(gx*0.001 + worldSeed*0.005)*0.5 + 0.5; + if (temp > 0.7) return 'desert'; + if (temp < 0.3) return 'tundra'; + if (humid > 0.7 && temp >= 0.3 && temp <= 0.7) return 'swamp'; + if (temp >= 0.5 && temp <= 0.7 && mtVal > 0.75) return 'mountains'; + return 'plains'; + } + + const biomeCache = {}; + function getCachedBiome(gx) { + const chunk = Math.floor(gx / 8); // cache per 8-tile chunk for smoother biomes + if (biomeCache[chunk] === undefined) biomeCache[chunk] = getBiome(chunk * 8); + return biomeCache[chunk]; + } + + // ==================== ПОГОДА ==================== + const weatherState = { type: 'clear', intensity: 0, timer: 0, duration: 180, nextChange: 120 + Math.random()*180 }; + const BIOME_WEATHER = { + plains: { clear:0.50, rain:0.30, storm:0.10, snow:0, fog:0.10 }, + desert: { clear:0.80, rain:0.05, storm:0, snow:0, fog:0.15 }, + tundra: { clear:0.20, rain:0, storm:0.10, snow:0.60, fog:0.10 }, + swamp: { clear:0.20, rain:0.30, storm:0.10, snow:0, fog:0.40 }, + mountains:{ clear:0.40, rain:0.30, storm:0.10, snow:0.10, fog:0.10 } + }; + + function updateWeather(dt) { + weatherState.timer += dt; + if (weatherState.timer >= weatherState.nextChange) { + weatherState.timer = 0; + weatherState.nextChange = 60 + Math.random() * 240; + const biome = getCachedBiome(Math.floor(player.x / TILE)); + const probs = BIOME_WEATHER[biome] || BIOME_WEATHER.plains; + const r = Math.random(); + let cum = 0; + if ((cum += probs.clear) > r) { weatherState.type = 'clear'; } + else if ((cum += probs.rain) > r) { weatherState.type = 'rain'; } + else if ((cum += probs.storm) > r) { weatherState.type = 'storm'; } + else if ((cum += probs.snow) > r) { weatherState.type = 'snow'; } + else { weatherState.type = 'fog'; } + weatherState.duration = 60 + Math.random() * 300; + } + // Intensity interpolation + const target = (weatherState.type === 'clear') ? 0 : 1; + weatherState.intensity += (target - weatherState.intensity) * dt * 0.5; + } + + function getWeatherSpeedMultiplier() { + if (weatherState.type === 'rain') return 0.85; + if (weatherState.type === 'snow') return 0.7; + if (weatherState.type === 'storm') return 0.85; + return 1; + } + + function isOutdoorLight(lx, ly) { + // Check if position is outdoors (no block above) + const aboveGy = Math.floor(ly / TILE) - 1; + const aboveGx = Math.floor(lx / TILE); + const above = getBlock(aboveGx, aboveGy); + return !above || !BLOCKS[above.t]?.solid; + } + + // ==================== СТРУКТУРЫ МИРА ==================== + function placeStructure(startGx, startGy, pattern) { + for (let dy = 0; dy < pattern.length; dy++) { + for (let dx = 0; dx < pattern[dy].length; dx++) { + const bt = pattern[dy][dx]; + if (bt && bt !== 'air') { + setBlock(startGx + dx, startGy + dy, bt); + } + } + } + } + + // Пирамида в пустыне + const PYRAMID_PATTERN = [ + ['sand','sand','sand','sand','sand','sand','sand'], + ['sand','stone','stone','stone','stone','stone','sand'], + ['sand','stone','air','air','air','stone','sand'], + ['sand','stone','air','air','air','stone','sand'], + ['sand','stone','air','air','air','stone','sand'], + ['sand','sand','stone','stone','stone','sand','sand'] + ]; + + // Дом в равнине + const HOUSE_PATTERN = [ + ['air','planks','planks','planks','planks','air'], + ['planks','air','air','air','air','planks'], + ['planks','air','torch','air','air','planks'], + ['planks','air','air','air','air','planks'], + ['planks','planks','air','planks','planks','planks'] + ]; + + // Хижина в болоте + const HUT_PATTERN = [ + ['air','wood','wood','wood','air'], + ['wood','air','air','air','wood'], + ['wood','air','torch','air','wood'], + ['moss','moss','air','moss','moss'] + ]; + + // ==================== ГЕНЕРАЦИЯ ==================== + const generated = new Set(); // gx already generated + function surfaceGyAt(gx) { + const biome = getCachedBiome(gx); + // Base noise (same for all biomes) + const n1 = Math.sin(gx*0.025 + worldSeed*0.001)*8; + const n2 = Math.sin(gx*0.012 + worldSeed*0.002)*12; + const n3 = Math.sin(gx*0.006 + worldSeed*0.003)*6; + const n4 = Math.sin(gx*0.045 + worldSeed*0.004)*4; + const n5 = Math.cos(gx*0.018 + worldSeed*0.005)*5; + let h; + switch(biome) { + case 'desert': + h = Math.floor(SEA_GY - 4 + n3*0.3 + n4*0.5); // flatter, slightly higher + break; + case 'tundra': + h = Math.floor(SEA_GY - 6 + n2*0.5 + n3*0.4 + n5*0.3); // gentle rolling + break; + case 'swamp': + h = Math.floor(SEA_GY - 2 + n3*0.2 + n4*0.3); // very flat, near sea level + h = Math.max(h, SEA_GY - 3); // never too deep + break; + case 'mountains': + h = Math.floor(SEA_GY - 15 + n1*1.5 + n2*1.2 + n3); // tall peaks + break; + default: // plains + h = Math.floor(SEA_GY - 8 + n1 + n2 + n3 + n4 + n5); + } + return h; + } + + function genColumn(gx) { + if(generated.has(gx)) return; + if(worldSeed == null) return; // wait for server seed in multiplayer + generated.add(gx); + + const sgy = surfaceGyAt(gx); + const biome = getCachedBiome(gx); + + // === Вода и поверхность === + if(sgy > SEA_GY) { + // ниже уровня моря — заливаем водой + for(let gy = SEA_GY; gy < sgy; gy++) { + const blockType = (biome === 'swamp' && gy >= SEA_GY - 1) ? 'swamp_water' : 'water'; + setBlock(gx, gy, blockType); + } + setBlock(gx, sgy, 'sand'); + } else { + // поверхность + const b = BIOMES[biome]; + setBlock(gx, sgy, b.surface); + // болото: случайные лужи болотной воды + if(biome === 'swamp' && seededRandom(gx*3, sgy) < 0.15) { + setBlock(gx, sgy-1, 'swamp_water'); + } + // тундра: лёд на воде рядом + if(biome === 'tundra' && sgy === SEA_GY && seededRandom(gx, SEA_GY-1) < 0.3) { + setBlock(gx, SEA_GY-1, 'ice'); + } + } + + // === Подповерхностные слои === + for(let gy = sgy+1; gy <= BEDROCK_GY; gy++) { + if(gy === BEDROCK_GY) { setBlock(gx,gy,'bedrock'); continue; } + + let t = BIOMES[biome].subsurface; + + // глубже — камень + if(gy > sgy + 3) t = 'stone'; + + // пустыня: sand глубже + if(biome === 'desert' && gy <= sgy + 6) t = 'sand'; + + // болото: глина ближе к поверхности + if(biome === 'swamp' && gy <= sgy + 2 && seededRandom(gx, gy) < 0.3) t = 'clay'; + + // горы: gravel + if(biome === 'mountains' && gy > sgy + 4 && seededRandom(gx, gy) < 0.12) t = 'gravel'; + + // общие руды + const depth = gy - sgy; + const r = seededRandom(gx, gy); + if(t === 'stone') { + if(r < 0.06) t = 'coal'; + else if(r < 0.10) t = 'copper_ore'; + else if(r < 0.13) t = 'iron_ore'; + else if(depth > 40 && r < 0.145) t = 'gold_ore'; + else if(depth > 70 && r < 0.152) t = 'diamond_ore'; + } + + setBlock(gx, gy, t); + } + + // === Растительность и декор === + const b = BIOMES[biome]; + const top = getBlock(gx, sgy); + + // цветы (только plain) + if(biome === 'plains' && top && top.t === 'grass' && seededRandom(gx, sgy-1) < 0.10) { + setBlock(gx, sgy-1, 'flower'); + } + + // деревья + if(b.trees && seededRandom(gx*7, sgy-2) < b.treeChance) { + if(biome === 'tundra') { + // ёлки (треугольные, 3-5 высоты) + const th = 3 + Math.floor(seededRandom(gx, sgy) * 3); + for(let i = 0; i < th; i++) setBlock(gx, sgy-1-i, 'wood'); + // крона (треугольник) + for(let row = 0; row < th; row++) { + const w = Math.min(row + 1, 2); + for(let dx = -w; dx <= w; dx++) { + const ly = sgy-1-th+row; + if(ly >= 0) setBlock(gx+dx, ly, 'spruce_leaves'); + } + } + } else if(biome === 'swamp') { + // болотное дерево (короче, с мхом) + setBlock(gx, sgy-1, 'wood'); + setBlock(gx, sgy-2, 'moss'); + setBlock(gx-1, sgy-2, 'leaves'); + setBlock(gx+1, sgy-2, 'leaves'); + } else { + // обычное дерево (plains) + setBlock(gx, sgy-1, 'wood'); + setBlock(gx, sgy-2, 'wood'); + setBlock(gx, sgy-3, 'leaves'); + setBlock(gx-1, sgy-3, 'leaves'); + setBlock(gx+1, sgy-3, 'leaves'); + } + } + + // кактусы (пустыня) + if(biome === 'desert' && seededRandom(gx*11, sgy) < 0.04) { + const ch = 1 + Math.floor(seededRandom(gx, sgy+1) * 2); + for(let i = 0; i < ch; i++) setBlock(gx, sgy-1-i, 'cactus'); + } + + // грибы (болото) + if(biome === 'swamp' && seededRandom(gx*13, sgy) < 0.06) { + setBlock(gx, sgy-1, 'mushroom'); + } + + // сухие кусты (пустыня) + if(biome === 'desert' && seededRandom(gx*17, sgy) < 0.05) { + setBlock(gx, sgy-1, 'dead_bush'); + } + + // === Структуры мира === + // Пирамида в пустыне + if(biome === 'desert' && ((gx % 200 + 200) % 200) === 47 && sgy < SEA_GY && sgy > SEA_GY - 5) { + placeStructure(gx, sgy - 5, PYRAMID_PATTERN); + } + // Дом в равнине + if(biome === 'plains' && ((gx % 150 + 150) % 150) === 33 && sgy < SEA_GY) { + placeStructure(gx, sgy - 4, HOUSE_PATTERN); + } + // Хижина в болоте + if(biome === 'swamp' && ((gx % 180 + 180) % 180) === 55 && sgy < SEA_GY) { + placeStructure(gx, sgy - 3, HUT_PATTERN); + } + + // Применяем серверные оверрайды для этой колонны + const colPrefix = gx + ','; + for (const [key, ov] of serverOverrides) { + if (!key.startsWith(colPrefix)) continue; + if (ov.op === 'remove') { + const b = grid.get(key); + if (b) { grid.delete(key); b.dead = true; } + } else if (ov.op === 'set') { + if (!grid.has(key)) { + const gy = parseInt(key.split(',')[1]); + const nb = { gx, gy, t: ov.t, dead: false, active: false, fuse: 0 }; + grid.set(key, nb); + blocks.push(nb); + } + } + } + } + + // Перегенерация видимых чанков (используется при загрузке сохранения) + function regenerateVisibleChunks(){ + const gx0 = Math.floor(camX/TILE); + for(let gx=gx0-GEN_MARGIN_X; gx<=gx0+GEN_MARGIN_X; gx++){ + // Принудительно перегенерируем колонну + generated.delete(gx); + genColumn(gx); + } + } + + function ensureGenAroundCamera(){ + const gx0 = Math.floor(camX/TILE); + for(let gx=gx0-GEN_MARGIN_X; gx<=gx0+GEN_MARGIN_X; gx++){ + genColumn(gx); + } + } + + // Лут с дерева/листвы: дерево -> wood; листья -> leaves + // (уже в mine добавляется inv[type] автоматически) + + // Рисование костра: огонь поверх текстуры + function drawFire(wx,wy,now){ + const baseX = wx; + const baseY = wy; + const flick = 1.5 + (Math.sin(now/200)+1)*1; + ctx.fillStyle = 'rgba(255,140,0,0.85)'; + ctx.beginPath(); + ctx.moveTo(baseX+10, baseY+30); + ctx.lineTo(baseX+20, baseY+30-flick); + ctx.lineTo(baseX+30, baseY+30); + ctx.fill(); + + ctx.fillStyle = 'rgba(255,230,150,0.75)'; + ctx.beginPath(); + ctx.moveTo(baseX+14, baseY+30); + ctx.lineTo(baseX+20, baseY+30-(flick*0.7)); + ctx.lineTo(baseX+26, baseY+30); + ctx.fill(); + } + + // Моб AI + function mobAI(m, dt){ + updateWaterFlag(m); + + if(m.kind==='zombie'){ + // активность ночью + const night = isNight(); + if(!night){ m.hp -= 30*dt; m.vx *= 0.5; return; } + const dir = Math.sign((player.x) - m.x); + m.vx = dir * m.speed; + if(m.inWater && Math.random()<0.06) m.vy = -260; + // атака + if(Math.abs((m.x+ m.w/2) - (player.x+player.w/2)) < 28 && + Math.abs((m.y+ m.h/2) - (player.y+player.h/2)) < 40 && + player.invuln <= 0){ + const damage = calculateDamage(15); + player.hp -= damage; + player.invuln = 0.8; + player.vx += dir*420; + player.vy -= 260; + playSound('hit1'); // Звук при атаке зомби + } + } else if(m.kind==='creeper'){ + // активность ночью + const night = isNight(); + if(!night){ m.hp -= 30*dt; m.vx *= 0.5; return; } + const dir = Math.sign((player.x) - m.x); + const dist = Math.hypot((player.x+player.w/2) - (m.x+m.w/2), (player.y+player.h/2) - (m.y+m.h/2)); + + // Движение к игроку + m.vx = dir * m.speed; + if(m.inWater && Math.random()<0.06) m.vy = -260; + + // Взрыв если близко к игроку + if(dist < 60){ + m.fuse -= dt; + if(m.fuse <= 0){ + explodeAt(Math.floor((m.x+m.w/2)/TILE), Math.floor((m.y+m.h/2)/TILE)); + m.hp = 0; + } + } else { + // Поджигаем если очень близко + if(dist < 40){ + m.fuse = 0.5; // Быстрый взрыв + } + } + } else if(m.kind==='skeleton'){ + // активность ночью + const night = isNight(); + if(!night){ m.hp -= 30*dt; m.vx *= 0.5; return; } + const dir = Math.sign((player.x) - m.x); + const dist = Math.hypot((player.x+player.w/2) - (m.x+m.w/2), (player.y+player.h/2) - (m.y+m.h/2)); + + // Движение к игроку + m.vx = dir * m.speed; + if(m.inWater && Math.random()<0.06) m.vy = -260; + + // Стрельба стрелами + m.shootCooldown -= dt; + if(dist < 300 && m.shootCooldown <= 0){ + m.shootCooldown = 2.0; + const dx = (player.x+player.w/2) - (m.x+m.w/2); + const dy = (player.y+player.h/2) - (m.y+m.h/2); + const angle = Math.atan2(dy, dx); + const speed = 450; + projectiles.push({ + x: m.x + m.w/2, + y: m.y + m.h/3, + vx: Math.cos(angle) * speed, + vy: Math.sin(angle) * speed, + dmg: 6, + owner: 'mob', + life: 3 + }); + } + } else if(m.kind==='scorpion') { + // Скорпион — бежит к игроку, яд (замедление) + const dir = Math.sign((player.x) - m.x); + m.vx = dir * m.speed; + if(m.inWater && Math.random()<0.06) m.vy = -260; + // Яд при касании — замедление на 3 сек + if(Math.abs((m.x+ m.w/2) - (player.x+player.w/2)) < 28 && + Math.abs((m.y+ m.h/2) - (player.y+player.h/2)) < 40 && + player.invuln <= 0){ + const damage = calculateDamage(8); + player.hp -= damage; + player.invuln = 0.8; + player.slowTimer = 3; // замедление + player.vx += dir*300; + player.vy -= 200; + playSound('hit1'); + } + } else if(m.kind==='polar_bear') { + // Белый медведь — нейтрален, атакует если ударили (hostile пока нет, атакует через proximity) + m.aiT -= dt; + if(m.aiT <= 0){ + m.aiT = 2.0 + Math.random()*3; + m.dir = Math.random()<0.5 ? -1 : 1; + if(Math.random()<0.3) m.dir = 0; + } + m.vx = m.dir * m.speed; + if(m.inWater) m.vy = -120; + } else if(m.kind==='slime') { + // Слизь — прыгает к игроку + const dir = Math.sign((player.x+player.w/2) - (m.x+m.w/2)); + m.aiT -= dt; + if(m.aiT <= 0){ + m.aiT = 1.5 + Math.random()*1.5; + m.dir = dir; + // Прыжок + m.vy = -200; + } + m.vx = m.dir * m.speed; + } else if(m.kind==='eagle') { + // Орёл — летает, атакует пикированием + const dx = (player.x+player.w/2) - (m.x+m.w/2); + const dy = (player.y+player.h/2) - (m.y+m.h/2); + const dist = Math.hypot(dx, dy); + if(dist < 400) { + // Пикирует на игрока + m.vx = Math.sign(dx) * m.speed; + m.vy = dy > 0 ? 60 : -60; // летит вниз к игроку + } else { + // Патрулирует + m.aiT -= dt; + if(m.aiT <= 0){ + m.aiT = 2+Math.random()*3; + m.dir = Math.random()<0.5 ? -1 : 1; + } + m.vx = m.dir * m.speed * 0.5; + m.vy = Math.sin(performance.now()/1000) * 30; // мягкое покачивание + } + // Атака при касании + if(dist < 30 && player.invuln <= 0){ + const damage = calculateDamage(10); + player.hp -= damage; + player.invuln = 0.8; + player.vy -= 200; + playSound('hit1'); + } + // Орёл не падает — летающий моб + m.vy *= 0.5; + m.grounded = false; + } else { + // животные (pig, chicken) + m.aiT -= dt; + if(m.aiT <= 0){ + m.aiT = 1.8 + Math.random()*2.5; + m.dir = Math.random()<0.5 ? -1 : 1; + if(Math.random()<0.25) m.dir = 0; + } + m.vx = m.dir * (m.kind==='chicken' ? 55 : 40); + if(m.inWater) m.vy = -120; + } + + // физика моба + const g = m.inWater ? GRAV_WATER : GRAV; + m.vy += g*dt; + + m.y += m.vy*dt; m.grounded=false; resolveY(m); + m.x += m.vx*dt; resolveX(m); + } + + function isNight(){ + // Автоматический цикл: ночь когда worldTime > 0.5 + return worldTime > 0.5; + } + + // Respawn + document.getElementById('respawnBtn').onclick = async () => { + playSound('click'); // Звук клика по кнопке + + console.log('=== RESPAWN CLICKED ==='); + console.log('isMultiplayer:', isMultiplayer); + console.log('otherPlayers.size:', otherPlayers.size); + console.log('player.hp before respawn:', player.hp); + + // В мультиплеере не загружаем сохранение, а возрождаемся в начальной точке + if (isMultiplayer && otherPlayers.size > 0) { + console.log('Мультиплеер режим - возрождение в начальной точке'); + player.hp = 100; + player.hunger = 100; + player.o2 = 100; + player.vx = player.vy = 0; + player.invuln = 0; + player.x = spawnPoint.x; + player.y = spawnPoint.y; + player.fallStartY = player.y; + console.log('Возрождение в начальной точке, HP:', player.hp); + } else { + console.log('Одиночный режим - загружаем последнее сохранение'); + // Одиночный режим - загружаем последнее сохранение + const loadedSave = await loadGame(); + if(loadedSave){ + await applySave(loadedSave); + console.log('Загружено последнее сохранение после смерти, final HP:', player.hp); + } else { + // Если сохранения нет, возрождаемся в начальной точке + player.hp = 100; + player.hunger = 100; + player.o2 = 100; + player.vx = player.vy = 0; + player.invuln = 0; + player.x = spawnPoint.x; + player.y = spawnPoint.y; + player.fallStartY = player.y; + console.log('Возрождение в начальной точке, HP:', player.hp); + } + } + + console.log('player.hp after respawn logic:', player.hp); + console.log('Hiding death screen...'); + deathEl.style.display='none'; + console.log('=== RESPAWN END ==='); + }; + + // Resize + function resize(){ + W = gameEl.clientWidth; + H = gameEl.clientHeight; + canvas.width = W*dpr; + canvas.height = H*dpr; + lightC.width = W*dpr; + lightC.height = H*dpr; + ctx.setTransform(dpr,0,0,dpr,0,0); + } + window.addEventListener('resize', resize); + + // init + resize(); + rebuildHotbar(); + + // Инициализируем и загружаем сохранение + initDB().then(async () => { + // Пытаемся загрузить сохранённую игру + const loadedSave = await loadGame(); + if(loadedSave){ + await applySave(loadedSave); + console.log('Загружено сохранение, HP:', player.hp); + + // Проверяем HP после загрузки - если <= 0, возрождаемся + if (player.hp <= 0) { + console.log('WARNING: HP <= 0 после загрузки, возрождаемся'); + player.hp = 100; + player.hunger = 100; + player.o2 = 100; + player.x = spawnPoint.x; + player.y = spawnPoint.y; + player.vx = player.vy = 0; + player.invuln = 0; + player.fallStartY = player.y; + } + } else { + console.log('Сохранение не найдено, начинаем новую игру'); + + // Инициализируем игрока для новой игры + player.hp = 100; + player.hunger = 100; + player.o2 = 100; + player.vx = player.vy = 0; + player.invuln = 0; + + // старт — на поверхности (используем ту же логику что и в world_state) + const startGX = 6; + for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx); + const surfaceY = surfaceGyAt(startGX); + let safeGY = surfaceY - 1; + const aboveBlock = getBlock(startGX, surfaceY - 1); + if (aboveBlock && aboveBlock.t === 'water') { + for (let gy = SEA_GY - 1; gy >= 0; gy--) { + const b = getBlock(startGX, gy); + if (!b || b.dead || b.t === 'air' || b.t === 'water') continue; + safeGY = gy - 1; + break; + } + } + player.y = safeGY * TILE; + player.x = startGX * TILE; + player.fallStartY = player.y; + + // Обновляем spawnPoint, чтобы возрождение было на поверхности + spawnPoint.x = player.x; + spawnPoint.y = player.y; + + console.log('Новая игра: startGX=', startGX, 'surfaceY=', surfaceY, 'player.y=', player.y, 'player.hp=', player.hp); + console.log('spawnPoint обновлён: x=', spawnPoint.x, 'y=', spawnPoint.y); + + // Генерируем карту вокруг стартовой позиции при инициализации + for(let gx = startGX - 50; gx <= startGX + 50; gx++){ + genColumn(gx); + } + } + + // Автосейв при скрытии страницы (защита от потери прогресса) + document.addEventListener('visibilitychange', () => { + if(document.hidden){ + saveGame(); + } + }); + + // Автосейв перед закрытием страницы (защита от потери прогресса) + window.addEventListener('beforeunload', () => { + saveGame(); + }); + }).catch(err => { + console.error('Ошибка инициализации:', err); + // При ошибке начинаем новую игру + const startGX = 6; + genColumn(startGX); + player.y = (surfaceGyAt(startGX)-1)*TILE; + player.fallStartY = player.y; + + for(let gx = startGX - 50; gx <= startGX + 50; gx++){ + genColumn(gx); + } + }); + + // main loop + let last = performance.now(); + let prevJump = false; + // При возврате на вкладку — сбрасываем last чтобы не было скачка dt + document.addEventListener('visibilitychange', () => { + if (!document.hidden) last = performance.now(); + }); + function loop(now){ + const rawDt = Math.min(0.05, (now-last)/1000); + last = now; + // Sub-stepping: разбиваем физику на шаги ≤16ms чтобы не проваливаться сквозь блоки + const PHYSICS_STEP = 0.016; + const steps = Math.max(1, Math.ceil(rawDt / PHYSICS_STEP)); + const dt = rawDt / steps; + + const jumpPressed = inp.j && !prevJump; + prevJump = inp.j; + + // Ускорение времени во время сна + if(player.sleeping && isNight()){ + worldTime += dt * 8 / DAY_LEN; // В 8 раз быстрее + // Восстанавливаем здоровье во время сна + player.hp = Math.min(100, player.hp + dt * 20); + // Автоматическое пробуждение когда наступает день + if(!isNight()){ + player.sleeping = false; + } + } else { + worldTime += dt / DAY_LEN; + } + if(worldTime >= 1) worldTime -= 1; + + // камера следует за игроком по X/Y + camX = Math.floor((player.x + player.w/2) - W/2); + camY = Math.floor((player.y + player.h/2) - H/2); + + ensureGenAroundCamera(); + + // clouds parallax + for(const c of clouds){ + c.x -= c.s * dt; + if(c.x + c.w < camX - 400) c.x = camX + W + Math.random()*700; + } + + // Погода (биом-зависимая) + updateWeather(dt); + + // player + updateWaterFlag(player); + + // кислород/утопление: тратим O2 только если голова под водой, иначе восстанавливаем [web:223] + if(player.headInWater){ + player.o2 = Math.max(0, player.o2 - 6*dt); // замедлил в 3.7 раза + if(player.o2 === 0){ + const damage = calculateDamage(4*dt); + player.hp -= damage; + } + } else { + player.o2 = Math.min(100, player.o2 + 10*dt); // замедлил восстановление в 4 раза + } + + // голод убывает, но HP не отнимает (как просили) + player.hunger = Math.max(0, player.hunger - dt*0.2); // замедлил в 4 раза + + + // ========== ТЕМПЕРАТУРА ========== + if (player.temperature === undefined) player.temperature = 15; + const BIOME_TEMP = { tundra: -20, plains: 15, swamp: 20, desert: 45, mountains: 5 }; + const UNDERGROUND_TEMP = 15; + const COLD_THRESHOLD = -5; + const HEAT_THRESHOLD = 35; + const HEAT_RADIUS = 5; + const pGX = Math.floor(player.x / TILE); + const biome = getCachedBiome(pGX); + const pGY = Math.floor(player.y / TILE); + const sGY = surfaceGyAt(pGX); + const isUndergroundTemp = (pGY - sGY) > 2; // player deeper than 2 blocks below surface + let targetTemp = BIOME_TEMP[biome] || 15; + if (isUndergroundTemp) targetTemp = UNDERGROUND_TEMP; + if (isNight() && !isUndergroundTemp) targetTemp -= 10; + if ((weatherState.type === 'rain' || weatherState.type === 'storm') && !isUndergroundTemp) targetTemp -= 5; + player.temperature += (targetTemp - player.temperature) * dt * 0.3; + let nearHeat = false, nearCool = false; + for (let dx = -HEAT_RADIUS; dx <= HEAT_RADIUS; dx++) { + for (let dy = -HEAT_RADIUS; dy <= HEAT_RADIUS; dy++) { + const b = getBlock(pGX + dx, pGY + dy); + if (!b || b.dead) continue; + if (b.t === 'campfire' || b.t === 'torch') nearHeat = true; + if (b.t === 'water' && biome === 'desert') nearCool = true; + } + } + if (nearHeat) { + player.temperature += (30 - player.temperature) * dt * 2; + if (player.hp < 100) player.hp = Math.min(100, player.hp + 8 * dt); + } + if (nearCool) { + player.temperature += (15 - player.temperature) * dt * 1.5; + if (player.hp < 100 && player.temperature > HEAT_THRESHOLD) { + player.hp = Math.min(100, player.hp + 8 * dt); + } + } + if (isUndergroundTemp && !nearHeat) { + if (player.hp < 100 && player.temperature >= COLD_THRESHOLD && player.temperature <= HEAT_THRESHOLD) { + player.hp = Math.min(100, player.hp + 2 * dt); + } + } + const aboveBlk = getBlock(pGX, pGY - 1); + const inShade = aboveBlk && BLOCKS[aboveBlk.t] && BLOCKS[aboveBlk.t].solid && aboveBlk.t !== 'glass'; + if (inShade && !isUndergroundTemp && !nearCool) { + player.temperature -= 5 * dt; + if (player.temperature > HEAT_THRESHOLD - 2 && player.hp < 100) { + player.hp = Math.min(100, player.hp + 1 * dt); + } + } + if (player.temperature < COLD_THRESHOLD && !isUndergroundTemp) { + const severity = Math.abs(player.temperature - COLD_THRESHOLD) / 15; + player.hp -= 1 * severity * dt; + if (severity > 0.5) player.vx *= (1 - 0.3 * Math.min(1, severity) * dt); + } + if (player.temperature > HEAT_THRESHOLD && !isUndergroundTemp) { + const severity = (player.temperature - HEAT_THRESHOLD) / 15; + player.hp -= 1 * severity * dt; + if (severity > 0.5) player.hunger -= 0.5 * severity * dt; + } + // Игрок не может двигаться во время сна + if(player.sleeping){ + player.vx = 0; + player.vy = 0; + } else { + const dir = (inp.r?1:0) - (inp.l?1:0); + const speedMult = getWeatherSpeedMultiplier() * (player.slowTimer > 0 ? 0.4 : 1.0); + if(dir) player.vx = dir * MOVE * speedMult; + else player.vx *= 0.82; + } + + // Звук шагов при движении по земле + if(player.grounded && !player.inWater && Math.abs(player.vx) > 50){ + const stepInterval = 0.35; // Интервал между шагами в секундах + if(now/1000 - player.lastStepTime > stepInterval){ + playSound('step'); + player.lastStepTime = now/1000; + } + } + + // прыжок/плавание (новая логика) + if(player.inBoat){ + // Игрок в лодке - лодка следует за игроком + const dir = (inp.r?1:0) - (inp.l?1:0); + if(dir) boat.vx = dir * MOVE; + else boat.vx *= 0.95; + + // Лодка плавает на воде + boat.vy = 0; + + // Игрок следует за лодкой (сидит внутри неё) + player.x = boat.x + 2; // Игрок по центру лодки + player.y = boat.y - 4; // Игрок выше лодки (сидит внутри) + player.vx = boat.vx; + player.vy = boat.vy; + player.grounded = true; + player.inWater = false; // Игрок не в воде когда в лодке + + // Прыжок из лодки (высадка) + if(jumpPressed){ + // Возвращаем лодку в инвентарь + inv.boat = (inv.boat || 0) + 1; + + player.inBoat = false; + boat.active = false; + player.y += TILE; // Прыгаем из лодки + player.vy = -JUMP * 0.5; + playSound('splash'); + } + + } else if(player.inWater){ + // сопротивление в воде + player.vx *= 0.90; + player.vy *= 0.92; + + // Если не нажимаем прыжок - тонем (гравитация в воде) + if(!jumpPressed && !inp.j){ + // Применяем гравитацию в воде - игрок тонет + player.vy += GRAV_WATER * dt; + } else { + // Если нажимаем прыжок - поднимаемся на поверхность + if(jumpPressed){ + player.vy = Math.min(player.vy, -520); // рывок вверх + } else if(inp.j){ + // если держим — мягкое всплытие + player.vy = Math.min(player.vy, -260); + } + } + + } else { + // обычный прыжок (только по нажатию) + if(jumpPressed && player.grounded && !player.sleeping){ + player.vy = -JUMP; + player.grounded = false; + player.fallStartY = player.y; + } + } + + // Гравитация применяется только вне воды и вне лодки + if(!player.inWater && !player.inBoat){ + player.vy += GRAV*dt; + } + + // Обновляем позицию лодки + if(boat.active){ + boat.x += boat.vx * dt; + boat.y += boat.vy * dt; + + // Лодка не выходит за пределы воды + const boatGX = Math.floor(boat.x / TILE); + const boatGY = Math.floor(boat.y / TILE); + const below = getBlock(boatGX, boatGY + 1); + + if(!below || below.t !== 'water'){ + // Если лодка вышла из воды - выкидываем игрока + inv.boat = (inv.boat || 0) + 1; + player.inBoat = false; + boat.active = false; + player.y += TILE; + player.vy = -200; + playSound('splash'); + } + } + + // Проверяем, не доплыл ли игрок из лодки + if(player.inBoat && !boat.active){ + inv.boat = (inv.boat || 0) + 1; + player.inBoat = false; + player.y += TILE; + player.vy = -200; + playSound('splash'); + } + + // Sub-stepped physics: применяем движение мелкими шагами + for (let step = 0; step < steps; step++) { + player.y += player.vy*dt; + resolveY(player); + player.x += player.vx*dt; + resolveX(player); + } + + // Отправляем позицию на сервер (мультиплеер) + sendPlayerPosition(); + + // Обновляем физику воды + updateWaterPhysics(dt); + + // Погода и дождь + updateWeather(dt); + updateRain(dt); + + player.invuln = Math.max(0, player.invuln - dt); + if(player.slowTimer > 0) player.slowTimer = Math.max(0, player.slowTimer - dt); + + // Рост культур + for(const key of Object.keys(growthTimers)){ + const tile = growthTimers[key]; + if(tile.stage < 3){ + tile.growTimer -= dt; + if(tile.growTimer <= 0){ + tile.stage++; + tile.growTimer = 8 + Math.random()*6; + // Обновляем визуальный блок + const [gxStr, gyStr] = key.split(','); + const gx = parseInt(gxStr), gy = parseInt(gyStr); + const curBlock = getBlock(gx, gy); + if(curBlock && curBlock.t !== curBlock.t.replace(/_stage\d/, '_stage'+tile.stage)){ + // Вычисляем следующую стадию + const baseType = curBlock.t.replace(/_stage\d/, ''); + const nextType = baseType + '_stage' + tile.stage; + setBlock(gx, gy, nextType); + sendBlockChange(gx, gy, nextType, 'add'); + // Проверяем созрел ли + if(tile.stage >= 3) delete growthTimers[key]; + } + } + } + } + + // Voice position update + voicePosT += dt; + if(voicePosT > 0.5 && voiceSocket && voiceSocket.connected){ + voicePosT = 0; + voiceSocket.emit('voice_pos', { x: player.x, y: player.y }); + } + + // Furnace tick + tickFurnaces(dt); + + // Обновляем UI печи если открыта + if(currentFurnaceKey && Math.random() < 0.1){ + renderFurnaceUI(); + } + + // Projectile tick (стрелы) + for(let i = projectiles.length-1; i>=0; i--){ + const p = projectiles[i]; + p.x += p.vx * dt; + p.y += p.vy * dt; + p.vy += 400 * dt; // гравитация + p.life -= dt; + + // Столкновение с блоком + const gx = Math.floor(p.x / TILE); + const gy = Math.floor(p.y / TILE); + const blk = getBlock(gx, gy); + if(blk && !blk.dead && BLOCKS[blk.t] && BLOCKS[blk.t].solid){ + // Врезался в стену + if(p.owner === 'player' && Math.random() < 0.5) inv.arrow++; // подбираем ~50% + projectiles.splice(i, 1); + continue; + } + + // Столкновение с сущностью + if(p.owner === 'mob'){ + // Попал в игрока + if(p.x > player.x && p.x < player.x+player.w && p.y > player.y && p.y < player.y+player.h){ + if(player.invuln <= 0){ + player.hp -= calculateDamage(p.dmg); + player.invuln = 0.4; + player.vx += p.vx * 0.3; + player.vy -= 150; + playSound('hit1'); + } + projectiles.splice(i, 1); + continue; + } + } else { + // Попал в моба — check all mobs (client-authoritative) + const allArrowMobs = getAllMobs(); + for(let j = allArrowMobs.length - 1; j >= 0; j--){ + const m = allArrowMobs[j]; + if(m.dead) continue; + if(p.x > m.x && p.x < m.x+m.w && p.y > m.y && p.y < m.y+m.h){ + m.hp -= p.dmg; + m.vx += p.vx * 0.2; + m.vy -= 200; + // Server-spawned mob: emit arrow hit to server for relay + if(m.id !== undefined && isMultiplayer){ + socket.emit('mob_arrow_hit', { id: m.id, dmg: p.dmg, vx: p.vx }); + if(m.hp <= 0){ + socket.emit('mob_died', { id: m.id }); + } + } + if(m.hp <= 0){ + spawnDrops(m.x + m.w/2, m.y + m.h/2, m.kind); + // Remove from the correct array + if(m.id !== undefined){ + serverMobs.delete(m.id); + } else { + const localIdx = mobs.indexOf(m); + if(localIdx >= 0) mobs.splice(localIdx, 1); + } + rebuildHotbar(); + } + projectiles.splice(i, 1); + break; + } + } + } + + // Таймаут + if(p.life <= 0) projectiles.splice(i, 1); + } + + // TNT tick + for(const key of Array.from(activeTNT)){ + const b = grid.get(key); + if(!b || b.dead){ activeTNT.delete(key); continue; } + b.fuse -= dt; + if(b.fuse <= 0){ + explodeAt(b.gx,b.gy); + } + } + + // mobs spawn (с обеих сторон камеры) — только в одиночном режиме (MP mobs come from server events) + spawnT += dt; + if(!isMultiplayer && spawnT > 1.8 && getAllMobs().length < 30){ + spawnT = 0; + + // Выбираем сторону спавна (левая или правая) + const spawnLeft = Math.random() < 0.5; + const gx = spawnLeft + ? Math.floor((camX - 200)/TILE) + : Math.floor((camX + W + 200)/TILE); + + genColumn(gx); + const sgy = surfaceGyAt(gx); + const wx = gx*TILE + 4; + const wy = (sgy-1)*TILE; // 1 блок выше поверхности + + // не спавнить в воде + const top = getBlock(gx, sgy); + if(top && top.t==='water') { + // skip + } else { + const night = isNight(); + if(night){ + // Ночью спавним враждебных мобов (максимум 12 хостайл) + const hostileCount = mobs.filter(m => m.kind==='zombie'||m.kind==='creeper'||m.kind==='skeleton').length; + if(hostileCount < 12){ + const rand = Math.random(); + if(rand < 0.35){ + mobs.push(new Zombie(wx, wy)); + } else if(rand < 0.55){ + mobs.push(new Creeper(wx, wy)); + } else { + mobs.push(new Skeleton(wx, wy)); + } + } + } + // Животные спавнятся и днём и ночью (с лимитом) + const animalCount = mobs.filter(m => m.kind==='pig'||m.kind==='chicken').length; + if(animalCount < 8){ + mobs.push(Math.random()<0.5 ? new Pig(wx, wy) : new Chicken(wx, wy)); + } + } + } + + // mobs update — mobAI runs on ALL mobs (client-authoritative for server-spawned mobs too) + { + // Local mobs + for(let i=mobs.length-1;i>=0;i--){ + const m = mobs[i]; + mobAI(m, dt); + if(m.hp<=0) mobs.splice(i,1); + } + // Server-spawned mobs: server is authoritative for positions + // mobAI does NOT run on serverMobs — server handles all physics + AI + // Only remove dead mobs from local map + if(isMultiplayer){ + for (const [id, sm] of serverMobs) { + if(sm.hp <= 0){ + sm.dead = true; + } + } + for (const [id, sm] of serverMobs) { + if(sm.dead) serverMobs.delete(id); + } + } + } + + // particles + for(let i=parts.length-1;i>=0;i--){ + const p = parts[i]; + p.t -= dt; + p.x += p.vx*dt; + p.y += p.vy*dt; + p.vy += GRAV*dt; + if(p.t <= 0) parts.splice(i,1); + } + + // death + if(player.hp <= 0){ + deathEl.style.display='flex'; + } else if(deathEl.style.display === 'flex') { + // Если HP > 0 но экран смерти всё ещё показан - скрываем его + deathEl.style.display='none'; + } + + // render + const night = isNight(); + + // sky + ctx.fillStyle = night ? '#070816' : (isRaining ? '#6B7B8D' : '#87CEEB'); + ctx.fillRect(0,0,W,H); + + // clouds (parallax x/y) + ctx.save(); + ctx.translate(-camX*0.5, -camY*0.15); + ctx.fillStyle = 'rgba(255,255,255,0.65)'; + for(const c of clouds){ + ctx.fillRect(c.x, c.y, c.w, 26); + ctx.fillRect(c.x+20, c.y-10, c.w*0.6, 22); + } + ctx.restore(); + + // world + ctx.save(); + ctx.translate(-camX, -camY); + + const minGX = Math.floor(camX/TILE)-2; + const maxGX = Math.floor((camX+W)/TILE)+2; + const minGY = Math.floor(camY/TILE)-6; + const maxGY = Math.floor((camY+H)/TILE)+6; + + // draw blocks (по массиву, но фильтруем диапазоном) + for(const b of blocks){ + if(b.dead) continue; + if(b.gx < minGX || b.gx > maxGX || b.gy < minGY || b.gy > maxGY) continue; + + const def = BLOCKS[b.t]; + if(def.alpha){ + ctx.save(); + ctx.globalAlpha = def.alpha; + ctx.drawImage(tex[b.t], b.gx*TILE, b.gy*TILE, TILE, TILE); + ctx.restore(); + } else { + ctx.drawImage(tex[b.t], b.gx*TILE, b.gy*TILE, TILE, TILE); + } + + // TNT мигает, если активирован + if(b.t==='tnt' && b.active && Math.sin(now/60)>0){ + ctx.fillStyle='rgba(255,255,255,0.45)'; + ctx.fillRect(b.gx*TILE, b.gy*TILE, TILE, TILE); + } + + // огонь костра + if(b.t==='campfire'){ + drawFire(b.gx*TILE, b.gy*TILE, now); + } + // Печь — огонь когда обжигает + if(b.t==='furnace' && activeFurnaces.has(`${b.gx},${b.gy}`)){ + drawFire(b.gx*TILE + 8, b.gy*TILE + 5, now); + } + } + + // mobs + const allMobsRender = getAllMobs(); + for(const m of allMobsRender){ + if(m.kind==='zombie'){ + ctx.fillStyle = '#2ecc71'; + ctx.fillRect(m.x, m.y, m.w, m.h); + ctx.fillStyle = '#c0392b'; + ctx.fillRect(m.x+6, m.y+12, 6,6); + ctx.fillRect(m.x+22, m.y+12, 6,6); + } else if(m.kind==='pig'){ + ctx.fillStyle = '#ffb6c1'; + ctx.fillRect(m.x, m.y, m.w, m.h); + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+22, m.y+5, 3,3); + ctx.fillStyle = '#ff69b4'; + ctx.fillRect(m.x+28, m.y+12, 6,6); + } else if(m.kind==='chicken'){ + // chicken + ctx.fillStyle = '#ecf0f1'; + ctx.fillRect(m.x, m.y, m.w, m.h); + ctx.fillStyle = '#f39c12'; + ctx.fillRect(m.x+18, m.y+10, 6,4); + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+8, m.y+6, 3,3); + } else if(m.kind==='creeper'){ + // creeper + ctx.fillStyle = '#4CAF50'; + ctx.fillRect(m.x, m.y, m.w, m.h); + // Глаза + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+8, m.y+8, 4,4); + ctx.fillRect(m.x+22, m.y+8, 4,4); + // Рот + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+12, m.y+20, 10,4); + // Ноги + ctx.fillStyle = '#4CAF50'; + ctx.fillRect(m.x+4, m.y+30, 6,20); + ctx.fillRect(m.x+24, m.y+30, 6,20); + } else if(m.kind==='skeleton'){ + // skeleton - детализированный + // Тело + ctx.fillStyle = '#ECEFF1'; + ctx.fillRect(m.x+10, m.y+20, 14, 12); + // Череп + ctx.fillRect(m.x+8, m.y+0, 18, 18); + // Глазницы + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+10, m.y+6, 4,4); + ctx.fillRect(m.x+20, m.y+6, 4,4); + // Нос + ctx.fillRect(m.x+15, m.y+12, 4,2); + // Руки + ctx.fillStyle = '#ECEFF1'; + ctx.fillRect(m.x+2, m.y+20, 6,14); + ctx.fillRect(m.x+26, m.y+20, 6,14); + // Ноги + ctx.fillRect(m.x+10, m.y+32, 6, 18); + ctx.fillRect(m.x+18, m.y+32, 6, 18); + // Лук в руке + ctx.save(); + ctx.translate(m.x + 30, m.y + 22); + ctx.strokeStyle = '#8B4513'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(0, 0, 8, -Math.PI*0.7, Math.PI*0.7); + ctx.stroke(); + // Тетива + ctx.strokeStyle = '#ccc'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(8*Math.cos(-Math.PI*0.7), 8*Math.sin(-Math.PI*0.7)); + ctx.lineTo(8*Math.cos(Math.PI*0.7), 8*Math.sin(Math.PI*0.7)); + ctx.stroke(); + ctx.restore(); + } else if(m.kind==='scorpion') { + // скорпион — оранжево-коричневый + ctx.fillStyle = '#d35400'; + ctx.fillRect(m.x+2, m.y+4, m.w-4, m.h-8); + ctx.fillStyle = '#c0392b'; + ctx.fillRect(m.x+m.w-4, m.y-6, 4, 10); + ctx.fillRect(m.x+m.w-2, m.y-10, 3, 5); + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+4, m.y+6, 3, 3); + ctx.fillRect(m.x+m.w-7, m.y+6, 3, 3); + ctx.fillStyle = '#d35400'; + ctx.fillRect(m.x-4, m.y+8, 6, 4); + ctx.fillRect(m.x+m.w-2, m.y+8, 6, 4); + } else if(m.kind==='polar_bear') { + ctx.fillStyle = '#ecf0f1'; + ctx.fillRect(m.x+2, m.y+4, m.w-4, m.h-4); + ctx.fillRect(m.x+8, m.y-2, m.w-16, 10); + ctx.fillStyle = '#bdc3c7'; + ctx.fillRect(m.x+m.w/2-3, m.y+2, 6, 4); + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+12, m.y+1, 3, 3); + ctx.fillRect(m.x+m.w-14, m.y+1, 3, 3); + ctx.fillStyle = '#ecf0f1'; + ctx.fillRect(m.x+8, m.y-4, 4, 4); + ctx.fillRect(m.x+m.w-12, m.y-4, 4, 4); + } else if(m.kind==='slime') { + const bounce = Math.sin(performance.now()/200)*3; + ctx.fillStyle = 'rgba(46,204,113,0.8)'; + ctx.fillRect(m.x-1, m.y-1+bounce, m.w+2, m.h+2); + ctx.fillStyle = 'rgba(39,174,96,0.9)'; + ctx.fillRect(m.x+1, m.y+1+bounce, m.w-2, m.h-2); + ctx.fillStyle = '#fff'; + ctx.fillRect(m.x+4, m.y+6+bounce, 6, 6); + ctx.fillRect(m.x+m.w-10, m.y+6+bounce, 6, 6); + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+6, m.y+8+bounce, 3, 3); + ctx.fillRect(m.x+m.w-8, m.y+8+bounce, 3, 3); + } else if(m.kind==='eagle') { + ctx.fillStyle = '#8B4513'; + ctx.fillRect(m.x+8, m.y+6, m.w-16, m.h-10); + const wingY = Math.sin(performance.now()/150)*4; + ctx.fillRect(m.x-6, m.y+4+wingY, 16, 6); + ctx.fillRect(m.x+m.w-10, m.y+4-wingY, 16, 6); + ctx.fillStyle = '#fff'; + ctx.fillRect(m.x+m.w/2-3, m.y+2, 6, 6); + ctx.fillStyle = '#f39c12'; + ctx.fillRect(m.x+m.w/2-1, m.y+6, 4, 3); + ctx.fillStyle = '#000'; + ctx.fillRect(m.x+m.w/2-1, m.y+3, 2, 2); + } + } + + // boat (рисуем первой, чтобы игрок был внутри неё) + if(boat.active){ + ctx.drawImage(tex['boat'], boat.x - (TILE-boat.w)/2, boat.y - (TILE-boat.h)/2, TILE, TILE); + } + + // Lerp other players towards target positions (smooth movement) + for(const [socketId, p] of otherPlayers){ + if (p.tx !== undefined) { + p.x += (p.tx - p.x) * 0.25; + p.y += (p.ty - p.y) * 0.25; + } + } + // Claim borders (приватные зоны) + if (claims.length > 0) { + for (const claim of claims) { + const isOwn = claim.owner_name === playerName; + const colorIdx = claims.indexOf(claim) % 5; + const borderColors = ["rgba(231,76,60,0.6)", "rgba(52,152,219,0.6)", "rgba(46,204,113,0.6)", "rgba(155,89,182,0.6)", "rgba(241,196,15,0.6)"]; + const fillColors = ["rgba(231,76,60,0.06)", "rgba(52,152,219,0.06)", "rgba(46,204,113,0.06)", "rgba(155,89,182,0.06)", "rgba(241,196,15,0.06)"]; + const borderColor = isOwn ? "rgba(46,204,113,0.7)" : borderColors[colorIdx]; + const fillColor = isOwn ? "rgba(46,204,113,0.06)" : fillColors[colorIdx]; + + const left = (claim.gx - claim.radius) * TILE - camX; + const top_ = (claim.gy - claim.radius) * TILE - camY; + const w = (claim.radius * 2 + 1) * TILE; + const h = (claim.radius * 2 + 1) * TILE; + + ctx.fillStyle = fillColor; + ctx.fillRect(left, top_, w, h); + ctx.strokeStyle = borderColor; + ctx.lineWidth = 2; + ctx.setLineDash([8, 4]); + ctx.strokeRect(left, top_, w, h); + ctx.setLineDash([]); + + // Owner label at top of claim + if (claim.owner_name) { + ctx.fillStyle = isOwn ? "#2ecc71" : "#e74c3c"; + ctx.font = "12px monospace"; + ctx.textAlign = "center"; + ctx.fillText((isOwn ? "🏠 " : "⛔ ") + claim.owner_name, left + w/2, top_ + 14); + ctx.textAlign = "start"; + } + } + } + + // other players (multiplayer) + for(const [socketId, p] of otherPlayers){ + if(heroImg.complete){ + ctx.drawImage(heroImg, p.x - (TILE-player.w)/2, p.y - (TILE-player.h)/2, TILE, TILE); + } else { + ctx.fillStyle = p.color; + ctx.fillRect(p.x, p.y, 34, 34); + } + // Имя игрока (мелко над персонажем) + ctx.fillStyle = '#fff'; + ctx.font = '12px system-ui'; + ctx.textAlign = 'center'; + ctx.fillText(p.name, p.x + 17, p.y - 8); + } + + // player + if(heroImg.complete){ + ctx.drawImage(heroImg, player.x - (TILE-player.w)/2, player.y - (TILE-player.h)/2, TILE, TILE); + } else { + ctx.fillStyle='#fff'; + ctx.fillRect(player.x, player.y, player.w, player.h); + } + + // projectiles (стрелы) + for(const p of projectiles){ + const angle = Math.atan2(p.vy, p.vx); + ctx.save(); + ctx.translate(p.x, p.y); + ctx.rotate(angle); + ctx.fillStyle = p.owner === 'mob' ? '#c0392b' : '#f1c40f'; + ctx.fillRect(-12, -1.5, 24, 3); + // наконечник + ctx.beginPath(); + ctx.moveTo(12, -4); + ctx.lineTo(16, 0); + ctx.lineTo(12, 4); + ctx.closePath(); + ctx.fill(); + // оперение + ctx.fillStyle = '#888'; + ctx.fillRect(-12, -3, 4, 2); + ctx.fillRect(-12, 1, 4, 2); + ctx.restore(); + } + + // particles + for(const p of parts){ + ctx.fillStyle = p.c; + ctx.fillRect(p.x-2, p.y-2, 4, 4); + } + + // Стрелы скелета + for(const m of mobs){ + if(m.kind==='skeleton' && m.shootCooldown > 0.5){ + // Рисуем стрелу + const arrowX = m.x + m.w/2; + const arrowY = m.y + 15; + const targetX = player.x + player.w/2; + const targetY = player.y + player.h/2; + const angle = Math.atan2(targetY - arrowY, targetX - arrowX); + const speed = 400; + + // Проверяем, попала ли стрела + const dx = targetX - arrowX; + const dy = targetY - arrowY; + const dist = Math.hypot(dx, dy); + + // Рисуем стрелу + ctx.save(); + ctx.translate(arrowX, arrowY); + ctx.rotate(angle); + ctx.fillStyle = '#ECEFF1'; + ctx.fillRect(0, -1, 16, 2); + ctx.restore(); + + // Урон игроку если попали + if(dist < 150 && player.invuln <= 0){ + player.hp -= 8; + player.invuln = 0.5; + player.vx += Math.cos(angle) * 300; + player.vy -= 200; + playSound('hit1'); + } + } + } + + // build ghost + if(mode()==='build' && mouse.x!==null && !craftOpen && player.hp>0){ + const wx = mouse.x + camX; + const wy = mouse.y + camY; + const gx = Math.floor(wx/TILE); + const gy = Math.floor(wy/TILE); + ctx.strokeStyle = 'rgba(255,255,255,0.9)'; + ctx.lineWidth = 2; + ctx.strokeRect(gx*TILE, gy*TILE, TILE, TILE); + } + + ctx.restore(); + + // lighting overlay at night (Minecraft-style: offscreen lightmap, wall occlusion, warm flicker) + if(night){ + // 1) Рисуем тёмный оверлей на offscreen canvas + lightC.width = W*dpr; + lightC.height = H*dpr; + lightCtx.setTransform(dpr,0,0,dpr,0,0); + lightCtx.fillStyle = 'rgba(0,0,12,0.82)'; + lightCtx.fillRect(0,0,W,H); + + // 2) Вырезаем «окна света» через destination-out — свет не проходит сквозь стены + lightCtx.globalCompositeOperation = 'destination-out'; + + // Функция: рисуем мягкий луч света с затуханием за стенами + function castLight(sx, sy, radius) { + const flick = 0.97 + Math.sin(now/300 + sx*0.01)*0.015 + Math.sin(now/470 + sy*0.02)*0.015; + const r = radius * flick; + // 24 луча — мягкий круглый свет + const steps = 24; + // Собираем дистанции до стен по лучам + const dists = new Float32Array(steps); + for(let i=0; i TILE*0.3){ + maxDist = step; + break; + } + } + dists[i] = maxDist; + } + // Рисуем сглаженный полигон по dists + const cx = sx-camX, cy = sy-camY; + // Центр: яркая точка + const maxR = Math.max(...dists); + const grad = lightCtx.createRadialGradient(cx, cy, 0, cx, cy, maxR); + grad.addColorStop(0, 'rgba(255,255,255,1)'); + grad.addColorStop(0.4, 'rgba(255,255,255,0.8)'); + grad.addColorStop(1, 'rgba(255,255,255,0)'); + lightCtx.fillStyle = grad; + // Рисуем shape по dists (звездоподобный полигон) + lightCtx.beginPath(); + for(let i=0; i<=steps; i++){ + const idx = i % steps; + const nextIdx = (i+1) % steps; + const avgD = (dists[idx] + dists[nextIdx]) / 2; + const angle = (idx/steps) * Math.PI * 2; + const px = cx + Math.cos(angle) * dists[idx]; + const py = cy + Math.sin(angle) * dists[idx]; + if(i===0) lightCtx.moveTo(px, py); + else lightCtx.lineTo(px, py); + } + lightCtx.closePath(); + lightCtx.fill(); + } + + // Источники света + for(const b of blocks){ + if(b.dead) continue; + if(b.gx < minGX-5 || b.gx > maxGX+5 || b.gy < minGY-5 || b.gy > maxGY+5) continue; + const def = BLOCKS[b.t]; + if(def.lightRadius){ + castLight(b.gx*TILE + TILE/2, b.gy*TILE + TILE/2, def.lightRadius); + } + } + + // 3) Накладываем lightmap на основной canvas + lightCtx.globalCompositeOperation = 'source-over'; + ctx.drawImage(lightC, 0, 0, W, H); + + // 4) Тёплый оверлей от источников света (additive, мягкий) + ctx.save(); + ctx.globalCompositeOperation = 'lighter'; + for(const b of blocks){ + if(b.dead) continue; + if(b.gx < minGX-3 || b.gx > maxGX+3 || b.gy < minGY-3 || b.gy > maxGY+3) continue; + const def = BLOCKS[b.t]; + if(def.lightRadius){ + const flick = 0.9 + Math.sin(now/350 + b.gx*3.7)*0.05 + Math.sin(now/530 + b.gy*2.3)*0.05; + const wx = b.gx*TILE + TILE/2 - camX; + const wy = b.gy*TILE + TILE/2 - camY; + const r = def.lightRadius * 0.85; + const grad = ctx.createRadialGradient(wx,wy, 0, wx,wy, r); + grad.addColorStop(0, `rgba(255,180,80,0.28)`); + grad.addColorStop(0.5, `rgba(255,140,40,0.13)`); + grad.addColorStop(1, 'rgba(255,100,20,0)'); + ctx.fillStyle = grad; + ctx.beginPath(); + ctx.arc(wx, wy, r, 0, Math.PI*2); + ctx.fill(); + } + } + ctx.restore(); + } + + // Дождь (после ночного оверлея) + drawRain(); + if(Math.random()<0.25){ + hpEl.textContent = Math.max(0, Math.ceil(player.hp)); + foodEl.textContent = Math.ceil(player.hunger); + document.getElementById('o2').textContent = Math.ceil(player.o2); + sxEl.textContent = Math.floor(player.x/TILE); + syEl.textContent = Math.floor(player.y/TILE); + todEl.textContent = night ? 'Ночь' : 'День'; + document.getElementById('xplevel').textContent = player.level; + const lvXpNext = xpForLevel(player.level + 1); + const lvXpCur = xpForLevel(player.level); + const xpInLevel = player.xp - lvXpCur; + const xpNeeded = lvXpNext - lvXpCur; + document.getElementById('xpbar').textContent = xpInLevel + '/' + xpNeeded; + tempEl.textContent = Math.round(player.temperature) + "°C"; + worldIdEl.textContent = worldId; + if(isMultiplayer){ + document.getElementById('multiplayerStatus').style.display = 'flex'; + playerCountEl.textContent = otherPlayers.size + 1; // +1 = мы сами + } else { + document.getElementById('multiplayerStatus').style.display = 'none'; + } + } + + // Индикатор сна + if(player.sleeping){ + ctx.fillStyle = 'rgba(0,0,0,0.7)'; + ctx.fillRect(0, 0, W, H); + ctx.fillStyle = '#fff'; + ctx.font = 'bold 32px system-ui'; + ctx.textAlign = 'center'; + ctx.fillText('💤 Спим...', W/2, H/2); + ctx.font = '18px system-ui'; + ctx.fillText('Нажмите на кровать чтобы проснуться', W/2, H/2 + 40); + } + + // Рисуем дропы + // Температурный оверлей (заморозка/тепловой удар) + if (typeof player.temperature !== 'undefined' && (player.temperature < -5 || player.temperature > 35)) { + const severity = player.temperature < -5 + ? Math.abs(player.temperature + 5) / 20 + : (player.temperature - 35) / 20; + const alpha = Math.min(0.35, severity * 0.15); + if (player.temperature < -5) { + ctx.fillStyle = 'rgba(100,150,255,' + alpha + ')'; + } else { + ctx.fillStyle = 'rgba(255,100,50,' + alpha + ')'; + } + ctx.fillRect(0, 0, W, H); + } + drawDrops(ctx); + // Пикап дропов + pickupDrops(); + // Popup уровня + drawLevelUpPopup(ctx); + + // Миникарта (обновляем раз в ~4 кадра для оптимизации) + if(minimapOpen && Math.random() < 0.25){ + renderMinimap(); + } + + requestAnimationFrame(loop); + } + + requestAnimationFrame(loop); +})(); diff --git a/game3d.js b/game3d.js new file mode 100644 index 0000000..518e447 --- /dev/null +++ b/game3d.js @@ -0,0 +1,6757 @@ +// GrechkaCraft — auto-bundled from ES modules +(function() { +"use strict"; + +// === core/constants.js === +// ==================== КОНСТАНТЫ ИГРЫ ==================== + +const TILE = 40; + +// Мир +const SEA_GY = 14; // уровень воды (gy) - уменьшил для меньших водоёмов +const BEDROCK_GY = 140; // глубина бедрока (gy), чем больше - тем глубже +const GEN_MARGIN_X = 40; // запас генерации по X (в тайлах) — увеличен для плавной прогрузки + +// Физика +const GRAV = 2200; +const GRAV_WATER = 550; +const MOVE = 320; +const JUMP = 760; + +// День/ночь +const DAY_LEN = 360; // замедлил смену дня/ночи в 3 раза + +// Сохранение +const SAVE_KEY = 'minegrechka_save'; + +// Сеть +const MOVE_SEND_INTERVAL = 0.05; // 50ms = 20 раз в секунду + +// Погода +const MAX_RAINDROPS = 200; + +// Чат +const MAX_CHAT_MESSAGES = 50; + +// Голосовой чат +const VOICE_SERVER = 'wss://voicegrech.mkn8n.ru'; + +// === core/state.js === +// Все мутабельные переменные игры в одном объекте состояния +const state = { + // Константы (для доступа из модулей) + TILE, + SEA_GY, + BEDROCK_GY, + GEN_MARGIN_X, + GRAV, + GRAV_WATER, + MOVE, + JUMP, + DAY_LEN, + SAVE_KEY, + MOVE_SEND_INTERVAL, + MAX_RAINDROPS, + MAX_CHAT_MESSAGES, + VOICE_SERVER, + + // Камера + camX: 0, + camY: 0, + W: 0, + H: 0, + dpr: 1, + + // Canvas/DOM (заполняется в main.js) + gameEl: null, + canvas: null, + ctx: null, + lightC: null, + lightCtx: null, + + // День/ночь + worldTime: 0.30, // старт в полдень (0 = рассвет, 0.5+ = ночь) + isNightTime: false, + dayNightCycle: true, + + // Мультиплеер + isMultiplayer: false, + mySocketId: null, + socket: null, + voiceSocket: null, + voicePosT: 0, + + // Сервер + SERVER_URL: '', + TELEGRAM_BOT_USERNAME: '', + TELEGRAM_APP_SHORT_NAME: '', + + // Игрок + playerName: null, + worldId: null, + + // Инвентарь/UI + selected: "dirt", + showFullInventory: false, + craftOpen: false, + inventoryOpen: false, + chatOpen: false, + minimapOpen: false, + modeIdx: 0, + currentFurnaceKey: null, + + // Мир + blocksVersion: 0, + worldSeed: Math.floor(Math.random() * 1000000), + + // Погода + weatherType: "clear", + isRaining: false, + rainIntensity: 0, + weatherTimer: 0, + weatherChangeInterval: 60 + Math.random() * 120, + snowflakes: [], + lightningFlash: 0, + lightningTimer: 0, + + // Мобы/спавн + spawnT: 0, + + // Цикл + last: 0, + prevJump: false, + + // Сеть — throttle отправки позиции + lastMoveSendTime: 0, + lastSentX: 0, + lastSentY: 0, + + // Mining progress + mining: { gx: null, gy: null, progress: 0, hardness: 0 }, + + // Trade/Market + myTgId: null, + tradePanel: null, + tradeContent: null, + marketPanel: null, + marketContent: null, + + // DOM-ссылки (заполняются в main.js) + hpEl: null, + foodEl: null, + sxEl: null, + syEl: null, + todEl: null, + worldIdEl: null, + playerCountEl: null, + deathEl: null, + hotbarEl: null, + craftPanel: null, + recipesEl: null, + inventoryPanel: null, + inventoryGrid: null, + multiplayerStatus: null, + tempEl: null, + + // Игрок + player: { + x: 6 * TILE, + y: 0 * TILE, + z: 0, + w: 34, + h: 34, + d: 34, + vx: 0, + vy: 0, + vz: 0, + grounded: false, + inWater: false, + headInWater: false, + hp: 100, + hunger: 100, + o2: 100, + invuln: 0, + fallStartY: 0, + lastStepTime: 0, + sleeping: false, + inBoat: false, + armor: 0, + equippedArmor: null, + temperature: 15, + slowTimer: 0 + }, + + // Точка спавна + spawnPoint: { x: 6 * TILE, y: 0 * TILE }, + + // Инвентарь + inv: { + dirt: 6, stone: 0, sand: 0, gravel: 0, clay: 0, + wood: 0, planks: 0, ladder: 0, leaves: 0, coal: 0, + copper_ore: 0, iron_ore: 0, gold_ore: 0, diamond_ore: 0, + brick: 0, glass: 0, + tnt: 1, campfire: 0, torch: 0, + meat: 0, cooked: 0, arrow: 0, + wood_pickaxe: 0, stone_pickaxe: 0, iron_pickaxe: 0, + wood_sword: 0, stone_sword: 0, iron_sword: 0, + wood_axe: 0, stone_axe: 0, iron_axe: 0, + wood_shovel: 0, stone_shovel: 0, iron_shovel: 0, + iron_armor: 0, gold_armor: 0, + bow: 0, furnace: 0, + bed: 0, boat: 0, + iron_ingot: 0, gold_ingot: 0, copper_ingot: 0, + cobblestone: 0, + // New items + chicken_meat: 0, feather: 0, bone: 0, gunpowder: 0, + wheat: 0, bread: 0, carrot: 0, potato: 0, baked_potato: 0, + mushroom_stew: 0, + scorpion_stinger: 0, polar_fur: 0, slime_ball: 0, eagle_feather: 0, + snow: 0, ice: 0, cactus: 0, mushroom: 0, moss: 0, farmland: 0, + spruce_leaves: 0, dead_bush: 0, swamp_water: 0, + wheat_seeds: 0, wheat_stage0: 0, carrot_stage0: 0, potato_stage0: 0 + }, + + // Лодка + boat: { + x: 0, y: 0, + w: 34, h: 34, + vx: 0, vy: 0, + active: false, + inWater: false + }, + + // Ввод + inp: { up: false, down: false, left: false, right: false, jump: false, mine: false, build: false, bow: false, l: false, r: false, j: false, s: false }, + + // Мышь + mouse: { x: 0, y: 0 }, + + // Другие игроки (MP) + otherPlayers: new Map(), + + // Серверные мобы (MP) + serverMobs: new Map(), + + // Мобы + mobs: [], + + // Снаряды + projectiles: [], + + // Отслеживание изменений мира + placedBlocks: [], + removedBlocks: [], + + // Серверные изменения + serverOverrides: new Map(), + + // Чат + chatMessages: [], + + // Погода — капли + raindrops: [], + + // Облака + clouds: Array.from({ length: 10 }, () => ({ + x: Math.random() * 2000, + y: -200 - Math.random() * 260, + w: 80 + Math.random() * 120, + s: 12 + Math.random() * 20 + })), + + // Частицы + parts: [], + + // Активный TNT + activeTNT: new Set(), + + // Прочность инструментов + toolDurability: new Map(), + protections: new Map(), + + // Последние выбранные предметы + recentItems: [], + + // Активные печи + activeFurnaces: new Map(), + cropTimers: new Map(), + + // Сгенерированные колонны + generated: new Set(), + + // Изображение героя + heroImg: new Image(), + + // Сетка мира + grid: new Map(), + blocks: [], + + // In-memory save fallback + inMemorySave: null, +}; + +// === config.js === +// ==================== КОНФИГУРАЦИЯ СЕРВЕРА ==================== +// This module only sets SERVER_URL. Player name and world ID are handled in main.js +// to avoid duplicate prompts. + +const urlParams = new URLSearchParams(window.location.search); +const SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru'; + +// Защита от mixed content +if (location.protocol === 'https:' && SERVER_URL.startsWith('http://')) { + console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP'); + alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.'); +} + +// === ui/start-menu.js === +// ==================== START MENU ==================== + + +// Detect Telegram WebApp +const isTgWebApp = !!(window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData); +let tgUser = null; +let tgSaveLoaded = false; + +if (isTgWebApp) { + try { + Telegram.WebApp.ready(); + Telegram.WebApp.expand(); + tgUser = Telegram.WebApp.initDataUnsafe?.user || null; + console.log('[TG] Mini App detected, user:', tgUser?.id, tgUser?.username); + // Set player name from TG + if (tgUser && tgUser.first_name) { + const tgName = tgUser.username || tgUser.first_name; + localStorage.setItem('minegrechka_playerName', tgName); + } + } catch (e) { + console.warn('[TG] WebApp init error:', e); + } +} + +// Export TG info for other modules + + +// ==================== HELPERS ==================== + +function generateShortWorldCode() { + // Generate readable 6-char code: consonants + digits (easy to copy/share) + const c = 'bcdfghjkmnpqrstvwxyz'; + const d = '23456789'; + let code = ''; + for (let i = 0; i < 3; i++) code += c[Math.floor(Math.random() * c.length)]; + for (let i = 0; i < 3; i++) code += d[Math.floor(Math.random() * d.length)]; + return code; +} + +function showToast(msg, duration) { + duration = duration || 1800; + const t = document.createElement('div'); + t.textContent = msg; + t.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#2ecc71;padding:10px 24px;border-radius:10px;font:bold 16px Arial,sans-serif;z-index:99999;pointer-events:none;transition:opacity 0.4s;'; + document.body.appendChild(t); + setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 400); }, duration); +} + +function customAlert(msg) { + const overlay = document.createElement('div'); + overlay.className = 'custom-modal-overlay'; + overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:100000;'; + const box = document.createElement('div'); + box.style.cssText = 'background:#C6C6C6;border:4px solid #000;padding:24px;max-width:360px;width:90%;box-shadow:4px 4px 0 #000;font-family:Courier New,monospace;'; + const text = document.createElement('div'); + text.textContent = msg; + text.style.marginBottom = '16px'; + text.style.fontWeight = 'bold'; + text.style.fontSize = '16px'; + const btn = document.createElement('button'); + btn.textContent = 'OK'; + btn.style.cssText = 'padding:8px 24px;font-family:Courier New,monospace;font-weight:bold;cursor:pointer;border:3px solid #000;background:#5A8F3C;color:#fff;box-shadow:2px 2px 0 #000;'; + btn.onclick = () => overlay.remove(); + box.appendChild(text); + box.appendChild(btn); + overlay.appendChild(box); + document.body.appendChild(overlay); +} + +// ==================== MAIN START MENU ==================== + +// Minecraft-style CSS for the start menu +const mcCSS = ` + .mc-overlay { position:fixed;top:0;left:0;width:100%;height:100%;z-index:99999; + display:flex;align-items:center;justify-content:center;font-family:'Courier New',monospace; + image-rendering:pixelated; } + .mc-bg { position:absolute;inset:0;background:#000;opacity:0.75; } + .mc-dirt { position:absolute;bottom:0;left:0;right:0;height:40%; + background:repeating-linear-gradient(0deg,#8B6914 0px,#8B6914 2px,#7A5B12 2px,#7A5B12 4px);opacity:0.3; } + .mc-box { position:relative;z-index:1;background:#C6C6C6;border:4px solid #000; + padding:0;max-width:420px;width:92%;box-shadow:4px 4px 0 #000;max-height:90vh;display:flex;flex-direction:column; } + .mc-titlebar { background:#1a1a2e;padding:12px 16px;text-align:center; + border-bottom:4px solid #000; } + .mc-title { font-size:24px;font-weight:bold;color:#FFAA00;text-shadow:2px 2px 0 #000; + letter-spacing:2px; } + .mc-subtitle { font-size:13px;color:#AAA;margin-top:4px; } + .mc-body { padding:16px;background:#C6C6C6;max-height:80vh;overflow-y:auto;-webkit-overflow-scrolling:touch; } + .mc-btn { display:block;width:100%;padding:10px 16px;margin-bottom:8px; + font-family:'Courier New',monospace;font-size:16px;font-weight:bold; + cursor:pointer;text-align:center;border:3px solid #000; + image-rendering:pixelated;box-shadow:3px 3px 0 #000; + transition:transform 0.05s,box-shadow 0.05s; } + .mc-btn:active { transform:translate(2px,2px);box-shadow:1px 1px 0 #000; } + .mc-btn-green { background:#5A8F3C;color:#E0E0E0;border-color:#3A5F1C;text-shadow:1px 1px 0 #222; } + .mc-btn-green:hover { background:#6DAF4E; } + .mc-btn-blue { background:#3C5F9E;color:#E0E0E0;border-color:#2A4070;text-shadow:1px 1px 0 #222; } + .mc-btn-blue:hover { background:#4B73BE; } + .mc-btn-purple { background:#7B3FA0;color:#E0E0E0;border-color:#5A2C78;text-shadow:1px 1px 0 #222; } + .mc-btn-purple:hover { background:#9555BE; } + .mc-btn-orange { background:#B86E00;color:#E0E0E0;border-color:#8A5200;text-shadow:1px 1px 0 #222; } + .mc-btn-orange:hover { background:#DA8500; } + .mc-btn-red { background:#9E2B2B;color:#E0E0E0;border-color:#6E1B1B;text-shadow:1px 1px 0 #222; } + .mc-btn-red:hover { background:#BE3B3B; } + .mc-btn-gray { background:#555;color:#AAA;border-color:#333;text-shadow:1px 1px 0 #111; } + .mc-btn-gray:hover { background:#666;color:#DDD; } + .mc-btn-outline { background:transparent;color:#555;border-color:#555; + text-shadow:none;box-shadow:2px 2px 0 #333; } + .mc-btn-outline:hover { background:#DDD;color:#222; } + .mc-input { width:100%;padding:8px 12px;font-family:'Courier New',monospace; + font-size:16px;background:#FFF;border:3px solid #000;color:#222; + box-shadow:inset 2px 2px 0 #999;margin:4px 0;box-sizing:border-box; } + .mc-input:focus { outline:none;border-color:#FFAA00; } + .mc-label { font-size:13px;color:#444;font-weight:bold;margin:8px 0 4px; } + .mc-divider { border:none;border-top:2px solid #999;margin:12px 0; } + .mc-friend-row { display:flex;justify-content:space-between;align-items:center; + padding:6px 8px;margin-bottom:4px;background:#E8E8E8;border:2px solid #999; + box-shadow:2px 2px 0 #999; } + .mc-friend-btn { padding:4px 10px;font-family:'Courier New',monospace; + font-size:13px;font-weight:bold;cursor:pointer;border:2px solid #000; + box-shadow:1px 1px 0 #000; } + .mc-friend-btn:active { transform:translate(1px,1px);box-shadow:none; } + .mc-friend-btn-green { background:#5A8F3C;color:#E0E0E0; } + .mc-friend-btn-red { background:#9E2B2B;color:#E0E0E0; } + .mc-checkbox-row { display:flex;align-items:center;gap:8px;font-size:13px;color:#444;cursor:pointer; } + .mc-checkbox { width:18px;height:18px;accent-color:#5A8F3C; } +`; + +function showStartMenu() { + return new Promise(async (resolve) => { + const urlParams = new URLSearchParams(window.location.search); + const worldParam = urlParams.get('world'); + const tgStartParam = (isTgWebApp && Telegram.WebApp.initDataUnsafe?.start_param) + ? Telegram.WebApp.initDataUnsafe.start_param.trim() + : null; + + // If world is in URL or TG start param — skip menu, auto-join + if ((worldParam && worldParam.trim() !== '') || tgStartParam) { + const worldId = (worldParam && worldParam.trim()) || tgStartParam; + console.log('[Menu] Joining world from URL/startParam:', worldId); + + // Set player name from TG or localStorage or default + if (!state.playerName) { + if (isTgWebApp && tgUser) { + state.playerName = tgUser.username || tgUser.first_name || 'Игрок'; + } else { + state.playerName = localStorage.getItem('minegrechka_playerName') || 'Игрок'; + } + localStorage.setItem('minegrechka_playerName', state.playerName); + } + + resolve(worldId); + return; + } + + // Inject CSS + const styleEl = document.createElement('style'); + styleEl.textContent = mcCSS; + document.head.appendChild(styleEl); + + // Build overlay + const overlay = document.createElement('div'); + overlay.className = 'mc-overlay'; + overlay.id = 'startMenu'; + overlay.innerHTML = '
'; + + const box = document.createElement('div'); + box.className = 'mc-box'; + + // Title bar + const titleBar = document.createElement('div'); + titleBar.className = 'mc-titlebar'; + titleBar.innerHTML = '
⛏️ GrechkaCraft
' + + '
' + (isTgWebApp ? ('Привет, ' + (tgUser?.first_name || 'Игрок') + '!') : 'Voxel-мир в браузере') + '
'; + box.appendChild(titleBar); + + const body = document.createElement('div'); + body.className = 'mc-body'; + + // Mutable worldId holder + let selectedWorldId = null; + + // === Buttons === + + // 🌍 New World + const btnNew = document.createElement('button'); + btnNew.className = 'mc-btn mc-btn-green'; + btnNew.textContent = '🌍 Новый мир'; + btnNew.onclick = () => { + selectedWorldId = generateShortWorldCode(); + overlay.remove(); + styleEl.remove(); + resolve(selectedWorldId); + }; + body.appendChild(btnNew); + + // 🔗 Join by code + const btnJoin = document.createElement('button'); + btnJoin.className = 'mc-btn mc-btn-blue'; + btnJoin.textContent = '🔗 Вступить по коду'; + btnJoin.onclick = () => { + body.innerHTML = ''; + const lbl = document.createElement('div'); + lbl.className = 'mc-label'; + lbl.textContent = 'Код мира:'; + body.appendChild(lbl); + const codeInput = document.createElement('input'); + codeInput.className = 'mc-input'; + codeInput.type = 'text'; + codeInput.placeholder = 'bkh237'; + codeInput.maxLength = 12; + body.appendChild(codeInput); + const btnGo = document.createElement('button'); + btnGo.className = 'mc-btn mc-btn-blue'; + btnGo.textContent = 'Войти →'; + btnGo.onclick = () => { + const code = codeInput.value.trim().toLowerCase(); + if (code.length >= 3) { + selectedWorldId = code; + overlay.remove(); + styleEl.remove(); + resolve(selectedWorldId); + } + }; + body.appendChild(btnGo); + const btnBack = document.createElement('button'); + btnBack.className = 'mc-btn mc-btn-gray'; + btnBack.textContent = '← Назад'; + btnBack.onclick = () => { overlay.remove(); styleEl.remove(); showStartMenu().then(resolve); }; + body.appendChild(btnBack); + codeInput.focus(); + codeInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') btnGo.click(); }); + }; + body.appendChild(btnJoin); + + // 💾 Continue saved game + const hasLocalSave = !!localStorage.getItem(state.SAVE_KEY || 'minegrechka_save'); + if (isTgWebApp || hasLocalSave) { + const btnHistory = document.createElement('button'); + btnHistory.className = 'mc-btn mc-btn-purple'; + btnHistory.textContent = '💾 Продолжить игру'; + btnHistory.onclick = async () => { + if (isTgWebApp && tgUser) { + try { + const resp = await fetch(state.SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id); + const data = await resp.json(); + if (data.ok && data.world_id) { + selectedWorldId = data.world_id; + tgSaveLoaded = true; + overlay.remove(); + styleEl.remove(); + resolve(selectedWorldId); + return; + } + } catch (e) { console.warn('[TG] Load save failed:', e); } + } + if (hasLocalSave) { + // Continue with a new world code (the save will be loaded separately by initDB/loadGame) + selectedWorldId = generateShortWorldCode(); + overlay.remove(); + styleEl.remove(); + resolve(selectedWorldId); + return; + } + customAlert('Сохранений не найдено'); + }; + body.appendChild(btnHistory); + } + + // === TG-only buttons === + if (isTgWebApp && window.Telegram && Telegram.WebApp.switchInlineQuery) { + // 📤 Invite friends + const btnInvite = document.createElement('button'); + btnInvite.className = 'mc-btn mc-btn-outline'; + btnInvite.style.marginTop = '16px'; + btnInvite.textContent = '📤 Пригласить друзей'; + btnInvite.onclick = () => { + const code = selectedWorldId || generateShortWorldCode(); + const url = 'https://t.me/' + state.TELEGRAM_BOT_USERNAME + '/app?startapp=' + encodeURIComponent(code); + if (Telegram.WebApp && Telegram.WebApp.openTelegramLink) { + Telegram.WebApp.openTelegramLink('https://t.me/share/url?url=' + encodeURIComponent(url) + '&text=' + encodeURIComponent('🏗️ Заходи в GrechkaCraft! Мир: ' + code)); + } else { + navigator.clipboard.writeText(url).then(() => showToast('📋 Скопировано!')).catch(() => showToast(url)); + } + }; + body.appendChild(btnInvite); + + // 👥 Friends + const btnFriends = document.createElement('button'); + btnFriends.className = 'mc-btn mc-btn-orange'; + btnFriends.style.marginTop = '8px'; + btnFriends.textContent = '👥 Друзья'; + btnFriends.onclick = async () => { + try { + const resp = await fetch(state.SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id); + const data = await resp.json(); + body.innerHTML = ''; + + // Back button at top + const btnBackTop = document.createElement('button'); + btnBackTop.className = 'mc-btn mc-btn-gray'; + btnBackTop.textContent = '← Назад'; + btnBackTop.style.marginBottom = '12px'; + btnBackTop.onclick = () => { overlay.remove(); styleEl.remove(); showStartMenu().then(resolve); }; + body.appendChild(btnBackTop); + + // Nickname section + if (data.nickname) { + const nickDiv = document.createElement('div'); + nickDiv.className = 'mc-friend-row'; + nickDiv.innerHTML = '🔗 Твой ник: ' + data.nickname + ''; + nickDiv.style.background = '#D5E8C5'; + body.appendChild(nickDiv); + } else { + const nickLabel = document.createElement('div'); + nickLabel.className = 'mc-label'; + nickLabel.style.color = '#9E2B2B'; + nickLabel.textContent = '⚡ Задай ник, чтобы друзья могли тебя найти:'; + body.appendChild(nickLabel); + const nickRow = document.createElement('div'); + nickRow.style.display = 'flex'; + nickRow.style.gap = '4px'; + nickRow.style.marginBottom = '8px'; + const nickInput = document.createElement('input'); + nickInput.className = 'mc-input'; + nickInput.style.flex = '1'; + nickInput.type = 'text'; + nickInput.maxLength = 16; + nickInput.placeholder = 'Твой ник'; + const nickBtn = document.createElement('button'); + nickBtn.className = 'mc-btn mc-btn-green'; + nickBtn.style.width = 'auto'; + nickBtn.style.marginBottom = '0'; + nickBtn.style.padding = '8px 16px'; + nickBtn.textContent = '✓'; + nickBtn.onclick = async () => { + const n = nickInput.value.trim().toLowerCase(); + if (!n || n.length < 2) { showToast('❌ Минимум 2 символа'); return; } + const check = await fetch(state.SERVER_URL + '/api/nick/check?nick=' + encodeURIComponent(n)); + const cd = await check.json(); + if (!cd.available) { showToast('❌ Ник занят'); return; } + const setResp = await fetch(state.SERVER_URL + '/api/nick/set', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tg_id: tgUser.id, nickname: n }) + }); + const sd = await setResp.json(); + if (sd.ok) { showToast('✅ Ник: ' + sd.nickname); btnFriends.onclick(); } else { showToast('❌ Ошибка'); } + }; + nickRow.appendChild(nickInput); + nickRow.appendChild(nickBtn); + body.appendChild(nickRow); + } + + // Friends list + if (data.friends && data.friends.length) { + const listLabel = document.createElement('div'); + listLabel.className = 'mc-label'; + listLabel.textContent = '👥 Друзья:'; + body.appendChild(listLabel); + for (const f of data.friends) { + const fDiv = document.createElement('div'); + fDiv.className = 'mc-friend-row'; + const statusDot = f.online ? '🟢' : '⚫'; + let info = statusDot + ' ' + f.nickname + ''; + if (f.online && f.world_id) info += ' — мир ' + f.world_id; + const fInfo = document.createElement('div'); + fInfo.innerHTML = info; + fDiv.appendChild(fInfo); + if (f.online && f.world_id) { + const joinBtn = document.createElement('button'); + joinBtn.className = 'mc-friend-btn mc-friend-btn-green'; + joinBtn.textContent = '🎮 Вступить'; + joinBtn.onclick = () => { + selectedWorldId = f.world_id; + overlay.remove(); + styleEl.remove(); + resolve(selectedWorldId); + }; + fDiv.appendChild(joinBtn); + } + body.appendChild(fDiv); + } + } else if (data.friends && !data.friends.length) { + const noFriends = document.createElement('div'); + noFriends.className = 'mc-label'; + noFriends.style.color = '#888'; + noFriends.textContent = 'Пока нет друзей'; + body.appendChild(noFriends); + } + + // Pending friend requests + if (data.pending && data.pending.length) { + const pLabel = document.createElement('div'); + pLabel.className = 'mc-label'; + pLabel.style.color = '#B86E00'; + pLabel.textContent = '📨 Запросы в друзья:'; + body.appendChild(pLabel); + for (const p of data.pending) { + const pItem = document.createElement('div'); + pItem.className = 'mc-friend-row'; + const span = document.createElement('span'); + span.textContent = p.from; + pItem.appendChild(span); + const btns = document.createElement('div'); + btns.style.display = 'flex'; + btns.style.gap = '4px'; + const acceptBtn = document.createElement('button'); + acceptBtn.className = 'mc-friend-btn mc-friend-btn-green'; + acceptBtn.textContent = '✓'; + acceptBtn.onclick = async () => { + const r = await fetch(state.SERVER_URL + '/api/friends/accept', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tg_id: tgUser.id, request_id: p.id }) + }); + const d = await r.json(); + if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.onclick(); } else { showToast('❌ Ошибка'); } + }; + const declineBtn = document.createElement('button'); + declineBtn.className = 'mc-friend-btn mc-friend-btn-red'; + declineBtn.textContent = '✕'; + declineBtn.onclick = async () => { + await fetch(state.SERVER_URL + '/api/friends/decline', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tg_id: tgUser.id, request_id: p.id }) + }); + showToast('Отклонено'); + btnFriends.onclick(); + }; + btns.appendChild(acceptBtn); + btns.appendChild(declineBtn); + pItem.appendChild(btns); + body.appendChild(pItem); + } + } + + // Add friend + body.appendChild(document.createElement('hr')); + const addLabel = document.createElement('div'); + addLabel.className = 'mc-label'; + addLabel.textContent = '➕ Добавить друга по нику:'; + body.appendChild(addLabel); + const addRow = document.createElement('div'); + addRow.style.display = 'flex'; + addRow.style.gap = '4px'; + const addInput = document.createElement('input'); + addInput.className = 'mc-input'; + addInput.style.flex = '1'; + addInput.type = 'text'; + addInput.maxLength = 16; + addInput.placeholder = 'Ник друга'; + const addBtn = document.createElement('button'); + addBtn.className = 'mc-btn mc-btn-blue'; + addBtn.style.width = 'auto'; + addBtn.style.marginBottom = '0'; + addBtn.style.padding = '8px 16px'; + addBtn.textContent = '+'; + addBtn.onclick = async () => { + const nick = addInput.value.trim().toLowerCase(); + if (!nick || nick.length < 2) { showToast('❌ Минимум 2 символа'); return; } + const r = await fetch(state.SERVER_URL + '/api/friends/request', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tg_id: tgUser.id, nickname: nick }) + }); + const d = await r.json(); + if (d.ok) { showToast(d.message === 'Already friends' ? '✅ Уже друзья!' : '📨 Запрос отправлен!'); } + else { showToast('❌ ' + (d.error || 'Ошибка')); } + }; + addRow.appendChild(addInput); + addRow.appendChild(addBtn); + body.appendChild(addRow); + + // Privacy checkboxes + const settings = data.settings || { allow_requests: 1, notify_online: 1 }; + const settingsDiv = document.createElement('div'); + settingsDiv.style.marginTop = '12px'; + const reqLabel = document.createElement('label'); + reqLabel.className = 'mc-checkbox-row'; + const reqCb = document.createElement('input'); + reqCb.className = 'mc-checkbox'; + reqCb.type = 'checkbox'; + reqCb.checked = settings.allow_requests; + reqCb.onchange = async () => { + await fetch(state.SERVER_URL + '/api/friends/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tg_id: tgUser.id, + allow_requests: reqCb.checked ? 1 : 0, + notify_online: settings.notify_online + }) + }); + }; + reqLabel.appendChild(reqCb); + reqLabel.appendChild(document.createTextNode(' Запросы в друзья')); + settingsDiv.appendChild(reqLabel); + body.appendChild(settingsDiv); + } catch (e) { showToast('❌ Ошибка загрузки друзей'); console.error(e); } + }; + body.appendChild(btnFriends); + } + + // Player name input — always visible, with Play button + { + const nameDivider = document.createElement('hr'); + nameDivider.className = 'mc-divider'; + body.appendChild(nameDivider); + const nameLabel = document.createElement('div'); + nameLabel.className = 'mc-label'; + nameLabel.textContent = 'Твоё имя:'; + body.appendChild(nameLabel); + const nameRow = document.createElement('div'); + nameRow.style.cssText = 'display:flex;gap:6px;'; + const nameInput = document.createElement('input'); + nameInput.className = 'mc-input'; + nameInput.style.flex = '1'; + nameInput.type = 'text'; + nameInput.placeholder = 'Игрок'; + nameInput.value = state.playerName || localStorage.getItem('minegrechka_playerName') || (tgUser ? (tgUser.username || tgUser.first_name || '') : ''); + nameInput.maxLength = 20; + nameRow.appendChild(nameInput); + + const playBtn = document.createElement('button'); + playBtn.className = 'mc-btn mc-btn-green'; + playBtn.style.cssText = 'width:auto;margin-bottom:0;padding:10px 20px;white-space:nowrap;'; + playBtn.textContent = '▶ Играть'; + playBtn.onclick = () => { + const name = nameInput.value.trim() || 'Игрок'; + state.playerName = name; + localStorage.setItem('minegrechka_playerName', name); + // If no worldId yet, generate one and start + if (!selectedWorldId && !state.worldId) { + selectedWorldId = generateShortWorldCode(); + } + overlay.remove(); + styleEl.remove(); + resolve(selectedWorldId || state.worldId); + }; + nameRow.appendChild(playBtn); + body.appendChild(nameRow); + + // Allow Enter key to start game + nameInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') playBtn.click(); + }); + + // Auto-focus input on mobile + setTimeout(() => nameInput.focus(), 100); + } + + box.appendChild(body); + overlay.appendChild(box); + document.body.appendChild(overlay); + + // Set default player name if still empty + if (!state.playerName) { + state.playerName = localStorage.getItem('minegrechka_playerName') || 'Игрок'; + localStorage.setItem('minegrechka_playerName', state.playerName); + } + + // Auto-load TG save info (background fetch) + if (isTgWebApp && tgUser && !worldParam) { + fetch(state.SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id) + .then(r => r.json()) + .then(data => { + if (data.ok && data.world_id) { + // could update UI but menu is already shown + } + }) + .catch(() => {}); + } + }); +} + +// === core/canvas.js === +// ==================== ХОЛСТ 3D (WebGL) — GrechkaCraft v2 ==================== +// Тот же контракт, что canvas.js: выставляет state.W/H, хранит canvas. +// WebGL-контекст создаёт render3d (лениво, при первом кадре). + + +function initCanvas() { + const gameEl = document.getElementById('game'); + const canvas = document.getElementById('c'); + + const dpr = Math.max(1, window.devicePixelRatio || 1); + + function resize() { + state.W = gameEl.clientWidth; + state.H = gameEl.clientHeight; + } + + window.addEventListener('resize', resize); + resize(); + + state.gameEl = gameEl; + state.canvas = canvas; + state.ctx = null; + state.lightC = null; + state.lightCtx = null; + state.dpr = dpr; +} + +function getCanvas() { return state.canvas; } +function getCtx() { return null; } +function getLightC() { return null; } +function getLightCtx() { return null; } + +// === data/blocks.js === +const BLOCKS = { + air: { n:'Воздух', solid:false, hardness:0 }, + grass: { n:'Трава', c:'#7cfc00', solid:true, hardness:3, toolType:'shovel' }, + dirt: { n:'Грязь', c:'#8b4513', solid:true, hardness:3, toolType:'shovel' }, + stone: { n:'Камень', c:'#7f8c8d', solid:true, hardness:6, toolType:'pickaxe' }, + sand: { n:'Песок', c:'#f4d06f', solid:true, hardness:2, toolType:'shovel' }, + gravel: { n:'Гравий', c:'#95a5a6', solid:true, hardness:3, toolType:'shovel' }, + clay: { n:'Глина', c:'#74b9ff', solid:true, hardness:3, toolType:'shovel' }, + wood: { n:'Дерево', c:'#d35400', solid:true, hardness:4, toolType:'axe' }, + planks: { n:'Доски', c:'#e67e22', solid:true, hardness:3, toolType:'axe' }, + ladder: { n:'Лестница',c:'#d35400', solid:false, climbable:true, hardness:1, toolType:null }, + leaves: { n:'Листва', c:'#2ecc71', solid:true, hardness:1, toolType:'axe', minTool:null }, + glass: { n:'Стекло', c:'rgba(200,240,255,0.25)', solid:true, alpha:0.55, hardness:1, toolType:null }, + water: { n:'Вода', c:'rgba(52,152,219,0.55)', solid:false, fluid:true, hardness:0 }, + coal: { n:'Уголь', c:'#2c3e50', solid:true, hardness:8, toolType:'pickaxe', minTool:'wood' }, + copper_ore:{ n:'Медь', c:'#e17055', solid:true, hardness:10, toolType:'pickaxe', minTool:'wood' }, + iron_ore: { n:'Железо', c:'#dcdde1', solid:true, hardness:12, toolType:'pickaxe', minTool:'stone' }, + iron_armor:{ n:'Железная броня', c:'#95a5a6', solid:false, armor:0.5, hardness:0 }, + gold_ore: { n:'Золото', c:'#f1c40f', solid:true, hardness:15, toolType:'pickaxe', minTool:'stone' }, + diamond_ore:{n:'Алмаз', c:'#00a8ff', solid:true, hardness:20, toolType:'pickaxe', minTool:'iron' }, + brick: { n:'Кирпич', c:'#c0392b', solid:true, hardness:8, toolType:'pickaxe' }, + tnt: { n:'TNT', c:'#e74c3c', solid:true, explosive:true, hardness:1, toolType:null }, + campfire: { n:'Костёр', c:'#e67e22', solid:true, lightRadius:280, hardness:2, toolType:null }, + torch: { n:'Факел', c:'#f9ca24', solid:true, lightRadius:220, hardness:1, toolType:null }, + bedrock: { n:'Бедрок', c:'#2d3436', solid:true, unbreakable:true, hardness:Infinity }, + flower: { n:'Цветок', c:'#ff4757', solid:false, decor:true, hardness:0 }, + bed: { n:'Кровать', c:'#e91e63', solid:true, bed:true, hardness:2, toolType:'axe' }, + boat: { n:'Лодка', c:'#8B4513', solid:false, hardness:0 }, + furnace: { n:'Печь', c:'#696969', solid:true, smelting:true, hardness:8, toolType:'pickaxe', minTool:'wood' }, + cobblestone: { n:'Булыжник', c:'#7f8c8d', solid:true, hardness:6, toolType:'pickaxe' }, + flag: { n:'Флаг', c:'#e74c3c', solid:true, hardness:1, toolType:'axe', protectionRadius:10 }, + // === BIOME BLOCKS === + snow: { n:'Снег', c:'#ecf0f1', solid:true, hardness:1, toolType:'shovel' }, + ice: { n:'Лёд', c:'#74b9ff', solid:true, hardness:1, slip:true, toolType:'pickaxe' }, + cactus: { n:'Кактус', c:'#27ae60', solid:true, hardness:1, hurt:true, toolType:null }, + mushroom: { n:'Гриб', c:'#e74c3c', solid:false, decor:true, hardness:0 }, + moss: { n:'Мох', c:'#0a6640', solid:true, hardness:1, toolType:null }, + swamp_water:{ n:'Болотная вода', c:'rgba(100,120,40,0.6)', solid:false, fluid:true, poison:true, hardness:0 }, + farmland: { n:'Грядка', c:'#8B6914', solid:true, hardness:1, farmable:true, toolType:'shovel' }, + dead_bush: { n:'Сухой куст', c:'#b2bec3', solid:false, decor:true, hardness:0 }, + spruce_leaves:{ n:'Ель', c:'#0a6640', solid:true, hardness:1, toolType:'axe' }, + // === CROP STAGES === + wheat_stage0:{ n:'Росток пшеницы', c:'#a8e6a0', solid:false, decor:true, hardness:0, growthStage:0, growthTime:30, growsTo:'wheat_stage1' }, + wheat_stage1:{ n:'Пшеница', c:'#7dcea0', solid:false, decor:true, hardness:0, growthStage:1, growthTime:30, growsTo:'wheat_stage2' }, + wheat_stage2:{ n:'Пшеница', c:'#b8d730', solid:false, decor:true, hardness:0, growthStage:2, growthTime:30, growsTo:'wheat_stage3' }, + wheat_stage3:{ n:'Пшеница', c:'#f1c40f', solid:false, decor:true, hardness:0, harvestable:true, harvestItem:'wheat', harvestQty:2 }, + carrot_stage0:{ n:'Росток моркови', c:'#a8e6a0', solid:false, decor:true, hardness:0, growthStage:0, growthTime:30, growsTo:'carrot_stage1' }, + carrot_stage1:{ n:'Морковь', c:'#f0c27a', solid:false, decor:true, hardness:0, growthStage:1, growthTime:30, growsTo:'carrot_stage2' }, + carrot_stage2:{ n:'Морковь', c:'#e8a040', solid:false, decor:true, hardness:0, growthStage:2, growthTime:30, growsTo:'carrot_stage3' }, + carrot_stage3:{ n:'Морковь', c:'#e67e22', solid:false, decor:true, hardness:0, harvestable:true, harvestItem:'carrot', harvestQty:3 }, + potato_stage0:{ n:'Росток картофеля', c:'#a8e6a0', solid:false, decor:true, hardness:0, growthStage:0, growthTime:30, growsTo:'potato_stage1' }, + potato_stage1:{ n:'Картофель', c:'#c8b888', solid:false, decor:true, hardness:0, growthStage:1, growthTime:30, growsTo:'potato_stage2' }, + potato_stage2:{ n:'Картофель', c:'#bfaa78', solid:false, decor:true, hardness:0, growthStage:2, growthTime:30, growsTo:'potato_stage3' }, + potato_stage3:{ n:'Картофель', c:'#dfe6e9', solid:false, decor:true, hardness:0, harvestable:true, harvestItem:'potato', harvestQty:2 }, +}; + +// === world/world-storage3d.js === +// ==================== ХРАНИЛИЩЕ МИРА 3D — GrechkaCraft v3 ==================== +// Ключ блока: "x,y,z". Единый источник для рендера, физики, сети и сейвов. +// Мир по всем трём осям: X — вперёд, Y — вверх, Z — вбок (глубина мира). + + + +const grid = state.grid; // Map: "x,y,z" => { gx, gy, gz, t, dead } +const blocks = state.blocks; // Array для перебора/рендера + +function k3(gx, gy, gz) { return gx + ',' + gy + ',' + gz; } + +function getBlock(gx, gy, gz) { + const b = grid.get(k3(gx, gy, gz)); + if (b && b.dead) return null; + return b || null; +} + +function hasBlock(gx, gy, gz) { return grid.has(k3(gx, gy, gz)); } + +function isSolid(gx, gy, gz) { + const b = getBlock(gx, gy, gz); + if (!b) return false; + const def = BLOCKS[b.t]; + return !!def.solid && !def.fluid && !def.decor && !def.climbable; +} + +// Непрозрачный ли блок (для отсечения внутренних граней не используется — мы рендерим кубы) +function isOpaque(gx, gy, gz) { + const b = getBlock(gx, gy, gz); + if (!b) return false; + const def = BLOCKS[b.t]; + return !!def.solid && !def.fluid && !def.decor && !def.climbable; +} + +function setBlock(gx, gy, gz, t, isPlayerPlaced = false) { + const key = k3(gx, gy, gz); + if (grid.has(key)) return false; + const b = { gx, gy, gz, t, dead: false, active: false, fuse: 0 }; + grid.set(key, b); + blocks.push(b); + if (isPlayerPlaced) { + state.placedBlocks.push({ gx, gy, gz, t }); + } + state.blocksVersion = (state.blocksVersion || 0) + 1; + return true; +} + +function removeBlock(gx, gy, gz) { + const key = k3(gx, gy, gz); + const b = grid.get(key); + if (!b) return null; + if (BLOCKS[b.t].unbreakable) return null; + grid.delete(key); + b.dead = true; + const wasPlayerPlaced = state.placedBlocks.some(pb => pb.gx === gx && pb.gy === gy && pb.gz === gz); + if (wasPlayerPlaced) { + state.placedBlocks = state.placedBlocks.filter(pb => !(pb.gx === gx && pb.gy === gy && pb.gz === gz)); + } else { + state.removedBlocks.push({ gx, gy, gz }); + } + state.blocksVersion = (state.blocksVersion || 0) + 1; + return b; +} + +// === world/generation3d.js === +// ==================== ГЕНЕРАЦИЯ 3D — GrechkaCraft v3 ==================== +// Объёмный мир: столбцы (x,z), рельеф из 2D-шума, пещеры, руды, растительность. +// Та же система биомов, что в 2D-игре, но по двум координатам. + + + + +// Радиус мира по Z — насколько далеко вбок генерируем вокруг игрока +const WORLD_DEPTH = 20; // полу-ширина: z от -20 до +20 +const CHUNK = 8; // шаг генерации + +// ========== ШУМ (детерминированный, от worldSeed) ========== +function hash2(x, z) { + let h = x * 374761393 + z * 668265263 + state.worldSeed * 1274126177; + h = (h ^ (h >> 13)) * 1274126177; + return ((h ^ (h >> 16)) >>> 0) / 4294967295; +} + +function smooth(t) { return t * t * (3 - 2 * t); } + +function valueNoise2(x, z, scale) { + const fx = x / scale, fz = z / scale; + const x0 = Math.floor(fx), z0 = Math.floor(fz); + const tx = smooth(fx - x0), tz = smooth(fz - z0); + const a = hash2(x0, z0), b = hash2(x0 + 1, z0); + const c = hash2(x0, z0 + 1), d = hash2(x0 + 1, z0 + 1); + return (a * (1 - tx) + b * tx) * (1 - tz) + (c * (1 - tx) + d * tx) * tz; +} + +function fbm2(x, z, octaves = 4) { + let sum = 0, amp = 0.5, freq = 1, norm = 0; + for (let i = 0; i < octaves; i++) { + sum += valueNoise2(x * freq, z * freq, 1) * amp; + norm += amp; + amp *= 0.5; + freq *= 2; + } + return sum / norm; +} + +// ========== БИОМЫ (те же пять, что в 2D) ========== +const BIOMES = { + plains: { name:'Равнина', surface:'grass', subsurface:'dirt', trees:true, treeChance:0.11 }, + desert: { name:'Пустыня', surface:'sand', subsurface:'sand', trees:false, treeChance:0 }, + tundra: { name:'Тундра', surface:'snow', subsurface:'dirt', trees:true, treeChance:0.05 }, + swamp: { name:'Болото', surface:'moss', subsurface:'dirt', trees:true, treeChance:0.09 }, + mountains: { name:'Горы', surface:'stone', subsurface:'stone', trees:false, treeChance:0 }, +}; + +function getBiome(gx, gz) { + const s = state.worldSeed; + const temp = fbm2(gx * 0.6, gz * 0.6 + 11, 3) * 0.5 + Math.sin(gx * 0.003 + s * 0.01) * 0.25 + 0.4; + const humid = fbm2(gx * 0.5 + 500, gz * 0.5 + 700, 3) * 0.5 + Math.cos(gx * 0.006 + s * 0.02) * 0.2 + 0.4; + const mt = fbm2(gx * 0.25 + 900, gz * 0.25 + 300, 3); + if (mt > 0.78) return 'mountains'; + if (temp > 0.66) return 'desert'; + if (temp < 0.34) return 'tundra'; + if (humid > 0.66) return 'swamp'; + return 'plains'; +} + +const biomeCache = new Map(); +function getCachedBiome(gx, gz) { + const cx = Math.floor(gx / CHUNK), cz = Math.floor(gz / CHUNK); + const key = cx + ',' + cz; + if (!biomeCache.has(key)) biomeCache.set(key, getBiome(cx * CHUNK, cz * CHUNK)); + return biomeCache.get(key); +} + +// ========== РЕЛЬЕФ ========== +// Высота поверхности в колонке (x,z). Одна и та же функция на сервере и клиенте. +function surfaceGyAt(gx, gz) { + const biome = getCachedBiome(gx, gz); + const n1 = Math.sin(gx * 0.025 + state.worldSeed * 0.001) * 8; + const n2 = Math.sin(gz * 0.021 - state.worldSeed * 0.0015) * 7; + const n3 = Math.sin((gx + gz) * 0.012 + state.worldSeed * 0.002) * 10; + const n4 = fbm2(gx * 0.5, gz * 0.5, 3) * 9; + let h; + switch (biome) { + case 'desert': h = 8 + n1 * 0.5 + n3 * 0.3 + n4 * 0.2; break; + case 'mountains': h = 16 + n3 * 1.1 + n4 * 1.2; break; + case 'tundra': h = 9 + n1 * 0.6 + n3 * 0.5 + n4 * 0.4; break; + case 'swamp': h = 4 + n1 * 0.4 + n2 * 0.6 + n4 * 0.3; break; + default: h = 10 + n1 * 0.7 + n2 * 0.7 + n3 * 0.4 + n4 * 0.4; + } + return Math.max(2, Math.min(BEDROCK_GY - 12, Math.round(h))); +} + +function seaLevel() { return SEA_GY; } + +// ========== ПЕЩЕРЫ ========== +function inCave(gx, gy, gz) { + if (gy < 3) return false; + const c1 = fbm2(gx * 0.9 + 11, gz * 0.9 + gy * 1.7, 2); + const c2 = fbm2(gx * 0.9 - 40, gz * 0.9 + gy * 1.7 + 90, 2); + // две «трубки» шума: пересечение даёт объёмные полости + return c1 > 0.62 && c2 > 0.60 && gy < surfaceGyAt(gx, gz) - 3; +} + +// ========== РУДЫ ========== +function oreAt(gx, gy, gz) { + if (gy < 2) return null; + const depth = gy / BEDROCK_GY; + const r = hash2(gx * 7 + gy, gz * 13); + const r2 = hash2(gx * 3 - gy, gz * 5 + 17); + if (depth < 0.30 && r > 0.982) return 'coal'; + if (depth < 0.26 && r > 0.990 && r2 > 0.6) return 'copper_ore'; + if (depth < 0.20 && r > 0.988 && r2 > 0.4) return 'iron_ore'; + if (depth < 0.13 && r > 0.994) return 'gold_ore'; + if (depth < 0.09 && r > 0.9965) return 'diamond_ore'; + return null; +} + +// ========== ДЕРЕВО (по XY, заполняет и Z) ========== +function growTree(gx, gy, gz, biome) { + const def = BIOMES[biome]; + if (!def || !def.trees) return; + if (hash2(gx * 31, gz * 17) > def.treeChance) return; + const h = 4 + Math.floor(hash2(gx * 5, gz * 11) * 3); + const leafType = biome === 'tundra' || biome === 'swamp' ? 'spruce_leaves' : 'leaves'; + for (let i = 1; i <= h; i++) { + if (!getBlock(gx, gy - i, gz)) setBlock(gx, gy - i, gz, 'wood'); + } + // крона объёмом: псевдосфера + const cy = gy - h; + const rad = 2; + for (let dy = -2; dy <= 1; dy++) { + for (let dz = -rad; dz <= rad; dz++) { + for (let dx = -rad; dx <= rad; dx++) { + const dist = dx * dx + dy * dy * 2 + dz * dz; + if (dist > rad * rad + 1) continue; + const yy = cy + dy; + if (!getBlock(gx + dx, yy, gz + dz)) setBlock(gx + dx, yy, gz + dz, leafType); + } + } + } +} + +function growDecor(gx, gy, gz, biome) { + const def = BIOMES[biome]; + const r = hash2(gx * 23 + 5, gz * 29 + 7); + if (def.flowers && r > 0.96 && !getBlock(gx, gy, gz)) setBlock(gx, gy, gz, 'flower'); + if ((biome === 'swamp' || biome === 'plains') && r > 0.985 && !getBlock(gx, gy, gz)) setBlock(gx, gy, gz, 'mushroom'); + if (biome === 'desert' && r > 0.978 && !getBlock(gx, gy, gz) && gy + 1 < surfaceGyAt(gx, gz)) setBlock(gx, gy, gz, 'cactus'); + if (biome === 'tundra' && r > 0.978 && !getBlock(gx, gy, gz) && gy + 2 < surfaceGyAt(gx, gz)) setBlock(gx, gy, gz, 'ice'); +} + +const generated = new Set(); // "cx,cz" — уже сгенерированные чанки + +// ========== ГЕНЕРАЦИЯ ЧАНКА ========== +function genChunk(cx, cz) { + const key = cx + ',' + cz; + if (generated.has(key)) return false; + generated.add(key); + const x0 = cx * CHUNK, z0 = cz * CHUNK; + for (let x = x0; x < x0 + CHUNK; x++) { + for (let z = z0; z < z0 + CHUNK; z++) { + genColumn(x, z); + } + } + return true; +} + +function genColumn(gx, gz) { + const biome = getCachedBiome(gx, gz); + const top = surfaceGyAt(gx, gz); + const sea = seaLevel(); + + // Генерируем до bedrock, но не ниже уровня «пещерной зоны» — экономим память + for (let gy = Math.min(BEDROCK_GY - 1, 44); gy >= 0; gy--) { + // бедрок внизу + if (gy >= BEDROCK_GY - 2) { + if (!getBlock(gx, gy, gz)) setBlock(gx, gy, gz, 'bedrock'); + continue; + } + if (gy > top) { + // над поверхностью: вода до уровня моря + if (gy <= sea) { + const t = (biome === 'swamp') ? 'swamp_water' : 'water'; + if (!getBlock(gx, gy, gz)) setBlock(gx, gy, gz, t); + } + continue; + } + // пещеры — только под поверхностью + if (gy < top - 2 && inCave(gx, gy, gz)) continue; + + const ore = oreAt(gx, gy, gz); + if (ore) { setBlock(gx, gy, gz, ore); continue; } + + const def = BIOMES[biome]; + if (gy === top) setBlock(gx, gy, gz, def.surface); + else if (gy > top - (biome === 'desert' || biome === 'mountains' ? 5 : 3)) setBlock(gx, gy, gz, def.subsurface); + else setBlock(gx, gy, gz, 'stone'); + } + + // вода в пещерах не заливаем — это гравитация + // растительность + const above = getBlock(gx, top, gz); + if (above && !BLOCKS_NO_SOLID(above.t)) growDecor(gx, top, gz, biome); + if (above && !BLOCKS_NO_SOLID(above.t) && hash2(gx * 13, gz * 17) > 0.93) growTree(gx, top, gz, biome); +} + +function BLOCKS_NO_SOLID(t) { return t === 'water' || t === 'swamp_water' || t === 'air'; } + +// =========─ ГЕНЕРАЦИЯ ВОКРУГ ИГРОКА =========─ +// Держим чанки сгенерированными вокруг позиции игрока по X и Z. +const genChunks = new Set(); +function ensureGenAroundCamera() { + const px = Math.floor(state.player.x / TILE); + const pz = Math.floor(state.player.z / TILE); + const rx = 3, rz = Math.ceil(WORLD_DEPTH / CHUNK) + 1; + const cx = Math.floor(px / CHUNK), cz = Math.floor(pz / CHUNK); + for (let dx = -rx; dx <= rx; dx++) { + for (let dz = -rz; dz <= rz; dz++) { + const kx = cx + dx, kz = cz + dz; + const key = kx + ',' + kz; + if (genChunks.has(key)) continue; + genChunks.add(key); + genChunk(kx, kz); + } + } + // чистим слишком далёкие + for (const key of genChunks) { + const [a, b] = key.split(',').map(Number); + if (Math.abs(a - cx) > rx + 2 || Math.abs(b - cz) > rz + 2) { + genChunks.delete(key); + generated.delete(key); + } + } +} + +function resetGenerationCache() { + generated.clear(); + genChunks.clear(); + biomeCache.clear(); +} + +// === audio/sound-engine.js === +// Звуковой движок +const sounds = {}; + + + +function loadSound(id, src) { + const audio = new Audio(); + audio.src = src; + audio.volume = 0.3; + sounds[id] = audio; +} + +// Загрузка звуков +loadSound('splash', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3'); +loadSound('sand1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3'); +loadSound('snow1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3'); +loadSound('stone1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3'); +loadSound('wood1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3'); +loadSound('cloth1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3'); +loadSound('fire', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3'); +loadSound('hit1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3'); +loadSound('attack', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3'); +loadSound('hurt_chicken', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3'); +loadSound('stone_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3'); +loadSound('wood_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3'); +loadSound('click', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3'); +loadSound('explode1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3'); +loadSound('glass1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3'); +loadSound('eat1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3'); +loadSound('step', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3'); + +function playSound(id) { + if (sounds[id]) { + sounds[id].currentTime = 0; + sounds[id].play().catch(e => console.error('Sound error:', e)); + } +} + +// === ui/chat.js === +// ==================== ЧАТ ==================== + + + + +const chatMessages = []; + +function addChatMessage(sender, message) { + const time = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); + chatMessages.push({ sender, message, time }); + if (chatMessages.length > MAX_CHAT_MESSAGES) { + chatMessages.shift(); + } + renderChatMessages(); +} + +function renderChatMessages() { + const chatMessagesEl = document.getElementById('chatMessages'); + if (!chatMessagesEl) return; + + chatMessagesEl.innerHTML = chatMessages.map(m => + `
${m.time} ${m.sender}: ${m.message}
` + ).join(''); + + // Прокручиваем вниз + chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight; +} + +function sendChatMessage(message) { + if (!message || message.trim() === '') return; + + if (state.isMultiplayer && state.socket && state.socket.connected) { + state.socket.emit('chat_message', { message: message.trim() }); + } else { + addChatMessage('Вы', message.trim()); + } +} + +function initChat() { + document.getElementById('chatToggle').onclick = () => { + playSound('click'); + state.chatOpen = !state.chatOpen; + document.getElementById('chatPanel').style.display = state.chatOpen ? 'block' : 'none'; + if (state.chatOpen) { + document.getElementById('chatInput').focus(); + } + }; + + document.getElementById('chatClose').onclick = () => { + playSound('click'); + state.chatOpen = false; + document.getElementById('chatPanel').style.display = 'none'; + }; + + document.getElementById('chatSend').onclick = () => { + const input = document.getElementById('chatInput'); + sendChatMessage(input.value); + input.value = ''; + }; + + document.getElementById('chatInput').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + sendChatMessage(e.target.value); + e.target.value = ''; + } + }); +} + +// === entities/player.js === +// Функция для расчёта урона с учётом брони +function calculateDamage(baseDamage) { + const reduction = state.player.armor; + const actualDamage = baseDamage * (1 - reduction); + return actualDamage; +} + +// Инициализация игрока +function initPlayer() { + state.player = { + x: 6 * TILE, + y: 0 * TILE, + w: 34, + h: 34, + vx: 0, + vy: 0, + grounded: false, + inWater: false, + headInWater: false, + hp: 100, + hunger: 100, + o2: 100, + invuln: 0, + fallStartY: 0, + lastStepTime: 0, + sleeping: false, + inBoat: false, + armor: 0, + equippedArmor: null, + temperature: 15, + slowTimer: 0 + }; + + state.spawnPoint = { x: 6 * TILE, y: 0 * TILE }; +} + +// === data/items.js === +const ITEMS = { + meat: { n:'Сырое мясо', icon:'🥩', food:15 }, + cooked: { n:'Жареное мясо', icon:'🍖', food:45 }, + arrow: { n:'Стрела', icon:'➡️', stack:64 }, + chicken_meat: { n:'Курица', icon:'🍗', food:12, stack:64 }, + feather: { n:'Перо', icon:'🪶', stack:64 }, + bone: { n:'Кость', icon:'🦴', stack:64 }, + gunpowder: { n:'Порох', icon:'💥', stack:64 }, + // === FARMING ITEMS === + wheat: { n:'Пшеница', icon:'🌾', stack:64 }, + bread: { n:'Хлеб', icon:'🍞', food:30 }, + carrot: { n:'Морковь', icon:'🥕', food:8, stack:64 }, + potato: { n:'Картофель', icon:'🥔', stack:64 }, + baked_potato:{ n:'Печёная картошка', icon:'🥔', food:25 }, + wheat_seeds: { n:'Семена пшеницы', icon:'🌱', stack:64 }, + mushroom_stew:{ n:'Грибной суп', icon:'🍲', food:40 }, + // === MOB DROP ITEMS === + scorpion_stinger:{ n:'Жало скорпиона', icon:'🔺', stack:64 }, + polar_fur: { n:'Шкура медведя', icon:'🧥', stack:64 }, + slime_ball: { n:'Слизь', icon:'🟢', stack:64 }, + eagle_feather:{ n:'Перо орла', icon:'🪶', stack:64 }, + // === ARMOR === + gold_armor: { n:'Золотая броня', icon:'🛡️', stack:1, armor:0.65 }, +}; + +// Runtime extensions — предметы от обжига +ITEMS.iron_ingot = { n:'Железный слиток', icon:'🔩' }; +ITEMS.gold_ingot = { n:'Золотой слиток', icon:'🪙' }; +ITEMS.copper_ingot = { n:'Медный слиток', icon:'🟤' }; + +// Axes and shovels +ITEMS.wood_axe = { n:'Деревянный топор', icon:'🪓' }; +ITEMS.stone_axe = { n:'Каменный топор', icon:'🪓' }; +ITEMS.iron_axe = { n:'Железный топор', icon:'🪓' }; +ITEMS.wood_shovel = { n:'Деревянная лопата', icon:'🔱' }; +ITEMS.stone_shovel = { n:'Каменная лопата', icon:'🔱' }; +ITEMS.iron_shovel = { n:'Железная лопата', icon:'🔱' }; + +// === data/tools.js === +const TOOLS = { + wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', damage:1, durability:60, toolType:'pickaxe', toolTier:'wood', speed:2 }, + stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', damage:2, durability:132, toolType:'pickaxe', toolTier:'stone', speed:4 }, + iron_pickaxe: { n:'Железная кирка', icon:'⛏️', damage:3, durability:250, toolType:'pickaxe', toolTier:'iron', speed:6 }, + wood_sword: { n:'Деревянный меч', icon:'🗡️', damage:4, durability:60 }, + stone_sword: { n:'Каменный меч', icon:'🗡️', damage:5, durability:132 }, + iron_sword: { n:'Железный меч', icon:'🗡️', damage:7, durability:250 }, + wood_axe: { n:'Деревянный топор', icon:'🪓', damage:3, durability:60, toolType:'axe', toolTier:'wood', speed:2 }, + stone_axe: { n:'Каменный топор', icon:'🪓', damage:4, durability:132, toolType:'axe', toolTier:'stone', speed:4 }, + iron_axe: { n:'Железный топор', icon:'🪓', damage:5, durability:250, toolType:'axe', toolTier:'iron', speed:6 }, + wood_shovel: { n:'Деревянная лопата', icon:'🔱', damage:1, durability:60, toolType:'shovel', toolTier:'wood', speed:2 }, + stone_shovel: { n:'Каменная лопата', icon:'🔱', damage:2, durability:132, toolType:'shovel', toolTier:'stone', speed:4 }, + iron_shovel: { n:'Железная лопата', icon:'🔱', damage:3, durability:250, toolType:'shovel', toolTier:'iron', speed:6 }, + bow: { n:'Лук', icon:'🏹', damage:10, durability:200 }, +}; + + + + +function addTool(type) { + const maxDur = TOOLS[type]?.durability || 60; + const key = `${type}_${Date.now()}_${Math.random().toString(36).slice(2,6)}`; + state.toolDurability.set(key, { type, current: maxDur, max: maxDur }); +} + +function useTool(type) { + for (const [tid, dur] of state.toolDurability) { + if (dur.type === type && dur.current > 0) { + dur.current--; + if (dur.current <= 0) { + state.toolDurability.delete(tid); + state.inv[type] = Math.max(0, (state.inv[type] || 0) - 1); + return true; // broke + } + return false; // used but not broke + } + } + return false; // no durability entry found +} + +function getBestTool(blockType) { + const block = BLOCKS[blockType]; + if (!block || !block.toolType) return null; + const toolType = block.toolType; + for (const tier of ['iron', 'stone', 'wood']) { + const toolId = `${tier}_${toolType}`; + if (TOOLS[toolId] && state.inv[toolId] > 0) return toolId; + } + return null; +} + +function toolTierLessThan(toolTier, minTier) { + const tiers = { wood: 1, stone: 2, iron: 3 }; + return (tiers[toolTier] || 0) < (tiers[minTier] || 0); +} + +function getMiningSpeed(blockType) { + const block = BLOCKS[blockType]; + if (!block) return 1; + const toolId = getBestTool(blockType); + if (toolId) { + const tool = TOOLS[toolId]; + if (tool.toolType === block.toolType) return tool.speed; + } + return 1; // Bare hands = 1 click per hit +} + +// === render/textures.js === +// Текстуры блоков (простые) + + +const tex = {}; + +// --- Item/tool sprite generator ---\n +const itemTex = {}; + +function drawArrow(ctx, color) { + ctx.fillStyle = '#8B4513'; + ctx.fillRect(8, 14, 16, 3); + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(24, 15); + ctx.lineTo(18, 10); + ctx.lineTo(18, 20); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = '#e74c3c'; + ctx.beginPath(); + ctx.moveTo(8, 15); + ctx.lineTo(4, 10); + ctx.lineTo(10, 14); + ctx.closePath(); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(8, 16); + ctx.lineTo(4, 21); + ctx.lineTo(10, 17); + ctx.closePath(); + ctx.fill(); +} + +function drawPickaxe(ctx, headColor, handleColor) { + ctx.fillStyle = handleColor || '#8B4513'; + ctx.save(); + ctx.translate(16, 16); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-2, 0, 4, 18); + ctx.fillStyle = headColor; + ctx.fillRect(-8, -3, 16, 5); + ctx.fillRect(-8, -3, 3, 8); + ctx.fillRect(5, -3, 3, 8); + ctx.restore(); +} + +function drawAxe(ctx, headColor, handleColor) { + ctx.fillStyle = handleColor || '#8B4513'; + ctx.save(); + ctx.translate(16, 16); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-2, 0, 4, 18); + ctx.fillStyle = headColor; + ctx.fillRect(-2, -8, 10, 8); + ctx.fillRect(-2, -8, 4, 12); + ctx.restore(); +} + +function drawShovel(ctx, headColor, handleColor) { + ctx.fillStyle = handleColor || '#8B4513'; + ctx.save(); + ctx.translate(16, 16); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-2, 0, 4, 18); + ctx.fillStyle = headColor; + ctx.fillRect(-6, -8, 12, 8); + ctx.fillRect(-4, -12, 8, 4); + ctx.restore(); +} + +function drawSword(ctx, bladeColor, guardColor) { + ctx.fillStyle = '#8B4513'; + ctx.fillRect(14, 18, 4, 10); + ctx.fillStyle = guardColor || '#f1c40f'; + ctx.fillRect(10, 16, 12, 3); + ctx.fillStyle = bladeColor; + ctx.beginPath(); + ctx.moveTo(16, 2); + ctx.lineTo(12, 16); + ctx.lineTo(20, 16); + ctx.closePath(); + ctx.fill(); +} + +function drawBow(ctx) { + ctx.strokeStyle = '#8B4513'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(12, 16, 12, -Math.PI * 0.7, Math.PI * 0.7); + ctx.stroke(); + ctx.strokeStyle = '#ddd'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(12, 4); + ctx.lineTo(12, 28); + ctx.stroke(); +} + +function drawIngot(ctx, color) { + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(6, 12); + ctx.lineTo(26, 12); + ctx.lineTo(24, 24); + ctx.lineTo(8, 24); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(8, 8, 16, 4); +} + +function drawMeat(ctx, cooked) { + ctx.fillStyle = cooked ? '#8B4513' : '#e74c3c'; + ctx.beginPath(); + ctx.ellipse(16, 18, 10, 8, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#f5f5dc'; + ctx.fillRect(8, 6, 3, 12); + ctx.fillRect(14, 4, 3, 8); +} + +function makeItemTex(id) { + const c = document.createElement('canvas'); + c.width = 32; c.height = 32; + const g = c.getContext('2d'); + + const tierColors = { + wood: { head: '#a0522d', blade: '#a0522d' }, + stone: { head: '#7f8c8d', blade: '#95a5a6' }, + iron: { head: '#bdc3c7', blade: '#ecf0f1' }, + }; + + switch (id) { + case 'arrow': + drawArrow(g, '#7f8c8d'); + break; + case 'bow': + drawBow(g); + break; + case 'wood_pickaxe': + drawPickaxe(g, tierColors.wood.head, tierColors.wood.blade); + break; + case 'stone_pickaxe': + drawPickaxe(g, tierColors.stone.head); + break; + case 'iron_pickaxe': + drawPickaxe(g, tierColors.iron.head); + break; + case 'wood_axe': + drawAxe(g, tierColors.wood.head); + break; + case 'stone_axe': + drawAxe(g, tierColors.stone.head); + break; + case 'iron_axe': + drawAxe(g, tierColors.iron.head); + break; + case 'wood_shovel': + drawShovel(g, tierColors.wood.head); + break; + case 'stone_shovel': + drawShovel(g, tierColors.stone.head); + break; + case 'iron_shovel': + drawShovel(g, tierColors.iron.head); + break; + case 'wood_sword': + drawSword(g, tierColors.wood.blade, '#a0522d'); + break; + case 'stone_sword': + drawSword(g, tierColors.stone.blade, '#7f8c8d'); + break; + case 'iron_sword': + drawSword(g, tierColors.iron.blade, '#bdc3c7'); + break; + case 'iron_ingot': + drawIngot(g, '#bdc3c7'); + break; + case 'gold_ingot': + drawIngot(g, '#f1c40f'); + break; + case 'copper_ingot': + drawIngot(g, '#e67e22'); + break; + case 'meat': + drawMeat(g, false); + break; + case 'cooked': + drawMeat(g, true); + break; + case 'wheat': + g.fillStyle = '#f1c40f'; + g.fillRect(12, 8, 3, 18); + g.fillRect(8, 6, 10, 4); + break; + case 'bread': + g.fillStyle = '#d4a030'; + g.fillRect(6, 12, 20, 12); + g.fillStyle = '#c08820'; + g.fillRect(6, 12, 20, 3); + break; + case 'carrot': + g.fillStyle = '#e67e22'; + g.fillRect(14, 10, 4, 18); + g.fillStyle = '#27ae60'; + g.fillRect(12, 6, 8, 5); + break; + case 'potato': + g.fillStyle = '#d4a050'; + g.beginPath(); + g.ellipse(16, 16, 8, 6, 0, 0, Math.PI * 2); + g.fill(); + g.fillStyle = '#b88a40'; + g.fillRect(10, 14, 2, 1); + g.fillRect(20, 16, 2, 1); + break; + case 'baked_potato': + g.fillStyle = '#c08820'; + g.beginPath(); + g.ellipse(16, 16, 8, 6, 0, 0, Math.PI * 2); + g.fill(); + g.fillStyle = '#f5e6cc'; + g.fillRect(12, 12, 8, 6); + break; + case 'mushroom_stew': + g.fillStyle = '#8B4513'; + g.fillRect(10, 16, 12, 12); + g.fillStyle = '#e74c3c'; + g.beginPath(); + g.ellipse(16, 14, 8, 5, 0, Math.PI, 0); + g.fill(); + break; + case 'scorpion_stinger': + g.fillStyle = '#d35400'; + g.beginPath(); + g.moveTo(16, 4); + g.lineTo(10, 28); + g.lineTo(22, 28); + g.closePath(); + g.fill(); + break; + case 'polar_fur': + g.fillStyle = '#ecf0f1'; + g.fillRect(6, 6, 20, 20); + g.fillStyle = '#bdc3c7'; + g.fillRect(12, 12, 8, 8); + break; + case 'slime_ball': + g.fillStyle = 'rgba(46,204,113,0.8)'; + g.beginPath(); + g.arc(16, 16, 10, 0, Math.PI * 2); + g.fill(); + g.fillStyle = '#fff'; + g.fillRect(10, 10, 4, 4); + g.fillRect(18, 10, 4, 4); + break; + case 'eagle_feather': + g.fillStyle = '#8B4513'; + g.fillRect(14, 4, 4, 24); + g.fillStyle = '#ecf0f1'; + g.fillRect(10, 6, 12, 10); + break; + case 'chicken_meat': + drawMeat(g, false); + g.fillStyle = '#fff'; + g.fillRect(12, 8, 4, 4); + break; + case 'feather': + g.fillStyle = '#ecf0f1'; + g.fillRect(14, 6, 4, 20); + g.fillStyle = '#bbb'; + g.fillRect(12, 8, 8, 8); + break; + case 'bone': + g.fillStyle = '#f5f5dc'; + g.fillRect(12, 10, 8, 14); + g.fillRect(8, 8, 4, 4); + g.fillRect(20, 8, 4, 4); + g.fillRect(8, 22, 4, 4); + g.fillRect(20, 22, 4, 4); + break; + case 'gunpowder': + g.fillStyle = '#555'; + g.fillRect(8, 10, 6, 6); + g.fillRect(18, 14, 6, 6); + g.fillRect(12, 20, 6, 6); + break; + case 'gold_armor': + g.fillStyle = '#f1c40f'; + g.fillRect(6, 6, 20, 20); + g.fillStyle = '#f39c12'; + g.fillRect(10, 4, 12, 6); + g.fillRect(4, 10, 6, 16); + g.fillRect(22, 10, 6, 16); + break; + case 'wheat_seeds': + g.fillStyle = '#7dcea0'; + g.fillRect(14, 16, 4, 10); + g.fillStyle = '#a8e6a0'; + g.fillRect(10, 8, 4, 4); + g.fillRect(18, 6, 4, 4); + g.fillRect(14, 4, 4, 4); + break; + default: + g.fillStyle = '#7f8c8d'; + g.fillRect(4, 4, 24, 24); + g.fillStyle = 'rgba(255,255,255,0.2)'; + g.fillRect(4, 4, 24, 4); + } + return c; +} + +function makeTex(type) { + const t = BLOCKS[type]; + const c = document.createElement('canvas'); + c.width = 32; c.height = 32; + const g = c.getContext('2d'); + + if (type === 'tnt') { + g.fillStyle = '#c0392b'; g.fillRect(0, 0, 32, 32); + g.fillStyle = '#fff'; g.fillRect(0, 12, 32, 8); + g.fillStyle = '#000'; g.font = 'bold 10px sans-serif'; g.fillText('TNT', 6, 20); + return c; + } + if (type === 'campfire') { + g.fillStyle = '#5d4037'; g.fillRect(4, 26, 24, 6); + g.fillStyle = '#3e2723'; g.fillRect(7, 23, 18, 4); + return c; + } + if (type === 'torch') { + g.fillStyle = '#6d4c41'; g.fillRect(14, 10, 4, 18); + g.fillStyle = '#f39c12'; g.fillRect(12, 6, 8, 8); + return c; + } + if (type === 'glass') { + g.fillStyle = 'rgba(200,240,255,0.25)'; g.fillRect(0, 0, 32, 32); + g.strokeStyle = 'rgba(255,255,255,0.65)'; g.strokeRect(2, 2, 28, 28); + g.beginPath(); g.moveTo(5, 27); g.lineTo(27, 5); g.stroke(); + return c; + } + if (type === 'water') { + g.fillStyle = t.c; g.fillRect(0, 0, 32, 32); + g.fillStyle = 'rgba(255,255,255,0.08)'; + g.fillRect(0, 6, 32, 2); + return c; + } + if (type === 'bed') { + g.fillStyle = '#e91e63'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#f8bbd0'; + g.fillRect(2, 2, 14, 14); + g.fillStyle = '#c2185b'; + g.fillRect(16, 4, 14, 24); + g.fillStyle = '#e91e63'; + g.fillRect(18, 6, 10, 20); + return c; + } + if (type === 'flower') { + g.fillStyle = '#2ecc71'; g.fillRect(14, 14, 4, 18); + g.fillStyle = t.c; g.beginPath(); g.arc(16, 12, 6, 0, 6.28); g.fill(); + return c; + } + if (type === 'boat') { + g.fillStyle = '#8B4513'; + g.fillRect(2, 12, 28, 8); + g.fillStyle = '#A0522D'; + g.fillRect(0, 10, 32, 12); + g.fillStyle = '#DEB887'; + g.fillRect(4, 14, 24, 4); + g.fillStyle = '#654321'; + g.fillRect(2, 20, 28, 4); + return c; + } + if (type === 'ladder') { + g.fillStyle = '#8B4513'; + g.fillRect(4, 0, 4, 32); + g.fillRect(24, 0, 4, 32); + g.fillStyle = '#A0522D'; + g.fillRect(4, 4, 24, 3); + g.fillRect(4, 12, 24, 3); + g.fillRect(4, 20, 24, 3); + g.fillRect(4, 28, 24, 3); + return c; + } + // === BIOME BLOCK TEXTURES === + if (type === 'snow') { + g.fillStyle = '#ecf0f1'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#dfe6e9'; + for (let i = 0; i < 4; i++) g.fillRect((Math.random()*26)|0, (Math.random()*26)|0, 5, 3); + return c; + } + if (type === 'ice') { + g.fillStyle = '#74b9ff'; + g.fillRect(0, 0, 32, 32); + g.strokeStyle = 'rgba(255,255,255,0.4)'; + g.beginPath(); g.moveTo(4,10); g.lineTo(20,16); g.lineTo(10,28); g.stroke(); + g.beginPath(); g.moveTo(18,4); g.lineTo(28,12); g.stroke(); + return c; + } + if (type === 'cactus') { + g.fillStyle = '#27ae60'; + g.fillRect(6, 2, 20, 28); + g.fillStyle = '#2ecc71'; + g.fillRect(2, 8, 6, 4); + g.fillRect(24, 14, 6, 4); + g.fillStyle = '#1e8449'; + g.fillRect(12, 0, 2, 30); + g.fillRect(18, 0, 2, 30); + return c; + } + if (type === 'mushroom') { + g.fillStyle = '#f5e6cc'; g.fillRect(14, 20, 4, 12); + g.fillStyle = '#e74c3c'; g.beginPath(); g.arc(16, 14, 10, Math.PI, 0); g.fill(); + g.fillStyle = '#fff'; g.fillRect(10, 10, 3, 3); g.fillRect(17, 8, 4, 3); + return c; + } + if (type === 'moss') { + g.fillStyle = '#0a6640'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#1e8449'; + for (let i = 0; i < 6; i++) g.fillRect((Math.random()*26)|0, (Math.random()*26)|0, 4, 3); + return c; + } + if (type === 'swamp_water') { + g.fillStyle = 'rgba(100,120,40,0.6)'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = 'rgba(80,100,20,0.3)'; + g.fillRect(0, 10, 32, 2); + g.fillRect(0, 22, 32, 2); + return c; + } + if (type === 'farmland') { + g.fillStyle = '#8B6914'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#7a5c10'; + for (let i = 0; i < 4; i++) g.fillRect(0, 6+i*8, 32, 2); + g.fillStyle = '#6B4E0A'; + g.fillRect(8, 2, 2, 28); + g.fillRect(18, 2, 2, 28); + return c; + } + if (type === 'dead_bush') { + g.fillStyle = '#b2bec3'; + g.fillRect(12, 16, 2, 16); + g.fillRect(8, 14, 8, 2); + g.fillRect(16, 12, 8, 2); + g.fillRect(6, 18, 4, 2); + return c; + } + if (type === 'spruce_leaves') { + g.fillStyle = '#0a6640'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#0d7a4d'; + for (let i = 0; i < 5; i++) g.fillRect((Math.random()*24)|0, (Math.random()*24)|0, 6, 4); + return c; + } + // CROP STAGES + if (type.startsWith('wheat_stage') || type.startsWith('carrot_stage') || type.startsWith('potato_stage')) { + const st = parseInt(type.charAt(type.length-1)); + const colors = type.startsWith('wheat') ? ['#a8e6a0','#7dcea0','#b8d730','#f1c40f'] : + type.startsWith('carrot') ? ['#a8e6a0','#f0c27a','#e8a040','#e67e22'] : + ['#a8e6a0','#c8b888','#bfaa78','#dfe6e9']; + g.fillStyle = '#5d4037'; + g.fillRect(15, 18, 2, 14); + if (st >= 1) { + g.fillStyle = colors[st]; + g.fillRect(10, 8 + (3-st)*3, 12, 10); + } else { + g.fillStyle = '#a8e6a0'; + g.fillRect(14, 22, 4, 4); + } + return c; + } + + g.fillStyle = t.c || '#000'; + g.fillRect(0, 0, 32, 32); + + g.fillStyle = 'rgba(0,0,0,0.10)'; + for (let i = 0; i < 6; i++) g.fillRect((Math.random() * 28) | 0, (Math.random() * 28) | 0, 4, 4); + + if (type.endsWith('_ore') || type === 'coal') { + g.fillStyle = 'rgba(0,0,0,0.35)'; + for (let i = 0; i < 4; i++) g.fillRect((Math.random() * 24) | 0, (Math.random() * 24) | 0, 6, 6); + } + return c; +} + +function initTextures() { + Object.keys(BLOCKS).forEach(k => tex[k] = makeTex(k)); + // Item/tool sprites + const itemIds = [ + 'arrow','bow', + 'wood_pickaxe','stone_pickaxe','iron_pickaxe', + 'wood_axe','stone_axe','iron_axe', + 'wood_shovel','stone_shovel','iron_shovel', + 'wood_sword','stone_sword','iron_sword', + 'iron_ingot','gold_ingot','copper_ingot', + 'meat','cooked', + 'wheat','bread','carrot','potato','baked_potato','mushroom_stew','wheat_seeds', + 'chicken_meat','feather','bone','gunpowder', + 'scorpion_stinger','polar_fur','slime_ball','eagle_feather', + 'gold_armor', + ]; + itemIds.forEach(id => { itemTex[id] = makeItemTex(id); }); +} + +// === ui/hotbar.js === +// Hotbar UI — 3-slot Minecraft-style bottom bar + selected item indicator + + + + + + + +const HOTBAR_SLOTS = 3; + +let selectedItemEl = null; + +function addToRecentItems(id) { + if (!id) return; + state.recentItems = state.recentItems.filter(item => item !== id); + state.recentItems.unshift(id); + state.recentItems = state.recentItems.slice(0, 5); +} + +function renderSlotIcon(el, id) { + if (BLOCKS[id] && tex[id]) { + el.style.backgroundImage = `url(${tex[id].toDataURL()})`; + el.style.backgroundSize = 'cover'; + } else if (itemTex[id]) { + el.style.backgroundImage = `url(${itemTex[id].toDataURL()})`; + el.style.backgroundSize = 'cover'; + } else if (ITEMS[id]) { + el.textContent = ITEMS[id].icon || '📦'; + } else if (TOOLS[id]) { + el.textContent = TOOLS[id].icon || '🔧'; + } else if (id === 'iron_armor') { + el.textContent = '🛡️'; + el.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)'; + } else { + el.textContent = id.charAt(0).toUpperCase(); + el.style.background = '#555'; + el.style.color = '#fff'; + el.style.fontWeight = '700'; + el.style.fontSize = '14px'; + } +} + +function updateSelectedItem() { + if (!selectedItemEl) selectedItemEl = document.getElementById('selectedItem'); + if (!selectedItemEl) return; + const sel = state.selected; + if (!sel || !state.inv[sel] || state.inv[sel] <= 0) { + selectedItemEl.style.display = 'none'; + return; + } + selectedItemEl.style.display = 'flex'; + // Clear previous content + selectedItemEl.textContent = ''; + selectedItemEl.style.backgroundImage = ''; + selectedItemEl.style.background = 'rgba(0,0,0,0.45)'; + // Render icon + if (BLOCKS[sel] && tex[sel]) { + selectedItemEl.style.backgroundImage = `url(${tex[sel].toDataURL()})`; + selectedItemEl.style.backgroundSize = 'cover'; + } else if (itemTex[sel]) { + selectedItemEl.style.backgroundImage = `url(${itemTex[sel].toDataURL()})`; + selectedItemEl.style.backgroundSize = 'cover'; + } else if (ITEMS[sel]) { + selectedItemEl.textContent = ITEMS[sel].icon || '📦'; + } else if (TOOLS[sel]) { + selectedItemEl.textContent = TOOLS[sel].icon || '🔧'; + } else { + selectedItemEl.textContent = sel.charAt(0).toUpperCase(); + } +} + +function rebuildHotbar() { + const hotbarEl = state.hotbarEl; + const inv = state.inv; + const selected = state.selected; + const toolDurability = state.toolDurability; + + hotbarEl.innerHTML = ''; + + // Get up to 3 recent items with count > 0 + const items = state.recentItems.filter(id => inv[id] > 0).slice(0, HOTBAR_SLOTS); + + // If selected item not in list and has count > 0, push to front + if (inv[selected] > 0 && !items.includes(selected)) { + items.unshift(selected); + if (items.length > HOTBAR_SLOTS) items.pop(); + } + + // Always render HOTBAR_SLOTS slots (fill empties) + for (let si = 0; si < HOTBAR_SLOTS; si++) { + const id = items[si] || null; + const s = document.createElement('div'); + s.className = 'slot' + (id && id === selected ? ' sel' : ''); + + // Slot number indicator (top-left) + const numEl = document.createElement('div'); + numEl.style.cssText = 'position:absolute;top:1px;left:3px;font-size:10px;color:rgba(255,255,255,0.55);font-weight:700;pointer-events:none;text-shadow:1px 1px 1px rgba(0,0,0,0.7);'; + numEl.textContent = (si + 1).toString(); + s.appendChild(numEl); + + if (id) { + renderSlotIcon(s, id); + + const c = document.createElement('div'); + c.className = 'count'; + c.textContent = inv[id]; + s.appendChild(c); + s.onclick = () => { + playSound('click'); + state.selected = id; + // Move to front of recentItems + state.recentItems = state.recentItems.filter(item => item !== id); + state.recentItems.unshift(id); + rebuildHotbar(); + }; + + if (id === 'iron_armor' && state.player.equippedArmor === 'iron_armor') { + const equipped = document.createElement('div'); + equipped.className = 'equipped-indicator'; + equipped.textContent = '✓'; + s.appendChild(equipped); + } + + // Durability bar + if (TOOLS[id] && inv[id] > 0) { + let curDur = 0, maxDur = TOOLS[id].durability; + for (const [tid, dur] of toolDurability) { + if (dur.type === id) { + curDur = dur.current; + maxDur = dur.max; + break; + } + } + if (maxDur > 0) { + const bar = document.createElement('div'); + bar.style.cssText = `position:absolute;bottom:0;left:0;right:0;height:3px;background:#333;border-radius:0 0 6px 6px;overflow:hidden;`; + const fill = document.createElement('div'); + const pct = curDur / maxDur; + const color = pct > 0.5 ? '#2ecc71' : pct > 0.25 ? '#f39c12' : '#e74c3c'; + fill.style.cssText = `width:${pct * 100}%;height:100%;background:${color};`; + bar.appendChild(fill); + s.appendChild(bar); + } + } + } + hotbarEl.appendChild(s); + } + + // Update selected item indicator above hotbar + updateSelectedItem(); +} + +// === world/tnt3d.js === +// ==================== TNT В 3D — GrechkaCraft v3 ==================== +// Взрыв сферический: сносит блоки в радиусе по всем трём осям. + + + + + +function activateTNT3D(b, fuseSeconds) { + b.active = true; + b.fuse = fuseSeconds; + playSound('fuse'); +} + +function explodeTNT3D(b, st) { + const TILE = st.TILE; + const cx = b.gx, cy = b.gy, cz = b.gz || 0; + const radius = 3.2; + const r2 = radius * radius; + + // убрать сам блок + removeBlock(cx, cy, cz); + + const toRemove = []; + const r = Math.ceil(radius); + for (let dx = -r; dx <= r; dx++) { + for (let dy = -r; dy <= r; dy++) { + for (let dz = -r; dz <= r; dz++) { + const d2 = dx * dx + dy * dy + dz * dz; + if (d2 > r2) continue; + const blk = getBlock(cx + dx, cy + dy, cz + dz); + if (!blk) continue; + const def = BLOCKS[blk.t]; + if (!def) continue; + if (def.unbreakable) continue; + // взрывная сила падает с расстоянием + if (d2 > r2 * 0.55 && blk.t !== 'dirt' && blk.t !== 'sand' && blk.t !== 'grass') continue; + toRemove.push({ gx: cx + dx, gy: cy + dy, gz: cz + dz, t: blk.t }); + } + } + } + for (const rem of toRemove) removeBlock(rem.gx, rem.gy, rem.gz); + + // частицы + const parts = st.parts || (st.parts = []); + const n = 30; + for (let i = 0; i < n; i++) { + const a = Math.random() * Math.PI * 2; + const p = Math.acos(2 * Math.random() - 1); + const sp = 60 + Math.random() * 180; + parts.push({ + x: (cx + 0.5) * TILE, + y: (cy + 0.5) * TILE, + z: (cz + 0.5) * TILE, + vx: Math.sin(p) * Math.cos(a) * sp, + vy: Math.cos(p) * sp, + vz: Math.sin(p) * Math.sin(a) * sp, + life: 0.6 + Math.random() * 0.5, + c: Math.random() > 0.5 ? '#ff7a1a' : '#f1c40f', + }); + } + + // урон мобам и игроку в радиусе + const dmg = (e) => { + const ex = (e.x + e.w / 2) / TILE, ey = (e.y + e.h / 2) / TILE, ez = ((e.z || 0) + e.d / 2) / TILE; + const d = Math.hypot(ex - cx, ey - cy, ez - cz); + if (d < radius + 1) { + const f = 1 - d / (radius + 1); + e.hp -= 14 * f; + e.vx += (ex - cx) * 220; + e.vy -= 300 * f; + e.vz = (e.vz || 0) + (ez - cz) * 220; + } + }; + for (const m of st.mobs) dmg(m); + const p = st.player; + dmg(p); + if (p.hp <= 0 && p.invuln <= 0) { p.hp = 0; p.invuln = 1.5; } + + playSound('explode'); + + // взрывные частицы убрать по таймеру + st.explosionFlash = 0.4; + setBlock(cx, cy, cz, 'air'); + void setBlock; +} + +// === ui/inventory.js === +// Inventory UI + + + + + + + + +function renderInventory() { + const inventoryGrid = state.inventoryGrid; + const inv = state.inv; + const selected = state.selected; + inventoryGrid.innerHTML = ''; + + const items = Object.keys(inv).filter(id => inv[id] > 0); + + for (let i = 0; i < 21; i++) { + const slot = document.createElement('div'); + slot.className = 'inv-slot' + (i < items.length && items[i] === selected ? ' sel' : ''); + + if (i < items.length) { + const id = items[i]; + // Canvas sprite: block > itemTex > emoji fallback > unknown item fallback + if (BLOCKS[id] && tex[id]) { + slot.style.backgroundImage = `url(${tex[id].toDataURL()})`; + slot.style.backgroundSize = 'cover'; + } else if (itemTex[id]) { + slot.style.backgroundImage = `url(${itemTex[id].toDataURL()})`; + slot.style.backgroundSize = 'cover'; + } else if (ITEMS[id]) { + slot.textContent = ITEMS[id].icon || '📦'; + } else if (TOOLS[id]) { + slot.textContent = TOOLS[id].icon || '🔧'; + } else if (id === 'iron_armor') { + slot.textContent = '🛡️'; + slot.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)'; + } else { + // Unknown item fallback — grey box with first letter + slot.textContent = id.charAt(0).toUpperCase(); + slot.style.background = '#555'; + slot.style.color = '#fff'; + slot.style.fontWeight = '700'; + slot.style.fontSize = '14px'; + } + + const count = document.createElement('div'); + count.className = 'inv-count'; + count.textContent = inv[id]; + slot.appendChild(count); + + slot.onclick = () => { + playSound('click'); + state.selected = id; + state.recentItems = state.recentItems.filter(item => item !== id); + state.recentItems.unshift(id); + state.recentItems = state.recentItems.slice(0, 5); + rebuildHotbar(); + renderInventory(); + }; + + // Двойной клик для надевания брони + slot.ondblclick = () => { + if (id === 'iron_armor' && inv.iron_armor > 0) { + if (state.player.equippedArmor === 'iron_armor') { + state.player.equippedArmor = null; + state.player.armor = 0; + } else { + state.player.equippedArmor = 'iron_armor'; + state.player.armor = BLOCKS['iron_armor'].armor; + } + playSound('click'); + renderInventory(); + } + }; + } + + inventoryGrid.appendChild(slot); + } +} + +// === game/save.js === +// ==================== СИСТЕМА СОХРАНЕНИЯ ИГРЫ ==================== + + + + + + +const SAVE_STORAGE_KEY = state.SAVE_KEY; +let db = null; // Оставляем для совместимости, но не используем + +// Инициализация (localStorage + in-memory fallback) +function initDB() { + return new Promise((resolve) => { + console.log('Используем localStorage для сохранений (sandbox режим)'); + resolve(null); + }); +} + +function saveGame() { + const saveData = { + version: 2, + worldSeed: state.worldSeed, + player: { + x: state.player.x, + y: state.player.y, + hp: state.player.hp, + hunger: state.player.hunger, + o2: state.player.o2 + }, + inventory: state.inv, + time: state.worldTime, + isNight: state.isNightTime, + dayNightCycle: state.dayNightCycle, + // Сохраняем только изменения + placedBlocks: state.placedBlocks.slice(), + removedBlocks: state.removedBlocks.slice() + }; + + const saveSize = JSON.stringify(saveData).length; + console.log('Сохранение: player HP:', state.player.hp, 'hunger:', state.player.hunger, 'o2:', state.player.o2); + + // Пробуем сохранить в localStorage (основной метод для персистентности) + try { + localStorage.setItem(SAVE_STORAGE_KEY, JSON.stringify(saveData)); + console.log(`Игра сохранена в localStorage (размер: ${saveSize} байт)`); + } catch (e) { + console.warn('Ошибка сохранения в localStorage, используем только in-memory:', e); + + // Если localStorage недоступен, используем in-memory fallback + state.inMemorySave = saveData; + console.log(`Игра сохранена в памяти (размер: ${saveSize} байт)`); + } +} + +function loadGame() { + return new Promise((resolve, reject) => { + // Пробуем localStorage + try { + const localSave = localStorage.getItem(SAVE_STORAGE_KEY); + if (localSave) { + const parsed = JSON.parse(localSave); + console.log('Загружено из localStorage, player HP:', parsed.player?.hp); + resolve(parsed); + return; + } + } catch (e) { + console.warn('Ошибка доступа к localStorage:', e); + } + + // Если localStorage недоступен, используем in-memory сохранение + if (state.inMemorySave) { + console.log('Загружено из in-memory сохранения, player HP:', state.inMemorySave.player?.hp); + resolve(state.inMemorySave); + return; + } + + console.log('Сохранение не найдено'); + resolve(null); + }); +} + +// Миграция с версии 1 на версию 2 +function migrateV1toV2(saveData) { + console.log('Миграция сохранения с версии 1 на версию 2...'); + + // Сохраняем seed из текущей игры (так как v1 его не хранил) + saveData.worldSeed = state.worldSeed; + + // Инициализируем массивы изменений + saveData.placedBlocks = []; + saveData.removedBlocks = []; + + // Для v1 нам нужно сравнить сгенерированные блоки с тем, что должно быть по seed + // Это сложно сделать без полной перегенерации, поэтому для v1 сохраняем только seed + // и при загрузке просто перегенерируем мир + + // Удаляем старые данные + delete saveData.generatedBlocks; + + saveData.version = 2; + console.log('Миграция завершена'); +} + +function applySave(saveData) { + if (!saveData) return; + + console.log('=== applySave START ==='); + console.log('player HP before applySave:', state.player.hp); + console.log('saveData.player.hp:', saveData.player?.hp); + + // Миграция версий + if (saveData.version === 1) { + migrateV1toV2(saveData); + } + + // Восстанавливаем seed + if (saveData.worldSeed !== undefined) { + state.worldSeed = saveData.worldSeed; + } + + // Восстанавливаем игрока + if (saveData.player) { + state.player.x = saveData.player.x; + state.player.y = saveData.player.y; + state.player.hunger = saveData.player.hunger; + state.player.o2 = saveData.player.o2; + + // Обновляем spawnPoint на позицию из сохранения + state.spawnPoint.x = state.player.x; + state.spawnPoint.y = state.player.y; + + // Проверяем HP из сохранения - если <= 0, устанавливаем 100 + const savedHP = saveData.player.hp; + console.log('Saved HP from file:', savedHP); + if (savedHP <= 0) { + console.log('WARNING: Saved HP is <= 0, setting to 100!'); + state.player.hp = 100; + } else { + state.player.hp = savedHP; + } + console.log('player HP after restore:', state.player.hp); + console.log('spawnPoint обновлён из сохранения: x=', state.spawnPoint.x, 'y=', state.spawnPoint.y); + } else { + console.log('No player data in save, setting default HP: 100'); + state.player.hp = 100; + } + + console.log('=== applySave END ==='); + + // Восстанавливаем инвентарь + if (saveData.inventory) { + for (const key in saveData.inventory) { + state.inv[key] = saveData.inventory[key]; + } + } + + // Восстанавливаем время + if (saveData.time !== undefined) { + state.worldTime = saveData.time; + } + + // Восстанавливаем день/ночь + if (saveData.isNight !== undefined) { + state.isNightTime = saveData.isNight; + } + if (saveData.dayNightCycle !== undefined) { + state.dayNightCycle = saveData.dayNightCycle; + } + + // Перегенерируем мир по seed + resetGenerationCache(); + ensureGenAroundCamera(); + + // Применяем изменения (только для v2) + if (saveData.version === 2) { + // Применяем блоки, установленные игроком + for (const block of saveData.placedBlocks) { + setBlock(block.gx, block.gy, block.gz || 0, block.t, true); + } + + // Применяем удалённые блоки + for (const block of saveData.removedBlocks) { + removeBlock(block.gx, block.gy, block.gz || 0); + } + + // Восстанавливаем массивы изменений + state.placedBlocks = saveData.placedBlocks || []; + state.removedBlocks = saveData.removedBlocks || []; + } + + // Fill recentItems from inventory if empty (hotbar won.t render otherwise) + if (state.recentItems.length === 0) { + state.recentItems = Object.keys(state.inv).filter(id => state.inv[id] > 0).slice(0, 5); + } + rebuildHotbar(); + renderInventory(); + console.log('Игра загружена'); +} + +// === ui/share.js === +// ==================== ПОДЕЛИТЬСЯ МИРОМ ==================== + + + + +function initShare() { + // Keep worldId click-to-copy as before + const worldIdEl = document.getElementById('worldId'); + if (worldIdEl) { + worldIdEl.onclick = () => shareWorld(); + } +} + +function shareWorld() { + const shareUrl = new URL(window.location.href); + shareUrl.searchParams.set('world', state.worldId); + const url = shareUrl.toString(); + + // In Telegram WebApp — use native share + if (isTgWebApp && window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.switchInlineQuery) { + const inviteUrl = 'https://t.me/' + (state.TELEGRAM_BOT_USERNAME || 'grechka_mkn8n_bot') + '/app?startapp=' + encodeURIComponent(state.worldId); + Telegram.WebApp.openTelegramLink( + 'https://t.me/share/url?url=' + encodeURIComponent(inviteUrl) + + '&text=' + encodeURIComponent('🏗️ Заходи в GrechkaCraft! Код мира: ' + state.worldId) + ); + return; + } + + // Otherwise — show custom modal with URL + copy button + const overlay = document.createElement('div'); + overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:100000;display:flex;align-items:center;justify-content:center;'; + + const box = document.createElement('div'); + box.style.cssText = 'background:#C6C6C6;border:4px solid #000;padding:20px;max-width:400px;width:90%;box-shadow:4px 4px 0 #000;font-family:Courier New,monospace;'; + + const title = document.createElement('div'); + title.textContent = '📤 Пригласить друзей'; + title.style.cssText = 'font-size:18px;font-weight:bold;margin-bottom:12px;color:#222;'; + box.appendChild(title); + + const codeLabel = document.createElement('div'); + codeLabel.style.cssText = 'font-size:13px;color:#444;font-weight:bold;margin-bottom:4px;'; + codeLabel.textContent = 'Код мира:'; + box.appendChild(codeLabel); + + const codeBox = document.createElement('div'); + codeBox.style.cssText = 'font-size:20px;font-weight:bold;background:#fff;border:3px solid #000;padding:8px 12px;margin-bottom:12px;letter-spacing:2px;text-align:center;color:#5A8F3C;'; + codeBox.textContent = state.worldId; + box.appendChild(codeBox); + + const linkLabel = document.createElement('div'); + linkLabel.style.cssText = 'font-size:13px;color:#444;font-weight:bold;margin-bottom:4px;'; + linkLabel.textContent = 'Ссылка:'; + box.appendChild(linkLabel); + + const linkBox = document.createElement('input'); + linkBox.type = 'text'; + linkBox.readOnly = true; + linkBox.value = url; + linkBox.style.cssText = 'width:100%;padding:8px;font-family:Courier New,monospace;font-size:13px;background:#fff;border:3px solid #000;color:#222;margin-bottom:12px;box-sizing:border-box;'; + linkBox.onclick = () => linkBox.select(); + box.appendChild(linkBox); + + const btnRow = document.createElement('div'); + btnRow.style.cssText = 'display:flex;gap:8px;'; + + const btnCopy = document.createElement('button'); + btnCopy.textContent = '📋 Скопировать'; + btnCopy.style.cssText = 'flex:1;padding:10px;font-family:Courier New,monospace;font-weight:bold;cursor:pointer;border:3px solid #000;background:#5A8F3C;color:#fff;box-shadow:2px 2px 0 #000;'; + btnCopy.onclick = () => { + navigator.clipboard.writeText(url).then(() => { + showToast('📋 Ссылка скопирована!'); + overlay.remove(); + }).catch(() => { + linkBox.select(); + document.execCommand('copy'); + showToast('📋 Ссылка скопирована!'); + overlay.remove(); + }); + }; + btnRow.appendChild(btnCopy); + + const btnCode = document.createElement('button'); + btnCode.textContent = '📋 Код'; + btnCode.style.cssText = 'padding:10px;font-family:Courier New,monospace;font-weight:bold;cursor:pointer;border:3px solid #000;background:#3C5F9E;color:#fff;box-shadow:2px 2px 0 #000;'; + btnCode.onclick = () => { + navigator.clipboard.writeText(state.worldId).then(() => { + showToast('📋 Код скопирован: ' + state.worldId); + }).catch(() => { + showToast('Код: ' + state.worldId); + }); + }; + btnRow.appendChild(btnCode); + + const btnClose = document.createElement('button'); + btnClose.textContent = '✕'; + btnClose.style.cssText = 'padding:10px 14px;font-family:Courier New,monospace;font-weight:bold;cursor:pointer;border:3px solid #000;background:#9E2B2B;color:#fff;box-shadow:2px 2px 0 #000;'; + btnClose.onclick = () => overlay.remove(); + btnRow.appendChild(btnClose); + + box.appendChild(btnRow); + overlay.appendChild(box); + + // Close on click outside box + overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); }; + + document.body.appendChild(overlay); +} + +// === ui/save-controls.js === +// ==================== МЕНЮ ⋯ + СОХРАНЕНИЕ / СБРОС ==================== + + + + + + +let dropdownOpen = false; + +function initSaveControls() { + const menuBtn = document.getElementById('menuBtn'); + const menuDropdown = document.getElementById('menuDropdown'); + const saveItem = document.getElementById('saveItem'); + const resetItem = document.getElementById('resetItem'); + const marketItem = document.getElementById('marketItem'); + const shareItem = document.getElementById('shareItem'); + const settingsItem = document.getElementById('settingsItem'); + const quitItem = document.getElementById('quitItem'); + + // Убить возможный старый onclick + if (menuBtn) menuBtn.onclick = null; + + // Тогл меню + if (menuBtn) { + menuBtn.addEventListener('click', (e) => { + e.stopPropagation(); + dropdownOpen = !dropdownOpen; + if (menuDropdown) menuDropdown.classList.toggle('open', dropdownOpen); + }); + } + + // Закрытие по клику вне + document.addEventListener('click', () => { + if (dropdownOpen) { + dropdownOpen = false; + if (menuDropdown) menuDropdown.classList.remove('open'); + } + }); + + if (menuDropdown) { + menuDropdown.addEventListener('click', (e) => e.stopPropagation()); + } + + // 💾 Сохранить + if (saveItem) { + saveItem.addEventListener('click', () => { + playSound('click'); + saveGame(); + dropdownOpen = false; + if (menuDropdown) menuDropdown.classList.remove('open'); + alert('Игра сохранена!'); + }); + } + + // 🔄 Новый мир + if (resetItem) { + resetItem.addEventListener('click', () => { + if (confirm('Вы уверены, что хотите удалить сохранение и начать новую игру?')) { + playSound('click'); + try { localStorage.removeItem(state.SAVE_KEY); } catch(e) {} + state.inMemorySave = null; + state.worldId = Math.random().toString(36).substring(2, 10); + try { + const newUrl = new URL(window.location.href); + newUrl.searchParams.set('world', state.worldId); + window.history.replaceState(null, '', newUrl.toString()); + } catch(e) {} + location.reload(); + } else { + dropdownOpen = false; + if (menuDropdown) menuDropdown.classList.remove('open'); + } + }); + } + + // 🏪 Рынок + if (marketItem) { + marketItem.addEventListener('click', () => { + dropdownOpen = false; + if (menuDropdown) menuDropdown.classList.remove('open'); + document.dispatchEvent(new CustomEvent('toggleMarket')); + }); + } + + // 📤 Пригласить + if (shareItem) { + shareItem.addEventListener('click', () => { + dropdownOpen = false; + if (menuDropdown) menuDropdown.classList.remove('open'); + shareWorld(); + }); + } + + // ⚙️ Настройки + if (settingsItem) { + settingsItem.addEventListener('click', () => { + dropdownOpen = false; + menuDropdown.classList.remove('open'); + const panel = document.getElementById('settingsPanel'); + if (!panel) return; + // Sync controls with current state + const toggle = document.getElementById('dayNightToggle'); + const timeSlider = document.getElementById('timeSlider'); + const wSlider = document.getElementById('weatherIntensity'); + if (toggle) toggle.checked = state.dayNightCycle !== false; + if (timeSlider) timeSlider.value = Math.round(state.worldTime * 100); + if (wSlider) wSlider.value = Math.round(state.rainIntensity * 100); + // Sync weather buttons — highlight the active type + const wt = state.weatherType || (state.isRaining ? 'rain' : 'clear'); + const activeId = 'weather' + wt.charAt(0).toUpperCase() + wt.slice(1); + for (const bid of ['weatherClear','weatherRain','weatherSnow','weatherThunder']) { + const b = document.getElementById(bid); + if (b) b.style.background = (bid === activeId) ? '#3498db' : '#555'; + } + panel.style.display = 'block'; + }); + } + + // Закрытие настроек + const settingsClose = document.getElementById('settingsClose'); + if (settingsClose) { + settingsClose.addEventListener('click', () => { + const settingsPanel = document.getElementById('settingsPanel'); + if (settingsPanel) settingsPanel.style.display = 'none'; + }); + } + + // Тогл день/ночи + const dayNightToggle = document.getElementById('dayNightToggle'); + if (dayNightToggle) { + dayNightToggle.addEventListener('change', () => { + state.dayNightCycle = dayNightToggle.checked; + saveGame(); + }); + } + + // 🏠 Главное меню + if (quitItem) { + quitItem.addEventListener('click', () => { + dropdownOpen = false; + if (menuDropdown) menuDropdown.classList.remove('open'); + saveGame(); + try { + const url = new URL(window.location.href); + url.searchParams.delete('world'); + window.location.href = url.toString(); + } catch(e) { location.reload(); } + }); + } +} + +function updateSaveButtonVisibility() { + const saveItem = document.getElementById('saveItem'); + if (!saveItem) return; + saveItem.style.display = (state.isMultiplayer && state.otherPlayers.size > 0) ? 'none' : 'block'; +} + +// === multiplayer/socket-helpers.js === +// Socket helpers: цвет, отправка позиции/блоков + + + + + +// Генерация случайного цвета для игрока на основе socket_id +function getRandomPlayerColor(socketId) { + const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e91e63', '#00bcd4']; + let hash = 0; + for (let i = 0; i < socketId.length; i++) { + hash = ((hash << 5) - hash) + socketId.charCodeAt(i); + hash = hash & hash; + } + return colors[Math.abs(hash) % colors.length]; +} + +// Throttle для отправки позиции (10-20 раз в секунду) +let lastMoveSendTime = 0; + +let lastSentX = 0, lastSentY = 0; + +// Отправка позиции игрока (с throttle) +function sendPlayerPosition() { + if (!state.isMultiplayer || !state.socket || !state.socket.connected) return; + + const now = performance.now() / 1000; + if (now - lastMoveSendTime < MOVE_SEND_INTERVAL) return; + + // Отправляем только если позиция изменилась + const dx = Math.abs(state.player.x - lastSentX); + const dy = Math.abs(state.player.y - lastSentY); + if (dx < 1 && dy < 1) return; + + lastMoveSendTime = now; + lastSentX = state.player.x; + lastSentY = state.player.y; + + state.socket.emit('player_move', { x: state.player.x, y: state.player.y, player_name: state.playerName }); +} + +// Отправка изменения блока +function sendBlockChange(gx, gy, t, op) { + if (!state.isMultiplayer || !state.socket || !state.socket.connected) return; + + state.socket.emit('block_change', { gx, gy, t, op }); +} + +// === ui/trade.js === +// Trade UI — личная торговля (лицом к лицу) + + + + + + + +function itemName(id) { + return BLOCKS[id]?.n || TOOLS[id]?.n || ITEMS[id]?.n || id; +} +function itemIcon(id) { + if (BLOCKS[id]) return '🟫'; + if (TOOLS[id]) return TOOLS[id].icon || '🔧'; + if (ITEMS[id]) return ITEMS[id].icon || '📦'; + return '📦'; +} + +let tradeOpen = false; +let currentTradeId = null; +let myOffer = {}; +let partnerOffer = {}; +let iConfirmed = false; + +function isTradeOpen() { return tradeOpen; } + +function initTradeSocket(socket) { + // Incoming trade request + socket.on('trade_request', (data) => { + playSound('click'); + // Show trade request modal + const overlay = document.createElement('div'); + overlay.id = 'tradeRequestOverlay'; + overlay.className = 'custom-modal-overlay'; + overlay.innerHTML = ` +
+
🤝 ${data.from_name} хочет торговать
+ +
`; + document.getElementById('game').appendChild(overlay); + + document.getElementById('tradeAcceptBtn').addEventListener('click', () => { + overlay.remove(); + socket.emit('trade_accept', { from_socket_id: data.from }); + }); + document.getElementById('tradeDeclineBtn').addEventListener('click', () => { + overlay.remove(); + socket.emit('trade_decline', { from_socket_id: data.from }); + }); + }); + + socket.on('trade_start', (data) => { + currentTradeId = data.trade_id; + myOffer = {}; + partnerOffer = {}; + iConfirmed = false; + tradeOpen = true; + renderTradeUI(data.partner_name); + state.tradePanel.style.display = 'block'; + }); + + socket.on('trade_update', (data) => { + partnerOffer = data.player1Offer || data.player2Offer || {}; + // Determine which offer is the partner's + const isP1 = state.socket && state.socket.id === data.player1Offer; + // The partner's offer is the one we didn't send + // Actually the server sends both — we figure out which is ours + // Simpler: just use the data directly + if (state.mySocketId) { + // We need to know which offer in the update is ours vs partner's + // The trade_update event doesn't tell us, but we know myOffer already + // So partnerOffer = the one that's not myOffer + } + renderTradeOffers(); + }); + + socket.on('trade_status', (data) => { + // Update confirm buttons + const confirmBtn = document.getElementById('tradeConfirmBtn'); + if (confirmBtn) { + const amIPlayer1 = true; // We need to track this + if (iConfirmed) { + confirmBtn.textContent = '✅ Ожидание...'; + confirmBtn.disabled = true; + confirmBtn.style.background = '#555'; + } + } + // Show partner confirmation status + const partnerStatus = document.getElementById('partnerConfirmStatus'); + if (partnerStatus) { + const partnerConfirmed = data.player1Confirm || data.player2Confirm; + partnerStatus.textContent = partnerConfirmed ? '✅ Подтвердил' : '⏳ Ещё не подтвердил'; + } + }); + + socket.on('trade_complete', (data) => { + // Add received items to inventory + const received = data.received || {}; + for (const [item, qty] of Object.entries(received)) { + if (qty > 0) { + state.inv[item] = (state.inv[item] || 0) + qty; + } + } + // Remove sent items from inventory + const sent = data.sent || {}; + for (const [item, qty] of Object.entries(sent)) { + if (qty > 0) { + state.inv[item] = (state.inv[item] || 0) - qty; + if (state.inv[item] < 0) state.inv[item] = 0; + } + } + addChatMessage('Система', '🤝 Сделка завершена!'); + closeTrade(); + }); + + socket.on('trade_declined', () => { + addChatMessage('Система', 'Торговля отклонена'); + closeTrade(); + }); + + socket.on('trade_cancelled', () => { + addChatMessage('Система', 'Торговля отменена'); + closeTrade(); + }); + + socket.on('trade_error', (data) => { + addChatMessage('Система', '❌ ' + data.msg); + }); +} + +function renderTradeUI(partnerName) { + const panel = state.tradePanel; + const content = state.tradeContent; + content.innerHTML = ` +
🤝 Торговля с ${partnerName}
+
+
+
Вы предлагаете:
+
+ +
+
⇄
+
+
Партнёр предлагает:
+
+ Ожидание... +
+
+
+
⏳ Партнёр ещё не подтвердил
+
+ + +
`; + + document.getElementById('tradeAddItemBtn')?.addEventListener('click', showAddItemDialog); + document.getElementById('tradeConfirmBtn')?.addEventListener('click', confirmTrade); + document.getElementById('tradeCancelBtn')?.addEventListener('click', cancelTrade); +} + +function renderTradeOffers() { + const myItems = document.getElementById('myOfferItems'); + if (myItems) { + const keys = Object.keys(myOffer).filter(k => myOffer[k] > 0); + if (keys.length === 0) { + myItems.innerHTML = 'Ничего'; + } else { + myItems.innerHTML = keys.map(k => `
${itemIcon(k)} ${itemName(k)} x${myOffer[k]}
`).join(''); + } + } +} + +function showAddItemDialog() { + const inv = state.inv; + const items = Object.keys(inv).filter(id => inv[id] > 0); + if (items.length === 0) { + addChatMessage('Система', 'Инвентарь пуст!'); + return; + } + + const overlay = document.createElement('div'); + overlay.id = 'tradeItemPicker'; + overlay.className = 'custom-modal-overlay'; + let html = '
Что предложить?
'; + for (const id of items) { + // Show already offered count + const offered = myOffer[id] || 0; + const available = inv[id] - offered; + if (available <= 0) continue; + html += `
+ ${itemIcon(id)} + ${itemName(id)} + x${available} +
`; + } + html += '
'; + overlay.innerHTML = html; + document.getElementById('game').appendChild(overlay); + + overlay.querySelectorAll('.trade-pick-item').forEach(el => { + el.addEventListener('click', () => { + const itemId = el.dataset.item; + // Ask quantity + const available = inv[itemId] - (myOffer[itemId] || 0); + const qty = Math.min(available, available > 1 ? promptForQty(itemId, available) : 1); + if (qty > 0) { + myOffer[itemId] = (myOffer[itemId] || 0) + qty; + renderTradeOffers(); + // Reset confirm when offer changes + iConfirmed = false; + const btn = document.getElementById('tradeConfirmBtn'); + if (btn) { btn.textContent = '✅ Подтвердить'; btn.disabled = false; btn.style.background = '#2ecc71'; } + } + overlay.remove(); + }); + }); +} + +function promptForQty(itemId, max) { + // Simple: just use 1 for now, can enhance later with a proper dialog + const val = parseInt(prompt(`Сколько ${itemName(itemId)}? (макс: ${max})`, '1')); + return isNaN(val) || val < 1 ? 1 : Math.min(val, max); +} + +function confirmTrade() { + if (!currentTradeId || !state.socket) return; + + // Deduct offered items from inventory + for (const [item, qty] of Object.entries(myOffer)) { + if (qty > 0) { + if (state.inv[item] < qty) { + addChatMessage('Система', `Недостаточно ${itemName(item)}!`); + return; + } + } + } + + // Send offer + confirm + state.socket.emit('trade_offer', { trade_id: currentTradeId, offer: myOffer }); + + // Small delay then confirm + setTimeout(() => { + state.socket.emit('trade_confirm', { trade_id: currentTradeId }); + iConfirmed = true; + const btn = document.getElementById('tradeConfirmBtn'); + if (btn) { btn.textContent = '✅ Ожидание...'; btn.disabled = true; btn.style.background = '#555'; } + }, 200); +} + +function cancelTrade() { + if (!currentTradeId || !state.socket) return; + state.socket.emit('trade_cancel', { trade_id: currentTradeId }); + closeTrade(); +} + +function closeTrade() { + tradeOpen = false; + currentTradeId = null; + myOffer = {}; + partnerOffer = {}; + iConfirmed = false; + if (state.tradePanel) state.tradePanel.style.display = 'none'; + // Remove any pick overlay + document.getElementById('tradeItemPicker')?.remove(); +} + +function requestTradeWithPlayer(targetName) { + if (!state.socket || !state.isMultiplayer) { + addChatMessage('Система', 'Торговля доступна только в мультиплеере'); + return; + } + state.socket.emit('trade_request', { target_name: targetName }); + addChatMessage('Система', `Запрос отправлен ${targetName}...`); +} + +// === multiplayer/socket.js === +// Socket.IO клиент — инициализация и все обработчики + + + + + + + + + + + + + + +// Mob sizes for server mob sync (server doesn't send w/h) +const MOB_SIZES = { + pig: { w: 34, h: 24 }, + chicken: { w: 26, h: 22 }, + zombie: { w: 34, h: 50 }, + creeper: { w: 34, h: 50 }, + skeleton: { w: 34, h: 50 }, + scorpion: { w: 26, h: 26 }, + polar_bear: { w: 40, h: 34 }, + slime: { w: 24, h: 24 }, + eagle: { w: 30, h: 22 }, +}; + +function applyMobSize(sm) { + const s = MOB_SIZES[sm.kind]; + if (s && !sm.w) { sm.w = s.w; sm.h = s.h; } + return sm; +} + +let socket = null; + + + +function initSocket() { + try { + socket = io(SERVER_URL, { + path: '/socket.io/', + transports: ['websocket', 'polling'] + }); + + // Store socket on state for other modules + state.socket = socket; + + socket.on('connect', () => { + console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${state.worldId}`); + state.mySocketId = socket.id; + state.isMultiplayer = true; + + // Присоединяемся к миру + socket.emit('join_world', { world_id: state.worldId, player_name: state.playerName }); + + // Показываем в UI + if (state.worldIdEl) state.worldIdEl.textContent = state.worldId; + if (state.multiplayerStatus) state.multiplayerStatus.style.display = 'block'; + + // Initialize trade socket handlers + initTradeSocket(socket); + }); + + socket.on('connect_error', (error) => { + console.error('Socket connection error:', error); + state.isMultiplayer = false; + }); + + socket.on('disconnect', () => { + console.log('Disconnected from server'); + state.isMultiplayer = false; + state.otherPlayers.clear(); + if (state.multiplayerStatus) state.multiplayerStatus.style.display = 'none'; + }); + + // Обработка world_state + socket.on('world_state', (data) => { + console.log('Received world_state:', data); + + if (data.seed !== undefined && data.seed !== state.worldSeed) { + const oldSeed = state.worldSeed; + state.worldSeed = data.seed; + console.log('World seed changed from', oldSeed, 'to', state.worldSeed); + + state.generated.clear(); + grid.clear(); + blocks.length = 0; + state.placedBlocks = []; + state.removedBlocks = []; + console.log('World regenerated with new seed:', state.worldSeed); + } + + if (data.blocks && Array.isArray(data.blocks)) { + for (const block of data.blocks) { + const key = k(block.gx, block.gy, block.gz || 0); + state.serverOverrides.set(key, { op: block.op, t: block.t }); + if (block.op === 'set') { + setBlock(block.gx, block.gy, block.gz || 0, block.t, false); + } else if (block.op === 'remove') { + removeBlock(block.gx, block.gy, block.gz || 0); + } + } + } + + if (data.time !== undefined) { + state.worldTime = data.time; + state.isNightTime = state.worldTime > 0.5; + } + + // Spawn point + { + const startGX = 6; + for (let dx = -4; dx <= 4; dx++) for (let dz = -24; dz <= 24; dz++) genColumn(startGX + dx, dz); + const surfaceY = surfaceGyAt(startGX, Math.floor((state.player.z || 0) / state.TILE)); + let safeGY = surfaceY - 1; + const aboveBlock = getBlock(startGX, surfaceY - 1); + if (aboveBlock && aboveBlock.t === 'water') { + for (let gy = SEA_GY - 1; gy >= 0; gy--) { + const b = getBlock(startGX, gy, Math.floor((state.player.z || 0) / state.TILE)); + if (!b || b.dead || b.t === 'air' || b.t === 'water') continue; + safeGY = gy - 1; + break; + } + } + state.spawnPoint.x = startGX * TILE; + state.spawnPoint.y = safeGY * TILE; + console.log('Client-side spawn point:', state.spawnPoint, 'surfaceY:', surfaceY, 'safeGY:', safeGY); + } + + state.player.x = state.spawnPoint.x; + state.player.y = state.spawnPoint.y; + state.player.vx = 0; + state.player.vy = 0; + state.player.fallStartY = state.player.y; + console.log('Player moved to spawn point:', state.player.x, state.player.y); + + state.player.hp = 100; + state.player.hunger = 100; + state.player.o2 = 100; + state.player.invuln = 0; + console.log('[MULTIPLAYER CONNECT] Player HP set to 100% on connect'); + + if (data.players && Array.isArray(data.players)) { + state.otherPlayers.clear(); + for (const p of data.players) { + if (p.socket_id !== state.mySocketId) { + state.otherPlayers.set(p.socket_id, { + x: p.x, + y: p.y, + color: getRandomPlayerColor(p.socket_id), + name: p.player_name || 'Игрок' + }); + } + } + state.playerCountEl.textContent = data.players.length; + } + // Server mobs + if (data.mobs && Array.isArray(data.mobs)) { + state.serverMobs.clear(); + for (const m of data.mobs) { + let sm = { ...m, maxHp: m.maxHp || m.hp, vx: m.vx || 0, vy: m.vy || 0, grounded: false, inWater: false, aiT: 0, dir: m.dir || 1, dead: false, fuse: m.fuse || 0, shootCooldown: 2, speed: m.speed || 80 }; + applyMobSize(sm); + state.serverMobs.set(m.id, sm); + } + } + + // Set myTgId from server if provided + if (data.tg_id) state.myTgId = data.tg_id; + }); + + socket.on('player_joined', (data) => { + console.log('Player joined:', data.socket_id); + if (data.socket_id !== state.mySocketId) { + const spawnGX = 6; + genColumn(spawnGX, Math.floor((state.player.z || 0) / state.TILE)); + const surfaceY = surfaceGyAt(spawnGX, Math.floor((state.player.z || 0) / state.TILE)); + const safeSpawnX = spawnGX * TILE; + const safeSpawnY = (surfaceY - 1) * TILE; + + state.otherPlayers.set(data.socket_id, { + x: safeSpawnX, + y: safeSpawnY, + color: getRandomPlayerColor(data.socket_id), + name: data.player_name || 'Игрок' + }); + addChatMessage('Система', `Игрок присоединился`); + updateSaveButtonVisibility(); + } + }); + + socket.on('player_moved', (data) => { + if (data.socket_id !== state.mySocketId && state.otherPlayers.has(data.socket_id)) { + const p = state.otherPlayers.get(data.socket_id); + p.x = data.x; + p.y = data.y; + if (data.player_name) { + p.name = data.player_name; + } + } + }); + + socket.on('player_left', (data) => { + console.log('Player left:', data.socket_id); + state.otherPlayers.delete(data.socket_id); + addChatMessage('Система', `Игрок покинул игру`); + updateSaveButtonVisibility(); + }); + + // === MOB SYNC === + socket.on('mob_spawned', (data) => { + let sm = { ...data, maxHp: data.maxHp || data.hp, vx: data.vx || 0, vy: data.vy || 0, grounded: false, inWater: false, aiT: 0, dir: data.dir || 1, dead: false, fuse: data.fuse || 0, shootCooldown: 2, speed: data.speed || 80 }; + applyMobSize(sm); + state.serverMobs.set(data.id, sm); + }); + + socket.on('mob_positions', (arr) => { + for (const u of arr) { + const sm = state.serverMobs.get(u.id); + if (sm) { sm.x = u.x; sm.y = u.y; sm.vx = u.vx; sm.vy = u.vy; sm.dir = u.dir; sm.hp = u.hp; sm.fuse = u.fuse || 0; } + } + }); + + socket.on('mob_despawned', (data) => { state.serverMobs.delete(data.id); }); + + socket.on('mob_died', (data) => { + const sm = state.serverMobs.get(data.id); + if (sm && data.killer === state.mySocketId) { + if (sm.kind === 'chicken') playSound('hurt_chicken'); + state.inv.meat += (sm.kind === 'chicken' ? 1 : 2); + if (sm.kind === 'skeleton') { + state.inv.arrow += 2 + Math.floor(Math.random() * 3); + if (Math.random() < 0.15) state.inv.bow = (state.inv.bow || 0) + 1; + } + rebuildHotbar(); + } + state.serverMobs.delete(data.id); + }); + + socket.on('mob_hurt_ack', (data) => { + const sm = state.serverMobs.get(data.id); + if (sm) sm.hp = data.hp; + }); + + socket.on('mob_explode', (data) => { + explodeTNT3D({ gx: data.gx, gy: data.gy, gz: data.gz || 0 }, state); + state.serverMobs.delete(data.id); + }); + + socket.on('mob_shoot', (data) => { + state.projectiles.push({ + x: data.x, y: data.y, vx: data.vx, vy: data.vy, + dmg: data.dmg, owner: 'mob', life: data.life + }); + }); + + socket.on('block_changed', (data) => { + const key = k(data.gx, data.gy, data.gz || 0); + state.serverOverrides.set(key, { op: data.op, t: data.t }); + if (data.op === 'set') { + setBlock(data.gx, data.gy, data.gz || 0, data.t, false); + } else if (data.op === 'remove') { + removeBlock(data.gx, data.gy, data.gz || 0); + } + }); + + socket.on('chat_message', (data) => { + const senderName = data.socket_id === state.mySocketId ? 'Вы' : `Игрок ${data.socket_id.substring(0, 6)}`; + addChatMessage(senderName, data.message); + }); + + socket.on('time_update', (data) => { + if (data.time !== undefined) { + state.worldTime = data.time; + state.isNightTime = state.worldTime > 0.5; + } + }); + + } catch (e) { + console.error('Error initializing socket:', e); + state.isMultiplayer = false; + } +} + +// === physics/water-detect3d.js === +// ==================== ВОДА 3D — GrechkaCraft v3 ==================== +// Та же логика 2D-игры (кислород, всплеск, болото), но с учётом Z. + + + + + +function blockAt3(px, py, pz) { + const gx = Math.floor(px / TILE); + const gy = Math.floor(py / TILE); + const gz = Math.floor(pz / TILE); + return getBlock(gx, gy, gz); +} + +function isWaterAt(px, py, pz) { + const b = blockAt3(px, py, pz); + return !!(b && (b.t === 'water' || b.t === 'swamp_water')); +} + +function isSwampWaterAt(px, py, pz) { + const b = blockAt3(px, py, pz); + return !!(b && b.t === 'swamp_water'); +} + +// Обновляет флаги воды на сущности (игрок или моб). +// dt передаётся явно — в 2D-версии он был неявной переменной цикла. +function updateWaterFlag3(e, dt) { + const cx = e.x + e.w / 2; + const cz = (e.z || 0) + (e.d || e.w) / 2; + const wasInWater = e.inWater; + + const mid = isWaterAt(cx, e.y + e.h / 2, cz); + const feet = isWaterAt(cx, e.y + e.h - 2, cz); + e.inWater = mid || feet; + + e.inSwampWater = isSwampWaterAt(cx, e.y + e.h / 2, cz); + e.headInWater = isWaterAt(cx, e.y + 4, cz); + + if (e === state.player && !wasInWater && e.inWater && e.vy > 100) { + playSound('splash'); + } + if (e === state.player && e.inSwampWater) { + state.player.hp -= 3 * (typeof dt === 'number' ? dt : 0.016); + } +} + +// Лодка тонет, если под ней нет воды (с учётом Z). +function updateWaterPhysics3(dt) { + void dt; +} + +// === physics/collision3d.js === +// ==================== ФИЗИКА 3D — GrechkaCraft v3 ==================== +// Осевое разрешение столкновений по X → Z → Y, как в Minecraft. +// Автошаг на 1 блок, прыжок, лестницы, плавание, лодка. + + + + + +const EPS = 0.001; + +// Собственная AABB-коробка сущности: x/z — центр, y — низ (как в 2D-игре: e.x, e.y = левый-ниний) +function boxOf(e) { + return { + x0: e.x, x1: e.x + e.w, + y0: e.y, y1: e.y + e.h, + z0: e.z, z1: e.z + e.d, + }; +} + +function solidAt(gx, gy, gz) { + const b = getBlock(gx, gy, gz); + if (!b) return false; + const def = BLOCKS[b.t]; + if (!def) return false; + if (def.fluid) return false; + if (def.decor || def.climbable) return false; + return !!def.solid; +} + +function climbableAt(gx, gy, gz) { + const b = getBlock(gx, gy, gz); + return !!(b && BLOCKS[b.t] && BLOCKS[b.t].climbable); +} + +function fluidAt(gx, gy, gz) { + const b = getBlock(gx, gy, gz); + return !!(b && BLOCKS[b.t] && BLOCKS[b.t].fluid); +} + +// Пересекает ли коробка сущности твёрдый блок +function hitsSolid(bx) { + const x0 = Math.floor(bx.x0 / TILE), x1 = Math.floor((bx.x1 - EPS) / TILE); + const y0 = Math.floor(bx.y0 / TILE), y1 = Math.floor((bx.y1 - EPS) / TILE); + const z0 = Math.floor(bx.z0 / TILE), z1 = Math.floor((bx.z1 - EPS) / TILE); + for (let gx = x0; gx <= x1; gx++) { + for (let gy = y0; gy <= y1; gy++) { + for (let gz = z0; gz <= z1; gz++) { + if (solidAt(gx, gy, gz)) return true; + } + } + } + return false; +} + +// Стоит ли сущность на чём-то (для grounded) +function checkGrounded(e) { + const bx = boxOf(e); + const gy = Math.floor((bx.y1 + 1) / TILE); + const x0 = Math.floor(bx.x0 / TILE), x1 = Math.floor((bx.x1 - EPS) / TILE); + const z0 = Math.floor(bx.z0 / TILE), z1 = Math.floor((bx.z1 - EPS) / TILE); + for (let gx = x0; gx <= x1; gx++) { + for (let gz = z0; gz <= z1; gz++) { + if (solidAt(gx, gy, gz)) return true; + } + } + return false; +} + +function onLadder(e) { + const cx = Math.floor((e.x + e.w / 2) / TILE); + const cy = Math.floor((e.y + e.h / 2) / TILE); + const cz = Math.floor((e.z + e.d / 2) / TILE); + return climbableAt(cx, cy, cz); +} + +function inFluid(e) { + const cx = Math.floor((e.x + e.w / 2) / TILE); + const cy = Math.floor((e.y + e.h * 0.4) / TILE); + const cz = Math.floor((e.z + e.d / 2) / TILE); + return fluidAt(cx, cy, cz); +} + +// Разрешение одной оси. delta — смещение за кадр. +function moveAxis(e, axis, delta) { + if (delta === 0) return false; + const bx = boxOf(e); + if (axis === 'x') { bx.x0 += delta; bx.x1 += delta; } + else if (axis === 'z') { bx.z0 += delta; bx.z1 += delta; } + else { bx.y0 += delta; bx.y1 += delta; } + + if (!hitsSolid(bx)) { + if (axis === 'x') e.x += delta; + else if (axis === 'z') e.z += delta; + else e.y += delta; + return false; + } + + const dir = Math.sign(delta); + + // Автошаг вверх на 1 блок + if ((axis === 'x' || axis === 'z') && e.grounded) { + const savedX = e.x, savedY = e.y, savedZ = e.z; + if (axis === 'x') e.x += delta; else e.z += delta; + e.y -= TILE; + if (!hitsSolid(boxOf(e))) { + return true; // шаг удался, продолжим движение + } + e.x = savedX; e.y = savedY; e.z = savedZ; + } + + // Откат к границе блока + // шаг за блок, пока не пересечение + const stepPx = dir * 0.5; + let moved = 0; + for (let i = 0; i < TILE * 4; i++) { + const test = boxOf(e); + if (axis === 'x') { test.x0 += stepPx; test.x1 += stepPx; } + else if (axis === 'z') { test.z0 += stepPx; test.z1 += stepPx; } + else { test.y0 += stepPx; test.y1 += stepPx; } + if (hitsSolid(test)) break; + moved += stepPx; + if (axis === 'x') e.x += stepPx; + else if (axis === 'z') e.z += stepPx; + else e.y += stepPx; + } + + if (axis === 'x') e.vx = 0; + else if (axis === 'z') e.vz = 0; + else e.vy = 0; + return true; +} + +function resolveX(e, dt) { return moveAxis(e, 'x', e.vx * dt); } +function resolveZ(e, dt) { return moveAxis(e, 'z', (e.vz || 0) * dt); } +function resolveY(e, dt) { + const hit = moveAxis(e, 'y', e.vy * dt); + e.grounded = false; + if (hit && e.vy >= 0) { + e.grounded = checkGrounded(e); + } else if (!hit) { + e.grounded = checkGrounded(e); + } + return hit; +} + +// === data/recipes.js === +const RECIPES = [ + { out:'planks', qty:4, cost:{ wood:1 } }, + { out:'ladder', qty:3, cost:{ planks:7 } }, + { out:'torch', qty:2, cost:{ coal:1, planks:1 } }, + { out:'glass', qty:1, cost:{ sand:3 } }, + { out:'brick', qty:1, cost:{ stone:2, clay:1 } }, + { out:'campfire', qty:1, cost:{ wood:1, coal:1 } }, + { out:'tnt', qty:1, cost:{ sand:2, coal:1 } }, + { out:'bed', qty:1, cost:{ wood: 3, planks: 3 } }, + { out:'boat', qty:1, cost:{ wood: 5 } }, + { out:'wood_pickaxe', qty:1, cost:{ wood: 3, planks: 2 } }, + { out:'stone_pickaxe', qty:1, cost:{ wood: 1, stone: 3 } }, + { out:'iron_pickaxe', qty:1, cost:{ wood: 1, iron_ore: 2 } }, + { out:'wood_sword', qty:1, cost:{ wood: 2, planks: 1 } }, + { out:'stone_sword', qty:1, cost:{ wood: 1, stone: 2 } }, + { out:'iron_sword', qty:1, cost:{ wood: 1, iron_ore: 1 } }, + { out:'wood_axe', qty:1, cost:{ wood:3, planks:2 } }, + { out:'stone_axe', qty:1, cost:{ wood:1, stone:3 } }, + { out:'iron_axe', qty:1, cost:{ wood:1, iron_ore:2 } }, + { out:'wood_shovel', qty:1, cost:{ wood:1, planks:1 } }, + { out:'stone_shovel', qty:1, cost:{ wood:1, stone:2 } }, + { out:'iron_shovel', qty:1, cost:{ wood:1, iron_ore:1 } }, + { out:'iron_armor', qty:1, cost:{ iron_ore: 5 } }, + { out:'gold_armor', qty:1, cost:{ gold_ore: 8 } }, + { out:'furnace', qty:1, cost:{ stone: 8 } }, + { out:'bow', qty:1, cost:{ wood: 3, planks: 2 } }, + { out:'arrow', qty:4, cost:{ stone: 1, wood: 1 } }, + { out:'flag', qty:1, cost:{ planks:3, iron_ore:1 } }, + { out:'bread', qty:1, cost:{ wheat: 3 } }, + { out:'farmland', qty:1, cost:{ dirt:1 } }, + { out:'wheat_seeds', qty:1, cost:{ farmland:1, dirt:1 } }, +]; + +// Рецепты печи (обжиг) +const SMELTING_RECIPES = [ + { in:'sand', qty:1, out:'glass', outQty:1, time:3 }, + { in:'clay', qty:1, out:'brick', outQty:1, time:3 }, + { in:'iron_ore', qty:1, out:'iron_ingot', outQty:1, time:5 }, + { in:'gold_ore', qty:1, out:'gold_ingot', outQty:1, time:5 }, + { in:'copper_ore', qty:1, out:'copper_ingot', outQty:1, time:4 }, + { in:'meat', qty:1, out:'cooked', outQty:1, time:2 }, + { in:'cobblestone', qty:1, out:'stone', outQty:1, time:2 }, + { in:'potato', qty:1, out:'baked_potato', outQty:1, time:2 } +]; + +// === ui/furnace.js === +// ==================== ПЕЧЬ (ОБЖИГ) ==================== + + + + + + + + +const furnacePanel = document.getElementById('furnacePanel'); +const furnaceContent = document.getElementById('furnaceContent'); + +function initFurnace() { + document.getElementById('furnaceClose').onclick = () => { + furnacePanel.style.display = 'none'; + state.currentFurnaceKey = null; + }; +} + +function openFurnaceUI(gx, gy, gz) { + state.currentFurnaceKey = `${gx},${gy},${gz || 0}`; + furnacePanel.style.display = 'block'; + renderFurnaceUI(); +} + +function renderFurnaceUI() { + if (!state.currentFurnaceKey) return; + + // Проверяем что печь всё ещё существует + const [fgx, fgy, fgz] = state.currentFurnaceKey.split(',').map(Number); + const fb = getBlock(fgx, fgy, fgz || 0); + if (!fb || fb.t !== 'furnace') { + furnacePanel.style.display = 'none'; + state.currentFurnaceKey = null; + return; + } + + // Текущий процесс обжига + const active = state.activeFurnaces.get(state.currentFurnaceKey); + + let html = '
'; + + // Доступные рецепты — показываем только те, для которых есть ресурсы + for (let i = 0; i < SMELTING_RECIPES.length; i++) { + const recipe = SMELTING_RECIPES[i]; + const haveCount = state.inv[recipe.in] || 0; + const canSmelt = haveCount >= recipe.qty; + + // Иконка результата + const outDef = BLOCKS[recipe.out]; + const outItem = ITEMS[recipe.out]; + const iconStr = outItem ? outItem.icon : (outDef ? '🧱' : '❓'); + const nameStr = outItem ? outItem.n : (outDef ? outDef.n : recipe.out); + const inItem = ITEMS[recipe.in]; + const inDef = BLOCKS[recipe.in]; + const inName = inItem ? inItem.n : (inDef ? inDef.n : recipe.in); + + html += `
`; + html += `
${iconStr}
`; + html += `
`; + html += `
${nameStr}
`; + html += `
${inName} x${recipe.qty} (есть: ${haveCount}) • ${recipe.time}с
`; + html += `
`; + html += ``; + html += `
`; + } + + // Текущий прогресс + if (active) { + const pct = Math.min(100, Math.floor((active.progress / active.recipe.time) * 100)); + html += `
`; + html += `
🔥 Обжиг: ${pct}%
`; + html += `
`; + html += `
`; + html += `
`; + } + + html += '
'; + furnaceContent.innerHTML = html; +} + +// Глобальная функция для кнопки обжига +window._smelt = (recipeIdx) => { + if (!state.currentFurnaceKey) return; + const recipe = SMELTING_RECIPES[recipeIdx]; + if ((state.inv[recipe.in] || 0) < recipe.qty) return; + + // Уже обжигаем в этой печи? + if (state.activeFurnaces.has(state.currentFurnaceKey)) return; + + // Забираем ресурсы + state.inv[recipe.in] -= recipe.qty; + + // Запускаем обжиг + state.activeFurnaces.set(state.currentFurnaceKey, { + recipe: recipe, + progress: 0 + }); + + playSound('fire'); + rebuildHotbar(); + renderFurnaceUI(); +}; + +// Тик печей — вызывается в главном цикле +function tickFurnaces(dt) { + for (const [key, furnace] of state.activeFurnaces) { + furnace.progress += dt; + if (furnace.progress >= furnace.recipe.time) { + // Обжиг завершён — выдаём результат + const outItem = furnace.recipe.out; + if (ITEMS[outItem]) { + state.inv[outItem] = (state.inv[outItem] || 0) + furnace.recipe.outQty; + } else if (BLOCKS[outItem]) { + state.inv[outItem] = (state.inv[outItem] || 0) + furnace.recipe.outQty; + } + playSound('stone_build'); + state.activeFurnaces.delete(key); + + // Если эта печь открыта — обновляем UI + if (key === state.currentFurnaceKey) { + renderFurnaceUI(); + } + } + } +} + +// === entities/mob-ai3d.js === +// ==================== МОБЫ 3D: ИИ + ФИЗИКА — GrechkaCraft v3 ==================== +// Вся логика из 2D-игры (все 9 видов, ночные/дневные, лут), но цель — +// настоящая 3D-точка игрока (x,y,z) и прыжки через стены по всем осям. + + + + + + + + + +function isNight() { + return state.worldTime > 0.5; +} + +// Центры сущностей в тайлах +function cx(e) { return (e.x + e.w / 2) / TILE; } +function cy(e) { return (e.y + e.h / 2) / TILE; } +function cz(e) { return ((e.z || 0) + (e.d || e.w) / 2) / TILE; } + +function playerC() { + const p = state.player; + return { x: cx(p), y: cy(p), z: cz(p) }; +} + +function dist3(a, b) { + return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z); +} + +// Тянет моба к игроку по обеим горизонтальным осям (X и Z) +function steerToPlayer(m, pc) { + const dx = pc.x - cx(m); + const dz = pc.z - cz(m); + const len = Math.hypot(dx, dz) || 1; + m.vx = (dx / len) * (m.speed || 60); + m.vz = (dz / len) * (m.speed || 60); + m.dir = dx >= 0 ? 1 : -1; + m.faceYaw = Math.atan2(dz, dx); +} + +function touchPlayer(m, pc, damage, knock = 1) { + const d = dist3({ x: cx(m), y: cy(m), z: cz(m) }, pc); + if (d < 1.6 && state.player.invuln <= 0) { + state.player.hp -= calculateDamage(damage); + state.player.invuln = 0.8; + const dx = state.player.x - m.x; + const dz = (state.player.z || 0) - (m.z || 0); + const len = Math.hypot(dx, dz) || 1; + state.player.vx += (dx / len) * 420 * knock; + state.player.vz += (dz / len) * 420 * knock; + state.player.vy -= 260 * knock; + playSound('hit1'); + return true; + } + return false; +} + +function mobAI(m, dt) { + updateWaterFlag3(m, dt); + const pc = playerC(); + const night = isNight(); + + // ---------- ночные мобы днём горят ---------- + if ((m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton') && !night) { + m.hp -= 30 * dt; + m.vx *= 0.5; + m.vz *= 0.5; + return; + } + + if (m.kind === 'zombie') { + steerToPlayer(m, pc); + if (m.inWater && Math.random() < 0.06) m.vy = -260; + touchPlayer(m, pc, 15); + } else if (m.kind === 'creeper') { + steerToPlayer(m, pc); + const d = dist3({ x: cx(m), y: cy(m), z: cz(m) }, pc); + if (m.inWater && Math.random() < 0.06) m.vy = -260; + if (d < 3) { + m.fuse -= dt; + if (m.fuse <= 0) { + explodeTNT3D({ gx: Math.floor(cx(m)), gy: Math.floor(cy(m)), gz: Math.floor(cz(m)) }, state); + m.hp = 0; + } + } else if (d < 2.5) { + m.fuse = 0.5; + } + } else if (m.kind === 'skeleton') { + steerToPlayer(m, pc); + const d = dist3({ x: cx(m), y: cy(m), z: cz(m) }, pc); + m.shootCooldown -= dt; + if (d < 15 && m.shootCooldown <= 0) { + m.shootCooldown = 2.0; + const ox = m.x + m.w / 2, oy = m.y + m.h / 3, oz = (m.z || 0) + (m.d || m.w) / 2; + const tx = state.player.x + state.player.w / 2; + const ty = state.player.y + state.player.h / 2; + const tz = (state.player.z || 0) + (state.player.d || state.player.w) / 2; + // полёт стрелы по 3D + const ddx = tx - ox, ddy = ty - oy, ddz = tz - oz; + const dist = Math.hypot(ddx, ddy, ddz) || 1; + const speed = 450; + state.projectiles.push({ + x: ox, y: oy, z: oz, + vx: ddx / dist * speed, + vy: ddy / dist * speed, + vz: ddz / dist * speed, + dmg: 6, owner: 'mob', life: 3, + }); + } + } else if (m.kind === 'scorpion') { + steerToPlayer(m, pc); + if (m.inWater && Math.random() < 0.06) m.vy = -260; + if (touchPlayer(m, pc, 8)) state.player.slowTimer = 3; + } else if (m.kind === 'polar_bear') { + // нейтрален: бродит + m.aiT -= dt; + if (m.aiT <= 0) { + m.aiT = 2.0 + Math.random() * 3; + const a = Math.random() * Math.PI * 2; + m.walkX = Math.cos(a); + m.walkZ = Math.sin(a); + if (Math.random() < 0.3) { m.walkX = 0; m.walkZ = 0; } + } + m.vx = (m.walkX || 0) * m.speed; + m.vz = (m.walkZ || 0) * m.speed; + if (m.inWater) m.vy = -120; + touchPlayer(m, pc, 10, 1.2); + } else if (m.kind === 'slime') { + // прыгает к игроку + m.aiT -= dt; + if (m.aiT <= 0) { + m.aiT = 1.5 + Math.random() * 1.5; + steerToPlayer(m, pc); + m.vy = -200; + } + touchPlayer(m, pc, 8); + } else if (m.kind === 'eagle') { + // летает, пикирует + const d = dist3({ x: cx(m), y: cy(m), z: cz(m) }, pc); + if (d < 20) { + const dx = pc.x - cx(m), dz = pc.z - cz(m), dy = pc.y - cy(m); + const hl = Math.hypot(dx, dz) || 1; + m.vx = dx / hl * m.speed; + m.vz = dz / hl * m.speed; + m.vy = dy > 0 ? 60 : -60; + } else { + m.aiT -= dt; + if (m.aiT <= 0) { + m.aiT = 2 + Math.random() * 3; + const a = Math.random() * Math.PI * 2; + m.walkX = Math.cos(a); + m.walkZ = Math.sin(a); + } + m.vx = (m.walkX || 0) * m.speed * 0.5; + m.vz = (m.walkZ || 0) * m.speed * 0.5; + m.vy = Math.sin(performance.now() / 1000) * 30; + } + if (d < 1.6 && state.player.invuln <= 0) { + state.player.hp -= calculateDamage(10); + state.player.invuln = 0.8; + state.player.vy -= 200; + playSound('hit1'); + } + m.vy *= 0.5; + m.flying = true; + } else { + // животные (pig, chicken): бродят + m.aiT -= dt; + if (m.aiT <= 0) { + m.aiT = 1.8 + Math.random() * 2.5; + const a = Math.random() * Math.PI * 2; + m.walkX = Math.cos(a); + m.walkZ = Math.sin(a); + if (Math.random() < 0.25) { m.walkX = 0; m.walkZ = 0; } + } + const sp = m.kind === 'chicken' ? 55 : 40; + m.vx = (m.walkX || 0) * sp; + m.vz = (m.walkZ || 0) * sp; + if (m.inWater) m.vy = -120; + } + + // ---------- прыжок через препятствие по всем осям ---------- + if (m.grounded && !m.flying && m.kind !== 'eagle') { + const speed = Math.hypot(m.vx || 0, m.vz || 0); + if (speed > 5) { + const gx = Math.floor((m.x + (m.vx > 0 ? m.w + 2 : -2)) / TILE); + const gz = Math.floor(((m.z || 0) + (m.vz > 0 ? (m.d || m.w) + 2 : -2)) / TILE); + const gy = Math.floor((m.y + m.h - 1) / TILE); + const bx = getBlock(gx, gy, Math.floor(cz(m))); + const bz = getBlock(Math.floor(cx(m)), gy, gz); + if (bx || bz) m.vy = -280; + } + } + + // ---------- ФИЗИКА ---------- + if (!m.flying) { + const g = m.inWater ? GRAV_WATER : GRAV; + m.vy += g * dt; + } + m.y += m.vy * dt; + m.x += (m.vx || 0) * dt; + m.z += (m.vz || 0) * dt; + resolveX(m, dt); + resolveZ(m, dt); + resolveY(m, dt); + + // орёл не падает + if (m.flying) m.grounded = false; +} + +// Лут — та же таблица, что в 2D +function getMobLoot(kind) { + const table = { + chicken: [{item:'chicken_meat',min:1,max:2,chance:1},{item:'feather',min:0,max:1,chance:0.5}], + pig: [{item:'meat',min:1,max:2,chance:1}], + zombie: [{item:'meat',min:0,max:1,chance:0.5},{item:'iron_ingot',min:0,max:1,chance:0.3}], + skeleton: [{item:'arrow',min:2,max:4,chance:1},{item:'bone',min:0,max:2,chance:0.6},{item:'bow',min:1,max:1,chance:0.15}], + creeper: [{item:'gunpowder',min:1,max:2,chance:1}], + scorpion: [{item:'scorpion_stinger',min:0,max:1,chance:0.4}], + polar_bear:[{item:'polar_fur',min:1,max:2,chance:0.8},{item:'meat',min:1,max:2,chance:1}], + slime: [{item:'slime_ball',min:1,max:2,chance:1}], + eagle: [{item:'eagle_feather',min:1,max:2,chance:0.7}], + }; + return table[kind] || []; +} + +// === world/weather.js === +// ==================== ПОГОДА (biome-aware) ==================== + + + + + +// Возможные погоды по биому +const BIOME_WEATHER = { + snow: ['clear', 'snow', 'blizzard'], + tundra: ['clear', 'snow', 'blizzard'], + forest: ['clear', 'rain', 'storm'], + plains: ['clear', 'rain'], + desert: ['clear', 'clear', 'rain'], + swamp: ['clear', 'rain', 'storm'], + mountains: ['clear', 'rain', 'snow'] +}; + +// Помощник интерполяции цветов +function lerpColor(a, b, t) { + const ar = parseInt(a.slice(1,3),16), ag = parseInt(a.slice(3,5),16), ab = parseInt(a.slice(5,7),16); + const br = parseInt(b.slice(1,3),16), bg = parseInt(b.slice(3,5),16), bb = parseInt(b.slice(5,7),16); + const r = Math.round(ar + (br - ar) * t); + const g = Math.round(ag + (bg - ag) * t); + const bl = Math.round(ab + (bb - ab) * t); + return '#' + ((1<<24)|(r<<16)|(g<<8)|bl).toString(16).slice(1); +} + +// Цвета неба по биому, погоде и времени суток +function skyColor(night, biome, weatherType, worldTime) { + function weatherSky() { + switch (weatherType) { + case 'blizzard': return '#8a9bae'; + case 'storm': return '#3d4f5f'; + case 'rain': return '#6B7B8D'; + case 'snow': return '#b8c6d4'; + case 'clear': return null; + default: return null; + } + } + + const dayColor = (biome === 'desert') ? '#f4d9a0' : (biome === 'tundra' || biome === 'snow') ? '#c4d4e0' : (biome === 'swamp') ? '#4a6741' : '#87CEEB'; + const dawnColor = '#FF9940'; + const duskMidColor = '#FF6B35'; + const duskEndColor = '#1a0533'; + const nightColor = '#070816'; + + let skyColorResult; + + if (worldTime < 0.05) { + const t = worldTime / 0.05; + skyColorResult = lerpColor(dawnColor, dayColor, t); + } else if (worldTime < 0.45) { + skyColorResult = dayColor; + } else if (worldTime < 0.55) { + const t = (worldTime - 0.45) / 0.1; + if (t < 0.5) { + skyColorResult = lerpColor(dayColor, duskMidColor, t * 2); + } else { + skyColorResult = lerpColor(duskMidColor, duskEndColor, (t - 0.5) * 2); + } + } else { + skyColorResult = nightColor; + if (weatherType === 'blizzard') return '#1a1a2e'; + if (weatherType === 'storm') return '#0a0a15'; + return nightColor; + } + + const wc = weatherSky(); + if (wc) { + const blendT = 0.6; + skyColorResult = lerpColor(skyColorResult, wc, blendT); + } + + return skyColorResult; +} + +function updateWeather(dt) { + state.weatherTimer += dt; + if (state.weatherTimer >= state.weatherChangeInterval) { + state.weatherTimer = 0; + state.weatherChangeInterval = 60 + Math.random() * 120; + + // Определяем биом по позиции игрока + const playerGx = Math.floor(state.player.x / state.TILE); + const biome = getCachedBiome(playerGx, Math.floor((state.player.z || 0) / state.TILE)); + const weathers = BIOME_WEATHER[biome] || ['clear']; + + const night = isNight(); + const weights = weathers.map(w => { + if (w === 'clear') return night ? 2 : 4; + if (w === 'rain') return night ? 3 : 2; + if (w === 'snow') return night ? 2 : 3; + if (w === 'storm') return night ? 3 : 1; + if (w === 'blizzard') return night ? 3 : 1; + return 1; + }); + const total = weights.reduce((a, b) => a + b, 0); + let r = Math.random() * total; + state.weatherType = weathers[0]; + for (let i = 0; i < weathers.length; i++) { + r -= weights[i]; + if (r <= 0) { state.weatherType = weathers[i]; break; } + } + + state.isRaining = (state.weatherType === 'rain' || state.weatherType === 'storm'); + } + + // Плавная интерполяция интенсивности дождя + const rainTarget = (state.weatherType === 'rain' || state.weatherType === 'storm') ? 0.6 : 0; + state.rainIntensity += (rainTarget - state.rainIntensity) * dt * 0.5; + if (state.rainIntensity < 0.01) state.rainIntensity = 0; + + // Молния (только при storm) + if (state.weatherType === 'storm') { + state.lightningTimer -= dt; + if (state.lightningTimer <= 0) { + state.lightningFlash = 0.25; + state.lightningTimer = 4 + Math.random() * 12; + } + } + if (state.lightningFlash > 0) { + state.lightningFlash -= dt; + if (state.lightningFlash < 0) state.lightningFlash = 0; + } + + // Снежинки (при snow / blizzard) + if (state.weatherType === 'snow' || state.weatherType === 'blizzard') { + const spawnCount = state.weatherType === 'blizzard' ? Math.floor(80 * dt) : Math.floor(30 * dt); + const maxFlakes = state.weatherType === 'blizzard' ? 600 : 250; + for (let i = 0; i < spawnCount && state.snowflakes.length < maxFlakes; i++) { + state.snowflakes.push({ + x: state.camX + Math.random() * state.W, + y: state.camY - 20, + vy: 40 + Math.random() * 60, + vx: (state.weatherType === 'blizzard' ? -60 - Math.random() * 80 : -10 - Math.random() * 20), + size: 2 + Math.random() * 3, + wobble: Math.random() * Math.PI * 2 + }); + } + } + // Обновление снежинок + for (let i = state.snowflakes.length - 1; i >= 0; i--) { + const s = state.snowflakes[i]; + s.y += s.vy * dt; + s.x += s.vx * dt; + s.wobble += dt * 3; + s.x += Math.sin(s.wobble) * 15 * dt; + if (s.y > state.camY + state.H + 20 || s.x < state.camX - 50 || s.x > state.camX + state.W + 50) { + state.snowflakes.splice(i, 1); + } + } + // Убираем снег при смене погоды + if (state.weatherType !== 'snow' && state.weatherType !== 'blizzard' && !state.isRaining) { + if (state.snowflakes.length > 0) { + state.snowflakes.splice(0, Math.ceil(state.snowflakes.length * dt * 2)); + } + } +} + +function updateRain(dt) { + if (!state.isRaining || state.rainIntensity < 0.01) { + if (state.raindrops.length > 0) { + state.raindrops.splice(0, Math.ceil(state.raindrops.length * dt * 3)); + } + return; + } + const spawnRate = Math.floor(state.rainIntensity * 60 * dt); + for (let i = 0; i < spawnRate && state.raindrops.length < state.MAX_RAINDROPS; i++) { + state.raindrops.push({ + x: state.camX + Math.random() * state.W, + y: state.camY - 20, + vy: 400 + Math.random() * 200, + len: 8 + Math.random() * 12 + }); + } + for (let i = state.raindrops.length - 1; i >= 0; i--) { + const d = state.raindrops[i]; + d.y += d.vy * dt; + d.x -= 30 * dt; + if (d.y > state.camY + state.H + 20) { + state.raindrops.splice(i, 1); + } + } +} + +function drawWeather(ctx) { + if (state.raindrops.length > 0) { + ctx.save(); + ctx.strokeStyle = 'rgba(174,194,224,0.5)'; + ctx.lineWidth = 1.5; + ctx.beginPath(); + for (const d of state.raindrops) { + ctx.moveTo(d.x, d.y); + ctx.lineTo(d.x - 3, d.y + d.len); + } + ctx.stroke(); + ctx.restore(); + } + + if (state.snowflakes.length > 0) { + ctx.save(); + ctx.fillStyle = 'rgba(255,255,255,0.85)'; + for (const s of state.snowflakes) { + ctx.beginPath(); + ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2); + ctx.fill(); + } + ctx.restore(); + } + + if (state.lightningFlash > 0) { + ctx.save(); + ctx.fillStyle = `rgba(255,255,255,${Math.min(1, state.lightningFlash * 4)})`; + ctx.fillRect(0, 0, state.W, state.H); + ctx.restore(); + } +} + +// === entities/mobs3d.js === +// Сущности: животные + зомби + новые мобы + +class Entity { + constructor(x, y, w, h) { + this.x = x; + this.y = y; + this.z = 0; // третье измерение: глубина мира + this.w = w; + this.h = h; + this.d = w; // размер по Z + this.vx = 0; + this.vy = 0; + this.vz = 0; + this.hp = 3; + this.grounded = false; + this.inWater = false; + this.aiT = 0; + this.dir = 1; + } +} + +class Pig extends Entity { + constructor(x, y, z) { super(x, y, 34, 24); this.kind = 'pig'; this.z = z || 0; this.hp = 2; } +} + +class Chicken extends Entity { + constructor(x, y, z) { super(x, y, 26, 22); this.kind = 'chicken'; this.z = z || 0; this.hp = 1; } +} + +class Zombie extends Entity { + constructor(x, y, z) { super(x, y, 34, 50); this.kind = 'zombie'; this.z = z || 0; this.hp = 4; this.speed = 80 + Math.random() * 40; } +} + +class Creeper extends Entity { + constructor(x, y, z) { super(x, y, 34, 50); this.kind = 'creeper'; this.z = z || 0; this.hp = 4; this.speed = 60 + Math.random() * 30; this.fuse = 3.2; } +} + +class Skeleton extends Entity { + constructor(x, y, z) { super(x, y, 34, 50); this.kind = 'skeleton'; this.z = z || 0; this.hp = 4; this.speed = 70 + Math.random() * 30; this.shootCooldown = 0; } +} + +class Scorpion extends Entity { + constructor(x, y, z) { super(x, y, 26, 26); this.kind = 'scorpion'; this.z = z || 0; this.hp = 3; this.speed = 90; this.fuse = 0; this.shootCooldown = 0; } +} + +class PolarBear extends Entity { + constructor(x, y, z) { super(x, y, 40, 34); this.kind = 'polar_bear'; this.z = z || 0; this.hp = 8; this.speed = 50; this.fuse = 0; this.shootCooldown = 2; } +} + +class Slime extends Entity { + constructor(x, y, z) { super(x, y, 24, 24); this.kind = 'slime'; this.z = z || 0; this.hp = 2; this.speed = 30; this.fuse = 0; this.shootCooldown = 2; } +} + +class Eagle extends Entity { + constructor(x, y, z) { super(x, y, 30, 22); this.kind = 'eagle'; this.z = z || 0; this.hp = 3; this.speed = 120; this.fuse = 0; this.shootCooldown = 0; } +} + +// === game/modes.js === +// ==================== РЕЖИМЫ ==================== + + + +const MODES = [{ id: 'mine', icon: '⛏️' }, { id: 'build', icon: '🧱' }]; + + + +function mode() { + return MODES[state.modeIdx].id; +} + +function initModes() { + const modeBtn = document.getElementById('modeBtn'); + modeBtn.onclick = () => { + playSound('click'); // Звук клика по кнопке режима + state.modeIdx = (state.modeIdx + 1) % MODES.length; + modeBtn.textContent = MODES[state.modeIdx].icon; + }; +} + +// === input/mouse-handler3d.js === +// ==================== ВЗАИМОДЕЙСТВИЕ 3D (мышь/тап) — GrechkaCraft v3 ==================== +// Ключевое отличие от 2D: блок добывается/ставится ТОЛЬКО если игрок рядом +// (в пределах REACH). Касание экрана вдали — лишь поворот камеры. + + + + + + + + + + + + + + + +// ---------- ДИСТАНЦИЯ ДОБЫЧИ (как в майнкрафте) ---------- +const REACH = 5.2; // тайлов от центра игрока до центра блока +const REACH_SQ = REACH * REACH; + +function distToBlockSq(gx, gy, gz) { + const p = state.player; + const TILE = state.TILE; + const px = (p.x + p.w / 2) / TILE; + const py = (p.y + p.h / 2) / TILE; + const pz = ((p.z || 0) + (p.d || p.w) / 2) / TILE; + const dx = gx + 0.5 - px; + const dy = gy + 0.5 - py; + const dz = (gz || 0) + 0.5 - pz; + return dx * dx + dy * dy + dz * dz; +} + +function withinReach(gx, gy, gz) { + return distToBlockSq(gx, gy, gz) <= REACH_SQ; +} + +// Моб тоже бьётся только вблизи +function withinReachEntity(e) { + const p = state.player; + const TILE = state.TILE; + const px = (p.x + p.w / 2) / TILE; + const py = (p.y + p.h / 2) / TILE; + const pz = ((p.z || 0) + (p.d || p.w) / 2) / TILE; + const ex = (e.x + e.w / 2) / TILE; + const ey = (e.y + e.h / 2) / TILE; + const ez = ((e.z || 0) + (e.d || e.w) / 2) / TILE; + const dx = ex - px, dy = ey - py, dz = ez - pz; + return dx * dx + dy * dy + dz * dz <= REACH_SQ; +} + +const CROP_SEED_MAP = { + wheat_seeds: 'wheat_stage0', + carrot: 'carrot_stage0', + potato: 'potato_stage0', +}; +const CROP_HARVEST_MAP = { wheat_stage3: 'wheat', carrot_stage3: 'carrot', potato_stage3: 'potato' }; + +function handleBlockDrop(removed, inv) { + if (removed.t === 'grass') { + if (Math.random() < 0.5) inv.wheat_seeds = (inv.wheat_seeds || 0) + 1; + return; + } + const harvestItem = CROP_HARVEST_MAP[removed.t]; + const seedItem = CROP_SEED_MAP[removed.t]; + if (harvestItem) { + inv[harvestItem] = (inv[harvestItem] || 0) + (BLOCKS[removed.t].harvestQty || 1); + addToRecentItems(harvestItem); + if (seedItem) { inv[seedItem] = (inv[seedItem] || 0) + 1; addToRecentItems(seedItem); } + } else if (seedItem) { + inv[seedItem] = (inv[seedItem] || 0) + 1; + addToRecentItems(seedItem); + } else { + inv[removed.t] = (inv[removed.t] || 0) + 1; + addToRecentItems(removed.t); + } +} + +const mouse = { x: null, y: null }; + +let miningHeld = false; +let miningInterval = null; + +function stopMining() { + miningHeld = false; + if (miningInterval) { clearInterval(miningInterval); miningInterval = null; } +} + +// Основное действие. coords — результат рейкаста; face — нормаль грани. +function handleTap(wx, wy, gz, face) { + if (state.craftOpen) return; + if (state.player.hp <= 0) return; + + const TILE = state.TILE; + // gz приходит из рейкаста; если нет — берём Z игрока + const z = gz === undefined || gz === null ? Math.floor((state.player.z || 0) / TILE) : gz; + + // Ближайший блок к точке касания + let gx = Math.floor(wx / TILE); + let gy = Math.floor(wy / TILE); + let b = getBlock(gx, gy, z); + + // Если блока нет — пробуем тот же столбец, но сдвинутые на 1 (декоративные/плоские) + if (!b) { + for (const dy of [-1, 1, 0]) { + const cand = getBlock(gx, gy + dy, z); + if (cand) { gy += dy; b = cand; break; } + } + } + + if (!b) return; + + // ---------- ДОСТИЖИМОСТЬ ---------- + if (!withinReach(gx, gy, z)) { + // Слишком далеко: подсказка, ничего не ломаем + showReachHint(); + return; + } + + // Пробуждение: клик по кровати когда спишь + if (state.player.sleeping && b.t === 'bed') { + state.player.sleeping = false; + return; + } + if (state.player.sleeping) return; + + // Печь — открываем панель обжига + if (b.t === 'furnace' && mode() === 'mine') { + openFurnaceUI(gx, gy, z); + return; + } + + // ---------- Урон мобам (только вблизи) ---------- + if (mode() === 'mine') { + let handled = false; + if (state.isMultiplayer) { + for (const [id, sm] of state.serverMobs) { + if (sm.dead) continue; + if (!withinReachEntity(sm)) continue; + if (state.player.invuln <= 0) { + let dmg = 1; + for (const st of ['iron_sword', 'stone_sword', 'wood_sword']) { + if (state.inv[st] > 0) { dmg = TOOLS[st].damage || 3; useTool(st); break; } + } + state.socket.emit('mob_hurt', { id, dmg }); + playSound('attack'); + handled = true; + } + break; + } + } + if (!handled) { + for (let i = state.mobs.length - 1; i >= 0; i--) { + const m = state.mobs[i]; + if (!withinReachEntity(m)) continue; + let dmg = 1; + for (const st of ['iron_sword', 'stone_sword', 'wood_sword']) { + if (state.inv[st] > 0) { dmg = TOOLS[st].damage || 3; useTool(st); break; } + } + m.hp -= dmg; + const kx = (state.player.x + state.player.w / 2) - (m.x + m.w / 2); + const kz = ((state.player.z || 0) + (state.player.d || state.player.w) / 2) - ((m.z || 0) + (m.d || m.w) / 2); + m.vx += (kx / (Math.hypot(kx, kz) || 1)) * 400; + m.vz += (kz / (Math.hypot(kx, kz) || 1)) * 400; + m.vy -= 200; + playSound('attack'); + if (m.hp <= 0) { + if (m.kind === 'chicken') playSound('hurt_chicken'); + state.inv.meat += (m.kind === 'chicken' ? 1 : 2); + if (m.kind === 'skeleton') { + state.inv.arrow += 2 + Math.floor(Math.random() * 3); + if (Math.random() < 0.15) state.inv.bow = (state.inv.bow || 0) + 1; + } + state.mobs.splice(i, 1); + rebuildHotbar(); + } + handled = true; + break; + } + } + if (handled) return; + } + + // ---------- Лук ---------- + if (state.selected === 'bow' && state.inv.bow > 0 && state.inv.arrow > 0) { + const p = state.player; + const ox = p.x + p.w / 2, oy = p.y + p.h / 3, oz = (p.z || 0) + (p.d || p.w) / 2; + const tx = wx, ty = wy, tz = (z + 0.5) * TILE; + const dx = tx - ox, dy = ty - oy, dz = tz - oz; + const dist = Math.hypot(dx, dy, dz) || 1; + const speed = 550; + state.projectiles.push({ + x: ox, y: oy, z: oz, + vx: dx / dist * speed, vy: dy / dist * speed, vz: dz / dist * speed, + dmg: 10, owner: 'player', life: 4, + }); + state.inv.arrow--; + useTool('bow'); + playSound('hit1'); + rebuildHotbar(); + return; + } + + // ---------- Еда ---------- + if (ITEMS[state.selected] && state.inv[state.selected] > 0) { + const it = ITEMS[state.selected]; + if (state.player.hp < 100 || state.player.hunger < 100) { + playSound('eat1'); + state.player.hunger = Math.min(100, state.player.hunger + it.food); + state.player.hp = Math.min(100, state.player.hp + 15); + state.inv[state.selected]--; + rebuildHotbar(); + } + return; + } + + // ---------- Жарка на костре ---------- + if (b.t === 'campfire' && state.selected === 'meat' && state.inv.meat > 0) { + playSound('fire'); + state.inv.meat--; + state.inv.cooked = (state.inv.cooked || 0) + 1; + rebuildHotbar(); + return; + } + + // ---------- Сон ---------- + if (b.t === 'bed' && isNight()) { + state.player.sleeping = true; + saveGame(); + return; + } + + // ---------- ДОБЫЧА ---------- + if (mode() === 'mine') { + const def = BLOCKS[b.t]; + if (def.fluid) return; + if (def.decor && def.growthStage === undefined && !def.harvestable) return; + + if (b.t === 'tnt') { activateTNT3D(b, 3.2); return; } + + const hardness = def.hardness || 1; + if (hardness === Infinity) return; + + if (hardness === 0) { + const removed = removeBlock(gx, gy, z); + if (removed) { + handleBlockDrop(removed, state.inv); + sendBlockChange(gx, gy, z, removed.t, 'remove'); + rebuildHotbar(); + } + return; + } + + if (state.mining.gx === gx && state.mining.gy === gy && (state.mining.gz || 0) === z) { + state.mining.progress += getMiningSpeed(b.t); + } else { + state.mining = { gx, gy, gz: z, progress: getMiningSpeed(b.t), hardness }; + } + + if (b.t === 'glass') playSound('glass1'); + else if (b.t === 'sand') playSound('sand1'); + else if (b.t === 'stone' || b.t.endsWith('_ore')) playSound('stone1'); + else if (b.t === 'wood' || b.t === 'planks') playSound('wood1'); + else playSound('cloth1'); + + if (state.mining.progress >= hardness) { + finishMining(gx, gy, z, b.t); + } else { + startHoldMining(gx, gy, z, b.t); + } + return; + } + + // ---------- СТРОИТЕЛЬСТВО ---------- + if (mode() === 'build') { + // посев на грядку + const seedToCrop = CROP_SEED_MAP[state.selected]; + if (seedToCrop && state.inv[state.selected] > 0) { + if (b.t === 'farmland' && !getBlock(gx, gy - 1, z)) { + setBlock(gx, gy - 1, z, seedToCrop, true); + state.inv[state.selected]--; + rebuildHotbar(); + return; + } + if (!b) { + const below = getBlock(gx, gy + 1, z); + if (below && below.t === 'farmland') { + setBlock(gx, gy, z, seedToCrop, true); + state.inv[state.selected]--; + rebuildHotbar(); + return; + } + } + } + + // цель — соседняя клетка по нормали грани + let tx2 = gx, ty2 = gy, tz2 = z; + if (face) { + if (Math.abs(face.x) >= Math.abs(face.y) && Math.abs(face.x) >= Math.abs(face.z)) tx2 += Math.sign(face.x); + else if (Math.abs(face.y) >= Math.abs(face.z)) ty2 += Math.sign(face.y); + else tz2 += Math.sign(face.z); + } else { + ty2 -= 1; + } + // по Y: в игре +y вниз, three +y вверх → нормаль -y = вниз = +gy + if (face && Math.abs(face.y) >= Math.abs(face.x) && Math.abs(face.y) >= Math.abs(face.z)) { + ty2 = gy - Math.sign(face.y); + } + + if (getBlock(tx2, ty2, tz2)) return; + if (state.inv[state.selected] <= 0) return; + if (!BLOCKS[state.selected]) return; + + // не ставим в себя + const p = state.player; + const bx = tx2 * TILE, by = ty2 * TILE, bz = tz2 * TILE; + const overlap = !(bx >= p.x + p.w || bx + TILE <= p.x || + by >= p.y + p.h || by + TILE <= p.y || + bz >= (p.z || 0) + (p.d || p.w) || bz + TILE <= (p.z || 0)); + if (overlap) return; + + setBlock(tx2, ty2, tz2, state.selected, true); + state.inv[state.selected]--; + sendBlockChange(tx2, ty2, tz2, state.selected, 'set'); + + if (state.selected === 'stone' || state.selected === 'brick') playSound('stone_build'); + else if (state.selected === 'wood' || state.selected === 'planks') playSound('wood_build'); + else if (state.selected === 'glass') playSound('glass1'); + else if (state.selected === 'sand') playSound('sand1'); + else playSound('cloth1'); + + rebuildHotbar(); + } +} + +function finishMining(gx, gy, gz, type) { + const def = BLOCKS[type]; + const minTool = def.minTool; + let drops = true; + if (minTool) { + const toolId = getBestTool(type); + if (toolId) { + const tool = TOOLS[toolId]; + if (toolTierLessThan(tool.toolTier, minTool)) drops = false; + else useTool(toolId); + } else { + drops = false; + } + } else { + const toolId = getBestTool(type); + if (toolId) useTool(toolId); + } + + const removed = removeBlock(gx, gy, gz); + if (removed) { + if (drops) { + handleBlockDrop(removed, state.inv); + } else if (def.toolType === 'pickaxe') { + state.inv.cobblestone = (state.inv.cobblestone || 0) + 1; + } + sendBlockChange(gx, gy, gz, removed.t, 'remove'); + playSound('stone1'); + rebuildHotbar(); + } + stopMining(); + state.mining = { gx: null, gy: null, gz: null, progress: 0, hardness: 0 }; +} + +function startHoldMining(gx, gy, gz, type) { + miningHeld = true; + if (miningInterval) clearInterval(miningInterval); + miningInterval = setInterval(() => { + if (!miningHeld) { clearInterval(miningInterval); miningInterval = null; return; } + const mg = state.mining; + if (mg.gx === null) { miningHeld = false; clearInterval(miningInterval); miningInterval = null; return; } + const mb = getBlock(mg.gx, mg.gy, mg.gz || 0); + if (!mb || mb.dead || mb.t !== type) { + miningHeld = false; + clearInterval(miningInterval); + miningInterval = null; + return; + } + if (!withinReach(mg.gx, mg.gy, mg.gz || 0)) { + // отошли — добыча прекращается + miningHeld = false; + clearInterval(miningInterval); + miningInterval = null; + state.mining = { gx: null, gy: null, gz: null, progress: 0, hardness: 0 }; + return; + } + mg.progress += getMiningSpeed(mb.t) * 0.3; + if (mg.progress >= mg.hardness) { + finishMining(mg.gx, mg.gy, mg.gz || 0, mb.t); + } + }, 100); +} + +// Подсказка «слишком далеко» — один элемент, не спамит +let hintEl = null; +let hintTimer = null; +function showReachHint() { + if (!hintEl) { + hintEl = document.createElement('div'); + hintEl.style.cssText = 'position:fixed;left:50%;top:18%;transform:translateX(-50%);' + + 'z-index:300;background:rgba(0,0,0,.72);color:#fff;padding:8px 14px;border-radius:8px;' + + 'font:600 14px system-ui;pointer-events:none;'; + document.body.appendChild(hintEl); + } + hintEl.textContent = 'Слишком далеко — подойди ближе'; + hintEl.style.display = 'block'; + clearTimeout(hintTimer); + hintTimer = setTimeout(() => { if (hintEl) hintEl.style.display = 'none'; }, 1200); +} + +// === render/hero-rig.js === +// ==================== ДАННЫЕ ГЕРОЯ (из платформёра grechka_game.html) ==================== +// COL/PARTS/PIVOT дословно оттуда: 107 прямоугольников, 5 частей (head/body/legs/tail/eye). +// Ноль rig = ступни; высота рисунка ~3.07 юнита при PARTS-координатах. + +const HERO_COL = { + blue: { hex: 0x0232fe, d: 0.60 }, + white: { hex: 0xffffff, d: 0.86 }, + cyan: { hex: 0x00a1ff, d: 1.00 }, + navy: { hex: 0x001a85, d: 1.12 }, +}; + +const HERO_PARTS = {"head":[[-0.90035,2.93184,0.40909,0.13636,"blue"],[-1.17308,2.79547,0.13636,0.13636,"blue"],[-0.62762,2.79547,0.13636,0.13636,"blue"],[-0.96853,2.65911,0.27273,0.13636,"blue"],[-0.49126,2.65911,0.13636,0.13636,"blue"],[-1.17308,2.52275,0.13636,0.13636,"blue"],[-0.62762,2.52275,0.13636,0.13636,"blue"],[0.12238,2.52275,1.09091,0.13636,"blue"],[-0.83217,2.38638,0.81818,0.13636,"blue"],[1.00874,2.38638,0.68182,0.13636,"blue"],[-0.83217,2.25002,0.27273,0.13636,"blue"],[1.41783,2.25002,0.13636,0.13636,"blue"],[-1.03671,2.11365,0.13636,0.13636,"blue"],[1.28147,2.11365,0.13636,0.13636,"blue"],[0.12238,2.38638,1.09091,0.13636,"white"],[0.32692,2.25002,2.04545,0.13636,"white"],[-0.6958,2.11365,0.54545,0.13636,"white"],[-0.15035,2.11365,0.27273,0.13636,"white"],[0.66783,2.11365,1.09091,0.13636,"white"],[-0.3549,2.11365,0.13636,0.13636,"cyan"],[0.0542,2.11365,0.13636,0.13636,"cyan"]],"body":[[-1.17308,1.97729,0.13636,0.13636,"blue"],[1.1451,1.97729,0.13636,0.13636,"blue"],[-0.96853,1.84093,0.27273,0.13636,"blue"],[1.28147,1.77275,0.13636,0.27273,"blue"],[-0.90035,1.70456,0.13636,0.13636,"blue"],[-1.1049,1.5682,0.27273,0.13636,"blue"],[-0.42308,1.5682,0.27273,0.13636,"blue"],[0.53147,1.5682,0.27273,0.13636,"blue"],[1.07692,1.5682,0.27273,0.13636,"blue"],[1.41783,1.5682,0.13636,0.13636,"blue"],[-1.30944,1.43184,0.13636,0.13636,"blue"],[1.00874,1.43184,0.13636,0.13636,"blue"],[1.28147,1.43184,0.13636,0.13636,"blue"],[-1.17308,1.15911,0.13636,0.40909,"blue"],[1.1451,1.22729,0.13636,0.27273,"blue"],[-1.4458,1.02275,0.13636,0.13636,"blue"],[1.00874,1.02275,0.13636,0.13636,"blue"],[-1.4458,0.88638,0.40909,0.13636,"blue"],[-0.96853,0.88638,0.27273,0.13636,"blue"],[-0.08217,0.88638,0.68182,0.13636,"blue"],[0.87238,0.88638,0.13636,0.13636,"blue"],[-1.51399,0.75002,0.54545,0.13636,"blue"],[-0.90035,0.75002,0.13636,0.13636,"blue"],[0.66783,0.75002,0.27273,0.13636,"blue"],[1.00874,0.75002,0.13636,0.13636,"blue"],[-0.6958,1.97729,0.81818,0.13636,"white"],[-0.01399,1.97729,0.27273,0.13636,"white"],[0.8042,1.97729,0.54545,0.13636,"white"],[-0.6958,1.77275,0.27273,0.27273,"white"],[-0.21853,1.84093,0.13636,0.13636,"white"],[0.32692,1.84093,0.13636,0.13636,"white"],[0.94056,1.77275,0.54545,0.27273,"white"],[0.0542,1.63638,0.68182,0.27273,"white"],[-0.76399,1.5682,0.40909,0.13636,"white"],[0.8042,1.5682,0.27273,0.13636,"white"],[1.28147,1.5682,0.13636,0.13636,"white"],[-1.1049,1.43184,0.27273,0.13636,"white"],[-0.01399,1.43184,1.36364,0.13636,"white"],[-0.21853,1.29547,1.5,0.13636,"white"],[0.87238,1.29547,0.40909,0.13636,"white"],[-0.90035,1.15911,0.13636,0.13636,"white"],[-0.08217,1.15911,1.22727,0.13636,"white"],[0.73601,1.15911,0.13636,0.13636,"white"],[1.00874,1.15911,0.13636,0.13636,"white"],[-1.03671,1.02275,0.13636,0.13636,"white"],[-0.01399,1.02275,1.36364,0.13636,"white"],[-0.62762,0.88638,0.40909,0.13636,"white"],[0.53147,0.88638,0.54545,0.13636,"white"],[-0.15035,0.75002,1.36364,0.13636,"white"],[0.87238,0.75002,0.13636,0.13636,"white"],[-0.21853,1.97729,0.13636,0.13636,"cyan"],[0.32692,1.97729,0.40909,0.13636,"cyan"],[0.0542,1.84093,0.40909,0.13636,"cyan"],[-0.83217,1.43184,0.27273,0.13636,"cyan"],[0.8042,1.43184,0.27273,0.13636,"cyan"],[-1.03671,1.22729,0.13636,0.27273,"cyan"],[0.59965,1.22729,0.13636,0.27273,"cyan"],[-0.76399,1.15911,0.13636,0.13636,"cyan"],[0.87238,1.15911,0.13636,0.13636,"cyan"],[-0.83217,1.02275,0.27273,0.13636,"cyan"],[0.8042,1.02275,0.27273,0.13636,"cyan"]],"tail":[[-1.71853,0.61365,0.13636,0.13636,"blue"],[-1.03671,0.61365,0.13636,0.13636,"blue"],[-0.62762,0.61365,0.40909,0.13636,"blue"],[-1.65035,0.47729,0.27273,0.13636,"blue"],[-0.90035,0.47729,0.13636,0.13636,"blue"],[-1.4458,0.34093,0.40909,0.13636,"blue"],[-0.62762,0.34093,0.40909,0.13636,"blue"],[-0.90035,0.20456,0.95455,0.13636,"blue"],[-0.90035,0.61365,0.13636,0.13636,"white"]],"legs":[[0.3951,0.61365,0.27273,0.13636,"blue"],[0.87238,0.61365,0.13636,0.13636,"blue"],[-0.55944,0.47729,0.27273,0.13636,"blue"],[0.32692,0.34093,0.13636,0.40909,"blue"],[-0.21853,0.20456,0.13636,0.13636,"blue"],[0.0542,0.20456,0.13636,0.13636,"blue"],[-0.08217,0.0682,0.95455,0.13636,"blue"],[-0.3549,0.47729,0.13636,0.13636,"white"],[-0.08217,0.34093,0.68182,0.13636,"white"],[-0.3549,0.20456,0.13636,0.13636,"white"],[-0.08217,0.20456,0.13636,0.13636,"white"],[0.19056,0.20456,0.13636,0.13636,"white"],[-0.08217,0.61365,0.68182,0.13636,"cyan"],[-0.01399,0.47729,0.54545,0.13636,"cyan"]],"eye":[[-0.42308,1.77275,0.27273,0.27273,"navy"],[0.53147,1.77275,0.27273,0.27273,"navy"]]}; + +const HERO_PIVOT = {"head":[0.0,2.5909],"body":[0.0,0.979],"legs":[-0.035,0.9091],"tail":[-0.7343,1.3287],"eye":[0.0524,2.1294]}; + +const HERO_SCALE = 0.50; + +// === render/render3d.js === +// ==================== РЕНДЕР 3D — GrechkaCraft v3 ==================== +// Объёмный мир: у каждого блока есть gx/gy/gz. Рендерим кубы по всем трём +// осям, отсекаем те, что не видны с камеры (соседи со всех шести сторон). + + + + + + + + + + +let TD = null; + +function TDparseColor(c) { + const m = /rgba?\(([^)]+)\)/.exec(c); + if (!m) return { color: new THREE.Color(c), opacity: 1 }; + const p = m[1].split(',').map(x => parseFloat(x)); + return { color: new THREE.Color(p[0] / 255, p[1] / 255, p[2] / 255), opacity: p.length > 3 ? p[3] : 1 }; +} + +function TDblockMat(t) { + const def = BLOCKS[t] || {}; + const { color, opacity } = TDparseColor(def.c || '#ff00ff'); + const a = def.alpha !== undefined ? def.alpha : opacity; + return new THREE.MeshLambertMaterial({ color, transparent: a < 1, opacity: a }); +} + +function TDisFluid(t) { const d = BLOCKS[t]; return !!(d && d.fluid); } +function TDisFlat(t) { + const d = BLOCKS[t]; + if (!d) return false; + return !d.solid || !!d.decor || !!d.climbable; +} + +// ---------- герой: тот же пиксельный рисунок, кубики ---------- +function TDbuildHeroRig() { + const group = new THREE.Group(); + const P = {}; + const MAT = {}; + for (const k of Object.keys(HERO_COL)) MAT[k] = new THREE.MeshLambertMaterial({ color: HERO_COL[k].hex }); + for (const name of Object.keys(HERO_PARTS)) { + const g = new THREE.Group(); + const j = HERO_PIVOT[name]; + g.position.set(j[0] * HERO_SCALE, j[1] * HERO_SCALE, 0); + const inner = new THREE.Group(); + inner.position.set(-j[0] * HERO_SCALE, -j[1] * HERO_SCALE, 0); + g.add(inner); + group.add(g); + P[name] = g; + for (const r of HERO_PARTS[name]) { + const depth = HERO_COL[r[4]].d * HERO_SCALE; + const m = new THREE.Mesh( + new THREE.BoxGeometry(r[2] * HERO_SCALE, r[3] * HERO_SCALE, depth), + MAT[r[4]] + ); + m.position.set((r[0] - 0.24) * HERO_SCALE, r[1] * HERO_SCALE, depth / 2); + m.castShadow = true; + m.receiveShadow = true; + inner.add(m); + } + } + return { group, P }; +} + +const TD_MOB_COLORS = { + zombie: 0x2ecc71, pig: 0xffb6c1, chicken: 0xecf0f1, creeper: 0x4caf50, + skeleton: 0xeceff1, scorpion: 0x5d4037, polar_bear: 0xeceff1, + slime: 0x4caf50, eagle: 0x5d4037, +}; + +function TDnameSprite(name) { + const cv = document.createElement('canvas'); + cv.width = 256; cv.height = 48; + const g = cv.getContext('2d'); + g.font = 'bold 26px system-ui'; + g.textAlign = 'center'; + g.fillStyle = 'rgba(0,0,0,0.45)'; + g.fillRect(0, 0, 256, 48); + g.fillStyle = '#fff'; + g.fillText(String(name || '?').slice(0, 14), 128, 33); + const sp = new THREE.Sprite(new THREE.SpriteMaterial({ map: new THREE.CanvasTexture(cv), depthTest: false })); + sp.scale.set(2.2, 0.41, 1); + return sp; +} + +// ==================== INIT ==================== +function TDinit() { + const canvas = state.canvas; + const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); + if (renderer.outputEncoding !== undefined && THREE.sRGBEncoding !== undefined) { + renderer.outputEncoding = THREE.sRGBEncoding; + } + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.BasicShadowMap; + + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0x8fc7f5); + scene.fog = new THREE.Fog(0x8fc7f5, 30, 90); + const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 300); + + const hemi = new THREE.HemisphereLight(0xcfe6ff, 0x6ab04c, 0.85); + const sun = new THREE.DirectionalLight(0xffffff, 1.5); + sun.castShadow = true; + sun.shadow.mapSize.set(512, 512); + const sc = sun.shadow.camera; + sc.left = -26; sc.right = 26; sc.top = 26; sc.bottom = -26; + sc.near = 1; sc.far = 140; + sun.shadow.normalBias = 0.05; + const amb = new THREE.AmbientLight(0xffffff, 0.25); + scene.add(hemi, sun, sun.target, amb); + + const hero = TDbuildHeroRig(); + scene.add(hero.group); + + // Рабочая область: только блоки рядом с игроком (по всем осям) + const crack = new THREE.Mesh( + new THREE.BoxGeometry(1.004, 1.004, 1.004), + new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0 }) + ); + crack.visible = false; + scene.add(crack); + + const ghost = new THREE.Mesh( + new THREE.BoxGeometry(1.02, 1.02, 1.02), + new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true, transparent: true, opacity: 0.9 }) + ); + ghost.visible = false; + scene.add(ghost); + + // куб подсветки выбранного блока + const outline = new THREE.LineSegments( + new THREE.EdgesGeometry(new THREE.BoxGeometry(1.03, 1.03, 1.03)), + new THREE.LineBasicMaterial({ color: 0xffffff }) + ); + outline.visible = false; + scene.add(outline); + + const cloudMeshes = []; + const cloudMat = new THREE.MeshLambertMaterial({ color: 0xffffff, transparent: true, opacity: 0.75 }); + for (let i = 0; i < 16; i++) { + const m = new THREE.Mesh(new THREE.BoxGeometry(4, 0.8, 2.2), cloudMat); + scene.add(m); + cloudMeshes.push(m); + } + + const projPool = []; + for (let i = 0; i < 24; i++) { + const m = new THREE.Mesh(new THREE.BoxGeometry(0.55, 0.1, 0.1), new THREE.MeshBasicMaterial({ color: 0xf1c40f })); + m.visible = false; + scene.add(m); + projPool.push(m); + } + + const PMAX = 800; + const pGeo = new THREE.BufferGeometry(); + const pPos = new Float32Array(PMAX * 3); + const pCol = new Float32Array(PMAX * 3); + pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3)); + pGeo.setAttribute('color', new THREE.BufferAttribute(pCol, 3)); + const points = new THREE.Points(pGeo, new THREE.PointsMaterial({ size: 0.16, vertexColors: true, depthWrite: false })); + points.frustumCulled = false; + scene.add(points); + + function makeFall(n, color, size) { + const g = new THREE.BufferGeometry(); + const pos = new Float32Array(n * 3); + g.setAttribute('position', new THREE.BufferAttribute(pos, 3)); + const pts = new THREE.Points(g, new THREE.PointsMaterial({ color, size, transparent: true, opacity: 0.7, depthWrite: false })); + pts.frustumCulled = false; + pts.visible = false; + scene.add(pts); + return { pts, pos, n }; + } + const rain = makeFall(500, 0x74b9ff, 0.2); + const snow = makeFall(300, 0xffffff, 0.17); + + const lightPool = []; + for (let i = 0; i < 6; i++) { + const L = new THREE.PointLight(0xff9a3c, 0, 16); + scene.add(L); + lightPool.push(L); + } + const flamePool = []; + for (let i = 0; i < 8; i++) { + const f = new THREE.Mesh(new THREE.ConeGeometry(0.3, 0.75, 6), new THREE.MeshBasicMaterial({ color: 0xff7a1a })); + f.visible = false; + scene.add(f); + flamePool.push(f); + } + + const boatMesh = new THREE.Mesh( + new THREE.BoxGeometry(1.4, 0.5, 1.0), + new THREE.MeshLambertMaterial({ color: 0x8b4513 }) + ); + boatMesh.visible = false; + boatMesh.castShadow = true; + scene.add(boatMesh); + + TD = { + renderer, scene, camera, hemi, sun, amb, hero, + crack, ghost, outline, cloudMeshes, projPool, points, pPos, pCol, PMAX, + rain, snow, lightPool, flamePool, boatMesh, + blockMeshes: [], lastVersion: -1, meshT: 0, + mobs: new Map(), players: new Map(), + yaw: 0.0, pitch: 0.42, dist: 10, + ptrs: new Map(), pinchD: 0, + lastTap: null, hovered: null, fireCache: null, + heroStep: 0, face: 1, frame: 0, lastT: 0, + colCache: new Map(), + }; + TDbindInput(canvas); + TDresize(); + window.addEventListener('resize', TDresize); +} + +function TDresize() { + if (!TD) return; + const w = window.innerWidth, h = window.innerHeight; + state.W = w; + state.H = h; + TD.renderer.setPixelRatio(1); + TD.renderer.setSize(w, h, false); + TD.camera.aspect = w / h; + TD.camera.updateProjectionMatrix(); +} + +// ==================== БЛОКИ ==================== +// Полный пересбор мешей редкий; в обычном кадре — только обновление матриц. +const VIEW_R = 14; // радиус прорисовки в тайлах по всем осям + +function isHiddenAt(gx, gy, gz) { + // спрятан ли блок: все 6 соседей непрозрачные? + return isOpaqueAt(gx + 1, gy, gz) && isOpaqueAt(gx - 1, gy, gz) && + isOpaqueAt(gx, gy + 1, gz) && isOpaqueAt(gx, gy - 1, gz) && + isOpaqueAt(gx, gy, gz + 1) && isOpaqueAt(gx, gy, gz - 1); +} + +function isOpaqueAt(gx, gy, gz) { + const b = getBlock(gx, gy, gz); + if (!b) return false; + const def = BLOCKS[b.t]; + if (!def) return false; + return !!def.solid && !def.fluid && !def.decor && !def.climbable && def.alpha === undefined; +} + +function TDrebuildBlocks(px, py, pz) { + for (const e of TD.blockMeshes) { + TD.scene.remove(e.mesh); + e.mesh.geometry.dispose(); + e.mesh.material.dispose(); + } + TD.blockMeshes = []; + const byType = new Map(); + for (const b of state.blocks) { + if (b.dead) continue; + if (!BLOCKS[b.t]) continue; + const dx = b.gx - px, dy = b.gy - py, dz = (b.gz || 0) - pz; // py — игровой gy + if (dx * dx + dy * dy + dz * dz > VIEW_R * VIEW_R) continue; + if (!TDisFluid(b.t) && !TDisFlat(b.t) && isHiddenAt(b.gx, b.gy, b.gz || 0)) continue; + if (!byType.has(b.t)) byType.set(b.t, []); + byType.get(b.t).push(b); + } + const boxGeo = new THREE.BoxGeometry(1, 1, 1); + const planeGeo = new THREE.PlaneGeometry(0.92, 0.92); + for (const [t, list] of byType) { + const fluid = TDisFluid(t); + const flat = !fluid && TDisFlat(t); + const mesh = new THREE.InstancedMesh(flat ? planeGeo : boxGeo, TDblockMat(t), list.length); + mesh.castShadow = !flat; + mesh.receiveShadow = true; + if (flat) mesh.material.side = THREE.DoubleSide; + const m = new THREE.Matrix4(); + const q = new THREE.Quaternion(); + const e = new THREE.Euler(); + for (let i = 0; i < list.length; i++) { + const b = list[i]; + if (flat) { + // плоские блоки (цветы) — поворачиваем к камере, стоят на земле + e.set(-Math.PI / 2, 0, 0); + q.setFromEuler(e); + m.compose(new THREE.Vector3(b.gx + 0.5, -b.gy + 0.05, (b.gz || 0) + 0.5), q, new THREE.Vector3(1, 1, 1)); + } else if (fluid) { + m.makeScale(1, 0.82, 1); + m.setPosition(b.gx + 0.5, -b.gy - 0.09, (b.gz || 0) + 0.5); + } else { + m.identity(); + m.setPosition(b.gx + 0.5, -b.gy - 0.5, (b.gz || 0) + 0.5); + } + mesh.setMatrixAt(i, m); + } + mesh.instanceMatrix.needsUpdate = true; + mesh.userData.list = list; + TD.scene.add(mesh); + TD.blockMeshes.push({ mesh, type: t }); + } +} + +// ==================== PICK (рейкаст по 3D) ==================== +const TDray = new THREE.Raycaster(); +const TDndc = new THREE.Vector2(); + +function TDpick(sx, sy) { + const r = state.canvas.getBoundingClientRect(); + TDndc.x = ((sx - r.left) / r.width) * 2 - 1; + TDndc.y = -((sy - r.top) / r.height) * 2 + 1; + TDray.setFromCamera(TDndc, TD.camera); + const hits = TDray.intersectObjects(TD.blockMeshes.map(x => x.mesh), false); + if (!hits.length) return null; + const h = hits[0]; + const b = h.object.userData.list[h.instanceId]; + if (!b) return null; + // мировые игровые координаты точки удара + const gx = Math.floor(h.point.x); + const gy = -Math.floor(h.point.y); + const gz = Math.floor(h.point.z); + const wx = (h.point.x) * state.TILE; + const wy = (-h.point.y) * state.TILE; + return { gx, gy, gz, wx, wy, face: h.face ? h.face.normal : null }; +} + +// ==================== ВВОД ==================== +function TDbindInput(canvas) { + canvas.addEventListener('pointerdown', (e) => { + TD.ptrs.set(e.pointerId, { x: e.clientX, y: e.clientY, sx: e.clientX, sy: e.clientY, moved: false, t: performance.now() }); + if (TD.ptrs.size === 2) { + const q = [...TD.ptrs.values()]; + TD.pinchD = Math.hypot(q[0].x - q[1].x, q[0].y - q[1].y); + } + stopMining(); + }); + + canvas.addEventListener('pointermove', (e) => { + const q = TD.ptrs.get(e.pointerId); + if (!q) return; + const dx = e.clientX - q.x, dy = e.clientY - q.y; + q.x = e.clientX; q.y = e.clientY; + if (Math.hypot(e.clientX - q.sx, e.clientY - q.sy) > 8) q.moved = true; + if (TD.ptrs.size === 2) { + const v = [...TD.ptrs.values()]; + const d = Math.hypot(v[0].x - v[1].x, v[0].y - v[1].y); + if (TD.pinchD > 0 && d > 0) TD.dist = Math.min(34, Math.max(4, TD.dist * TD.pinchD / d)); + TD.pinchD = d; + return; + } + if (q.moved) { + TD.yaw -= dx * 0.005; + TD.pitch = Math.min(1.45, Math.max(0.06, TD.pitch + dy * 0.005)); + } + }); + + const up = (e) => { + const q = TD.ptrs.get(e.pointerId); + TD.ptrs.delete(e.pointerId); + TD.pinchD = 0; + if (q && !q.moved && performance.now() - q.t < 600) { + const hit = TDpick(e.clientX, e.clientY); + if (hit) { + TD.lastTap = hit; + handleTap(hit.wx, hit.wy, hit.gz, hit.face); + } + } + }; + canvas.addEventListener('pointerup', up); + canvas.addEventListener('pointercancel', (e) => { TD.ptrs.delete(e.pointerId); TD.pinchD = 0; }); + canvas.addEventListener('contextmenu', (e) => e.preventDefault()); + canvas.addEventListener('wheel', (e) => { + e.preventDefault(); + e.stopPropagation(); + TD.dist = Math.min(34, Math.max(4, TD.dist * (1 + e.deltaY * 0.001))); + }, { passive: false, capture: true }); +} + +// ==================== СУЩНОСТИ ==================== +function TDmobMesh(m) { + let e = TD.mobs.get(m); + if (!e) { + const g = new THREE.Group(); + const body = new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + new THREE.MeshLambertMaterial({ color: TD_MOB_COLORS[m.kind] || 0x2ecc71 }) + ); + body.castShadow = true; + g.add(body); + const eyeM = new THREE.MeshBasicMaterial({ color: 0x111111 }); + const e1 = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.14, 0.05), eyeM); + e1.position.set(-0.16, 0.22, 0.26); + const e2 = e1.clone(); + e2.position.x = 0.16; + g.add(e1, e2); + TD.scene.add(g); + e = { g, body }; + TD.mobs.set(m, e); + } + return e; +} + +function TDplayerEntry(id, p) { + let e = TD.players.get(id); + if (!e) { + const rig = TDbuildHeroRig(); + const label = TDnameSprite(p.name); + label.position.y = 2.2; + rig.group.add(label); + TD.scene.add(rig.group); + e = { rig, label }; + TD.players.set(id, e); + } + return e; +} + +// ==================== КАДР ==================== +function renderFrame3D(now) { + if (!TD) TDinit(); + const p = state.player; + const TILE = state.TILE; + TD.frame++; + const dt = Math.min(0.05, Math.max(0.001, (now - (TD.lastT || now - 16)) / 1000)); + TD.lastT = now; + + const ptx = (p.x + (p.w || 34) / 2) / TILE; + const pgy = (p.y + (p.h || 34) / 2) / TILE; // игровый Y (вниз) + const py3 = -pgy; // three Y (вверх) + const ptz = ((p.z || 0) + (p.d || 34) / 2) / TILE; + + // пересборка мешей: при изменении мира или раз в ~2 сек (для плавной подгрузки) + // пересборка не чаще раза в 700 мс и только при изменении мира + const ver = state.blocksVersion || 0; + if ((ver !== TD.lastVersion && now - TD.meshT > 700) || !TD.blockMeshes.length) { + TD.meshT = now; + TD.lastVersion = ver; + TDrebuildBlocks(ptx, pgy, ptz); + } + + // небо/свет. Первые секунды после старта держим полдень, чтобы не было «рассвета» + const night = isNight(); + const sky = skyColor(night, 'plains', state.weatherType, state.worldTime); + TD.scene.background.set(sky); + TD.scene.fog.color.copy(TD.scene.background); + TD.sun.intensity = night ? 0.14 : 1.5; + TD.hemi.intensity = night ? 0.3 : 0.85; + TD.amb.intensity = night ? 0.34 : 0.25; + if (state.lightningFlash > 0) TD.hemi.intensity += Math.min(2.2, state.lightningFlash * 2.4); + TD.sun.position.set(ptx + 20, py3 + 36, ptz + 20); + TD.sun.target.position.set(ptx, py3, ptz); + TD.sun.target.updateMatrixWorld(); + + // камера-орбита; yaw — тот же угол, что и для WASD + camDir.yaw = TD.yaw; + const cp = Math.cos(TD.pitch), sp2 = Math.sin(TD.pitch); + TD.camera.position.set( + ptx + TD.dist * Math.sin(TD.yaw) * cp, + py3 + TD.dist * sp2, + ptz + TD.dist * Math.cos(TD.yaw) * cp + ); + TD.camera.lookAt(ptx, py3, ptz); + + // герой: позиция в ногах коллайдера, поворот по направлению взгляда + TD.hero.group.position.set(ptx, -p.y / TILE, ptz); + if (p.facing !== undefined) { + const wantYaw = -p.facing; + TD.hero.group.rotation.y += (wantYaw - TD.hero.group.rotation.y) * Math.min(1, 9 * dt); + } + const moving = Math.hypot(p.vx || 0, p.vz || 0) > 20 && p.grounded; + if (moving) TD.heroStep += dt * 11; + TD.hero.P.legs.rotation.z = moving ? Math.sin(TD.heroStep) * 0.34 : 0; + TD.hero.P.legs.rotation.x = moving ? Math.cos(TD.heroStep) * 0.22 : 0; + + // мобы + const allMobs = state.isMultiplayer ? Array.from(state.serverMobs.values()) : state.mobs; + const seen = new Set(); + for (const m of allMobs) { + if (m.dead) continue; + seen.add(m); + const e = TDmobMesh(m); + e.body.scale.set((m.w || 30) / TILE, (m.h || 30) / TILE, (m.d || m.w || 30) / TILE); + e.g.position.set( + (m.x + (m.w || 30) / 2) / TILE, + -(m.y + (m.h || 30) / 2) / TILE, + ((m.z || 0) + (m.d || m.w || 30) / 2) / TILE + ); + } + for (const [m, e] of TD.mobs) { + if (!seen.has(m)) { TD.scene.remove(e.g); TD.mobs.delete(m); } + } + + // игроки сети + const seenP = new Set(); + if (state.otherPlayers) { + for (const [id, pl] of state.otherPlayers) { + seenP.add(id); + const e = TDplayerEntry(id, pl); + e.rig.group.position.set((pl.x + 17) / TILE, -pl.y / TILE, ((pl.z || 0) + 17) / TILE); + } + } + for (const [id, e] of TD.players) { + if (!seenP.has(id)) { TD.scene.remove(e.rig.group); TD.players.delete(id); } + } + + // лодка + if (state.boat && state.boat.active) { + TD.boatMesh.visible = true; + TD.boatMesh.position.set(state.boat.x / TILE, -state.boat.y / TILE, (state.boat.z || 0) / TILE); + } else TD.boatMesh.visible = false; + + // снаряды + const projs = state.projectiles || []; + for (let i = 0; i < TD.projPool.length; i++) { + const mesh = TD.projPool[i]; + const pr = projs[i]; + if (!pr) { mesh.visible = false; continue; } + mesh.visible = true; + mesh.material.color.set(pr.owner === 'mob' ? 0xc0392b : 0xf1c40f); + mesh.position.set(pr.x / TILE, -pr.y / TILE, (pr.z || 0) / TILE); + mesh.rotation.z = -Math.atan2(pr.vy, pr.vx); + } + + // частицы + const parts = state.parts || []; + const n = Math.min(parts.length, TD.PMAX); + for (let i = 0; i < n; i++) { + const pt = parts[i]; + TD.pPos[i * 3] = pt.x / TILE; + TD.pPos[i * 3 + 1] = -pt.y / TILE; + TD.pPos[i * 3 + 2] = (pt.z || 0) / TILE; + let cc = TD.colCache.get(pt.c); + if (!cc) { cc = new THREE.Color(pt.c || '#ffffff'); TD.colCache.set(pt.c, cc); } + TD.pCol[i * 3] = cc.r; TD.pCol[i * 3 + 1] = cc.g; TD.pCol[i * 3 + 2] = cc.b; + } + TD.points.geometry.setDrawRange(0, n); + TD.points.geometry.attributes.position.needsUpdate = true; + TD.points.geometry.attributes.color.needsUpdate = true; + + // облака + const clouds = state.clouds || []; + for (let i = 0; i < TD.cloudMeshes.length; i++) { + const cm = TD.cloudMeshes[i]; + const c = clouds.length ? clouds[i % clouds.length] : null; + if (!c) { cm.visible = false; continue; } + cm.visible = true; + cm.scale.set((c.w || 120) / TILE, 1, 1); + cm.position.set(c.x / TILE, -c.y / TILE, ptz - 12); + } + + // огни + if (TD.frame % 20 === 0 || !TD.fireCache) { + const found = []; + for (const b of state.blocks) { + if (b.dead) continue; + const def = BLOCKS[b.t]; + if (!def) continue; + const lit = def.lightRadius || b.t === 'campfire' || + (b.t === 'furnace' && state.activeFurnaces && state.activeFurnaces.has(b.gx + ',' + b.gy + ',' + (b.gz || 0))); + if (!lit) continue; + const dx = b.gx - ptx, dy = -b.gy + pty, dz = (b.gz || 0) - ptz; + found.push({ t: b.t, gx: b.gx, gy: b.gy, gz: b.gz || 0, d: dx * dx + dy * dy + dz * dz }); + } + found.sort((a, b) => a.d - b.d); + TD.fireCache = found.slice(0, 8); + } + for (const L of TD.lightPool) L.intensity = 0; + for (const f of TD.flamePool) f.visible = false; + let li = 0, fi = 0; + for (const src of (TD.fireCache || [])) { + if (li < TD.lightPool.length) { + const L = TD.lightPool[li++]; + L.position.set(src.gx + 0.5, -src.gy + 0.5, src.gz + 0.5); + L.intensity = 1.6 + Math.sin(now / 130 + src.gx) * 0.35; + } + if ((src.t === 'campfire' || src.t === 'furnace') && fi < TD.flamePool.length) { + const f = TD.flamePool[fi++]; + f.visible = true; + f.position.set(src.gx + 0.5, -src.gy + 0.78 + Math.sin(now / 90 + src.gy) * 0.06, src.gz + 0.5); + const sc2 = 0.9 + Math.sin(now / 70 + src.gx * 2) * 0.18; + f.scale.set(sc2, sc2 * (1 + Math.sin(now / 55) * 0.12), sc2); + } + } + + // погода + if (state.isRaining) { + TD.rain.pts.visible = true; + for (let i = 0; i < TD.rain.n; i++) { + let y = TD.rain.pos[i * 3 + 1] - dt * 26; + let x = TD.rain.pos[i * 3]; + if (y < py3 - 12) { y = py3 + 12; x = ptx + (Math.random() - 0.5) * 40; } + TD.rain.pos[i * 3] = x; + TD.rain.pos[i * 3 + 1] = y; + TD.rain.pos[i * 3 + 2] = ptz + 6; + } + TD.rain.pts.geometry.attributes.position.needsUpdate = true; + } else TD.rain.pts.visible = false; + + if (state.weatherType === 'snow') { + TD.snow.pts.visible = true; + for (let i = 0; i < TD.snow.n; i++) { + let y = TD.snow.pos[i * 3 + 1] - dt * 2; + let x = TD.snow.pos[i * 3] + Math.sin(now / 900 + i) * dt * 1.5; + if (y < py3 - 12) { y = py3 + 12; x = ptx + (Math.random() - 0.5) * 40; } + TD.snow.pos[i * 3] = x; + TD.snow.pos[i * 3 + 1] = y; + TD.snow.pos[i * 3 + 2] = ptz + 6; + } + TD.snow.pts.geometry.attributes.position.needsUpdate = true; + } else TD.snow.pts.visible = false; + + // подсветка блока под курсором / прогресс добычи + const mg = state.mining; + if (mg && mg.gx !== null && mg.hardness > 0) { + TD.crack.visible = true; + TD.crack.position.set(mg.gx + 0.5, -mg.gy - 0.5, (mg.gz || 0) + 0.5); + TD.crack.material.opacity = Math.min(0.55, (mg.progress / mg.hardness) * 0.55); + } else TD.crack.visible = false; + + if (mode() === 'build' && TD.lastTap) { + TD.ghost.visible = true; + TD.ghost.position.set(TD.lastTap.gx + 0.5, -TD.lastTap.gy - 0.5, (TD.lastTap.gz || 0) + 0.5); + } else TD.ghost.visible = false; + + if (state.W !== window.innerWidth || state.H !== window.innerHeight) TDresize(); + TD.camera.aspect = window.innerWidth / window.innerHeight; + TD.camera.updateProjectionMatrix(); + + TD.renderer.render(TD.scene, TD.camera); +} + +// === game/loop3d.js === +// ==================== ЦИКЛ ИГРЫ 3D — GrechkaCraft v3 ==================== +// Всё как в 2D-игре, но движение по трём осям: X (вперёд-назад по миру), +// Y (вверх-вниз), Z (вбок). WASD работают относительно взгляда камеры. + + + + + + + + + + + + + + + + + +let last = performance.now(); +let prevJump = false; + +document.addEventListener('visibilitychange', () => { + if (!document.hidden) last = performance.now(); +}); + +// Направление взгляда камеры (задаётся рендером) — WASD относительно него +const camDir = { yaw: 0, pitch: 0 }; + +function loop(now) { + const rawDt = Math.min(0.05, (now - last) / 1000); + last = now; + const PHYSICS_STEP = 0.016; + const steps = Math.max(1, Math.ceil(rawDt / PHYSICS_STEP)); + const dt = rawDt / steps; + + const player = state.player; + const inp = state.inp; + const mobs = state.mobs; + const projectiles = state.projectiles; + const boat = state.boat; + + const jumpPressed = inp.j && !prevJump; + prevJump = inp.j; + + // ---------- день/ночь ---------- + if (!state.dayNightCycle) { + // время стоит + } else if (player.sleeping && isNight()) { + state.worldTime += dt * 8 / state.DAY_LEN; + player.hp = Math.min(100, player.hp + dt * 20); + if (!isNight()) player.sleeping = false; + } else { + state.worldTime += dt / state.DAY_LEN; + } + if (state.worldTime >= 1) state.worldTime -= 1; + + // ---------- генерация мира ---------- + ensureGenAroundCamera(); + + // ---------- кислород/утопление ---------- + if (player.headInWater) { + player.o2 = Math.max(0, player.o2 - 6 * dt); + if (player.o2 === 0) player.hp -= calculateDamage(4 * dt); + } else { + player.o2 = Math.min(100, player.o2 + 10 * dt); + } + + player.hunger = Math.max(0, player.hunger - dt * 0.2); + + // ---------- температура по биому (теперь биом 2D) ---------- + if (player.temperature === undefined) player.temperature = 15; + const BIOME_TEMP = { snow: -20, plains: 15, swamp: 20, desert: 45, mountains: 5 }; + const COLD_THRESHOLD = -5; + const HEAT_THRESHOLD = 35; + const pGX = Math.floor((player.x + player.w / 2) / state.TILE); + const pGZ = Math.floor((player.z + player.d / 2) / state.TILE); + const pGY = Math.floor((player.y + player.h / 2) / state.TILE); + const biome = getCachedBiome(pGX, pGZ); + const sGY = surfaceGyAt(pGX, pGZ); + const isUnderground = (pGY - sGY) > 2; + let targetTemp = BIOME_TEMP[biome] || 15; + if (isUnderground) targetTemp = 15; + if (isNight() && !isUnderground) targetTemp -= 10; + if ((state.weatherType === 'rain' || state.weatherType === 'storm') && !isUnderground) targetTemp -= 5; + player.temperature += (targetTemp - player.temperature) * dt * 0.3; + + // костры/факелы греют, вода в пустыне холодит — ищем в кубе вокруг + let nearHeat = false, nearCool = false; + for (let dx = -2; dx <= 2 && !nearHeat; dx++) { + for (let dy = -2; dy <= 2 && !nearHeat; dy++) { + for (let dz = -2; dz <= 2 && !nearHeat; dz++) { + const b = getBlock(pGX + dx, pGY + dy, pGZ + dz); + if (b && (b.t === 'campfire' || b.t === 'torch' || b.t === 'furnace')) nearHeat = true; + } + } + } + if (!nearHeat && biome === 'desert') { + for (let dx = -2; dx <= 2 && !nearCool; dx++) { + for (let dz = -2; dz <= 2 && !nearCool; dz++) { + const b = getBlock(pGX + dx, pGY, pGZ + dz); + if (b && b.t === 'water') nearCool = true; + } + } + } + if (nearHeat) { + player.temperature += (30 - player.temperature) * dt * 2; + if (player.hp < 100) player.hp = Math.min(100, player.hp + 8 * dt); + } + if (nearCool) { + player.temperature += (15 - player.temperature) * dt * 1.5; + if (player.hp < 100 && player.temperature > HEAT_THRESHOLD) player.hp = Math.min(100, player.hp + 8 * dt); + } + if (isUnderground && !nearHeat && player.hp < 100 && + player.temperature >= COLD_THRESHOLD && player.temperature <= HEAT_THRESHOLD) { + player.hp = Math.min(100, player.hp + 2 * dt); + } + const aboveBlk = getBlock(pGX, pGY + 1, pGZ); + if (aboveBlk && BLOCKS[aboveBlk.t] && BLOCKS[aboveBlk.t].solid && aboveBlk.t !== 'glass' && !isUnderground && !nearCool) { + player.temperature -= 5 * dt; + } + if (player.temperature < COLD_THRESHOLD) { + player.hp -= 1 * dt; + player.hunger -= 0.5 * dt; + } + if (player.slowTimer > 0) player.slowTimer -= dt; + + // ---------- кактус: урон в кубе ---------- + for (let dx = -1; dx <= 1; dx++) { + for (let dy = -1; dy <= 1; dy++) { + for (let dz = -1; dz <= 1; dz++) { + const cb = getBlock(pGX + dx, pGY + dy, pGZ + dz); + if (cb && BLOCKS[cb.t] && BLOCKS[cb.t].hurt && player.invuln <= 0) { + player.hp -= 2; + player.invuln = 0.5; + } + } + } + } + + // ---------- лёд: скольжение ---------- + const underBlk = getBlock(pGX, pGY - 1, pGZ); + if (underBlk && BLOCKS[underBlk.t] && BLOCKS[underBlk.t].slip && player.grounded) { + player.vx *= 0.98; + player.vz *= 0.98; + } + + // ---------- ДВИЖЕНИЕ: WASD относительно камеры ---------- + if (player.sleeping) { + player.vx = player.vy = player.vz = 0; + } else { + // локальные оси ввода + let fwd = 0, side = 0; + if (inp.up) fwd -= 1; + if (inp.r || inp.right) side += 1; + if (inp.l || inp.left) side -= 1; + if (inp.d || inp.back) fwd += 1; + + // в мировых осях через угол камеры + const cy = Math.cos(camDir.yaw), sy = Math.sin(camDir.yaw); + let dx = side * cy - fwd * sy; + let dz = side * sy + fwd * cy; + const len = Math.hypot(dx, dz); + if (len > 0.0001) { + dx /= len; dz /= len; + player.vx = dx * state.MOVE; + player.vz = dz * state.MOVE; + player.facing = Math.atan2(dz, dx); + } else { + player.vx *= 0.82; + player.vz *= 0.82; + } + } + + // шаги + if (player.grounded && !player.inWater && Math.hypot(player.vx, player.vz) > 50) { + if (now / 1000 - player.lastStepTime > 0.35) { + playSound('step'); + player.lastStepTime = now / 1000; + } + } + + // ---------- прыжок / плавание / лодка ---------- + if (player.inBoat) { + player.vy = 0; + player.x = boat.x + 2; + player.y = boat.y - 4; + player.z = boat.z + 2; + player.grounded = true; + player.inWater = false; + if (jumpPressed) { + state.inv.boat = (state.inv.boat || 0) + 1; + player.inBoat = false; + boat.active = false; + player.y += state.TILE; + player.vy = -state.JUMP * 0.5; + playSound('splash'); + } + } else if (player.inWater) { + player.vx *= 0.90; + player.vz *= 0.90; + player.vy *= 0.92; + if (!jumpPressed && !inp.j) { + player.vy += state.GRAV_WATER * dt; + } else if (jumpPressed) { + player.vy = Math.min(player.vy, -520); + } else if (inp.j) { + player.vy = Math.min(player.vy, -260); + } + } else { + if (jumpPressed && player.grounded && !player.sleeping) { + player.vy = -state.JUMP; + player.grounded = false; + player.fallStartY = player.y; + } + } + + if (!player.inWater && !player.inBoat) { + player.vy += state.GRAV * dt; + } + + // ---------- лодка: плывёт, тонет вне воды ---------- + if (boat.active) { + boat.x += boat.vx * dt; + boat.y += boat.vy * dt; + boat.z += (boat.vz || 0) * dt; + const boatGX = Math.floor(boat.x / state.TILE); + const boatGY = Math.floor(boat.y / state.TILE); + const boatGZ = Math.floor(boat.z / state.TILE); + const below = getBlock(boatGX, boatGY + 1, boatGZ); + if (!below || (below.t !== 'water' && below.t !== 'swamp_water')) { + state.inv.boat = (state.inv.boat || 0) + 1; + player.inBoat = false; + boat.active = false; + player.y += state.TILE; + player.vy = -200; + playSound('splash'); + } + } + + if (player.inBoat && !boat.active) { + state.inv.boat = (state.inv.boat || 0) + 1; + player.inBoat = false; + player.y += state.TILE; + player.vy = -200; + playSound('splash'); + } + + // ---------- границы мира по Z ---------- + const maxZ = WORLD_DEPTH * state.TILE; + if (player.z < -maxZ) { player.z = -maxZ; player.vz = 0; } + if (player.z > maxZ) { player.z = maxZ; player.vz = 0; } + + // ---------- ФИЗИКА: X → Z → Y ---------- + const sub = dt; + for (let step = 0; step < steps; step++) { + player.x += player.vx * sub; + player.z += player.vz * sub; + player.y += player.vy * sub; + resolveX(player, sub); + resolveZ(player, sub); + resolveY(player, sub); + } + + // лестница: вертикальное перемещение + if (onLadder(player)) { + if (inp.j) player.vy = -200; + else if (inp.s || inp.down) player.vy = 100; + else player.vy = 0; + } + + // урон от падения: дистанция от последнего касания земли + if (player.grounded) { + if (player.fallStartY !== undefined) { + const fallDist = player.fallStartY - player.y; + if (fallDist > state.TILE * 6) { + player.hp -= calculateDamage((fallDist / state.TILE - 6) * 2.5); + } + } + player.fallStartY = player.y; + } else if (player.vy > 0 && player.fallStartY === undefined) { + player.fallStartY = player.y; + } + + // вода + updateWaterFlag3(player, dt); + + sendPlayerPosition(); + updateWeather(dt); + updateRain(dt); + player.invuln = Math.max(0, player.invuln - dt); + + // ---------- мобы ---------- + state.spawnT += dt; + if (state.spawnT > 6) { + state.spawnT = 0; + spawnMobs3D(); + } + for (const m of mobs) { + mobAI(m, dt); // физика 3D внутри mob-ai3d + } + // снаряды + for (let i = projectiles.length - 1; i >= 0; i--) { + const pr = projectiles[i]; + pr.x += pr.vx * dt; + pr.y += pr.vy * dt; + pr.life -= dt; + // столкновение с блоком (по Z игрока/снаряда) + const gx = Math.floor(pr.x / state.TILE); + const gy = Math.floor(pr.y / state.TILE); + const gz = Math.floor((pr.z || player.z) / state.TILE); + const b = getBlock(gx, gy, gz); + if (b && BLOCKS[b.t] && BLOCKS[b.t].solid) { + projectiles.splice(i, 1); + continue; + } + if (pr.life <= 0) projectiles.splice(i, 1); + } + + // ---------- TNT ---------- + for (const b of state.blocks) { + if (b.dead || b.t !== 'tnt' || !b.active) continue; + b.fuse -= dt; + if (b.fuse <= 0) { + explodeTNT3D(b, state); + } + } + + // ---------- печь ---------- + tickFurnaces(dt); + if (state.currentFurnaceKey && Math.random() < 0.1) renderFurnaceUI(); + + // ---------- смерть/возрождение ---------- + if (player.hp <= 0 && state.deathEl && state.deathEl.style.display !== 'flex') { + player.hp = 0; + state.deathEl.style.display = 'flex'; + } else if (player.hp > 0 && state.deathEl && state.deathEl.style.display === 'flex') { + state.deathEl.style.display = 'none'; + } + + // ---------- HUD ---------- + if (Math.random() < 0.25) { + state.hpEl.textContent = Math.max(0, Math.ceil(player.hp)); + state.foodEl.textContent = Math.ceil(player.hunger); + const o2El = document.getElementById('o2'); + if (o2El) o2El.textContent = Math.ceil(player.o2); + const tempEl = document.getElementById('temp'); + if (tempEl) { + const t = Math.round(player.temperature); + tempEl.textContent = t + '°C'; + if (player.temperature < COLD_THRESHOLD) tempEl.style.color = '#74b9ff'; + else if (player.temperature > HEAT_THRESHOLD) tempEl.style.color = '#e17055'; + else tempEl.style.color = '#55efc4'; + } + state.sxEl.textContent = pGX; + state.syEl.textContent = pGY; + state.todEl.textContent = isNight() ? 'Ночь' : 'День'; + state.worldIdEl.textContent = state.worldId; + if (state.isMultiplayer) { + const st = document.getElementById('multiplayerStatus'); + if (st) st.style.display = 'flex'; + state.playerCountEl.textContent = state.otherPlayers.size + 1; + } else { + const st = document.getElementById('multiplayerStatus'); + if (st) st.style.display = 'none'; + } + } + + // сон — DOM-оверлей + const sleepOv = document.getElementById('sleepOverlay'); + if (sleepOv) sleepOv.style.display = player.sleeping ? 'flex' : 'none'; + + // ---------- РЕНДЕР ---------- + renderFrame3D(now); + + requestAnimationFrame(loop); +} + +// ---------- спавн мобов в 3D ---------- +function spawnMobs3D() { + const p = state.player; + const pcx = p.x / state.TILE; + const pcy = p.y / state.TILE; + const pcz = (p.z || 0) / state.TILE; + const kinds = [Zombie, Pig, Chicken, Creeper, Skeleton, Scorpion, PolarBear, Slime, Eagle]; + for (let i = 0; i < 2; i++) { + const ang = Math.random() * Math.PI * 2; + const dist = 14 + Math.random() * 12; + const mx = Math.floor(pcx + Math.cos(ang) * dist); + const mz = Math.floor(pcz + Math.sin(ang) * dist); + if (Math.abs(mz) > WORLD_DEPTH) continue; + const sy = surfaceGyAt(mx, mz) - 1; + if (sy < 1) continue; + const K = kinds[Math.floor(Math.random() * kinds.length)]; + const m = new K(mx * state.TILE, sy * state.TILE, mz * state.TILE); + m.z = mz * state.TILE; + m.d = m.d || m.w; + m.vz = 0; + state.mobs.push(m); + } +} + +// === ui/minimap.js === +// ==================== МИНИКАРТА ==================== + + + + + + +const minimapWrap = document.getElementById('minimapWrap'); +const minimapCanvas = document.getElementById('minimap'); +const minimapCtx = minimapCanvas.getContext('2d'); + +// Цвета блоков для миникарты (по 1 пикселю на блок) +const MINIMAP_COLORS = { + grass: '#4a8c2a', dirt: '#6b3410', stone: '#6a6a6a', sand: '#d4b84a', + gravel: '#888', clay: '#5a9ad4', wood: '#a63d00', planks: '#c46a10', + leaves: '#2a8a3a', glass: '#aaddee', water: '#2980b9', coal: '#1a1a2a', + copper_ore: '#a05535', iron_ore: '#b0b0b0', gold_ore: '#d4a017', + diamond_ore: '#0090d0', brick: '#8a2010', tnt: '#c02020', + campfire: '#d08020', torch: '#d0a020', bedrock: '#1a1a1a', + flower: '#d03050', bed: '#c03060', ladder: '#a04000', boat: '#6b3410' +}; + +function initMinimap() { + document.getElementById('mapToggle').onclick = () => { + playSound('click'); + state.minimapOpen = !state.minimapOpen; + minimapWrap.style.display = state.minimapOpen ? 'block' : 'none'; + }; + + const minimapClose = document.getElementById('minimapClose'); + if (minimapClose) { + minimapClose.addEventListener('click', (e) => { + e.stopPropagation(); + state.minimapOpen = false; + minimapWrap.style.display = 'none'; + }); + } +} + +function renderMinimap() { + if (!state.minimapOpen) return; + const mW = minimapCanvas.width; + const mH = minimapCanvas.height; + const scale = 2; // пикселей на блок + const TILE = state.TILE; + const player = state.player; + + // Область карты — центрирована на игроке + const pGX = Math.floor(player.x / TILE); + const pGY = Math.floor(player.y / TILE); + const viewW = Math.floor(mW / scale); + const viewH = Math.floor(mH / scale); + const startGX = pGX - Math.floor(viewW / 2); + const startGY = pGY - Math.floor(viewH / 2); + + // Очищаем + minimapCtx.fillStyle = isNight() ? '#070816' : '#87CEEB'; + minimapCtx.fillRect(0, 0, mW, mH); + + // Рисуем блоки + const imgData = minimapCtx.createImageData(mW, mH); + const data = imgData.data; + + for (let dx = 0; dx < viewW; dx++) { + for (let dy = 0; dy < viewH; dy++) { + const gx = startGX + dx; + const gy = startGY + dy; + const b = getBlock(gx, gy, 0); + if (!b || b.dead || b.t === 'air') continue; + + const color = MINIMAP_COLORS[b.t]; + if (!color) continue; + + // Парсим hex цвет + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const bl = parseInt(color.slice(5, 7), 16); + + // Заполняем scale x scale пикселей + for (let sx = 0; sx < scale; sx++) { + for (let sy = 0; sy < scale; sy++) { + const px = dx * scale + sx; + const py = dy * scale + sy; + if (px >= mW || py >= mH) continue; + const idx = (py * mW + px) * 4; + data[idx] = r; + data[idx + 1] = g; + data[idx + 2] = bl; + data[idx + 3] = 255; + } + } + } + } + + minimapCtx.putImageData(imgData, 0, 0); + + // Игрок — белый пиксель по центру + minimapCtx.fillStyle = '#fff'; + minimapCtx.fillRect(Math.floor(mW / 2) - 2, Math.floor(mH / 2) - 2, 4, 4); + + // Другие игроки — жёлтые точки + for (const [sid, p] of state.otherPlayers) { + const dx = Math.floor(p.x / TILE) - startGX; + const dy = Math.floor(p.y / TILE) - startGY; + if (dx >= 0 && dx < viewW && dy >= 0 && dy < viewH) { + minimapCtx.fillStyle = '#f1c40f'; + minimapCtx.fillRect(dx * scale - 1, dy * scale - 1, 3, 3); + } + } + + // Мобы — красные (враждебные) / зелёные (животные) + const allMobsForMap = state.isMultiplayer ? Array.from(state.serverMobs.values()) : state.mobs; + for (const m of allMobsForMap) { + const dx = Math.floor(m.x / TILE) - startGX; + const dy = Math.floor(m.y / TILE) - startGY; + if (dx >= 0 && dx < viewW && dy >= 0 && dy < viewH) { + const hostile = m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton'; + minimapCtx.fillStyle = hostile ? '#e74c3c' : '#2ecc71'; + minimapCtx.fillRect(dx * scale, dy * scale, 2, 2); + } + } +} + +// === ui/market.js === +// ==================== РЫНОК ПРЕДЛОЖЕНИЙ ==================== + + + +let marketPanel, marketClose, marketContent; + +function initMarket() { + marketPanel = document.getElementById('marketPanel'); + marketClose = document.getElementById('marketClose'); + marketContent = document.getElementById('marketContent'); + + if (!marketPanel) return; + + if (marketClose) marketClose.addEventListener('click', () => closeMarket()); + document.addEventListener('toggleMarket', () => toggleMarket()); +} + +function toggleMarket() { + if (!marketPanel) return; + if (marketPanel.style.display === 'none' || !marketPanel.style.display) { + openMarket(); + } else { + closeMarket(); + } +} + +function closeMarket() { + if (marketPanel) marketPanel.style.display = 'none'; +} + +function openMarket() { + if (marketPanel) { + marketPanel.style.display = 'block'; + renderMarket(); + } +} + +function renderMarket() { + if (!marketContent) return; + if (!state.socket) { + marketContent.innerHTML = '

Подключение к серверу...

'; + return; + } + + state.socket.emit('market_list', {}, (orders) => { + if (!orders || !orders.length) { + marketContent.innerHTML = '

Предложений пока нет

'; + const btn = document.getElementById('createOrderBtn'); + if (btn) btn.addEventListener('click', showCreateOrder); + return; + } + + let html = '
'; + html += orders.map(o => '
' + (o.offer_item||'') + ' x' + (o.offer_qty||1) + ' ⇄ ' + (o.want_item||'') + ' x' + (o.want_qty||1) + '
' + (o.seller || 'Аноним') + '
').join(''); + marketContent.innerHTML = html; + const btn = document.getElementById('createOrderBtn'); + if (btn) btn.addEventListener('click', showCreateOrder); + }); +} + +function showCreateOrder() { + if (!marketContent) return; + const invKeys = Object.keys(state.inv || {}); + const invOpts = invKeys.filter(k => (state.inv[k]||0) > 0).map(k => '').join(''); + + marketContent.innerHTML = '

Выставить предмет

'; + + const cancelBtn = document.getElementById('cancelOrder'); + if (cancelBtn) cancelBtn.addEventListener('click', () => renderMarket()); + const submitBtn = document.getElementById('submitOrder'); + if (submitBtn) submitBtn.addEventListener('click', () => { + const offerItem = document.getElementById('offerItem'); + const wantItem = document.getElementById('wantItem'); + if (!offerItem || !wantItem) return; + const offer = offerItem.value; + const want = wantItem.value.trim(); + if (!offer || !want) { alert('Выберите предмет и укажите что хотите получить'); return; } + if (state.socket) { + state.socket.emit('market_create', { offer_item: offer, offer_qty: 1, want_item: want, want_qty: 1 }); + playSound('click'); + setTimeout(renderMarket, 500); + } + }); +} + +// === ui/settings.js === +// Настройки + + +let settingsPanel = null; +let dayNightToggle = null; + +// Weather button IDs in order +const WEATHER_BTNS = ['weatherClear', 'weatherRain', 'weatherSnow', 'weatherThunder']; +const WEATHER_ACTIVE_BG = '#3498db'; +const WEATHER_INACTIVE_BG = '#555'; + +function setWeatherButtonActive(activeId) { + for (const id of WEATHER_BTNS) { + const btn = document.getElementById(id); + if (btn) { + btn.style.background = (id === activeId) ? WEATHER_ACTIVE_BG : WEATHER_INACTIVE_BG; + } + } +} + +function initSettings() { + settingsPanel = document.getElementById('settingsPanel'); + dayNightToggle = document.getElementById('dayNightToggle'); + + if (!settingsPanel) return; + + const closeBtn = document.getElementById('settingsClose'); + if (closeBtn) { + closeBtn.addEventListener('click', () => { + settingsPanel.style.display = 'none'; + }); + } + + // Day/night cycle toggle + if (dayNightToggle) { + dayNightToggle.checked = state.dayNightCycle !== false; + dayNightToggle.addEventListener('change', () => { + state.dayNightCycle = dayNightToggle.checked; + }); + } + + // Time-of-day slider + const timeSlider = document.getElementById('timeSlider'); + if (timeSlider) { + timeSlider.value = Math.round(state.worldTime * 100); + timeSlider.addEventListener('input', () => { + state.worldTime = parseInt(timeSlider.value) / 100; + if (state.dayNightCycle) { + state.dayNightCycle = false; + if (dayNightToggle) dayNightToggle.checked = false; + } + }); + } + + // ====== Weather controls (Ясно / Дождь / Снег / Гроза) ====== + const weatherIntensitySlider = document.getElementById('weatherIntensity'); + + // Helper: apply a weather type + function applyWeather(type) { + state.weatherType = type; + state.weatherChangeInterval = Infinity; // Lock weather manually + + switch (type) { + case 'clear': + state.isRaining = false; + state.rainIntensity = 0; + if (weatherIntensitySlider) weatherIntensitySlider.value = 0; + break; + case 'rain': + state.isRaining = true; + state.rainIntensity = parseInt(weatherIntensitySlider?.value || 40) / 100; + break; + case 'snow': + state.isRaining = false; // snow uses its own rendering + state.rainIntensity = parseInt(weatherIntensitySlider?.value || 40) / 100; + break; + case 'thunder': + state.isRaining = true; // thunder includes rain + state.rainIntensity = parseInt(weatherIntensitySlider?.value || 60) / 100; + if (weatherIntensitySlider) weatherIntensitySlider.value = Math.round(state.rainIntensity * 100); + break; + } + + setWeatherButtonActive('weather' + type.charAt(0).toUpperCase() + type.slice(1)); + } + + // Button handlers + for (const id of WEATHER_BTNS) { + const btn = document.getElementById(id); + if (btn) { + const weatherType = btn.dataset.weather || id.replace('weather', '').toLowerCase(); + btn.addEventListener('click', () => applyWeather(weatherType)); + } + } + + // Intensity slider + if (weatherIntensitySlider) { + weatherIntensitySlider.addEventListener('input', () => { + const val = parseInt(weatherIntensitySlider.value) / 100; + if (state.weatherType !== 'clear') { + state.rainIntensity = val; + if (state.weatherType === 'thunder' || state.weatherType === 'rain') { + state.isRaining = true; + } + } + }); + } + + // Listen for toggleSettings custom event + document.addEventListener('toggleSettings', () => { + if (settingsPanel) { + // Sync all controls when panel opens + if (timeSlider) timeSlider.value = Math.round(state.worldTime * 100); + if (dayNightToggle) dayNightToggle.checked = state.dayNightCycle !== false; + if (weatherIntensitySlider) weatherIntensitySlider.value = Math.round(state.rainIntensity * 100); + // Highlight active weather button + const wt = state.weatherType || (state.isRaining ? 'rain' : 'clear'); + const activeId = 'weather' + wt.charAt(0).toUpperCase() + wt.slice(1); + setWeatherButtonActive(activeId); + settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none'; + } + }); +} + +function isDayNightEnabled() { + return state.dayNightCycle !== false; +} + +// === ui/respawn.js === +// ==================== RESPAWN ==================== + + + + +function initRespawn() { + document.getElementById('respawnBtn').onclick = async () => { + playSound('click'); // Звук клика по кнопке + + console.log('=== RESPAWN CLICKED ==='); + console.log('isMultiplayer:', state.isMultiplayer); + console.log('otherPlayers.size:', state.otherPlayers.size); + console.log('player.hp before respawn:', state.player.hp); + + const player = state.player; + const deathEl = state.deathEl; + + // В мультиплеере не загружаем сохранение, а возрождаемся в начальной точке + if (state.isMultiplayer && state.otherPlayers.size > 0) { + console.log('Мультиплеер режим - возрождение в начальной точке'); + player.hp = 100; + player.hunger = 100; + player.o2 = 100; + player.vx = player.vy = 0; + player.invuln = 0; + player.x = state.spawnPoint.x; + player.y = state.spawnPoint.y; + player.fallStartY = player.y; + console.log('Возрождение в начальной точке, HP:', player.hp); + } else { + console.log('Одиночный режим - загружаем последнее сохранение'); + // Одиночный режим - загружаем последнее сохранение + const loadedSave = await loadGame(); + if (loadedSave) { + await applySave(loadedSave); + console.log('Загружено последнее сохранение после смерти, final HP:', player.hp); + } else { + // Если сохранения нет, возрождаемся в начальной точке + player.hp = 100; + player.hunger = 100; + player.o2 = 100; + player.vx = player.vy = 0; + player.invuln = 0; + player.x = state.spawnPoint.x; + player.y = state.spawnPoint.y; + player.fallStartY = player.y; + console.log('Возрождение в начальной точке, HP:', player.hp); + } + } + + console.log('player.hp after respawn logic:', player.hp); + console.log('Hiding death screen...'); + deathEl.style.display = 'none'; + console.log('=== RESPAWN END ==='); + }; +} + +// === input/controls.js === +// ==================== УПРАВЛЕНИЕ ==================== + + +const inp = state.inp; + +function bindHold(el, key) { + const down = (e) => { e.preventDefault(); state.inp[key] = true; }; + const up = (e) => { e.preventDefault(); state.inp[key] = false; }; + el.addEventListener('pointerdown', down); + el.addEventListener('pointerup', up); + el.addEventListener('pointerleave', up); +} + +function initControls() { + const leftBtn = document.getElementById('left'); + const rightBtn = document.getElementById('right'); + const jumpBtn = document.getElementById('jump'); + const downBtn = document.getElementById('down'); + + if (leftBtn) bindHold(leftBtn, 'l'); + if (rightBtn) bindHold(rightBtn, 'r'); + if (jumpBtn) bindHold(jumpBtn, 'j'); + if (downBtn) bindHold(downBtn, 's'); + + window.addEventListener('keydown', (e) => { + if (e.code === 'KeyA' || e.code === 'ArrowLeft') state.inp.l = true; + if (e.code === 'KeyD' || e.code === 'ArrowRight') state.inp.r = true; + if (e.code === 'Space' || e.code === 'KeyW' || e.code === 'ArrowUp') state.inp.j = true; + if (e.code === 'KeyS' || e.code === 'ArrowDown') state.inp.s = true; + }); + window.addEventListener('keyup', (e) => { + if (e.code === 'KeyA' || e.code === 'ArrowLeft') state.inp.l = false; + if (e.code === 'KeyD' || e.code === 'ArrowRight') state.inp.r = false; + if (e.code === 'Space' || e.code === 'KeyW' || e.code === 'ArrowUp') state.inp.j = false; + if (e.code === 'KeyS' || e.code === 'ArrowDown') state.inp.s = false; + }); +} + +// === multiplayer/voice-chat.js === +// Голосовой чат + + + + + +let voiceSocket = null; +let voiceStream = null; +let audioCtx = null; +let voiceProcessor = null; +let voiceActive = false; + +// Кнопка микрофона +const voiceBtn = document.createElement('div'); +voiceBtn.innerHTML = '🎤/'; +voiceBtn.title = 'Голосовой чат (выкл)'; +voiceBtn.style.cssText = 'position:absolute;top:10px;right:130px;width:52px;height:52px;border-radius:12px;background:#555;z-index:200;display:flex;align-items:center;justify-content:center;font-size:24px;cursor:pointer;border:2px solid rgba(255,255,255,0.9);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;'; +document.querySelector('.ui').appendChild(voiceBtn); + +// Индикатор говорящего +const speakingIndicator = document.createElement('div'); +speakingIndicator.style.cssText = 'position:absolute;top:130px;right:10px;z-index:200;display:none;background:rgba(0,0,0,0.7);color:#2ecc71;padding:4px 8px;border-radius:6px;font-size:12px;'; +speakingIndicator.textContent = '🔊'; +document.querySelector('.ui').appendChild(speakingIndicator); +let speakingTimeout = null; + +voiceBtn.onclick = async () => { + if (voiceActive) { + // Выключить + voiceActive = false; + voiceBtn.innerHTML = '🎤/'; + voiceBtn.style.background = '#555'; + if (voiceStream) { + voiceStream.getTracks().forEach(t => t.stop()); + voiceStream = null; + } + if (voiceProcessor) { voiceProcessor.disconnect(); voiceProcessor = null; } + if (audioCtx) { audioCtx.close(); audioCtx = null; } + if (voiceSocket) { voiceSocket.disconnect(); voiceSocket = null; } + return; + } + + // Включить + try { + voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 24000 } }); + audioCtx = new AudioContext({ sampleRate: 24000 }); + const source = audioCtx.createMediaStreamSource(voiceStream); + voiceProcessor = audioCtx.createScriptProcessor(4096, 1, 1); + + voiceProcessor.onaudioprocess = (e) => { + if (!voiceActive || !voiceSocket || !voiceSocket.connected) return; + const pcm = e.inputBuffer.getChannelData(0); + // Конвертируем float32 → int16 для экономии трафика + const int16 = new Int16Array(pcm.length); + for (let i = 0; i < pcm.length; i++) { + const s = Math.max(-1, Math.min(1, pcm[i])); + int16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; + } + voiceSocket.emit('voice_data', int16.buffer); + }; + + source.connect(voiceProcessor); + voiceProcessor.connect(audioCtx.createGain()); // не в destination — чтобы не слышать себя + + // Подключаемся к голосовому серверу + voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] }); + voiceSocket.on('connect', () => { + voiceSocket.emit('voice_join', { world_id: state.worldId, x: state.player.x, y: state.player.y, name: state.playerName || 'Игрок' }); + }); + + voiceSocket.on('voice_in', (payload) => { + // Воспроизводим входящий голос + const { data, meta, volume: rawVolume } = payload; + const volume = rawVolume !== undefined ? rawVolume : 1.0; + if (!audioCtx || audioCtx.state === 'closed') return; + + // Int16 → Float32 (volume applied via gain node, not in conversion) + const int16 = new Int16Array(data); + const float32 = new Float32Array(int16.length); + for (let i = 0; i < int16.length; i++) { + float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF); + } + + const buf = audioCtx.createBuffer(1, float32.length, 24000); + buf.getChannelData(0).set(float32); + const src = audioCtx.createBufferSource(); + src.buffer = buf; + + const gain = audioCtx.createGain(); + gain.gain.value = volume; + src.connect(gain).connect(audioCtx.destination); + src.start(); + + // Индикатор + speakingIndicator.style.display = 'block'; + speakingIndicator.textContent = `🔊 ${meta.name}`; + clearTimeout(speakingTimeout); + speakingTimeout = setTimeout(() => { speakingIndicator.style.display = 'none'; }, 500); + }); + + voiceActive = true; + voiceBtn.textContent = '🎤'; + voiceBtn.style.background = '#2ecc71'; + } catch (e) { + console.error('Voice error:', e); + voiceBtn.style.background = '#e74c3c'; + } +}; + +function initVoice() { + // Voice position update hook — should be called from main loop + let voicePosT = 0; + return { + update(dt) { + voicePosT += dt; + if (voicePosT > 0.5 && voiceSocket && voiceSocket.connected) { + voicePosT = 0; + voiceSocket.emit('voice_pos', { x: state.player.x, y: state.player.y }); + } + } + }; +} + +// === ui/craft.js === +// Craft UI + + + + + + + + + + + +function canCraft(r) { + const inv = state.inv; + for (const res in r.cost) { + if ((inv[res] || 0) < r.cost[res]) return false; + } + return true; +} + +function renderCraft() { + const inv = state.inv; + const recipesEl = state.recipesEl; + recipesEl.innerHTML = ''; + for (const r of RECIPES) { + const row = document.createElement('div'); + row.className = 'recipe'; + const icon = document.createElement('div'); + icon.className = 'ricon'; + // Иконка результата — canvas sprite > emoji + if (tex[r.out]) { + icon.style.backgroundImage = `url(${tex[r.out].toDataURL()})`; + } else if (itemTex[r.out]) { + icon.style.backgroundImage = `url(${itemTex[r.out].toDataURL()})`; + } else if (TOOLS[r.out]) { + icon.textContent = TOOLS[r.out].icon || '🔧'; + icon.style.fontSize = '24px'; + icon.style.display = 'flex'; + icon.style.alignItems = 'center'; + icon.style.justifyContent = 'center'; + } else if (ITEMS[r.out]) { + icon.textContent = ITEMS[r.out].icon || '📦'; + icon.style.fontSize = '24px'; + icon.style.display = 'flex'; + icon.style.alignItems = 'center'; + icon.style.justifyContent = 'center'; + } + const info = document.createElement('div'); + info.className = 'rinfo'; + const nm = document.createElement('div'); + nm.className = 'rname'; + const itemName = BLOCKS[r.out]?.n || TOOLS[r.out]?.n || ITEMS[r.out]?.n || r.out; + nm.textContent = `${itemName} x${r.qty}`; + const cs = document.createElement('div'); + cs.className = 'rcost'; + cs.textContent = Object.keys(r.cost).map(x => { + const cn = BLOCKS[x]?.n || TOOLS[x]?.n || ITEMS[x]?.n || x; + return `${cn}: ${(inv[x] || 0)}/${r.cost[x]}`; + }).join(' '); + info.appendChild(nm); info.appendChild(cs); + const btn = document.createElement('button'); + btn.className = 'rcraft'; + btn.textContent = 'Создать'; + btn.disabled = !canCraft(r); + btn.onclick = () => { + if (!canCraft(r)) return; + playSound('click'); + for (const res in r.cost) inv[res] -= r.cost[res]; + inv[r.out] = (inv[r.out] || 0) + r.qty; + if (TOOLS[r.out]) addTool(r.out); + if (!state.recentItems.includes(r.out)) { state.recentItems.unshift(r.out); state.recentItems = state.recentItems.slice(0, 5); } + rebuildHotbar(); + renderCraft(); + renderInventory(); + }; + row.appendChild(icon); row.appendChild(info); row.appendChild(btn); + recipesEl.appendChild(row); + } +} + +let craftOpen = false; +let inventoryOpen = false; + +function toggleCraft() { + playSound('click'); + craftOpen = !craftOpen; + state.craftPanel.style.display = craftOpen ? 'block' : 'none'; + if (craftOpen) { + renderCraft(); + inventoryOpen = false; + state.inventoryPanel.style.display = 'none'; + } +} + +function closeCraft() { + playSound('click'); + craftOpen = false; + state.craftPanel.style.display = 'none'; +} + +function toggleInventory() { + playSound('click'); + inventoryOpen = true; + state.inventoryPanel.style.display = 'block'; + renderInventory(); + craftOpen = false; + state.craftPanel.style.display = 'none'; +} + +function closeInventory() { + playSound('click'); + inventoryOpen = false; + state.inventoryPanel.style.display = 'none'; +} + +// === main3d.js === +// ==================== ENTRY POINT ==================== + + + + + + + + + + + + + + + + + + + + + + + + + +// ==================== 1) CONFIG — parse URL ==================== +// urlParams defined in config.js +state.SERVER_URL = SERVER_URL; +state.TELEGRAM_BOT_USERNAME = 'Grechkacraft_bot'; +state.TELEGRAM_APP_SHORT_NAME = 'minegrechka'; + +// Set TG-related state +if (isTgWebApp) { + state.myTgId = tgUser?.id || null; +} + +state.heroImg = new Image(); state.heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png'; + +// ==================== 2) START MENU ==================== +// Show the start menu before initializing the game. +// The menu returns a Promise that resolves with the worldId. +async function startGame() { + const worldId = await showStartMenu(); + + // Set worldId and playerName + state.worldId = worldId; + if (!state.playerName) { + state.playerName = localStorage.getItem('minegrechka_playerName') || 'Игрок'; + localStorage.setItem('minegrechka_playerName', state.playerName); + } + console.log('[Launch] worldId:', state.worldId, 'player:', state.playerName, 'tg:', isTgWebApp); + + // Validate TG initData if in TG context + if (isTgWebApp && window.Telegram && Telegram.WebApp.initData) { + try { + const resp = await fetch(state.SERVER_URL + '/api/tg/auth', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ initData: Telegram.WebApp.initData }) + }); + const data = await resp.json(); + if (data.ok && data.tg_id) { + console.log('[TG] Authenticated, tg_id:', data.tg_id); + state.myTgId = data.tg_id; + } + } catch (e) { console.warn('[TG] Auth failed:', e); } + } + + // Update URL with world code + try { + const newUrl = new URL(window.location.href); + newUrl.searchParams.set('world', state.worldId); + if (typeof history !== 'undefined' && history.replaceState) { + history.replaceState(null, '', newUrl.toString()); + } + } catch (e) {} + + // ==================== 3) CANVAS — init from canvas module ==================== + initCanvas(); + + // ==================== 4) STATE — set DOM refs ==================== + state.hpEl = document.getElementById('hp'); + state.foodEl = document.getElementById('food'); + state.sxEl = document.getElementById('sx'); + state.syEl = document.getElementById('sy'); + state.todEl = document.getElementById('tod'); + state.worldIdEl = document.getElementById('worldId'); + state.playerCountEl = document.getElementById('playerCount'); + state.deathEl = document.getElementById('death'); + state.hotbarEl = document.getElementById('hotbar'); + state.craftPanel = document.getElementById('craftPanel'); + state.recipesEl = document.getElementById('recipes'); + state.inventoryPanel = document.getElementById('inventoryPanel'); + state.inventoryGrid = document.getElementById('inventoryGrid'); + state.marketPanel = document.getElementById('marketPanel'); + state.marketContent = document.getElementById('marketContent'); + state.tradePanel = document.getElementById('tradePanel'); + state.tradeContent = document.getElementById('tradeContent'); + state.multiplayerStatus = document.getElementById('multiplayerStatus'); + + // ==================== 5) TEXTURES — initTextures() ==================== + initTextures(); + state.tex = tex; + + // ==================== 6) CRAFT/INVENTORY BUTTONS ==================== + const craftBtn = document.getElementById('craftBtn'); + const invToggle = document.getElementById('invToggle'); + const craftClose = document.getElementById('craftClose'); + const inventoryClose = document.getElementById('inventoryClose'); + + if (craftBtn) craftBtn.onclick = () => toggleCraft(); + if (craftClose) craftClose.onclick = () => closeCraft(); + if (invToggle) invToggle.onclick = () => toggleInventory(); + if (inventoryClose) inventoryClose.onclick = () => closeInventory(); + + // ==================== 7) CONTROLS — initControls ==================== + initControls(); + + // ==================== 9) UI MODULES — init all UI ==================== + initChat(); + initFurnace(); + initMinimap(); + initSaveControls(); + initMarket(); + initRespawn(); + initShare(); + initSettings(); + initModes(); + + // ==================== 10) SOCKET — initSocket ==================== + initSocket(); + + // ==================== 11) VOICE — initVoice ==================== + initVoice(); + + // ==================== 12) SAVE — loadGame, applySave ==================== + // Ensure hotbar has items on first load + if (state.recentItems.length === 0) { + state.recentItems = Object.keys(state.inv).filter(k => state.inv[k] > 0).slice(0, 5); + } + // Make sure selected item is valid and in recentItems + if (!state.selected || !state.inv[state.selected] || state.inv[state.selected] <= 0) { + state.selected = state.recentItems[0] || 'dirt'; + } + if (!state.recentItems.includes(state.selected)) { + state.recentItems.unshift(state.selected); + state.recentItems = state.recentItems.slice(0, 5); + } + rebuildHotbar(); + + // Keyboard hotbar switching (1/2/3) + document.addEventListener('keydown', (e) => { + if (state.showFullInventory || state.craftOpen || state.inventoryOpen || state.chatOpen) return; + if (e.key >= '1' && e.key <= '3') { + const items = state.recentItems.filter(id => state.inv[id] > 0).slice(0, 3); + const idx = parseInt(e.key) - 1; + if (items[idx]) { + state.selected = items[idx]; + rebuildHotbar(); + } + } + }); + + // Scroll wheel hotbar cycling + document.addEventListener('wheel', (e) => { + if (state.showFullInventory || state.craftOpen || state.inventoryOpen || state.chatOpen) return; + const items = state.recentItems.filter(id => state.inv[id] > 0).slice(0, 3); + if (items.length === 0) return; + let idx = items.indexOf(state.selected); + if (idx < 0) idx = 0; + if (e.deltaY > 0) idx = (idx + 1) % items.length; + else idx = (idx - 1 + items.length) % items.length; + state.selected = items[idx]; + rebuildHotbar(); + e.preventDefault(); + }, { passive: false }); + + initDB().then(async () => { + const loadedSave = await loadGame(); + if (loadedSave) { + await applySave(loadedSave); + console.log('Загружено сохранение, HP:', state.player.hp); + + if (state.player.hp <= 0) { + console.log('WARNING: HP <= 0 после загрузки, возрождаемся'); + state.player.hp = 100; + state.player.hunger = 100; + state.player.o2 = 100; + state.player.x = state.spawnPoint.x; + state.player.y = state.spawnPoint.y; + state.player.vx = state.player.vy = 0; + state.player.invuln = 0; + state.player.fallStartY = state.player.y; + } + } else { + console.log('Сохранение не найдено, начинаем новую игру'); + // Fill recentItems from inventory + state.recentItems = Object.keys(state.inv).filter(k => state.inv[k] > 0).slice(0, 5); + rebuildHotbar(); + + state.player.hp = 100; + state.player.hunger = 100; + state.player.o2 = 100; + state.player.vx = state.player.vy = 0; + state.player.invuln = 0; + + // старт — на поверхности + const startGX = 6, startGZ = 0; + state.player.x = startGX * state.TILE; + state.player.z = startGZ * state.TILE; + state.player.vx = state.player.vy = state.player.vz = 0; + for (let dx = -4; dx <= 4; dx++) { + for (let dz = -20; dz <= 20; dz++) genColumn(startGX + dx, startGZ + dz); + } + const surfaceY = surfaceGyAt(startGX, startGZ); + let safeGY = surfaceY - 1; + const aboveBlock = getBlock(startGX, surfaceY - 1, startGZ); + if (aboveBlock && (aboveBlock.t === 'water' || aboveBlock.t === 'swamp_water')) { + for (let gy = state.SEA_GY - 1; gy >= 0; gy--) { + const b = getBlock(startGX, gy, startGZ); + if (!b || b.dead || b.t === 'air' || b.t === 'water') continue; + safeGY = gy - 1; + break; + } + } + state.player.y = safeGY * state.TILE; + state.player.fallStartY = state.player.y; + state.spawnPoint.x = state.player.x; + state.spawnPoint.y = state.player.y; + state.spawnPoint.z = state.player.z; + console.log('Новая 3D игра: gx=', startGX, 'gz=', startGZ, 'surfaceY=', surfaceY, 'y=', state.player.y); + } + + // Автосейв при скрытии страницы + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + saveGame(); + } + }); + + // Автосейв перед закрытием страницы + window.addEventListener('beforeunload', () => { + saveGame(); + }); + }).catch(err => { + console.error('Ошибка инициализации:', err); + const startGX = 6, startGZ = 0; + genColumn(startGX, startGZ); + state.player.y = (surfaceGyAt(startGX, startGZ) - 1) * state.TILE; + state.player.fallStartY = state.player.y; + for (let gx = startGX - 4; gx <= startGX + 4; gx++) { + for (let gz = -20; gz <= 20; gz++) genColumn(gx, startGZ + gz); + } + }); + + // ==================== 13) LOOP — startLoop ==================== + requestAnimationFrame(loop); +} + +// Start the game (v3 — 3D) +startGame().catch(err => { + console.error('Fatal error starting game:', err); +}); + +// Debug: give items +window.debugGive = function(id, count) { + state.inv[id] = (state.inv[id] || 0) + (count || 1); + if (!state.recentItems.includes(id)) { state.recentItems.unshift(id); state.recentItems = state.recentItems.slice(0, 5); } + rebuildHotbar(); + // Can't call renderInventory from here, but user can click inv + console.log('Gave', count || 1, id, '=>', state.inv[id]); + console.log('inv keys>0:', Object.keys(state.inv).filter(k => state.inv[k] > 0).join(', ')); +}; +window.debugInv = function() { return Object.keys(state.inv).filter(k => state.inv[k] > 0).map(k => k + ':' + state.inv[k]); }; + +})(); diff --git a/index-v2.html b/index-v2.html new file mode 100644 index 0000000..7a26e96 --- /dev/null +++ b/index-v2.html @@ -0,0 +1,153 @@ + + + + + + + + + + + +GrechkaCraft + + + + + + + + +
+ + +
+
+
❤️ 100   🍗 100
+
🫁 100
+
🌡️ 15°C
+
📍 X:0 Y:0
+
🕒 День
+
🌐 default
+
⭐ Lv.1 | XP: 0/50
+ +
+ +
⛏️
+
🔨
+ +
💬
+
📦
+
🗺️
+ + +
+
+
+ + + + + + + +
+
⬅️
+
⬆️
+
⬇️
+ +
+ + + + + + + + + + + + + + +
+ + + + + diff --git a/index.html b/index.html index a0a81ad..a779040 100644 --- a/index.html +++ b/index.html @@ -5,9 +5,8 @@ GrechkaCraft: Multiplayer - - + @@ -18,7 +17,6 @@
❤️ 100   🍗 100
🫁 100
-
🌡️ 15°C
📍 X:0 Y:0
🕒 День
🌐 default
@@ -27,13 +25,17 @@
⛏️
-
💾
🔨
-
🔄
💬
📦
🗺️
+ +
@@ -93,8 +95,16 @@ + + - + diff --git a/index.html.bak.v30 b/index.html.bak.v30 new file mode 100644 index 0000000..136a890 --- /dev/null +++ b/index.html.bak.v30 @@ -0,0 +1,110 @@ + + + + + +GrechkaCraft: Multiplayer + + + + + + +
+ + +
+
+
❤️ 100   🍗 100
+
🫁 100
+
📍 X:0 Y:0
+
🕒 День
+
🌐 default
+
⭐ Lv.1 | XP: 0/50
+ +
+ +
⛏️
+
🔨
+ +
💬
+
📦
+
🗺️
+ + +
+
+ + + + + + + +
+
⬅️
+
⬆️
+
⬇️
+ +
+ + + + + + + + + + + +
+ + + + diff --git a/nginx.conf b/nginx.conf index 52c51cd..27a9e74 100644 --- a/nginx.conf +++ b/nginx.conf @@ -5,6 +5,15 @@ server { add_header Access-Control-Allow-Origin *; + location = /v2 { + rewrite ^ /v2.html last; + } + + location = /v2.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Access-Control-Allow-Origin *; + } + location ~* \.(js|mjs|css)$ { add_header Cache-Control "no-cache, no-store, must-revalidate"; add_header Pragma "no-cache"; @@ -13,7 +22,15 @@ server { default_type application/javascript; } + location /src/ { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Access-Control-Allow-Origin *; + add_header Content-Type "application/javascript; charset=utf-8"; + expires 0; + } + location / { try_files $uri $uri/ /index.html; } -} \ No newline at end of file +} diff --git a/node_modules/.bin/esbuild b/node_modules/.bin/esbuild new file mode 120000 index 0000000..c83ac07 --- /dev/null +++ b/node_modules/.bin/esbuild @@ -0,0 +1 @@ +../esbuild/bin/esbuild \ No newline at end of file diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json new file mode 100644 index 0000000..54c3384 --- /dev/null +++ b/node_modules/.package-lock.json @@ -0,0 +1,64 @@ +{ + "name": "grechka-game", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + } + } +} diff --git a/node_modules/@esbuild/linux-x64/README.md b/node_modules/@esbuild/linux-x64/README.md new file mode 100644 index 0000000..b2f1930 --- /dev/null +++ b/node_modules/@esbuild/linux-x64/README.md @@ -0,0 +1,3 @@ +# esbuild + +This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details. diff --git a/node_modules/@esbuild/linux-x64/bin/esbuild b/node_modules/@esbuild/linux-x64/bin/esbuild new file mode 100755 index 0000000..1d0369b Binary files /dev/null and b/node_modules/@esbuild/linux-x64/bin/esbuild differ diff --git a/node_modules/@esbuild/linux-x64/package.json b/node_modules/@esbuild/linux-x64/package.json new file mode 100644 index 0000000..76f25d1 --- /dev/null +++ b/node_modules/@esbuild/linux-x64/package.json @@ -0,0 +1,20 @@ +{ + "name": "@esbuild/linux-x64", + "version": "0.28.0", + "description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.", + "repository": { + "type": "git", + "url": "git+https://github.com/evanw/esbuild.git" + }, + "license": "MIT", + "preferUnplugged": true, + "engines": { + "node": ">=18" + }, + "os": [ + "linux" + ], + "cpu": [ + "x64" + ] +} diff --git a/node_modules/esbuild/LICENSE.md b/node_modules/esbuild/LICENSE.md new file mode 100644 index 0000000..2027e8d --- /dev/null +++ b/node_modules/esbuild/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Evan Wallace + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/esbuild/README.md b/node_modules/esbuild/README.md new file mode 100644 index 0000000..93863d1 --- /dev/null +++ b/node_modules/esbuild/README.md @@ -0,0 +1,3 @@ +# esbuild + +This is a JavaScript bundler and minifier. See https://github.com/evanw/esbuild and the [JavaScript API documentation](https://esbuild.github.io/api/) for details. diff --git a/node_modules/esbuild/bin/esbuild b/node_modules/esbuild/bin/esbuild new file mode 100755 index 0000000..1d0369b Binary files /dev/null and b/node_modules/esbuild/bin/esbuild differ diff --git a/node_modules/esbuild/install.js b/node_modules/esbuild/install.js new file mode 100644 index 0000000..bf26e25 --- /dev/null +++ b/node_modules/esbuild/install.js @@ -0,0 +1,300 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + +// lib/npm/node-platform.ts +var fs = require("fs"); +var os = require("os"); +var path = require("path"); +var ESBUILD_BINARY_PATH = process.env.ESBUILD_BINARY_PATH || ESBUILD_BINARY_PATH; +var isValidBinaryPath = (x) => !!x && x !== "/usr/bin/esbuild"; +var knownWindowsPackages = { + "win32 arm64 LE": "@esbuild/win32-arm64", + "win32 ia32 LE": "@esbuild/win32-ia32", + "win32 x64 LE": "@esbuild/win32-x64" +}; +var knownUnixlikePackages = { + "aix ppc64 BE": "@esbuild/aix-ppc64", + "android arm64 LE": "@esbuild/android-arm64", + "darwin arm64 LE": "@esbuild/darwin-arm64", + "darwin x64 LE": "@esbuild/darwin-x64", + "freebsd arm64 LE": "@esbuild/freebsd-arm64", + "freebsd x64 LE": "@esbuild/freebsd-x64", + "linux arm LE": "@esbuild/linux-arm", + "linux arm64 LE": "@esbuild/linux-arm64", + "linux ia32 LE": "@esbuild/linux-ia32", + "linux mips64el LE": "@esbuild/linux-mips64el", + "linux ppc64 LE": "@esbuild/linux-ppc64", + "linux riscv64 LE": "@esbuild/linux-riscv64", + "linux s390x BE": "@esbuild/linux-s390x", + "linux x64 LE": "@esbuild/linux-x64", + "linux loong64 LE": "@esbuild/linux-loong64", + "netbsd arm64 LE": "@esbuild/netbsd-arm64", + "netbsd x64 LE": "@esbuild/netbsd-x64", + "openbsd arm64 LE": "@esbuild/openbsd-arm64", + "openbsd x64 LE": "@esbuild/openbsd-x64", + "sunos x64 LE": "@esbuild/sunos-x64" +}; +var knownWebAssemblyFallbackPackages = { + "android arm LE": "@esbuild/android-arm", + "android x64 LE": "@esbuild/android-x64", + "openharmony arm64 LE": "@esbuild/openharmony-arm64" +}; +function pkgAndSubpathForCurrentPlatform() { + let pkg; + let subpath; + let isWASM = false; + let platformKey = `${process.platform} ${os.arch()} ${os.endianness()}`; + if (platformKey in knownWindowsPackages) { + pkg = knownWindowsPackages[platformKey]; + subpath = "esbuild.exe"; + } else if (platformKey in knownUnixlikePackages) { + pkg = knownUnixlikePackages[platformKey]; + subpath = "bin/esbuild"; + } else if (platformKey in knownWebAssemblyFallbackPackages) { + pkg = knownWebAssemblyFallbackPackages[platformKey]; + subpath = "bin/esbuild"; + isWASM = true; + } else { + throw new Error(`Unsupported platform: ${platformKey}`); + } + return { pkg, subpath, isWASM }; +} +function downloadedBinPath(pkg, subpath) { + const esbuildLibDir = path.dirname(require.resolve("esbuild")); + return path.join(esbuildLibDir, `downloaded-${pkg.replace("/", "-")}-${path.basename(subpath)}`); +} + +// lib/npm/node-install.ts +var fs2 = require("fs"); +var os2 = require("os"); +var path2 = require("path"); +var zlib = require("zlib"); +var https = require("https"); +var crypto = require("crypto"); +var child_process = require("child_process"); +var packageJSON = require(path2.join(__dirname, "package.json")); +var toPath = path2.join(__dirname, "bin", "esbuild"); +var isToPathJS = true; +function validateBinaryVersion(...command) { + command.push("--version"); + let stdout; + try { + stdout = child_process.execFileSync(command.shift(), command, { + // Without this, this install script strangely crashes with the error + // "EACCES: permission denied, write" but only on Ubuntu Linux when node is + // installed from the Snap Store. This is not a problem when you download + // the official version of node. The problem appears to be that stderr + // (i.e. file descriptor 2) isn't writable? + // + // More info: + // - https://snapcraft.io/ (what the Snap Store is) + // - https://nodejs.org/dist/ (download the official version of node) + // - https://github.com/evanw/esbuild/issues/1711#issuecomment-1027554035 + // + stdio: "pipe" + }).toString().trim(); + } catch (err) { + if (os2.platform() === "darwin" && /_SecTrustEvaluateWithError/.test(err + "")) { + let os3 = "this version of macOS"; + try { + os3 = "macOS " + child_process.execFileSync("sw_vers", ["-productVersion"]).toString().trim(); + } catch { + } + throw new Error(`The "esbuild" package cannot be installed because ${os3} is too outdated. + +The Go compiler (which esbuild relies on) no longer supports ${os3}, +which means the "esbuild" binary executable can't be run. You can either: + + * Update your version of macOS to one that the Go compiler supports + * Use the "esbuild-wasm" package instead of the "esbuild" package + * Build esbuild yourself using an older version of the Go compiler +`); + } + throw err; + } + if (stdout !== packageJSON.version) { + throw new Error(`Expected ${JSON.stringify(packageJSON.version)} but got ${JSON.stringify(stdout)}`); + } +} +function isYarn() { + const { npm_config_user_agent } = process.env; + if (npm_config_user_agent) { + return /\byarn\//.test(npm_config_user_agent); + } + return false; +} +function fetch(url) { + return new Promise((resolve, reject) => { + https.get(url, (res) => { + if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) + return fetch(res.headers.location).then(resolve, reject); + if (res.statusCode !== 200) + return reject(new Error(`Server responded with ${res.statusCode}`)); + let chunks = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => resolve(Buffer.concat(chunks))); + }).on("error", reject); + }); +} +function extractFileFromTarGzip(buffer, subpath) { + try { + buffer = zlib.unzipSync(buffer); + } catch (err) { + throw new Error(`Invalid gzip data in archive: ${err && err.message || err}`); + } + let str = (i, n) => String.fromCharCode(...buffer.subarray(i, i + n)).replace(/\0.*$/, ""); + let offset = 0; + subpath = `package/${subpath}`; + while (offset < buffer.length) { + let name = str(offset, 100); + let size = parseInt(str(offset + 124, 12), 8); + offset += 512; + if (!isNaN(size)) { + if (name === subpath) return buffer.subarray(offset, offset + size); + offset += size + 511 & ~511; + } + } + throw new Error(`Could not find ${JSON.stringify(subpath)} in archive`); +} +function installUsingNPM(pkg, subpath, binPath) { + const env = { ...process.env, npm_config_global: void 0 }; + const esbuildLibDir = path2.dirname(require.resolve("esbuild")); + const installDir = path2.join(esbuildLibDir, "npm-install"); + fs2.mkdirSync(installDir); + try { + fs2.writeFileSync(path2.join(installDir, "package.json"), "{}"); + child_process.execSync( + `npm install --loglevel=error --prefer-offline --no-audit --progress=false ${pkg}@${packageJSON.version}`, + { cwd: installDir, stdio: "pipe", env } + ); + const installedBinPath = path2.join(installDir, "node_modules", pkg, subpath); + binaryIntegrityCheck(pkg, subpath, fs2.readFileSync(installedBinPath)); + fs2.renameSync(installedBinPath, binPath); + } finally { + try { + removeRecursive(installDir); + } catch { + } + } +} +function removeRecursive(dir) { + for (const entry of fs2.readdirSync(dir)) { + const entryPath = path2.join(dir, entry); + let stats; + try { + stats = fs2.lstatSync(entryPath); + } catch { + continue; + } + if (stats.isDirectory()) removeRecursive(entryPath); + else fs2.unlinkSync(entryPath); + } + fs2.rmdirSync(dir); +} +function applyManualBinaryPathOverride(overridePath) { + const pathString = JSON.stringify(overridePath); + fs2.writeFileSync(toPath, `#!/usr/bin/env node +require('child_process').execFileSync(${pathString}, process.argv.slice(2), { stdio: 'inherit' }); +`); + const libMain = path2.join(__dirname, "lib", "main.js"); + const code = fs2.readFileSync(libMain, "utf8"); + fs2.writeFileSync(libMain, `var ESBUILD_BINARY_PATH = ${pathString}; +${code}`); +} +function maybeOptimizePackage(binPath, isWASM) { + if (os2.platform() !== "win32" && !isYarn() && !isWASM) { + const tempPath = path2.join(__dirname, "bin-esbuild"); + try { + fs2.linkSync(binPath, tempPath); + fs2.renameSync(tempPath, toPath); + isToPathJS = false; + fs2.unlinkSync(tempPath); + } catch { + } + } +} +function binaryIntegrityCheck(pkg, subpath, bytes) { + const hash = crypto.createHash("sha256").update(bytes).digest("hex"); + const key = `${pkg}/${subpath}`; + const expected = packageJSON["esbuild.binaryHashes"][key]; + if (!expected) throw new Error(`Missing hash for "${key}"`); + if (hash !== expected) throw new Error(`"${hash.slice(0, 8)}..." doesn't match "${expected.slice(0, 8)}..."`); +} +async function downloadDirectlyFromNPM(pkg, subpath, binPath) { + const url = `https://registry.npmjs.org/${pkg}/-/${pkg.replace("@esbuild/", "")}-${packageJSON.version}.tgz`; + console.error(`[esbuild] Trying to download ${JSON.stringify(url)}`); + try { + const bytes = extractFileFromTarGzip(await fetch(url), subpath); + binaryIntegrityCheck(pkg, subpath, bytes); + fs2.writeFileSync(binPath, bytes); + fs2.chmodSync(binPath, 493); + } catch (e) { + console.error(`[esbuild] Failed to download ${JSON.stringify(url)}: ${e && e.message || e}`); + throw e; + } +} +async function checkAndPreparePackage() { + if (isValidBinaryPath(ESBUILD_BINARY_PATH)) { + if (!fs2.existsSync(ESBUILD_BINARY_PATH)) { + console.warn(`[esbuild] Ignoring bad configuration: ESBUILD_BINARY_PATH=${ESBUILD_BINARY_PATH}`); + } else { + applyManualBinaryPathOverride(ESBUILD_BINARY_PATH); + return; + } + } + const { pkg, subpath, isWASM } = pkgAndSubpathForCurrentPlatform(); + let binPath; + try { + binPath = require.resolve(`${pkg}/${subpath}`); + } catch (e) { + console.error(`[esbuild] Failed to find package "${pkg}" on the file system + +This can happen if you use the "--no-optional" flag. The "optionalDependencies" +package.json feature is used by esbuild to install the correct binary executable +for your current platform. This install script will now attempt to work around +this. If that fails, you need to remove the "--no-optional" flag to use esbuild. +`); + if (isWASM) throw new Error(`Failed to install package "${pkg}"`); + binPath = downloadedBinPath(pkg, subpath); + try { + console.error(`[esbuild] Trying to install package "${pkg}" using npm`); + installUsingNPM(pkg, subpath, binPath); + } catch (e2) { + console.error(`[esbuild] Failed to install package "${pkg}" using npm: ${e2 && e2.message || e2}`); + try { + await downloadDirectlyFromNPM(pkg, subpath, binPath); + } catch (e3) { + throw new Error(`Failed to install package "${pkg}"`); + } + } + } + maybeOptimizePackage(binPath, isWASM); +} +checkAndPreparePackage().then(() => { + if (isToPathJS) { + validateBinaryVersion(process.execPath, toPath); + } else { + validateBinaryVersion(toPath); + } +}); diff --git a/node_modules/esbuild/lib/main.d.ts b/node_modules/esbuild/lib/main.d.ts new file mode 100644 index 0000000..9e69c39 --- /dev/null +++ b/node_modules/esbuild/lib/main.d.ts @@ -0,0 +1,716 @@ +export type Platform = 'browser' | 'node' | 'neutral' +export type Format = 'iife' | 'cjs' | 'esm' +export type Loader = 'base64' | 'binary' | 'copy' | 'css' | 'dataurl' | 'default' | 'empty' | 'file' | 'js' | 'json' | 'jsx' | 'local-css' | 'text' | 'ts' | 'tsx' +export type LogLevel = 'verbose' | 'debug' | 'info' | 'warning' | 'error' | 'silent' +export type Charset = 'ascii' | 'utf8' +export type Drop = 'console' | 'debugger' +export type AbsPaths = 'code' | 'log' | 'metafile' + +interface CommonOptions { + /** Documentation: https://esbuild.github.io/api/#sourcemap */ + sourcemap?: boolean | 'linked' | 'inline' | 'external' | 'both' + /** Documentation: https://esbuild.github.io/api/#legal-comments */ + legalComments?: 'none' | 'inline' | 'eof' | 'linked' | 'external' + /** Documentation: https://esbuild.github.io/api/#source-root */ + sourceRoot?: string + /** Documentation: https://esbuild.github.io/api/#sources-content */ + sourcesContent?: boolean + + /** Documentation: https://esbuild.github.io/api/#format */ + format?: Format + /** Documentation: https://esbuild.github.io/api/#global-name */ + globalName?: string + /** Documentation: https://esbuild.github.io/api/#target */ + target?: string | string[] + /** Documentation: https://esbuild.github.io/api/#supported */ + supported?: Record + /** Documentation: https://esbuild.github.io/api/#platform */ + platform?: Platform + + /** Documentation: https://esbuild.github.io/api/#mangle-props */ + mangleProps?: RegExp + /** Documentation: https://esbuild.github.io/api/#mangle-props */ + reserveProps?: RegExp + /** Documentation: https://esbuild.github.io/api/#mangle-props */ + mangleQuoted?: boolean + /** Documentation: https://esbuild.github.io/api/#mangle-props */ + mangleCache?: Record + /** Documentation: https://esbuild.github.io/api/#drop */ + drop?: Drop[] + /** Documentation: https://esbuild.github.io/api/#drop-labels */ + dropLabels?: string[] + /** Documentation: https://esbuild.github.io/api/#minify */ + minify?: boolean + /** Documentation: https://esbuild.github.io/api/#minify */ + minifyWhitespace?: boolean + /** Documentation: https://esbuild.github.io/api/#minify */ + minifyIdentifiers?: boolean + /** Documentation: https://esbuild.github.io/api/#minify */ + minifySyntax?: boolean + /** Documentation: https://esbuild.github.io/api/#line-limit */ + lineLimit?: number + /** Documentation: https://esbuild.github.io/api/#charset */ + charset?: Charset + /** Documentation: https://esbuild.github.io/api/#tree-shaking */ + treeShaking?: boolean + /** Documentation: https://esbuild.github.io/api/#ignore-annotations */ + ignoreAnnotations?: boolean + + /** Documentation: https://esbuild.github.io/api/#jsx */ + jsx?: 'transform' | 'preserve' | 'automatic' + /** Documentation: https://esbuild.github.io/api/#jsx-factory */ + jsxFactory?: string + /** Documentation: https://esbuild.github.io/api/#jsx-fragment */ + jsxFragment?: string + /** Documentation: https://esbuild.github.io/api/#jsx-import-source */ + jsxImportSource?: string + /** Documentation: https://esbuild.github.io/api/#jsx-development */ + jsxDev?: boolean + /** Documentation: https://esbuild.github.io/api/#jsx-side-effects */ + jsxSideEffects?: boolean + + /** Documentation: https://esbuild.github.io/api/#define */ + define?: { [key: string]: string } + /** Documentation: https://esbuild.github.io/api/#pure */ + pure?: string[] + /** Documentation: https://esbuild.github.io/api/#keep-names */ + keepNames?: boolean + + /** Documentation: https://esbuild.github.io/api/#abs-paths */ + absPaths?: AbsPaths[] + /** Documentation: https://esbuild.github.io/api/#color */ + color?: boolean + /** Documentation: https://esbuild.github.io/api/#log-level */ + logLevel?: LogLevel + /** Documentation: https://esbuild.github.io/api/#log-limit */ + logLimit?: number + /** Documentation: https://esbuild.github.io/api/#log-override */ + logOverride?: Record + + /** Documentation: https://esbuild.github.io/api/#tsconfig-raw */ + tsconfigRaw?: string | TsconfigRaw +} + +export interface TsconfigRaw { + compilerOptions?: { + alwaysStrict?: boolean + baseUrl?: string + experimentalDecorators?: boolean + importsNotUsedAsValues?: 'remove' | 'preserve' | 'error' + jsx?: 'preserve' | 'react-native' | 'react' | 'react-jsx' | 'react-jsxdev' + jsxFactory?: string + jsxFragmentFactory?: string + jsxImportSource?: string + paths?: Record + preserveValueImports?: boolean + strict?: boolean + target?: string + useDefineForClassFields?: boolean + verbatimModuleSyntax?: boolean + } +} + +export interface BuildOptions extends CommonOptions { + /** Documentation: https://esbuild.github.io/api/#bundle */ + bundle?: boolean + /** Documentation: https://esbuild.github.io/api/#splitting */ + splitting?: boolean + /** Documentation: https://esbuild.github.io/api/#preserve-symlinks */ + preserveSymlinks?: boolean + /** Documentation: https://esbuild.github.io/api/#outfile */ + outfile?: string + /** Documentation: https://esbuild.github.io/api/#metafile */ + metafile?: boolean + /** Documentation: https://esbuild.github.io/api/#outdir */ + outdir?: string + /** Documentation: https://esbuild.github.io/api/#outbase */ + outbase?: string + /** Documentation: https://esbuild.github.io/api/#external */ + external?: string[] + /** Documentation: https://esbuild.github.io/api/#packages */ + packages?: 'bundle' | 'external' + /** Documentation: https://esbuild.github.io/api/#alias */ + alias?: Record + /** Documentation: https://esbuild.github.io/api/#loader */ + loader?: { [ext: string]: Loader } + /** Documentation: https://esbuild.github.io/api/#resolve-extensions */ + resolveExtensions?: string[] + /** Documentation: https://esbuild.github.io/api/#main-fields */ + mainFields?: string[] + /** Documentation: https://esbuild.github.io/api/#conditions */ + conditions?: string[] + /** Documentation: https://esbuild.github.io/api/#write */ + write?: boolean + /** Documentation: https://esbuild.github.io/api/#allow-overwrite */ + allowOverwrite?: boolean + /** Documentation: https://esbuild.github.io/api/#tsconfig */ + tsconfig?: string + /** Documentation: https://esbuild.github.io/api/#out-extension */ + outExtension?: { [ext: string]: string } + /** Documentation: https://esbuild.github.io/api/#public-path */ + publicPath?: string + /** Documentation: https://esbuild.github.io/api/#entry-names */ + entryNames?: string + /** Documentation: https://esbuild.github.io/api/#chunk-names */ + chunkNames?: string + /** Documentation: https://esbuild.github.io/api/#asset-names */ + assetNames?: string + /** Documentation: https://esbuild.github.io/api/#inject */ + inject?: string[] + /** Documentation: https://esbuild.github.io/api/#banner */ + banner?: { [type: string]: string } + /** Documentation: https://esbuild.github.io/api/#footer */ + footer?: { [type: string]: string } + /** Documentation: https://esbuild.github.io/api/#entry-points */ + entryPoints?: (string | { in: string, out: string })[] | Record + /** Documentation: https://esbuild.github.io/api/#stdin */ + stdin?: StdinOptions + /** Documentation: https://esbuild.github.io/plugins/ */ + plugins?: Plugin[] + /** Documentation: https://esbuild.github.io/api/#working-directory */ + absWorkingDir?: string + /** Documentation: https://esbuild.github.io/api/#node-paths */ + nodePaths?: string[]; // The "NODE_PATH" variable from Node.js +} + +export interface StdinOptions { + contents: string | Uint8Array + resolveDir?: string + sourcefile?: string + loader?: Loader +} + +export interface Message { + id: string + pluginName: string + text: string + location: Location | null + notes: Note[] + + /** + * Optional user-specified data that is passed through unmodified. You can + * use this to stash the original error, for example. + */ + detail: any +} + +export interface Note { + text: string + location: Location | null +} + +export interface Location { + file: string + namespace: string + /** 1-based */ + line: number + /** 0-based, in bytes */ + column: number + /** in bytes */ + length: number + lineText: string + suggestion: string +} + +export interface OutputFile { + path: string + contents: Uint8Array + hash: string + /** "contents" as text (changes automatically with "contents") */ + readonly text: string +} + +export interface BuildResult { + errors: Message[] + warnings: Message[] + /** Only when "write: false" */ + outputFiles: OutputFile[] | (ProvidedOptions['write'] extends false ? never : undefined) + /** Only when "metafile: true" */ + metafile: Metafile | (ProvidedOptions['metafile'] extends true ? never : undefined) + /** Only when "mangleCache" is present */ + mangleCache: Record | (ProvidedOptions['mangleCache'] extends Object ? never : undefined) +} + +export interface BuildFailure extends Error { + errors: Message[] + warnings: Message[] +} + +/** Documentation: https://esbuild.github.io/api/#serve-arguments */ +export interface ServeOptions { + port?: number + host?: string + servedir?: string + keyfile?: string + certfile?: string + fallback?: string + cors?: CORSOptions + onRequest?: (args: ServeOnRequestArgs) => void +} + +/** Documentation: https://esbuild.github.io/api/#cors */ +export interface CORSOptions { + origin?: string | string[] +} + +export interface ServeOnRequestArgs { + remoteAddress: string + method: string + path: string + status: number + /** The time to generate the response, not to send it */ + timeInMS: number +} + +/** Documentation: https://esbuild.github.io/api/#serve-return-values */ +export interface ServeResult { + port: number + hosts: string[] +} + +export interface TransformOptions extends CommonOptions { + /** Documentation: https://esbuild.github.io/api/#sourcefile */ + sourcefile?: string + /** Documentation: https://esbuild.github.io/api/#loader */ + loader?: Loader + /** Documentation: https://esbuild.github.io/api/#banner */ + banner?: string + /** Documentation: https://esbuild.github.io/api/#footer */ + footer?: string +} + +export interface TransformResult { + code: string + map: string + warnings: Message[] + /** Only when "mangleCache" is present */ + mangleCache: Record | (ProvidedOptions['mangleCache'] extends Object ? never : undefined) + /** Only when "legalComments" is "external" */ + legalComments: string | (ProvidedOptions['legalComments'] extends 'external' ? never : undefined) +} + +export interface TransformFailure extends Error { + errors: Message[] + warnings: Message[] +} + +export interface Plugin { + name: string + setup: (build: PluginBuild) => (void | Promise) +} + +export interface PluginBuild { + /** Documentation: https://esbuild.github.io/plugins/#build-options */ + initialOptions: BuildOptions + + /** Documentation: https://esbuild.github.io/plugins/#resolve */ + resolve(path: string, options?: ResolveOptions): Promise + + /** Documentation: https://esbuild.github.io/plugins/#on-start */ + onStart(callback: () => + (OnStartResult | null | void | Promise)): void + + /** Documentation: https://esbuild.github.io/plugins/#on-end */ + onEnd(callback: (result: BuildResult) => + (OnEndResult | null | void | Promise)): void + + /** Documentation: https://esbuild.github.io/plugins/#on-resolve */ + onResolve(options: OnResolveOptions, callback: (args: OnResolveArgs) => + (OnResolveResult | null | undefined | Promise)): void + + /** Documentation: https://esbuild.github.io/plugins/#on-load */ + onLoad(options: OnLoadOptions, callback: (args: OnLoadArgs) => + (OnLoadResult | null | undefined | Promise)): void + + /** Documentation: https://esbuild.github.io/plugins/#on-dispose */ + onDispose(callback: () => void): void + + // This is a full copy of the esbuild library in case you need it + esbuild: { + context: typeof context, + build: typeof build, + buildSync: typeof buildSync, + transform: typeof transform, + transformSync: typeof transformSync, + formatMessages: typeof formatMessages, + formatMessagesSync: typeof formatMessagesSync, + analyzeMetafile: typeof analyzeMetafile, + analyzeMetafileSync: typeof analyzeMetafileSync, + initialize: typeof initialize, + version: typeof version, + } +} + +/** Documentation: https://esbuild.github.io/plugins/#resolve-options */ +export interface ResolveOptions { + pluginName?: string + importer?: string + namespace?: string + resolveDir?: string + kind?: ImportKind + pluginData?: any + with?: Record +} + +/** Documentation: https://esbuild.github.io/plugins/#resolve-results */ +export interface ResolveResult { + errors: Message[] + warnings: Message[] + + path: string + external: boolean + sideEffects: boolean + namespace: string + suffix: string + pluginData: any +} + +export interface OnStartResult { + errors?: PartialMessage[] + warnings?: PartialMessage[] +} + +export interface OnEndResult { + errors?: PartialMessage[] + warnings?: PartialMessage[] +} + +/** Documentation: https://esbuild.github.io/plugins/#on-resolve-options */ +export interface OnResolveOptions { + filter: RegExp + namespace?: string +} + +/** Documentation: https://esbuild.github.io/plugins/#on-resolve-arguments */ +export interface OnResolveArgs { + path: string + importer: string + namespace: string + resolveDir: string + kind: ImportKind + pluginData: any + with: Record +} + +export type ImportKind = + | 'entry-point' + + // JS + | 'import-statement' + | 'require-call' + | 'dynamic-import' + | 'require-resolve' + + // CSS + | 'import-rule' + | 'composes-from' + | 'url-token' + +/** Documentation: https://esbuild.github.io/plugins/#on-resolve-results */ +export interface OnResolveResult { + pluginName?: string + + errors?: PartialMessage[] + warnings?: PartialMessage[] + + path?: string + external?: boolean + sideEffects?: boolean + namespace?: string + suffix?: string + pluginData?: any + + watchFiles?: string[] + watchDirs?: string[] +} + +/** Documentation: https://esbuild.github.io/plugins/#on-load-options */ +export interface OnLoadOptions { + filter: RegExp + namespace?: string +} + +/** Documentation: https://esbuild.github.io/plugins/#on-load-arguments */ +export interface OnLoadArgs { + path: string + namespace: string + suffix: string + pluginData: any + with: Record +} + +/** Documentation: https://esbuild.github.io/plugins/#on-load-results */ +export interface OnLoadResult { + pluginName?: string + + errors?: PartialMessage[] + warnings?: PartialMessage[] + + contents?: string | Uint8Array + resolveDir?: string + loader?: Loader + pluginData?: any + + watchFiles?: string[] + watchDirs?: string[] +} + +export interface PartialMessage { + id?: string + pluginName?: string + text?: string + location?: Partial | null + notes?: PartialNote[] + detail?: any +} + +export interface PartialNote { + text?: string + location?: Partial | null +} + +/** Documentation: https://esbuild.github.io/api/#metafile */ +export interface Metafile { + inputs: { + [path: string]: { + bytes: number + imports: { + path: string + kind: ImportKind + external?: boolean + original?: string + with?: Record + }[] + format?: 'cjs' | 'esm' + with?: Record + } + } + outputs: { + [path: string]: { + bytes: number + inputs: { + [path: string]: { + bytesInOutput: number + } + } + imports: { + path: string + kind: ImportKind | 'file-loader' + external?: boolean + }[] + exports: string[] + entryPoint?: string + cssBundle?: string + } + } +} + +export interface FormatMessagesOptions { + kind: 'error' | 'warning' + color?: boolean + terminalWidth?: number +} + +export interface AnalyzeMetafileOptions { + color?: boolean + verbose?: boolean +} + +/** Documentation: https://esbuild.github.io/api/#watch-arguments */ +export interface WatchOptions { + delay?: number // In milliseconds +} + +export interface BuildContext { + /** Documentation: https://esbuild.github.io/api/#rebuild */ + rebuild(): Promise> + + /** Documentation: https://esbuild.github.io/api/#watch */ + watch(options?: WatchOptions): Promise + + /** Documentation: https://esbuild.github.io/api/#serve */ + serve(options?: ServeOptions): Promise + + cancel(): Promise + dispose(): Promise +} + +// This is a TypeScript type-level function which replaces any keys in "In" +// that aren't in "Out" with "never". We use this to reject properties with +// typos in object literals. See: https://stackoverflow.com/questions/49580725 +type SameShape = In & { [Key in Exclude]: never } + +/** + * This function invokes the "esbuild" command-line tool for you. It returns a + * promise that either resolves with a "BuildResult" object or rejects with a + * "BuildFailure" object. + * + * - Works in node: yes + * - Works in browser: yes + * + * Documentation: https://esbuild.github.io/api/#build + */ +export declare function build(options: SameShape): Promise> + +/** + * This is the advanced long-running form of "build" that supports additional + * features such as watch mode and a local development server. + * + * - Works in node: yes + * - Works in browser: no + * + * Documentation: https://esbuild.github.io/api/#build + */ +export declare function context(options: SameShape): Promise> + +/** + * This function transforms a single JavaScript file. It can be used to minify + * JavaScript, convert TypeScript/JSX to JavaScript, or convert newer JavaScript + * to older JavaScript. It returns a promise that is either resolved with a + * "TransformResult" object or rejected with a "TransformFailure" object. + * + * - Works in node: yes + * - Works in browser: yes + * + * Documentation: https://esbuild.github.io/api/#transform + */ +export declare function transform(input: string | Uint8Array, options?: SameShape): Promise> + +/** + * Converts log messages to formatted message strings suitable for printing in + * the terminal. This allows you to reuse the built-in behavior of esbuild's + * log message formatter. This is a batch-oriented API for efficiency. + * + * - Works in node: yes + * - Works in browser: yes + */ +export declare function formatMessages(messages: PartialMessage[], options: FormatMessagesOptions): Promise + +/** + * Pretty-prints an analysis of the metafile JSON to a string. This is just for + * convenience to be able to match esbuild's pretty-printing exactly. If you want + * to customize it, you can just inspect the data in the metafile yourself. + * + * - Works in node: yes + * - Works in browser: yes + * + * Documentation: https://esbuild.github.io/api/#analyze + */ +export declare function analyzeMetafile(metafile: Metafile | string, options?: AnalyzeMetafileOptions): Promise + +/** + * A synchronous version of "build". + * + * - Works in node: yes + * - Works in browser: no + * + * Documentation: https://esbuild.github.io/api/#build + */ +export declare function buildSync(options: SameShape): BuildResult + +/** + * A synchronous version of "transform". + * + * - Works in node: yes + * - Works in browser: no + * + * Documentation: https://esbuild.github.io/api/#transform + */ +export declare function transformSync(input: string | Uint8Array, options?: SameShape): TransformResult + +/** + * A synchronous version of "formatMessages". + * + * - Works in node: yes + * - Works in browser: no + */ +export declare function formatMessagesSync(messages: PartialMessage[], options: FormatMessagesOptions): string[] + +/** + * A synchronous version of "analyzeMetafile". + * + * - Works in node: yes + * - Works in browser: no + * + * Documentation: https://esbuild.github.io/api/#analyze + */ +export declare function analyzeMetafileSync(metafile: Metafile | string, options?: AnalyzeMetafileOptions): string + +/** + * This configures the browser-based version of esbuild. It is necessary to + * call this first and wait for the returned promise to be resolved before + * making other API calls when using esbuild in the browser. + * + * - Works in node: yes + * - Works in browser: yes ("options" is required) + * + * Documentation: https://esbuild.github.io/api/#browser + */ +export declare function initialize(options: InitializeOptions): Promise + +export interface InitializeOptions { + /** + * The URL of the "esbuild.wasm" file. This must be provided when running + * esbuild in the browser. + */ + wasmURL?: string | URL + + /** + * The result of calling "new WebAssembly.Module(buffer)" where "buffer" + * is a typed array or ArrayBuffer containing the binary code of the + * "esbuild.wasm" file. + * + * You can use this as an alternative to "wasmURL" for environments where it's + * not possible to download the WebAssembly module. + */ + wasmModule?: WebAssembly.Module + + /** + * By default esbuild runs the WebAssembly-based browser API in a web worker + * to avoid blocking the UI thread. This can be disabled by setting "worker" + * to false. + */ + worker?: boolean +} + +export let version: string + +// Call this function to terminate esbuild's child process. The child process +// is not terminated and re-created after each API call because it's more +// efficient to keep it around when there are multiple API calls. +// +// In node this happens automatically before the parent node process exits. So +// you only need to call this if you know you will not make any more esbuild +// API calls and you want to clean up resources. +// +// Unlike node, Deno lacks the necessary APIs to clean up child processes +// automatically. You must manually call stop() in Deno when you're done +// using esbuild or Deno will continue running forever. +// +// Another reason you might want to call this is if you are using esbuild from +// within a Deno test. Deno fails tests that create a child process without +// killing it before the test ends, so you have to call this function (and +// await the returned promise) in every Deno test that uses esbuild. +export declare function stop(): Promise + +// Note: These declarations exist to avoid type errors when you omit "dom" from +// "lib" in your "tsconfig.json" file. TypeScript confusingly declares the +// global "WebAssembly" type in "lib.dom.d.ts" even though it has nothing to do +// with the browser DOM and is present in many non-browser JavaScript runtimes +// (e.g. node and deno). Declaring it here allows esbuild's API to be used in +// these scenarios. +// +// There's an open issue about getting this problem corrected (although these +// declarations will need to remain even if this is fixed for backward +// compatibility with older TypeScript versions): +// +// https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/826 +// +declare global { + namespace WebAssembly { + interface Module { + } + } + interface URL { + } +} diff --git a/node_modules/esbuild/lib/main.js b/node_modules/esbuild/lib/main.js new file mode 100644 index 0000000..3a48a5c --- /dev/null +++ b/node_modules/esbuild/lib/main.js @@ -0,0 +1,2532 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// lib/npm/node.ts +var node_exports = {}; +__export(node_exports, { + analyzeMetafile: () => analyzeMetafile, + analyzeMetafileSync: () => analyzeMetafileSync, + build: () => build, + buildSync: () => buildSync, + context: () => context, + default: () => node_default, + formatMessages: () => formatMessages, + formatMessagesSync: () => formatMessagesSync, + initialize: () => initialize, + stop: () => stop, + transform: () => transform, + transformSync: () => transformSync, + version: () => version +}); +module.exports = __toCommonJS(node_exports); + +// lib/shared/stdio_protocol.ts +function encodePacket(packet) { + let visit = (value) => { + if (value === null) { + bb.write8(0); + } else if (typeof value === "boolean") { + bb.write8(1); + bb.write8(+value); + } else if (typeof value === "number") { + bb.write8(2); + bb.write32(value | 0); + } else if (typeof value === "string") { + bb.write8(3); + bb.write(encodeUTF8(value)); + } else if (value instanceof Uint8Array) { + bb.write8(4); + bb.write(value); + } else if (value instanceof Array) { + bb.write8(5); + bb.write32(value.length); + for (let item of value) { + visit(item); + } + } else { + let keys = Object.keys(value); + bb.write8(6); + bb.write32(keys.length); + for (let key of keys) { + bb.write(encodeUTF8(key)); + visit(value[key]); + } + } + }; + let bb = new ByteBuffer(); + bb.write32(0); + bb.write32(packet.id << 1 | +!packet.isRequest); + visit(packet.value); + writeUInt32LE(bb.buf, bb.len - 4, 0); + return bb.buf.subarray(0, bb.len); +} +function decodePacket(bytes) { + let visit = () => { + switch (bb.read8()) { + case 0: + return null; + case 1: + return !!bb.read8(); + case 2: + return bb.read32(); + case 3: + return decodeUTF8(bb.read()); + case 4: + return bb.read(); + case 5: { + let count = bb.read32(); + let value2 = []; + for (let i = 0; i < count; i++) { + value2.push(visit()); + } + return value2; + } + case 6: { + let count = bb.read32(); + let value2 = {}; + for (let i = 0; i < count; i++) { + value2[decodeUTF8(bb.read())] = visit(); + } + return value2; + } + default: + throw new Error("Invalid packet"); + } + }; + let bb = new ByteBuffer(bytes); + let id = bb.read32(); + let isRequest = (id & 1) === 0; + id >>>= 1; + let value = visit(); + if (bb.ptr !== bytes.length) { + throw new Error("Invalid packet"); + } + return { id, isRequest, value }; +} +var ByteBuffer = class { + constructor(buf = new Uint8Array(1024)) { + this.buf = buf; + this.len = 0; + this.ptr = 0; + } + _write(delta) { + if (this.len + delta > this.buf.length) { + let clone = new Uint8Array((this.len + delta) * 2); + clone.set(this.buf); + this.buf = clone; + } + this.len += delta; + return this.len - delta; + } + write8(value) { + let offset = this._write(1); + this.buf[offset] = value; + } + write32(value) { + let offset = this._write(4); + writeUInt32LE(this.buf, value, offset); + } + write(bytes) { + let offset = this._write(4 + bytes.length); + writeUInt32LE(this.buf, bytes.length, offset); + this.buf.set(bytes, offset + 4); + } + _read(delta) { + if (this.ptr + delta > this.buf.length) { + throw new Error("Invalid packet"); + } + this.ptr += delta; + return this.ptr - delta; + } + read8() { + return this.buf[this._read(1)]; + } + read32() { + return readUInt32LE(this.buf, this._read(4)); + } + read() { + let length = this.read32(); + let bytes = new Uint8Array(length); + let ptr = this._read(bytes.length); + bytes.set(this.buf.subarray(ptr, ptr + length)); + return bytes; + } +}; +var encodeUTF8; +var decodeUTF8; +var encodeInvariant; +if (typeof TextEncoder !== "undefined" && typeof TextDecoder !== "undefined") { + let encoder = new TextEncoder(); + let decoder = new TextDecoder(); + encodeUTF8 = (text) => encoder.encode(text); + decodeUTF8 = (bytes) => decoder.decode(bytes); + encodeInvariant = 'new TextEncoder().encode("")'; +} else if (typeof Buffer !== "undefined") { + encodeUTF8 = (text) => Buffer.from(text); + decodeUTF8 = (bytes) => { + let { buffer, byteOffset, byteLength } = bytes; + return Buffer.from(buffer, byteOffset, byteLength).toString(); + }; + encodeInvariant = 'Buffer.from("")'; +} else { + throw new Error("No UTF-8 codec found"); +} +if (!(encodeUTF8("") instanceof Uint8Array)) + throw new Error(`Invariant violation: "${encodeInvariant} instanceof Uint8Array" is incorrectly false + +This indicates that your JavaScript environment is broken. You cannot use +esbuild in this environment because esbuild relies on this invariant. This +is not a problem with esbuild. You need to fix your environment instead. +`); +function readUInt32LE(buffer, offset) { + return (buffer[offset++] | buffer[offset++] << 8 | buffer[offset++] << 16 | buffer[offset++] << 24) >>> 0; +} +function writeUInt32LE(buffer, value, offset) { + buffer[offset++] = value; + buffer[offset++] = value >> 8; + buffer[offset++] = value >> 16; + buffer[offset++] = value >> 24; +} + +// lib/shared/uint8array_json_parser.ts +var fromCharCode = String.fromCharCode; +function throwSyntaxError(bytes, index, message) { + const c = bytes[index]; + let line = 1; + let column = 0; + for (let i = 0; i < index; i++) { + if (bytes[i] === 10 /* Newline */) { + line++; + column = 0; + } else { + column++; + } + } + throw new SyntaxError( + message ? message : index === bytes.length ? "Unexpected end of input while parsing JSON" : c >= 32 && c <= 126 ? `Unexpected character ${fromCharCode(c)} in JSON at position ${index} (line ${line}, column ${column})` : `Unexpected byte 0x${c.toString(16)} in JSON at position ${index} (line ${line}, column ${column})` + ); +} +function JSON_parse(bytes) { + if (!(bytes instanceof Uint8Array)) { + throw new Error(`JSON input must be a Uint8Array`); + } + const propertyStack = []; + const objectStack = []; + const stateStack = []; + const length = bytes.length; + let property = null; + let state = 0 /* TopLevel */; + let object; + let i = 0; + while (i < length) { + let c = bytes[i++]; + if (c <= 32 /* Space */) { + continue; + } + let value; + if (state === 2 /* Object */ && property === null && c !== 34 /* Quote */ && c !== 125 /* CloseBrace */) { + throwSyntaxError(bytes, --i); + } + switch (c) { + // True + case 116 /* LowerT */: { + if (bytes[i++] !== 114 /* LowerR */ || bytes[i++] !== 117 /* LowerU */ || bytes[i++] !== 101 /* LowerE */) { + throwSyntaxError(bytes, --i); + } + value = true; + break; + } + // False + case 102 /* LowerF */: { + if (bytes[i++] !== 97 /* LowerA */ || bytes[i++] !== 108 /* LowerL */ || bytes[i++] !== 115 /* LowerS */ || bytes[i++] !== 101 /* LowerE */) { + throwSyntaxError(bytes, --i); + } + value = false; + break; + } + // Null + case 110 /* LowerN */: { + if (bytes[i++] !== 117 /* LowerU */ || bytes[i++] !== 108 /* LowerL */ || bytes[i++] !== 108 /* LowerL */) { + throwSyntaxError(bytes, --i); + } + value = null; + break; + } + // Number begin + case 45 /* Minus */: + case 46 /* Dot */: + case 48 /* Digit0 */: + case 49 /* Digit1 */: + case 50 /* Digit2 */: + case 51 /* Digit3 */: + case 52 /* Digit4 */: + case 53 /* Digit5 */: + case 54 /* Digit6 */: + case 55 /* Digit7 */: + case 56 /* Digit8 */: + case 57 /* Digit9 */: { + let index = i; + value = fromCharCode(c); + c = bytes[i]; + while (true) { + switch (c) { + case 43 /* Plus */: + case 45 /* Minus */: + case 46 /* Dot */: + case 48 /* Digit0 */: + case 49 /* Digit1 */: + case 50 /* Digit2 */: + case 51 /* Digit3 */: + case 52 /* Digit4 */: + case 53 /* Digit5 */: + case 54 /* Digit6 */: + case 55 /* Digit7 */: + case 56 /* Digit8 */: + case 57 /* Digit9 */: + case 101 /* LowerE */: + case 69 /* UpperE */: { + value += fromCharCode(c); + c = bytes[++i]; + continue; + } + } + break; + } + value = +value; + if (isNaN(value)) { + throwSyntaxError(bytes, --index, "Invalid number"); + } + break; + } + // String begin + case 34 /* Quote */: { + value = ""; + while (true) { + if (i >= length) { + throwSyntaxError(bytes, length); + } + c = bytes[i++]; + if (c === 34 /* Quote */) { + break; + } else if (c === 92 /* Backslash */) { + switch (bytes[i++]) { + // Normal escape sequence + case 34 /* Quote */: + value += '"'; + break; + case 47 /* Slash */: + value += "/"; + break; + case 92 /* Backslash */: + value += "\\"; + break; + case 98 /* LowerB */: + value += "\b"; + break; + case 102 /* LowerF */: + value += "\f"; + break; + case 110 /* LowerN */: + value += "\n"; + break; + case 114 /* LowerR */: + value += "\r"; + break; + case 116 /* LowerT */: + value += " "; + break; + // Unicode escape sequence + case 117 /* LowerU */: { + let code = 0; + for (let j = 0; j < 4; j++) { + c = bytes[i++]; + code <<= 4; + if (c >= 48 /* Digit0 */ && c <= 57 /* Digit9 */) code |= c - 48 /* Digit0 */; + else if (c >= 97 /* LowerA */ && c <= 102 /* LowerF */) code |= c + (10 - 97 /* LowerA */); + else if (c >= 65 /* UpperA */ && c <= 70 /* UpperF */) code |= c + (10 - 65 /* UpperA */); + else throwSyntaxError(bytes, --i); + } + value += fromCharCode(code); + break; + } + // Invalid escape sequence + default: + throwSyntaxError(bytes, --i); + break; + } + } else if (c <= 127) { + value += fromCharCode(c); + } else if ((c & 224) === 192) { + value += fromCharCode((c & 31) << 6 | bytes[i++] & 63); + } else if ((c & 240) === 224) { + value += fromCharCode((c & 15) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63); + } else if ((c & 248) == 240) { + let codePoint = (c & 7) << 18 | (bytes[i++] & 63) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63; + if (codePoint > 65535) { + codePoint -= 65536; + value += fromCharCode(codePoint >> 10 & 1023 | 55296); + codePoint = 56320 | codePoint & 1023; + } + value += fromCharCode(codePoint); + } + } + value[0]; + break; + } + // Array begin + case 91 /* OpenBracket */: { + value = []; + propertyStack.push(property); + objectStack.push(object); + stateStack.push(state); + property = null; + object = value; + state = 1 /* Array */; + continue; + } + // Object begin + case 123 /* OpenBrace */: { + value = {}; + propertyStack.push(property); + objectStack.push(object); + stateStack.push(state); + property = null; + object = value; + state = 2 /* Object */; + continue; + } + // Array end + case 93 /* CloseBracket */: { + if (state !== 1 /* Array */) { + throwSyntaxError(bytes, --i); + } + value = object; + property = propertyStack.pop(); + object = objectStack.pop(); + state = stateStack.pop(); + break; + } + // Object end + case 125 /* CloseBrace */: { + if (state !== 2 /* Object */) { + throwSyntaxError(bytes, --i); + } + value = object; + property = propertyStack.pop(); + object = objectStack.pop(); + state = stateStack.pop(); + break; + } + default: { + throwSyntaxError(bytes, --i); + } + } + c = bytes[i]; + while (c <= 32 /* Space */) { + c = bytes[++i]; + } + switch (state) { + case 0 /* TopLevel */: { + if (i === length) { + return value; + } + break; + } + case 1 /* Array */: { + object.push(value); + if (c === 44 /* Comma */) { + i++; + continue; + } + if (c === 93 /* CloseBracket */) { + continue; + } + break; + } + case 2 /* Object */: { + if (property === null) { + property = value; + if (c === 58 /* Colon */) { + i++; + continue; + } + } else { + object[property] = value; + property = null; + if (c === 44 /* Comma */) { + i++; + continue; + } + if (c === 125 /* CloseBrace */) { + continue; + } + } + break; + } + } + break; + } + throwSyntaxError(bytes, i); +} + +// lib/shared/common.ts +var quote = JSON.stringify; +var buildLogLevelDefault = "warning"; +var transformLogLevelDefault = "silent"; +function validateAndJoinStringArray(values, what) { + const toJoin = []; + for (const value of values) { + validateStringValue(value, what); + if (value.indexOf(",") >= 0) throw new Error(`Invalid ${what}: ${value}`); + toJoin.push(value); + } + return toJoin.join(","); +} +var canBeAnything = () => null; +var mustBeBoolean = (value) => typeof value === "boolean" ? null : "a boolean"; +var mustBeString = (value) => typeof value === "string" ? null : "a string"; +var mustBeRegExp = (value) => value instanceof RegExp ? null : "a RegExp object"; +var mustBeInteger = (value) => typeof value === "number" && value === (value | 0) ? null : "an integer"; +var mustBeValidPortNumber = (value) => typeof value === "number" && value === (value | 0) && value >= 0 && value <= 65535 ? null : "a valid port number"; +var mustBeFunction = (value) => typeof value === "function" ? null : "a function"; +var mustBeArray = (value) => Array.isArray(value) ? null : "an array"; +var mustBeArrayOfStrings = (value) => Array.isArray(value) && value.every((x) => typeof x === "string") ? null : "an array of strings"; +var mustBeObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? null : "an object"; +var mustBeEntryPoints = (value) => typeof value === "object" && value !== null ? null : "an array or an object"; +var mustBeWebAssemblyModule = (value) => value instanceof WebAssembly.Module ? null : "a WebAssembly.Module"; +var mustBeObjectOrNull = (value) => typeof value === "object" && !Array.isArray(value) ? null : "an object or null"; +var mustBeStringOrBoolean = (value) => typeof value === "string" || typeof value === "boolean" ? null : "a string or a boolean"; +var mustBeStringOrObject = (value) => typeof value === "string" || typeof value === "object" && value !== null && !Array.isArray(value) ? null : "a string or an object"; +var mustBeStringOrArrayOfStrings = (value) => typeof value === "string" || Array.isArray(value) && value.every((x) => typeof x === "string") ? null : "a string or an array of strings"; +var mustBeStringOrUint8Array = (value) => typeof value === "string" || value instanceof Uint8Array ? null : "a string or a Uint8Array"; +var mustBeStringOrURL = (value) => typeof value === "string" || value instanceof URL ? null : "a string or a URL"; +function getFlag(object, keys, key, mustBeFn) { + let value = object[key]; + keys[key + ""] = true; + if (value === void 0) return void 0; + let mustBe = mustBeFn(value); + if (mustBe !== null) throw new Error(`${quote(key)} must be ${mustBe}`); + return value; +} +function checkForInvalidFlags(object, keys, where) { + for (let key in object) { + if (!(key in keys)) { + throw new Error(`Invalid option ${where}: ${quote(key)}`); + } + } +} +function validateInitializeOptions(options) { + let keys = /* @__PURE__ */ Object.create(null); + let wasmURL = getFlag(options, keys, "wasmURL", mustBeStringOrURL); + let wasmModule = getFlag(options, keys, "wasmModule", mustBeWebAssemblyModule); + let worker = getFlag(options, keys, "worker", mustBeBoolean); + checkForInvalidFlags(options, keys, "in initialize() call"); + return { + wasmURL, + wasmModule, + worker + }; +} +function validateMangleCache(mangleCache) { + let validated; + if (mangleCache !== void 0) { + validated = /* @__PURE__ */ Object.create(null); + for (let key in mangleCache) { + let value = mangleCache[key]; + if (typeof value === "string" || value === false) { + validated[key] = value; + } else { + throw new Error(`Expected ${quote(key)} in mangle cache to map to either a string or false`); + } + } + } + return validated; +} +function pushLogFlags(flags, options, keys, isTTY2, logLevelDefault) { + let color = getFlag(options, keys, "color", mustBeBoolean); + let logLevel = getFlag(options, keys, "logLevel", mustBeString); + let logLimit = getFlag(options, keys, "logLimit", mustBeInteger); + if (color !== void 0) flags.push(`--color=${color}`); + else if (isTTY2) flags.push(`--color=true`); + flags.push(`--log-level=${logLevel || logLevelDefault}`); + flags.push(`--log-limit=${logLimit || 0}`); +} +function validateStringValue(value, what, key) { + if (typeof value !== "string") { + throw new Error(`Expected value for ${what}${key !== void 0 ? " " + quote(key) : ""} to be a string, got ${typeof value} instead`); + } + return value; +} +function pushCommonFlags(flags, options, keys) { + let legalComments = getFlag(options, keys, "legalComments", mustBeString); + let sourceRoot = getFlag(options, keys, "sourceRoot", mustBeString); + let sourcesContent = getFlag(options, keys, "sourcesContent", mustBeBoolean); + let target = getFlag(options, keys, "target", mustBeStringOrArrayOfStrings); + let format = getFlag(options, keys, "format", mustBeString); + let globalName = getFlag(options, keys, "globalName", mustBeString); + let mangleProps = getFlag(options, keys, "mangleProps", mustBeRegExp); + let reserveProps = getFlag(options, keys, "reserveProps", mustBeRegExp); + let mangleQuoted = getFlag(options, keys, "mangleQuoted", mustBeBoolean); + let minify = getFlag(options, keys, "minify", mustBeBoolean); + let minifySyntax = getFlag(options, keys, "minifySyntax", mustBeBoolean); + let minifyWhitespace = getFlag(options, keys, "minifyWhitespace", mustBeBoolean); + let minifyIdentifiers = getFlag(options, keys, "minifyIdentifiers", mustBeBoolean); + let lineLimit = getFlag(options, keys, "lineLimit", mustBeInteger); + let drop = getFlag(options, keys, "drop", mustBeArrayOfStrings); + let dropLabels = getFlag(options, keys, "dropLabels", mustBeArrayOfStrings); + let charset = getFlag(options, keys, "charset", mustBeString); + let treeShaking = getFlag(options, keys, "treeShaking", mustBeBoolean); + let ignoreAnnotations = getFlag(options, keys, "ignoreAnnotations", mustBeBoolean); + let jsx = getFlag(options, keys, "jsx", mustBeString); + let jsxFactory = getFlag(options, keys, "jsxFactory", mustBeString); + let jsxFragment = getFlag(options, keys, "jsxFragment", mustBeString); + let jsxImportSource = getFlag(options, keys, "jsxImportSource", mustBeString); + let jsxDev = getFlag(options, keys, "jsxDev", mustBeBoolean); + let jsxSideEffects = getFlag(options, keys, "jsxSideEffects", mustBeBoolean); + let define = getFlag(options, keys, "define", mustBeObject); + let logOverride = getFlag(options, keys, "logOverride", mustBeObject); + let supported = getFlag(options, keys, "supported", mustBeObject); + let pure = getFlag(options, keys, "pure", mustBeArrayOfStrings); + let keepNames = getFlag(options, keys, "keepNames", mustBeBoolean); + let platform = getFlag(options, keys, "platform", mustBeString); + let tsconfigRaw = getFlag(options, keys, "tsconfigRaw", mustBeStringOrObject); + let absPaths = getFlag(options, keys, "absPaths", mustBeArrayOfStrings); + if (legalComments) flags.push(`--legal-comments=${legalComments}`); + if (sourceRoot !== void 0) flags.push(`--source-root=${sourceRoot}`); + if (sourcesContent !== void 0) flags.push(`--sources-content=${sourcesContent}`); + if (target) flags.push(`--target=${validateAndJoinStringArray(Array.isArray(target) ? target : [target], "target")}`); + if (format) flags.push(`--format=${format}`); + if (globalName) flags.push(`--global-name=${globalName}`); + if (platform) flags.push(`--platform=${platform}`); + if (tsconfigRaw) flags.push(`--tsconfig-raw=${typeof tsconfigRaw === "string" ? tsconfigRaw : JSON.stringify(tsconfigRaw)}`); + if (minify) flags.push("--minify"); + if (minifySyntax) flags.push("--minify-syntax"); + if (minifyWhitespace) flags.push("--minify-whitespace"); + if (minifyIdentifiers) flags.push("--minify-identifiers"); + if (lineLimit) flags.push(`--line-limit=${lineLimit}`); + if (charset) flags.push(`--charset=${charset}`); + if (treeShaking !== void 0) flags.push(`--tree-shaking=${treeShaking}`); + if (ignoreAnnotations) flags.push(`--ignore-annotations`); + if (drop) for (let what of drop) flags.push(`--drop:${validateStringValue(what, "drop")}`); + if (dropLabels) flags.push(`--drop-labels=${validateAndJoinStringArray(dropLabels, "drop label")}`); + if (absPaths) flags.push(`--abs-paths=${validateAndJoinStringArray(absPaths, "abs paths")}`); + if (mangleProps) flags.push(`--mangle-props=${jsRegExpToGoRegExp(mangleProps)}`); + if (reserveProps) flags.push(`--reserve-props=${jsRegExpToGoRegExp(reserveProps)}`); + if (mangleQuoted !== void 0) flags.push(`--mangle-quoted=${mangleQuoted}`); + if (jsx) flags.push(`--jsx=${jsx}`); + if (jsxFactory) flags.push(`--jsx-factory=${jsxFactory}`); + if (jsxFragment) flags.push(`--jsx-fragment=${jsxFragment}`); + if (jsxImportSource) flags.push(`--jsx-import-source=${jsxImportSource}`); + if (jsxDev) flags.push(`--jsx-dev`); + if (jsxSideEffects) flags.push(`--jsx-side-effects`); + if (define) { + for (let key in define) { + if (key.indexOf("=") >= 0) throw new Error(`Invalid define: ${key}`); + flags.push(`--define:${key}=${validateStringValue(define[key], "define", key)}`); + } + } + if (logOverride) { + for (let key in logOverride) { + if (key.indexOf("=") >= 0) throw new Error(`Invalid log override: ${key}`); + flags.push(`--log-override:${key}=${validateStringValue(logOverride[key], "log override", key)}`); + } + } + if (supported) { + for (let key in supported) { + if (key.indexOf("=") >= 0) throw new Error(`Invalid supported: ${key}`); + const value = supported[key]; + if (typeof value !== "boolean") throw new Error(`Expected value for supported ${quote(key)} to be a boolean, got ${typeof value} instead`); + flags.push(`--supported:${key}=${value}`); + } + } + if (pure) for (let fn of pure) flags.push(`--pure:${validateStringValue(fn, "pure")}`); + if (keepNames) flags.push(`--keep-names`); +} +function flagsForBuildOptions(callName, options, isTTY2, logLevelDefault, writeDefault) { + var _a2; + let flags = []; + let entries = []; + let keys = /* @__PURE__ */ Object.create(null); + let stdinContents = null; + let stdinResolveDir = null; + pushLogFlags(flags, options, keys, isTTY2, logLevelDefault); + pushCommonFlags(flags, options, keys); + let sourcemap = getFlag(options, keys, "sourcemap", mustBeStringOrBoolean); + let bundle = getFlag(options, keys, "bundle", mustBeBoolean); + let splitting = getFlag(options, keys, "splitting", mustBeBoolean); + let preserveSymlinks = getFlag(options, keys, "preserveSymlinks", mustBeBoolean); + let metafile = getFlag(options, keys, "metafile", mustBeBoolean); + let outfile = getFlag(options, keys, "outfile", mustBeString); + let outdir = getFlag(options, keys, "outdir", mustBeString); + let outbase = getFlag(options, keys, "outbase", mustBeString); + let tsconfig = getFlag(options, keys, "tsconfig", mustBeString); + let resolveExtensions = getFlag(options, keys, "resolveExtensions", mustBeArrayOfStrings); + let nodePathsInput = getFlag(options, keys, "nodePaths", mustBeArrayOfStrings); + let mainFields = getFlag(options, keys, "mainFields", mustBeArrayOfStrings); + let conditions = getFlag(options, keys, "conditions", mustBeArrayOfStrings); + let external = getFlag(options, keys, "external", mustBeArrayOfStrings); + let packages = getFlag(options, keys, "packages", mustBeString); + let alias = getFlag(options, keys, "alias", mustBeObject); + let loader = getFlag(options, keys, "loader", mustBeObject); + let outExtension = getFlag(options, keys, "outExtension", mustBeObject); + let publicPath = getFlag(options, keys, "publicPath", mustBeString); + let entryNames = getFlag(options, keys, "entryNames", mustBeString); + let chunkNames = getFlag(options, keys, "chunkNames", mustBeString); + let assetNames = getFlag(options, keys, "assetNames", mustBeString); + let inject = getFlag(options, keys, "inject", mustBeArrayOfStrings); + let banner = getFlag(options, keys, "banner", mustBeObject); + let footer = getFlag(options, keys, "footer", mustBeObject); + let entryPoints = getFlag(options, keys, "entryPoints", mustBeEntryPoints); + let absWorkingDir = getFlag(options, keys, "absWorkingDir", mustBeString); + let stdin = getFlag(options, keys, "stdin", mustBeObject); + let write = (_a2 = getFlag(options, keys, "write", mustBeBoolean)) != null ? _a2 : writeDefault; + let allowOverwrite = getFlag(options, keys, "allowOverwrite", mustBeBoolean); + let mangleCache = getFlag(options, keys, "mangleCache", mustBeObject); + keys.plugins = true; + checkForInvalidFlags(options, keys, `in ${callName}() call`); + if (sourcemap) flags.push(`--sourcemap${sourcemap === true ? "" : `=${sourcemap}`}`); + if (bundle) flags.push("--bundle"); + if (allowOverwrite) flags.push("--allow-overwrite"); + if (splitting) flags.push("--splitting"); + if (preserveSymlinks) flags.push("--preserve-symlinks"); + if (metafile) flags.push(`--metafile`); + if (outfile) flags.push(`--outfile=${outfile}`); + if (outdir) flags.push(`--outdir=${outdir}`); + if (outbase) flags.push(`--outbase=${outbase}`); + if (tsconfig) flags.push(`--tsconfig=${tsconfig}`); + if (packages) flags.push(`--packages=${packages}`); + if (resolveExtensions) flags.push(`--resolve-extensions=${validateAndJoinStringArray(resolveExtensions, "resolve extension")}`); + if (publicPath) flags.push(`--public-path=${publicPath}`); + if (entryNames) flags.push(`--entry-names=${entryNames}`); + if (chunkNames) flags.push(`--chunk-names=${chunkNames}`); + if (assetNames) flags.push(`--asset-names=${assetNames}`); + if (mainFields) flags.push(`--main-fields=${validateAndJoinStringArray(mainFields, "main field")}`); + if (conditions) flags.push(`--conditions=${validateAndJoinStringArray(conditions, "condition")}`); + if (external) for (let name of external) flags.push(`--external:${validateStringValue(name, "external")}`); + if (alias) { + for (let old in alias) { + if (old.indexOf("=") >= 0) throw new Error(`Invalid package name in alias: ${old}`); + flags.push(`--alias:${old}=${validateStringValue(alias[old], "alias", old)}`); + } + } + if (banner) { + for (let type in banner) { + if (type.indexOf("=") >= 0) throw new Error(`Invalid banner file type: ${type}`); + flags.push(`--banner:${type}=${validateStringValue(banner[type], "banner", type)}`); + } + } + if (footer) { + for (let type in footer) { + if (type.indexOf("=") >= 0) throw new Error(`Invalid footer file type: ${type}`); + flags.push(`--footer:${type}=${validateStringValue(footer[type], "footer", type)}`); + } + } + if (inject) for (let path3 of inject) flags.push(`--inject:${validateStringValue(path3, "inject")}`); + if (loader) { + for (let ext in loader) { + if (ext.indexOf("=") >= 0) throw new Error(`Invalid loader extension: ${ext}`); + flags.push(`--loader:${ext}=${validateStringValue(loader[ext], "loader", ext)}`); + } + } + if (outExtension) { + for (let ext in outExtension) { + if (ext.indexOf("=") >= 0) throw new Error(`Invalid out extension: ${ext}`); + flags.push(`--out-extension:${ext}=${validateStringValue(outExtension[ext], "out extension", ext)}`); + } + } + if (entryPoints) { + if (Array.isArray(entryPoints)) { + for (let i = 0, n = entryPoints.length; i < n; i++) { + let entryPoint = entryPoints[i]; + if (typeof entryPoint === "object" && entryPoint !== null) { + let entryPointKeys = /* @__PURE__ */ Object.create(null); + let input = getFlag(entryPoint, entryPointKeys, "in", mustBeString); + let output = getFlag(entryPoint, entryPointKeys, "out", mustBeString); + checkForInvalidFlags(entryPoint, entryPointKeys, "in entry point at index " + i); + if (input === void 0) throw new Error('Missing property "in" for entry point at index ' + i); + if (output === void 0) throw new Error('Missing property "out" for entry point at index ' + i); + entries.push([output, input]); + } else { + entries.push(["", validateStringValue(entryPoint, "entry point at index " + i)]); + } + } + } else { + for (let key in entryPoints) { + entries.push([key, validateStringValue(entryPoints[key], "entry point", key)]); + } + } + } + if (stdin) { + let stdinKeys = /* @__PURE__ */ Object.create(null); + let contents = getFlag(stdin, stdinKeys, "contents", mustBeStringOrUint8Array); + let resolveDir = getFlag(stdin, stdinKeys, "resolveDir", mustBeString); + let sourcefile = getFlag(stdin, stdinKeys, "sourcefile", mustBeString); + let loader2 = getFlag(stdin, stdinKeys, "loader", mustBeString); + checkForInvalidFlags(stdin, stdinKeys, 'in "stdin" object'); + if (sourcefile) flags.push(`--sourcefile=${sourcefile}`); + if (loader2) flags.push(`--loader=${loader2}`); + if (resolveDir) stdinResolveDir = resolveDir; + if (typeof contents === "string") stdinContents = encodeUTF8(contents); + else if (contents instanceof Uint8Array) stdinContents = contents; + } + let nodePaths = []; + if (nodePathsInput) { + for (let value of nodePathsInput) { + value += ""; + nodePaths.push(value); + } + } + return { + entries, + flags, + write, + stdinContents, + stdinResolveDir, + absWorkingDir, + nodePaths, + mangleCache: validateMangleCache(mangleCache) + }; +} +function flagsForTransformOptions(callName, options, isTTY2, logLevelDefault) { + let flags = []; + let keys = /* @__PURE__ */ Object.create(null); + pushLogFlags(flags, options, keys, isTTY2, logLevelDefault); + pushCommonFlags(flags, options, keys); + let sourcemap = getFlag(options, keys, "sourcemap", mustBeStringOrBoolean); + let sourcefile = getFlag(options, keys, "sourcefile", mustBeString); + let loader = getFlag(options, keys, "loader", mustBeString); + let banner = getFlag(options, keys, "banner", mustBeString); + let footer = getFlag(options, keys, "footer", mustBeString); + let mangleCache = getFlag(options, keys, "mangleCache", mustBeObject); + checkForInvalidFlags(options, keys, `in ${callName}() call`); + if (sourcemap) flags.push(`--sourcemap=${sourcemap === true ? "external" : sourcemap}`); + if (sourcefile) flags.push(`--sourcefile=${sourcefile}`); + if (loader) flags.push(`--loader=${loader}`); + if (banner) flags.push(`--banner=${banner}`); + if (footer) flags.push(`--footer=${footer}`); + return { + flags, + mangleCache: validateMangleCache(mangleCache) + }; +} +function createChannel(streamIn) { + const requestCallbacksByKey = {}; + const closeData = { didClose: false, reason: "" }; + let responseCallbacks = {}; + let nextRequestID = 0; + let nextBuildKey = 0; + let stdout = new Uint8Array(16 * 1024); + let stdoutUsed = 0; + let readFromStdout = (chunk) => { + let limit = stdoutUsed + chunk.length; + if (limit > stdout.length) { + let swap = new Uint8Array(limit * 2); + swap.set(stdout); + stdout = swap; + } + stdout.set(chunk, stdoutUsed); + stdoutUsed += chunk.length; + let offset = 0; + while (offset + 4 <= stdoutUsed) { + let length = readUInt32LE(stdout, offset); + if (offset + 4 + length > stdoutUsed) { + break; + } + offset += 4; + handleIncomingPacket(stdout.subarray(offset, offset + length)); + offset += length; + } + if (offset > 0) { + stdout.copyWithin(0, offset, stdoutUsed); + stdoutUsed -= offset; + } + }; + let afterClose = (error) => { + closeData.didClose = true; + if (error) closeData.reason = ": " + (error.message || error); + const text = "The service was stopped" + closeData.reason; + for (let id in responseCallbacks) { + responseCallbacks[id](text, null); + } + responseCallbacks = {}; + }; + let sendRequest = (refs, value, callback) => { + if (closeData.didClose) return callback("The service is no longer running" + closeData.reason, null); + let id = nextRequestID++; + responseCallbacks[id] = (error, response) => { + try { + callback(error, response); + } finally { + if (refs) refs.unref(); + } + }; + if (refs) refs.ref(); + streamIn.writeToStdin(encodePacket({ id, isRequest: true, value })); + }; + let sendResponse = (id, value) => { + if (closeData.didClose) throw new Error("The service is no longer running" + closeData.reason); + streamIn.writeToStdin(encodePacket({ id, isRequest: false, value })); + }; + let handleRequest = async (id, request) => { + try { + if (request.command === "ping") { + sendResponse(id, {}); + return; + } + if (typeof request.key === "number") { + const requestCallbacks = requestCallbacksByKey[request.key]; + if (!requestCallbacks) { + return; + } + const callback = requestCallbacks[request.command]; + if (callback) { + await callback(id, request); + return; + } + } + throw new Error(`Invalid command: ` + request.command); + } catch (e) { + const errors = [extractErrorMessageV8(e, streamIn, null, void 0, "")]; + try { + sendResponse(id, { errors }); + } catch { + } + } + }; + let isFirstPacket = true; + let handleIncomingPacket = (bytes) => { + if (isFirstPacket) { + isFirstPacket = false; + let binaryVersion = String.fromCharCode(...bytes); + if (binaryVersion !== "0.28.0") { + throw new Error(`Cannot start service: Host version "${"0.28.0"}" does not match binary version ${quote(binaryVersion)}`); + } + return; + } + let packet = decodePacket(bytes); + if (packet.isRequest) { + handleRequest(packet.id, packet.value); + } else { + let callback = responseCallbacks[packet.id]; + delete responseCallbacks[packet.id]; + if (packet.value.error) callback(packet.value.error, {}); + else callback(null, packet.value); + } + }; + let buildOrContext = ({ callName, refs, options, isTTY: isTTY2, defaultWD: defaultWD2, callback }) => { + let refCount = 0; + const buildKey = nextBuildKey++; + const requestCallbacks = {}; + const buildRefs = { + ref() { + if (++refCount === 1) { + if (refs) refs.ref(); + } + }, + unref() { + if (--refCount === 0) { + delete requestCallbacksByKey[buildKey]; + if (refs) refs.unref(); + } + } + }; + requestCallbacksByKey[buildKey] = requestCallbacks; + buildRefs.ref(); + buildOrContextImpl( + callName, + buildKey, + sendRequest, + sendResponse, + buildRefs, + streamIn, + requestCallbacks, + options, + isTTY2, + defaultWD2, + (err, res) => { + try { + callback(err, res); + } finally { + buildRefs.unref(); + } + } + ); + }; + let transform2 = ({ callName, refs, input, options, isTTY: isTTY2, fs: fs3, callback }) => { + const details = createObjectStash(); + let start = (inputPath) => { + try { + if (typeof input !== "string" && !(input instanceof Uint8Array)) + throw new Error('The input to "transform" must be a string or a Uint8Array'); + let { + flags, + mangleCache + } = flagsForTransformOptions(callName, options, isTTY2, transformLogLevelDefault); + let request = { + command: "transform", + flags, + inputFS: inputPath !== null, + input: inputPath !== null ? encodeUTF8(inputPath) : typeof input === "string" ? encodeUTF8(input) : input + }; + if (mangleCache) request.mangleCache = mangleCache; + sendRequest(refs, request, (error, response) => { + if (error) return callback(new Error(error), null); + let errors = replaceDetailsInMessages(response.errors, details); + let warnings = replaceDetailsInMessages(response.warnings, details); + let outstanding = 1; + let next = () => { + if (--outstanding === 0) { + let result = { + warnings, + code: response.code, + map: response.map, + mangleCache: void 0, + legalComments: void 0 + }; + if ("legalComments" in response) result.legalComments = response == null ? void 0 : response.legalComments; + if (response.mangleCache) result.mangleCache = response == null ? void 0 : response.mangleCache; + callback(null, result); + } + }; + if (errors.length > 0) return callback(failureErrorWithLog("Transform failed", errors, warnings), null); + if (response.codeFS) { + outstanding++; + fs3.readFile(response.code, (err, contents) => { + if (err !== null) { + callback(err, null); + } else { + response.code = contents; + next(); + } + }); + } + if (response.mapFS) { + outstanding++; + fs3.readFile(response.map, (err, contents) => { + if (err !== null) { + callback(err, null); + } else { + response.map = contents; + next(); + } + }); + } + next(); + }); + } catch (e) { + let flags = []; + try { + pushLogFlags(flags, options, {}, isTTY2, transformLogLevelDefault); + } catch { + } + const error = extractErrorMessageV8(e, streamIn, details, void 0, ""); + sendRequest(refs, { command: "error", flags, error }, () => { + error.detail = details.load(error.detail); + callback(failureErrorWithLog("Transform failed", [error], []), null); + }); + } + }; + if ((typeof input === "string" || input instanceof Uint8Array) && input.length > 1024 * 1024) { + let next = start; + start = () => fs3.writeFile(input, next); + } + start(null); + }; + let formatMessages2 = ({ callName, refs, messages, options, callback }) => { + if (!options) throw new Error(`Missing second argument in ${callName}() call`); + let keys = {}; + let kind = getFlag(options, keys, "kind", mustBeString); + let color = getFlag(options, keys, "color", mustBeBoolean); + let terminalWidth = getFlag(options, keys, "terminalWidth", mustBeInteger); + checkForInvalidFlags(options, keys, `in ${callName}() call`); + if (kind === void 0) throw new Error(`Missing "kind" in ${callName}() call`); + if (kind !== "error" && kind !== "warning") throw new Error(`Expected "kind" to be "error" or "warning" in ${callName}() call`); + let request = { + command: "format-msgs", + messages: sanitizeMessages(messages, "messages", null, "", terminalWidth), + isWarning: kind === "warning" + }; + if (color !== void 0) request.color = color; + if (terminalWidth !== void 0) request.terminalWidth = terminalWidth; + sendRequest(refs, request, (error, response) => { + if (error) return callback(new Error(error), null); + callback(null, response.messages); + }); + }; + let analyzeMetafile2 = ({ callName, refs, metafile, options, callback }) => { + if (options === void 0) options = {}; + let keys = {}; + let color = getFlag(options, keys, "color", mustBeBoolean); + let verbose = getFlag(options, keys, "verbose", mustBeBoolean); + checkForInvalidFlags(options, keys, `in ${callName}() call`); + let request = { + command: "analyze-metafile", + metafile + }; + if (color !== void 0) request.color = color; + if (verbose !== void 0) request.verbose = verbose; + sendRequest(refs, request, (error, response) => { + if (error) return callback(new Error(error), null); + callback(null, response.result); + }); + }; + return { + readFromStdout, + afterClose, + service: { + buildOrContext, + transform: transform2, + formatMessages: formatMessages2, + analyzeMetafile: analyzeMetafile2 + } + }; +} +function buildOrContextImpl(callName, buildKey, sendRequest, sendResponse, refs, streamIn, requestCallbacks, options, isTTY2, defaultWD2, callback) { + const details = createObjectStash(); + const isContext = callName === "context"; + const handleError = (e, pluginName) => { + const flags = []; + try { + pushLogFlags(flags, options, {}, isTTY2, buildLogLevelDefault); + } catch { + } + const message = extractErrorMessageV8(e, streamIn, details, void 0, pluginName); + sendRequest(refs, { command: "error", flags, error: message }, () => { + message.detail = details.load(message.detail); + callback(failureErrorWithLog(isContext ? "Context failed" : "Build failed", [message], []), null); + }); + }; + let plugins; + if (typeof options === "object") { + const value = options.plugins; + if (value !== void 0) { + if (!Array.isArray(value)) return handleError(new Error(`"plugins" must be an array`), ""); + plugins = value; + } + } + if (plugins && plugins.length > 0) { + if (streamIn.isSync) return handleError(new Error("Cannot use plugins in synchronous API calls"), ""); + handlePlugins( + buildKey, + sendRequest, + sendResponse, + refs, + streamIn, + requestCallbacks, + options, + plugins, + details + ).then( + (result) => { + if (!result.ok) return handleError(result.error, result.pluginName); + try { + buildOrContextContinue(result.requestPlugins, result.runOnEndCallbacks, result.scheduleOnDisposeCallbacks); + } catch (e) { + handleError(e, ""); + } + }, + (e) => handleError(e, "") + ); + return; + } + try { + buildOrContextContinue(null, (result, done) => done([], []), () => { + }); + } catch (e) { + handleError(e, ""); + } + function buildOrContextContinue(requestPlugins, runOnEndCallbacks, scheduleOnDisposeCallbacks) { + const writeDefault = streamIn.hasFS; + const { + entries, + flags, + write, + stdinContents, + stdinResolveDir, + absWorkingDir, + nodePaths, + mangleCache + } = flagsForBuildOptions(callName, options, isTTY2, buildLogLevelDefault, writeDefault); + if (write && !streamIn.hasFS) throw new Error(`The "write" option is unavailable in this environment`); + const request = { + command: "build", + key: buildKey, + entries, + flags, + write, + stdinContents, + stdinResolveDir, + absWorkingDir: absWorkingDir || defaultWD2, + nodePaths, + context: isContext + }; + if (requestPlugins) request.plugins = requestPlugins; + if (mangleCache) request.mangleCache = mangleCache; + const buildResponseToResult = (response, callback2) => { + const result = { + errors: replaceDetailsInMessages(response.errors, details), + warnings: replaceDetailsInMessages(response.warnings, details), + outputFiles: void 0, + metafile: void 0, + mangleCache: void 0 + }; + const originalErrors = result.errors.slice(); + const originalWarnings = result.warnings.slice(); + if (response.outputFiles) result.outputFiles = response.outputFiles.map(convertOutputFiles); + if (response.metafile && response.metafile.length) result.metafile = parseJSON(response.metafile); + if (response.mangleCache) result.mangleCache = response.mangleCache; + if (response.writeToStdout !== void 0) console.log(decodeUTF8(response.writeToStdout).replace(/\n$/, "")); + runOnEndCallbacks(result, (onEndErrors, onEndWarnings) => { + if (originalErrors.length > 0 || onEndErrors.length > 0) { + const error = failureErrorWithLog("Build failed", originalErrors.concat(onEndErrors), originalWarnings.concat(onEndWarnings)); + return callback2(error, null, onEndErrors, onEndWarnings); + } + callback2(null, result, onEndErrors, onEndWarnings); + }); + }; + let latestResultPromise; + let provideLatestResult; + if (isContext) + requestCallbacks["on-end"] = (id, request2) => new Promise((resolve) => { + buildResponseToResult(request2, (err, result, onEndErrors, onEndWarnings) => { + const response = { + errors: onEndErrors, + warnings: onEndWarnings + }; + if (provideLatestResult) provideLatestResult(err, result); + latestResultPromise = void 0; + provideLatestResult = void 0; + sendResponse(id, response); + resolve(); + }); + }); + sendRequest(refs, request, (error, response) => { + if (error) return callback(new Error(error), null); + if (!isContext) { + return buildResponseToResult(response, (err, res) => { + scheduleOnDisposeCallbacks(); + return callback(err, res); + }); + } + if (response.errors.length > 0) { + return callback(failureErrorWithLog("Context failed", response.errors, response.warnings), null); + } + let didDispose = false; + const result = { + rebuild: () => { + if (!latestResultPromise) latestResultPromise = new Promise((resolve, reject) => { + let settlePromise; + provideLatestResult = (err, result2) => { + if (!settlePromise) settlePromise = () => err ? reject(err) : resolve(result2); + }; + const triggerAnotherBuild = () => { + const request2 = { + command: "rebuild", + key: buildKey + }; + sendRequest(refs, request2, (error2, response2) => { + if (error2) { + reject(new Error(error2)); + } else if (settlePromise) { + settlePromise(); + } else { + triggerAnotherBuild(); + } + }); + }; + triggerAnotherBuild(); + }); + return latestResultPromise; + }, + watch: (options2 = {}) => new Promise((resolve, reject) => { + if (!streamIn.hasFS) throw new Error(`Cannot use the "watch" API in this environment`); + const keys = {}; + const delay = getFlag(options2, keys, "delay", mustBeInteger); + checkForInvalidFlags(options2, keys, `in watch() call`); + const request2 = { + command: "watch", + key: buildKey + }; + if (delay) request2.delay = delay; + sendRequest(refs, request2, (error2) => { + if (error2) reject(new Error(error2)); + else resolve(void 0); + }); + }), + serve: (options2 = {}) => new Promise((resolve, reject) => { + if (!streamIn.hasFS) throw new Error(`Cannot use the "serve" API in this environment`); + const keys = {}; + const port = getFlag(options2, keys, "port", mustBeValidPortNumber); + const host = getFlag(options2, keys, "host", mustBeString); + const servedir = getFlag(options2, keys, "servedir", mustBeString); + const keyfile = getFlag(options2, keys, "keyfile", mustBeString); + const certfile = getFlag(options2, keys, "certfile", mustBeString); + const fallback = getFlag(options2, keys, "fallback", mustBeString); + const cors = getFlag(options2, keys, "cors", mustBeObject); + const onRequest = getFlag(options2, keys, "onRequest", mustBeFunction); + checkForInvalidFlags(options2, keys, `in serve() call`); + const request2 = { + command: "serve", + key: buildKey, + onRequest: !!onRequest + }; + if (port !== void 0) request2.port = port; + if (host !== void 0) request2.host = host; + if (servedir !== void 0) request2.servedir = servedir; + if (keyfile !== void 0) request2.keyfile = keyfile; + if (certfile !== void 0) request2.certfile = certfile; + if (fallback !== void 0) request2.fallback = fallback; + if (cors) { + const corsKeys = {}; + const origin = getFlag(cors, corsKeys, "origin", mustBeStringOrArrayOfStrings); + checkForInvalidFlags(cors, corsKeys, `on "cors" object`); + if (Array.isArray(origin)) request2.corsOrigin = origin; + else if (origin !== void 0) request2.corsOrigin = [origin]; + } + sendRequest(refs, request2, (error2, response2) => { + if (error2) return reject(new Error(error2)); + if (onRequest) { + requestCallbacks["serve-request"] = (id, request3) => { + onRequest(request3.args); + sendResponse(id, {}); + }; + } + resolve(response2); + }); + }), + cancel: () => new Promise((resolve) => { + if (didDispose) return resolve(); + const request2 = { + command: "cancel", + key: buildKey + }; + sendRequest(refs, request2, () => { + resolve(); + }); + }), + dispose: () => new Promise((resolve) => { + if (didDispose) return resolve(); + didDispose = true; + const request2 = { + command: "dispose", + key: buildKey + }; + sendRequest(refs, request2, () => { + resolve(); + scheduleOnDisposeCallbacks(); + refs.unref(); + }); + }) + }; + refs.ref(); + callback(null, result); + }); + } +} +var handlePlugins = async (buildKey, sendRequest, sendResponse, refs, streamIn, requestCallbacks, initialOptions, plugins, details) => { + let onStartCallbacks = []; + let onEndCallbacks = []; + let onResolveCallbacks = {}; + let onLoadCallbacks = {}; + let onDisposeCallbacks = []; + let nextCallbackID = 0; + let i = 0; + let requestPlugins = []; + let isSetupDone = false; + plugins = [...plugins]; + for (let item of plugins) { + let keys = {}; + if (typeof item !== "object") throw new Error(`Plugin at index ${i} must be an object`); + const name = getFlag(item, keys, "name", mustBeString); + if (typeof name !== "string" || name === "") throw new Error(`Plugin at index ${i} is missing a name`); + try { + let setup = getFlag(item, keys, "setup", mustBeFunction); + if (typeof setup !== "function") throw new Error(`Plugin is missing a setup function`); + checkForInvalidFlags(item, keys, `on plugin ${quote(name)}`); + let plugin = { + name, + onStart: false, + onEnd: false, + onResolve: [], + onLoad: [] + }; + i++; + let resolve = (path3, options = {}) => { + if (!isSetupDone) throw new Error('Cannot call "resolve" before plugin setup has completed'); + if (typeof path3 !== "string") throw new Error(`The path to resolve must be a string`); + let keys2 = /* @__PURE__ */ Object.create(null); + let pluginName = getFlag(options, keys2, "pluginName", mustBeString); + let importer = getFlag(options, keys2, "importer", mustBeString); + let namespace = getFlag(options, keys2, "namespace", mustBeString); + let resolveDir = getFlag(options, keys2, "resolveDir", mustBeString); + let kind = getFlag(options, keys2, "kind", mustBeString); + let pluginData = getFlag(options, keys2, "pluginData", canBeAnything); + let importAttributes = getFlag(options, keys2, "with", mustBeObject); + checkForInvalidFlags(options, keys2, "in resolve() call"); + return new Promise((resolve2, reject) => { + const request = { + command: "resolve", + path: path3, + key: buildKey, + pluginName: name + }; + if (pluginName != null) request.pluginName = pluginName; + if (importer != null) request.importer = importer; + if (namespace != null) request.namespace = namespace; + if (resolveDir != null) request.resolveDir = resolveDir; + if (kind != null) request.kind = kind; + else throw new Error(`Must specify "kind" when calling "resolve"`); + if (pluginData != null) request.pluginData = details.store(pluginData); + if (importAttributes != null) request.with = sanitizeStringMap(importAttributes, "with"); + sendRequest(refs, request, (error, response) => { + if (error !== null) reject(new Error(error)); + else resolve2({ + errors: replaceDetailsInMessages(response.errors, details), + warnings: replaceDetailsInMessages(response.warnings, details), + path: response.path, + external: response.external, + sideEffects: response.sideEffects, + namespace: response.namespace, + suffix: response.suffix, + pluginData: details.load(response.pluginData) + }); + }); + }); + }; + let promise = setup({ + initialOptions, + resolve, + onStart(callback) { + let registeredText = `This error came from the "onStart" callback registered here:`; + let registeredNote = extractCallerV8(new Error(registeredText), streamIn, "onStart"); + onStartCallbacks.push({ name, callback, note: registeredNote }); + plugin.onStart = true; + }, + onEnd(callback) { + let registeredText = `This error came from the "onEnd" callback registered here:`; + let registeredNote = extractCallerV8(new Error(registeredText), streamIn, "onEnd"); + onEndCallbacks.push({ name, callback, note: registeredNote }); + plugin.onEnd = true; + }, + onResolve(options, callback) { + let registeredText = `This error came from the "onResolve" callback registered here:`; + let registeredNote = extractCallerV8(new Error(registeredText), streamIn, "onResolve"); + let keys2 = {}; + let filter = getFlag(options, keys2, "filter", mustBeRegExp); + let namespace = getFlag(options, keys2, "namespace", mustBeString); + checkForInvalidFlags(options, keys2, `in onResolve() call for plugin ${quote(name)}`); + if (filter == null) throw new Error(`onResolve() call is missing a filter`); + let id = nextCallbackID++; + onResolveCallbacks[id] = { name, callback, note: registeredNote }; + plugin.onResolve.push({ id, filter: jsRegExpToGoRegExp(filter), namespace: namespace || "" }); + }, + onLoad(options, callback) { + let registeredText = `This error came from the "onLoad" callback registered here:`; + let registeredNote = extractCallerV8(new Error(registeredText), streamIn, "onLoad"); + let keys2 = {}; + let filter = getFlag(options, keys2, "filter", mustBeRegExp); + let namespace = getFlag(options, keys2, "namespace", mustBeString); + checkForInvalidFlags(options, keys2, `in onLoad() call for plugin ${quote(name)}`); + if (filter == null) throw new Error(`onLoad() call is missing a filter`); + let id = nextCallbackID++; + onLoadCallbacks[id] = { name, callback, note: registeredNote }; + plugin.onLoad.push({ id, filter: jsRegExpToGoRegExp(filter), namespace: namespace || "" }); + }, + onDispose(callback) { + onDisposeCallbacks.push(callback); + }, + esbuild: streamIn.esbuild + }); + if (promise) await promise; + requestPlugins.push(plugin); + } catch (e) { + return { ok: false, error: e, pluginName: name }; + } + } + requestCallbacks["on-start"] = async (id, request) => { + details.clear(); + let response = { errors: [], warnings: [] }; + await Promise.all(onStartCallbacks.map(async ({ name, callback, note }) => { + try { + let result = await callback(); + if (result != null) { + if (typeof result !== "object") throw new Error(`Expected onStart() callback in plugin ${quote(name)} to return an object`); + let keys = {}; + let errors = getFlag(result, keys, "errors", mustBeArray); + let warnings = getFlag(result, keys, "warnings", mustBeArray); + checkForInvalidFlags(result, keys, `from onStart() callback in plugin ${quote(name)}`); + if (errors != null) response.errors.push(...sanitizeMessages(errors, "errors", details, name, void 0)); + if (warnings != null) response.warnings.push(...sanitizeMessages(warnings, "warnings", details, name, void 0)); + } + } catch (e) { + response.errors.push(extractErrorMessageV8(e, streamIn, details, note && note(), name)); + } + })); + sendResponse(id, response); + }; + requestCallbacks["on-resolve"] = async (id, request) => { + let response = {}, name = "", callback, note; + for (let id2 of request.ids) { + try { + ({ name, callback, note } = onResolveCallbacks[id2]); + let result = await callback({ + path: request.path, + importer: request.importer, + namespace: request.namespace, + resolveDir: request.resolveDir, + kind: request.kind, + pluginData: details.load(request.pluginData), + with: request.with + }); + if (result != null) { + if (typeof result !== "object") throw new Error(`Expected onResolve() callback in plugin ${quote(name)} to return an object`); + let keys = {}; + let pluginName = getFlag(result, keys, "pluginName", mustBeString); + let path3 = getFlag(result, keys, "path", mustBeString); + let namespace = getFlag(result, keys, "namespace", mustBeString); + let suffix = getFlag(result, keys, "suffix", mustBeString); + let external = getFlag(result, keys, "external", mustBeBoolean); + let sideEffects = getFlag(result, keys, "sideEffects", mustBeBoolean); + let pluginData = getFlag(result, keys, "pluginData", canBeAnything); + let errors = getFlag(result, keys, "errors", mustBeArray); + let warnings = getFlag(result, keys, "warnings", mustBeArray); + let watchFiles = getFlag(result, keys, "watchFiles", mustBeArrayOfStrings); + let watchDirs = getFlag(result, keys, "watchDirs", mustBeArrayOfStrings); + checkForInvalidFlags(result, keys, `from onResolve() callback in plugin ${quote(name)}`); + response.id = id2; + if (pluginName != null) response.pluginName = pluginName; + if (path3 != null) response.path = path3; + if (namespace != null) response.namespace = namespace; + if (suffix != null) response.suffix = suffix; + if (external != null) response.external = external; + if (sideEffects != null) response.sideEffects = sideEffects; + if (pluginData != null) response.pluginData = details.store(pluginData); + if (errors != null) response.errors = sanitizeMessages(errors, "errors", details, name, void 0); + if (warnings != null) response.warnings = sanitizeMessages(warnings, "warnings", details, name, void 0); + if (watchFiles != null) response.watchFiles = sanitizeStringArray(watchFiles, "watchFiles"); + if (watchDirs != null) response.watchDirs = sanitizeStringArray(watchDirs, "watchDirs"); + break; + } + } catch (e) { + response = { id: id2, errors: [extractErrorMessageV8(e, streamIn, details, note && note(), name)] }; + break; + } + } + sendResponse(id, response); + }; + requestCallbacks["on-load"] = async (id, request) => { + let response = {}, name = "", callback, note; + for (let id2 of request.ids) { + try { + ({ name, callback, note } = onLoadCallbacks[id2]); + let result = await callback({ + path: request.path, + namespace: request.namespace, + suffix: request.suffix, + pluginData: details.load(request.pluginData), + with: request.with + }); + if (result != null) { + if (typeof result !== "object") throw new Error(`Expected onLoad() callback in plugin ${quote(name)} to return an object`); + let keys = {}; + let pluginName = getFlag(result, keys, "pluginName", mustBeString); + let contents = getFlag(result, keys, "contents", mustBeStringOrUint8Array); + let resolveDir = getFlag(result, keys, "resolveDir", mustBeString); + let pluginData = getFlag(result, keys, "pluginData", canBeAnything); + let loader = getFlag(result, keys, "loader", mustBeString); + let errors = getFlag(result, keys, "errors", mustBeArray); + let warnings = getFlag(result, keys, "warnings", mustBeArray); + let watchFiles = getFlag(result, keys, "watchFiles", mustBeArrayOfStrings); + let watchDirs = getFlag(result, keys, "watchDirs", mustBeArrayOfStrings); + checkForInvalidFlags(result, keys, `from onLoad() callback in plugin ${quote(name)}`); + response.id = id2; + if (pluginName != null) response.pluginName = pluginName; + if (contents instanceof Uint8Array) response.contents = contents; + else if (contents != null) response.contents = encodeUTF8(contents); + if (resolveDir != null) response.resolveDir = resolveDir; + if (pluginData != null) response.pluginData = details.store(pluginData); + if (loader != null) response.loader = loader; + if (errors != null) response.errors = sanitizeMessages(errors, "errors", details, name, void 0); + if (warnings != null) response.warnings = sanitizeMessages(warnings, "warnings", details, name, void 0); + if (watchFiles != null) response.watchFiles = sanitizeStringArray(watchFiles, "watchFiles"); + if (watchDirs != null) response.watchDirs = sanitizeStringArray(watchDirs, "watchDirs"); + break; + } + } catch (e) { + response = { id: id2, errors: [extractErrorMessageV8(e, streamIn, details, note && note(), name)] }; + break; + } + } + sendResponse(id, response); + }; + let runOnEndCallbacks = (result, done) => done([], []); + if (onEndCallbacks.length > 0) { + runOnEndCallbacks = (result, done) => { + (async () => { + const onEndErrors = []; + const onEndWarnings = []; + for (const { name, callback, note } of onEndCallbacks) { + let newErrors; + let newWarnings; + try { + const value = await callback(result); + if (value != null) { + if (typeof value !== "object") throw new Error(`Expected onEnd() callback in plugin ${quote(name)} to return an object`); + let keys = {}; + let errors = getFlag(value, keys, "errors", mustBeArray); + let warnings = getFlag(value, keys, "warnings", mustBeArray); + checkForInvalidFlags(value, keys, `from onEnd() callback in plugin ${quote(name)}`); + if (errors != null) newErrors = sanitizeMessages(errors, "errors", details, name, void 0); + if (warnings != null) newWarnings = sanitizeMessages(warnings, "warnings", details, name, void 0); + } + } catch (e) { + newErrors = [extractErrorMessageV8(e, streamIn, details, note && note(), name)]; + } + if (newErrors) { + onEndErrors.push(...newErrors); + try { + result.errors.push(...newErrors); + } catch { + } + } + if (newWarnings) { + onEndWarnings.push(...newWarnings); + try { + result.warnings.push(...newWarnings); + } catch { + } + } + } + done(onEndErrors, onEndWarnings); + })(); + }; + } + let scheduleOnDisposeCallbacks = () => { + for (const cb of onDisposeCallbacks) { + setTimeout(() => cb(), 0); + } + }; + isSetupDone = true; + return { + ok: true, + requestPlugins, + runOnEndCallbacks, + scheduleOnDisposeCallbacks + }; +}; +function createObjectStash() { + const map = /* @__PURE__ */ new Map(); + let nextID = 0; + return { + clear() { + map.clear(); + }, + load(id) { + return map.get(id); + }, + store(value) { + if (value === void 0) return -1; + const id = nextID++; + map.set(id, value); + return id; + } + }; +} +function extractCallerV8(e, streamIn, ident) { + let note; + let tried = false; + return () => { + if (tried) return note; + tried = true; + try { + let lines = (e.stack + "").split("\n"); + lines.splice(1, 1); + let location = parseStackLinesV8(streamIn, lines, ident); + if (location) { + note = { text: e.message, location }; + return note; + } + } catch { + } + }; +} +function extractErrorMessageV8(e, streamIn, stash, note, pluginName) { + let text = "Internal error"; + let location = null; + try { + text = (e && e.message || e) + ""; + } catch { + } + try { + location = parseStackLinesV8(streamIn, (e.stack + "").split("\n"), ""); + } catch { + } + return { id: "", pluginName, text, location, notes: note ? [note] : [], detail: stash ? stash.store(e) : -1 }; +} +function parseStackLinesV8(streamIn, lines, ident) { + let at = " at "; + if (streamIn.readFileSync && !lines[0].startsWith(at) && lines[1].startsWith(at)) { + for (let i = 1; i < lines.length; i++) { + let line = lines[i]; + if (!line.startsWith(at)) continue; + line = line.slice(at.length); + while (true) { + let match = /^(?:new |async )?\S+ \((.*)\)$/.exec(line); + if (match) { + line = match[1]; + continue; + } + match = /^eval at \S+ \((.*)\)(?:, \S+:\d+:\d+)?$/.exec(line); + if (match) { + line = match[1]; + continue; + } + match = /^(\S+):(\d+):(\d+)$/.exec(line); + if (match) { + let contents; + try { + contents = streamIn.readFileSync(match[1], "utf8"); + } catch { + break; + } + let lineText = contents.split(/\r\n|\r|\n|\u2028|\u2029/)[+match[2] - 1] || ""; + let column = +match[3] - 1; + let length = lineText.slice(column, column + ident.length) === ident ? ident.length : 0; + return { + file: match[1], + namespace: "file", + line: +match[2], + column: encodeUTF8(lineText.slice(0, column)).length, + length: encodeUTF8(lineText.slice(column, column + length)).length, + lineText: lineText + "\n" + lines.slice(1).join("\n"), + suggestion: "" + }; + } + break; + } + } + } + return null; +} +function failureErrorWithLog(text, errors, warnings) { + let limit = 5; + text += errors.length < 1 ? "" : ` with ${errors.length} error${errors.length < 2 ? "" : "s"}:` + errors.slice(0, limit + 1).map((e, i) => { + if (i === limit) return "\n..."; + if (!e.location) return ` +error: ${e.text}`; + let { file, line, column } = e.location; + let pluginText = e.pluginName ? `[plugin: ${e.pluginName}] ` : ""; + return ` +${file}:${line}:${column}: ERROR: ${pluginText}${e.text}`; + }).join(""); + let error = new Error(text); + for (const [key, value] of [["errors", errors], ["warnings", warnings]]) { + Object.defineProperty(error, key, { + configurable: true, + enumerable: true, + get: () => value, + set: (value2) => Object.defineProperty(error, key, { + configurable: true, + enumerable: true, + value: value2 + }) + }); + } + return error; +} +function replaceDetailsInMessages(messages, stash) { + for (const message of messages) { + message.detail = stash.load(message.detail); + } + return messages; +} +function sanitizeLocation(location, where, terminalWidth) { + if (location == null) return null; + let keys = {}; + let file = getFlag(location, keys, "file", mustBeString); + let namespace = getFlag(location, keys, "namespace", mustBeString); + let line = getFlag(location, keys, "line", mustBeInteger); + let column = getFlag(location, keys, "column", mustBeInteger); + let length = getFlag(location, keys, "length", mustBeInteger); + let lineText = getFlag(location, keys, "lineText", mustBeString); + let suggestion = getFlag(location, keys, "suggestion", mustBeString); + checkForInvalidFlags(location, keys, where); + if (lineText) { + const relevantASCII = lineText.slice( + 0, + (column && column > 0 ? column : 0) + (length && length > 0 ? length : 0) + (terminalWidth && terminalWidth > 0 ? terminalWidth : 80) + ); + if (!/[\x7F-\uFFFF]/.test(relevantASCII) && !/\n/.test(lineText)) { + lineText = relevantASCII; + } + } + return { + file: file || "", + namespace: namespace || "", + line: line || 0, + column: column || 0, + length: length || 0, + lineText: lineText || "", + suggestion: suggestion || "" + }; +} +function sanitizeMessages(messages, property, stash, fallbackPluginName, terminalWidth) { + let messagesClone = []; + let index = 0; + for (const message of messages) { + let keys = {}; + let id = getFlag(message, keys, "id", mustBeString); + let pluginName = getFlag(message, keys, "pluginName", mustBeString); + let text = getFlag(message, keys, "text", mustBeString); + let location = getFlag(message, keys, "location", mustBeObjectOrNull); + let notes = getFlag(message, keys, "notes", mustBeArray); + let detail = getFlag(message, keys, "detail", canBeAnything); + let where = `in element ${index} of "${property}"`; + checkForInvalidFlags(message, keys, where); + let notesClone = []; + if (notes) { + for (const note of notes) { + let noteKeys = {}; + let noteText = getFlag(note, noteKeys, "text", mustBeString); + let noteLocation = getFlag(note, noteKeys, "location", mustBeObjectOrNull); + checkForInvalidFlags(note, noteKeys, where); + notesClone.push({ + text: noteText || "", + location: sanitizeLocation(noteLocation, where, terminalWidth) + }); + } + } + messagesClone.push({ + id: id || "", + pluginName: pluginName || fallbackPluginName, + text: text || "", + location: sanitizeLocation(location, where, terminalWidth), + notes: notesClone, + detail: stash ? stash.store(detail) : -1 + }); + index++; + } + return messagesClone; +} +function sanitizeStringArray(values, property) { + const result = []; + for (const value of values) { + if (typeof value !== "string") throw new Error(`${quote(property)} must be an array of strings`); + result.push(value); + } + return result; +} +function sanitizeStringMap(map, property) { + const result = /* @__PURE__ */ Object.create(null); + for (const key in map) { + const value = map[key]; + if (typeof value !== "string") throw new Error(`key ${quote(key)} in object ${quote(property)} must be a string`); + result[key] = value; + } + return result; +} +function convertOutputFiles({ path: path3, contents, hash }) { + let text = null; + return { + path: path3, + contents, + hash, + get text() { + const binary = this.contents; + if (text === null || binary !== contents) { + contents = binary; + text = decodeUTF8(binary); + } + return text; + } + }; +} +function jsRegExpToGoRegExp(regexp) { + let result = regexp.source; + if (regexp.flags) result = `(?${regexp.flags})${result}`; + return result; +} +function parseJSON(bytes) { + let text; + try { + text = decodeUTF8(bytes); + } catch { + return JSON_parse(bytes); + } + return JSON.parse(text); +} + +// lib/npm/node-platform.ts +var fs = require("fs"); +var os = require("os"); +var path = require("path"); +var ESBUILD_BINARY_PATH = process.env.ESBUILD_BINARY_PATH || ESBUILD_BINARY_PATH; +var isValidBinaryPath = (x) => !!x && x !== "/usr/bin/esbuild"; +var packageDarwin_arm64 = "@esbuild/darwin-arm64"; +var packageDarwin_x64 = "@esbuild/darwin-x64"; +var knownWindowsPackages = { + "win32 arm64 LE": "@esbuild/win32-arm64", + "win32 ia32 LE": "@esbuild/win32-ia32", + "win32 x64 LE": "@esbuild/win32-x64" +}; +var knownUnixlikePackages = { + "aix ppc64 BE": "@esbuild/aix-ppc64", + "android arm64 LE": "@esbuild/android-arm64", + "darwin arm64 LE": "@esbuild/darwin-arm64", + "darwin x64 LE": "@esbuild/darwin-x64", + "freebsd arm64 LE": "@esbuild/freebsd-arm64", + "freebsd x64 LE": "@esbuild/freebsd-x64", + "linux arm LE": "@esbuild/linux-arm", + "linux arm64 LE": "@esbuild/linux-arm64", + "linux ia32 LE": "@esbuild/linux-ia32", + "linux mips64el LE": "@esbuild/linux-mips64el", + "linux ppc64 LE": "@esbuild/linux-ppc64", + "linux riscv64 LE": "@esbuild/linux-riscv64", + "linux s390x BE": "@esbuild/linux-s390x", + "linux x64 LE": "@esbuild/linux-x64", + "linux loong64 LE": "@esbuild/linux-loong64", + "netbsd arm64 LE": "@esbuild/netbsd-arm64", + "netbsd x64 LE": "@esbuild/netbsd-x64", + "openbsd arm64 LE": "@esbuild/openbsd-arm64", + "openbsd x64 LE": "@esbuild/openbsd-x64", + "sunos x64 LE": "@esbuild/sunos-x64" +}; +var knownWebAssemblyFallbackPackages = { + "android arm LE": "@esbuild/android-arm", + "android x64 LE": "@esbuild/android-x64", + "openharmony arm64 LE": "@esbuild/openharmony-arm64" +}; +function pkgAndSubpathForCurrentPlatform() { + let pkg; + let subpath; + let isWASM = false; + let platformKey = `${process.platform} ${os.arch()} ${os.endianness()}`; + if (platformKey in knownWindowsPackages) { + pkg = knownWindowsPackages[platformKey]; + subpath = "esbuild.exe"; + } else if (platformKey in knownUnixlikePackages) { + pkg = knownUnixlikePackages[platformKey]; + subpath = "bin/esbuild"; + } else if (platformKey in knownWebAssemblyFallbackPackages) { + pkg = knownWebAssemblyFallbackPackages[platformKey]; + subpath = "bin/esbuild"; + isWASM = true; + } else { + throw new Error(`Unsupported platform: ${platformKey}`); + } + return { pkg, subpath, isWASM }; +} +function pkgForSomeOtherPlatform() { + const libMainJS = require.resolve("esbuild"); + const nodeModulesDirectory = path.dirname(path.dirname(path.dirname(libMainJS))); + if (path.basename(nodeModulesDirectory) === "node_modules") { + for (const unixKey in knownUnixlikePackages) { + try { + const pkg = knownUnixlikePackages[unixKey]; + if (fs.existsSync(path.join(nodeModulesDirectory, pkg))) return pkg; + } catch { + } + } + for (const windowsKey in knownWindowsPackages) { + try { + const pkg = knownWindowsPackages[windowsKey]; + if (fs.existsSync(path.join(nodeModulesDirectory, pkg))) return pkg; + } catch { + } + } + } + return null; +} +function downloadedBinPath(pkg, subpath) { + const esbuildLibDir = path.dirname(require.resolve("esbuild")); + return path.join(esbuildLibDir, `downloaded-${pkg.replace("/", "-")}-${path.basename(subpath)}`); +} +function generateBinPath() { + if (isValidBinaryPath(ESBUILD_BINARY_PATH)) { + if (!fs.existsSync(ESBUILD_BINARY_PATH)) { + console.warn(`[esbuild] Ignoring bad configuration: ESBUILD_BINARY_PATH=${ESBUILD_BINARY_PATH}`); + } else { + return { binPath: ESBUILD_BINARY_PATH, isWASM: false }; + } + } + const { pkg, subpath, isWASM } = pkgAndSubpathForCurrentPlatform(); + let binPath; + try { + binPath = require.resolve(`${pkg}/${subpath}`); + } catch (e) { + binPath = downloadedBinPath(pkg, subpath); + if (!fs.existsSync(binPath)) { + try { + require.resolve(pkg); + } catch { + const otherPkg = pkgForSomeOtherPlatform(); + if (otherPkg) { + let suggestions = ` +Specifically the "${otherPkg}" package is present but this platform +needs the "${pkg}" package instead. People often get into this +situation by installing esbuild on Windows or macOS and copying "node_modules" +into a Docker image that runs Linux, or by copying "node_modules" between +Windows and WSL environments. + +If you are installing with npm, you can try not copying the "node_modules" +directory when you copy the files over, and running "npm ci" or "npm install" +on the destination platform after the copy. Or you could consider using yarn +instead of npm which has built-in support for installing a package on multiple +platforms simultaneously. + +If you are installing with yarn, you can try listing both this platform and the +other platform in your ".yarnrc.yml" file using the "supportedArchitectures" +feature: https://yarnpkg.com/configuration/yarnrc/#supportedArchitectures +Keep in mind that this means multiple copies of esbuild will be present. +`; + if (pkg === packageDarwin_x64 && otherPkg === packageDarwin_arm64 || pkg === packageDarwin_arm64 && otherPkg === packageDarwin_x64) { + suggestions = ` +Specifically the "${otherPkg}" package is present but this platform +needs the "${pkg}" package instead. People often get into this +situation by installing esbuild with npm running inside of Rosetta 2 and then +trying to use it with node running outside of Rosetta 2, or vice versa (Rosetta +2 is Apple's on-the-fly x86_64-to-arm64 translation service). + +If you are installing with npm, you can try ensuring that both npm and node are +not running under Rosetta 2 and then reinstalling esbuild. This likely involves +changing how you installed npm and/or node. For example, installing node with +the universal installer here should work: https://nodejs.org/en/download/. Or +you could consider using yarn instead of npm which has built-in support for +installing a package on multiple platforms simultaneously. + +If you are installing with yarn, you can try listing both "arm64" and "x64" +in your ".yarnrc.yml" file using the "supportedArchitectures" feature: +https://yarnpkg.com/configuration/yarnrc/#supportedArchitectures +Keep in mind that this means multiple copies of esbuild will be present. +`; + } + throw new Error(` +You installed esbuild for another platform than the one you're currently using. +This won't work because esbuild is written with native code and needs to +install a platform-specific binary executable. +${suggestions} +Another alternative is to use the "esbuild-wasm" package instead, which works +the same way on all platforms. But it comes with a heavy performance cost and +can sometimes be 10x slower than the "esbuild" package, so you may also not +want to do that. +`); + } + throw new Error(`The package "${pkg}" could not be found, and is needed by esbuild. + +If you are installing esbuild with npm, make sure that you don't specify the +"--no-optional" or "--omit=optional" flags. The "optionalDependencies" feature +of "package.json" is used by esbuild to install the correct binary executable +for your current platform.`); + } + throw e; + } + } + if (/\.zip\//.test(binPath)) { + let pnpapi; + try { + pnpapi = require("pnpapi"); + } catch (e) { + } + if (pnpapi) { + const root = pnpapi.getPackageInformation(pnpapi.topLevel).packageLocation; + const binTargetPath = path.join( + root, + "node_modules", + ".cache", + "esbuild", + `pnpapi-${pkg.replace("/", "-")}-${"0.28.0"}-${path.basename(subpath)}` + ); + if (!fs.existsSync(binTargetPath)) { + fs.mkdirSync(path.dirname(binTargetPath), { recursive: true }); + fs.copyFileSync(binPath, binTargetPath); + fs.chmodSync(binTargetPath, 493); + } + return { binPath: binTargetPath, isWASM }; + } + } + return { binPath, isWASM }; +} + +// lib/npm/node.ts +var child_process = require("child_process"); +var crypto = require("crypto"); +var path2 = require("path"); +var fs2 = require("fs"); +var os2 = require("os"); +var tty = require("tty"); +var worker_threads; +if (process.env.ESBUILD_WORKER_THREADS !== "0") { + try { + worker_threads = require("worker_threads"); + } catch { + } + let [major, minor] = process.versions.node.split("."); + if ( + // { + if ((!ESBUILD_BINARY_PATH || false) && (path2.basename(__filename) !== "main.js" || path2.basename(__dirname) !== "lib")) { + throw new Error( + `The esbuild JavaScript API cannot be bundled. Please mark the "esbuild" package as external so it's not included in the bundle. + +More information: The file containing the code for esbuild's JavaScript API (${__filename}) does not appear to be inside the esbuild package on the file system, which usually means that the esbuild package was bundled into another file. This is problematic because the API needs to run a binary executable inside the esbuild package which is located using a relative path from the API code to the executable. If the esbuild package is bundled, the relative path will be incorrect and the executable won't be found.` + ); + } + if (false) { + return ["node", [path2.join(__dirname, "..", "bin", "esbuild")]]; + } else { + const { binPath, isWASM } = generateBinPath(); + if (isWASM) { + return ["node", [binPath]]; + } else { + return [binPath, []]; + } + } +}; +var isTTY = () => tty.isatty(2); +var fsSync = { + readFile(tempFile, callback) { + try { + let contents = fs2.readFileSync(tempFile, "utf8"); + try { + fs2.unlinkSync(tempFile); + } catch { + } + callback(null, contents); + } catch (err) { + callback(err, null); + } + }, + writeFile(contents, callback) { + try { + let tempFile = randomFileName(); + fs2.writeFileSync(tempFile, contents); + callback(tempFile); + } catch { + callback(null); + } + } +}; +var fsAsync = { + readFile(tempFile, callback) { + try { + fs2.readFile(tempFile, "utf8", (err, contents) => { + try { + fs2.unlink(tempFile, () => callback(err, contents)); + } catch { + callback(err, contents); + } + }); + } catch (err) { + callback(err, null); + } + }, + writeFile(contents, callback) { + try { + let tempFile = randomFileName(); + fs2.writeFile(tempFile, contents, (err) => err !== null ? callback(null) : callback(tempFile)); + } catch { + callback(null); + } + } +}; +var version = "0.28.0"; +var build = (options) => ensureServiceIsRunning().build(options); +var context = (buildOptions) => ensureServiceIsRunning().context(buildOptions); +var transform = (input, options) => ensureServiceIsRunning().transform(input, options); +var formatMessages = (messages, options) => ensureServiceIsRunning().formatMessages(messages, options); +var analyzeMetafile = (messages, options) => ensureServiceIsRunning().analyzeMetafile(messages, options); +var buildSync = (options) => { + if (worker_threads && !isInternalWorkerThread) { + if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads); + return workerThreadService.buildSync(options); + } + let result; + runServiceSync((service) => service.buildOrContext({ + callName: "buildSync", + refs: null, + options, + isTTY: isTTY(), + defaultWD, + callback: (err, res) => { + if (err) throw err; + result = res; + } + })); + return result; +}; +var transformSync = (input, options) => { + if (worker_threads && !isInternalWorkerThread) { + if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads); + return workerThreadService.transformSync(input, options); + } + let result; + runServiceSync((service) => service.transform({ + callName: "transformSync", + refs: null, + input, + options: options || {}, + isTTY: isTTY(), + fs: fsSync, + callback: (err, res) => { + if (err) throw err; + result = res; + } + })); + return result; +}; +var formatMessagesSync = (messages, options) => { + if (worker_threads && !isInternalWorkerThread) { + if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads); + return workerThreadService.formatMessagesSync(messages, options); + } + let result; + runServiceSync((service) => service.formatMessages({ + callName: "formatMessagesSync", + refs: null, + messages, + options, + callback: (err, res) => { + if (err) throw err; + result = res; + } + })); + return result; +}; +var analyzeMetafileSync = (metafile, options) => { + if (worker_threads && !isInternalWorkerThread) { + if (!workerThreadService) workerThreadService = startWorkerThreadService(worker_threads); + return workerThreadService.analyzeMetafileSync(metafile, options); + } + let result; + runServiceSync((service) => service.analyzeMetafile({ + callName: "analyzeMetafileSync", + refs: null, + metafile: typeof metafile === "string" ? metafile : JSON.stringify(metafile), + options, + callback: (err, res) => { + if (err) throw err; + result = res; + } + })); + return result; +}; +var stop = () => { + if (stopService) stopService(); + if (workerThreadService) workerThreadService.stop(); + return Promise.resolve(); +}; +var initializeWasCalled = false; +var initialize = (options) => { + options = validateInitializeOptions(options || {}); + if (options.wasmURL) throw new Error(`The "wasmURL" option only works in the browser`); + if (options.wasmModule) throw new Error(`The "wasmModule" option only works in the browser`); + if (options.worker) throw new Error(`The "worker" option only works in the browser`); + if (initializeWasCalled) throw new Error('Cannot call "initialize" more than once'); + ensureServiceIsRunning(); + initializeWasCalled = true; + return Promise.resolve(); +}; +var defaultWD = process.cwd(); +var longLivedService; +var stopService; +var ensureServiceIsRunning = () => { + if (longLivedService) return longLivedService; + let [command, args] = esbuildCommandAndArgs(); + let child = child_process.spawn(command, args.concat(`--service=${"0.28.0"}`, "--ping"), { + windowsHide: true, + stdio: ["pipe", "pipe", "inherit"], + cwd: defaultWD + }); + let { readFromStdout, afterClose, service } = createChannel({ + writeToStdin(bytes) { + child.stdin.write(bytes, (err) => { + if (err) afterClose(err); + }); + }, + readFileSync: fs2.readFileSync, + isSync: false, + hasFS: true, + esbuild: node_exports + }); + child.stdin.on("error", afterClose); + child.on("error", afterClose); + const stdin = child.stdin; + const stdout = child.stdout; + stdout.on("data", readFromStdout); + stdout.on("end", afterClose); + stopService = () => { + stdin.destroy(); + stdout.destroy(); + child.kill(); + initializeWasCalled = false; + longLivedService = void 0; + stopService = void 0; + }; + let refCount = 0; + child.unref(); + if (stdin.unref) { + stdin.unref(); + } + if (stdout.unref) { + stdout.unref(); + } + const refs = { + ref() { + if (++refCount === 1) child.ref(); + }, + unref() { + if (--refCount === 0) child.unref(); + } + }; + longLivedService = { + build: (options) => new Promise((resolve, reject) => { + service.buildOrContext({ + callName: "build", + refs, + options, + isTTY: isTTY(), + defaultWD, + callback: (err, res) => err ? reject(err) : resolve(res) + }); + }), + context: (options) => new Promise((resolve, reject) => service.buildOrContext({ + callName: "context", + refs, + options, + isTTY: isTTY(), + defaultWD, + callback: (err, res) => err ? reject(err) : resolve(res) + })), + transform: (input, options) => new Promise((resolve, reject) => service.transform({ + callName: "transform", + refs, + input, + options: options || {}, + isTTY: isTTY(), + fs: fsAsync, + callback: (err, res) => err ? reject(err) : resolve(res) + })), + formatMessages: (messages, options) => new Promise((resolve, reject) => service.formatMessages({ + callName: "formatMessages", + refs, + messages, + options, + callback: (err, res) => err ? reject(err) : resolve(res) + })), + analyzeMetafile: (metafile, options) => new Promise((resolve, reject) => service.analyzeMetafile({ + callName: "analyzeMetafile", + refs, + metafile: typeof metafile === "string" ? metafile : JSON.stringify(metafile), + options, + callback: (err, res) => err ? reject(err) : resolve(res) + })) + }; + return longLivedService; +}; +var runServiceSync = (callback) => { + let [command, args] = esbuildCommandAndArgs(); + let stdin = new Uint8Array(); + let { readFromStdout, afterClose, service } = createChannel({ + writeToStdin(bytes) { + if (stdin.length !== 0) throw new Error("Must run at most one command"); + stdin = bytes; + }, + isSync: true, + hasFS: true, + esbuild: node_exports + }); + callback(service); + let stdout = child_process.execFileSync(command, args.concat(`--service=${"0.28.0"}`), { + cwd: defaultWD, + windowsHide: true, + input: stdin, + // We don't know how large the output could be. If it's too large, the + // command will fail with ENOBUFS. Reserve 16mb for now since that feels + // like it should be enough. Also allow overriding this with an environment + // variable. + maxBuffer: +process.env.ESBUILD_MAX_BUFFER || 16 * 1024 * 1024 + }); + readFromStdout(stdout); + afterClose(null); +}; +var randomFileName = () => { + return path2.join(os2.tmpdir(), `esbuild-${crypto.randomBytes(32).toString("hex")}`); +}; +var workerThreadService = null; +var startWorkerThreadService = (worker_threads2) => { + let { port1: mainPort, port2: workerPort } = new worker_threads2.MessageChannel(); + let worker = new worker_threads2.Worker(__filename, { + workerData: { workerPort, defaultWD, esbuildVersion: "0.28.0" }, + transferList: [workerPort], + // From node's documentation: https://nodejs.org/api/worker_threads.html + // + // Take care when launching worker threads from preload scripts (scripts loaded + // and run using the `-r` command line flag). Unless the `execArgv` option is + // explicitly set, new Worker threads automatically inherit the command line flags + // from the running process and will preload the same preload scripts as the main + // thread. If the preload script unconditionally launches a worker thread, every + // thread spawned will spawn another until the application crashes. + // + execArgv: [] + }); + let nextID = 0; + let fakeBuildError = (text) => { + let error = new Error(`Build failed with 1 error: +error: ${text}`); + let errors = [{ id: "", pluginName: "", text, location: null, notes: [], detail: void 0 }]; + error.errors = errors; + error.warnings = []; + return error; + }; + let validateBuildSyncOptions = (options) => { + if (!options) return; + let plugins = options.plugins; + if (plugins && plugins.length > 0) throw fakeBuildError(`Cannot use plugins in synchronous API calls`); + }; + let applyProperties = (object, properties) => { + for (let key in properties) { + object[key] = properties[key]; + } + }; + let runCallSync = (command, args) => { + let id = nextID++; + let sharedBuffer = new SharedArrayBuffer(8); + let sharedBufferView = new Int32Array(sharedBuffer); + let msg = { sharedBuffer, id, command, args }; + worker.postMessage(msg); + let status = Atomics.wait(sharedBufferView, 0, 0); + if (status !== "ok" && status !== "not-equal") throw new Error("Internal error: Atomics.wait() failed: " + status); + let { message: { id: id2, resolve, reject, properties } } = worker_threads2.receiveMessageOnPort(mainPort); + if (id !== id2) throw new Error(`Internal error: Expected id ${id} but got id ${id2}`); + if (reject) { + applyProperties(reject, properties); + throw reject; + } + return resolve; + }; + worker.unref(); + return { + buildSync(options) { + validateBuildSyncOptions(options); + return runCallSync("build", [options]); + }, + transformSync(input, options) { + return runCallSync("transform", [input, options]); + }, + formatMessagesSync(messages, options) { + return runCallSync("formatMessages", [messages, options]); + }, + analyzeMetafileSync(metafile, options) { + return runCallSync("analyzeMetafile", [metafile, options]); + }, + stop() { + worker.terminate(); + workerThreadService = null; + } + }; +}; +var startSyncServiceWorker = () => { + let workerPort = worker_threads.workerData.workerPort; + let parentPort = worker_threads.parentPort; + let extractProperties = (object) => { + let properties = {}; + if (object && typeof object === "object") { + for (let key in object) { + properties[key] = object[key]; + } + } + return properties; + }; + try { + let service = ensureServiceIsRunning(); + defaultWD = worker_threads.workerData.defaultWD; + parentPort.on("message", (msg) => { + (async () => { + let { sharedBuffer, id, command, args } = msg; + let sharedBufferView = new Int32Array(sharedBuffer); + try { + switch (command) { + case "build": + workerPort.postMessage({ id, resolve: await service.build(args[0]) }); + break; + case "transform": + workerPort.postMessage({ id, resolve: await service.transform(args[0], args[1]) }); + break; + case "formatMessages": + workerPort.postMessage({ id, resolve: await service.formatMessages(args[0], args[1]) }); + break; + case "analyzeMetafile": + workerPort.postMessage({ id, resolve: await service.analyzeMetafile(args[0], args[1]) }); + break; + default: + throw new Error(`Invalid command: ${command}`); + } + } catch (reject) { + workerPort.postMessage({ id, reject, properties: extractProperties(reject) }); + } + Atomics.add(sharedBufferView, 0, 1); + Atomics.notify(sharedBufferView, 0, Infinity); + })(); + }); + } catch (reject) { + parentPort.on("message", (msg) => { + let { sharedBuffer, id } = msg; + let sharedBufferView = new Int32Array(sharedBuffer); + workerPort.postMessage({ id, reject, properties: extractProperties(reject) }); + Atomics.add(sharedBufferView, 0, 1); + Atomics.notify(sharedBufferView, 0, Infinity); + }); + } +}; +if (isInternalWorkerThread) { + startSyncServiceWorker(); +} +var node_default = node_exports; +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + analyzeMetafile, + analyzeMetafileSync, + build, + buildSync, + context, + formatMessages, + formatMessagesSync, + initialize, + stop, + transform, + transformSync, + version +}); diff --git a/node_modules/esbuild/package.json b/node_modules/esbuild/package.json new file mode 100644 index 0000000..6e02d99 --- /dev/null +++ b/node_modules/esbuild/package.json @@ -0,0 +1,74 @@ +{ + "name": "esbuild", + "version": "0.28.0", + "description": "An extremely fast JavaScript and CSS bundler and minifier.", + "repository": { + "type": "git", + "url": "git+https://github.com/evanw/esbuild.git" + }, + "scripts": { + "postinstall": "node install.js" + }, + "main": "lib/main.js", + "types": "lib/main.d.ts", + "engines": { + "node": ">=18" + }, + "bin": { + "esbuild": "bin/esbuild" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + }, + "license": "MIT", + "esbuild.binaryHashes": { + "@esbuild/aix-ppc64/bin/esbuild": "2037fe2a315e737da7d8fbfff5a582243e0a2ee32462f08c69d4833b4e3f9e06", + "@esbuild/android-arm64/bin/esbuild": "6acacac48d2104e6e58bd92104e86acfb3c8f344e965a910a749ea024ec044e5", + "@esbuild/darwin-arm64/bin/esbuild": "6f0e1237f63fa3bc03963e58f0b0be1b9bfacd8f2dc9a3f28483e8f97e4ef2d6", + "@esbuild/darwin-x64/bin/esbuild": "23dadf855f4ef8cf7c159ed5e4018b8f95d66f7e230a3a04f0add7fb809640ad", + "@esbuild/freebsd-arm64/bin/esbuild": "94f2b3af237a7fb2c5061f311e35b7fca15553304f77222b4da7996a35049b88", + "@esbuild/freebsd-x64/bin/esbuild": "c9f571484b1661b93c90cca5781fccd8fc08116eccd4c77079452e51aa2a8240", + "@esbuild/linux-arm/bin/esbuild": "cd2cf1814f32e671c3765e9a26da6a28fc659be2f95426e8f4a116a1f4eb6949", + "@esbuild/linux-arm64/bin/esbuild": "f76afb23c4c27f7837a4fc5fc7123e2e2c75c7f8c6a54e09bddfc06d39e288dd", + "@esbuild/linux-ia32/bin/esbuild": "37ce681c3d0193063cbbf95267d191583f2ffbb818fc81716915f99613af42e9", + "@esbuild/linux-loong64/bin/esbuild": "0565b36f66537e657447aacf6a79f0d4fb91bca78803d20e46c9077e75af8230", + "@esbuild/linux-mips64el/bin/esbuild": "33e1bdf0e9423efddb2515b0a29fa409826a18eadd6340fff0e3927574b58475", + "@esbuild/linux-ppc64/bin/esbuild": "38a3dbbce29bb4ffb5fa2121478f46eb0d6d4979687718c20111372ccfdba169", + "@esbuild/linux-riscv64/bin/esbuild": "2a7317332a2d2bc27dc0ad33123571fa1a9f3aa448c07f6eb7b7c11354e49da8", + "@esbuild/linux-s390x/bin/esbuild": "d4eb9dcf9f112fee07d3f2256b397cc1ff49ac097d579d07dc5ceacec1276c12", + "@esbuild/linux-x64/bin/esbuild": "aafacdf135322bf47c882a4ea4db33d0375583f5b9c3fd2d4e12258e470568be", + "@esbuild/netbsd-arm64/bin/esbuild": "b4d529ea102b3d8e1d77f136be1d8b8496d641c216db8e4ce314f01b9ba02cd1", + "@esbuild/netbsd-x64/bin/esbuild": "ef999b6e59c2f7ed319f0bc804bffd9c5fec4dd7fd60c77f52300a7ffde2f12d", + "@esbuild/openbsd-arm64/bin/esbuild": "5de4790175cb306939af247b0a33036067a2677162d1a79635c77161c13cebee", + "@esbuild/openbsd-x64/bin/esbuild": "1b0dffef766fd0443f76b3031535a79ebf91050e0438399569539f9eae940d24", + "@esbuild/sunos-x64/bin/esbuild": "81ded843321d3d7a51986859ce859ea8d497e6fa4e3400c9d5ba3df0671a12df", + "@esbuild/win32-arm64/esbuild.exe": "ab1477cf488a7f961c3cd7da0689e4a78f8cac3c30eb1021ca0dba46aab25493", + "@esbuild/win32-ia32/esbuild.exe": "6b687d79c8d59bee797ac9872f3e25f229451059e12e60951f476a32d0e8ee71", + "@esbuild/win32-x64/esbuild.exe": "2ccfd6d5292493a810d21c0b40f9de3ff5252a0a24eb5dea3df07f33dcea0c0e" + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d3a5972 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,469 @@ +{ + "name": "grechka-game", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "esbuild": "^0.28.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4d071dd --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "esbuild": "^0.28.0" + } +} diff --git a/src/config.js b/src/config.js index e90eea2..df7b636 100644 --- a/src/config.js +++ b/src/config.js @@ -1,5 +1,7 @@ // ==================== КОНФИГУРАЦИЯ СЕРВЕРА ==================== -// Возможность переопределить сервер через query string +// This module only sets SERVER_URL. Player name and world ID are handled in main.js +// to avoid duplicate prompts. + const urlParams = new URLSearchParams(window.location.search); export const SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru'; @@ -8,60 +10,3 @@ if (location.protocol === 'https:' && SERVER_URL.startsWith('http://')) { console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP'); alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.'); } - -// ==================== WORLD ID И ИГРОКА ==================== -let worldId = null; -let playerName = localStorage.getItem('minegrechka_playerName') || null; - -// Запрашиваем имя игрока, если его нет -if (!playerName) { - playerName = prompt('Введите ваше имя для игры:') || 'Игрок'; - localStorage.setItem('minegrechka_playerName', playerName); - console.log('Player name set:', playerName); -} - -// Берём worldId из URL или генерируем новый -console.log('Current URL:', window.location.href); -const worldParam = urlParams.get('world'); -console.log('world param:', worldParam); - -// Проверяем на null, undefined или пустую строку -worldId = (worldParam && worldParam.trim() !== '') ? worldParam : null; - -console.log('worldId after params:', worldId, 'type:', typeof worldId); - -// Если worldId отсутствует - генерируем новый и записываем в URL -if (!worldId) { - worldId = Math.random().toString(36).substring(2, 10); - console.log('Generated worldId:', worldId); - - try { - const newUrl = new URL(window.location.href); - newUrl.searchParams.set('world', worldId); - const newUrlString = newUrl.toString(); - console.log('New URL to set:', newUrlString); - - // Проверяем, поддерживается ли history API - if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') { - window.history.replaceState(null, '', newUrlString); - console.log('URL after replaceState:', window.location.href); - console.log('URL after replaceState (direct check):', window.location.search); - } else { - console.error('History API not supported!'); - } - } catch (e) { - console.error('Error updating URL:', e); - } - - console.log('Generated new worldId for browser:', worldId); -} - -console.log('Final worldId:', worldId, 'Player name:', playerName); - -console.log(`Server URL: ${SERVER_URL}, World ID: ${worldId}`); - -// Setters для изменения worldId/playerName из других модулей -export function setWorldId(id) { worldId = id; } -export function setPlayerName(name) { playerName = name; } - -export { worldId, playerName }; \ No newline at end of file diff --git a/src/core/canvas.js b/src/core/canvas.js index 3bd9abe..38a281f 100644 --- a/src/core/canvas.js +++ b/src/core/canvas.js @@ -1,27 +1,41 @@ // ==================== ХОЛСТ И РАЗМЕРЫ ==================== +import { state } from './state.js'; -const gameEl = document.getElementById('game'); -const canvas = document.getElementById('c'); -const ctx = canvas.getContext('2d'); +export function initCanvas() { + const gameEl = document.getElementById('game'); + const canvas = document.getElementById('c'); + const ctx = canvas.getContext('2d'); -// offscreen light map (не вставляем в DOM) -const lightC = document.createElement('canvas'); -const lightCtx = lightC.getContext('2d'); + // offscreen light map + const lightC = document.createElement('canvas'); + const lightCtx = lightC.getContext('2d'); -const dpr = Math.max(1, window.devicePixelRatio || 1); -let W = 0, H = 0; + const dpr = Math.max(1, window.devicePixelRatio || 1); -function resize() { - W = gameEl.clientWidth; - H = gameEl.clientHeight; - canvas.width = W * dpr; - canvas.height = H * dpr; - lightC.width = W * dpr; - lightC.height = H * dpr; - ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + function resize() { + state.W = gameEl.clientWidth; + state.H = gameEl.clientHeight; + canvas.width = state.W * dpr; + canvas.height = state.H * dpr; + lightC.width = state.W * dpr; + lightC.height = state.H * dpr; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + } + + window.addEventListener('resize', resize); + resize(); + + // Store everything on state + state.gameEl = gameEl; + state.canvas = canvas; + state.ctx = ctx; + state.lightC = lightC; + state.lightCtx = lightCtx; + state.dpr = dpr; } -function setW(v) { W = v; } -function setH(v) { H = v; } - -export { canvas, ctx, lightC, lightCtx, dpr, W, H, gameEl, resize, setW, setH }; \ No newline at end of file +// Convenience exports for modules that use canvas directly +export function getCanvas() { return state.canvas; } +export function getCtx() { return state.ctx; } +export function getLightC() { return state.lightC; } +export function getLightCtx() { return state.lightCtx; } diff --git a/src/core/state.js b/src/core/state.js index 17552ba..66df448 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -1,10 +1,36 @@ -import { TILE } from './constants.js'; +import { TILE, SEA_GY, BEDROCK_GY, GEN_MARGIN_X, GRAV, GRAV_WATER, MOVE, JUMP, DAY_LEN, SAVE_KEY, MOVE_SEND_INTERVAL, MAX_RAINDROPS, MAX_CHAT_MESSAGES, VOICE_SERVER } from './constants.js'; // Все мутабельные переменные игры в одном объекте состояния export const state = { + // Константы (для доступа из модулей) + TILE, + SEA_GY, + BEDROCK_GY, + GEN_MARGIN_X, + GRAV, + GRAV_WATER, + MOVE, + JUMP, + DAY_LEN, + SAVE_KEY, + MOVE_SEND_INTERVAL, + MAX_RAINDROPS, + MAX_CHAT_MESSAGES, + VOICE_SERVER, + // Камера camX: 0, camY: 0, + W: 0, + H: 0, + dpr: 1, + + // Canvas/DOM (заполняется в main.js) + gameEl: null, + canvas: null, + ctx: null, + lightC: null, + lightCtx: null, // День/ночь worldTime: 0, @@ -14,6 +40,17 @@ export const state = { isMultiplayer: false, mySocketId: null, socket: null, + voiceSocket: null, + voicePosT: 0, + + // Сервер + SERVER_URL: '', + TELEGRAM_BOT_USERNAME: '', + TELEGRAM_APP_SHORT_NAME: '', + + // Игрок + playerName: null, + worldId: null, // Инвентарь/UI selected: 0, @@ -21,7 +58,9 @@ export const state = { craftOpen: false, inventoryOpen: false, chatOpen: false, + minimapOpen: false, modeIdx: 0, + currentFurnaceKey: null, // Мир worldSeed: Math.floor(Math.random() * 1000000), @@ -44,6 +83,32 @@ export const state = { lastSentX: 0, lastSentY: 0, + // Mining progress + mining: { gx: null, gy: null, progress: 0, hardness: 0 }, + + // Trade/Market + myTgId: null, + tradePanel: null, + tradeContent: null, + marketPanel: null, + marketContent: null, + + // DOM-ссылки (заполняются в main.js) + hpEl: null, + foodEl: null, + sxEl: null, + syEl: null, + todEl: null, + worldIdEl: null, + playerCountEl: null, + deathEl: null, + hotbarEl: null, + craftPanel: null, + recipesEl: null, + inventoryPanel: null, + inventoryGrid: null, + multiplayerStatus: null, + // Игрок player: { x: 6 * TILE, @@ -80,10 +145,13 @@ export const state = { meat: 0, cooked: 0, arrow: 0, wood_pickaxe: 0, stone_pickaxe: 0, iron_pickaxe: 0, wood_sword: 0, stone_sword: 0, iron_sword: 0, + wood_axe: 0, stone_axe: 0, iron_axe: 0, + wood_shovel: 0, stone_shovel: 0, iron_shovel: 0, iron_armor: 0, bow: 0, furnace: 0, bed: 0, boat: 0, - iron_ingot: 0, gold_ingot: 0, copper_ingot: 0 + iron_ingot: 0, gold_ingot: 0, copper_ingot: 0, + cobblestone: 0, }, // Лодка @@ -96,7 +164,7 @@ export const state = { }, // Ввод - inp: { up: false, down: false, left: false, right: false, jump: false, mine: false, build: false, bow: false }, + inp: { up: false, down: false, left: false, right: false, jump: false, mine: false, build: false, bow: false, l: false, r: false, j: false, s: false }, // Мышь mouse: { x: 0, y: 0 }, @@ -153,5 +221,12 @@ export const state = { generated: new Set(), // Изображение героя - heroImg: null -}; \ No newline at end of file + heroImg: null, + + // Сетка мира + grid: new Map(), + blocks: [], + + // In-memory save fallback + inMemorySave: null, +}; diff --git a/src/data/blocks.js b/src/data/blocks.js index 03beedf..8a7b8d5 100644 --- a/src/data/blocks.js +++ b/src/data/blocks.js @@ -1,30 +1,31 @@ export const BLOCKS = { - air: { n:'Воздух', solid:false }, - grass: { n:'Трава', c:'#7cfc00', solid:true }, - dirt: { n:'Грязь', c:'#8b4513', solid:true }, - stone: { n:'Камень', c:'#7f8c8d', solid:true }, - sand: { n:'Песок', c:'#f4d06f', solid:true }, - gravel: { n:'Гравий', c:'#95a5a6', solid:true }, - clay: { n:'Глина', c:'#74b9ff', solid:true }, - wood: { n:'Дерево', c:'#d35400', solid:true }, - planks: { n:'Доски', c:'#e67e22', solid:true }, - ladder: { n:'Лестница',c:'#d35400', solid:false, climbable:true }, - leaves: { n:'Листва', c:'#2ecc71', solid:true }, - glass: { n:'Стекло', c:'rgba(200,240,255,0.25)', solid:true, alpha:0.55 }, - water: { n:'Вода', c:'rgba(52,152,219,0.55)', solid:false, fluid:true }, - coal: { n:'Уголь', c:'#2c3e50', solid:true }, - copper_ore:{ n:'Медь', c:'#e17055', solid:true }, - iron_ore: { n:'Железо', c:'#dcdde1', solid:true }, - iron_armor:{ n:'Железная броня', c:'#95a5a6', solid:false, armor:0.5 }, - gold_ore: { n:'Золото', c:'#f1c40f', solid:true }, - diamond_ore:{n:'Алмаз', c:'#00a8ff', solid:true }, - brick: { n:'Кирпич', c:'#c0392b', solid:true }, - tnt: { n:'TNT', c:'#e74c3c', solid:true, explosive:true }, - campfire: { n:'Костёр', c:'#e67e22', solid:true, lightRadius:190 }, - torch: { n:'Факел', c:'#f9ca24', solid:true, lightRadius:140 }, - bedrock: { n:'Бедрок', c:'#2d3436', solid:true, unbreakable:true }, - flower: { n:'Цветок', c:'#ff4757', solid:false, decor:true }, - bed: { n:'Кровать', c:'#e91e63', solid:true, bed:true }, - boat: { n:'Лодка', c:'#8B4513', solid:false }, - furnace: { n:'Печь', c:'#696969', solid:true, smelting:true } + air: { n:'Воздух', solid:false, hardness:0 }, + grass: { n:'Трава', c:'#7cfc00', solid:true, hardness:3, toolType:'shovel' }, + dirt: { n:'Грязь', c:'#8b4513', solid:true, hardness:3, toolType:'shovel' }, + stone: { n:'Камень', c:'#7f8c8d', solid:true, hardness:6, toolType:'pickaxe' }, + sand: { n:'Песок', c:'#f4d06f', solid:true, hardness:2, toolType:'shovel' }, + gravel: { n:'Гравий', c:'#95a5a6', solid:true, hardness:3, toolType:'shovel' }, + clay: { n:'Глина', c:'#74b9ff', solid:true, hardness:3, toolType:'shovel' }, + wood: { n:'Дерево', c:'#d35400', solid:true, hardness:4, toolType:'axe' }, + planks: { n:'Доски', c:'#e67e22', solid:true, hardness:3, toolType:'axe' }, + ladder: { n:'Лестница',c:'#d35400', solid:false, climbable:true, hardness:1, toolType:null }, + leaves: { n:'Листва', c:'#2ecc71', solid:true, hardness:1, toolType:'axe', minTool:null }, + glass: { n:'Стекло', c:'rgba(200,240,255,0.25)', solid:true, alpha:0.55, hardness:1, toolType:null }, + water: { n:'Вода', c:'rgba(52,152,219,0.55)', solid:false, fluid:true, hardness:0 }, + coal: { n:'Уголь', c:'#2c3e50', solid:true, hardness:8, toolType:'pickaxe', minTool:'wood' }, + copper_ore:{ n:'Медь', c:'#e17055', solid:true, hardness:10, toolType:'pickaxe', minTool:'wood' }, + iron_ore: { n:'Железо', c:'#dcdde1', solid:true, hardness:12, toolType:'pickaxe', minTool:'stone' }, + iron_armor:{ n:'Железная броня', c:'#95a5a6', solid:false, armor:0.5, hardness:0 }, + gold_ore: { n:'Золото', c:'#f1c40f', solid:true, hardness:15, toolType:'pickaxe', minTool:'stone' }, + diamond_ore:{n:'Алмаз', c:'#00a8ff', solid:true, hardness:20, toolType:'pickaxe', minTool:'iron' }, + brick: { n:'Кирпич', c:'#c0392b', solid:true, hardness:8, toolType:'pickaxe' }, + tnt: { n:'TNT', c:'#e74c3c', solid:true, explosive:true, hardness:1, toolType:null }, + campfire: { n:'Костёр', c:'#e67e22', solid:true, lightRadius:190, hardness:2, toolType:null }, + torch: { n:'Факел', c:'#f9ca24', solid:true, lightRadius:140, hardness:1, toolType:null }, + bedrock: { n:'Бедрок', c:'#2d3436', solid:true, unbreakable:true, hardness:Infinity }, + flower: { n:'Цветок', c:'#ff4757', solid:false, decor:true, hardness:0 }, + bed: { n:'Кровать', c:'#e91e63', solid:true, bed:true, hardness:2, toolType:'axe' }, + boat: { n:'Лодка', c:'#8B4513', solid:false, hardness:0 }, + furnace: { n:'Печь', c:'#696969', solid:true, smelting:true, hardness:8, toolType:'pickaxe', minTool:'wood' }, + cobblestone: { n:'Булыжник', c:'#7f8c8d', solid:true, hardness:6, toolType:'pickaxe' }, }; \ No newline at end of file diff --git a/src/data/items.js b/src/data/items.js index ea0439d..1ef6cd7 100644 --- a/src/data/items.js +++ b/src/data/items.js @@ -7,4 +7,12 @@ export const ITEMS = { // Runtime extensions — предметы от обжига ITEMS.iron_ingot = { n:'Железный слиток', icon:'🔩' }; ITEMS.gold_ingot = { n:'Золотой слиток', icon:'🪙' }; -ITEMS.copper_ingot = { n:'Медный слиток', icon:'🟤' }; \ No newline at end of file +ITEMS.copper_ingot = { n:'Медный слиток', icon:'🟤' }; + +// Axes and shovels +ITEMS.wood_axe = { n:'Деревянный топор', icon:'🪓' }; +ITEMS.stone_axe = { n:'Каменный топор', icon:'🪓' }; +ITEMS.iron_axe = { n:'Железный топор', icon:'🪓' }; +ITEMS.wood_shovel = { n:'Деревянная лопата', icon:'🔱' }; +ITEMS.stone_shovel = { n:'Каменная лопата', icon:'🔱' }; +ITEMS.iron_shovel = { n:'Железная лопата', icon:'🔱' }; \ No newline at end of file diff --git a/src/data/recipes.js b/src/data/recipes.js index cc0dfa2..dcf3e33 100644 --- a/src/data/recipes.js +++ b/src/data/recipes.js @@ -14,6 +14,12 @@ export const RECIPES = [ { out:'wood_sword', qty:1, cost:{ wood: 2, planks: 1 } }, { out:'stone_sword', qty:1, cost:{ wood: 1, stone: 2 } }, { out:'iron_sword', qty:1, cost:{ wood: 1, iron_ore: 1 } }, + { out:'wood_axe', qty:1, cost:{ wood:3, planks:2 } }, + { out:'stone_axe', qty:1, cost:{ wood:1, stone:3 } }, + { out:'iron_axe', qty:1, cost:{ wood:1, iron_ore:2 } }, + { out:'wood_shovel', qty:1, cost:{ wood:1, planks:1 } }, + { out:'stone_shovel', qty:1, cost:{ wood:1, stone:2 } }, + { out:'iron_shovel', qty:1, cost:{ wood:1, iron_ore:1 } }, { out:'iron_armor', qty:1, cost:{ iron_ore: 5 } }, { out:'furnace', qty:1, cost:{ stone: 8 } }, { out:'bow', qty:1, cost:{ wood: 3, planks: 2 } }, @@ -22,11 +28,11 @@ export const RECIPES = [ // Рецепты печи (обжиг) export const SMELTING_RECIPES = [ - { in:'sand', qty:1, out:'glass', outQty:1, time:3 }, // песок → стекло - { in:'clay', qty:1, out:'brick', outQty:1, time:3 }, // глина → кирпич - { in:'iron_ore', qty:1, out:'iron_ingot', outQty:1, time:5 }, // железо → слиток - { in:'gold_ore', qty:1, out:'gold_ingot', outQty:1, time:5 }, // золото → слиток - { in:'copper_ore', qty:1, out:'copper_ingot', outQty:1, time:4 }, // медь → слиток - { in:'meat', qty:1, out:'cooked', outQty:1, time:2 }, // сырое мясо → жареное - { in:'cobblestone', qty:1, out:'stone', outQty:1, time:2 } // булыжник → камень + { in:'sand', qty:1, out:'glass', outQty:1, time:3 }, + { in:'clay', qty:1, out:'brick', outQty:1, time:3 }, + { in:'iron_ore', qty:1, out:'iron_ingot', outQty:1, time:5 }, + { in:'gold_ore', qty:1, out:'gold_ingot', outQty:1, time:5 }, + { in:'copper_ore', qty:1, out:'copper_ingot', outQty:1, time:4 }, + { in:'meat', qty:1, out:'cooked', outQty:1, time:2 }, + { in:'cobblestone', qty:1, out:'stone', outQty:1, time:2 } ]; \ No newline at end of file diff --git a/src/data/tools.js b/src/data/tools.js index 10e56eb..87113c9 100644 --- a/src/data/tools.js +++ b/src/data/tools.js @@ -1,14 +1,21 @@ export const TOOLS = { - wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', durability: 60, miningPower: 1, craft: { wood: 3, planks: 2 } }, - stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', durability: 130, miningPower: 2, craft: { wood: 1, stone: 3 } }, - iron_pickaxe: { n:'Железная кирка', icon:'⛏️', durability: 250, miningPower: 3, craft: { wood: 1, iron_ore: 2 } }, - wood_sword: { n:'Деревянный меч', icon:'⚔️', durability: 40, damage: 5, craft: { wood: 2, planks: 1 } }, - stone_sword: { n:'Каменный меч', icon:'⚔️', durability: 100, damage: 8, craft: { wood: 1, stone: 2 } }, - iron_sword: { n:'Железный меч', icon:'⚔️', durability: 200, damage: 12, craft: { wood: 1, iron_ore: 1 } }, - bow: { n:'Лук', icon:'🏹', durability: 150, craft: { wood: 3, string: 0 } } + wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', damage:1, durability:60, toolType:'pickaxe', toolTier:'wood', speed:2 }, + stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', damage:2, durability:132, toolType:'pickaxe', toolTier:'stone', speed:4 }, + iron_pickaxe: { n:'Железная кирка', icon:'⛏️', damage:3, durability:250, toolType:'pickaxe', toolTier:'iron', speed:6 }, + wood_sword: { n:'Деревянный меч', icon:'🗡️', damage:4, durability:60 }, + stone_sword: { n:'Каменный меч', icon:'🗡️', damage:5, durability:132 }, + iron_sword: { n:'Железный меч', icon:'🗡️', damage:7, durability:250 }, + wood_axe: { n:'Деревянный топор', icon:'🪓', damage:3, durability:60, toolType:'axe', toolTier:'wood', speed:2 }, + stone_axe: { n:'Каменный топор', icon:'🪓', damage:4, durability:132, toolType:'axe', toolTier:'stone', speed:4 }, + iron_axe: { n:'Железный топор', icon:'🪓', damage:5, durability:250, toolType:'axe', toolTier:'iron', speed:6 }, + wood_shovel: { n:'Деревянная лопата', icon:'🔱', damage:1, durability:60, toolType:'shovel', toolTier:'wood', speed:2 }, + stone_shovel: { n:'Каменная лопата', icon:'🔱', damage:2, durability:132, toolType:'shovel', toolTier:'stone', speed:4 }, + iron_shovel: { n:'Железная лопата', icon:'🔱', damage:3, durability:250, toolType:'shovel', toolTier:'iron', speed:6 }, + bow: { n:'Лук', icon:'🏹', damage:10, durability:200 }, }; import { state } from '../core/state.js'; +import { BLOCKS } from './blocks.js'; export function addTool(type) { const maxDur = TOOLS[type]?.durability || 60; @@ -29,4 +36,31 @@ export function useTool(type) { } } return false; // no durability entry found +} + +export function getBestTool(blockType) { + const block = BLOCKS[blockType]; + if (!block || !block.toolType) return null; + const toolType = block.toolType; + for (const tier of ['iron', 'stone', 'wood']) { + const toolId = `${tier}_${toolType}`; + if (TOOLS[toolId] && state.inv[toolId] > 0) return toolId; + } + return null; +} + +export function toolTierLessThan(toolTier, minTier) { + const tiers = { wood: 1, stone: 2, iron: 3 }; + return (tiers[toolTier] || 0) < (tiers[minTier] || 0); +} + +export function getMiningSpeed(blockType) { + const block = BLOCKS[blockType]; + if (!block) return 1; + const toolId = getBestTool(blockType); + if (toolId) { + const tool = TOOLS[toolId]; + if (tool.toolType === block.toolType) return tool.speed; + } + return 1; // Bare hands = 1 click per hit } \ No newline at end of file diff --git a/src/game/loop.js b/src/game/loop.js index d30fff5..963a071 100644 --- a/src/game/loop.js +++ b/src/game/loop.js @@ -3,7 +3,7 @@ import { state } from '../core/state.js'; import { BLOCKS } from '../data/blocks.js'; import { playSound } from '../audio/sound-engine.js'; import { getBlock } from '../world/world-storage.js'; -import { updateWaterPhysics } from '../physics/water.js'; +import { updateWaterPhysics } from '../world/water.js'; import { updateWaterFlag } from '../physics/water-detect.js'; import { resolveY, resolveX } from '../physics/collision.js'; import { calculateDamage } from '../entities/player.js'; @@ -54,10 +54,8 @@ export function loop(now) { // Ускорение времени во время сна if (player.sleeping && isNight()) { - state.worldTime += dt * 8 / state.DAY_LEN; // В 8 раз быстрее - // Восстанавливаем здоровье во время сна + state.worldTime += dt * 8 / state.DAY_LEN; player.hp = Math.min(100, player.hp + dt * 20); - // Автоматическое пробуждение когда наступает день if (!isNight()) { player.sleeping = false; } @@ -81,19 +79,19 @@ export function loop(now) { // player updateWaterFlag(player); - // кислород/утопление: тратим O2 только если голова под водой, иначе восстанавливаем + // кислород/утопление if (player.headInWater) { - player.o2 = Math.max(0, player.o2 - 6 * dt); // замедлил в 3.7 раза + player.o2 = Math.max(0, player.o2 - 6 * dt); if (player.o2 === 0) { const damage = calculateDamage(4 * dt); player.hp -= damage; } } else { - player.o2 = Math.min(100, player.o2 + 10 * dt); // замедлил восстановление в 4 раза + player.o2 = Math.min(100, player.o2 + 10 * dt); } - // голод убывает, но HP не отнимает (как просили) - player.hunger = Math.max(0, player.hunger - dt * 0.2); // замедлил в 4 раза + // голод убывает + player.hunger = Math.max(0, player.hunger - dt * 0.2); // Игрок не может двигаться во время сна if (player.sleeping) { @@ -107,64 +105,50 @@ export function loop(now) { // Звук шагов при движении по земле if (player.grounded && !player.inWater && Math.abs(player.vx) > 50) { - const stepInterval = 0.35; // Интервал между шагами в секундах + const stepInterval = 0.35; if (now / 1000 - player.lastStepTime > stepInterval) { playSound('step'); player.lastStepTime = now / 1000; } } - // прыжок/плавание (новая логика) + // прыжок/плавание if (player.inBoat) { - // Игрок в лодке - лодка следует за игроком const dir = (inp.r ? 1 : 0) - (inp.l ? 1 : 0); if (dir) boat.vx = dir * state.MOVE; else boat.vx *= 0.95; - - // Лодка плавает на воде boat.vy = 0; - - // Игрок следует за лодкой (сидит внутри неё) - player.x = boat.x + 2; // Игрок по центру лодки - player.y = boat.y - 4; // Игрок выше лодки (сидит внутри) + player.x = boat.x + 2; + player.y = boat.y - 4; player.vx = boat.vx; player.vy = boat.vy; player.grounded = true; - player.inWater = false; // Игрок не в воде когда в лодке + player.inWater = false; - // Прыжок из лодки (высадка) if (jumpPressed) { - // Возвращаем лодку в инвентарь inv.boat = (inv.boat || 0) + 1; - player.inBoat = false; boat.active = false; - player.y += state.TILE; // Прыгаем из лодки + player.y += state.TILE; player.vy = -state.JUMP * 0.5; playSound('splash'); } } else if (player.inWater) { - // сопротивление в воде player.vx *= 0.90; player.vy *= 0.92; - // Если не нажимаем прыжок - тонем (гравитация в воде) if (!jumpPressed && !inp.j) { - // Применяем гравитацию в воде - игрок тонет player.vy += state.GRAV_WATER * dt; } else { - // Если нажимаем прыжок - поднимаемся на поверхность if (jumpPressed) { - player.vy = Math.min(player.vy, -520); // рывок вверх + player.vy = Math.min(player.vy, -520); } else if (inp.j) { - // если держим — мягкое всплытие player.vy = Math.min(player.vy, -260); } } } else { - // обычный прыжок (только по нажатию) if (jumpPressed && player.grounded && !player.sleeping) { player.vy = -state.JUMP; player.grounded = false; @@ -172,7 +156,7 @@ export function loop(now) { } } - // Гравитация применяется только вне воды и вне лодки + // Гравитация if (!player.inWater && !player.inBoat) { player.vy += state.GRAV * dt; } @@ -182,13 +166,11 @@ export function loop(now) { boat.x += boat.vx * dt; boat.y += boat.vy * dt; - // Лодка не выходит за пределы воды const boatGX = Math.floor(boat.x / state.TILE); const boatGY = Math.floor(boat.y / state.TILE); const below = getBlock(boatGX, boatGY + 1); if (!below || below.t !== 'water') { - // Если лодка вышла из воды - выкидываем игрока inv.boat = (inv.boat || 0) + 1; player.inBoat = false; boat.active = false; @@ -198,7 +180,6 @@ export function loop(now) { } } - // Проверяем, не доплыл ли игрок из лодки if (player.inBoat && !boat.active) { inv.boat = (inv.boat || 0) + 1; player.inBoat = false; @@ -207,7 +188,7 @@ export function loop(now) { playSound('splash'); } - // Sub-stepped physics: применяем движение мелкими шагами + // Sub-stepped physics for (let step = 0; step < steps; step++) { player.y += player.vy * dt; resolveY(player); @@ -215,13 +196,10 @@ export function loop(now) { resolveX(player); } - // Отправляем позицию на сервер (мультиплеер) sendPlayerPosition(); - // Обновляем физику воды updateWaterPhysics(dt); - // Погода и дождь updateWeather(dt); updateRain(dt); @@ -237,33 +215,28 @@ export function loop(now) { // Furnace tick tickFurnaces(dt); - // Обновляем UI печи если открыта if (state.currentFurnaceKey && Math.random() < 0.1) { renderFurnaceUI(); } - // Projectile tick (стрелы) + // Projectile tick for (let i = projectiles.length - 1; i >= 0; i--) { const p = projectiles[i]; p.x += p.vx * dt; p.y += p.vy * dt; - p.vy += 400 * dt; // гравитация + p.vy += 400 * dt; p.life -= dt; - // Столкновение с блоком const gx = Math.floor(p.x / state.TILE); const gy = Math.floor(p.y / state.TILE); const blk = getBlock(gx, gy); if (blk && !blk.dead && BLOCKS[blk.t] && BLOCKS[blk.t].solid) { - // Врезался в стену - if (p.owner === 'player' && Math.random() < 0.5) inv.arrow++; // подбираем ~50% + if (p.owner === 'player' && Math.random() < 0.5) inv.arrow++; projectiles.splice(i, 1); continue; } - // Столкновение с сущностью if (p.owner === 'mob') { - // Попал в игрока if (p.x > player.x && p.x < player.x + player.w && p.y > player.y && p.y < player.y + player.h) { if (player.invuln <= 0) { player.hp -= calculateDamage(p.dmg); @@ -276,7 +249,6 @@ export function loop(now) { continue; } } else { - // Попал в моба — server mobs first in multiplayer let hitMob = false; if (state.isMultiplayer) { for (const [id, sm] of state.serverMobs) { @@ -290,7 +262,6 @@ export function loop(now) { } } if (!hitMob) { - // Local mobs for (let j = mobs.length - 1; j >= 0; j--) { const m = mobs[j]; if (p.x > m.x && p.x < m.x + m.w && p.y > m.y && p.y < m.y + m.h) { @@ -313,7 +284,6 @@ export function loop(now) { } } - // Таймаут if (p.life <= 0) projectiles.splice(i, 1); } @@ -327,12 +297,11 @@ export function loop(now) { } } - // mobs spawn (с обеих сторон камеры) — только в одиночном режиме + // mobs spawn — only in singleplayer state.spawnT += dt; if (!state.isMultiplayer && state.spawnT > 1.8 && mobs.length < 30) { state.spawnT = 0; - // Выбираем сторону спавна (левая или правая) const spawnLeft = Math.random() < 0.5; const gx = spawnLeft ? Math.floor((state.camX - 200) / state.TILE) @@ -343,14 +312,12 @@ export function loop(now) { const wx = gx * state.TILE + 4; const wy = (sgy - 2) * state.TILE; - // не спавнить в воде const top = getBlock(gx, sgy); if (top && top.t === 'water') { // skip } else { const night = isNight(); if (night) { - // Ночью спавним враждебных мобов (максимум 12 хостайл) const hostileCount = mobs.filter(m => m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton').length; if (hostileCount < 12) { const rand = Math.random(); @@ -363,7 +330,6 @@ export function loop(now) { } } } - // Животные спавнятся и днём и ночью (с лимитом) const animalCount = mobs.filter(m => m.kind === 'pig' || m.kind === 'chicken').length; if (animalCount < 8) { mobs.push(Math.random() < 0.5 ? new Pig(wx, wy) : new Chicken(wx, wy)); @@ -371,7 +337,7 @@ export function loop(now) { } } - // mobs update — только локальные (singleplayer) + // mobs update — только локальные if (!state.isMultiplayer) { for (let i = mobs.length - 1; i >= 0; i--) { const m = mobs[i]; @@ -394,7 +360,6 @@ export function loop(now) { if (player.hp <= 0) { state.deathEl.style.display = 'flex'; } else if (state.deathEl.style.display === 'flex') { - // Если HP > 0 но экран смерти всё ещё показан - скрываем его state.deathEl.style.display = 'none'; } @@ -412,7 +377,7 @@ export function loop(now) { ctx.fillStyle = night ? '#070816' : (state.isRaining ? '#6B7B8D' : '#87CEEB'); ctx.fillRect(0, 0, W, H); - // clouds (parallax x/y) + // clouds ctx.save(); ctx.translate(-camX * 0.5, -camY * 0.15); ctx.fillStyle = 'rgba(255,255,255,0.65)'; @@ -432,7 +397,7 @@ export function loop(now) { const maxGY = Math.floor((camY + H) / TILE) + 6; const blocks = state.blocks; - // draw blocks (по массиву, но фильтруем диапазоном) + // draw blocks for (const b of blocks) { if (b.dead) continue; if (b.gx < minGX || b.gx > maxGX || b.gy < minGY || b.gy > maxGY) continue; @@ -447,7 +412,7 @@ export function loop(now) { ctx.drawImage(tex[b.t], b.gx * TILE, b.gy * TILE, TILE, TILE); } - // TNT мигает, если активирован + // TNT мигает if (b.t === 'tnt' && b.active && Math.sin(now / 60) > 0) { ctx.fillStyle = 'rgba(255,255,255,0.45)'; ctx.fillRect(b.gx * TILE, b.gy * TILE, TILE, TILE); @@ -463,6 +428,42 @@ export function loop(now) { } } + // ====== MINING CRACK OVERLAY ====== + if (state.mining.gx !== null && state.mining.hardness > 0) { + const mg = state.mining; + const progress = Math.min(1, mg.progress / mg.hardness); + const cx = mg.gx * TILE - camX + camX; // already in world coords since we translated + const cy = mg.gy * TILE - camY + camY; + // Actually, we're in translated context, so just use world coords + const wx = mg.gx * TILE; + const wy = mg.gy * TILE; + ctx.save(); + ctx.globalAlpha = Math.min(0.8, progress); + // Draw crack lines + const numCracks = Math.floor(progress * 5); + ctx.strokeStyle = '#000'; + ctx.lineWidth = 2; + for (let i = 0; i < numCracks; i++) { + ctx.beginPath(); + ctx.moveTo(wx + TILE/2, wy + TILE/2); + const angle = (i * 72 + 36) * Math.PI / 180; + ctx.lineTo(wx + TILE/2 + Math.cos(angle) * TILE * 0.4, wy + TILE/2 + Math.sin(angle) * TILE * 0.4); + ctx.stroke(); + } + // Darkening overlay + ctx.fillStyle = `rgba(0,0,0,${progress * 0.4})`; + ctx.fillRect(wx, wy, TILE, TILE); + + // Progress bar under block + ctx.globalAlpha = 1; + ctx.fillStyle = '#333'; + ctx.fillRect(wx, wy + TILE - 4, TILE, 4); + ctx.fillStyle = '#2ecc71'; + ctx.fillRect(wx, wy + TILE - 4, TILE * progress, 4); + + ctx.restore(); + } + // mobs const allMobsRender = state.isMultiplayer ? Array.from(state.serverMobs.values()) : mobs; for (const m of allMobsRender) { @@ -480,7 +481,6 @@ export function loop(now) { ctx.fillStyle = '#ff69b4'; ctx.fillRect(m.x + 28, m.y + 12, 6, 6); } else if (m.kind === 'chicken') { - // chicken ctx.fillStyle = '#ecf0f1'; ctx.fillRect(m.x, m.y, m.w, m.h); ctx.fillStyle = '#f39c12'; @@ -488,22 +488,16 @@ export function loop(now) { ctx.fillStyle = '#000'; ctx.fillRect(m.x + 8, m.y + 6, 3, 3); } else if (m.kind === 'creeper') { - // creeper ctx.fillStyle = '#4CAF50'; ctx.fillRect(m.x, m.y, m.w, m.h); - // Глаза ctx.fillStyle = '#000'; ctx.fillRect(m.x + 8, m.y + 8, 4, 4); ctx.fillRect(m.x + 22, m.y + 8, 4, 4); - // Рот - ctx.fillStyle = '#000'; ctx.fillRect(m.x + 12, m.y + 20, 10, 4); - // Ноги ctx.fillStyle = '#4CAF50'; ctx.fillRect(m.x + 4, m.y + 30, 6, 20); ctx.fillRect(m.x + 24, m.y + 30, 6, 20); } else if (m.kind === 'skeleton') { - // skeleton - детализированный ctx.fillStyle = '#ECEFF1'; ctx.fillRect(m.x + 10, m.y + 20, 14, 12); ctx.fillRect(m.x + 8, m.y + 0, 18, 18); @@ -516,7 +510,6 @@ export function loop(now) { ctx.fillRect(m.x + 26, m.y + 20, 6, 14); ctx.fillRect(m.x + 10, m.y + 32, 6, 18); ctx.fillRect(m.x + 18, m.y + 32, 6, 18); - // Лук в руке ctx.save(); ctx.translate(m.x + 30, m.y + 22); ctx.strokeStyle = '#8B4513'; @@ -534,12 +527,12 @@ export function loop(now) { } } - // boat (рисуем первой, чтобы игрок был внутри неё) + // boat if (boat.active) { ctx.drawImage(tex['boat'], boat.x - (TILE - boat.w) / 2, boat.y - (TILE - boat.h) / 2, TILE, TILE); } - // other players (multiplayer) + // other players for (const [socketId, p] of state.otherPlayers) { if (state.heroImg.complete) { ctx.drawImage(state.heroImg, p.x - (TILE - player.w) / 2, p.y - (TILE - player.h) / 2, TILE, TILE); @@ -561,7 +554,7 @@ export function loop(now) { ctx.fillRect(player.x, player.y, player.w, player.h); } - // projectiles (стрелы) + // projectiles for (const p of projectiles) { const angle = Math.atan2(p.vy, p.vx); ctx.save(); @@ -629,7 +622,7 @@ export function loop(now) { ctx.restore(); - // lighting overlay at night (Minecraft-style: offscreen lightmap, wall occlusion, warm flicker) + // lighting overlay at night if (night) { const lightC = state.lightC; const lightCtx = state.lightCtx; @@ -719,7 +712,7 @@ export function loop(now) { ctx.restore(); } - // Дождь (после ночного оверлея) + // Дождь drawRain(); if (Math.random() < 0.25) { state.hpEl.textContent = Math.max(0, Math.ceil(player.hp)); @@ -749,7 +742,7 @@ export function loop(now) { ctx.fillText('Нажмите на кровать чтобы проснуться', W / 2, H / 2 + 40); } - // Миникарта (обновляем раз в ~4 кадра для оптимизации) + // Миникарта if (state.minimapOpen && Math.random() < 0.25) { renderMinimap(); } diff --git a/src/input/mouse-handler.js b/src/input/mouse-handler.js index cfb6014..b492dc7 100644 --- a/src/input/mouse-handler.js +++ b/src/input/mouse-handler.js @@ -3,19 +3,23 @@ import { state } from '../core/state.js'; import { BLOCKS } from '../data/blocks.js'; import { ITEMS } from '../data/items.js'; import { TOOLS } from '../data/tools.js'; +import { getBestTool, useTool, toolTierLessThan, getMiningSpeed } from '../data/tools.js'; import { getBlock, setBlock, removeBlock } from '../world/world-storage.js'; import { playSound } from '../audio/sound-engine.js'; import { rebuildHotbar } from '../ui/hotbar.js'; import { mode } from '../game/modes.js'; import { openFurnaceUI } from '../ui/furnace.js'; import { saveGame } from '../game/save.js'; -import { useTool } from '../data/tools.js'; import { activateTNT } from '../world/tnt.js'; import { sendBlockChange } from '../multiplayer/socket-helpers.js'; import { isNight } from '../entities/mob-ai.js'; export const mouse = { x: null, y: null }; +// Hold-to-mine tracking +let miningHeld = false; +let miningInterval = null; + export function initMouseHandlers() { const canvas = state.canvas; @@ -125,7 +129,7 @@ export function initMouseHandlers() { if (ITEMS[state.selected] && state.inv[state.selected] > 0) { const it = ITEMS[state.selected]; if (state.player.hp < 100 || state.player.hunger < 100) { - playSound('eat1'); // Звук употребления еды + playSound('eat1'); state.player.hunger = Math.min(100, state.player.hunger + it.food); state.player.hp = Math.min(100, state.player.hp + 15); state.inv[state.selected]--; @@ -136,7 +140,7 @@ export function initMouseHandlers() { // жарка на костре: выбран meat + клик по campfire if (b && b.t === 'campfire' && state.selected === 'meat' && state.inv.meat > 0) { - playSound('fire'); // Звук при жарке на костре + playSound('fire'); state.inv.meat--; state.inv.cooked++; rebuildHotbar(); @@ -146,46 +150,151 @@ export function initMouseHandlers() { // Сон на кровати: клик по bed if (b && b.t === 'bed' && isNight()) { state.player.sleeping = true; - saveGame(); // Сохраняем при отходе ко сну + saveGame(); return; } + // ====== MINING PROGRESSION SYSTEM ====== if (mode() === 'mine') { if (!b) return; if (BLOCKS[b.t].fluid || BLOCKS[b.t].decor) return; - if (b.t === 'tnt') { activateTNT(b, 3.2); return; } // не взрывается сразу + if (b.t === 'tnt') { activateTNT(b, 3.2); return; } - const removed = removeBlock(gx, gy); - if (removed) { - state.inv[removed.t] = (state.inv[removed.t] || 0) + 1; + // Calculate mining progress + const hardness = BLOCKS[b.t].hardness || 1; + if (hardness === Infinity) return; // unbreakable (bedrock) + if (hardness === 0) { + // Instant-mine blocks (flower, boat, etc.) + const removed = removeBlock(gx, gy); + if (removed) { + state.inv[removed.t] = (state.inv[removed.t] || 0) + 1; + sendBlockChange(gx, gy, removed.t, 'remove'); + rebuildHotbar(); + } + return; + } - // Тратим прочность кирки (если есть в инвентаре) - const pickTypes = ['iron_pickaxe', 'stone_pickaxe', 'wood_pickaxe']; - for (const pt of pickTypes) { - if (state.inv[pt] > 0) { - const broke = useTool(pt); - if (broke) playSound('cloth1'); // звук поломки - break; + // Same block being mined? + if (state.mining.gx === gx && state.mining.gy === gy) { + state.mining.progress += getMiningSpeed(b.t); + } else { + // New block, start mining + state.mining = { gx, gy, progress: getMiningSpeed(b.t), hardness }; + } + + // Play mining hit sound + if (b.t === 'glass') playSound('glass1'); + else if (b.t === 'sand') playSound('sand1'); + else if (b.t === 'stone' || b.t.endsWith('_ore')) playSound('stone1'); + else if (b.t === 'wood' || b.t === 'planks') playSound('wood1'); + else playSound('cloth1'); + + // Check if block is fully mined + if (state.mining.progress >= hardness) { + // Check drop requirements + const minTool = BLOCKS[b.t].minTool; + let drops = true; + if (minTool) { + const toolId = getBestTool(b.t); + if (toolId) { + const tool = TOOLS[toolId]; + if (toolTierLessThan(tool.toolTier, minTool)) drops = false; + else useTool(toolId); + } else { + drops = false; } + } else { + // Use tool durability if applicable + const toolId = getBestTool(b.t); + if (toolId) useTool(toolId); } - // Отправляем изменение блока на сервер - sendBlockChange(gx, gy, removed.t, 'remove'); + const removed = removeBlock(gx, gy); + if (removed) { + if (drops) { + state.inv[removed.t] = (state.inv[removed.t] || 0) + 1; + } else { + // Drop cobblestone instead for stone-type blocks + if (BLOCKS[removed.t]?.toolType === 'pickaxe') { + state.inv.cobblestone = (state.inv.cobblestone || 0) + 1; + } + // Else nothing drops + } + sendBlockChange(gx, gy, removed.t, 'remove'); - // Звуки при добыче блоков - if (removed.t === 'glass') playSound('glass1'); - else if (removed.t === 'sand') playSound('sand1'); - else if (removed.t === 'snow') playSound('snow1'); - else if (removed.t === 'stone' || removed.t.endsWith('_ore')) playSound('stone1'); - else if (removed.t === 'wood') playSound('wood1'); - else playSound('cloth1'); + // Sound for block break + if (removed.t === 'glass') playSound('glass1'); + else if (removed.t === 'sand') playSound('sand1'); + else if (removed.t === 'stone' || removed.t.endsWith('_ore')) playSound('stone1'); + else if (removed.t === 'wood') playSound('wood1'); + else playSound('cloth1'); - rebuildHotbar(); + rebuildHotbar(); + } + state.mining = { gx: null, gy: null, progress: 0, hardness: 0 }; } + + // Start hold-to-mine for desktop + miningHeld = true; + if (miningInterval) clearInterval(miningInterval); + miningInterval = setInterval(() => { + if (!miningHeld) { clearInterval(miningInterval); miningInterval = null; return; } + // Re-check the block still exists at mining position + const mb = getBlock(state.mining.gx, state.mining.gy); + if (!mb || mb.dead || mb.t !== b.t) { + miningHeld = false; + clearInterval(miningInterval); + miningInterval = null; + return; + } + // Accumulate progress + if (state.mining.gx !== null && state.mining.hardness > 0) { + state.mining.progress += getMiningSpeed(mb.t) * 0.3; // 0.3x per tick for held mining + + if (state.mining.progress >= state.mining.hardness) { + const mgx = state.mining.gx; + const mgy = state.mining.gy; + const mMinTool = BLOCKS[mb.t].minTool; + let mDrops = true; + if (mMinTool) { + const mToolId = getBestTool(mb.t); + if (mToolId) { + const mTool = TOOLS[mToolId]; + if (toolTierLessThan(mTool.toolTier, mMinTool)) mDrops = false; + else useTool(mToolId); + } else { + mDrops = false; + } + } else { + const mToolId = getBestTool(mb.t); + if (mToolId) useTool(mToolId); + } + + const removed = removeBlock(mgx, mgy); + if (removed) { + if (mDrops) { + state.inv[removed.t] = (state.inv[removed.t] || 0) + 1; + } else { + if (BLOCKS[removed.t]?.toolType === 'pickaxe') { + state.inv.cobblestone = (state.inv.cobblestone || 0) + 1; + } + } + sendBlockChange(mgx, mgy, removed.t, 'remove'); + rebuildHotbar(); + } + state.mining = { gx: null, gy: null, progress: 0, hardness: 0 }; + miningHeld = false; + clearInterval(miningInterval); + miningInterval = null; + } + } + }, 100); + return; } + // ====== BUILD MODE ====== if (mode() === 'build') { if (state.inv[state.selected] <= 0) return; if (!BLOCKS[state.selected]) return; @@ -193,27 +302,21 @@ export function initMouseHandlers() { // Проверяем, ставим ли лодку if (state.selected === 'boat') { - // Лодку можно ставить только на воду const waterBelow = getBlock(gx, gy + 1); if (!waterBelow || waterBelow.t !== 'water') { return; } - - // Создаём лодку state.boat.x = gx * state.TILE; state.boat.y = gy * state.TILE; state.boat.vx = 0; state.boat.vy = 0; state.boat.active = true; state.boat.inWater = true; - - // Сажаем игрока в лодку state.player.inBoat = true; state.player.x = state.boat.x; state.player.y = state.boat.y; state.player.vx = 0; state.player.vy = 0; - playSound('splash'); state.inv[state.selected]--; rebuildHotbar(); @@ -226,22 +329,29 @@ export function initMouseHandlers() { const overlap = !(bx >= state.player.x + state.player.w || bx + TILE <= state.player.x || by >= state.player.y + state.player.h || by + TILE <= state.player.y); if (overlap) return; - setBlock(gx, gy, state.selected, true); // true = блок установлен игроком + setBlock(gx, gy, state.selected, true); state.inv[state.selected]--; - // Отправляем изменение блока на сервер sendBlockChange(gx, gy, state.selected, 'set'); - // Звук при строительстве if (state.selected === 'stone' || state.selected === 'brick') playSound('stone_build'); else if (state.selected === 'wood' || state.selected === 'planks') playSound('wood_build'); else if (state.selected === 'glass') playSound('glass1'); else if (state.selected === 'sand') playSound('sand1'); - else if (state.selected === 'snow') playSound('snow1'); else if (state.selected === 'dirt' || state.selected === 'grass') playSound('cloth1'); rebuildHotbar(); return; } }); + + // Release mouse — stop hold-to-mine + canvas.addEventListener('pointerup', () => { + miningHeld = false; + }); + + canvas.addEventListener('pointerleave', () => { + miningHeld = false; + // Reset mining when mouse leaves canvas + }); } \ No newline at end of file diff --git a/src/main.js b/src/main.js index a95594a..7e986e4 100644 --- a/src/main.js +++ b/src/main.js @@ -1,13 +1,14 @@ // ==================== ENTRY POINT ==================== import { state } from './core/state.js'; +import { initCanvas } from './core/canvas.js'; +import { SERVER_URL } from './config.js'; import { initSocket } from './multiplayer/socket.js'; -import { sendBlockChange, sendPlayerPosition } from './multiplayer/socket-helpers.js'; import { loadSound, playSound } from './audio/sound-engine.js'; import { BLOCKS } from './data/blocks.js'; import { ITEMS } from './data/items.js'; import { TOOLS } from './data/tools.js'; import { SMELTING_RECIPES } from './data/recipes.js'; -import { initTextures } from './render/textures.js'; +import { initTextures, tex, itemTex } from './render/textures.js'; import { getBlock, setBlock, removeBlock } from './world/world-storage.js'; import { genColumn, surfaceGyAt, ensureGenAroundCamera, regenerateVisibleChunks } from './world/generation.js'; import { initDB, loadGame, applySave, saveGame } from './game/save.js'; @@ -16,32 +17,23 @@ import { initChat } from './ui/chat.js'; import { initFurnace } from './ui/furnace.js'; import { initMinimap } from './ui/minimap.js'; import { initSaveControls, updateSaveButtonVisibility } from './ui/save-controls.js'; +import { initMarket } from './ui/market.js'; import { initRespawn } from './ui/respawn.js'; import { initShare } from './ui/share.js'; import { initControls } from './input/controls.js'; import { initMouseHandlers } from './input/mouse-handler.js'; import { initModes } from './game/modes.js'; import { initVoice } from './multiplayer/voice-chat.js'; -import { resolveY, resolveX } from './physics/collision.js'; -import { calculateDamage } from './entities/player.js'; -import { updateWaterFlag } from './physics/water-detect.js'; -import { updateWaterPhysics } from './physics/water.js'; -import { explodeAt, activateTNT } from './world/tnt.js'; -import { useTool } from './data/tools.js'; import { rebuildHotbar } from './ui/hotbar.js'; +import { toggleCraft, closeCraft, toggleInventory, closeInventory } from './ui/craft.js'; +import { renderCraft } from './ui/craft.js'; // ==================== 1) CONFIG — parse URL/getName ==================== const urlParams = new URLSearchParams(window.location.search); -state.SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru'; +state.SERVER_URL = SERVER_URL; state.TELEGRAM_BOT_USERNAME = 'Grechkacraft_bot'; state.TELEGRAM_APP_SHORT_NAME = 'minegrechka'; -// Защита от mixed content -if (location.protocol === 'https:' && state.SERVER_URL.startsWith('http://')) { - console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP'); - alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.'); -} - // Player name state.playerName = localStorage.getItem('minegrechka_playerName') || null; if (!state.playerName) { @@ -83,64 +75,10 @@ if (!state.worldId) { console.log('Final worldId:', state.worldId, 'Player name:', state.playerName); -// ==================== 2) CANVAS — get elements, resize ==================== -state.gameEl = document.getElementById('game'); -state.canvas = document.getElementById('c'); -state.ctx = state.canvas.getContext('2d'); +// ==================== 2) CANVAS — init from canvas module ==================== +initCanvas(); -// offscreen light map -state.lightC = document.createElement('canvas'); -state.lightCtx = state.lightC.getContext('2d'); - -state.dpr = Math.max(1, window.devicePixelRatio || 1); - -function resize() { - state.W = state.gameEl.clientWidth; - state.H = state.gameEl.clientHeight; - state.canvas.width = state.W * state.dpr; - state.canvas.height = state.H * state.dpr; - state.lightC.width = state.W * state.dpr; - state.lightC.height = state.H * state.dpr; - state.ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0); -} -window.addEventListener('resize', resize); -resize(); - -// ==================== 3) STATE — init player, inv, etc ==================== -state.otherPlayers = new Map(); -state.serverMobs = new Map(); -state.grid = new Map(); -state.blocks = []; -state.generated = new Set(); -state.serverOverrides = new Map(); -state.activeFurnaces = new Map(); -state.activeTNT = new Set(); -state.toolDurability = new Map(); -state.worldSeed = Math.floor(Math.random() * 1000000); - -// Clouds -state.clouds = Array.from({ length: 10 }, () => ({ - x: Math.random() * 2000, - y: -200 - Math.random() * 260, - w: 80 + Math.random() * 120, - s: 12 + Math.random() * 20 -})); - -// Weather -state.weatherTimer = 0; -state.weatherChangeInterval = 60 + Math.random() * 120; - -// Player spawn -state.player.x = 6 * state.TILE; -state.player.y = 0; -state.spawnPoint.x = 6 * state.TILE; -state.spawnPoint.y = 0; - -// Hero image -state.heroImg = new Image(); -state.heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png'; - -// UI elements +// ==================== 3) STATE — set DOM refs ==================== state.hpEl = document.getElementById('hp'); state.foodEl = document.getElementById('food'); state.sxEl = document.getElementById('sx'); @@ -149,51 +87,59 @@ state.todEl = document.getElementById('tod'); state.worldIdEl = document.getElementById('worldId'); state.playerCountEl = document.getElementById('playerCount'); state.deathEl = document.getElementById('death'); +state.hotbarEl = document.getElementById('hotbar'); +state.craftPanel = document.getElementById('craftPanel'); +state.recipesEl = document.getElementById('recipes'); +state.inventoryPanel = document.getElementById('inventoryPanel'); +state.inventoryGrid = document.getElementById('inventoryGrid'); +state.marketPanel = document.getElementById('marketPanel'); +state.marketContent = document.getElementById('marketContent'); +state.tradePanel = document.getElementById('tradePanel'); +state.tradeContent = document.getElementById('tradeContent'); +state.multiplayerStatus = document.getElementById('multiplayerStatus'); // ==================== 4) TEXTURES — initTextures() ==================== initTextures(); +state.tex = tex; +state.itemTex = itemTex; -// ==================== 5) AUDIO — loadSound for each ==================== -loadSound('splash', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3'); -loadSound('sand1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3'); -loadSound('snow1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3'); -loadSound('stone1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3'); -loadSound('wood1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3'); -loadSound('cloth1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3'); -loadSound('fire', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3'); -loadSound('hit1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3'); -loadSound('attack', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3'); -loadSound('hurt_chicken', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3'); -loadSound('stone_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3'); -loadSound('wood_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3'); -loadSound('click', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3'); -loadSound('explode1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3'); -loadSound('glass1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3'); -loadSound('eat1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3'); -loadSound('step', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3'); +// ==================== 5) AUDIO — sounds already loaded by sound-engine.js module init ==================== +// (sound-engine.js loads all sounds at module init time) -// ==================== 6) CONTROLS — initControls ==================== +// ==================== 6) CRAFT/INVENTORY BUTTONS ==================== +const craftBtn = document.getElementById('craftBtn'); +const invToggle = document.getElementById('invToggle'); +const craftClose = document.getElementById('craftClose'); +const inventoryClose = document.getElementById('inventoryClose'); + +if (craftBtn) craftBtn.onclick = () => toggleCraft(); +if (craftClose) craftClose.onclick = () => closeCraft(); +if (invToggle) invToggle.onclick = () => toggleInventory(); +if (inventoryClose) inventoryClose.onclick = () => closeInventory(); + +// ==================== 7) CONTROLS — initControls ==================== initControls(); -// ==================== 7) MOUSE — initMouseHandlers ==================== +// ==================== 8) MOUSE — initMouseHandlers ==================== initMouseHandlers(); -// ==================== 8) UI MODULES — init all UI ==================== +// ==================== 9) UI MODULES — init all UI ==================== initChat(); initFurnace(); initMinimap(); initSaveControls(); +initMarket(); initRespawn(); initShare(); initModes(); -// ==================== 9) SOCKET — initSocket ==================== +// ==================== 10) SOCKET — initSocket ==================== initSocket(); -// ==================== 10) VOICE — initVoice ==================== +// ==================== 11) VOICE — initVoice ==================== initVoice(); -// ==================== 11) SAVE — loadGame, applySave ==================== +// ==================== 12) SAVE — loadGame, applySave ==================== rebuildHotbar(); initDB().then(async () => { @@ -275,4 +221,4 @@ initDB().then(async () => { }); // ==================== 13) LOOP — startLoop ==================== -requestAnimationFrame(loop); \ No newline at end of file +requestAnimationFrame(loop); diff --git a/src/main.js.bak.broken b/src/main.js.bak.broken new file mode 100644 index 0000000..ea8d1ac --- /dev/null +++ b/src/main.js.bak.broken @@ -0,0 +1,299 @@ +// ==================== ENTRY POINT ==================== +import { state } from './core/state.js'; +import { initSocket } from './multiplayer/socket.js'; +import { sendBlockChange, sendPlayerPosition } from './multiplayer/socket-helpers.js'; +import { loadSound, playSound } from './audio/sound-engine.js'; +import { BLOCKS } from './data/blocks.js'; +import { ITEMS } from './data/items.js'; +import { TOOLS } from './data/tools.js'; +import { SMELTING_RECIPES } from './data/recipes.js'; +import { initTextures } from './render/textures.js'; +import { getBlock, setBlock, removeBlock } from './world/world-storage.js'; +import { genColumn, surfaceGyAt, ensureGenAroundCamera, regenerateVisibleChunks } from './world/generation.js'; +import { initDB, loadGame, applySave, saveGame } from './game/save.js'; +import { loop } from './game/loop.js'; +import { initChat } from './ui/chat.js'; +import { initFurnace } from './ui/furnace.js'; +import { initMinimap } from './ui/minimap.js'; +import { initSaveControls, updateSaveButtonVisibility } from './ui/save-controls.js'; +import { initRespawn } from './ui/respawn.js'; +import { initShare } from './ui/share.js'; +import { initControls } from './input/controls.js'; +import { initMouseHandlers } from './input/mouse-handler.js'; +import { initModes } from './game/modes.js'; +import { initVoice } from './multiplayer/voice-chat.js'; +import { resolveY, resolveX } from './physics/collision.js'; +import { calculateDamage } from './entities/player.js'; +import { updateWaterFlag } from './physics/water-detect.js'; +import { updateWaterPhysics } from './physics/water.js'; +import { explodeAt, activateTNT } from './world/tnt.js'; +import { useTool } from './data/tools.js'; +import { rebuildHotbar } from './ui/hotbar.js'; +import { closeTrade } from './ui/trade.js'; +import { toggleMarket, closeMarket } from './ui/market.js'; + +// ==================== 1) CONFIG — parse URL/getName ==================== +const urlParams = new URLSearchParams(window.location.search); +state.SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru'; +state.TELEGRAM_BOT_USERNAME = 'Grechkacraft_bot'; +state.TELEGRAM_APP_SHORT_NAME = 'minegrechka'; + +// Защита от mixed content +if (location.protocol === 'https:' && state.SERVER_URL.startsWith('http://')) { + console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP'); + alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.'); +} + +// Player name +state.playerName = localStorage.getItem('minegrechka_playerName') || null; +if (!state.playerName) { + state.playerName = prompt('Введите ваше имя для игры:') || 'Игрок'; + localStorage.setItem('minegrechka_playerName', state.playerName); + console.log('Player name set:', state.playerName); +} + +// World ID from URL or generate new +console.log('Current URL:', window.location.href); +const worldParam = urlParams.get('world'); +console.log('world param:', worldParam); + +state.worldId = (worldParam && worldParam.trim() !== '') ? worldParam : null; +console.log('worldId after params:', state.worldId, 'type:', typeof state.worldId); + +if (!state.worldId) { + state.worldId = Math.random().toString(36).substring(2, 10); + console.log('Generated worldId:', state.worldId); + + try { + const newUrl = new URL(window.location.href); + newUrl.searchParams.set('world', state.worldId); + const newUrlString = newUrl.toString(); + console.log('New URL to set:', newUrlString); + + if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') { + window.history.replaceState(null, '', newUrlString); + console.log('URL after replaceState:', window.location.href); + } else { + console.error('History API not supported!'); + } + } catch (e) { + console.error('Error updating URL:', e); + } + + console.log('Generated new worldId for browser:', state.worldId); +} + +console.log('Final worldId:', state.worldId, 'Player name:', state.playerName); + +// ==================== 2) CANVAS — get elements, resize ==================== +state.gameEl = document.getElementById('game'); +state.canvas = document.getElementById('c'); +state.ctx = state.canvas.getContext('2d'); + +// offscreen light map +state.lightC = document.createElement('canvas'); +state.lightCtx = state.lightC.getContext('2d'); + +state.dpr = Math.max(1, window.devicePixelRatio || 1); + +function resize() { + state.W = state.gameEl.clientWidth; + state.H = state.gameEl.clientHeight; + state.canvas.width = state.W * state.dpr; + state.canvas.height = state.H * state.dpr; + state.lightC.width = state.W * state.dpr; + state.lightC.height = state.H * state.dpr; + state.ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0); +} +window.addEventListener('resize', resize); +resize(); + +// ==================== 3) STATE — init player, inv, etc ==================== +state.otherPlayers = new Map(); +state.serverMobs = new Map(); +state.grid = new Map(); +state.blocks = []; +state.generated = new Set(); +state.serverOverrides = new Map(); +state.activeFurnaces = new Map(); +state.activeTNT = new Set(); +state.toolDurability = new Map(); +state.worldSeed = Math.floor(Math.random() * 1000000); + +// Clouds +state.clouds = Array.from({ length: 10 }, () => ({ + x: Math.random() * 2000, + y: -200 - Math.random() * 260, + w: 80 + Math.random() * 120, + s: 12 + Math.random() * 20 +})); + +// Weather +state.weatherTimer = 0; +state.weatherChangeInterval = 60 + Math.random() * 120; + +// Player spawn +state.player.x = 6 * state.TILE; +state.player.y = 0; +state.spawnPoint.x = 6 * state.TILE; +state.spawnPoint.y = 0; + +// Hero image +state.heroImg = new Image(); +state.heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png'; + +// UI elements +state.hpEl = document.getElementById('hp'); +state.foodEl = document.getElementById('food'); +state.sxEl = document.getElementById('sx'); +state.syEl = document.getElementById('sy'); +state.todEl = document.getElementById('tod'); +state.worldIdEl = document.getElementById('worldId'); +state.playerCountEl = document.getElementById('playerCount'); +state.deathEl = document.getElementById('death'); +state.multiplayerStatus = document.getElementById('multiplayerStatus'); + +// Market and trade panel DOM refs +state.marketPanel = document.getElementById('marketPanel'); +state.marketContent = document.getElementById('marketContent'); +state.tradePanel = document.getElementById('tradePanel'); +state.tradeContent = document.getElementById('tradeContent'); + +// ==================== 4) TEXTURES — initTextures() ==================== +initTextures(); + +// ==================== 5) AUDIO — loadSound for each ==================== +loadSound('splash', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3'); +loadSound('sand1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3'); +loadSound('snow1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3'); +loadSound('stone1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3'); +loadSound('wood1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3'); +loadSound('cloth1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3'); +loadSound('fire', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3'); +loadSound('hit1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3'); +loadSound('attack', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3'); +loadSound('hurt_chicken', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3'); +loadSound('stone_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3'); +loadSound('wood_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3'); +loadSound('click', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3'); +loadSound('explode1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3'); +loadSound('glass1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3'); +loadSound('eat1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3'); +loadSound('step', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3'); + +// ==================== 6) CONTROLS — initControls ==================== +initControls(); + +// ==================== 7) MOUSE — initMouseHandlers ==================== +initMouseHandlers(); + +// ==================== 8) UI MODULES — init all UI ==================== +initChat(); +initFurnace(); +initMinimap(); +initSaveControls(); +initRespawn(); +initShare(); +initModes(); + +// ==================== MARKET & TRADE UI ==================== +// Market close +const marketClose = document.getElementById('marketClose'); +if (marketClose) { + marketClose.addEventListener('click', () => { + closeMarket(); + }); +} +// Trade close +const tradeClose = document.getElementById('tradeClose'); +if (tradeClose) { + tradeClose.addEventListener('click', () => { + closeTrade(); + }); +} + +// ==================== 9) SOCKET — initSocket ==================== +initSocket(); + +// ==================== 10) VOICE — initVoice ==================== +initVoice(); + +// ==================== 11) SAVE — loadGame, applySave ==================== +rebuildHotbar(); + +initDB().then(async () => { + const loadedSave = await loadGame(); + if (loadedSave) { + await applySave(loadedSave); + console.log('Загружено сохранение, HP:', state.player.hp); + + if (state.player.hp <= 0) { + console.log('WARNING: HP <= 0 после загрузки, возрождаемся'); + state.player.hp = 100; + state.player.hunger = 100; + state.player.o2 = 100; + state.player.x = state.spawnPoint.x; + state.player.y = state.spawnPoint.y; + state.player.vx = state.player.vy = 0; + state.player.invuln = 0; + state.player.fallStartY = state.player.y; + } + } else { + console.log('Сохранение не найдено, начинаем новую игру'); + + state.player.hp = 100; + state.player.hunger = 100; + state.player.o2 = 100; + state.player.vx = state.player.vy = 0; + state.player.invuln = 0; + + // старт — на поверхности + const startGX = 6; + for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx); + const surfaceY = surfaceGyAt(startGX); + let safeGY = surfaceY - 1; + const aboveBlock = getBlock(startGX, surfaceY - 1); + if (aboveBlock && aboveBlock.t === 'water') { + for (let gy = state.SEA_GY - 1; gy >= 0; gy--) { + const b = getBlock(startGX, gy); + if (!b || b.dead || b.t === 'air' || b.t === 'water') continue; + safeGY = gy - 1; + break; + } + } + state.player.y = safeGY * state.TILE; + state.player.x = startGX * state.TILE; + state.player.fallStartY = state.player.y; + + state.spawnPoint.x = state.player.x; + state.spawnPoint.y = state.player.y; + + console.log('Новая игра: startGX=', startGX, 'surfaceY=', surfaceY, 'player.y=', state.player.y); + + for (let gx = startGX - 50; gx <= startGX + 50; gx++) { + genColumn(gx); + } + } + + // Автосейв + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + saveGame(); + } + + window.addEventListener('beforeunload', () => { + saveGame(); +}).catch(err => { + console.error('Ошибка инициализации:', err); + const startGX = 6; + genColumn(startGX); + state.player.y = (surfaceGyAt(startGX) - 1) * state.TILE; + state.player.fallStartY = state.player.y; + + for (let gx = startGX - 50; gx <= startGX + 50; gx++) { + genColumn(gx); + } +}); + +// ==================== 13) LOOP — startLoop ==================== +requestAnimationFrame(loop); \ No newline at end of file diff --git a/src/multiplayer/socket-helpers.js b/src/multiplayer/socket-helpers.js index 5823d0a..5f5e8d2 100644 --- a/src/multiplayer/socket-helpers.js +++ b/src/multiplayer/socket-helpers.js @@ -1,6 +1,6 @@ // Socket helpers: цвет, отправка позиции/блоков import { state } from '../core/state.js'; -import { SERVER_URL } from '../config.js'; + // Генерация случайного цвета для игрока на основе socket_id export function getRandomPlayerColor(socketId) { diff --git a/src/multiplayer/socket.js b/src/multiplayer/socket.js index 94bebb8..a2d9c05 100644 --- a/src/multiplayer/socket.js +++ b/src/multiplayer/socket.js @@ -1,9 +1,9 @@ // Socket.IO клиент — инициализация и все обработчики import { state } from '../core/state.js'; -import { SERVER_URL, worldId, playerName } from '../config.js'; +import { SERVER_URL } from '../config.js'; import { TILE, SEA_GY } from '../core/constants.js'; import { BLOCKS } from '../data/blocks.js'; -import { k, setBlock, removeBlock, grid, blocks } from '../world/world-storage.js'; +import { k, setBlock, removeBlock, grid, blocks, getBlock } from '../world/world-storage.js'; import { genColumn, surfaceGyAt } from '../world/generation.js'; import { addChatMessage } from '../ui/chat.js'; import { calculateDamage } from '../entities/player.js'; @@ -12,6 +12,7 @@ import { rebuildHotbar } from '../ui/hotbar.js'; import { explodeAt } from '../world/tnt.js'; import { updateSaveButtonVisibility } from '../ui/save-controls.js'; import { getRandomPlayerColor, sendPlayerPosition, sendBlockChange } from './socket-helpers.js'; +import { initTradeSocket } from '../ui/trade.js'; let socket = null; @@ -24,17 +25,23 @@ export function initSocket() { transports: ['websocket', 'polling'] }); + // Store socket on state for other modules + state.socket = socket; + socket.on('connect', () => { - console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${worldId}`); + console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${state.worldId}`); state.mySocketId = socket.id; state.isMultiplayer = true; // Присоединяемся к миру - socket.emit('join_world', { world_id: worldId, player_name: playerName }); + socket.emit('join_world', { world_id: state.worldId, player_name: state.playerName }); // Показываем в UI - state.worldIdEl.textContent = worldId; - state.multiplayerStatus.style.display = 'block'; + if (state.worldIdEl) state.worldIdEl.textContent = state.worldId; + if (state.multiplayerStatus) state.multiplayerStatus.style.display = 'block'; + + // Initialize trade socket handlers + initTradeSocket(socket); }); socket.on('connect_error', (error) => { @@ -46,20 +53,18 @@ export function initSocket() { console.log('Disconnected from server'); state.isMultiplayer = false; state.otherPlayers.clear(); - state.multiplayerStatus.style.display = 'none'; + if (state.multiplayerStatus) state.multiplayerStatus.style.display = 'none'; }); // Обработка world_state socket.on('world_state', (data) => { console.log('Received world_state:', data); - // Устанавливаем seed и перегенерируем мир если он изменился if (data.seed !== undefined && data.seed !== state.worldSeed) { const oldSeed = state.worldSeed; state.worldSeed = data.seed; console.log('World seed changed from', oldSeed, 'to', state.worldSeed); - // Очищаем и перегенерируем мир с новым seed state.generated.clear(); grid.clear(); blocks.length = 0; @@ -68,12 +73,10 @@ export function initSocket() { console.log('World regenerated with new seed:', state.worldSeed); } - // Применяем блоки — сохраняем в state.serverOverrides для применения после genColumn if (data.blocks && Array.isArray(data.blocks)) { for (const block of data.blocks) { const key = k(block.gx, block.gy); state.serverOverrides.set(key, { op: block.op, t: block.t }); - // Также пробуем применить сразу (если колонна уже сгенерирована) if (block.op === 'set') { setBlock(block.gx, block.gy, block.t, false); } else if (block.op === 'remove') { @@ -82,25 +85,19 @@ export function initSocket() { } } - // Устанавливаем время if (data.time !== undefined) { state.worldTime = data.time; state.isNightTime = state.worldTime > 0.5; } - // Всегда считаем spawnPoint на клиенте по той же формуле что и surfaceGyAt - // Это гарантирует совпадение с terrain generation + // Spawn point { const startGX = 6; - // Генерируем колонну и соседние для безопасного спавна for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx); const surfaceY = surfaceGyAt(startGX); - // Ищем ближайшую небудущую позицию сверху вниз от поверхности let safeGY = surfaceY - 1; - // Проверяем что над поверхностью воздух (не в воде) const aboveBlock = getBlock(startGX, surfaceY - 1); if (aboveBlock && aboveBlock.t === 'water') { - // Если в воде — ищем поверхность выше уровня моря for (let gy = SEA_GY - 1; gy >= 0; gy--) { const b = getBlock(startGX, gy); if (!b || b.dead || b.t === 'air' || b.t === 'water') continue; @@ -113,7 +110,6 @@ export function initSocket() { console.log('Client-side spawn point:', state.spawnPoint, 'surfaceY:', surfaceY, 'safeGY:', safeGY); } - // Устанавливаем игрока в точку спавна state.player.x = state.spawnPoint.x; state.player.y = state.spawnPoint.y; state.player.vx = 0; @@ -121,14 +117,12 @@ export function initSocket() { state.player.fallStartY = state.player.y; console.log('Player moved to spawn point:', state.player.x, state.player.y); - // Устанавливаем HP на 100% при каждом подключении к миру state.player.hp = 100; state.player.hunger = 100; state.player.o2 = 100; state.player.invuln = 0; console.log('[MULTIPLAYER CONNECT] Player HP set to 100% on connect'); - // Обновляем список игроков if (data.players && Array.isArray(data.players)) { state.otherPlayers.clear(); for (const p of data.players) { @@ -141,7 +135,6 @@ export function initSocket() { }); } } - // Обновляем счётчик игроков state.playerCountEl.textContent = data.players.length; } // Server mobs @@ -152,13 +145,14 @@ export function initSocket() { state.serverMobs.set(m.id, sm); } } + + // Set myTgId from server if provided + if (data.tg_id) state.myTgId = data.tg_id; }); - // Игрок присоединился socket.on('player_joined', (data) => { console.log('Player joined:', data.socket_id); if (data.socket_id !== state.mySocketId) { - // Генерируем безопасную позицию для нового игрока const spawnGX = 6; genColumn(spawnGX); const surfaceY = surfaceGyAt(spawnGX); @@ -172,35 +166,29 @@ export function initSocket() { name: data.player_name || 'Игрок' }); addChatMessage('Система', `Игрок присоединился`); - // Обновляем видимость кнопки сохранения updateSaveButtonVisibility(); } }); - // Игрок переместился socket.on('player_moved', (data) => { if (data.socket_id !== state.mySocketId && state.otherPlayers.has(data.socket_id)) { const p = state.otherPlayers.get(data.socket_id); p.x = data.x; p.y = data.y; - // Обновляем имя, если оно пришло if (data.player_name) { p.name = data.player_name; } } }); - // Игрок покинул socket.on('player_left', (data) => { console.log('Player left:', data.socket_id); state.otherPlayers.delete(data.socket_id); addChatMessage('Система', `Игрок покинул игру`); - // Обновляем видимость кнопки сохранения updateSaveButtonVisibility(); }); - // === MOB SYNC (multiplayer) === - + // === MOB SYNC === socket.on('mob_spawned', (data) => { const sm = { ...data, maxHp: data.maxHp || data.hp, vx: data.vx || 0, vy: data.vy || 0, grounded: false, inWater: false, aiT: 0, dir: data.dir || 1, dead: false, fuse: data.fuse || 0, shootCooldown: 2, speed: data.speed || 80 }; state.serverMobs.set(data.id, sm); @@ -218,7 +206,6 @@ export function initSocket() { socket.on('mob_died', (data) => { const sm = state.serverMobs.get(data.id); if (sm && data.killer === state.mySocketId) { - // Give loot to the killer if (sm.kind === 'chicken') playSound('hurt_chicken'); state.inv.meat += (sm.kind === 'chicken' ? 1 : 2); if (sm.kind === 'skeleton') { @@ -247,7 +234,6 @@ export function initSocket() { }); }); - // Блок изменён socket.on('block_changed', (data) => { const key = k(data.gx, data.gy); state.serverOverrides.set(key, { op: data.op, t: data.t }); @@ -258,13 +244,11 @@ export function initSocket() { } }); - // Сообщение в чат socket.on('chat_message', (data) => { const senderName = data.socket_id === state.mySocketId ? 'Вы' : `Игрок ${data.socket_id.substring(0, 6)}`; addChatMessage(senderName, data.message); }); - // Обновление времени socket.on('time_update', (data) => { if (data.time !== undefined) { state.worldTime = data.time; @@ -276,4 +260,4 @@ export function initSocket() { console.error('Error initializing socket:', e); state.isMultiplayer = false; } -} \ No newline at end of file +} diff --git a/src/multiplayer/voice-chat.js b/src/multiplayer/voice-chat.js index affc6b4..298e71c 100644 --- a/src/multiplayer/voice-chat.js +++ b/src/multiplayer/voice-chat.js @@ -1,6 +1,6 @@ // Голосовой чат import { state } from '../core/state.js'; -import { worldId } from '../config.js'; + let voiceSocket = null; let voiceStream = null; @@ -64,7 +64,7 @@ voiceBtn.onclick = async () => { // Подключаемся к голосовому серверу voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] }); voiceSocket.on('connect', () => { - voiceSocket.emit('voice_join', { world_id: worldId, x: state.player.x, y: state.player.y, name: state.playerName || 'Игрок' }); + voiceSocket.emit('voice_join', { world_id: state.worldId, x: state.player.x, y: state.player.y, name: state.playerName || 'Игрок' }); }); voiceSocket.on('voice_in', (payload) => { diff --git a/src/render/textures.js b/src/render/textures.js index 1d0e575..016721f 100644 --- a/src/render/textures.js +++ b/src/render/textures.js @@ -3,6 +3,210 @@ import { BLOCKS } from '../data/blocks.js'; export const tex = {}; +// --- Item/tool sprite generator --- +export const itemTex = {}; + +function drawArrow(ctx, color) { + // Стрела — древко + наконечник + оперение + ctx.fillStyle = '#8B4513'; + ctx.fillRect(8, 14, 16, 3); // древко + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(24, 15); // кончик + ctx.lineTo(18, 10); + ctx.lineTo(18, 20); + ctx.closePath(); + ctx.fill(); + // оперение + ctx.fillStyle = '#e74c3c'; + ctx.beginPath(); + ctx.moveTo(8, 15); + ctx.lineTo(4, 10); + ctx.lineTo(10, 14); + ctx.closePath(); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(8, 16); + ctx.lineTo(4, 21); + ctx.lineTo(10, 17); + ctx.closePath(); + ctx.fill(); +} + +function drawPickaxe(ctx, headColor, handleColor) { + // Кирка — рукоять + голова с двумя зубцами + ctx.fillStyle = handleColor || '#8B4513'; + ctx.save(); + ctx.translate(16, 16); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-2, 0, 4, 18); + ctx.fillStyle = headColor; + ctx.fillRect(-8, -3, 16, 5); + ctx.fillRect(-8, -3, 3, 8); + ctx.fillRect(5, -3, 3, 8); + ctx.restore(); +} + +function drawAxe(ctx, headColor, handleColor) { + // Топор — рукоять + лезвие + ctx.fillStyle = handleColor || '#8B4513'; + ctx.save(); + ctx.translate(16, 16); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-2, 0, 4, 18); + ctx.fillStyle = headColor; + ctx.fillRect(-2, -8, 10, 8); + ctx.fillRect(-2, -8, 4, 12); + ctx.restore(); +} + +function drawShovel(ctx, headColor, handleColor) { + // Лопата — рукоять + лопасть + ctx.fillStyle = handleColor || '#8B4513'; + ctx.save(); + ctx.translate(16, 16); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-2, 0, 4, 18); + ctx.fillStyle = headColor; + ctx.fillRect(-6, -8, 12, 8); + ctx.fillRect(-4, -12, 8, 4); + ctx.restore(); +} + +function drawSword(ctx, bladeColor, guardColor) { + // Меч — клинок + гарда + рукоять + ctx.fillStyle = '#8B4513'; + ctx.fillRect(14, 18, 4, 10); + ctx.fillStyle = guardColor || '#f1c40f'; + ctx.fillRect(10, 16, 12, 3); + ctx.fillStyle = bladeColor; + ctx.beginPath(); + ctx.moveTo(16, 2); + ctx.lineTo(12, 16); + ctx.lineTo(20, 16); + ctx.closePath(); + ctx.fill(); +} + +function drawBow(ctx) { + // Лук — дуга + тетива + ctx.strokeStyle = '#8B4513'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(12, 16, 12, -Math.PI * 0.7, Math.PI * 0.7); + ctx.stroke(); + ctx.strokeStyle = '#ddd'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(12, 4); + ctx.lineTo(12, 28); + ctx.stroke(); +} + +function drawIngot(ctx, color) { + // Слиток + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(6, 12); + ctx.lineTo(26, 12); + ctx.lineTo(24, 24); + ctx.lineTo(8, 24); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(8, 8, 16, 4); +} + +function drawMeat(ctx, cooked) { + // Мясо — кость + кусок + ctx.fillStyle = cooked ? '#8B4513' : '#e74c3c'; + ctx.beginPath(); + ctx.ellipse(16, 18, 10, 8, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#f5f5dc'; + ctx.fillRect(8, 6, 3, 12); + ctx.fillRect(14, 4, 3, 8); +} + +function makeItemTex(id) { + const c = document.createElement('canvas'); + c.width = 32; c.height = 32; + const g = c.getContext('2d'); + + const tierColors = { + wood: { head: '#a0522d', blade: '#a0522d' }, + stone: { head: '#7f8c8d', blade: '#95a5a6' }, + iron: { head: '#bdc3c7', blade: '#ecf0f1' }, + }; + + switch (id) { + case 'arrow': + drawArrow(g, '#7f8c8d'); + break; + case 'bow': + drawBow(g); + break; + case 'wood_pickaxe': + drawPickaxe(g, tierColors.wood.head, tierColors.wood.blade); + break; + case 'stone_pickaxe': + drawPickaxe(g, tierColors.stone.head); + break; + case 'iron_pickaxe': + drawPickaxe(g, tierColors.iron.head); + break; + case 'wood_axe': + drawAxe(g, tierColors.wood.head); + break; + case 'stone_axe': + drawAxe(g, tierColors.stone.head); + break; + case 'iron_axe': + drawAxe(g, tierColors.iron.head); + break; + case 'wood_shovel': + drawShovel(g, tierColors.wood.head); + break; + case 'stone_shovel': + drawShovel(g, tierColors.stone.head); + break; + case 'iron_shovel': + drawShovel(g, tierColors.iron.head); + break; + case 'wood_sword': + drawSword(g, tierColors.wood.blade, '#a0522d'); + break; + case 'stone_sword': + drawSword(g, tierColors.stone.blade, '#7f8c8d'); + break; + case 'iron_sword': + drawSword(g, tierColors.iron.blade, '#bdc3c7'); + break; + case 'iron_ingot': + drawIngot(g, '#bdc3c7'); + break; + case 'gold_ingot': + drawIngot(g, '#f1c40f'); + break; + case 'copper_ingot': + drawIngot(g, '#e67e22'); + break; + case 'meat': + drawMeat(g, false); + break; + case 'cooked': + drawMeat(g, true); + break; + default: + // Generic box + g.fillStyle = '#7f8c8d'; + g.fillRect(4, 4, 24, 24); + g.fillStyle = 'rgba(255,255,255,0.2)'; + g.fillRect(4, 4, 24, 4); + } + return c; +} + export function makeTex(type) { const t = BLOCKS[type]; const c = document.createElement('canvas'); @@ -38,16 +242,12 @@ export function makeTex(type) { return c; } if (type === 'bed') { - // Основание кровати g.fillStyle = '#e91e63'; g.fillRect(0, 0, 32, 32); - // Подушка g.fillStyle = '#f8bbd0'; g.fillRect(2, 2, 14, 14); - // Одеяло g.fillStyle = '#c2185b'; g.fillRect(16, 4, 14, 24); - // Детали одеяла g.fillStyle = '#e91e63'; g.fillRect(18, 6, 10, 20); return c; @@ -58,26 +258,20 @@ export function makeTex(type) { return c; } if (type === 'boat') { - // Корпус лодки g.fillStyle = '#8B4513'; g.fillRect(2, 12, 28, 8); - // Борта g.fillStyle = '#A0522D'; g.fillRect(0, 10, 32, 12); - // Внутренность g.fillStyle = '#DEB887'; g.fillRect(4, 14, 24, 4); - // Дно g.fillStyle = '#654321'; g.fillRect(2, 20, 28, 4); return c; } if (type === 'ladder') { - // Боковые стойки лестницы g.fillStyle = '#8B4513'; g.fillRect(4, 0, 4, 32); g.fillRect(24, 0, 4, 32); - // Ступени g.fillStyle = '#A0522D'; g.fillRect(4, 4, 24, 3); g.fillRect(4, 12, 24, 3); @@ -101,4 +295,15 @@ export function makeTex(type) { export function initTextures() { Object.keys(BLOCKS).forEach(k => tex[k] = makeTex(k)); -} \ No newline at end of file + // Item/tool sprites + const itemIds = [ + 'arrow','bow', + 'wood_pickaxe','stone_pickaxe','iron_pickaxe', + 'wood_axe','stone_axe','iron_axe', + 'wood_shovel','stone_shovel','iron_shovel', + 'wood_sword','stone_sword','iron_sword', + 'iron_ingot','gold_ingot','copper_ingot', + 'meat','cooked', + ]; + itemIds.forEach(id => { itemTex[id] = makeItemTex(id); }); +} diff --git a/src/ui/craft.js b/src/ui/craft.js index deaa3db..693582d 100644 --- a/src/ui/craft.js +++ b/src/ui/craft.js @@ -5,19 +5,15 @@ import { ITEMS } from '../data/items.js'; import { TOOLS } from '../data/tools.js'; import { RECIPES } from '../data/recipes.js'; import { addTool } from '../data/tools.js'; -import { tex } from '../render/textures.js'; +import { tex, itemTex } from '../render/textures.js'; import { playSound } from '../audio/sound-engine.js'; import { rebuildHotbar } from './hotbar.js'; import { renderInventory } from './inventory.js'; export function canCraft(r) { const inv = state.inv; - console.log('[CRAFT] Checking recipe for:', r.out, '- cost:', r.cost); for (const res in r.cost) { - const have = inv[res] || 0; - const need = r.cost[res]; - console.log('[CRAFT] Resource:', res, '- have:', have, '- need:', need, '- can craft:', have >= need); - if (have < need) return false; + if ((inv[res] || 0) < r.cost[res]) return false; } return true; } @@ -31,17 +27,19 @@ export function renderCraft() { row.className = 'recipe'; const icon = document.createElement('div'); icon.className = 'ricon'; - // Иконка — блок, инструмент или предмет + // Иконка результата — canvas sprite > emoji if (tex[r.out]) { icon.style.backgroundImage = `url(${tex[r.out].toDataURL()})`; + } else if (itemTex[r.out]) { + icon.style.backgroundImage = `url(${itemTex[r.out].toDataURL()})`; } else if (TOOLS[r.out]) { - icon.textContent = TOOLS[r.out].icon; + icon.textContent = TOOLS[r.out].icon || '🔧'; icon.style.fontSize = '24px'; icon.style.display = 'flex'; icon.style.alignItems = 'center'; icon.style.justifyContent = 'center'; } else if (ITEMS[r.out]) { - icon.textContent = ITEMS[r.out].icon; + icon.textContent = ITEMS[r.out].icon || '📦'; icon.style.fontSize = '24px'; icon.style.display = 'flex'; icon.style.alignItems = 'center'; @@ -82,35 +80,33 @@ let craftOpen = false; let inventoryOpen = false; export function toggleCraft() { - playSound('click'); // Звук клика по кнопке + playSound('click'); craftOpen = !craftOpen; state.craftPanel.style.display = craftOpen ? 'block' : 'none'; if (craftOpen) { renderCraft(); - // Закрываем инвентарь если открыт крафт inventoryOpen = false; state.inventoryPanel.style.display = 'none'; } } export function closeCraft() { - playSound('click'); // Звук клика по кнопке + playSound('click'); craftOpen = false; state.craftPanel.style.display = 'none'; } export function toggleInventory() { - playSound('click'); // Звук клика по кнопке + playSound('click'); inventoryOpen = true; state.inventoryPanel.style.display = 'block'; renderInventory(); - // Закрываем крафт если открыт инвентарь craftOpen = false; state.craftPanel.style.display = 'none'; } export function closeInventory() { - playSound('click'); // Звук клика по кнопке + playSound('click'); inventoryOpen = false; state.inventoryPanel.style.display = 'none'; -} \ No newline at end of file +} diff --git a/src/ui/dom-refs.js b/src/ui/dom-refs.js index f280ccb..a704ae1 100644 --- a/src/ui/dom-refs.js +++ b/src/ui/dom-refs.js @@ -11,4 +11,8 @@ export const craftPanel = document.getElementById('craftPanel'); export const recipesEl = document.getElementById('recipes'); export const deathEl = document.getElementById('death'); export const inventoryPanel = document.getElementById('inventoryPanel'); -export const inventoryGrid = document.getElementById('inventoryGrid'); \ No newline at end of file +export const inventoryGrid = document.getElementById('inventoryGrid'); +export const marketPanel = document.getElementById('marketPanel'); +export const marketContent = document.getElementById('marketContent'); +export const tradePanel = document.getElementById('tradePanel'); +export const tradeContent = document.getElementById('tradeContent'); \ No newline at end of file diff --git a/src/ui/hotbar.js b/src/ui/hotbar.js index deccf0c..15d848f 100644 --- a/src/ui/hotbar.js +++ b/src/ui/hotbar.js @@ -3,7 +3,7 @@ import { state } from '../core/state.js'; import { BLOCKS } from '../data/blocks.js'; import { ITEMS } from '../data/items.js'; import { TOOLS } from '../data/tools.js'; -import { tex } from '../render/textures.js'; +import { tex, itemTex } from '../render/textures.js'; import { playSound } from '../audio/sound-engine.js'; export function rebuildHotbar() { @@ -15,38 +15,41 @@ export function rebuildHotbar() { hotbarEl.innerHTML = ''; - // Показываем последние 5 выбранных предметов (если они есть в инвентаре) const items = recentItems.filter(id => inv[id] > 0).slice(0, 5); for (const id of items) { const s = document.createElement('div'); s.className = 'slot' + (id === selected ? ' sel' : ''); - if (BLOCKS[id]) { + + // Try canvas sprite: block texture > itemTex > fallback emoji + if (BLOCKS[id] && tex[id]) { s.style.backgroundImage = `url(${tex[id].toDataURL()})`; s.style.backgroundSize = 'cover'; + } else if (itemTex[id]) { + s.style.backgroundImage = `url(${itemTex[id].toDataURL()})`; + s.style.backgroundSize = 'cover'; } else if (ITEMS[id]) { - s.textContent = ITEMS[id].icon; + s.textContent = ITEMS[id].icon || '📦'; } else if (TOOLS[id]) { - s.textContent = TOOLS[id].icon; + s.textContent = TOOLS[id].icon || '🔧'; } else if (id === 'iron_armor') { s.textContent = '🛡️'; s.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)'; } + const c = document.createElement('div'); c.className = 'count'; c.textContent = inv[id]; s.appendChild(c); s.onclick = () => { - playSound('click'); // Звук клика по инвентарю + playSound('click'); state.selected = id; - // Обновляем список последних предметов - state.recentItems = state.recentItems.filter(item => item !== id); // Удаляем если уже есть - state.recentItems.unshift(id); // Добавляем в начало - state.recentItems = state.recentItems.slice(0, 5); // Оставляем только 5 + state.recentItems = state.recentItems.filter(item => item !== id); + state.recentItems.unshift(id); + state.recentItems = state.recentItems.slice(0, 5); rebuildHotbar(); }; - // Показываем индикатор надетой брони if (id === 'iron_armor' && state.player.equippedArmor === 'iron_armor') { const equipped = document.createElement('div'); equipped.className = 'equipped-indicator'; @@ -54,9 +57,8 @@ export function rebuildHotbar() { s.appendChild(equipped); } - // Durability bar для инструментов + // Durability bar if (TOOLS[id] && inv[id] > 0) { - // Находим текущую прочность let curDur = 0, maxDur = TOOLS[id].durability; for (const [tid, dur] of toolDurability) { if (dur.type === id) { @@ -78,4 +80,4 @@ export function rebuildHotbar() { } hotbarEl.appendChild(s); } -} \ No newline at end of file +} diff --git a/src/ui/inventory.js b/src/ui/inventory.js index c7cc20c..6c0d7dc 100644 --- a/src/ui/inventory.js +++ b/src/ui/inventory.js @@ -3,7 +3,7 @@ import { state } from '../core/state.js'; import { BLOCKS } from '../data/blocks.js'; import { ITEMS } from '../data/items.js'; import { TOOLS } from '../data/tools.js'; -import { tex } from '../render/textures.js'; +import { tex, itemTex } from '../render/textures.js'; import { playSound } from '../audio/sound-engine.js'; import { rebuildHotbar } from './hotbar.js'; @@ -13,23 +13,25 @@ export function renderInventory() { const selected = state.selected; inventoryGrid.innerHTML = ''; - // Создаём сетку инвентаря 7x3 const items = Object.keys(inv).filter(id => inv[id] > 0); - // Добавляем пустые слоты для полной сетки for (let i = 0; i < 21; i++) { const slot = document.createElement('div'); slot.className = 'inv-slot' + (i < items.length && items[i] === selected ? ' sel' : ''); if (i < items.length) { const id = items[i]; - if (BLOCKS[id]) { + // Canvas sprite: block > itemTex > emoji fallback + if (BLOCKS[id] && tex[id]) { slot.style.backgroundImage = `url(${tex[id].toDataURL()})`; slot.style.backgroundSize = 'cover'; + } else if (itemTex[id]) { + slot.style.backgroundImage = `url(${itemTex[id].toDataURL()})`; + slot.style.backgroundSize = 'cover'; } else if (ITEMS[id]) { - slot.textContent = ITEMS[id].icon; + slot.textContent = ITEMS[id].icon || '📦'; } else if (TOOLS[id]) { - slot.textContent = TOOLS[id].icon; + slot.textContent = TOOLS[id].icon || '🔧'; } else if (id === 'iron_armor') { slot.textContent = '🛡️'; slot.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)'; @@ -41,12 +43,11 @@ export function renderInventory() { slot.appendChild(count); slot.onclick = () => { - playSound('click'); // Звук клика по инвентарю + playSound('click'); state.selected = id; - // Обновляем список последних предметов - state.recentItems = state.recentItems.filter(item => item !== id); // Удаляем если уже есть - state.recentItems.unshift(id); // Добавляем в начало - state.recentItems = state.recentItems.slice(0, 5); // Оставляем только 5 + state.recentItems = state.recentItems.filter(item => item !== id); + state.recentItems.unshift(id); + state.recentItems = state.recentItems.slice(0, 5); rebuildHotbar(); renderInventory(); }; @@ -54,16 +55,12 @@ export function renderInventory() { // Двойной клик для надевания брони slot.ondblclick = () => { if (id === 'iron_armor' && inv.iron_armor > 0) { - // Если уже надета броня - снимаем её if (state.player.equippedArmor === 'iron_armor') { state.player.equippedArmor = null; state.player.armor = 0; - console.log('[ARMOR] Iron armor unequipped'); } else { - // Надеваем броню state.player.equippedArmor = 'iron_armor'; state.player.armor = BLOCKS['iron_armor'].armor; - console.log('[ARMOR] Iron armor equipped - armor:', state.player.armor); } playSound('click'); renderInventory(); @@ -73,4 +70,4 @@ export function renderInventory() { inventoryGrid.appendChild(slot); } -} \ No newline at end of file +} diff --git a/src/ui/market.js b/src/ui/market.js new file mode 100644 index 0000000..24972d4 --- /dev/null +++ b/src/ui/market.js @@ -0,0 +1,115 @@ +// ==================== РЫНОК ПРЕДЛОЖЕНИЙ ==================== +import { state } from '../core/state.js'; +import { playSound } from '../audio/sound-engine.js'; + +const marketPanel = document.getElementById('marketPanel'); +const marketClose = document.getElementById('marketClose'); +const marketContent = document.getElementById('marketContent'); + +export function initMarket() { + // Кнопка закрытия + marketClose.addEventListener('click', () => closeMarket()); + + // Слушаем событие от save-controls.js + document.addEventListener('toggleMarket', () => toggleMarket()); +} + +export function toggleMarket() { + if (marketPanel.style.display === 'none' || !marketPanel.style.display) { + openMarket(); + } else { + closeMarket(); + } +} + +export function closeMarket() { + marketPanel.style.display = 'none'; +} + +function openMarket() { + marketPanel.style.display = 'block'; + renderMarket(); +} + +function renderMarket() { + if (!state.socket) { + marketContent.innerHTML = '

Подключение к серверу...

'; + return; + } + + // Запрашиваем список ордеров с сервера + state.socket.emit('market_list', {}, (orders) => { + if (!orders || !orders.length) { + marketContent.innerHTML = ` +

Предложений пока нет

+
+ +
+ `; + const btn = document.getElementById('createOrderBtn'); + if (btn) btn.addEventListener('click', showCreateOrder); + return; + } + + let html = '
'; + + html += orders.map(o => ` +
+
+ ${o.offer_item} ×${o.offer_qty} + ⇄ + ${o.want_item} ×${o.want_qty} +
+
${o.seller || 'Аноним'}
+
+ `).join(''); + + marketContent.innerHTML = html; + const btn = document.getElementById('createOrderBtn'); + if (btn) btn.addEventListener('click', showCreateOrder); + }); +} + +function showCreateOrder() { + const inv = state.player.inventory || []; + const items = inv.filter(slot => slot && slot.id); + + marketContent.innerHTML = ` +
+

Выставить предмет

+ + + + +
+ + +
+
+ `; + + document.getElementById('cancelOrder').addEventListener('click', () => renderMarket()); + document.getElementById('submitOrder').addEventListener('click', () => { + const offerItem = document.getElementById('offerItem').value; + const wantItem = document.getElementById('wantItem').value.trim(); + if (!offerItem || !wantItem) { + alert('Выберите предмет и укажите что хотите получить'); + return; + } + if (state.socket) { + state.socket.emit('market_create', { + offer_item: offerItem, + offer_qty: 1, + want_item: wantItem, + want_qty: 1 + }); + playSound('click'); + setTimeout(renderMarket, 500); + } + }); +} \ No newline at end of file diff --git a/src/ui/save-controls.js b/src/ui/save-controls.js index 445a7c4..4d4c382 100644 --- a/src/ui/save-controls.js +++ b/src/ui/save-controls.js @@ -1,22 +1,54 @@ -// ==================== КНОПКИ СОХРАНЕНИЯ / СБРОСА ==================== +// ==================== МЕНЮ ⋯ + СОХРАНЕНИЕ / СБРОС ==================== import { state } from '../core/state.js'; import { playSound } from '../audio/sound-engine.js'; import { saveGame } from '../game/save.js'; +let dropdownOpen = false; + export function initSaveControls() { - const saveBtn = document.getElementById('saveBtn'); - saveBtn.onclick = () => { + const menuBtn = document.getElementById('menuBtn'); + const menuDropdown = document.getElementById('menuDropdown'); + const saveItem = document.getElementById('saveItem'); + const resetItem = document.getElementById('resetItem'); + const marketItem = document.getElementById('marketItem'); + + // Убить возможный старый onclick (предотвращает race condition) + menuBtn.onclick = null; + + // Тогл меню + menuBtn.addEventListener('click', (e) => { + e.stopPropagation(); + dropdownOpen = !dropdownOpen; + menuDropdown.classList.toggle('open', dropdownOpen); + }); + + // Закрытие по клику вне + document.addEventListener('click', () => { + if (dropdownOpen) { + dropdownOpen = false; + menuDropdown.classList.remove('open'); + } + }); + + // Предотвращаем закрытие при клике внутри дропдауна + menuDropdown.addEventListener('click', (e) => { + e.stopPropagation(); + }); + + // 💾 Сохранить + saveItem.addEventListener('click', () => { playSound('click'); saveGame(); + dropdownOpen = false; + menuDropdown.classList.remove('open'); alert('Игра сохранена!'); - }; + }); - const resetBtn = document.getElementById('resetBtn'); - resetBtn.onclick = () => { + // 🔄 Новый мир + resetItem.addEventListener('click', () => { if (confirm('Вы уверены, что хотите удалить сохранение и начать новую игру?')) { playSound('click'); - // Удаляем сохранение из localStorage try { localStorage.removeItem(state.SAVE_KEY); console.log('Сохранение удалено из localStorage'); @@ -24,39 +56,44 @@ export function initSaveControls() { console.warn('Ошибка удаления сохранения:', e); } - // Сбрасываем in-memory сохранение state.inMemorySave = null; - - // Генерируем новый worldId state.worldId = Math.random().toString(36).substring(2, 10); console.log('Новый worldId после сброса:', state.worldId); - // Обновляем URL try { const newUrl = new URL(window.location.href); newUrl.searchParams.set('world', state.worldId); - const newUrlString = newUrl.toString(); - if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') { - window.history.replaceState(null, '', newUrlString); - console.log('URL обновлён:', newUrlString); + window.history.replaceState(null, '', newUrl.toString()); + console.log('URL обновлён:', newUrl.toString()); } } catch (e) { console.error('Ошибка обновления URL:', e); } - // Перезагружаем страницу location.reload(); + } else { + dropdownOpen = false; + menuDropdown.classList.remove('open'); } - }; + }); + + // 🏪 Рынок + marketItem.addEventListener('click', () => { + dropdownOpen = false; + menuDropdown.classList.remove('open'); + // toggleMarket импортируется в main.js, вызовем через кастомное событие + document.dispatchEvent(new CustomEvent('toggleMarket')); + }); } -// Показываем кнопку сохранения только если играем одни +// Показываем/скрываем пункт сохранения в зависимости от режима export function updateSaveButtonVisibility() { - const saveBtn = document.getElementById('saveBtn'); + const saveItem = document.getElementById('saveItem'); + if (!saveItem) return; if (state.isMultiplayer && state.otherPlayers.size > 0) { - saveBtn.style.display = 'none'; + saveItem.style.display = 'none'; } else { - saveBtn.style.display = 'flex'; + saveItem.style.display = 'block'; } } \ No newline at end of file diff --git a/src/ui/trade.js b/src/ui/trade.js new file mode 100644 index 0000000..6cc19bd --- /dev/null +++ b/src/ui/trade.js @@ -0,0 +1,277 @@ +// Trade UI — личная торговля (лицом к лицу) +import { state } from '../core/state.js'; +import { BLOCKS } from '../data/blocks.js'; +import { ITEMS } from '../data/items.js'; +import { TOOLS } from '../data/tools.js'; +import { playSound } from '../audio/sound-engine.js'; +import { addChatMessage } from './chat.js'; + +function itemName(id) { + return BLOCKS[id]?.n || TOOLS[id]?.n || ITEMS[id]?.n || id; +} +function itemIcon(id) { + if (BLOCKS[id]) return '🟫'; + if (TOOLS[id]) return TOOLS[id].icon || '🔧'; + if (ITEMS[id]) return ITEMS[id].icon || '📦'; + return '📦'; +} + +let tradeOpen = false; +let currentTradeId = null; +let myOffer = {}; +let partnerOffer = {}; +let iConfirmed = false; + +export function isTradeOpen() { return tradeOpen; } + +export function initTradeSocket(socket) { + // Incoming trade request + socket.on('trade_request', (data) => { + playSound('click'); + // Show trade request modal + const overlay = document.createElement('div'); + overlay.id = 'tradeRequestOverlay'; + overlay.className = 'custom-modal-overlay'; + overlay.innerHTML = ` +
+
🤝 ${data.from_name} хочет торговать
+ +
`; + document.getElementById('game').appendChild(overlay); + + document.getElementById('tradeAcceptBtn').addEventListener('click', () => { + overlay.remove(); + socket.emit('trade_accept', { from_socket_id: data.from }); + }); + document.getElementById('tradeDeclineBtn').addEventListener('click', () => { + overlay.remove(); + socket.emit('trade_decline', { from_socket_id: data.from }); + }); + }); + + socket.on('trade_start', (data) => { + currentTradeId = data.trade_id; + myOffer = {}; + partnerOffer = {}; + iConfirmed = false; + tradeOpen = true; + renderTradeUI(data.partner_name); + state.tradePanel.style.display = 'block'; + }); + + socket.on('trade_update', (data) => { + partnerOffer = data.player1Offer || data.player2Offer || {}; + // Determine which offer is the partner's + const isP1 = state.socket && state.socket.id === data.player1Offer; + // The partner's offer is the one we didn't send + // Actually the server sends both — we figure out which is ours + // Simpler: just use the data directly + if (state.mySocketId) { + // We need to know which offer in the update is ours vs partner's + // The trade_update event doesn't tell us, but we know myOffer already + // So partnerOffer = the one that's not myOffer + } + renderTradeOffers(); + }); + + socket.on('trade_status', (data) => { + // Update confirm buttons + const confirmBtn = document.getElementById('tradeConfirmBtn'); + if (confirmBtn) { + const amIPlayer1 = true; // We need to track this + if (iConfirmed) { + confirmBtn.textContent = '✅ Ожидание...'; + confirmBtn.disabled = true; + confirmBtn.style.background = '#555'; + } + } + // Show partner confirmation status + const partnerStatus = document.getElementById('partnerConfirmStatus'); + if (partnerStatus) { + const partnerConfirmed = data.player1Confirm || data.player2Confirm; + partnerStatus.textContent = partnerConfirmed ? '✅ Подтвердил' : '⏳ Ещё не подтвердил'; + } + }); + + socket.on('trade_complete', (data) => { + // Add received items to inventory + const received = data.received || {}; + for (const [item, qty] of Object.entries(received)) { + if (qty > 0) { + state.inv[item] = (state.inv[item] || 0) + qty; + } + } + // Remove sent items from inventory + const sent = data.sent || {}; + for (const [item, qty] of Object.entries(sent)) { + if (qty > 0) { + state.inv[item] = (state.inv[item] || 0) - qty; + if (state.inv[item] < 0) state.inv[item] = 0; + } + } + addChatMessage('Система', '🤝 Сделка завершена!'); + closeTrade(); + }); + + socket.on('trade_declined', () => { + addChatMessage('Система', 'Торговля отклонена'); + closeTrade(); + }); + + socket.on('trade_cancelled', () => { + addChatMessage('Система', 'Торговля отменена'); + closeTrade(); + }); + + socket.on('trade_error', (data) => { + addChatMessage('Система', '❌ ' + data.msg); + }); +} + +function renderTradeUI(partnerName) { + const panel = state.tradePanel; + const content = state.tradeContent; + content.innerHTML = ` +
🤝 Торговля с ${partnerName}
+
+
+
Вы предлагаете:
+
+ +
+
⇄
+
+
Партнёр предлагает:
+
+ Ожидание... +
+
+
+
⏳ Партнёр ещё не подтвердил
+
+ + +
`; + + document.getElementById('tradeAddItemBtn')?.addEventListener('click', showAddItemDialog); + document.getElementById('tradeConfirmBtn')?.addEventListener('click', confirmTrade); + document.getElementById('tradeCancelBtn')?.addEventListener('click', cancelTrade); +} + +function renderTradeOffers() { + const myItems = document.getElementById('myOfferItems'); + if (myItems) { + const keys = Object.keys(myOffer).filter(k => myOffer[k] > 0); + if (keys.length === 0) { + myItems.innerHTML = 'Ничего'; + } else { + myItems.innerHTML = keys.map(k => `
${itemIcon(k)} ${itemName(k)} x${myOffer[k]}
`).join(''); + } + } +} + +function showAddItemDialog() { + const inv = state.inv; + const items = Object.keys(inv).filter(id => inv[id] > 0); + if (items.length === 0) { + addChatMessage('Система', 'Инвентарь пуст!'); + return; + } + + const overlay = document.createElement('div'); + overlay.id = 'tradeItemPicker'; + overlay.className = 'custom-modal-overlay'; + let html = '
Что предложить?
'; + for (const id of items) { + // Show already offered count + const offered = myOffer[id] || 0; + const available = inv[id] - offered; + if (available <= 0) continue; + html += `
+ ${itemIcon(id)} + ${itemName(id)} + x${available} +
`; + } + html += '
'; + overlay.innerHTML = html; + document.getElementById('game').appendChild(overlay); + + overlay.querySelectorAll('.trade-pick-item').forEach(el => { + el.addEventListener('click', () => { + const itemId = el.dataset.item; + // Ask quantity + const available = inv[itemId] - (myOffer[itemId] || 0); + const qty = Math.min(available, available > 1 ? promptForQty(itemId, available) : 1); + if (qty > 0) { + myOffer[itemId] = (myOffer[itemId] || 0) + qty; + renderTradeOffers(); + // Reset confirm when offer changes + iConfirmed = false; + const btn = document.getElementById('tradeConfirmBtn'); + if (btn) { btn.textContent = '✅ Подтвердить'; btn.disabled = false; btn.style.background = '#2ecc71'; } + } + overlay.remove(); + }); + }); +} + +function promptForQty(itemId, max) { + // Simple: just use 1 for now, can enhance later with a proper dialog + const val = parseInt(prompt(`Сколько ${itemName(itemId)}? (макс: ${max})`, '1')); + return isNaN(val) || val < 1 ? 1 : Math.min(val, max); +} + +function confirmTrade() { + if (!currentTradeId || !state.socket) return; + + // Deduct offered items from inventory + for (const [item, qty] of Object.entries(myOffer)) { + if (qty > 0) { + if (state.inv[item] < qty) { + addChatMessage('Система', `Недостаточно ${itemName(item)}!`); + return; + } + } + } + + // Send offer + confirm + state.socket.emit('trade_offer', { trade_id: currentTradeId, offer: myOffer }); + + // Small delay then confirm + setTimeout(() => { + state.socket.emit('trade_confirm', { trade_id: currentTradeId }); + iConfirmed = true; + const btn = document.getElementById('tradeConfirmBtn'); + if (btn) { btn.textContent = '✅ Ожидание...'; btn.disabled = true; btn.style.background = '#555'; } + }, 200); +} + +function cancelTrade() { + if (!currentTradeId || !state.socket) return; + state.socket.emit('trade_cancel', { trade_id: currentTradeId }); + closeTrade(); +} + +export function closeTrade() { + tradeOpen = false; + currentTradeId = null; + myOffer = {}; + partnerOffer = {}; + iConfirmed = false; + if (state.tradePanel) state.tradePanel.style.display = 'none'; + // Remove any pick overlay + document.getElementById('tradeItemPicker')?.remove(); +} + +export function requestTradeWithPlayer(targetName) { + if (!state.socket || !state.isMultiplayer) { + addChatMessage('Система', 'Торговля доступна только в мультиплеере'); + return; + } + state.socket.emit('trade_request', { target_name: targetName }); + addChatMessage('Система', `Запрос отправлен ${targetName}...`); +} \ No newline at end of file diff --git a/src/world/water.js b/src/world/water.js index efb6627..f93cfd0 100644 --- a/src/world/water.js +++ b/src/world/water.js @@ -1,6 +1,5 @@ import { TILE, SEA_GY } from '../core/constants.js'; import { state } from '../core/state.js'; -import { W, H } from '../core/canvas.js'; import { grid, blocks, k, isSolid } from './world-storage.js'; // Физика жидкости @@ -19,6 +18,8 @@ function updateWaterPhysics(dt) { // Собираем только видимые водные блоки в очередь (оптимизация) waterUpdateQueue.clear(); + const W = state.W; + const H = state.H; const minGX = Math.floor(state.camX / TILE) - 10; const maxGX = Math.floor((state.camX + W) / TILE) + 10; const minGY = Math.floor(state.camY / TILE) - 10; @@ -123,4 +124,4 @@ function updateWaterPhysics(dt) { } } -export { updateWaterPhysics }; \ No newline at end of file +export { updateWaterPhysics }; diff --git a/src/world/world-storage.js b/src/world/world-storage.js index fb249cc..1d2716d 100644 --- a/src/world/world-storage.js +++ b/src/world/world-storage.js @@ -1,9 +1,10 @@ import { BLOCKS } from '../data/blocks.js'; import { state } from '../core/state.js'; -// Мир-хранилище -const grid = new Map(); // key "gx,gy" => {gx,gy,t, ...} -const blocks = []; // для рендера/перебора видимых +// Мир-хранилище — используем state.grid и state.blocks для единого источника данных +// Это гарантирует, что все модули (включая loop.js) видят одни и те же данные +const grid = state.grid; // Map: key "gx,gy" => {gx,gy,t, ...} +const blocks = state.blocks; // Array для рендера/перебора видимых function k(gx, gy) { return gx + ',' + gy; } @@ -54,4 +55,4 @@ function removeBlock(gx, gy) { return b; } -export { grid, blocks, k, getBlock, hasBlock, isSolid, setBlock, removeBlock }; \ No newline at end of file +export { grid, blocks, k, getBlock, hasBlock, isSolid, setBlock, removeBlock }; diff --git a/srcv3/audio/sound-engine.js b/srcv3/audio/sound-engine.js new file mode 100644 index 0000000..8d1671e --- /dev/null +++ b/srcv3/audio/sound-engine.js @@ -0,0 +1,37 @@ +// Звуковой движок +const sounds = {}; + +export { sounds }; + +export function loadSound(id, src) { + const audio = new Audio(); + audio.src = src; + audio.volume = 0.3; + sounds[id] = audio; +} + +// Загрузка звуков +loadSound('splash', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//splash.mp3'); +loadSound('sand1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand1.mp3'); +loadSound('snow1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//snow1.mp3'); +loadSound('stone1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1.mp3'); +loadSound('wood1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1.mp3'); +loadSound('cloth1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//cloth1.mp3'); +loadSound('fire', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//fire.mp3'); +loadSound('hit1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hit1.mp3'); +loadSound('attack', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//attack.mp3'); +loadSound('hurt_chicken', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//hurt1_chicken.mp3'); +loadSound('stone_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//stone1%20(1).mp3'); +loadSound('wood_build', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//wood1%20(1).mp3'); +loadSound('click', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//click.mp3'); +loadSound('explode1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//explode1.mp3'); +loadSound('glass1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//glass1.mp3'); +loadSound('eat1', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//eat1.mp3'); +loadSound('step', 'https://supamg.mkn8n.ru/storage/v1/object/public/sounds//sand2.mp3'); + +export function playSound(id) { + if (sounds[id]) { + sounds[id].currentTime = 0; + sounds[id].play().catch(e => console.error('Sound error:', e)); + } +} \ No newline at end of file diff --git a/srcv3/config.js b/srcv3/config.js new file mode 100644 index 0000000..df7b636 --- /dev/null +++ b/srcv3/config.js @@ -0,0 +1,12 @@ +// ==================== КОНФИГУРАЦИЯ СЕРВЕРА ==================== +// This module only sets SERVER_URL. Player name and world ID are handled in main.js +// to avoid duplicate prompts. + +const urlParams = new URLSearchParams(window.location.search); +export const SERVER_URL = urlParams.get('server') || 'https://apigrech.mkn8n.ru'; + +// Защита от mixed content +if (location.protocol === 'https:' && SERVER_URL.startsWith('http://')) { + console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP'); + alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.'); +} diff --git a/srcv3/core/canvas.js b/srcv3/core/canvas.js new file mode 100644 index 0000000..5378c40 --- /dev/null +++ b/srcv3/core/canvas.js @@ -0,0 +1,31 @@ +// ==================== ХОЛСТ 3D (WebGL) — GrechkaCraft v2 ==================== +// Тот же контракт, что canvas.js: выставляет state.W/H, хранит canvas. +// WebGL-контекст создаёт render3d (лениво, при первом кадре). +import { state } from './state.js'; + +export function initCanvas() { + const gameEl = document.getElementById('game'); + const canvas = document.getElementById('c'); + + const dpr = Math.max(1, window.devicePixelRatio || 1); + + function resize() { + state.W = gameEl.clientWidth; + state.H = gameEl.clientHeight; + } + + window.addEventListener('resize', resize); + resize(); + + state.gameEl = gameEl; + state.canvas = canvas; + state.ctx = null; + state.lightC = null; + state.lightCtx = null; + state.dpr = dpr; +} + +export function getCanvas() { return state.canvas; } +export function getCtx() { return null; } +export function getLightC() { return null; } +export function getLightCtx() { return null; } diff --git a/srcv3/core/constants.js b/srcv3/core/constants.js new file mode 100644 index 0000000..e48a06f --- /dev/null +++ b/srcv3/core/constants.js @@ -0,0 +1,32 @@ +// ==================== КОНСТАНТЫ ИГРЫ ==================== + +export const TILE = 40; + +// Мир +export const SEA_GY = 14; // уровень воды (gy) - уменьшил для меньших водоёмов +export const BEDROCK_GY = 140; // глубина бедрока (gy), чем больше - тем глубже +export const GEN_MARGIN_X = 40; // запас генерации по X (в тайлах) — увеличен для плавной прогрузки + +// Физика +export const GRAV = 2200; +export const GRAV_WATER = 550; +export const MOVE = 320; +export const JUMP = 760; + +// День/ночь +export const DAY_LEN = 360; // замедлил смену дня/ночи в 3 раза + +// Сохранение +export const SAVE_KEY = 'minegrechka_save'; + +// Сеть +export const MOVE_SEND_INTERVAL = 0.05; // 50ms = 20 раз в секунду + +// Погода +export const MAX_RAINDROPS = 200; + +// Чат +export const MAX_CHAT_MESSAGES = 50; + +// Голосовой чат +export const VOICE_SERVER = 'wss://voicegrech.mkn8n.ru'; \ No newline at end of file diff --git a/srcv3/core/state.js b/srcv3/core/state.js new file mode 100644 index 0000000..ec8edd6 --- /dev/null +++ b/srcv3/core/state.js @@ -0,0 +1,254 @@ +import { TILE, SEA_GY, BEDROCK_GY, GEN_MARGIN_X, GRAV, GRAV_WATER, MOVE, JUMP, DAY_LEN, SAVE_KEY, MOVE_SEND_INTERVAL, MAX_RAINDROPS, MAX_CHAT_MESSAGES, VOICE_SERVER } from './constants.js'; + +// Все мутабельные переменные игры в одном объекте состояния +export const state = { + // Константы (для доступа из модулей) + TILE, + SEA_GY, + BEDROCK_GY, + GEN_MARGIN_X, + GRAV, + GRAV_WATER, + MOVE, + JUMP, + DAY_LEN, + SAVE_KEY, + MOVE_SEND_INTERVAL, + MAX_RAINDROPS, + MAX_CHAT_MESSAGES, + VOICE_SERVER, + + // Камера + camX: 0, + camY: 0, + W: 0, + H: 0, + dpr: 1, + + // Canvas/DOM (заполняется в main.js) + gameEl: null, + canvas: null, + ctx: null, + lightC: null, + lightCtx: null, + + // День/ночь + worldTime: 0.30, // старт в полдень (0 = рассвет, 0.5+ = ночь) + isNightTime: false, + dayNightCycle: true, + + // Мультиплеер + isMultiplayer: false, + mySocketId: null, + socket: null, + voiceSocket: null, + voicePosT: 0, + + // Сервер + SERVER_URL: '', + TELEGRAM_BOT_USERNAME: '', + TELEGRAM_APP_SHORT_NAME: '', + + // Игрок + playerName: null, + worldId: null, + + // Инвентарь/UI + selected: "dirt", + showFullInventory: false, + craftOpen: false, + inventoryOpen: false, + chatOpen: false, + minimapOpen: false, + modeIdx: 0, + currentFurnaceKey: null, + + // Мир + blocksVersion: 0, + worldSeed: Math.floor(Math.random() * 1000000), + + // Погода + weatherType: "clear", + isRaining: false, + rainIntensity: 0, + weatherTimer: 0, + weatherChangeInterval: 60 + Math.random() * 120, + snowflakes: [], + lightningFlash: 0, + lightningTimer: 0, + + // Мобы/спавн + spawnT: 0, + + // Цикл + last: 0, + prevJump: false, + + // Сеть — throttle отправки позиции + lastMoveSendTime: 0, + lastSentX: 0, + lastSentY: 0, + + // Mining progress + mining: { gx: null, gy: null, progress: 0, hardness: 0 }, + + // Trade/Market + myTgId: null, + tradePanel: null, + tradeContent: null, + marketPanel: null, + marketContent: null, + + // DOM-ссылки (заполняются в main.js) + hpEl: null, + foodEl: null, + sxEl: null, + syEl: null, + todEl: null, + worldIdEl: null, + playerCountEl: null, + deathEl: null, + hotbarEl: null, + craftPanel: null, + recipesEl: null, + inventoryPanel: null, + inventoryGrid: null, + multiplayerStatus: null, + tempEl: null, + + // Игрок + player: { + x: 6 * TILE, + y: 0 * TILE, + z: 0, + w: 34, + h: 34, + d: 34, + vx: 0, + vy: 0, + vz: 0, + grounded: false, + inWater: false, + headInWater: false, + hp: 100, + hunger: 100, + o2: 100, + invuln: 0, + fallStartY: 0, + lastStepTime: 0, + sleeping: false, + inBoat: false, + armor: 0, + equippedArmor: null, + temperature: 15, + slowTimer: 0 + }, + + // Точка спавна + spawnPoint: { x: 6 * TILE, y: 0 * TILE }, + + // Инвентарь + inv: { + dirt: 6, stone: 0, sand: 0, gravel: 0, clay: 0, + wood: 0, planks: 0, ladder: 0, leaves: 0, coal: 0, + copper_ore: 0, iron_ore: 0, gold_ore: 0, diamond_ore: 0, + brick: 0, glass: 0, + tnt: 1, campfire: 0, torch: 0, + meat: 0, cooked: 0, arrow: 0, + wood_pickaxe: 0, stone_pickaxe: 0, iron_pickaxe: 0, + wood_sword: 0, stone_sword: 0, iron_sword: 0, + wood_axe: 0, stone_axe: 0, iron_axe: 0, + wood_shovel: 0, stone_shovel: 0, iron_shovel: 0, + iron_armor: 0, gold_armor: 0, + bow: 0, furnace: 0, + bed: 0, boat: 0, + iron_ingot: 0, gold_ingot: 0, copper_ingot: 0, + cobblestone: 0, + // New items + chicken_meat: 0, feather: 0, bone: 0, gunpowder: 0, + wheat: 0, bread: 0, carrot: 0, potato: 0, baked_potato: 0, + mushroom_stew: 0, + scorpion_stinger: 0, polar_fur: 0, slime_ball: 0, eagle_feather: 0, + snow: 0, ice: 0, cactus: 0, mushroom: 0, moss: 0, farmland: 0, + spruce_leaves: 0, dead_bush: 0, swamp_water: 0, + wheat_seeds: 0, wheat_stage0: 0, carrot_stage0: 0, potato_stage0: 0 + }, + + // Лодка + boat: { + x: 0, y: 0, + w: 34, h: 34, + vx: 0, vy: 0, + active: false, + inWater: false + }, + + // Ввод + inp: { up: false, down: false, left: false, right: false, jump: false, mine: false, build: false, bow: false, l: false, r: false, j: false, s: false }, + + // Мышь + mouse: { x: 0, y: 0 }, + + // Другие игроки (MP) + otherPlayers: new Map(), + + // Серверные мобы (MP) + serverMobs: new Map(), + + // Мобы + mobs: [], + + // Снаряды + projectiles: [], + + // Отслеживание изменений мира + placedBlocks: [], + removedBlocks: [], + + // Серверные изменения + serverOverrides: new Map(), + + // Чат + chatMessages: [], + + // Погода — капли + raindrops: [], + + // Облака + clouds: Array.from({ length: 10 }, () => ({ + x: Math.random() * 2000, + y: -200 - Math.random() * 260, + w: 80 + Math.random() * 120, + s: 12 + Math.random() * 20 + })), + + // Частицы + parts: [], + + // Активный TNT + activeTNT: new Set(), + + // Прочность инструментов + toolDurability: new Map(), + protections: new Map(), + + // Последние выбранные предметы + recentItems: [], + + // Активные печи + activeFurnaces: new Map(), + cropTimers: new Map(), + + // Сгенерированные колонны + generated: new Set(), + + // Изображение героя + heroImg: new Image(), + + // Сетка мира + grid: new Map(), + blocks: [], + + // In-memory save fallback + inMemorySave: null, +}; diff --git a/srcv3/data/blocks.js b/srcv3/data/blocks.js new file mode 100644 index 0000000..5f8994d --- /dev/null +++ b/srcv3/data/blocks.js @@ -0,0 +1,55 @@ +export const BLOCKS = { + air: { n:'Воздух', solid:false, hardness:0 }, + grass: { n:'Трава', c:'#7cfc00', solid:true, hardness:3, toolType:'shovel' }, + dirt: { n:'Грязь', c:'#8b4513', solid:true, hardness:3, toolType:'shovel' }, + stone: { n:'Камень', c:'#7f8c8d', solid:true, hardness:6, toolType:'pickaxe' }, + sand: { n:'Песок', c:'#f4d06f', solid:true, hardness:2, toolType:'shovel' }, + gravel: { n:'Гравий', c:'#95a5a6', solid:true, hardness:3, toolType:'shovel' }, + clay: { n:'Глина', c:'#74b9ff', solid:true, hardness:3, toolType:'shovel' }, + wood: { n:'Дерево', c:'#d35400', solid:true, hardness:4, toolType:'axe' }, + planks: { n:'Доски', c:'#e67e22', solid:true, hardness:3, toolType:'axe' }, + ladder: { n:'Лестница',c:'#d35400', solid:false, climbable:true, hardness:1, toolType:null }, + leaves: { n:'Листва', c:'#2ecc71', solid:true, hardness:1, toolType:'axe', minTool:null }, + glass: { n:'Стекло', c:'rgba(200,240,255,0.25)', solid:true, alpha:0.55, hardness:1, toolType:null }, + water: { n:'Вода', c:'rgba(52,152,219,0.55)', solid:false, fluid:true, hardness:0 }, + coal: { n:'Уголь', c:'#2c3e50', solid:true, hardness:8, toolType:'pickaxe', minTool:'wood' }, + copper_ore:{ n:'Медь', c:'#e17055', solid:true, hardness:10, toolType:'pickaxe', minTool:'wood' }, + iron_ore: { n:'Железо', c:'#dcdde1', solid:true, hardness:12, toolType:'pickaxe', minTool:'stone' }, + iron_armor:{ n:'Железная броня', c:'#95a5a6', solid:false, armor:0.5, hardness:0 }, + gold_ore: { n:'Золото', c:'#f1c40f', solid:true, hardness:15, toolType:'pickaxe', minTool:'stone' }, + diamond_ore:{n:'Алмаз', c:'#00a8ff', solid:true, hardness:20, toolType:'pickaxe', minTool:'iron' }, + brick: { n:'Кирпич', c:'#c0392b', solid:true, hardness:8, toolType:'pickaxe' }, + tnt: { n:'TNT', c:'#e74c3c', solid:true, explosive:true, hardness:1, toolType:null }, + campfire: { n:'Костёр', c:'#e67e22', solid:true, lightRadius:280, hardness:2, toolType:null }, + torch: { n:'Факел', c:'#f9ca24', solid:true, lightRadius:220, hardness:1, toolType:null }, + bedrock: { n:'Бедрок', c:'#2d3436', solid:true, unbreakable:true, hardness:Infinity }, + flower: { n:'Цветок', c:'#ff4757', solid:false, decor:true, hardness:0 }, + bed: { n:'Кровать', c:'#e91e63', solid:true, bed:true, hardness:2, toolType:'axe' }, + boat: { n:'Лодка', c:'#8B4513', solid:false, hardness:0 }, + furnace: { n:'Печь', c:'#696969', solid:true, smelting:true, hardness:8, toolType:'pickaxe', minTool:'wood' }, + cobblestone: { n:'Булыжник', c:'#7f8c8d', solid:true, hardness:6, toolType:'pickaxe' }, + flag: { n:'Флаг', c:'#e74c3c', solid:true, hardness:1, toolType:'axe', protectionRadius:10 }, + // === BIOME BLOCKS === + snow: { n:'Снег', c:'#ecf0f1', solid:true, hardness:1, toolType:'shovel' }, + ice: { n:'Лёд', c:'#74b9ff', solid:true, hardness:1, slip:true, toolType:'pickaxe' }, + cactus: { n:'Кактус', c:'#27ae60', solid:true, hardness:1, hurt:true, toolType:null }, + mushroom: { n:'Гриб', c:'#e74c3c', solid:false, decor:true, hardness:0 }, + moss: { n:'Мох', c:'#0a6640', solid:true, hardness:1, toolType:null }, + swamp_water:{ n:'Болотная вода', c:'rgba(100,120,40,0.6)', solid:false, fluid:true, poison:true, hardness:0 }, + farmland: { n:'Грядка', c:'#8B6914', solid:true, hardness:1, farmable:true, toolType:'shovel' }, + dead_bush: { n:'Сухой куст', c:'#b2bec3', solid:false, decor:true, hardness:0 }, + spruce_leaves:{ n:'Ель', c:'#0a6640', solid:true, hardness:1, toolType:'axe' }, + // === CROP STAGES === + wheat_stage0:{ n:'Росток пшеницы', c:'#a8e6a0', solid:false, decor:true, hardness:0, growthStage:0, growthTime:30, growsTo:'wheat_stage1' }, + wheat_stage1:{ n:'Пшеница', c:'#7dcea0', solid:false, decor:true, hardness:0, growthStage:1, growthTime:30, growsTo:'wheat_stage2' }, + wheat_stage2:{ n:'Пшеница', c:'#b8d730', solid:false, decor:true, hardness:0, growthStage:2, growthTime:30, growsTo:'wheat_stage3' }, + wheat_stage3:{ n:'Пшеница', c:'#f1c40f', solid:false, decor:true, hardness:0, harvestable:true, harvestItem:'wheat', harvestQty:2 }, + carrot_stage0:{ n:'Росток моркови', c:'#a8e6a0', solid:false, decor:true, hardness:0, growthStage:0, growthTime:30, growsTo:'carrot_stage1' }, + carrot_stage1:{ n:'Морковь', c:'#f0c27a', solid:false, decor:true, hardness:0, growthStage:1, growthTime:30, growsTo:'carrot_stage2' }, + carrot_stage2:{ n:'Морковь', c:'#e8a040', solid:false, decor:true, hardness:0, growthStage:2, growthTime:30, growsTo:'carrot_stage3' }, + carrot_stage3:{ n:'Морковь', c:'#e67e22', solid:false, decor:true, hardness:0, harvestable:true, harvestItem:'carrot', harvestQty:3 }, + potato_stage0:{ n:'Росток картофеля', c:'#a8e6a0', solid:false, decor:true, hardness:0, growthStage:0, growthTime:30, growsTo:'potato_stage1' }, + potato_stage1:{ n:'Картофель', c:'#c8b888', solid:false, decor:true, hardness:0, growthStage:1, growthTime:30, growsTo:'potato_stage2' }, + potato_stage2:{ n:'Картофель', c:'#bfaa78', solid:false, decor:true, hardness:0, growthStage:2, growthTime:30, growsTo:'potato_stage3' }, + potato_stage3:{ n:'Картофель', c:'#dfe6e9', solid:false, decor:true, hardness:0, harvestable:true, harvestItem:'potato', harvestQty:2 }, +}; diff --git a/srcv3/data/items.js b/srcv3/data/items.js new file mode 100644 index 0000000..cead22f --- /dev/null +++ b/srcv3/data/items.js @@ -0,0 +1,37 @@ +export const ITEMS = { + meat: { n:'Сырое мясо', icon:'🥩', food:15 }, + cooked: { n:'Жареное мясо', icon:'🍖', food:45 }, + arrow: { n:'Стрела', icon:'➡️', stack:64 }, + chicken_meat: { n:'Курица', icon:'🍗', food:12, stack:64 }, + feather: { n:'Перо', icon:'🪶', stack:64 }, + bone: { n:'Кость', icon:'🦴', stack:64 }, + gunpowder: { n:'Порох', icon:'💥', stack:64 }, + // === FARMING ITEMS === + wheat: { n:'Пшеница', icon:'🌾', stack:64 }, + bread: { n:'Хлеб', icon:'🍞', food:30 }, + carrot: { n:'Морковь', icon:'🥕', food:8, stack:64 }, + potato: { n:'Картофель', icon:'🥔', stack:64 }, + baked_potato:{ n:'Печёная картошка', icon:'🥔', food:25 }, + wheat_seeds: { n:'Семена пшеницы', icon:'🌱', stack:64 }, + mushroom_stew:{ n:'Грибной суп', icon:'🍲', food:40 }, + // === MOB DROP ITEMS === + scorpion_stinger:{ n:'Жало скорпиона', icon:'🔺', stack:64 }, + polar_fur: { n:'Шкура медведя', icon:'🧥', stack:64 }, + slime_ball: { n:'Слизь', icon:'🟢', stack:64 }, + eagle_feather:{ n:'Перо орла', icon:'🪶', stack:64 }, + // === ARMOR === + gold_armor: { n:'Золотая броня', icon:'🛡️', stack:1, armor:0.65 }, +}; + +// Runtime extensions — предметы от обжига +ITEMS.iron_ingot = { n:'Железный слиток', icon:'🔩' }; +ITEMS.gold_ingot = { n:'Золотой слиток', icon:'🪙' }; +ITEMS.copper_ingot = { n:'Медный слиток', icon:'🟤' }; + +// Axes and shovels +ITEMS.wood_axe = { n:'Деревянный топор', icon:'🪓' }; +ITEMS.stone_axe = { n:'Каменный топор', icon:'🪓' }; +ITEMS.iron_axe = { n:'Железный топор', icon:'🪓' }; +ITEMS.wood_shovel = { n:'Деревянная лопата', icon:'🔱' }; +ITEMS.stone_shovel = { n:'Каменная лопата', icon:'🔱' }; +ITEMS.iron_shovel = { n:'Железная лопата', icon:'🔱' }; diff --git a/srcv3/data/recipes.js b/srcv3/data/recipes.js new file mode 100644 index 0000000..f4b9d37 --- /dev/null +++ b/srcv3/data/recipes.js @@ -0,0 +1,44 @@ +export const RECIPES = [ + { out:'planks', qty:4, cost:{ wood:1 } }, + { out:'ladder', qty:3, cost:{ planks:7 } }, + { out:'torch', qty:2, cost:{ coal:1, planks:1 } }, + { out:'glass', qty:1, cost:{ sand:3 } }, + { out:'brick', qty:1, cost:{ stone:2, clay:1 } }, + { out:'campfire', qty:1, cost:{ wood:1, coal:1 } }, + { out:'tnt', qty:1, cost:{ sand:2, coal:1 } }, + { out:'bed', qty:1, cost:{ wood: 3, planks: 3 } }, + { out:'boat', qty:1, cost:{ wood: 5 } }, + { out:'wood_pickaxe', qty:1, cost:{ wood: 3, planks: 2 } }, + { out:'stone_pickaxe', qty:1, cost:{ wood: 1, stone: 3 } }, + { out:'iron_pickaxe', qty:1, cost:{ wood: 1, iron_ore: 2 } }, + { out:'wood_sword', qty:1, cost:{ wood: 2, planks: 1 } }, + { out:'stone_sword', qty:1, cost:{ wood: 1, stone: 2 } }, + { out:'iron_sword', qty:1, cost:{ wood: 1, iron_ore: 1 } }, + { out:'wood_axe', qty:1, cost:{ wood:3, planks:2 } }, + { out:'stone_axe', qty:1, cost:{ wood:1, stone:3 } }, + { out:'iron_axe', qty:1, cost:{ wood:1, iron_ore:2 } }, + { out:'wood_shovel', qty:1, cost:{ wood:1, planks:1 } }, + { out:'stone_shovel', qty:1, cost:{ wood:1, stone:2 } }, + { out:'iron_shovel', qty:1, cost:{ wood:1, iron_ore:1 } }, + { out:'iron_armor', qty:1, cost:{ iron_ore: 5 } }, + { out:'gold_armor', qty:1, cost:{ gold_ore: 8 } }, + { out:'furnace', qty:1, cost:{ stone: 8 } }, + { out:'bow', qty:1, cost:{ wood: 3, planks: 2 } }, + { out:'arrow', qty:4, cost:{ stone: 1, wood: 1 } }, + { out:'flag', qty:1, cost:{ planks:3, iron_ore:1 } }, + { out:'bread', qty:1, cost:{ wheat: 3 } }, + { out:'farmland', qty:1, cost:{ dirt:1 } }, + { out:'wheat_seeds', qty:1, cost:{ farmland:1, dirt:1 } }, +]; + +// Рецепты печи (обжиг) +export const SMELTING_RECIPES = [ + { in:'sand', qty:1, out:'glass', outQty:1, time:3 }, + { in:'clay', qty:1, out:'brick', outQty:1, time:3 }, + { in:'iron_ore', qty:1, out:'iron_ingot', outQty:1, time:5 }, + { in:'gold_ore', qty:1, out:'gold_ingot', outQty:1, time:5 }, + { in:'copper_ore', qty:1, out:'copper_ingot', outQty:1, time:4 }, + { in:'meat', qty:1, out:'cooked', outQty:1, time:2 }, + { in:'cobblestone', qty:1, out:'stone', outQty:1, time:2 }, + { in:'potato', qty:1, out:'baked_potato', outQty:1, time:2 } +]; diff --git a/srcv3/data/tools.js b/srcv3/data/tools.js new file mode 100644 index 0000000..87113c9 --- /dev/null +++ b/srcv3/data/tools.js @@ -0,0 +1,66 @@ +export const TOOLS = { + wood_pickaxe: { n:'Деревянная кирка', icon:'⛏️', damage:1, durability:60, toolType:'pickaxe', toolTier:'wood', speed:2 }, + stone_pickaxe: { n:'Каменная кирка', icon:'⛏️', damage:2, durability:132, toolType:'pickaxe', toolTier:'stone', speed:4 }, + iron_pickaxe: { n:'Железная кирка', icon:'⛏️', damage:3, durability:250, toolType:'pickaxe', toolTier:'iron', speed:6 }, + wood_sword: { n:'Деревянный меч', icon:'🗡️', damage:4, durability:60 }, + stone_sword: { n:'Каменный меч', icon:'🗡️', damage:5, durability:132 }, + iron_sword: { n:'Железный меч', icon:'🗡️', damage:7, durability:250 }, + wood_axe: { n:'Деревянный топор', icon:'🪓', damage:3, durability:60, toolType:'axe', toolTier:'wood', speed:2 }, + stone_axe: { n:'Каменный топор', icon:'🪓', damage:4, durability:132, toolType:'axe', toolTier:'stone', speed:4 }, + iron_axe: { n:'Железный топор', icon:'🪓', damage:5, durability:250, toolType:'axe', toolTier:'iron', speed:6 }, + wood_shovel: { n:'Деревянная лопата', icon:'🔱', damage:1, durability:60, toolType:'shovel', toolTier:'wood', speed:2 }, + stone_shovel: { n:'Каменная лопата', icon:'🔱', damage:2, durability:132, toolType:'shovel', toolTier:'stone', speed:4 }, + iron_shovel: { n:'Железная лопата', icon:'🔱', damage:3, durability:250, toolType:'shovel', toolTier:'iron', speed:6 }, + bow: { n:'Лук', icon:'🏹', damage:10, durability:200 }, +}; + +import { state } from '../core/state.js'; +import { BLOCKS } from './blocks.js'; + +export function addTool(type) { + const maxDur = TOOLS[type]?.durability || 60; + const key = `${type}_${Date.now()}_${Math.random().toString(36).slice(2,6)}`; + state.toolDurability.set(key, { type, current: maxDur, max: maxDur }); +} + +export function useTool(type) { + for (const [tid, dur] of state.toolDurability) { + if (dur.type === type && dur.current > 0) { + dur.current--; + if (dur.current <= 0) { + state.toolDurability.delete(tid); + state.inv[type] = Math.max(0, (state.inv[type] || 0) - 1); + return true; // broke + } + return false; // used but not broke + } + } + return false; // no durability entry found +} + +export function getBestTool(blockType) { + const block = BLOCKS[blockType]; + if (!block || !block.toolType) return null; + const toolType = block.toolType; + for (const tier of ['iron', 'stone', 'wood']) { + const toolId = `${tier}_${toolType}`; + if (TOOLS[toolId] && state.inv[toolId] > 0) return toolId; + } + return null; +} + +export function toolTierLessThan(toolTier, minTier) { + const tiers = { wood: 1, stone: 2, iron: 3 }; + return (tiers[toolTier] || 0) < (tiers[minTier] || 0); +} + +export function getMiningSpeed(blockType) { + const block = BLOCKS[blockType]; + if (!block) return 1; + const toolId = getBestTool(blockType); + if (toolId) { + const tool = TOOLS[toolId]; + if (tool.toolType === block.toolType) return tool.speed; + } + return 1; // Bare hands = 1 click per hit +} \ No newline at end of file diff --git a/srcv3/entities/boat.js b/srcv3/entities/boat.js new file mode 100644 index 0000000..545ce6b --- /dev/null +++ b/srcv3/entities/boat.js @@ -0,0 +1,19 @@ +// Лодка +import { state } from '../core/state.js'; + +export const boat = { + x: 0, y: 0, + w: 34, h: 34, + vx: 0, vy: 0, + active: false, + inWater: false +}; + +export function initBoat() { + boat.x = 0; + boat.y = 0; + boat.vx = 0; + boat.vy = 0; + boat.active = false; + boat.inWater = false; +} \ No newline at end of file diff --git a/srcv3/entities/mob-ai3d.js b/srcv3/entities/mob-ai3d.js new file mode 100644 index 0000000..6e48303 --- /dev/null +++ b/srcv3/entities/mob-ai3d.js @@ -0,0 +1,226 @@ +// ==================== МОБЫ 3D: ИИ + ФИЗИКА — GrechkaCraft v3 ==================== +// Вся логика из 2D-игры (все 9 видов, ночные/дневные, лут), но цель — +// настоящая 3D-точка игрока (x,y,z) и прыжки через стены по всем осям. +import { state } from '../core/state.js'; +import { GRAV, GRAV_WATER, TILE } from '../core/constants.js'; +import { updateWaterFlag3 } from '../physics/water-detect3d.js'; +import { calculateDamage } from '../entities/player.js'; +import { playSound } from '../audio/sound-engine.js'; +import { explodeTNT3D } from '../world/tnt3d.js'; +import { resolveY, resolveX, resolveZ } from '../physics/collision3d.js'; +import { getBlock } from '../world/world-storage3d.js'; + +export function isNight() { + return state.worldTime > 0.5; +} + +// Центры сущностей в тайлах +function cx(e) { return (e.x + e.w / 2) / TILE; } +function cy(e) { return (e.y + e.h / 2) / TILE; } +function cz(e) { return ((e.z || 0) + (e.d || e.w) / 2) / TILE; } + +function playerC() { + const p = state.player; + return { x: cx(p), y: cy(p), z: cz(p) }; +} + +function dist3(a, b) { + return Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z); +} + +// Тянет моба к игроку по обеим горизонтальным осям (X и Z) +function steerToPlayer(m, pc) { + const dx = pc.x - cx(m); + const dz = pc.z - cz(m); + const len = Math.hypot(dx, dz) || 1; + m.vx = (dx / len) * (m.speed || 60); + m.vz = (dz / len) * (m.speed || 60); + m.dir = dx >= 0 ? 1 : -1; + m.faceYaw = Math.atan2(dz, dx); +} + +function touchPlayer(m, pc, damage, knock = 1) { + const d = dist3({ x: cx(m), y: cy(m), z: cz(m) }, pc); + if (d < 1.6 && state.player.invuln <= 0) { + state.player.hp -= calculateDamage(damage); + state.player.invuln = 0.8; + const dx = state.player.x - m.x; + const dz = (state.player.z || 0) - (m.z || 0); + const len = Math.hypot(dx, dz) || 1; + state.player.vx += (dx / len) * 420 * knock; + state.player.vz += (dz / len) * 420 * knock; + state.player.vy -= 260 * knock; + playSound('hit1'); + return true; + } + return false; +} + +export function mobAI(m, dt) { + updateWaterFlag3(m, dt); + const pc = playerC(); + const night = isNight(); + + // ---------- ночные мобы днём горят ---------- + if ((m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton') && !night) { + m.hp -= 30 * dt; + m.vx *= 0.5; + m.vz *= 0.5; + return; + } + + if (m.kind === 'zombie') { + steerToPlayer(m, pc); + if (m.inWater && Math.random() < 0.06) m.vy = -260; + touchPlayer(m, pc, 15); + } else if (m.kind === 'creeper') { + steerToPlayer(m, pc); + const d = dist3({ x: cx(m), y: cy(m), z: cz(m) }, pc); + if (m.inWater && Math.random() < 0.06) m.vy = -260; + if (d < 3) { + m.fuse -= dt; + if (m.fuse <= 0) { + explodeTNT3D({ gx: Math.floor(cx(m)), gy: Math.floor(cy(m)), gz: Math.floor(cz(m)) }, state); + m.hp = 0; + } + } else if (d < 2.5) { + m.fuse = 0.5; + } + } else if (m.kind === 'skeleton') { + steerToPlayer(m, pc); + const d = dist3({ x: cx(m), y: cy(m), z: cz(m) }, pc); + m.shootCooldown -= dt; + if (d < 15 && m.shootCooldown <= 0) { + m.shootCooldown = 2.0; + const ox = m.x + m.w / 2, oy = m.y + m.h / 3, oz = (m.z || 0) + (m.d || m.w) / 2; + const tx = state.player.x + state.player.w / 2; + const ty = state.player.y + state.player.h / 2; + const tz = (state.player.z || 0) + (state.player.d || state.player.w) / 2; + // полёт стрелы по 3D + const ddx = tx - ox, ddy = ty - oy, ddz = tz - oz; + const dist = Math.hypot(ddx, ddy, ddz) || 1; + const speed = 450; + state.projectiles.push({ + x: ox, y: oy, z: oz, + vx: ddx / dist * speed, + vy: ddy / dist * speed, + vz: ddz / dist * speed, + dmg: 6, owner: 'mob', life: 3, + }); + } + } else if (m.kind === 'scorpion') { + steerToPlayer(m, pc); + if (m.inWater && Math.random() < 0.06) m.vy = -260; + if (touchPlayer(m, pc, 8)) state.player.slowTimer = 3; + } else if (m.kind === 'polar_bear') { + // нейтрален: бродит + m.aiT -= dt; + if (m.aiT <= 0) { + m.aiT = 2.0 + Math.random() * 3; + const a = Math.random() * Math.PI * 2; + m.walkX = Math.cos(a); + m.walkZ = Math.sin(a); + if (Math.random() < 0.3) { m.walkX = 0; m.walkZ = 0; } + } + m.vx = (m.walkX || 0) * m.speed; + m.vz = (m.walkZ || 0) * m.speed; + if (m.inWater) m.vy = -120; + touchPlayer(m, pc, 10, 1.2); + } else if (m.kind === 'slime') { + // прыгает к игроку + m.aiT -= dt; + if (m.aiT <= 0) { + m.aiT = 1.5 + Math.random() * 1.5; + steerToPlayer(m, pc); + m.vy = -200; + } + touchPlayer(m, pc, 8); + } else if (m.kind === 'eagle') { + // летает, пикирует + const d = dist3({ x: cx(m), y: cy(m), z: cz(m) }, pc); + if (d < 20) { + const dx = pc.x - cx(m), dz = pc.z - cz(m), dy = pc.y - cy(m); + const hl = Math.hypot(dx, dz) || 1; + m.vx = dx / hl * m.speed; + m.vz = dz / hl * m.speed; + m.vy = dy > 0 ? 60 : -60; + } else { + m.aiT -= dt; + if (m.aiT <= 0) { + m.aiT = 2 + Math.random() * 3; + const a = Math.random() * Math.PI * 2; + m.walkX = Math.cos(a); + m.walkZ = Math.sin(a); + } + m.vx = (m.walkX || 0) * m.speed * 0.5; + m.vz = (m.walkZ || 0) * m.speed * 0.5; + m.vy = Math.sin(performance.now() / 1000) * 30; + } + if (d < 1.6 && state.player.invuln <= 0) { + state.player.hp -= calculateDamage(10); + state.player.invuln = 0.8; + state.player.vy -= 200; + playSound('hit1'); + } + m.vy *= 0.5; + m.flying = true; + } else { + // животные (pig, chicken): бродят + m.aiT -= dt; + if (m.aiT <= 0) { + m.aiT = 1.8 + Math.random() * 2.5; + const a = Math.random() * Math.PI * 2; + m.walkX = Math.cos(a); + m.walkZ = Math.sin(a); + if (Math.random() < 0.25) { m.walkX = 0; m.walkZ = 0; } + } + const sp = m.kind === 'chicken' ? 55 : 40; + m.vx = (m.walkX || 0) * sp; + m.vz = (m.walkZ || 0) * sp; + if (m.inWater) m.vy = -120; + } + + // ---------- прыжок через препятствие по всем осям ---------- + if (m.grounded && !m.flying && m.kind !== 'eagle') { + const speed = Math.hypot(m.vx || 0, m.vz || 0); + if (speed > 5) { + const gx = Math.floor((m.x + (m.vx > 0 ? m.w + 2 : -2)) / TILE); + const gz = Math.floor(((m.z || 0) + (m.vz > 0 ? (m.d || m.w) + 2 : -2)) / TILE); + const gy = Math.floor((m.y + m.h - 1) / TILE); + const bx = getBlock(gx, gy, Math.floor(cz(m))); + const bz = getBlock(Math.floor(cx(m)), gy, gz); + if (bx || bz) m.vy = -280; + } + } + + // ---------- ФИЗИКА ---------- + if (!m.flying) { + const g = m.inWater ? GRAV_WATER : GRAV; + m.vy += g * dt; + } + m.y += m.vy * dt; + m.x += (m.vx || 0) * dt; + m.z += (m.vz || 0) * dt; + resolveX(m, dt); + resolveZ(m, dt); + resolveY(m, dt); + + // орёл не падает + if (m.flying) m.grounded = false; +} + +// Лут — та же таблица, что в 2D +export function getMobLoot(kind) { + const table = { + chicken: [{item:'chicken_meat',min:1,max:2,chance:1},{item:'feather',min:0,max:1,chance:0.5}], + pig: [{item:'meat',min:1,max:2,chance:1}], + zombie: [{item:'meat',min:0,max:1,chance:0.5},{item:'iron_ingot',min:0,max:1,chance:0.3}], + skeleton: [{item:'arrow',min:2,max:4,chance:1},{item:'bone',min:0,max:2,chance:0.6},{item:'bow',min:1,max:1,chance:0.15}], + creeper: [{item:'gunpowder',min:1,max:2,chance:1}], + scorpion: [{item:'scorpion_stinger',min:0,max:1,chance:0.4}], + polar_bear:[{item:'polar_fur',min:1,max:2,chance:0.8},{item:'meat',min:1,max:2,chance:1}], + slime: [{item:'slime_ball',min:1,max:2,chance:1}], + eagle: [{item:'eagle_feather',min:1,max:2,chance:0.7}], + }; + return table[kind] || []; +} diff --git a/srcv3/entities/mobs3d.js b/srcv3/entities/mobs3d.js new file mode 100644 index 0000000..07fc08b --- /dev/null +++ b/srcv3/entities/mobs3d.js @@ -0,0 +1,56 @@ +// Сущности: животные + зомби + новые мобы + +export class Entity { + constructor(x, y, w, h) { + this.x = x; + this.y = y; + this.z = 0; // третье измерение: глубина мира + this.w = w; + this.h = h; + this.d = w; // размер по Z + this.vx = 0; + this.vy = 0; + this.vz = 0; + this.hp = 3; + this.grounded = false; + this.inWater = false; + this.aiT = 0; + this.dir = 1; + } +} + +export class Pig extends Entity { + constructor(x, y, z) { super(x, y, 34, 24); this.kind = 'pig'; this.z = z || 0; this.hp = 2; } +} + +export class Chicken extends Entity { + constructor(x, y, z) { super(x, y, 26, 22); this.kind = 'chicken'; this.z = z || 0; this.hp = 1; } +} + +export class Zombie extends Entity { + constructor(x, y, z) { super(x, y, 34, 50); this.kind = 'zombie'; this.z = z || 0; this.hp = 4; this.speed = 80 + Math.random() * 40; } +} + +export class Creeper extends Entity { + constructor(x, y, z) { super(x, y, 34, 50); this.kind = 'creeper'; this.z = z || 0; this.hp = 4; this.speed = 60 + Math.random() * 30; this.fuse = 3.2; } +} + +export class Skeleton extends Entity { + constructor(x, y, z) { super(x, y, 34, 50); this.kind = 'skeleton'; this.z = z || 0; this.hp = 4; this.speed = 70 + Math.random() * 30; this.shootCooldown = 0; } +} + +export class Scorpion extends Entity { + constructor(x, y, z) { super(x, y, 26, 26); this.kind = 'scorpion'; this.z = z || 0; this.hp = 3; this.speed = 90; this.fuse = 0; this.shootCooldown = 0; } +} + +export class PolarBear extends Entity { + constructor(x, y, z) { super(x, y, 40, 34); this.kind = 'polar_bear'; this.z = z || 0; this.hp = 8; this.speed = 50; this.fuse = 0; this.shootCooldown = 2; } +} + +export class Slime extends Entity { + constructor(x, y, z) { super(x, y, 24, 24); this.kind = 'slime'; this.z = z || 0; this.hp = 2; this.speed = 30; this.fuse = 0; this.shootCooldown = 2; } +} + +export class Eagle extends Entity { + constructor(x, y, z) { super(x, y, 30, 22); this.kind = 'eagle'; this.z = z || 0; this.hp = 3; this.speed = 120; this.fuse = 0; this.shootCooldown = 0; } +} diff --git a/srcv3/entities/player.js b/srcv3/entities/player.js new file mode 100644 index 0000000..577682c --- /dev/null +++ b/srcv3/entities/player.js @@ -0,0 +1,40 @@ +import { TILE } from '../core/constants.js'; +import { state } from '../core/state.js'; + +// Функция для расчёта урона с учётом брони +function calculateDamage(baseDamage) { + const reduction = state.player.armor; + const actualDamage = baseDamage * (1 - reduction); + return actualDamage; +} + +// Инициализация игрока +function initPlayer() { + state.player = { + x: 6 * TILE, + y: 0 * TILE, + w: 34, + h: 34, + vx: 0, + vy: 0, + grounded: false, + inWater: false, + headInWater: false, + hp: 100, + hunger: 100, + o2: 100, + invuln: 0, + fallStartY: 0, + lastStepTime: 0, + sleeping: false, + inBoat: false, + armor: 0, + equippedArmor: null, + temperature: 15, + slowTimer: 0 + }; + + state.spawnPoint = { x: 6 * TILE, y: 0 * TILE }; +} + +export { calculateDamage, initPlayer }; diff --git a/srcv3/game/loop3d.js b/srcv3/game/loop3d.js new file mode 100644 index 0000000..470ee19 --- /dev/null +++ b/srcv3/game/loop3d.js @@ -0,0 +1,402 @@ +// ==================== ЦИКЛ ИГРЫ 3D — GrechkaCraft v3 ==================== +// Всё как в 2D-игре, но движение по трём осям: X (вперёд-назад по миру), +// Y (вверх-вниз), Z (вбок). WASD работают относительно взгляда камеры. +import { state } from '../core/state.js'; +import { BLOCKS } from '../data/blocks.js'; +import { playSound } from '../audio/sound-engine.js'; +import { getBlock } from '../world/world-storage3d.js'; +import { updateWaterFlag3 } from '../physics/water-detect3d.js'; +import { resolveY, resolveX, resolveZ, onLadder } from '../physics/collision3d.js'; +import { calculateDamage } from '../entities/player.js'; +import { tickFurnaces } from '../ui/furnace.js'; +import { renderFurnaceUI } from '../ui/furnace.js'; +import { updateWeather, updateRain } from '../world/weather.js'; +import { ensureGenAroundCamera, surfaceGyAt, getCachedBiome, WORLD_DEPTH } from '../world/generation3d.js'; +import { Zombie, Creeper, Skeleton, Pig, Chicken, Scorpion, PolarBear, Slime, Eagle } from '../entities/mobs3d.js'; +import { isNight, mobAI } from '../entities/mob-ai3d.js'; +import { sendPlayerPosition } from '../multiplayer/socket-helpers.js'; +import { renderFrame3D } from '../render/render3d.js'; +import { explodeTNT3D } from '../world/tnt3d.js'; + +let last = performance.now(); +let prevJump = false; + +document.addEventListener('visibilitychange', () => { + if (!document.hidden) last = performance.now(); +}); + +// Направление взгляда камеры (задаётся рендером) — WASD относительно него +export const camDir = { yaw: 0, pitch: 0 }; + +export function loop(now) { + const rawDt = Math.min(0.05, (now - last) / 1000); + last = now; + const PHYSICS_STEP = 0.016; + const steps = Math.max(1, Math.ceil(rawDt / PHYSICS_STEP)); + const dt = rawDt / steps; + + const player = state.player; + const inp = state.inp; + const mobs = state.mobs; + const projectiles = state.projectiles; + const boat = state.boat; + + const jumpPressed = inp.j && !prevJump; + prevJump = inp.j; + + // ---------- день/ночь ---------- + if (!state.dayNightCycle) { + // время стоит + } else if (player.sleeping && isNight()) { + state.worldTime += dt * 8 / state.DAY_LEN; + player.hp = Math.min(100, player.hp + dt * 20); + if (!isNight()) player.sleeping = false; + } else { + state.worldTime += dt / state.DAY_LEN; + } + if (state.worldTime >= 1) state.worldTime -= 1; + + // ---------- генерация мира ---------- + ensureGenAroundCamera(); + + // ---------- кислород/утопление ---------- + if (player.headInWater) { + player.o2 = Math.max(0, player.o2 - 6 * dt); + if (player.o2 === 0) player.hp -= calculateDamage(4 * dt); + } else { + player.o2 = Math.min(100, player.o2 + 10 * dt); + } + + player.hunger = Math.max(0, player.hunger - dt * 0.2); + + // ---------- температура по биому (теперь биом 2D) ---------- + if (player.temperature === undefined) player.temperature = 15; + const BIOME_TEMP = { snow: -20, plains: 15, swamp: 20, desert: 45, mountains: 5 }; + const COLD_THRESHOLD = -5; + const HEAT_THRESHOLD = 35; + const pGX = Math.floor((player.x + player.w / 2) / state.TILE); + const pGZ = Math.floor((player.z + player.d / 2) / state.TILE); + const pGY = Math.floor((player.y + player.h / 2) / state.TILE); + const biome = getCachedBiome(pGX, pGZ); + const sGY = surfaceGyAt(pGX, pGZ); + const isUnderground = (pGY - sGY) > 2; + let targetTemp = BIOME_TEMP[biome] || 15; + if (isUnderground) targetTemp = 15; + if (isNight() && !isUnderground) targetTemp -= 10; + if ((state.weatherType === 'rain' || state.weatherType === 'storm') && !isUnderground) targetTemp -= 5; + player.temperature += (targetTemp - player.temperature) * dt * 0.3; + + // костры/факелы греют, вода в пустыне холодит — ищем в кубе вокруг + let nearHeat = false, nearCool = false; + for (let dx = -2; dx <= 2 && !nearHeat; dx++) { + for (let dy = -2; dy <= 2 && !nearHeat; dy++) { + for (let dz = -2; dz <= 2 && !nearHeat; dz++) { + const b = getBlock(pGX + dx, pGY + dy, pGZ + dz); + if (b && (b.t === 'campfire' || b.t === 'torch' || b.t === 'furnace')) nearHeat = true; + } + } + } + if (!nearHeat && biome === 'desert') { + for (let dx = -2; dx <= 2 && !nearCool; dx++) { + for (let dz = -2; dz <= 2 && !nearCool; dz++) { + const b = getBlock(pGX + dx, pGY, pGZ + dz); + if (b && b.t === 'water') nearCool = true; + } + } + } + if (nearHeat) { + player.temperature += (30 - player.temperature) * dt * 2; + if (player.hp < 100) player.hp = Math.min(100, player.hp + 8 * dt); + } + if (nearCool) { + player.temperature += (15 - player.temperature) * dt * 1.5; + if (player.hp < 100 && player.temperature > HEAT_THRESHOLD) player.hp = Math.min(100, player.hp + 8 * dt); + } + if (isUnderground && !nearHeat && player.hp < 100 && + player.temperature >= COLD_THRESHOLD && player.temperature <= HEAT_THRESHOLD) { + player.hp = Math.min(100, player.hp + 2 * dt); + } + const aboveBlk = getBlock(pGX, pGY + 1, pGZ); + if (aboveBlk && BLOCKS[aboveBlk.t] && BLOCKS[aboveBlk.t].solid && aboveBlk.t !== 'glass' && !isUnderground && !nearCool) { + player.temperature -= 5 * dt; + } + if (player.temperature < COLD_THRESHOLD) { + player.hp -= 1 * dt; + player.hunger -= 0.5 * dt; + } + if (player.slowTimer > 0) player.slowTimer -= dt; + + // ---------- кактус: урон в кубе ---------- + for (let dx = -1; dx <= 1; dx++) { + for (let dy = -1; dy <= 1; dy++) { + for (let dz = -1; dz <= 1; dz++) { + const cb = getBlock(pGX + dx, pGY + dy, pGZ + dz); + if (cb && BLOCKS[cb.t] && BLOCKS[cb.t].hurt && player.invuln <= 0) { + player.hp -= 2; + player.invuln = 0.5; + } + } + } + } + + // ---------- лёд: скольжение ---------- + const underBlk = getBlock(pGX, pGY - 1, pGZ); + if (underBlk && BLOCKS[underBlk.t] && BLOCKS[underBlk.t].slip && player.grounded) { + player.vx *= 0.98; + player.vz *= 0.98; + } + + // ---------- ДВИЖЕНИЕ: WASD относительно камеры ---------- + if (player.sleeping) { + player.vx = player.vy = player.vz = 0; + } else { + // локальные оси ввода + let fwd = 0, side = 0; + if (inp.up) fwd -= 1; + if (inp.r || inp.right) side += 1; + if (inp.l || inp.left) side -= 1; + if (inp.d || inp.back) fwd += 1; + + // в мировых осях через угол камеры + const cy = Math.cos(camDir.yaw), sy = Math.sin(camDir.yaw); + let dx = side * cy - fwd * sy; + let dz = side * sy + fwd * cy; + const len = Math.hypot(dx, dz); + if (len > 0.0001) { + dx /= len; dz /= len; + player.vx = dx * state.MOVE; + player.vz = dz * state.MOVE; + player.facing = Math.atan2(dz, dx); + } else { + player.vx *= 0.82; + player.vz *= 0.82; + } + } + + // шаги + if (player.grounded && !player.inWater && Math.hypot(player.vx, player.vz) > 50) { + if (now / 1000 - player.lastStepTime > 0.35) { + playSound('step'); + player.lastStepTime = now / 1000; + } + } + + // ---------- прыжок / плавание / лодка ---------- + if (player.inBoat) { + player.vy = 0; + player.x = boat.x + 2; + player.y = boat.y - 4; + player.z = boat.z + 2; + player.grounded = true; + player.inWater = false; + if (jumpPressed) { + state.inv.boat = (state.inv.boat || 0) + 1; + player.inBoat = false; + boat.active = false; + player.y += state.TILE; + player.vy = -state.JUMP * 0.5; + playSound('splash'); + } + } else if (player.inWater) { + player.vx *= 0.90; + player.vz *= 0.90; + player.vy *= 0.92; + if (!jumpPressed && !inp.j) { + player.vy += state.GRAV_WATER * dt; + } else if (jumpPressed) { + player.vy = Math.min(player.vy, -520); + } else if (inp.j) { + player.vy = Math.min(player.vy, -260); + } + } else { + if (jumpPressed && player.grounded && !player.sleeping) { + player.vy = -state.JUMP; + player.grounded = false; + player.fallStartY = player.y; + } + } + + if (!player.inWater && !player.inBoat) { + player.vy += state.GRAV * dt; + } + + // ---------- лодка: плывёт, тонет вне воды ---------- + if (boat.active) { + boat.x += boat.vx * dt; + boat.y += boat.vy * dt; + boat.z += (boat.vz || 0) * dt; + const boatGX = Math.floor(boat.x / state.TILE); + const boatGY = Math.floor(boat.y / state.TILE); + const boatGZ = Math.floor(boat.z / state.TILE); + const below = getBlock(boatGX, boatGY + 1, boatGZ); + if (!below || (below.t !== 'water' && below.t !== 'swamp_water')) { + state.inv.boat = (state.inv.boat || 0) + 1; + player.inBoat = false; + boat.active = false; + player.y += state.TILE; + player.vy = -200; + playSound('splash'); + } + } + + if (player.inBoat && !boat.active) { + state.inv.boat = (state.inv.boat || 0) + 1; + player.inBoat = false; + player.y += state.TILE; + player.vy = -200; + playSound('splash'); + } + + // ---------- границы мира по Z ---------- + const maxZ = WORLD_DEPTH * state.TILE; + if (player.z < -maxZ) { player.z = -maxZ; player.vz = 0; } + if (player.z > maxZ) { player.z = maxZ; player.vz = 0; } + + // ---------- ФИЗИКА: X → Z → Y ---------- + const sub = dt; + for (let step = 0; step < steps; step++) { + player.x += player.vx * sub; + player.z += player.vz * sub; + player.y += player.vy * sub; + resolveX(player, sub); + resolveZ(player, sub); + resolveY(player, sub); + } + + // лестница: вертикальное перемещение + if (onLadder(player)) { + if (inp.j) player.vy = -200; + else if (inp.s || inp.down) player.vy = 100; + else player.vy = 0; + } + + // урон от падения: дистанция от последнего касания земли + if (player.grounded) { + if (player.fallStartY !== undefined) { + const fallDist = player.fallStartY - player.y; + if (fallDist > state.TILE * 6) { + player.hp -= calculateDamage((fallDist / state.TILE - 6) * 2.5); + } + } + player.fallStartY = player.y; + } else if (player.vy > 0 && player.fallStartY === undefined) { + player.fallStartY = player.y; + } + + // вода + updateWaterFlag3(player, dt); + + sendPlayerPosition(); + updateWeather(dt); + updateRain(dt); + player.invuln = Math.max(0, player.invuln - dt); + + // ---------- мобы ---------- + state.spawnT += dt; + if (state.spawnT > 6) { + state.spawnT = 0; + spawnMobs3D(); + } + for (const m of mobs) { + mobAI(m, dt); // физика 3D внутри mob-ai3d + } + // снаряды + for (let i = projectiles.length - 1; i >= 0; i--) { + const pr = projectiles[i]; + pr.x += pr.vx * dt; + pr.y += pr.vy * dt; + pr.life -= dt; + // столкновение с блоком (по Z игрока/снаряда) + const gx = Math.floor(pr.x / state.TILE); + const gy = Math.floor(pr.y / state.TILE); + const gz = Math.floor((pr.z || player.z) / state.TILE); + const b = getBlock(gx, gy, gz); + if (b && BLOCKS[b.t] && BLOCKS[b.t].solid) { + projectiles.splice(i, 1); + continue; + } + if (pr.life <= 0) projectiles.splice(i, 1); + } + + // ---------- TNT ---------- + for (const b of state.blocks) { + if (b.dead || b.t !== 'tnt' || !b.active) continue; + b.fuse -= dt; + if (b.fuse <= 0) { + explodeTNT3D(b, state); + } + } + + // ---------- печь ---------- + tickFurnaces(dt); + if (state.currentFurnaceKey && Math.random() < 0.1) renderFurnaceUI(); + + // ---------- смерть/возрождение ---------- + if (player.hp <= 0 && state.deathEl && state.deathEl.style.display !== 'flex') { + player.hp = 0; + state.deathEl.style.display = 'flex'; + } else if (player.hp > 0 && state.deathEl && state.deathEl.style.display === 'flex') { + state.deathEl.style.display = 'none'; + } + + // ---------- HUD ---------- + if (Math.random() < 0.25) { + state.hpEl.textContent = Math.max(0, Math.ceil(player.hp)); + state.foodEl.textContent = Math.ceil(player.hunger); + const o2El = document.getElementById('o2'); + if (o2El) o2El.textContent = Math.ceil(player.o2); + const tempEl = document.getElementById('temp'); + if (tempEl) { + const t = Math.round(player.temperature); + tempEl.textContent = t + '°C'; + if (player.temperature < COLD_THRESHOLD) tempEl.style.color = '#74b9ff'; + else if (player.temperature > HEAT_THRESHOLD) tempEl.style.color = '#e17055'; + else tempEl.style.color = '#55efc4'; + } + state.sxEl.textContent = pGX; + state.syEl.textContent = pGY; + state.todEl.textContent = isNight() ? 'Ночь' : 'День'; + state.worldIdEl.textContent = state.worldId; + if (state.isMultiplayer) { + const st = document.getElementById('multiplayerStatus'); + if (st) st.style.display = 'flex'; + state.playerCountEl.textContent = state.otherPlayers.size + 1; + } else { + const st = document.getElementById('multiplayerStatus'); + if (st) st.style.display = 'none'; + } + } + + // сон — DOM-оверлей + const sleepOv = document.getElementById('sleepOverlay'); + if (sleepOv) sleepOv.style.display = player.sleeping ? 'flex' : 'none'; + + // ---------- РЕНДЕР ---------- + renderFrame3D(now); + + requestAnimationFrame(loop); +} + +// ---------- спавн мобов в 3D ---------- +function spawnMobs3D() { + const p = state.player; + const pcx = p.x / state.TILE; + const pcy = p.y / state.TILE; + const pcz = (p.z || 0) / state.TILE; + const kinds = [Zombie, Pig, Chicken, Creeper, Skeleton, Scorpion, PolarBear, Slime, Eagle]; + for (let i = 0; i < 2; i++) { + const ang = Math.random() * Math.PI * 2; + const dist = 14 + Math.random() * 12; + const mx = Math.floor(pcx + Math.cos(ang) * dist); + const mz = Math.floor(pcz + Math.sin(ang) * dist); + if (Math.abs(mz) > WORLD_DEPTH) continue; + const sy = surfaceGyAt(mx, mz) - 1; + if (sy < 1) continue; + const K = kinds[Math.floor(Math.random() * kinds.length)]; + const m = new K(mx * state.TILE, sy * state.TILE, mz * state.TILE); + m.z = mz * state.TILE; + m.d = m.d || m.w; + m.vz = 0; + state.mobs.push(m); + } +} diff --git a/srcv3/game/modes.js b/srcv3/game/modes.js new file mode 100644 index 0000000..90d09f9 --- /dev/null +++ b/srcv3/game/modes.js @@ -0,0 +1,20 @@ +// ==================== РЕЖИМЫ ==================== +import { state } from '../core/state.js'; +import { playSound } from '../audio/sound-engine.js'; + +const MODES = [{ id: 'mine', icon: '⛏️' }, { id: 'build', icon: '🧱' }]; + +export { MODES }; + +export function mode() { + return MODES[state.modeIdx].id; +} + +export function initModes() { + const modeBtn = document.getElementById('modeBtn'); + modeBtn.onclick = () => { + playSound('click'); // Звук клика по кнопке режима + state.modeIdx = (state.modeIdx + 1) % MODES.length; + modeBtn.textContent = MODES[state.modeIdx].icon; + }; +} \ No newline at end of file diff --git a/srcv3/game/save.js b/srcv3/game/save.js new file mode 100644 index 0000000..5be5ad3 --- /dev/null +++ b/srcv3/game/save.js @@ -0,0 +1,198 @@ +// ==================== СИСТЕМА СОХРАНЕНИЯ ИГРЫ ==================== +import { state } from '../core/state.js'; +import { getBlock, setBlock, removeBlock } from '../world/world-storage3d.js'; +import { resetGenerationCache, ensureGenAroundCamera } from '../world/generation3d.js'; +import { rebuildHotbar } from '../ui/hotbar.js'; +import { renderInventory } from '../ui/inventory.js'; + +const SAVE_STORAGE_KEY = state.SAVE_KEY; +let db = null; // Оставляем для совместимости, но не используем + +// Инициализация (localStorage + in-memory fallback) +export function initDB() { + return new Promise((resolve) => { + console.log('Используем localStorage для сохранений (sandbox режим)'); + resolve(null); + }); +} + +export function saveGame() { + const saveData = { + version: 2, + worldSeed: state.worldSeed, + player: { + x: state.player.x, + y: state.player.y, + hp: state.player.hp, + hunger: state.player.hunger, + o2: state.player.o2 + }, + inventory: state.inv, + time: state.worldTime, + isNight: state.isNightTime, + dayNightCycle: state.dayNightCycle, + // Сохраняем только изменения + placedBlocks: state.placedBlocks.slice(), + removedBlocks: state.removedBlocks.slice() + }; + + const saveSize = JSON.stringify(saveData).length; + console.log('Сохранение: player HP:', state.player.hp, 'hunger:', state.player.hunger, 'o2:', state.player.o2); + + // Пробуем сохранить в localStorage (основной метод для персистентности) + try { + localStorage.setItem(SAVE_STORAGE_KEY, JSON.stringify(saveData)); + console.log(`Игра сохранена в localStorage (размер: ${saveSize} байт)`); + } catch (e) { + console.warn('Ошибка сохранения в localStorage, используем только in-memory:', e); + + // Если localStorage недоступен, используем in-memory fallback + state.inMemorySave = saveData; + console.log(`Игра сохранена в памяти (размер: ${saveSize} байт)`); + } +} + +export function loadGame() { + return new Promise((resolve, reject) => { + // Пробуем localStorage + try { + const localSave = localStorage.getItem(SAVE_STORAGE_KEY); + if (localSave) { + const parsed = JSON.parse(localSave); + console.log('Загружено из localStorage, player HP:', parsed.player?.hp); + resolve(parsed); + return; + } + } catch (e) { + console.warn('Ошибка доступа к localStorage:', e); + } + + // Если localStorage недоступен, используем in-memory сохранение + if (state.inMemorySave) { + console.log('Загружено из in-memory сохранения, player HP:', state.inMemorySave.player?.hp); + resolve(state.inMemorySave); + return; + } + + console.log('Сохранение не найдено'); + resolve(null); + }); +} + +// Миграция с версии 1 на версию 2 +export function migrateV1toV2(saveData) { + console.log('Миграция сохранения с версии 1 на версию 2...'); + + // Сохраняем seed из текущей игры (так как v1 его не хранил) + saveData.worldSeed = state.worldSeed; + + // Инициализируем массивы изменений + saveData.placedBlocks = []; + saveData.removedBlocks = []; + + // Для v1 нам нужно сравнить сгенерированные блоки с тем, что должно быть по seed + // Это сложно сделать без полной перегенерации, поэтому для v1 сохраняем только seed + // и при загрузке просто перегенерируем мир + + // Удаляем старые данные + delete saveData.generatedBlocks; + + saveData.version = 2; + console.log('Миграция завершена'); +} + +export async function applySave(saveData) { + if (!saveData) return; + + console.log('=== applySave START ==='); + console.log('player HP before applySave:', state.player.hp); + console.log('saveData.player.hp:', saveData.player?.hp); + + // Миграция версий + if (saveData.version === 1) { + migrateV1toV2(saveData); + } + + // Восстанавливаем seed + if (saveData.worldSeed !== undefined) { + state.worldSeed = saveData.worldSeed; + } + + // Восстанавливаем игрока + if (saveData.player) { + state.player.x = saveData.player.x; + state.player.y = saveData.player.y; + state.player.hunger = saveData.player.hunger; + state.player.o2 = saveData.player.o2; + + // Обновляем spawnPoint на позицию из сохранения + state.spawnPoint.x = state.player.x; + state.spawnPoint.y = state.player.y; + + // Проверяем HP из сохранения - если <= 0, устанавливаем 100 + const savedHP = saveData.player.hp; + console.log('Saved HP from file:', savedHP); + if (savedHP <= 0) { + console.log('WARNING: Saved HP is <= 0, setting to 100!'); + state.player.hp = 100; + } else { + state.player.hp = savedHP; + } + console.log('player HP after restore:', state.player.hp); + console.log('spawnPoint обновлён из сохранения: x=', state.spawnPoint.x, 'y=', state.spawnPoint.y); + } else { + console.log('No player data in save, setting default HP: 100'); + state.player.hp = 100; + } + + console.log('=== applySave END ==='); + + // Восстанавливаем инвентарь + if (saveData.inventory) { + for (const key in saveData.inventory) { + state.inv[key] = saveData.inventory[key]; + } + } + + // Восстанавливаем время + if (saveData.time !== undefined) { + state.worldTime = saveData.time; + } + + // Восстанавливаем день/ночь + if (saveData.isNight !== undefined) { + state.isNightTime = saveData.isNight; + } + if (saveData.dayNightCycle !== undefined) { + state.dayNightCycle = saveData.dayNightCycle; + } + + // Перегенерируем мир по seed + resetGenerationCache(); + ensureGenAroundCamera(); + + // Применяем изменения (только для v2) + if (saveData.version === 2) { + // Применяем блоки, установленные игроком + for (const block of saveData.placedBlocks) { + setBlock(block.gx, block.gy, block.gz || 0, block.t, true); + } + + // Применяем удалённые блоки + for (const block of saveData.removedBlocks) { + removeBlock(block.gx, block.gy, block.gz || 0); + } + + // Восстанавливаем массивы изменений + state.placedBlocks = saveData.placedBlocks || []; + state.removedBlocks = saveData.removedBlocks || []; + } + + // Fill recentItems from inventory if empty (hotbar won.t render otherwise) + if (state.recentItems.length === 0) { + state.recentItems = Object.keys(state.inv).filter(id => state.inv[id] > 0).slice(0, 5); + } + rebuildHotbar(); + renderInventory(); + console.log('Игра загружена'); +} \ No newline at end of file diff --git a/srcv3/input/controls.js b/srcv3/input/controls.js new file mode 100644 index 0000000..c9a9614 --- /dev/null +++ b/srcv3/input/controls.js @@ -0,0 +1,37 @@ +// ==================== УПРАВЛЕНИЕ ==================== +import { state } from '../core/state.js'; + +export const inp = state.inp; + +export function bindHold(el, key) { + const down = (e) => { e.preventDefault(); state.inp[key] = true; }; + const up = (e) => { e.preventDefault(); state.inp[key] = false; }; + el.addEventListener('pointerdown', down); + el.addEventListener('pointerup', up); + el.addEventListener('pointerleave', up); +} + +export function initControls() { + const leftBtn = document.getElementById('left'); + const rightBtn = document.getElementById('right'); + const jumpBtn = document.getElementById('jump'); + const downBtn = document.getElementById('down'); + + if (leftBtn) bindHold(leftBtn, 'l'); + if (rightBtn) bindHold(rightBtn, 'r'); + if (jumpBtn) bindHold(jumpBtn, 'j'); + if (downBtn) bindHold(downBtn, 's'); + + window.addEventListener('keydown', (e) => { + if (e.code === 'KeyA' || e.code === 'ArrowLeft') state.inp.l = true; + if (e.code === 'KeyD' || e.code === 'ArrowRight') state.inp.r = true; + if (e.code === 'Space' || e.code === 'KeyW' || e.code === 'ArrowUp') state.inp.j = true; + if (e.code === 'KeyS' || e.code === 'ArrowDown') state.inp.s = true; + }); + window.addEventListener('keyup', (e) => { + if (e.code === 'KeyA' || e.code === 'ArrowLeft') state.inp.l = false; + if (e.code === 'KeyD' || e.code === 'ArrowRight') state.inp.r = false; + if (e.code === 'Space' || e.code === 'KeyW' || e.code === 'ArrowUp') state.inp.j = false; + if (e.code === 'KeyS' || e.code === 'ArrowDown') state.inp.s = false; + }); +} \ No newline at end of file diff --git a/srcv3/input/mouse-handler3d.js b/srcv3/input/mouse-handler3d.js new file mode 100644 index 0000000..155a25c --- /dev/null +++ b/srcv3/input/mouse-handler3d.js @@ -0,0 +1,414 @@ +// ==================== ВЗАИМОДЕЙСТВИЕ 3D (мышь/тап) — GrechkaCraft v3 ==================== +// Ключевое отличие от 2D: блок добывается/ставится ТОЛЬКО если игрок рядом +// (в пределах REACH). Касание экрана вдали — лишь поворот камеры. +import { state } from '../core/state.js'; +import { BLOCKS } from '../data/blocks.js'; +import { ITEMS } from '../data/items.js'; +import { TOOLS } from '../data/tools.js'; +import { getBestTool, useTool, toolTierLessThan, getMiningSpeed } from '../data/tools.js'; +import { getBlock, setBlock, removeBlock } from '../world/world-storage3d.js'; +import { playSound } from '../audio/sound-engine.js'; +import { rebuildHotbar, addToRecentItems } from '../ui/hotbar.js'; +import { mode } from '../game/modes.js'; +import { openFurnaceUI } from '../ui/furnace.js'; +import { saveGame } from '../game/save.js'; +import { activateTNT3D } from '../world/tnt3d.js'; +import { sendBlockChange } from '../multiplayer/socket-helpers.js'; +import { isNight } from '../entities/mob-ai3d.js'; + +// ---------- ДИСТАНЦИЯ ДОБЫЧИ (как в майнкрафте) ---------- +export const REACH = 5.2; // тайлов от центра игрока до центра блока +export const REACH_SQ = REACH * REACH; + +function distToBlockSq(gx, gy, gz) { + const p = state.player; + const TILE = state.TILE; + const px = (p.x + p.w / 2) / TILE; + const py = (p.y + p.h / 2) / TILE; + const pz = ((p.z || 0) + (p.d || p.w) / 2) / TILE; + const dx = gx + 0.5 - px; + const dy = gy + 0.5 - py; + const dz = (gz || 0) + 0.5 - pz; + return dx * dx + dy * dy + dz * dz; +} + +export function withinReach(gx, gy, gz) { + return distToBlockSq(gx, gy, gz) <= REACH_SQ; +} + +// Моб тоже бьётся только вблизи +function withinReachEntity(e) { + const p = state.player; + const TILE = state.TILE; + const px = (p.x + p.w / 2) / TILE; + const py = (p.y + p.h / 2) / TILE; + const pz = ((p.z || 0) + (p.d || p.w) / 2) / TILE; + const ex = (e.x + e.w / 2) / TILE; + const ey = (e.y + e.h / 2) / TILE; + const ez = ((e.z || 0) + (e.d || e.w) / 2) / TILE; + const dx = ex - px, dy = ey - py, dz = ez - pz; + return dx * dx + dy * dy + dz * dz <= REACH_SQ; +} + +const CROP_SEED_MAP = { + wheat_seeds: 'wheat_stage0', + carrot: 'carrot_stage0', + potato: 'potato_stage0', +}; +const CROP_HARVEST_MAP = { wheat_stage3: 'wheat', carrot_stage3: 'carrot', potato_stage3: 'potato' }; + +function handleBlockDrop(removed, inv) { + if (removed.t === 'grass') { + if (Math.random() < 0.5) inv.wheat_seeds = (inv.wheat_seeds || 0) + 1; + return; + } + const harvestItem = CROP_HARVEST_MAP[removed.t]; + const seedItem = CROP_SEED_MAP[removed.t]; + if (harvestItem) { + inv[harvestItem] = (inv[harvestItem] || 0) + (BLOCKS[removed.t].harvestQty || 1); + addToRecentItems(harvestItem); + if (seedItem) { inv[seedItem] = (inv[seedItem] || 0) + 1; addToRecentItems(seedItem); } + } else if (seedItem) { + inv[seedItem] = (inv[seedItem] || 0) + 1; + addToRecentItems(seedItem); + } else { + inv[removed.t] = (inv[removed.t] || 0) + 1; + addToRecentItems(removed.t); + } +} + +export const mouse = { x: null, y: null }; + +let miningHeld = false; +let miningInterval = null; + +export function stopMining() { + miningHeld = false; + if (miningInterval) { clearInterval(miningInterval); miningInterval = null; } +} + +// Основное действие. coords — результат рейкаста; face — нормаль грани. +export function handleTap(wx, wy, gz, face) { + if (state.craftOpen) return; + if (state.player.hp <= 0) return; + + const TILE = state.TILE; + // gz приходит из рейкаста; если нет — берём Z игрока + const z = gz === undefined || gz === null ? Math.floor((state.player.z || 0) / TILE) : gz; + + // Ближайший блок к точке касания + let gx = Math.floor(wx / TILE); + let gy = Math.floor(wy / TILE); + let b = getBlock(gx, gy, z); + + // Если блока нет — пробуем тот же столбец, но сдвинутые на 1 (декоративные/плоские) + if (!b) { + for (const dy of [-1, 1, 0]) { + const cand = getBlock(gx, gy + dy, z); + if (cand) { gy += dy; b = cand; break; } + } + } + + if (!b) return; + + // ---------- ДОСТИЖИМОСТЬ ---------- + if (!withinReach(gx, gy, z)) { + // Слишком далеко: подсказка, ничего не ломаем + showReachHint(); + return; + } + + // Пробуждение: клик по кровати когда спишь + if (state.player.sleeping && b.t === 'bed') { + state.player.sleeping = false; + return; + } + if (state.player.sleeping) return; + + // Печь — открываем панель обжига + if (b.t === 'furnace' && mode() === 'mine') { + openFurnaceUI(gx, gy, z); + return; + } + + // ---------- Урон мобам (только вблизи) ---------- + if (mode() === 'mine') { + let handled = false; + if (state.isMultiplayer) { + for (const [id, sm] of state.serverMobs) { + if (sm.dead) continue; + if (!withinReachEntity(sm)) continue; + if (state.player.invuln <= 0) { + let dmg = 1; + for (const st of ['iron_sword', 'stone_sword', 'wood_sword']) { + if (state.inv[st] > 0) { dmg = TOOLS[st].damage || 3; useTool(st); break; } + } + state.socket.emit('mob_hurt', { id, dmg }); + playSound('attack'); + handled = true; + } + break; + } + } + if (!handled) { + for (let i = state.mobs.length - 1; i >= 0; i--) { + const m = state.mobs[i]; + if (!withinReachEntity(m)) continue; + let dmg = 1; + for (const st of ['iron_sword', 'stone_sword', 'wood_sword']) { + if (state.inv[st] > 0) { dmg = TOOLS[st].damage || 3; useTool(st); break; } + } + m.hp -= dmg; + const kx = (state.player.x + state.player.w / 2) - (m.x + m.w / 2); + const kz = ((state.player.z || 0) + (state.player.d || state.player.w) / 2) - ((m.z || 0) + (m.d || m.w) / 2); + m.vx += (kx / (Math.hypot(kx, kz) || 1)) * 400; + m.vz += (kz / (Math.hypot(kx, kz) || 1)) * 400; + m.vy -= 200; + playSound('attack'); + if (m.hp <= 0) { + if (m.kind === 'chicken') playSound('hurt_chicken'); + state.inv.meat += (m.kind === 'chicken' ? 1 : 2); + if (m.kind === 'skeleton') { + state.inv.arrow += 2 + Math.floor(Math.random() * 3); + if (Math.random() < 0.15) state.inv.bow = (state.inv.bow || 0) + 1; + } + state.mobs.splice(i, 1); + rebuildHotbar(); + } + handled = true; + break; + } + } + if (handled) return; + } + + // ---------- Лук ---------- + if (state.selected === 'bow' && state.inv.bow > 0 && state.inv.arrow > 0) { + const p = state.player; + const ox = p.x + p.w / 2, oy = p.y + p.h / 3, oz = (p.z || 0) + (p.d || p.w) / 2; + const tx = wx, ty = wy, tz = (z + 0.5) * TILE; + const dx = tx - ox, dy = ty - oy, dz = tz - oz; + const dist = Math.hypot(dx, dy, dz) || 1; + const speed = 550; + state.projectiles.push({ + x: ox, y: oy, z: oz, + vx: dx / dist * speed, vy: dy / dist * speed, vz: dz / dist * speed, + dmg: 10, owner: 'player', life: 4, + }); + state.inv.arrow--; + useTool('bow'); + playSound('hit1'); + rebuildHotbar(); + return; + } + + // ---------- Еда ---------- + if (ITEMS[state.selected] && state.inv[state.selected] > 0) { + const it = ITEMS[state.selected]; + if (state.player.hp < 100 || state.player.hunger < 100) { + playSound('eat1'); + state.player.hunger = Math.min(100, state.player.hunger + it.food); + state.player.hp = Math.min(100, state.player.hp + 15); + state.inv[state.selected]--; + rebuildHotbar(); + } + return; + } + + // ---------- Жарка на костре ---------- + if (b.t === 'campfire' && state.selected === 'meat' && state.inv.meat > 0) { + playSound('fire'); + state.inv.meat--; + state.inv.cooked = (state.inv.cooked || 0) + 1; + rebuildHotbar(); + return; + } + + // ---------- Сон ---------- + if (b.t === 'bed' && isNight()) { + state.player.sleeping = true; + saveGame(); + return; + } + + // ---------- ДОБЫЧА ---------- + if (mode() === 'mine') { + const def = BLOCKS[b.t]; + if (def.fluid) return; + if (def.decor && def.growthStage === undefined && !def.harvestable) return; + + if (b.t === 'tnt') { activateTNT3D(b, 3.2); return; } + + const hardness = def.hardness || 1; + if (hardness === Infinity) return; + + if (hardness === 0) { + const removed = removeBlock(gx, gy, z); + if (removed) { + handleBlockDrop(removed, state.inv); + sendBlockChange(gx, gy, z, removed.t, 'remove'); + rebuildHotbar(); + } + return; + } + + if (state.mining.gx === gx && state.mining.gy === gy && (state.mining.gz || 0) === z) { + state.mining.progress += getMiningSpeed(b.t); + } else { + state.mining = { gx, gy, gz: z, progress: getMiningSpeed(b.t), hardness }; + } + + if (b.t === 'glass') playSound('glass1'); + else if (b.t === 'sand') playSound('sand1'); + else if (b.t === 'stone' || b.t.endsWith('_ore')) playSound('stone1'); + else if (b.t === 'wood' || b.t === 'planks') playSound('wood1'); + else playSound('cloth1'); + + if (state.mining.progress >= hardness) { + finishMining(gx, gy, z, b.t); + } else { + startHoldMining(gx, gy, z, b.t); + } + return; + } + + // ---------- СТРОИТЕЛЬСТВО ---------- + if (mode() === 'build') { + // посев на грядку + const seedToCrop = CROP_SEED_MAP[state.selected]; + if (seedToCrop && state.inv[state.selected] > 0) { + if (b.t === 'farmland' && !getBlock(gx, gy - 1, z)) { + setBlock(gx, gy - 1, z, seedToCrop, true); + state.inv[state.selected]--; + rebuildHotbar(); + return; + } + if (!b) { + const below = getBlock(gx, gy + 1, z); + if (below && below.t === 'farmland') { + setBlock(gx, gy, z, seedToCrop, true); + state.inv[state.selected]--; + rebuildHotbar(); + return; + } + } + } + + // цель — соседняя клетка по нормали грани + let tx2 = gx, ty2 = gy, tz2 = z; + if (face) { + if (Math.abs(face.x) >= Math.abs(face.y) && Math.abs(face.x) >= Math.abs(face.z)) tx2 += Math.sign(face.x); + else if (Math.abs(face.y) >= Math.abs(face.z)) ty2 += Math.sign(face.y); + else tz2 += Math.sign(face.z); + } else { + ty2 -= 1; + } + // по Y: в игре +y вниз, three +y вверх → нормаль -y = вниз = +gy + if (face && Math.abs(face.y) >= Math.abs(face.x) && Math.abs(face.y) >= Math.abs(face.z)) { + ty2 = gy - Math.sign(face.y); + } + + if (getBlock(tx2, ty2, tz2)) return; + if (state.inv[state.selected] <= 0) return; + if (!BLOCKS[state.selected]) return; + + // не ставим в себя + const p = state.player; + const bx = tx2 * TILE, by = ty2 * TILE, bz = tz2 * TILE; + const overlap = !(bx >= p.x + p.w || bx + TILE <= p.x || + by >= p.y + p.h || by + TILE <= p.y || + bz >= (p.z || 0) + (p.d || p.w) || bz + TILE <= (p.z || 0)); + if (overlap) return; + + setBlock(tx2, ty2, tz2, state.selected, true); + state.inv[state.selected]--; + sendBlockChange(tx2, ty2, tz2, state.selected, 'set'); + + if (state.selected === 'stone' || state.selected === 'brick') playSound('stone_build'); + else if (state.selected === 'wood' || state.selected === 'planks') playSound('wood_build'); + else if (state.selected === 'glass') playSound('glass1'); + else if (state.selected === 'sand') playSound('sand1'); + else playSound('cloth1'); + + rebuildHotbar(); + } +} + +function finishMining(gx, gy, gz, type) { + const def = BLOCKS[type]; + const minTool = def.minTool; + let drops = true; + if (minTool) { + const toolId = getBestTool(type); + if (toolId) { + const tool = TOOLS[toolId]; + if (toolTierLessThan(tool.toolTier, minTool)) drops = false; + else useTool(toolId); + } else { + drops = false; + } + } else { + const toolId = getBestTool(type); + if (toolId) useTool(toolId); + } + + const removed = removeBlock(gx, gy, gz); + if (removed) { + if (drops) { + handleBlockDrop(removed, state.inv); + } else if (def.toolType === 'pickaxe') { + state.inv.cobblestone = (state.inv.cobblestone || 0) + 1; + } + sendBlockChange(gx, gy, gz, removed.t, 'remove'); + playSound('stone1'); + rebuildHotbar(); + } + stopMining(); + state.mining = { gx: null, gy: null, gz: null, progress: 0, hardness: 0 }; +} + +function startHoldMining(gx, gy, gz, type) { + miningHeld = true; + if (miningInterval) clearInterval(miningInterval); + miningInterval = setInterval(() => { + if (!miningHeld) { clearInterval(miningInterval); miningInterval = null; return; } + const mg = state.mining; + if (mg.gx === null) { miningHeld = false; clearInterval(miningInterval); miningInterval = null; return; } + const mb = getBlock(mg.gx, mg.gy, mg.gz || 0); + if (!mb || mb.dead || mb.t !== type) { + miningHeld = false; + clearInterval(miningInterval); + miningInterval = null; + return; + } + if (!withinReach(mg.gx, mg.gy, mg.gz || 0)) { + // отошли — добыча прекращается + miningHeld = false; + clearInterval(miningInterval); + miningInterval = null; + state.mining = { gx: null, gy: null, gz: null, progress: 0, hardness: 0 }; + return; + } + mg.progress += getMiningSpeed(mb.t) * 0.3; + if (mg.progress >= mg.hardness) { + finishMining(mg.gx, mg.gy, mg.gz || 0, mb.t); + } + }, 100); +} + +// Подсказка «слишком далеко» — один элемент, не спамит +let hintEl = null; +let hintTimer = null; +function showReachHint() { + if (!hintEl) { + hintEl = document.createElement('div'); + hintEl.style.cssText = 'position:fixed;left:50%;top:18%;transform:translateX(-50%);' + + 'z-index:300;background:rgba(0,0,0,.72);color:#fff;padding:8px 14px;border-radius:8px;' + + 'font:600 14px system-ui;pointer-events:none;'; + document.body.appendChild(hintEl); + } + hintEl.textContent = 'Слишком далеко — подойди ближе'; + hintEl.style.display = 'block'; + clearTimeout(hintTimer); + hintTimer = setTimeout(() => { if (hintEl) hintEl.style.display = 'none'; }, 1200); +} diff --git a/srcv3/main3d.js b/srcv3/main3d.js new file mode 100644 index 0000000..77cac06 --- /dev/null +++ b/srcv3/main3d.js @@ -0,0 +1,275 @@ +// ==================== ENTRY POINT ==================== +import { state } from './core/state.js'; +import { SERVER_URL } from './config.js'; +import { showStartMenu, isTgWebApp, tgUser } from './ui/start-menu.js'; +import { initCanvas } from './core/canvas.js'; +import { initSocket } from './multiplayer/socket.js'; +import { loadSound, playSound } from './audio/sound-engine.js'; +import { initTextures, tex } from './render/textures.js'; +import { getBlock, setBlock, removeBlock } from './world/world-storage3d.js'; +import { genColumn, surfaceGyAt, resetGenerationCache } from './world/generation3d.js'; +import { initDB, loadGame, applySave, saveGame } from './game/save.js'; +import { loop } from './game/loop3d.js'; +import { initChat } from './ui/chat.js'; +import { initFurnace } from './ui/furnace.js'; +import { initMinimap } from './ui/minimap.js'; +import { initSaveControls, updateSaveButtonVisibility } from './ui/save-controls.js'; +import { initMarket } from './ui/market.js'; +import { initSettings } from './ui/settings.js'; +import { initRespawn } from './ui/respawn.js'; +import { initShare } from './ui/share.js'; +import { initControls } from './input/controls.js'; +import { initModes } from './game/modes.js'; +import { initVoice } from './multiplayer/voice-chat.js'; +import { rebuildHotbar } from './ui/hotbar.js'; +import { toggleCraft, closeCraft, toggleInventory, closeInventory } from './ui/craft.js'; + +// ==================== 1) CONFIG — parse URL ==================== +// urlParams defined in config.js +state.SERVER_URL = SERVER_URL; +state.TELEGRAM_BOT_USERNAME = 'Grechkacraft_bot'; +state.TELEGRAM_APP_SHORT_NAME = 'minegrechka'; + +// Set TG-related state +if (isTgWebApp) { + state.myTgId = tgUser?.id || null; +} + +state.heroImg = new Image(); state.heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png'; + +// ==================== 2) START MENU ==================== +// Show the start menu before initializing the game. +// The menu returns a Promise that resolves with the worldId. +async function startGame() { + const worldId = await showStartMenu(); + + // Set worldId and playerName + state.worldId = worldId; + if (!state.playerName) { + state.playerName = localStorage.getItem('minegrechka_playerName') || 'Игрок'; + localStorage.setItem('minegrechka_playerName', state.playerName); + } + console.log('[Launch] worldId:', state.worldId, 'player:', state.playerName, 'tg:', isTgWebApp); + + // Validate TG initData if in TG context + if (isTgWebApp && window.Telegram && Telegram.WebApp.initData) { + try { + const resp = await fetch(state.SERVER_URL + '/api/tg/auth', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ initData: Telegram.WebApp.initData }) + }); + const data = await resp.json(); + if (data.ok && data.tg_id) { + console.log('[TG] Authenticated, tg_id:', data.tg_id); + state.myTgId = data.tg_id; + } + } catch (e) { console.warn('[TG] Auth failed:', e); } + } + + // Update URL with world code + try { + const newUrl = new URL(window.location.href); + newUrl.searchParams.set('world', state.worldId); + if (typeof history !== 'undefined' && history.replaceState) { + history.replaceState(null, '', newUrl.toString()); + } + } catch (e) {} + + // ==================== 3) CANVAS — init from canvas module ==================== + initCanvas(); + + // ==================== 4) STATE — set DOM refs ==================== + state.hpEl = document.getElementById('hp'); + state.foodEl = document.getElementById('food'); + state.sxEl = document.getElementById('sx'); + state.syEl = document.getElementById('sy'); + state.todEl = document.getElementById('tod'); + state.worldIdEl = document.getElementById('worldId'); + state.playerCountEl = document.getElementById('playerCount'); + state.deathEl = document.getElementById('death'); + state.hotbarEl = document.getElementById('hotbar'); + state.craftPanel = document.getElementById('craftPanel'); + state.recipesEl = document.getElementById('recipes'); + state.inventoryPanel = document.getElementById('inventoryPanel'); + state.inventoryGrid = document.getElementById('inventoryGrid'); + state.marketPanel = document.getElementById('marketPanel'); + state.marketContent = document.getElementById('marketContent'); + state.tradePanel = document.getElementById('tradePanel'); + state.tradeContent = document.getElementById('tradeContent'); + state.multiplayerStatus = document.getElementById('multiplayerStatus'); + + // ==================== 5) TEXTURES — initTextures() ==================== + initTextures(); + state.tex = tex; + + // ==================== 6) CRAFT/INVENTORY BUTTONS ==================== + const craftBtn = document.getElementById('craftBtn'); + const invToggle = document.getElementById('invToggle'); + const craftClose = document.getElementById('craftClose'); + const inventoryClose = document.getElementById('inventoryClose'); + + if (craftBtn) craftBtn.onclick = () => toggleCraft(); + if (craftClose) craftClose.onclick = () => closeCraft(); + if (invToggle) invToggle.onclick = () => toggleInventory(); + if (inventoryClose) inventoryClose.onclick = () => closeInventory(); + + // ==================== 7) CONTROLS — initControls ==================== + initControls(); + + // ==================== 9) UI MODULES — init all UI ==================== + initChat(); + initFurnace(); + initMinimap(); + initSaveControls(); + initMarket(); + initRespawn(); + initShare(); + initSettings(); + initModes(); + + // ==================== 10) SOCKET — initSocket ==================== + initSocket(); + + // ==================== 11) VOICE — initVoice ==================== + initVoice(); + + // ==================== 12) SAVE — loadGame, applySave ==================== + // Ensure hotbar has items on first load + if (state.recentItems.length === 0) { + state.recentItems = Object.keys(state.inv).filter(k => state.inv[k] > 0).slice(0, 5); + } + // Make sure selected item is valid and in recentItems + if (!state.selected || !state.inv[state.selected] || state.inv[state.selected] <= 0) { + state.selected = state.recentItems[0] || 'dirt'; + } + if (!state.recentItems.includes(state.selected)) { + state.recentItems.unshift(state.selected); + state.recentItems = state.recentItems.slice(0, 5); + } + rebuildHotbar(); + + // Keyboard hotbar switching (1/2/3) + document.addEventListener('keydown', (e) => { + if (state.showFullInventory || state.craftOpen || state.inventoryOpen || state.chatOpen) return; + if (e.key >= '1' && e.key <= '3') { + const items = state.recentItems.filter(id => state.inv[id] > 0).slice(0, 3); + const idx = parseInt(e.key) - 1; + if (items[idx]) { + state.selected = items[idx]; + rebuildHotbar(); + } + } + }); + + // Scroll wheel hotbar cycling + document.addEventListener('wheel', (e) => { + if (state.showFullInventory || state.craftOpen || state.inventoryOpen || state.chatOpen) return; + const items = state.recentItems.filter(id => state.inv[id] > 0).slice(0, 3); + if (items.length === 0) return; + let idx = items.indexOf(state.selected); + if (idx < 0) idx = 0; + if (e.deltaY > 0) idx = (idx + 1) % items.length; + else idx = (idx - 1 + items.length) % items.length; + state.selected = items[idx]; + rebuildHotbar(); + e.preventDefault(); + }, { passive: false }); + + initDB().then(async () => { + const loadedSave = await loadGame(); + if (loadedSave) { + await applySave(loadedSave); + console.log('Загружено сохранение, HP:', state.player.hp); + + if (state.player.hp <= 0) { + console.log('WARNING: HP <= 0 после загрузки, возрождаемся'); + state.player.hp = 100; + state.player.hunger = 100; + state.player.o2 = 100; + state.player.x = state.spawnPoint.x; + state.player.y = state.spawnPoint.y; + state.player.vx = state.player.vy = 0; + state.player.invuln = 0; + state.player.fallStartY = state.player.y; + } + } else { + console.log('Сохранение не найдено, начинаем новую игру'); + // Fill recentItems from inventory + state.recentItems = Object.keys(state.inv).filter(k => state.inv[k] > 0).slice(0, 5); + rebuildHotbar(); + + state.player.hp = 100; + state.player.hunger = 100; + state.player.o2 = 100; + state.player.vx = state.player.vy = 0; + state.player.invuln = 0; + + // старт — на поверхности + const startGX = 6, startGZ = 0; + state.player.x = startGX * state.TILE; + state.player.z = startGZ * state.TILE; + state.player.vx = state.player.vy = state.player.vz = 0; + for (let dx = -4; dx <= 4; dx++) { + for (let dz = -20; dz <= 20; dz++) genColumn(startGX + dx, startGZ + dz); + } + const surfaceY = surfaceGyAt(startGX, startGZ); + let safeGY = surfaceY - 1; + const aboveBlock = getBlock(startGX, surfaceY - 1, startGZ); + if (aboveBlock && (aboveBlock.t === 'water' || aboveBlock.t === 'swamp_water')) { + for (let gy = state.SEA_GY - 1; gy >= 0; gy--) { + const b = getBlock(startGX, gy, startGZ); + if (!b || b.dead || b.t === 'air' || b.t === 'water') continue; + safeGY = gy - 1; + break; + } + } + state.player.y = safeGY * state.TILE; + state.player.fallStartY = state.player.y; + state.spawnPoint.x = state.player.x; + state.spawnPoint.y = state.player.y; + state.spawnPoint.z = state.player.z; + console.log('Новая 3D игра: gx=', startGX, 'gz=', startGZ, 'surfaceY=', surfaceY, 'y=', state.player.y); + } + + // Автосейв при скрытии страницы + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + saveGame(); + } + }); + + // Автосейв перед закрытием страницы + window.addEventListener('beforeunload', () => { + saveGame(); + }); + }).catch(err => { + console.error('Ошибка инициализации:', err); + const startGX = 6, startGZ = 0; + genColumn(startGX, startGZ); + state.player.y = (surfaceGyAt(startGX, startGZ) - 1) * state.TILE; + state.player.fallStartY = state.player.y; + for (let gx = startGX - 4; gx <= startGX + 4; gx++) { + for (let gz = -20; gz <= 20; gz++) genColumn(gx, startGZ + gz); + } + }); + + // ==================== 13) LOOP — startLoop ==================== + requestAnimationFrame(loop); +} + +// Start the game (v3 — 3D) +startGame().catch(err => { + console.error('Fatal error starting game:', err); +}); + +// Debug: give items +window.debugGive = function(id, count) { + state.inv[id] = (state.inv[id] || 0) + (count || 1); + if (!state.recentItems.includes(id)) { state.recentItems.unshift(id); state.recentItems = state.recentItems.slice(0, 5); } + rebuildHotbar(); + // Can't call renderInventory from here, but user can click inv + console.log('Gave', count || 1, id, '=>', state.inv[id]); + console.log('inv keys>0:', Object.keys(state.inv).filter(k => state.inv[k] > 0).join(', ')); +}; +window.debugInv = function() { return Object.keys(state.inv).filter(k => state.inv[k] > 0).map(k => k + ':' + state.inv[k]); }; diff --git a/srcv3/multiplayer/socket-helpers.js b/srcv3/multiplayer/socket-helpers.js new file mode 100644 index 0000000..9a98176 --- /dev/null +++ b/srcv3/multiplayer/socket-helpers.js @@ -0,0 +1,47 @@ +// Socket helpers: цвет, отправка позиции/блоков +import { state } from '../core/state.js'; +import { SERVER_URL } from '../config.js'; +import { MOVE_SEND_INTERVAL } from '../core/constants.js'; + + +// Генерация случайного цвета для игрока на основе socket_id +export function getRandomPlayerColor(socketId) { + const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e91e63', '#00bcd4']; + let hash = 0; + for (let i = 0; i < socketId.length; i++) { + hash = ((hash << 5) - hash) + socketId.charCodeAt(i); + hash = hash & hash; + } + return colors[Math.abs(hash) % colors.length]; +} + +// Throttle для отправки позиции (10-20 раз в секунду) +let lastMoveSendTime = 0; + +let lastSentX = 0, lastSentY = 0; + +// Отправка позиции игрока (с throttle) +export function sendPlayerPosition() { + if (!state.isMultiplayer || !state.socket || !state.socket.connected) return; + + const now = performance.now() / 1000; + if (now - lastMoveSendTime < MOVE_SEND_INTERVAL) return; + + // Отправляем только если позиция изменилась + const dx = Math.abs(state.player.x - lastSentX); + const dy = Math.abs(state.player.y - lastSentY); + if (dx < 1 && dy < 1) return; + + lastMoveSendTime = now; + lastSentX = state.player.x; + lastSentY = state.player.y; + + state.socket.emit('player_move', { x: state.player.x, y: state.player.y, player_name: state.playerName }); +} + +// Отправка изменения блока +export function sendBlockChange(gx, gy, t, op) { + if (!state.isMultiplayer || !state.socket || !state.socket.connected) return; + + state.socket.emit('block_change', { gx, gy, t, op }); +} \ No newline at end of file diff --git a/srcv3/multiplayer/socket.js b/srcv3/multiplayer/socket.js new file mode 100644 index 0000000..142d617 --- /dev/null +++ b/srcv3/multiplayer/socket.js @@ -0,0 +1,283 @@ +// Socket.IO клиент — инициализация и все обработчики +import { state } from '../core/state.js'; +import { SERVER_URL } from '../config.js'; +import { TILE, SEA_GY } from '../core/constants.js'; +import { BLOCKS } from '../data/blocks.js'; +import { k3 as k, setBlock, removeBlock, grid, blocks, getBlock } from '../world/world-storage3d.js'; +import { genColumn, surfaceGyAt } from '../world/generation3d.js'; +import { addChatMessage } from '../ui/chat.js'; +import { calculateDamage } from '../entities/player.js'; +import { playSound } from '../audio/sound-engine.js'; +import { rebuildHotbar } from '../ui/hotbar.js'; +import { explodeTNT3D } from '../world/tnt3d.js'; +import { updateSaveButtonVisibility } from '../ui/save-controls.js'; +import { getRandomPlayerColor, sendPlayerPosition, sendBlockChange } from './socket-helpers.js'; +import { initTradeSocket } from '../ui/trade.js'; +// Mob sizes for server mob sync (server doesn't send w/h) +const MOB_SIZES = { + pig: { w: 34, h: 24 }, + chicken: { w: 26, h: 22 }, + zombie: { w: 34, h: 50 }, + creeper: { w: 34, h: 50 }, + skeleton: { w: 34, h: 50 }, + scorpion: { w: 26, h: 26 }, + polar_bear: { w: 40, h: 34 }, + slime: { w: 24, h: 24 }, + eagle: { w: 30, h: 22 }, +}; + +function applyMobSize(sm) { + const s = MOB_SIZES[sm.kind]; + if (s && !sm.w) { sm.w = s.w; sm.h = s.h; } + return sm; +} + +let socket = null; + +export { socket }; + +export function initSocket() { + try { + socket = io(SERVER_URL, { + path: '/socket.io/', + transports: ['websocket', 'polling'] + }); + + // Store socket on state for other modules + state.socket = socket; + + socket.on('connect', () => { + console.log(`Connected to server: ${SERVER_URL}, socket.id: ${socket.id}, worldId: ${state.worldId}`); + state.mySocketId = socket.id; + state.isMultiplayer = true; + + // Присоединяемся к миру + socket.emit('join_world', { world_id: state.worldId, player_name: state.playerName }); + + // Показываем в UI + if (state.worldIdEl) state.worldIdEl.textContent = state.worldId; + if (state.multiplayerStatus) state.multiplayerStatus.style.display = 'block'; + + // Initialize trade socket handlers + initTradeSocket(socket); + }); + + socket.on('connect_error', (error) => { + console.error('Socket connection error:', error); + state.isMultiplayer = false; + }); + + socket.on('disconnect', () => { + console.log('Disconnected from server'); + state.isMultiplayer = false; + state.otherPlayers.clear(); + if (state.multiplayerStatus) state.multiplayerStatus.style.display = 'none'; + }); + + // Обработка world_state + socket.on('world_state', (data) => { + console.log('Received world_state:', data); + + if (data.seed !== undefined && data.seed !== state.worldSeed) { + const oldSeed = state.worldSeed; + state.worldSeed = data.seed; + console.log('World seed changed from', oldSeed, 'to', state.worldSeed); + + state.generated.clear(); + grid.clear(); + blocks.length = 0; + state.placedBlocks = []; + state.removedBlocks = []; + console.log('World regenerated with new seed:', state.worldSeed); + } + + if (data.blocks && Array.isArray(data.blocks)) { + for (const block of data.blocks) { + const key = k(block.gx, block.gy, block.gz || 0); + state.serverOverrides.set(key, { op: block.op, t: block.t }); + if (block.op === 'set') { + setBlock(block.gx, block.gy, block.gz || 0, block.t, false); + } else if (block.op === 'remove') { + removeBlock(block.gx, block.gy, block.gz || 0); + } + } + } + + if (data.time !== undefined) { + state.worldTime = data.time; + state.isNightTime = state.worldTime > 0.5; + } + + // Spawn point + { + const startGX = 6; + for (let dx = -4; dx <= 4; dx++) for (let dz = -24; dz <= 24; dz++) genColumn(startGX + dx, dz); + const surfaceY = surfaceGyAt(startGX, Math.floor((state.player.z || 0) / state.TILE)); + let safeGY = surfaceY - 1; + const aboveBlock = getBlock(startGX, surfaceY - 1); + if (aboveBlock && aboveBlock.t === 'water') { + for (let gy = SEA_GY - 1; gy >= 0; gy--) { + const b = getBlock(startGX, gy, Math.floor((state.player.z || 0) / state.TILE)); + if (!b || b.dead || b.t === 'air' || b.t === 'water') continue; + safeGY = gy - 1; + break; + } + } + state.spawnPoint.x = startGX * TILE; + state.spawnPoint.y = safeGY * TILE; + console.log('Client-side spawn point:', state.spawnPoint, 'surfaceY:', surfaceY, 'safeGY:', safeGY); + } + + state.player.x = state.spawnPoint.x; + state.player.y = state.spawnPoint.y; + state.player.vx = 0; + state.player.vy = 0; + state.player.fallStartY = state.player.y; + console.log('Player moved to spawn point:', state.player.x, state.player.y); + + state.player.hp = 100; + state.player.hunger = 100; + state.player.o2 = 100; + state.player.invuln = 0; + console.log('[MULTIPLAYER CONNECT] Player HP set to 100% on connect'); + + if (data.players && Array.isArray(data.players)) { + state.otherPlayers.clear(); + for (const p of data.players) { + if (p.socket_id !== state.mySocketId) { + state.otherPlayers.set(p.socket_id, { + x: p.x, + y: p.y, + color: getRandomPlayerColor(p.socket_id), + name: p.player_name || 'Игрок' + }); + } + } + state.playerCountEl.textContent = data.players.length; + } + // Server mobs + if (data.mobs && Array.isArray(data.mobs)) { + state.serverMobs.clear(); + for (const m of data.mobs) { + let sm = { ...m, maxHp: m.maxHp || m.hp, vx: m.vx || 0, vy: m.vy || 0, grounded: false, inWater: false, aiT: 0, dir: m.dir || 1, dead: false, fuse: m.fuse || 0, shootCooldown: 2, speed: m.speed || 80 }; + applyMobSize(sm); + state.serverMobs.set(m.id, sm); + } + } + + // Set myTgId from server if provided + if (data.tg_id) state.myTgId = data.tg_id; + }); + + socket.on('player_joined', (data) => { + console.log('Player joined:', data.socket_id); + if (data.socket_id !== state.mySocketId) { + const spawnGX = 6; + genColumn(spawnGX, Math.floor((state.player.z || 0) / state.TILE)); + const surfaceY = surfaceGyAt(spawnGX, Math.floor((state.player.z || 0) / state.TILE)); + const safeSpawnX = spawnGX * TILE; + const safeSpawnY = (surfaceY - 1) * TILE; + + state.otherPlayers.set(data.socket_id, { + x: safeSpawnX, + y: safeSpawnY, + color: getRandomPlayerColor(data.socket_id), + name: data.player_name || 'Игрок' + }); + addChatMessage('Система', `Игрок присоединился`); + updateSaveButtonVisibility(); + } + }); + + socket.on('player_moved', (data) => { + if (data.socket_id !== state.mySocketId && state.otherPlayers.has(data.socket_id)) { + const p = state.otherPlayers.get(data.socket_id); + p.x = data.x; + p.y = data.y; + if (data.player_name) { + p.name = data.player_name; + } + } + }); + + socket.on('player_left', (data) => { + console.log('Player left:', data.socket_id); + state.otherPlayers.delete(data.socket_id); + addChatMessage('Система', `Игрок покинул игру`); + updateSaveButtonVisibility(); + }); + + // === MOB SYNC === + socket.on('mob_spawned', (data) => { + let sm = { ...data, maxHp: data.maxHp || data.hp, vx: data.vx || 0, vy: data.vy || 0, grounded: false, inWater: false, aiT: 0, dir: data.dir || 1, dead: false, fuse: data.fuse || 0, shootCooldown: 2, speed: data.speed || 80 }; + applyMobSize(sm); + state.serverMobs.set(data.id, sm); + }); + + socket.on('mob_positions', (arr) => { + for (const u of arr) { + const sm = state.serverMobs.get(u.id); + if (sm) { sm.x = u.x; sm.y = u.y; sm.vx = u.vx; sm.vy = u.vy; sm.dir = u.dir; sm.hp = u.hp; sm.fuse = u.fuse || 0; } + } + }); + + socket.on('mob_despawned', (data) => { state.serverMobs.delete(data.id); }); + + socket.on('mob_died', (data) => { + const sm = state.serverMobs.get(data.id); + if (sm && data.killer === state.mySocketId) { + if (sm.kind === 'chicken') playSound('hurt_chicken'); + state.inv.meat += (sm.kind === 'chicken' ? 1 : 2); + if (sm.kind === 'skeleton') { + state.inv.arrow += 2 + Math.floor(Math.random() * 3); + if (Math.random() < 0.15) state.inv.bow = (state.inv.bow || 0) + 1; + } + rebuildHotbar(); + } + state.serverMobs.delete(data.id); + }); + + socket.on('mob_hurt_ack', (data) => { + const sm = state.serverMobs.get(data.id); + if (sm) sm.hp = data.hp; + }); + + socket.on('mob_explode', (data) => { + explodeTNT3D({ gx: data.gx, gy: data.gy, gz: data.gz || 0 }, state); + state.serverMobs.delete(data.id); + }); + + socket.on('mob_shoot', (data) => { + state.projectiles.push({ + x: data.x, y: data.y, vx: data.vx, vy: data.vy, + dmg: data.dmg, owner: 'mob', life: data.life + }); + }); + + socket.on('block_changed', (data) => { + const key = k(data.gx, data.gy, data.gz || 0); + state.serverOverrides.set(key, { op: data.op, t: data.t }); + if (data.op === 'set') { + setBlock(data.gx, data.gy, data.gz || 0, data.t, false); + } else if (data.op === 'remove') { + removeBlock(data.gx, data.gy, data.gz || 0); + } + }); + + socket.on('chat_message', (data) => { + const senderName = data.socket_id === state.mySocketId ? 'Вы' : `Игрок ${data.socket_id.substring(0, 6)}`; + addChatMessage(senderName, data.message); + }); + + socket.on('time_update', (data) => { + if (data.time !== undefined) { + state.worldTime = data.time; + state.isNightTime = state.worldTime > 0.5; + } + }); + + } catch (e) { + console.error('Error initializing socket:', e); + state.isMultiplayer = false; + } +} diff --git a/srcv3/multiplayer/voice-chat.js b/srcv3/multiplayer/voice-chat.js new file mode 100644 index 0000000..17fbc98 --- /dev/null +++ b/srcv3/multiplayer/voice-chat.js @@ -0,0 +1,122 @@ +// Голосовой чат +import { state } from '../core/state.js'; +import { worldId } from '../config.js'; +import { VOICE_SERVER } from '../core/constants.js'; + + +let voiceSocket = null; +let voiceStream = null; +let audioCtx = null; +let voiceProcessor = null; +let voiceActive = false; + +// Кнопка микрофона +const voiceBtn = document.createElement('div'); +voiceBtn.innerHTML = '🎤/'; +voiceBtn.title = 'Голосовой чат (выкл)'; +voiceBtn.style.cssText = 'position:absolute;top:10px;right:130px;width:52px;height:52px;border-radius:12px;background:#555;z-index:200;display:flex;align-items:center;justify-content:center;font-size:24px;cursor:pointer;border:2px solid rgba(255,255,255,0.9);box-shadow:0 4px 0 rgba(0,0,0,0.5);pointer-events:auto;'; +document.querySelector('.ui').appendChild(voiceBtn); + +// Индикатор говорящего +const speakingIndicator = document.createElement('div'); +speakingIndicator.style.cssText = 'position:absolute;top:130px;right:10px;z-index:200;display:none;background:rgba(0,0,0,0.7);color:#2ecc71;padding:4px 8px;border-radius:6px;font-size:12px;'; +speakingIndicator.textContent = '🔊'; +document.querySelector('.ui').appendChild(speakingIndicator); +let speakingTimeout = null; + +voiceBtn.onclick = async () => { + if (voiceActive) { + // Выключить + voiceActive = false; + voiceBtn.innerHTML = '🎤/'; + voiceBtn.style.background = '#555'; + if (voiceStream) { + voiceStream.getTracks().forEach(t => t.stop()); + voiceStream = null; + } + if (voiceProcessor) { voiceProcessor.disconnect(); voiceProcessor = null; } + if (audioCtx) { audioCtx.close(); audioCtx = null; } + if (voiceSocket) { voiceSocket.disconnect(); voiceSocket = null; } + return; + } + + // Включить + try { + voiceStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, sampleRate: 24000 } }); + audioCtx = new AudioContext({ sampleRate: 24000 }); + const source = audioCtx.createMediaStreamSource(voiceStream); + voiceProcessor = audioCtx.createScriptProcessor(4096, 1, 1); + + voiceProcessor.onaudioprocess = (e) => { + if (!voiceActive || !voiceSocket || !voiceSocket.connected) return; + const pcm = e.inputBuffer.getChannelData(0); + // Конвертируем float32 → int16 для экономии трафика + const int16 = new Int16Array(pcm.length); + for (let i = 0; i < pcm.length; i++) { + const s = Math.max(-1, Math.min(1, pcm[i])); + int16[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; + } + voiceSocket.emit('voice_data', int16.buffer); + }; + + source.connect(voiceProcessor); + voiceProcessor.connect(audioCtx.createGain()); // не в destination — чтобы не слышать себя + + // Подключаемся к голосовому серверу + voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] }); + voiceSocket.on('connect', () => { + voiceSocket.emit('voice_join', { world_id: state.worldId, x: state.player.x, y: state.player.y, name: state.playerName || 'Игрок' }); + }); + + voiceSocket.on('voice_in', (payload) => { + // Воспроизводим входящий голос + const { data, meta, volume: rawVolume } = payload; + const volume = rawVolume !== undefined ? rawVolume : 1.0; + if (!audioCtx || audioCtx.state === 'closed') return; + + // Int16 → Float32 (volume applied via gain node, not in conversion) + const int16 = new Int16Array(data); + const float32 = new Float32Array(int16.length); + for (let i = 0; i < int16.length; i++) { + float32[i] = int16[i] / (int16[i] < 0 ? 0x8000 : 0x7FFF); + } + + const buf = audioCtx.createBuffer(1, float32.length, 24000); + buf.getChannelData(0).set(float32); + const src = audioCtx.createBufferSource(); + src.buffer = buf; + + const gain = audioCtx.createGain(); + gain.gain.value = volume; + src.connect(gain).connect(audioCtx.destination); + src.start(); + + // Индикатор + speakingIndicator.style.display = 'block'; + speakingIndicator.textContent = `🔊 ${meta.name}`; + clearTimeout(speakingTimeout); + speakingTimeout = setTimeout(() => { speakingIndicator.style.display = 'none'; }, 500); + }); + + voiceActive = true; + voiceBtn.textContent = '🎤'; + voiceBtn.style.background = '#2ecc71'; + } catch (e) { + console.error('Voice error:', e); + voiceBtn.style.background = '#e74c3c'; + } +}; + +export function initVoice() { + // Voice position update hook — should be called from main loop + let voicePosT = 0; + return { + update(dt) { + voicePosT += dt; + if (voicePosT > 0.5 && voiceSocket && voiceSocket.connected) { + voicePosT = 0; + voiceSocket.emit('voice_pos', { x: state.player.x, y: state.player.y }); + } + } + }; +} \ No newline at end of file diff --git a/srcv3/physics/collision3d.js b/srcv3/physics/collision3d.js new file mode 100644 index 0000000..33e4480 --- /dev/null +++ b/srcv3/physics/collision3d.js @@ -0,0 +1,146 @@ +// ==================== ФИЗИКА 3D — GrechkaCraft v3 ==================== +// Осевое разрешение столкновений по X → Z → Y, как в Minecraft. +// Автошаг на 1 блок, прыжок, лестницы, плавание, лодка. +import { TILE } from '../core/constants.js'; +import { BLOCKS } from '../data/blocks.js'; +import { state } from '../core/state.js'; +import { getBlock, isSolid } from '../world/world-storage3d.js'; + +const EPS = 0.001; + +// Собственная AABB-коробка сущности: x/z — центр, y — низ (как в 2D-игре: e.x, e.y = левый-ниний) +function boxOf(e) { + return { + x0: e.x, x1: e.x + e.w, + y0: e.y, y1: e.y + e.h, + z0: e.z, z1: e.z + e.d, + }; +} + +function solidAt(gx, gy, gz) { + const b = getBlock(gx, gy, gz); + if (!b) return false; + const def = BLOCKS[b.t]; + if (!def) return false; + if (def.fluid) return false; + if (def.decor || def.climbable) return false; + return !!def.solid; +} + +function climbableAt(gx, gy, gz) { + const b = getBlock(gx, gy, gz); + return !!(b && BLOCKS[b.t] && BLOCKS[b.t].climbable); +} + +function fluidAt(gx, gy, gz) { + const b = getBlock(gx, gy, gz); + return !!(b && BLOCKS[b.t] && BLOCKS[b.t].fluid); +} + +// Пересекает ли коробка сущности твёрдый блок +function hitsSolid(bx) { + const x0 = Math.floor(bx.x0 / TILE), x1 = Math.floor((bx.x1 - EPS) / TILE); + const y0 = Math.floor(bx.y0 / TILE), y1 = Math.floor((bx.y1 - EPS) / TILE); + const z0 = Math.floor(bx.z0 / TILE), z1 = Math.floor((bx.z1 - EPS) / TILE); + for (let gx = x0; gx <= x1; gx++) { + for (let gy = y0; gy <= y1; gy++) { + for (let gz = z0; gz <= z1; gz++) { + if (solidAt(gx, gy, gz)) return true; + } + } + } + return false; +} + +// Стоит ли сущность на чём-то (для grounded) +function checkGrounded(e) { + const bx = boxOf(e); + const gy = Math.floor((bx.y1 + 1) / TILE); + const x0 = Math.floor(bx.x0 / TILE), x1 = Math.floor((bx.x1 - EPS) / TILE); + const z0 = Math.floor(bx.z0 / TILE), z1 = Math.floor((bx.z1 - EPS) / TILE); + for (let gx = x0; gx <= x1; gx++) { + for (let gz = z0; gz <= z1; gz++) { + if (solidAt(gx, gy, gz)) return true; + } + } + return false; +} + +function onLadder(e) { + const cx = Math.floor((e.x + e.w / 2) / TILE); + const cy = Math.floor((e.y + e.h / 2) / TILE); + const cz = Math.floor((e.z + e.d / 2) / TILE); + return climbableAt(cx, cy, cz); +} + +function inFluid(e) { + const cx = Math.floor((e.x + e.w / 2) / TILE); + const cy = Math.floor((e.y + e.h * 0.4) / TILE); + const cz = Math.floor((e.z + e.d / 2) / TILE); + return fluidAt(cx, cy, cz); +} + +// Разрешение одной оси. delta — смещение за кадр. +function moveAxis(e, axis, delta) { + if (delta === 0) return false; + const bx = boxOf(e); + if (axis === 'x') { bx.x0 += delta; bx.x1 += delta; } + else if (axis === 'z') { bx.z0 += delta; bx.z1 += delta; } + else { bx.y0 += delta; bx.y1 += delta; } + + if (!hitsSolid(bx)) { + if (axis === 'x') e.x += delta; + else if (axis === 'z') e.z += delta; + else e.y += delta; + return false; + } + + const dir = Math.sign(delta); + + // Автошаг вверх на 1 блок + if ((axis === 'x' || axis === 'z') && e.grounded) { + const savedX = e.x, savedY = e.y, savedZ = e.z; + if (axis === 'x') e.x += delta; else e.z += delta; + e.y -= TILE; + if (!hitsSolid(boxOf(e))) { + return true; // шаг удался, продолжим движение + } + e.x = savedX; e.y = savedY; e.z = savedZ; + } + + // Откат к границе блока + // шаг за блок, пока не пересечение + const stepPx = dir * 0.5; + let moved = 0; + for (let i = 0; i < TILE * 4; i++) { + const test = boxOf(e); + if (axis === 'x') { test.x0 += stepPx; test.x1 += stepPx; } + else if (axis === 'z') { test.z0 += stepPx; test.z1 += stepPx; } + else { test.y0 += stepPx; test.y1 += stepPx; } + if (hitsSolid(test)) break; + moved += stepPx; + if (axis === 'x') e.x += stepPx; + else if (axis === 'z') e.z += stepPx; + else e.y += stepPx; + } + + if (axis === 'x') e.vx = 0; + else if (axis === 'z') e.vz = 0; + else e.vy = 0; + return true; +} + +export function resolveX(e, dt) { return moveAxis(e, 'x', e.vx * dt); } +export function resolveZ(e, dt) { return moveAxis(e, 'z', (e.vz || 0) * dt); } +export function resolveY(e, dt) { + const hit = moveAxis(e, 'y', e.vy * dt); + e.grounded = false; + if (hit && e.vy >= 0) { + e.grounded = checkGrounded(e); + } else if (!hit) { + e.grounded = checkGrounded(e); + } + return hit; +} + +export { boxOf, hitsSolid, checkGrounded, onLadder, inFluid, solidAt, fluidAt, climbableAt }; diff --git a/srcv3/physics/water-detect3d.js b/srcv3/physics/water-detect3d.js new file mode 100644 index 0000000..c3ce025 --- /dev/null +++ b/srcv3/physics/water-detect3d.js @@ -0,0 +1,52 @@ +// ==================== ВОДА 3D — GrechkaCraft v3 ==================== +// Та же логика 2D-игры (кислород, всплеск, болото), но с учётом Z. +import { TILE } from '../core/constants.js'; +import { state } from '../core/state.js'; +import { getBlock } from '../world/world-storage3d.js'; +import { playSound } from '../audio/sound-engine.js'; + +function blockAt3(px, py, pz) { + const gx = Math.floor(px / TILE); + const gy = Math.floor(py / TILE); + const gz = Math.floor(pz / TILE); + return getBlock(gx, gy, gz); +} + +function isWaterAt(px, py, pz) { + const b = blockAt3(px, py, pz); + return !!(b && (b.t === 'water' || b.t === 'swamp_water')); +} + +function isSwampWaterAt(px, py, pz) { + const b = blockAt3(px, py, pz); + return !!(b && b.t === 'swamp_water'); +} + +// Обновляет флаги воды на сущности (игрок или моб). +// dt передаётся явно — в 2D-версии он был неявной переменной цикла. +export function updateWaterFlag3(e, dt) { + const cx = e.x + e.w / 2; + const cz = (e.z || 0) + (e.d || e.w) / 2; + const wasInWater = e.inWater; + + const mid = isWaterAt(cx, e.y + e.h / 2, cz); + const feet = isWaterAt(cx, e.y + e.h - 2, cz); + e.inWater = mid || feet; + + e.inSwampWater = isSwampWaterAt(cx, e.y + e.h / 2, cz); + e.headInWater = isWaterAt(cx, e.y + 4, cz); + + if (e === state.player && !wasInWater && e.inWater && e.vy > 100) { + playSound('splash'); + } + if (e === state.player && e.inSwampWater) { + state.player.hp -= 3 * (typeof dt === 'number' ? dt : 0.016); + } +} + +// Лодка тонет, если под ней нет воды (с учётом Z). +export function updateWaterPhysics3(dt) { + void dt; +} + +export { isWaterAt, isSwampWaterAt }; diff --git a/srcv3/render/draw-fire.js b/srcv3/render/draw-fire.js new file mode 100644 index 0000000..7c56e97 --- /dev/null +++ b/srcv3/render/draw-fire.js @@ -0,0 +1,19 @@ +// Рисование костра: огонь поверх текстуры +export function drawFire(ctx, wx, wy, now) { + const baseX = wx; + const baseY = wy; + const flick = 6 + (Math.sin(now / 90) + 1) * 4; + ctx.fillStyle = 'rgba(255,140,0,0.85)'; + ctx.beginPath(); + ctx.moveTo(baseX + 10, baseY + 30); + ctx.lineTo(baseX + 20, baseY + 30 - flick); + ctx.lineTo(baseX + 30, baseY + 30); + ctx.fill(); + + ctx.fillStyle = 'rgba(255,230,150,0.75)'; + ctx.beginPath(); + ctx.moveTo(baseX + 14, baseY + 30); + ctx.lineTo(baseX + 20, baseY + 30 - (flick * 0.7)); + ctx.lineTo(baseX + 26, baseY + 30); + ctx.fill(); +} \ No newline at end of file diff --git a/srcv3/render/hero-rig.js b/srcv3/render/hero-rig.js new file mode 100644 index 0000000..1abd1bd --- /dev/null +++ b/srcv3/render/hero-rig.js @@ -0,0 +1,16 @@ +// ==================== ДАННЫЕ ГЕРОЯ (из платформёра grechka_game.html) ==================== +// COL/PARTS/PIVOT дословно оттуда: 107 прямоугольников, 5 частей (head/body/legs/tail/eye). +// Ноль rig = ступни; высота рисунка ~3.07 юнита при PARTS-координатах. + +export const HERO_COL = { + blue: { hex: 0x0232fe, d: 0.60 }, + white: { hex: 0xffffff, d: 0.86 }, + cyan: { hex: 0x00a1ff, d: 1.00 }, + navy: { hex: 0x001a85, d: 1.12 }, +}; + +export const HERO_PARTS = {"head":[[-0.90035,2.93184,0.40909,0.13636,"blue"],[-1.17308,2.79547,0.13636,0.13636,"blue"],[-0.62762,2.79547,0.13636,0.13636,"blue"],[-0.96853,2.65911,0.27273,0.13636,"blue"],[-0.49126,2.65911,0.13636,0.13636,"blue"],[-1.17308,2.52275,0.13636,0.13636,"blue"],[-0.62762,2.52275,0.13636,0.13636,"blue"],[0.12238,2.52275,1.09091,0.13636,"blue"],[-0.83217,2.38638,0.81818,0.13636,"blue"],[1.00874,2.38638,0.68182,0.13636,"blue"],[-0.83217,2.25002,0.27273,0.13636,"blue"],[1.41783,2.25002,0.13636,0.13636,"blue"],[-1.03671,2.11365,0.13636,0.13636,"blue"],[1.28147,2.11365,0.13636,0.13636,"blue"],[0.12238,2.38638,1.09091,0.13636,"white"],[0.32692,2.25002,2.04545,0.13636,"white"],[-0.6958,2.11365,0.54545,0.13636,"white"],[-0.15035,2.11365,0.27273,0.13636,"white"],[0.66783,2.11365,1.09091,0.13636,"white"],[-0.3549,2.11365,0.13636,0.13636,"cyan"],[0.0542,2.11365,0.13636,0.13636,"cyan"]],"body":[[-1.17308,1.97729,0.13636,0.13636,"blue"],[1.1451,1.97729,0.13636,0.13636,"blue"],[-0.96853,1.84093,0.27273,0.13636,"blue"],[1.28147,1.77275,0.13636,0.27273,"blue"],[-0.90035,1.70456,0.13636,0.13636,"blue"],[-1.1049,1.5682,0.27273,0.13636,"blue"],[-0.42308,1.5682,0.27273,0.13636,"blue"],[0.53147,1.5682,0.27273,0.13636,"blue"],[1.07692,1.5682,0.27273,0.13636,"blue"],[1.41783,1.5682,0.13636,0.13636,"blue"],[-1.30944,1.43184,0.13636,0.13636,"blue"],[1.00874,1.43184,0.13636,0.13636,"blue"],[1.28147,1.43184,0.13636,0.13636,"blue"],[-1.17308,1.15911,0.13636,0.40909,"blue"],[1.1451,1.22729,0.13636,0.27273,"blue"],[-1.4458,1.02275,0.13636,0.13636,"blue"],[1.00874,1.02275,0.13636,0.13636,"blue"],[-1.4458,0.88638,0.40909,0.13636,"blue"],[-0.96853,0.88638,0.27273,0.13636,"blue"],[-0.08217,0.88638,0.68182,0.13636,"blue"],[0.87238,0.88638,0.13636,0.13636,"blue"],[-1.51399,0.75002,0.54545,0.13636,"blue"],[-0.90035,0.75002,0.13636,0.13636,"blue"],[0.66783,0.75002,0.27273,0.13636,"blue"],[1.00874,0.75002,0.13636,0.13636,"blue"],[-0.6958,1.97729,0.81818,0.13636,"white"],[-0.01399,1.97729,0.27273,0.13636,"white"],[0.8042,1.97729,0.54545,0.13636,"white"],[-0.6958,1.77275,0.27273,0.27273,"white"],[-0.21853,1.84093,0.13636,0.13636,"white"],[0.32692,1.84093,0.13636,0.13636,"white"],[0.94056,1.77275,0.54545,0.27273,"white"],[0.0542,1.63638,0.68182,0.27273,"white"],[-0.76399,1.5682,0.40909,0.13636,"white"],[0.8042,1.5682,0.27273,0.13636,"white"],[1.28147,1.5682,0.13636,0.13636,"white"],[-1.1049,1.43184,0.27273,0.13636,"white"],[-0.01399,1.43184,1.36364,0.13636,"white"],[-0.21853,1.29547,1.5,0.13636,"white"],[0.87238,1.29547,0.40909,0.13636,"white"],[-0.90035,1.15911,0.13636,0.13636,"white"],[-0.08217,1.15911,1.22727,0.13636,"white"],[0.73601,1.15911,0.13636,0.13636,"white"],[1.00874,1.15911,0.13636,0.13636,"white"],[-1.03671,1.02275,0.13636,0.13636,"white"],[-0.01399,1.02275,1.36364,0.13636,"white"],[-0.62762,0.88638,0.40909,0.13636,"white"],[0.53147,0.88638,0.54545,0.13636,"white"],[-0.15035,0.75002,1.36364,0.13636,"white"],[0.87238,0.75002,0.13636,0.13636,"white"],[-0.21853,1.97729,0.13636,0.13636,"cyan"],[0.32692,1.97729,0.40909,0.13636,"cyan"],[0.0542,1.84093,0.40909,0.13636,"cyan"],[-0.83217,1.43184,0.27273,0.13636,"cyan"],[0.8042,1.43184,0.27273,0.13636,"cyan"],[-1.03671,1.22729,0.13636,0.27273,"cyan"],[0.59965,1.22729,0.13636,0.27273,"cyan"],[-0.76399,1.15911,0.13636,0.13636,"cyan"],[0.87238,1.15911,0.13636,0.13636,"cyan"],[-0.83217,1.02275,0.27273,0.13636,"cyan"],[0.8042,1.02275,0.27273,0.13636,"cyan"]],"tail":[[-1.71853,0.61365,0.13636,0.13636,"blue"],[-1.03671,0.61365,0.13636,0.13636,"blue"],[-0.62762,0.61365,0.40909,0.13636,"blue"],[-1.65035,0.47729,0.27273,0.13636,"blue"],[-0.90035,0.47729,0.13636,0.13636,"blue"],[-1.4458,0.34093,0.40909,0.13636,"blue"],[-0.62762,0.34093,0.40909,0.13636,"blue"],[-0.90035,0.20456,0.95455,0.13636,"blue"],[-0.90035,0.61365,0.13636,0.13636,"white"]],"legs":[[0.3951,0.61365,0.27273,0.13636,"blue"],[0.87238,0.61365,0.13636,0.13636,"blue"],[-0.55944,0.47729,0.27273,0.13636,"blue"],[0.32692,0.34093,0.13636,0.40909,"blue"],[-0.21853,0.20456,0.13636,0.13636,"blue"],[0.0542,0.20456,0.13636,0.13636,"blue"],[-0.08217,0.0682,0.95455,0.13636,"blue"],[-0.3549,0.47729,0.13636,0.13636,"white"],[-0.08217,0.34093,0.68182,0.13636,"white"],[-0.3549,0.20456,0.13636,0.13636,"white"],[-0.08217,0.20456,0.13636,0.13636,"white"],[0.19056,0.20456,0.13636,0.13636,"white"],[-0.08217,0.61365,0.68182,0.13636,"cyan"],[-0.01399,0.47729,0.54545,0.13636,"cyan"]],"eye":[[-0.42308,1.77275,0.27273,0.27273,"navy"],[0.53147,1.77275,0.27273,0.27273,"navy"]]}; + +export const HERO_PIVOT = {"head":[0.0,2.5909],"body":[0.0,0.979],"legs":[-0.035,0.9091],"tail":[-0.7343,1.3287],"eye":[0.0524,2.1294]}; + +export const HERO_SCALE = 0.50; diff --git a/srcv3/render/particles.js b/srcv3/render/particles.js new file mode 100644 index 0000000..0b2e764 --- /dev/null +++ b/srcv3/render/particles.js @@ -0,0 +1,14 @@ +// Частицы (взрыв) +export const parts = []; + +export function spawnExplosion(x, y, power) { + const n = Math.floor(16 + power * 10); + for (let i = 0; i < n; i++) { + parts.push({ + x, y, + vx: (Math.random() - 0.5) * (300 + power * 200), + vy: (Math.random() - 0.5) * (300 + power * 200), + t: 0.7, c: '#ffa500' + }); + } +} \ No newline at end of file diff --git a/srcv3/render/render3d.js b/srcv3/render/render3d.js new file mode 100644 index 0000000..d5bd613 --- /dev/null +++ b/srcv3/render/render3d.js @@ -0,0 +1,625 @@ +// ==================== РЕНДЕР 3D — GrechkaCraft v3 ==================== +// Объёмный мир: у каждого блока есть gx/gy/gz. Рендерим кубы по всем трём +// осям, отсекаем те, что не видны с камеры (соседи со всех шести сторон). +import { state } from '../core/state.js'; +import { BLOCKS } from '../data/blocks.js'; +import { isNight } from '../entities/mob-ai3d.js'; +import { skyColor } from '../world/weather.js'; +import { mode } from '../game/modes.js'; +import { handleTap, stopMining } from '../input/mouse-handler3d.js'; +import { getBlock } from '../world/world-storage3d.js'; +import { camDir } from '../game/loop3d.js'; +import { HERO_COL, HERO_PARTS, HERO_PIVOT, HERO_SCALE } from './hero-rig.js'; + +let TD = null; + +function TDparseColor(c) { + const m = /rgba?\(([^)]+)\)/.exec(c); + if (!m) return { color: new THREE.Color(c), opacity: 1 }; + const p = m[1].split(',').map(x => parseFloat(x)); + return { color: new THREE.Color(p[0] / 255, p[1] / 255, p[2] / 255), opacity: p.length > 3 ? p[3] : 1 }; +} + +function TDblockMat(t) { + const def = BLOCKS[t] || {}; + const { color, opacity } = TDparseColor(def.c || '#ff00ff'); + const a = def.alpha !== undefined ? def.alpha : opacity; + return new THREE.MeshLambertMaterial({ color, transparent: a < 1, opacity: a }); +} + +function TDisFluid(t) { const d = BLOCKS[t]; return !!(d && d.fluid); } +function TDisFlat(t) { + const d = BLOCKS[t]; + if (!d) return false; + return !d.solid || !!d.decor || !!d.climbable; +} + +// ---------- герой: тот же пиксельный рисунок, кубики ---------- +function TDbuildHeroRig() { + const group = new THREE.Group(); + const P = {}; + const MAT = {}; + for (const k of Object.keys(HERO_COL)) MAT[k] = new THREE.MeshLambertMaterial({ color: HERO_COL[k].hex }); + for (const name of Object.keys(HERO_PARTS)) { + const g = new THREE.Group(); + const j = HERO_PIVOT[name]; + g.position.set(j[0] * HERO_SCALE, j[1] * HERO_SCALE, 0); + const inner = new THREE.Group(); + inner.position.set(-j[0] * HERO_SCALE, -j[1] * HERO_SCALE, 0); + g.add(inner); + group.add(g); + P[name] = g; + for (const r of HERO_PARTS[name]) { + const depth = HERO_COL[r[4]].d * HERO_SCALE; + const m = new THREE.Mesh( + new THREE.BoxGeometry(r[2] * HERO_SCALE, r[3] * HERO_SCALE, depth), + MAT[r[4]] + ); + m.position.set((r[0] - 0.24) * HERO_SCALE, r[1] * HERO_SCALE, depth / 2); + m.castShadow = true; + m.receiveShadow = true; + inner.add(m); + } + } + return { group, P }; +} + +const TD_MOB_COLORS = { + zombie: 0x2ecc71, pig: 0xffb6c1, chicken: 0xecf0f1, creeper: 0x4caf50, + skeleton: 0xeceff1, scorpion: 0x5d4037, polar_bear: 0xeceff1, + slime: 0x4caf50, eagle: 0x5d4037, +}; + +function TDnameSprite(name) { + const cv = document.createElement('canvas'); + cv.width = 256; cv.height = 48; + const g = cv.getContext('2d'); + g.font = 'bold 26px system-ui'; + g.textAlign = 'center'; + g.fillStyle = 'rgba(0,0,0,0.45)'; + g.fillRect(0, 0, 256, 48); + g.fillStyle = '#fff'; + g.fillText(String(name || '?').slice(0, 14), 128, 33); + const sp = new THREE.Sprite(new THREE.SpriteMaterial({ map: new THREE.CanvasTexture(cv), depthTest: false })); + sp.scale.set(2.2, 0.41, 1); + return sp; +} + +// ==================== INIT ==================== +function TDinit() { + const canvas = state.canvas; + const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); + if (renderer.outputEncoding !== undefined && THREE.sRGBEncoding !== undefined) { + renderer.outputEncoding = THREE.sRGBEncoding; + } + renderer.shadowMap.enabled = true; + renderer.shadowMap.type = THREE.BasicShadowMap; + + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0x8fc7f5); + scene.fog = new THREE.Fog(0x8fc7f5, 30, 90); + const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 300); + + const hemi = new THREE.HemisphereLight(0xcfe6ff, 0x6ab04c, 0.85); + const sun = new THREE.DirectionalLight(0xffffff, 1.5); + sun.castShadow = true; + sun.shadow.mapSize.set(512, 512); + const sc = sun.shadow.camera; + sc.left = -26; sc.right = 26; sc.top = 26; sc.bottom = -26; + sc.near = 1; sc.far = 140; + sun.shadow.normalBias = 0.05; + const amb = new THREE.AmbientLight(0xffffff, 0.25); + scene.add(hemi, sun, sun.target, amb); + + const hero = TDbuildHeroRig(); + scene.add(hero.group); + + // Рабочая область: только блоки рядом с игроком (по всем осям) + const crack = new THREE.Mesh( + new THREE.BoxGeometry(1.004, 1.004, 1.004), + new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0 }) + ); + crack.visible = false; + scene.add(crack); + + const ghost = new THREE.Mesh( + new THREE.BoxGeometry(1.02, 1.02, 1.02), + new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true, transparent: true, opacity: 0.9 }) + ); + ghost.visible = false; + scene.add(ghost); + + // куб подсветки выбранного блока + const outline = new THREE.LineSegments( + new THREE.EdgesGeometry(new THREE.BoxGeometry(1.03, 1.03, 1.03)), + new THREE.LineBasicMaterial({ color: 0xffffff }) + ); + outline.visible = false; + scene.add(outline); + + const cloudMeshes = []; + const cloudMat = new THREE.MeshLambertMaterial({ color: 0xffffff, transparent: true, opacity: 0.75 }); + for (let i = 0; i < 16; i++) { + const m = new THREE.Mesh(new THREE.BoxGeometry(4, 0.8, 2.2), cloudMat); + scene.add(m); + cloudMeshes.push(m); + } + + const projPool = []; + for (let i = 0; i < 24; i++) { + const m = new THREE.Mesh(new THREE.BoxGeometry(0.55, 0.1, 0.1), new THREE.MeshBasicMaterial({ color: 0xf1c40f })); + m.visible = false; + scene.add(m); + projPool.push(m); + } + + const PMAX = 800; + const pGeo = new THREE.BufferGeometry(); + const pPos = new Float32Array(PMAX * 3); + const pCol = new Float32Array(PMAX * 3); + pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3)); + pGeo.setAttribute('color', new THREE.BufferAttribute(pCol, 3)); + const points = new THREE.Points(pGeo, new THREE.PointsMaterial({ size: 0.16, vertexColors: true, depthWrite: false })); + points.frustumCulled = false; + scene.add(points); + + function makeFall(n, color, size) { + const g = new THREE.BufferGeometry(); + const pos = new Float32Array(n * 3); + g.setAttribute('position', new THREE.BufferAttribute(pos, 3)); + const pts = new THREE.Points(g, new THREE.PointsMaterial({ color, size, transparent: true, opacity: 0.7, depthWrite: false })); + pts.frustumCulled = false; + pts.visible = false; + scene.add(pts); + return { pts, pos, n }; + } + const rain = makeFall(500, 0x74b9ff, 0.2); + const snow = makeFall(300, 0xffffff, 0.17); + + const lightPool = []; + for (let i = 0; i < 6; i++) { + const L = new THREE.PointLight(0xff9a3c, 0, 16); + scene.add(L); + lightPool.push(L); + } + const flamePool = []; + for (let i = 0; i < 8; i++) { + const f = new THREE.Mesh(new THREE.ConeGeometry(0.3, 0.75, 6), new THREE.MeshBasicMaterial({ color: 0xff7a1a })); + f.visible = false; + scene.add(f); + flamePool.push(f); + } + + const boatMesh = new THREE.Mesh( + new THREE.BoxGeometry(1.4, 0.5, 1.0), + new THREE.MeshLambertMaterial({ color: 0x8b4513 }) + ); + boatMesh.visible = false; + boatMesh.castShadow = true; + scene.add(boatMesh); + + TD = { + renderer, scene, camera, hemi, sun, amb, hero, + crack, ghost, outline, cloudMeshes, projPool, points, pPos, pCol, PMAX, + rain, snow, lightPool, flamePool, boatMesh, + blockMeshes: [], lastVersion: -1, meshT: 0, + mobs: new Map(), players: new Map(), + yaw: 0.0, pitch: 0.42, dist: 10, + ptrs: new Map(), pinchD: 0, + lastTap: null, hovered: null, fireCache: null, + heroStep: 0, face: 1, frame: 0, lastT: 0, + colCache: new Map(), + }; + TDbindInput(canvas); + TDresize(); + window.addEventListener('resize', TDresize); +} + +function TDresize() { + if (!TD) return; + const w = window.innerWidth, h = window.innerHeight; + state.W = w; + state.H = h; + TD.renderer.setPixelRatio(1); + TD.renderer.setSize(w, h, false); + TD.camera.aspect = w / h; + TD.camera.updateProjectionMatrix(); +} + +// ==================== БЛОКИ ==================== +// Полный пересбор мешей редкий; в обычном кадре — только обновление матриц. +const VIEW_R = 14; // радиус прорисовки в тайлах по всем осям + +function isHiddenAt(gx, gy, gz) { + // спрятан ли блок: все 6 соседей непрозрачные? + return isOpaqueAt(gx + 1, gy, gz) && isOpaqueAt(gx - 1, gy, gz) && + isOpaqueAt(gx, gy + 1, gz) && isOpaqueAt(gx, gy - 1, gz) && + isOpaqueAt(gx, gy, gz + 1) && isOpaqueAt(gx, gy, gz - 1); +} + +function isOpaqueAt(gx, gy, gz) { + const b = getBlock(gx, gy, gz); + if (!b) return false; + const def = BLOCKS[b.t]; + if (!def) return false; + return !!def.solid && !def.fluid && !def.decor && !def.climbable && def.alpha === undefined; +} + +function TDrebuildBlocks(px, py, pz) { + for (const e of TD.blockMeshes) { + TD.scene.remove(e.mesh); + e.mesh.geometry.dispose(); + e.mesh.material.dispose(); + } + TD.blockMeshes = []; + const byType = new Map(); + for (const b of state.blocks) { + if (b.dead) continue; + if (!BLOCKS[b.t]) continue; + const dx = b.gx - px, dy = b.gy - py, dz = (b.gz || 0) - pz; // py — игровой gy + if (dx * dx + dy * dy + dz * dz > VIEW_R * VIEW_R) continue; + if (!TDisFluid(b.t) && !TDisFlat(b.t) && isHiddenAt(b.gx, b.gy, b.gz || 0)) continue; + if (!byType.has(b.t)) byType.set(b.t, []); + byType.get(b.t).push(b); + } + const boxGeo = new THREE.BoxGeometry(1, 1, 1); + const planeGeo = new THREE.PlaneGeometry(0.92, 0.92); + for (const [t, list] of byType) { + const fluid = TDisFluid(t); + const flat = !fluid && TDisFlat(t); + const mesh = new THREE.InstancedMesh(flat ? planeGeo : boxGeo, TDblockMat(t), list.length); + mesh.castShadow = !flat; + mesh.receiveShadow = true; + if (flat) mesh.material.side = THREE.DoubleSide; + const m = new THREE.Matrix4(); + const q = new THREE.Quaternion(); + const e = new THREE.Euler(); + for (let i = 0; i < list.length; i++) { + const b = list[i]; + if (flat) { + // плоские блоки (цветы) — поворачиваем к камере, стоят на земле + e.set(-Math.PI / 2, 0, 0); + q.setFromEuler(e); + m.compose(new THREE.Vector3(b.gx + 0.5, -b.gy + 0.05, (b.gz || 0) + 0.5), q, new THREE.Vector3(1, 1, 1)); + } else if (fluid) { + m.makeScale(1, 0.82, 1); + m.setPosition(b.gx + 0.5, -b.gy - 0.09, (b.gz || 0) + 0.5); + } else { + m.identity(); + m.setPosition(b.gx + 0.5, -b.gy - 0.5, (b.gz || 0) + 0.5); + } + mesh.setMatrixAt(i, m); + } + mesh.instanceMatrix.needsUpdate = true; + mesh.userData.list = list; + TD.scene.add(mesh); + TD.blockMeshes.push({ mesh, type: t }); + } +} + +// ==================== PICK (рейкаст по 3D) ==================== +const TDray = new THREE.Raycaster(); +const TDndc = new THREE.Vector2(); + +function TDpick(sx, sy) { + const r = state.canvas.getBoundingClientRect(); + TDndc.x = ((sx - r.left) / r.width) * 2 - 1; + TDndc.y = -((sy - r.top) / r.height) * 2 + 1; + TDray.setFromCamera(TDndc, TD.camera); + const hits = TDray.intersectObjects(TD.blockMeshes.map(x => x.mesh), false); + if (!hits.length) return null; + const h = hits[0]; + const b = h.object.userData.list[h.instanceId]; + if (!b) return null; + // мировые игровые координаты точки удара + const gx = Math.floor(h.point.x); + const gy = -Math.floor(h.point.y); + const gz = Math.floor(h.point.z); + const wx = (h.point.x) * state.TILE; + const wy = (-h.point.y) * state.TILE; + return { gx, gy, gz, wx, wy, face: h.face ? h.face.normal : null }; +} + +// ==================== ВВОД ==================== +function TDbindInput(canvas) { + canvas.addEventListener('pointerdown', (e) => { + TD.ptrs.set(e.pointerId, { x: e.clientX, y: e.clientY, sx: e.clientX, sy: e.clientY, moved: false, t: performance.now() }); + if (TD.ptrs.size === 2) { + const q = [...TD.ptrs.values()]; + TD.pinchD = Math.hypot(q[0].x - q[1].x, q[0].y - q[1].y); + } + stopMining(); + }); + + canvas.addEventListener('pointermove', (e) => { + const q = TD.ptrs.get(e.pointerId); + if (!q) return; + const dx = e.clientX - q.x, dy = e.clientY - q.y; + q.x = e.clientX; q.y = e.clientY; + if (Math.hypot(e.clientX - q.sx, e.clientY - q.sy) > 8) q.moved = true; + if (TD.ptrs.size === 2) { + const v = [...TD.ptrs.values()]; + const d = Math.hypot(v[0].x - v[1].x, v[0].y - v[1].y); + if (TD.pinchD > 0 && d > 0) TD.dist = Math.min(34, Math.max(4, TD.dist * TD.pinchD / d)); + TD.pinchD = d; + return; + } + if (q.moved) { + TD.yaw -= dx * 0.005; + TD.pitch = Math.min(1.45, Math.max(0.06, TD.pitch + dy * 0.005)); + } + }); + + const up = (e) => { + const q = TD.ptrs.get(e.pointerId); + TD.ptrs.delete(e.pointerId); + TD.pinchD = 0; + if (q && !q.moved && performance.now() - q.t < 600) { + const hit = TDpick(e.clientX, e.clientY); + if (hit) { + TD.lastTap = hit; + handleTap(hit.wx, hit.wy, hit.gz, hit.face); + } + } + }; + canvas.addEventListener('pointerup', up); + canvas.addEventListener('pointercancel', (e) => { TD.ptrs.delete(e.pointerId); TD.pinchD = 0; }); + canvas.addEventListener('contextmenu', (e) => e.preventDefault()); + canvas.addEventListener('wheel', (e) => { + e.preventDefault(); + e.stopPropagation(); + TD.dist = Math.min(34, Math.max(4, TD.dist * (1 + e.deltaY * 0.001))); + }, { passive: false, capture: true }); +} + +// ==================== СУЩНОСТИ ==================== +function TDmobMesh(m) { + let e = TD.mobs.get(m); + if (!e) { + const g = new THREE.Group(); + const body = new THREE.Mesh( + new THREE.BoxGeometry(1, 1, 1), + new THREE.MeshLambertMaterial({ color: TD_MOB_COLORS[m.kind] || 0x2ecc71 }) + ); + body.castShadow = true; + g.add(body); + const eyeM = new THREE.MeshBasicMaterial({ color: 0x111111 }); + const e1 = new THREE.Mesh(new THREE.BoxGeometry(0.14, 0.14, 0.05), eyeM); + e1.position.set(-0.16, 0.22, 0.26); + const e2 = e1.clone(); + e2.position.x = 0.16; + g.add(e1, e2); + TD.scene.add(g); + e = { g, body }; + TD.mobs.set(m, e); + } + return e; +} + +function TDplayerEntry(id, p) { + let e = TD.players.get(id); + if (!e) { + const rig = TDbuildHeroRig(); + const label = TDnameSprite(p.name); + label.position.y = 2.2; + rig.group.add(label); + TD.scene.add(rig.group); + e = { rig, label }; + TD.players.set(id, e); + } + return e; +} + +// ==================== КАДР ==================== +export function renderFrame3D(now) { + if (!TD) TDinit(); + const p = state.player; + const TILE = state.TILE; + TD.frame++; + const dt = Math.min(0.05, Math.max(0.001, (now - (TD.lastT || now - 16)) / 1000)); + TD.lastT = now; + + const ptx = (p.x + (p.w || 34) / 2) / TILE; + const pgy = (p.y + (p.h || 34) / 2) / TILE; // игровый Y (вниз) + const py3 = -pgy; // three Y (вверх) + const ptz = ((p.z || 0) + (p.d || 34) / 2) / TILE; + + // пересборка мешей: при изменении мира или раз в ~2 сек (для плавной подгрузки) + // пересборка не чаще раза в 700 мс и только при изменении мира + const ver = state.blocksVersion || 0; + if ((ver !== TD.lastVersion && now - TD.meshT > 700) || !TD.blockMeshes.length) { + TD.meshT = now; + TD.lastVersion = ver; + TDrebuildBlocks(ptx, pgy, ptz); + } + + // небо/свет. Первые секунды после старта держим полдень, чтобы не было «рассвета» + const night = isNight(); + const sky = skyColor(night, 'plains', state.weatherType, state.worldTime); + TD.scene.background.set(sky); + TD.scene.fog.color.copy(TD.scene.background); + TD.sun.intensity = night ? 0.14 : 1.5; + TD.hemi.intensity = night ? 0.3 : 0.85; + TD.amb.intensity = night ? 0.34 : 0.25; + if (state.lightningFlash > 0) TD.hemi.intensity += Math.min(2.2, state.lightningFlash * 2.4); + TD.sun.position.set(ptx + 20, py3 + 36, ptz + 20); + TD.sun.target.position.set(ptx, py3, ptz); + TD.sun.target.updateMatrixWorld(); + + // камера-орбита; yaw — тот же угол, что и для WASD + camDir.yaw = TD.yaw; + const cp = Math.cos(TD.pitch), sp2 = Math.sin(TD.pitch); + TD.camera.position.set( + ptx + TD.dist * Math.sin(TD.yaw) * cp, + py3 + TD.dist * sp2, + ptz + TD.dist * Math.cos(TD.yaw) * cp + ); + TD.camera.lookAt(ptx, py3, ptz); + + // герой: позиция в ногах коллайдера, поворот по направлению взгляда + TD.hero.group.position.set(ptx, -p.y / TILE, ptz); + if (p.facing !== undefined) { + const wantYaw = -p.facing; + TD.hero.group.rotation.y += (wantYaw - TD.hero.group.rotation.y) * Math.min(1, 9 * dt); + } + const moving = Math.hypot(p.vx || 0, p.vz || 0) > 20 && p.grounded; + if (moving) TD.heroStep += dt * 11; + TD.hero.P.legs.rotation.z = moving ? Math.sin(TD.heroStep) * 0.34 : 0; + TD.hero.P.legs.rotation.x = moving ? Math.cos(TD.heroStep) * 0.22 : 0; + + // мобы + const allMobs = state.isMultiplayer ? Array.from(state.serverMobs.values()) : state.mobs; + const seen = new Set(); + for (const m of allMobs) { + if (m.dead) continue; + seen.add(m); + const e = TDmobMesh(m); + e.body.scale.set((m.w || 30) / TILE, (m.h || 30) / TILE, (m.d || m.w || 30) / TILE); + e.g.position.set( + (m.x + (m.w || 30) / 2) / TILE, + -(m.y + (m.h || 30) / 2) / TILE, + ((m.z || 0) + (m.d || m.w || 30) / 2) / TILE + ); + } + for (const [m, e] of TD.mobs) { + if (!seen.has(m)) { TD.scene.remove(e.g); TD.mobs.delete(m); } + } + + // игроки сети + const seenP = new Set(); + if (state.otherPlayers) { + for (const [id, pl] of state.otherPlayers) { + seenP.add(id); + const e = TDplayerEntry(id, pl); + e.rig.group.position.set((pl.x + 17) / TILE, -pl.y / TILE, ((pl.z || 0) + 17) / TILE); + } + } + for (const [id, e] of TD.players) { + if (!seenP.has(id)) { TD.scene.remove(e.rig.group); TD.players.delete(id); } + } + + // лодка + if (state.boat && state.boat.active) { + TD.boatMesh.visible = true; + TD.boatMesh.position.set(state.boat.x / TILE, -state.boat.y / TILE, (state.boat.z || 0) / TILE); + } else TD.boatMesh.visible = false; + + // снаряды + const projs = state.projectiles || []; + for (let i = 0; i < TD.projPool.length; i++) { + const mesh = TD.projPool[i]; + const pr = projs[i]; + if (!pr) { mesh.visible = false; continue; } + mesh.visible = true; + mesh.material.color.set(pr.owner === 'mob' ? 0xc0392b : 0xf1c40f); + mesh.position.set(pr.x / TILE, -pr.y / TILE, (pr.z || 0) / TILE); + mesh.rotation.z = -Math.atan2(pr.vy, pr.vx); + } + + // частицы + const parts = state.parts || []; + const n = Math.min(parts.length, TD.PMAX); + for (let i = 0; i < n; i++) { + const pt = parts[i]; + TD.pPos[i * 3] = pt.x / TILE; + TD.pPos[i * 3 + 1] = -pt.y / TILE; + TD.pPos[i * 3 + 2] = (pt.z || 0) / TILE; + let cc = TD.colCache.get(pt.c); + if (!cc) { cc = new THREE.Color(pt.c || '#ffffff'); TD.colCache.set(pt.c, cc); } + TD.pCol[i * 3] = cc.r; TD.pCol[i * 3 + 1] = cc.g; TD.pCol[i * 3 + 2] = cc.b; + } + TD.points.geometry.setDrawRange(0, n); + TD.points.geometry.attributes.position.needsUpdate = true; + TD.points.geometry.attributes.color.needsUpdate = true; + + // облака + const clouds = state.clouds || []; + for (let i = 0; i < TD.cloudMeshes.length; i++) { + const cm = TD.cloudMeshes[i]; + const c = clouds.length ? clouds[i % clouds.length] : null; + if (!c) { cm.visible = false; continue; } + cm.visible = true; + cm.scale.set((c.w || 120) / TILE, 1, 1); + cm.position.set(c.x / TILE, -c.y / TILE, ptz - 12); + } + + // огни + if (TD.frame % 20 === 0 || !TD.fireCache) { + const found = []; + for (const b of state.blocks) { + if (b.dead) continue; + const def = BLOCKS[b.t]; + if (!def) continue; + const lit = def.lightRadius || b.t === 'campfire' || + (b.t === 'furnace' && state.activeFurnaces && state.activeFurnaces.has(b.gx + ',' + b.gy + ',' + (b.gz || 0))); + if (!lit) continue; + const dx = b.gx - ptx, dy = -b.gy + pty, dz = (b.gz || 0) - ptz; + found.push({ t: b.t, gx: b.gx, gy: b.gy, gz: b.gz || 0, d: dx * dx + dy * dy + dz * dz }); + } + found.sort((a, b) => a.d - b.d); + TD.fireCache = found.slice(0, 8); + } + for (const L of TD.lightPool) L.intensity = 0; + for (const f of TD.flamePool) f.visible = false; + let li = 0, fi = 0; + for (const src of (TD.fireCache || [])) { + if (li < TD.lightPool.length) { + const L = TD.lightPool[li++]; + L.position.set(src.gx + 0.5, -src.gy + 0.5, src.gz + 0.5); + L.intensity = 1.6 + Math.sin(now / 130 + src.gx) * 0.35; + } + if ((src.t === 'campfire' || src.t === 'furnace') && fi < TD.flamePool.length) { + const f = TD.flamePool[fi++]; + f.visible = true; + f.position.set(src.gx + 0.5, -src.gy + 0.78 + Math.sin(now / 90 + src.gy) * 0.06, src.gz + 0.5); + const sc2 = 0.9 + Math.sin(now / 70 + src.gx * 2) * 0.18; + f.scale.set(sc2, sc2 * (1 + Math.sin(now / 55) * 0.12), sc2); + } + } + + // погода + if (state.isRaining) { + TD.rain.pts.visible = true; + for (let i = 0; i < TD.rain.n; i++) { + let y = TD.rain.pos[i * 3 + 1] - dt * 26; + let x = TD.rain.pos[i * 3]; + if (y < py3 - 12) { y = py3 + 12; x = ptx + (Math.random() - 0.5) * 40; } + TD.rain.pos[i * 3] = x; + TD.rain.pos[i * 3 + 1] = y; + TD.rain.pos[i * 3 + 2] = ptz + 6; + } + TD.rain.pts.geometry.attributes.position.needsUpdate = true; + } else TD.rain.pts.visible = false; + + if (state.weatherType === 'snow') { + TD.snow.pts.visible = true; + for (let i = 0; i < TD.snow.n; i++) { + let y = TD.snow.pos[i * 3 + 1] - dt * 2; + let x = TD.snow.pos[i * 3] + Math.sin(now / 900 + i) * dt * 1.5; + if (y < py3 - 12) { y = py3 + 12; x = ptx + (Math.random() - 0.5) * 40; } + TD.snow.pos[i * 3] = x; + TD.snow.pos[i * 3 + 1] = y; + TD.snow.pos[i * 3 + 2] = ptz + 6; + } + TD.snow.pts.geometry.attributes.position.needsUpdate = true; + } else TD.snow.pts.visible = false; + + // подсветка блока под курсором / прогресс добычи + const mg = state.mining; + if (mg && mg.gx !== null && mg.hardness > 0) { + TD.crack.visible = true; + TD.crack.position.set(mg.gx + 0.5, -mg.gy - 0.5, (mg.gz || 0) + 0.5); + TD.crack.material.opacity = Math.min(0.55, (mg.progress / mg.hardness) * 0.55); + } else TD.crack.visible = false; + + if (mode() === 'build' && TD.lastTap) { + TD.ghost.visible = true; + TD.ghost.position.set(TD.lastTap.gx + 0.5, -TD.lastTap.gy - 0.5, (TD.lastTap.gz || 0) + 0.5); + } else TD.ghost.visible = false; + + if (state.W !== window.innerWidth || state.H !== window.innerHeight) TDresize(); + TD.camera.aspect = window.innerWidth / window.innerHeight; + TD.camera.updateProjectionMatrix(); + + TD.renderer.render(TD.scene, TD.camera); +} diff --git a/srcv3/render/textures.js b/srcv3/render/textures.js new file mode 100644 index 0000000..5de9efc --- /dev/null +++ b/srcv3/render/textures.js @@ -0,0 +1,506 @@ +// Текстуры блоков (простые) +import { BLOCKS } from '../data/blocks.js'; + +export const tex = {}; + +// --- Item/tool sprite generator ---\n +export const itemTex = {}; + +function drawArrow(ctx, color) { + ctx.fillStyle = '#8B4513'; + ctx.fillRect(8, 14, 16, 3); + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(24, 15); + ctx.lineTo(18, 10); + ctx.lineTo(18, 20); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = '#e74c3c'; + ctx.beginPath(); + ctx.moveTo(8, 15); + ctx.lineTo(4, 10); + ctx.lineTo(10, 14); + ctx.closePath(); + ctx.fill(); + ctx.beginPath(); + ctx.moveTo(8, 16); + ctx.lineTo(4, 21); + ctx.lineTo(10, 17); + ctx.closePath(); + ctx.fill(); +} + +function drawPickaxe(ctx, headColor, handleColor) { + ctx.fillStyle = handleColor || '#8B4513'; + ctx.save(); + ctx.translate(16, 16); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-2, 0, 4, 18); + ctx.fillStyle = headColor; + ctx.fillRect(-8, -3, 16, 5); + ctx.fillRect(-8, -3, 3, 8); + ctx.fillRect(5, -3, 3, 8); + ctx.restore(); +} + +function drawAxe(ctx, headColor, handleColor) { + ctx.fillStyle = handleColor || '#8B4513'; + ctx.save(); + ctx.translate(16, 16); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-2, 0, 4, 18); + ctx.fillStyle = headColor; + ctx.fillRect(-2, -8, 10, 8); + ctx.fillRect(-2, -8, 4, 12); + ctx.restore(); +} + +function drawShovel(ctx, headColor, handleColor) { + ctx.fillStyle = handleColor || '#8B4513'; + ctx.save(); + ctx.translate(16, 16); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-2, 0, 4, 18); + ctx.fillStyle = headColor; + ctx.fillRect(-6, -8, 12, 8); + ctx.fillRect(-4, -12, 8, 4); + ctx.restore(); +} + +function drawSword(ctx, bladeColor, guardColor) { + ctx.fillStyle = '#8B4513'; + ctx.fillRect(14, 18, 4, 10); + ctx.fillStyle = guardColor || '#f1c40f'; + ctx.fillRect(10, 16, 12, 3); + ctx.fillStyle = bladeColor; + ctx.beginPath(); + ctx.moveTo(16, 2); + ctx.lineTo(12, 16); + ctx.lineTo(20, 16); + ctx.closePath(); + ctx.fill(); +} + +function drawBow(ctx) { + ctx.strokeStyle = '#8B4513'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(12, 16, 12, -Math.PI * 0.7, Math.PI * 0.7); + ctx.stroke(); + ctx.strokeStyle = '#ddd'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(12, 4); + ctx.lineTo(12, 28); + ctx.stroke(); +} + +function drawIngot(ctx, color) { + ctx.fillStyle = color; + ctx.beginPath(); + ctx.moveTo(6, 12); + ctx.lineTo(26, 12); + ctx.lineTo(24, 24); + ctx.lineTo(8, 24); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + ctx.fillRect(8, 8, 16, 4); +} + +function drawMeat(ctx, cooked) { + ctx.fillStyle = cooked ? '#8B4513' : '#e74c3c'; + ctx.beginPath(); + ctx.ellipse(16, 18, 10, 8, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#f5f5dc'; + ctx.fillRect(8, 6, 3, 12); + ctx.fillRect(14, 4, 3, 8); +} + +function makeItemTex(id) { + const c = document.createElement('canvas'); + c.width = 32; c.height = 32; + const g = c.getContext('2d'); + + const tierColors = { + wood: { head: '#a0522d', blade: '#a0522d' }, + stone: { head: '#7f8c8d', blade: '#95a5a6' }, + iron: { head: '#bdc3c7', blade: '#ecf0f1' }, + }; + + switch (id) { + case 'arrow': + drawArrow(g, '#7f8c8d'); + break; + case 'bow': + drawBow(g); + break; + case 'wood_pickaxe': + drawPickaxe(g, tierColors.wood.head, tierColors.wood.blade); + break; + case 'stone_pickaxe': + drawPickaxe(g, tierColors.stone.head); + break; + case 'iron_pickaxe': + drawPickaxe(g, tierColors.iron.head); + break; + case 'wood_axe': + drawAxe(g, tierColors.wood.head); + break; + case 'stone_axe': + drawAxe(g, tierColors.stone.head); + break; + case 'iron_axe': + drawAxe(g, tierColors.iron.head); + break; + case 'wood_shovel': + drawShovel(g, tierColors.wood.head); + break; + case 'stone_shovel': + drawShovel(g, tierColors.stone.head); + break; + case 'iron_shovel': + drawShovel(g, tierColors.iron.head); + break; + case 'wood_sword': + drawSword(g, tierColors.wood.blade, '#a0522d'); + break; + case 'stone_sword': + drawSword(g, tierColors.stone.blade, '#7f8c8d'); + break; + case 'iron_sword': + drawSword(g, tierColors.iron.blade, '#bdc3c7'); + break; + case 'iron_ingot': + drawIngot(g, '#bdc3c7'); + break; + case 'gold_ingot': + drawIngot(g, '#f1c40f'); + break; + case 'copper_ingot': + drawIngot(g, '#e67e22'); + break; + case 'meat': + drawMeat(g, false); + break; + case 'cooked': + drawMeat(g, true); + break; + case 'wheat': + g.fillStyle = '#f1c40f'; + g.fillRect(12, 8, 3, 18); + g.fillRect(8, 6, 10, 4); + break; + case 'bread': + g.fillStyle = '#d4a030'; + g.fillRect(6, 12, 20, 12); + g.fillStyle = '#c08820'; + g.fillRect(6, 12, 20, 3); + break; + case 'carrot': + g.fillStyle = '#e67e22'; + g.fillRect(14, 10, 4, 18); + g.fillStyle = '#27ae60'; + g.fillRect(12, 6, 8, 5); + break; + case 'potato': + g.fillStyle = '#d4a050'; + g.beginPath(); + g.ellipse(16, 16, 8, 6, 0, 0, Math.PI * 2); + g.fill(); + g.fillStyle = '#b88a40'; + g.fillRect(10, 14, 2, 1); + g.fillRect(20, 16, 2, 1); + break; + case 'baked_potato': + g.fillStyle = '#c08820'; + g.beginPath(); + g.ellipse(16, 16, 8, 6, 0, 0, Math.PI * 2); + g.fill(); + g.fillStyle = '#f5e6cc'; + g.fillRect(12, 12, 8, 6); + break; + case 'mushroom_stew': + g.fillStyle = '#8B4513'; + g.fillRect(10, 16, 12, 12); + g.fillStyle = '#e74c3c'; + g.beginPath(); + g.ellipse(16, 14, 8, 5, 0, Math.PI, 0); + g.fill(); + break; + case 'scorpion_stinger': + g.fillStyle = '#d35400'; + g.beginPath(); + g.moveTo(16, 4); + g.lineTo(10, 28); + g.lineTo(22, 28); + g.closePath(); + g.fill(); + break; + case 'polar_fur': + g.fillStyle = '#ecf0f1'; + g.fillRect(6, 6, 20, 20); + g.fillStyle = '#bdc3c7'; + g.fillRect(12, 12, 8, 8); + break; + case 'slime_ball': + g.fillStyle = 'rgba(46,204,113,0.8)'; + g.beginPath(); + g.arc(16, 16, 10, 0, Math.PI * 2); + g.fill(); + g.fillStyle = '#fff'; + g.fillRect(10, 10, 4, 4); + g.fillRect(18, 10, 4, 4); + break; + case 'eagle_feather': + g.fillStyle = '#8B4513'; + g.fillRect(14, 4, 4, 24); + g.fillStyle = '#ecf0f1'; + g.fillRect(10, 6, 12, 10); + break; + case 'chicken_meat': + drawMeat(g, false); + g.fillStyle = '#fff'; + g.fillRect(12, 8, 4, 4); + break; + case 'feather': + g.fillStyle = '#ecf0f1'; + g.fillRect(14, 6, 4, 20); + g.fillStyle = '#bbb'; + g.fillRect(12, 8, 8, 8); + break; + case 'bone': + g.fillStyle = '#f5f5dc'; + g.fillRect(12, 10, 8, 14); + g.fillRect(8, 8, 4, 4); + g.fillRect(20, 8, 4, 4); + g.fillRect(8, 22, 4, 4); + g.fillRect(20, 22, 4, 4); + break; + case 'gunpowder': + g.fillStyle = '#555'; + g.fillRect(8, 10, 6, 6); + g.fillRect(18, 14, 6, 6); + g.fillRect(12, 20, 6, 6); + break; + case 'gold_armor': + g.fillStyle = '#f1c40f'; + g.fillRect(6, 6, 20, 20); + g.fillStyle = '#f39c12'; + g.fillRect(10, 4, 12, 6); + g.fillRect(4, 10, 6, 16); + g.fillRect(22, 10, 6, 16); + break; + case 'wheat_seeds': + g.fillStyle = '#7dcea0'; + g.fillRect(14, 16, 4, 10); + g.fillStyle = '#a8e6a0'; + g.fillRect(10, 8, 4, 4); + g.fillRect(18, 6, 4, 4); + g.fillRect(14, 4, 4, 4); + break; + default: + g.fillStyle = '#7f8c8d'; + g.fillRect(4, 4, 24, 24); + g.fillStyle = 'rgba(255,255,255,0.2)'; + g.fillRect(4, 4, 24, 4); + } + return c; +} + +export function makeTex(type) { + const t = BLOCKS[type]; + const c = document.createElement('canvas'); + c.width = 32; c.height = 32; + const g = c.getContext('2d'); + + if (type === 'tnt') { + g.fillStyle = '#c0392b'; g.fillRect(0, 0, 32, 32); + g.fillStyle = '#fff'; g.fillRect(0, 12, 32, 8); + g.fillStyle = '#000'; g.font = 'bold 10px sans-serif'; g.fillText('TNT', 6, 20); + return c; + } + if (type === 'campfire') { + g.fillStyle = '#5d4037'; g.fillRect(4, 26, 24, 6); + g.fillStyle = '#3e2723'; g.fillRect(7, 23, 18, 4); + return c; + } + if (type === 'torch') { + g.fillStyle = '#6d4c41'; g.fillRect(14, 10, 4, 18); + g.fillStyle = '#f39c12'; g.fillRect(12, 6, 8, 8); + return c; + } + if (type === 'glass') { + g.fillStyle = 'rgba(200,240,255,0.25)'; g.fillRect(0, 0, 32, 32); + g.strokeStyle = 'rgba(255,255,255,0.65)'; g.strokeRect(2, 2, 28, 28); + g.beginPath(); g.moveTo(5, 27); g.lineTo(27, 5); g.stroke(); + return c; + } + if (type === 'water') { + g.fillStyle = t.c; g.fillRect(0, 0, 32, 32); + g.fillStyle = 'rgba(255,255,255,0.08)'; + g.fillRect(0, 6, 32, 2); + return c; + } + if (type === 'bed') { + g.fillStyle = '#e91e63'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#f8bbd0'; + g.fillRect(2, 2, 14, 14); + g.fillStyle = '#c2185b'; + g.fillRect(16, 4, 14, 24); + g.fillStyle = '#e91e63'; + g.fillRect(18, 6, 10, 20); + return c; + } + if (type === 'flower') { + g.fillStyle = '#2ecc71'; g.fillRect(14, 14, 4, 18); + g.fillStyle = t.c; g.beginPath(); g.arc(16, 12, 6, 0, 6.28); g.fill(); + return c; + } + if (type === 'boat') { + g.fillStyle = '#8B4513'; + g.fillRect(2, 12, 28, 8); + g.fillStyle = '#A0522D'; + g.fillRect(0, 10, 32, 12); + g.fillStyle = '#DEB887'; + g.fillRect(4, 14, 24, 4); + g.fillStyle = '#654321'; + g.fillRect(2, 20, 28, 4); + return c; + } + if (type === 'ladder') { + g.fillStyle = '#8B4513'; + g.fillRect(4, 0, 4, 32); + g.fillRect(24, 0, 4, 32); + g.fillStyle = '#A0522D'; + g.fillRect(4, 4, 24, 3); + g.fillRect(4, 12, 24, 3); + g.fillRect(4, 20, 24, 3); + g.fillRect(4, 28, 24, 3); + return c; + } + // === BIOME BLOCK TEXTURES === + if (type === 'snow') { + g.fillStyle = '#ecf0f1'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#dfe6e9'; + for (let i = 0; i < 4; i++) g.fillRect((Math.random()*26)|0, (Math.random()*26)|0, 5, 3); + return c; + } + if (type === 'ice') { + g.fillStyle = '#74b9ff'; + g.fillRect(0, 0, 32, 32); + g.strokeStyle = 'rgba(255,255,255,0.4)'; + g.beginPath(); g.moveTo(4,10); g.lineTo(20,16); g.lineTo(10,28); g.stroke(); + g.beginPath(); g.moveTo(18,4); g.lineTo(28,12); g.stroke(); + return c; + } + if (type === 'cactus') { + g.fillStyle = '#27ae60'; + g.fillRect(6, 2, 20, 28); + g.fillStyle = '#2ecc71'; + g.fillRect(2, 8, 6, 4); + g.fillRect(24, 14, 6, 4); + g.fillStyle = '#1e8449'; + g.fillRect(12, 0, 2, 30); + g.fillRect(18, 0, 2, 30); + return c; + } + if (type === 'mushroom') { + g.fillStyle = '#f5e6cc'; g.fillRect(14, 20, 4, 12); + g.fillStyle = '#e74c3c'; g.beginPath(); g.arc(16, 14, 10, Math.PI, 0); g.fill(); + g.fillStyle = '#fff'; g.fillRect(10, 10, 3, 3); g.fillRect(17, 8, 4, 3); + return c; + } + if (type === 'moss') { + g.fillStyle = '#0a6640'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#1e8449'; + for (let i = 0; i < 6; i++) g.fillRect((Math.random()*26)|0, (Math.random()*26)|0, 4, 3); + return c; + } + if (type === 'swamp_water') { + g.fillStyle = 'rgba(100,120,40,0.6)'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = 'rgba(80,100,20,0.3)'; + g.fillRect(0, 10, 32, 2); + g.fillRect(0, 22, 32, 2); + return c; + } + if (type === 'farmland') { + g.fillStyle = '#8B6914'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#7a5c10'; + for (let i = 0; i < 4; i++) g.fillRect(0, 6+i*8, 32, 2); + g.fillStyle = '#6B4E0A'; + g.fillRect(8, 2, 2, 28); + g.fillRect(18, 2, 2, 28); + return c; + } + if (type === 'dead_bush') { + g.fillStyle = '#b2bec3'; + g.fillRect(12, 16, 2, 16); + g.fillRect(8, 14, 8, 2); + g.fillRect(16, 12, 8, 2); + g.fillRect(6, 18, 4, 2); + return c; + } + if (type === 'spruce_leaves') { + g.fillStyle = '#0a6640'; + g.fillRect(0, 0, 32, 32); + g.fillStyle = '#0d7a4d'; + for (let i = 0; i < 5; i++) g.fillRect((Math.random()*24)|0, (Math.random()*24)|0, 6, 4); + return c; + } + // CROP STAGES + if (type.startsWith('wheat_stage') || type.startsWith('carrot_stage') || type.startsWith('potato_stage')) { + const st = parseInt(type.charAt(type.length-1)); + const colors = type.startsWith('wheat') ? ['#a8e6a0','#7dcea0','#b8d730','#f1c40f'] : + type.startsWith('carrot') ? ['#a8e6a0','#f0c27a','#e8a040','#e67e22'] : + ['#a8e6a0','#c8b888','#bfaa78','#dfe6e9']; + g.fillStyle = '#5d4037'; + g.fillRect(15, 18, 2, 14); + if (st >= 1) { + g.fillStyle = colors[st]; + g.fillRect(10, 8 + (3-st)*3, 12, 10); + } else { + g.fillStyle = '#a8e6a0'; + g.fillRect(14, 22, 4, 4); + } + return c; + } + + g.fillStyle = t.c || '#000'; + g.fillRect(0, 0, 32, 32); + + g.fillStyle = 'rgba(0,0,0,0.10)'; + for (let i = 0; i < 6; i++) g.fillRect((Math.random() * 28) | 0, (Math.random() * 28) | 0, 4, 4); + + if (type.endsWith('_ore') || type === 'coal') { + g.fillStyle = 'rgba(0,0,0,0.35)'; + for (let i = 0; i < 4; i++) g.fillRect((Math.random() * 24) | 0, (Math.random() * 24) | 0, 6, 6); + } + return c; +} + +export function initTextures() { + Object.keys(BLOCKS).forEach(k => tex[k] = makeTex(k)); + // Item/tool sprites + const itemIds = [ + 'arrow','bow', + 'wood_pickaxe','stone_pickaxe','iron_pickaxe', + 'wood_axe','stone_axe','iron_axe', + 'wood_shovel','stone_shovel','iron_shovel', + 'wood_sword','stone_sword','iron_sword', + 'iron_ingot','gold_ingot','copper_ingot', + 'meat','cooked', + 'wheat','bread','carrot','potato','baked_potato','mushroom_stew','wheat_seeds', + 'chicken_meat','feather','bone','gunpowder', + 'scorpion_stinger','polar_fur','slime_ball','eagle_feather', + 'gold_armor', + ]; + itemIds.forEach(id => { itemTex[id] = makeItemTex(id); }); +} diff --git a/srcv3/ui/chat.js b/srcv3/ui/chat.js new file mode 100644 index 0000000..56ef740 --- /dev/null +++ b/srcv3/ui/chat.js @@ -0,0 +1,67 @@ +// ==================== ЧАТ ==================== +import { state } from '../core/state.js'; +import { MAX_CHAT_MESSAGES } from '../core/constants.js'; +import { playSound } from '../audio/sound-engine.js'; + +const chatMessages = []; + +export function addChatMessage(sender, message) { + const time = new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }); + chatMessages.push({ sender, message, time }); + if (chatMessages.length > MAX_CHAT_MESSAGES) { + chatMessages.shift(); + } + renderChatMessages(); +} + +export function renderChatMessages() { + const chatMessagesEl = document.getElementById('chatMessages'); + if (!chatMessagesEl) return; + + chatMessagesEl.innerHTML = chatMessages.map(m => + `
${m.time} ${m.sender}: ${m.message}
` + ).join(''); + + // Прокручиваем вниз + chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight; +} + +export function sendChatMessage(message) { + if (!message || message.trim() === '') return; + + if (state.isMultiplayer && state.socket && state.socket.connected) { + state.socket.emit('chat_message', { message: message.trim() }); + } else { + addChatMessage('Вы', message.trim()); + } +} + +export function initChat() { + document.getElementById('chatToggle').onclick = () => { + playSound('click'); + state.chatOpen = !state.chatOpen; + document.getElementById('chatPanel').style.display = state.chatOpen ? 'block' : 'none'; + if (state.chatOpen) { + document.getElementById('chatInput').focus(); + } + }; + + document.getElementById('chatClose').onclick = () => { + playSound('click'); + state.chatOpen = false; + document.getElementById('chatPanel').style.display = 'none'; + }; + + document.getElementById('chatSend').onclick = () => { + const input = document.getElementById('chatInput'); + sendChatMessage(input.value); + input.value = ''; + }; + + document.getElementById('chatInput').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + sendChatMessage(e.target.value); + e.target.value = ''; + } + }); +} \ No newline at end of file diff --git a/srcv3/ui/craft.js b/srcv3/ui/craft.js new file mode 100644 index 0000000..5d958c2 --- /dev/null +++ b/srcv3/ui/craft.js @@ -0,0 +1,114 @@ +// Craft UI +import { state } from '../core/state.js'; +import { BLOCKS } from '../data/blocks.js'; +import { ITEMS } from '../data/items.js'; +import { TOOLS } from '../data/tools.js'; +import { RECIPES } from '../data/recipes.js'; +import { addTool } from '../data/tools.js'; +import { tex, itemTex } from '../render/textures.js'; +import { playSound } from '../audio/sound-engine.js'; +import { rebuildHotbar } from './hotbar.js'; +import { renderInventory } from './inventory.js'; + +export function canCraft(r) { + const inv = state.inv; + for (const res in r.cost) { + if ((inv[res] || 0) < r.cost[res]) return false; + } + return true; +} + +export function renderCraft() { + const inv = state.inv; + const recipesEl = state.recipesEl; + recipesEl.innerHTML = ''; + for (const r of RECIPES) { + const row = document.createElement('div'); + row.className = 'recipe'; + const icon = document.createElement('div'); + icon.className = 'ricon'; + // Иконка результата — canvas sprite > emoji + if (tex[r.out]) { + icon.style.backgroundImage = `url(${tex[r.out].toDataURL()})`; + } else if (itemTex[r.out]) { + icon.style.backgroundImage = `url(${itemTex[r.out].toDataURL()})`; + } else if (TOOLS[r.out]) { + icon.textContent = TOOLS[r.out].icon || '🔧'; + icon.style.fontSize = '24px'; + icon.style.display = 'flex'; + icon.style.alignItems = 'center'; + icon.style.justifyContent = 'center'; + } else if (ITEMS[r.out]) { + icon.textContent = ITEMS[r.out].icon || '📦'; + icon.style.fontSize = '24px'; + icon.style.display = 'flex'; + icon.style.alignItems = 'center'; + icon.style.justifyContent = 'center'; + } + const info = document.createElement('div'); + info.className = 'rinfo'; + const nm = document.createElement('div'); + nm.className = 'rname'; + const itemName = BLOCKS[r.out]?.n || TOOLS[r.out]?.n || ITEMS[r.out]?.n || r.out; + nm.textContent = `${itemName} x${r.qty}`; + const cs = document.createElement('div'); + cs.className = 'rcost'; + cs.textContent = Object.keys(r.cost).map(x => { + const cn = BLOCKS[x]?.n || TOOLS[x]?.n || ITEMS[x]?.n || x; + return `${cn}: ${(inv[x] || 0)}/${r.cost[x]}`; + }).join(' '); + info.appendChild(nm); info.appendChild(cs); + const btn = document.createElement('button'); + btn.className = 'rcraft'; + btn.textContent = 'Создать'; + btn.disabled = !canCraft(r); + btn.onclick = () => { + if (!canCraft(r)) return; + playSound('click'); + for (const res in r.cost) inv[res] -= r.cost[res]; + inv[r.out] = (inv[r.out] || 0) + r.qty; + if (TOOLS[r.out]) addTool(r.out); + if (!state.recentItems.includes(r.out)) { state.recentItems.unshift(r.out); state.recentItems = state.recentItems.slice(0, 5); } + rebuildHotbar(); + renderCraft(); + renderInventory(); + }; + row.appendChild(icon); row.appendChild(info); row.appendChild(btn); + recipesEl.appendChild(row); + } +} + +let craftOpen = false; +let inventoryOpen = false; + +export function toggleCraft() { + playSound('click'); + craftOpen = !craftOpen; + state.craftPanel.style.display = craftOpen ? 'block' : 'none'; + if (craftOpen) { + renderCraft(); + inventoryOpen = false; + state.inventoryPanel.style.display = 'none'; + } +} + +export function closeCraft() { + playSound('click'); + craftOpen = false; + state.craftPanel.style.display = 'none'; +} + +export function toggleInventory() { + playSound('click'); + inventoryOpen = true; + state.inventoryPanel.style.display = 'block'; + renderInventory(); + craftOpen = false; + state.craftPanel.style.display = 'none'; +} + +export function closeInventory() { + playSound('click'); + inventoryOpen = false; + state.inventoryPanel.style.display = 'none'; +} diff --git a/srcv3/ui/dom-refs.js b/srcv3/ui/dom-refs.js new file mode 100644 index 0000000..a704ae1 --- /dev/null +++ b/srcv3/ui/dom-refs.js @@ -0,0 +1,18 @@ +// DOM-ссылки — все document.getElementById +export const hpEl = document.getElementById('hp'); +export const foodEl = document.getElementById('food'); +export const sxEl = document.getElementById('sx'); +export const syEl = document.getElementById('sy'); +export const todEl = document.getElementById('tod'); +export const worldIdEl = document.getElementById('worldId'); +export const playerCountEl = document.getElementById('playerCount'); +export const hotbarEl = document.getElementById('hotbar'); +export const craftPanel = document.getElementById('craftPanel'); +export const recipesEl = document.getElementById('recipes'); +export const deathEl = document.getElementById('death'); +export const inventoryPanel = document.getElementById('inventoryPanel'); +export const inventoryGrid = document.getElementById('inventoryGrid'); +export const marketPanel = document.getElementById('marketPanel'); +export const marketContent = document.getElementById('marketContent'); +export const tradePanel = document.getElementById('tradePanel'); +export const tradeContent = document.getElementById('tradeContent'); \ No newline at end of file diff --git a/srcv3/ui/furnace.js b/srcv3/ui/furnace.js new file mode 100644 index 0000000..28a81f2 --- /dev/null +++ b/srcv3/ui/furnace.js @@ -0,0 +1,126 @@ +// ==================== ПЕЧЬ (ОБЖИГ) ==================== +import { state } from '../core/state.js'; +import { BLOCKS } from '../data/blocks.js'; +import { ITEMS } from '../data/items.js'; +import { SMELTING_RECIPES } from '../data/recipes.js'; +import { playSound } from '../audio/sound-engine.js'; +import { rebuildHotbar } from '../ui/hotbar.js'; +import { getBlock } from '../world/world-storage3d.js'; + +const furnacePanel = document.getElementById('furnacePanel'); +const furnaceContent = document.getElementById('furnaceContent'); + +export function initFurnace() { + document.getElementById('furnaceClose').onclick = () => { + furnacePanel.style.display = 'none'; + state.currentFurnaceKey = null; + }; +} + +export function openFurnaceUI(gx, gy, gz) { + state.currentFurnaceKey = `${gx},${gy},${gz || 0}`; + furnacePanel.style.display = 'block'; + renderFurnaceUI(); +} + +export function renderFurnaceUI() { + if (!state.currentFurnaceKey) return; + + // Проверяем что печь всё ещё существует + const [fgx, fgy, fgz] = state.currentFurnaceKey.split(',').map(Number); + const fb = getBlock(fgx, fgy, fgz || 0); + if (!fb || fb.t !== 'furnace') { + furnacePanel.style.display = 'none'; + state.currentFurnaceKey = null; + return; + } + + // Текущий процесс обжига + const active = state.activeFurnaces.get(state.currentFurnaceKey); + + let html = '
'; + + // Доступные рецепты — показываем только те, для которых есть ресурсы + for (let i = 0; i < SMELTING_RECIPES.length; i++) { + const recipe = SMELTING_RECIPES[i]; + const haveCount = state.inv[recipe.in] || 0; + const canSmelt = haveCount >= recipe.qty; + + // Иконка результата + const outDef = BLOCKS[recipe.out]; + const outItem = ITEMS[recipe.out]; + const iconStr = outItem ? outItem.icon : (outDef ? '🧱' : '❓'); + const nameStr = outItem ? outItem.n : (outDef ? outDef.n : recipe.out); + const inItem = ITEMS[recipe.in]; + const inDef = BLOCKS[recipe.in]; + const inName = inItem ? inItem.n : (inDef ? inDef.n : recipe.in); + + html += `
`; + html += `
${iconStr}
`; + html += `
`; + html += `
${nameStr}
`; + html += `
${inName} x${recipe.qty} (есть: ${haveCount}) • ${recipe.time}с
`; + html += `
`; + html += ``; + html += `
`; + } + + // Текущий прогресс + if (active) { + const pct = Math.min(100, Math.floor((active.progress / active.recipe.time) * 100)); + html += `
`; + html += `
🔥 Обжиг: ${pct}%
`; + html += `
`; + html += `
`; + html += `
`; + } + + html += '
'; + furnaceContent.innerHTML = html; +} + +// Глобальная функция для кнопки обжига +window._smelt = (recipeIdx) => { + if (!state.currentFurnaceKey) return; + const recipe = SMELTING_RECIPES[recipeIdx]; + if ((state.inv[recipe.in] || 0) < recipe.qty) return; + + // Уже обжигаем в этой печи? + if (state.activeFurnaces.has(state.currentFurnaceKey)) return; + + // Забираем ресурсы + state.inv[recipe.in] -= recipe.qty; + + // Запускаем обжиг + state.activeFurnaces.set(state.currentFurnaceKey, { + recipe: recipe, + progress: 0 + }); + + playSound('fire'); + rebuildHotbar(); + renderFurnaceUI(); +}; + +// Тик печей — вызывается в главном цикле +export function tickFurnaces(dt) { + for (const [key, furnace] of state.activeFurnaces) { + furnace.progress += dt; + if (furnace.progress >= furnace.recipe.time) { + // Обжиг завершён — выдаём результат + const outItem = furnace.recipe.out; + if (ITEMS[outItem]) { + state.inv[outItem] = (state.inv[outItem] || 0) + furnace.recipe.outQty; + } else if (BLOCKS[outItem]) { + state.inv[outItem] = (state.inv[outItem] || 0) + furnace.recipe.outQty; + } + playSound('stone_build'); + state.activeFurnaces.delete(key); + + // Если эта печь открыта — обновляем UI + if (key === state.currentFurnaceKey) { + renderFurnaceUI(); + } + } + } +} \ No newline at end of file diff --git a/srcv3/ui/hotbar.js b/srcv3/ui/hotbar.js new file mode 100644 index 0000000..1b05e9d --- /dev/null +++ b/srcv3/ui/hotbar.js @@ -0,0 +1,151 @@ +// Hotbar UI — 3-slot Minecraft-style bottom bar + selected item indicator +import { state } from '../core/state.js'; +import { BLOCKS } from '../data/blocks.js'; +import { ITEMS } from '../data/items.js'; +import { TOOLS } from '../data/tools.js'; +import { tex, itemTex } from '../render/textures.js'; +import { playSound } from '../audio/sound-engine.js'; + +const HOTBAR_SLOTS = 3; + +let selectedItemEl = null; + +export function addToRecentItems(id) { + if (!id) return; + state.recentItems = state.recentItems.filter(item => item !== id); + state.recentItems.unshift(id); + state.recentItems = state.recentItems.slice(0, 5); +} + +function renderSlotIcon(el, id) { + if (BLOCKS[id] && tex[id]) { + el.style.backgroundImage = `url(${tex[id].toDataURL()})`; + el.style.backgroundSize = 'cover'; + } else if (itemTex[id]) { + el.style.backgroundImage = `url(${itemTex[id].toDataURL()})`; + el.style.backgroundSize = 'cover'; + } else if (ITEMS[id]) { + el.textContent = ITEMS[id].icon || '📦'; + } else if (TOOLS[id]) { + el.textContent = TOOLS[id].icon || '🔧'; + } else if (id === 'iron_armor') { + el.textContent = '🛡️'; + el.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)'; + } else { + el.textContent = id.charAt(0).toUpperCase(); + el.style.background = '#555'; + el.style.color = '#fff'; + el.style.fontWeight = '700'; + el.style.fontSize = '14px'; + } +} + +function updateSelectedItem() { + if (!selectedItemEl) selectedItemEl = document.getElementById('selectedItem'); + if (!selectedItemEl) return; + const sel = state.selected; + if (!sel || !state.inv[sel] || state.inv[sel] <= 0) { + selectedItemEl.style.display = 'none'; + return; + } + selectedItemEl.style.display = 'flex'; + // Clear previous content + selectedItemEl.textContent = ''; + selectedItemEl.style.backgroundImage = ''; + selectedItemEl.style.background = 'rgba(0,0,0,0.45)'; + // Render icon + if (BLOCKS[sel] && tex[sel]) { + selectedItemEl.style.backgroundImage = `url(${tex[sel].toDataURL()})`; + selectedItemEl.style.backgroundSize = 'cover'; + } else if (itemTex[sel]) { + selectedItemEl.style.backgroundImage = `url(${itemTex[sel].toDataURL()})`; + selectedItemEl.style.backgroundSize = 'cover'; + } else if (ITEMS[sel]) { + selectedItemEl.textContent = ITEMS[sel].icon || '📦'; + } else if (TOOLS[sel]) { + selectedItemEl.textContent = TOOLS[sel].icon || '🔧'; + } else { + selectedItemEl.textContent = sel.charAt(0).toUpperCase(); + } +} + +export function rebuildHotbar() { + const hotbarEl = state.hotbarEl; + const inv = state.inv; + const selected = state.selected; + const toolDurability = state.toolDurability; + + hotbarEl.innerHTML = ''; + + // Get up to 3 recent items with count > 0 + const items = state.recentItems.filter(id => inv[id] > 0).slice(0, HOTBAR_SLOTS); + + // If selected item not in list and has count > 0, push to front + if (inv[selected] > 0 && !items.includes(selected)) { + items.unshift(selected); + if (items.length > HOTBAR_SLOTS) items.pop(); + } + + // Always render HOTBAR_SLOTS slots (fill empties) + for (let si = 0; si < HOTBAR_SLOTS; si++) { + const id = items[si] || null; + const s = document.createElement('div'); + s.className = 'slot' + (id && id === selected ? ' sel' : ''); + + // Slot number indicator (top-left) + const numEl = document.createElement('div'); + numEl.style.cssText = 'position:absolute;top:1px;left:3px;font-size:10px;color:rgba(255,255,255,0.55);font-weight:700;pointer-events:none;text-shadow:1px 1px 1px rgba(0,0,0,0.7);'; + numEl.textContent = (si + 1).toString(); + s.appendChild(numEl); + + if (id) { + renderSlotIcon(s, id); + + const c = document.createElement('div'); + c.className = 'count'; + c.textContent = inv[id]; + s.appendChild(c); + s.onclick = () => { + playSound('click'); + state.selected = id; + // Move to front of recentItems + state.recentItems = state.recentItems.filter(item => item !== id); + state.recentItems.unshift(id); + rebuildHotbar(); + }; + + if (id === 'iron_armor' && state.player.equippedArmor === 'iron_armor') { + const equipped = document.createElement('div'); + equipped.className = 'equipped-indicator'; + equipped.textContent = '✓'; + s.appendChild(equipped); + } + + // Durability bar + if (TOOLS[id] && inv[id] > 0) { + let curDur = 0, maxDur = TOOLS[id].durability; + for (const [tid, dur] of toolDurability) { + if (dur.type === id) { + curDur = dur.current; + maxDur = dur.max; + break; + } + } + if (maxDur > 0) { + const bar = document.createElement('div'); + bar.style.cssText = `position:absolute;bottom:0;left:0;right:0;height:3px;background:#333;border-radius:0 0 6px 6px;overflow:hidden;`; + const fill = document.createElement('div'); + const pct = curDur / maxDur; + const color = pct > 0.5 ? '#2ecc71' : pct > 0.25 ? '#f39c12' : '#e74c3c'; + fill.style.cssText = `width:${pct * 100}%;height:100%;background:${color};`; + bar.appendChild(fill); + s.appendChild(bar); + } + } + } + hotbarEl.appendChild(s); + } + + // Update selected item indicator above hotbar + updateSelectedItem(); +} \ No newline at end of file diff --git a/srcv3/ui/inventory.js b/srcv3/ui/inventory.js new file mode 100644 index 0000000..7cf6dba --- /dev/null +++ b/srcv3/ui/inventory.js @@ -0,0 +1,80 @@ +// Inventory UI +import { state } from '../core/state.js'; +import { BLOCKS } from '../data/blocks.js'; +import { ITEMS } from '../data/items.js'; +import { TOOLS } from '../data/tools.js'; +import { tex, itemTex } from '../render/textures.js'; +import { playSound } from '../audio/sound-engine.js'; +import { rebuildHotbar } from './hotbar.js'; + +export function renderInventory() { + const inventoryGrid = state.inventoryGrid; + const inv = state.inv; + const selected = state.selected; + inventoryGrid.innerHTML = ''; + + const items = Object.keys(inv).filter(id => inv[id] > 0); + + for (let i = 0; i < 21; i++) { + const slot = document.createElement('div'); + slot.className = 'inv-slot' + (i < items.length && items[i] === selected ? ' sel' : ''); + + if (i < items.length) { + const id = items[i]; + // Canvas sprite: block > itemTex > emoji fallback > unknown item fallback + if (BLOCKS[id] && tex[id]) { + slot.style.backgroundImage = `url(${tex[id].toDataURL()})`; + slot.style.backgroundSize = 'cover'; + } else if (itemTex[id]) { + slot.style.backgroundImage = `url(${itemTex[id].toDataURL()})`; + slot.style.backgroundSize = 'cover'; + } else if (ITEMS[id]) { + slot.textContent = ITEMS[id].icon || '📦'; + } else if (TOOLS[id]) { + slot.textContent = TOOLS[id].icon || '🔧'; + } else if (id === 'iron_armor') { + slot.textContent = '🛡️'; + slot.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)'; + } else { + // Unknown item fallback — grey box with first letter + slot.textContent = id.charAt(0).toUpperCase(); + slot.style.background = '#555'; + slot.style.color = '#fff'; + slot.style.fontWeight = '700'; + slot.style.fontSize = '14px'; + } + + const count = document.createElement('div'); + count.className = 'inv-count'; + count.textContent = inv[id]; + slot.appendChild(count); + + slot.onclick = () => { + playSound('click'); + state.selected = id; + state.recentItems = state.recentItems.filter(item => item !== id); + state.recentItems.unshift(id); + state.recentItems = state.recentItems.slice(0, 5); + rebuildHotbar(); + renderInventory(); + }; + + // Двойной клик для надевания брони + slot.ondblclick = () => { + if (id === 'iron_armor' && inv.iron_armor > 0) { + if (state.player.equippedArmor === 'iron_armor') { + state.player.equippedArmor = null; + state.player.armor = 0; + } else { + state.player.equippedArmor = 'iron_armor'; + state.player.armor = BLOCKS['iron_armor'].armor; + } + playSound('click'); + renderInventory(); + } + }; + } + + inventoryGrid.appendChild(slot); + } +} diff --git a/srcv3/ui/market.js b/srcv3/ui/market.js new file mode 100644 index 0000000..d03dae1 --- /dev/null +++ b/srcv3/ui/market.js @@ -0,0 +1,84 @@ +// ==================== РЫНОК ПРЕДЛОЖЕНИЙ ==================== +import { state } from '../core/state.js'; +import { playSound } from '../audio/sound-engine.js'; + +let marketPanel, marketClose, marketContent; + +export function initMarket() { + marketPanel = document.getElementById('marketPanel'); + marketClose = document.getElementById('marketClose'); + marketContent = document.getElementById('marketContent'); + + if (!marketPanel) return; + + if (marketClose) marketClose.addEventListener('click', () => closeMarket()); + document.addEventListener('toggleMarket', () => toggleMarket()); +} + +export function toggleMarket() { + if (!marketPanel) return; + if (marketPanel.style.display === 'none' || !marketPanel.style.display) { + openMarket(); + } else { + closeMarket(); + } +} + +export function closeMarket() { + if (marketPanel) marketPanel.style.display = 'none'; +} + +function openMarket() { + if (marketPanel) { + marketPanel.style.display = 'block'; + renderMarket(); + } +} + +function renderMarket() { + if (!marketContent) return; + if (!state.socket) { + marketContent.innerHTML = '

Подключение к серверу...

'; + return; + } + + state.socket.emit('market_list', {}, (orders) => { + if (!orders || !orders.length) { + marketContent.innerHTML = '

Предложений пока нет

'; + const btn = document.getElementById('createOrderBtn'); + if (btn) btn.addEventListener('click', showCreateOrder); + return; + } + + let html = '
'; + html += orders.map(o => '
' + (o.offer_item||'') + ' x' + (o.offer_qty||1) + ' ⇄ ' + (o.want_item||'') + ' x' + (o.want_qty||1) + '
' + (o.seller || 'Аноним') + '
').join(''); + marketContent.innerHTML = html; + const btn = document.getElementById('createOrderBtn'); + if (btn) btn.addEventListener('click', showCreateOrder); + }); +} + +function showCreateOrder() { + if (!marketContent) return; + const invKeys = Object.keys(state.inv || {}); + const invOpts = invKeys.filter(k => (state.inv[k]||0) > 0).map(k => '').join(''); + + marketContent.innerHTML = '

Выставить предмет

'; + + const cancelBtn = document.getElementById('cancelOrder'); + if (cancelBtn) cancelBtn.addEventListener('click', () => renderMarket()); + const submitBtn = document.getElementById('submitOrder'); + if (submitBtn) submitBtn.addEventListener('click', () => { + const offerItem = document.getElementById('offerItem'); + const wantItem = document.getElementById('wantItem'); + if (!offerItem || !wantItem) return; + const offer = offerItem.value; + const want = wantItem.value.trim(); + if (!offer || !want) { alert('Выберите предмет и укажите что хотите получить'); return; } + if (state.socket) { + state.socket.emit('market_create', { offer_item: offer, offer_qty: 1, want_item: want, want_qty: 1 }); + playSound('click'); + setTimeout(renderMarket, 500); + } + }); +} diff --git a/srcv3/ui/minimap.js b/srcv3/ui/minimap.js new file mode 100644 index 0000000..0a66faf --- /dev/null +++ b/srcv3/ui/minimap.js @@ -0,0 +1,122 @@ +// ==================== МИНИКАРТА ==================== +import { state } from '../core/state.js'; +import { BLOCKS } from '../data/blocks.js'; +import { getBlock } from '../world/world-storage3d.js'; +import { playSound } from '../audio/sound-engine.js'; +import { isNight } from '../entities/mob-ai3d.js'; + +const minimapWrap = document.getElementById('minimapWrap'); +const minimapCanvas = document.getElementById('minimap'); +const minimapCtx = minimapCanvas.getContext('2d'); + +// Цвета блоков для миникарты (по 1 пикселю на блок) +const MINIMAP_COLORS = { + grass: '#4a8c2a', dirt: '#6b3410', stone: '#6a6a6a', sand: '#d4b84a', + gravel: '#888', clay: '#5a9ad4', wood: '#a63d00', planks: '#c46a10', + leaves: '#2a8a3a', glass: '#aaddee', water: '#2980b9', coal: '#1a1a2a', + copper_ore: '#a05535', iron_ore: '#b0b0b0', gold_ore: '#d4a017', + diamond_ore: '#0090d0', brick: '#8a2010', tnt: '#c02020', + campfire: '#d08020', torch: '#d0a020', bedrock: '#1a1a1a', + flower: '#d03050', bed: '#c03060', ladder: '#a04000', boat: '#6b3410' +}; + +export function initMinimap() { + document.getElementById('mapToggle').onclick = () => { + playSound('click'); + state.minimapOpen = !state.minimapOpen; + minimapWrap.style.display = state.minimapOpen ? 'block' : 'none'; + }; + + const minimapClose = document.getElementById('minimapClose'); + if (minimapClose) { + minimapClose.addEventListener('click', (e) => { + e.stopPropagation(); + state.minimapOpen = false; + minimapWrap.style.display = 'none'; + }); + } +} + +export function renderMinimap() { + if (!state.minimapOpen) return; + const mW = minimapCanvas.width; + const mH = minimapCanvas.height; + const scale = 2; // пикселей на блок + const TILE = state.TILE; + const player = state.player; + + // Область карты — центрирована на игроке + const pGX = Math.floor(player.x / TILE); + const pGY = Math.floor(player.y / TILE); + const viewW = Math.floor(mW / scale); + const viewH = Math.floor(mH / scale); + const startGX = pGX - Math.floor(viewW / 2); + const startGY = pGY - Math.floor(viewH / 2); + + // Очищаем + minimapCtx.fillStyle = isNight() ? '#070816' : '#87CEEB'; + minimapCtx.fillRect(0, 0, mW, mH); + + // Рисуем блоки + const imgData = minimapCtx.createImageData(mW, mH); + const data = imgData.data; + + for (let dx = 0; dx < viewW; dx++) { + for (let dy = 0; dy < viewH; dy++) { + const gx = startGX + dx; + const gy = startGY + dy; + const b = getBlock(gx, gy, 0); + if (!b || b.dead || b.t === 'air') continue; + + const color = MINIMAP_COLORS[b.t]; + if (!color) continue; + + // Парсим hex цвет + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const bl = parseInt(color.slice(5, 7), 16); + + // Заполняем scale x scale пикселей + for (let sx = 0; sx < scale; sx++) { + for (let sy = 0; sy < scale; sy++) { + const px = dx * scale + sx; + const py = dy * scale + sy; + if (px >= mW || py >= mH) continue; + const idx = (py * mW + px) * 4; + data[idx] = r; + data[idx + 1] = g; + data[idx + 2] = bl; + data[idx + 3] = 255; + } + } + } + } + + minimapCtx.putImageData(imgData, 0, 0); + + // Игрок — белый пиксель по центру + minimapCtx.fillStyle = '#fff'; + minimapCtx.fillRect(Math.floor(mW / 2) - 2, Math.floor(mH / 2) - 2, 4, 4); + + // Другие игроки — жёлтые точки + for (const [sid, p] of state.otherPlayers) { + const dx = Math.floor(p.x / TILE) - startGX; + const dy = Math.floor(p.y / TILE) - startGY; + if (dx >= 0 && dx < viewW && dy >= 0 && dy < viewH) { + minimapCtx.fillStyle = '#f1c40f'; + minimapCtx.fillRect(dx * scale - 1, dy * scale - 1, 3, 3); + } + } + + // Мобы — красные (враждебные) / зелёные (животные) + const allMobsForMap = state.isMultiplayer ? Array.from(state.serverMobs.values()) : state.mobs; + for (const m of allMobsForMap) { + const dx = Math.floor(m.x / TILE) - startGX; + const dy = Math.floor(m.y / TILE) - startGY; + if (dx >= 0 && dx < viewW && dy >= 0 && dy < viewH) { + const hostile = m.kind === 'zombie' || m.kind === 'creeper' || m.kind === 'skeleton'; + minimapCtx.fillStyle = hostile ? '#e74c3c' : '#2ecc71'; + minimapCtx.fillRect(dx * scale, dy * scale, 2, 2); + } + } +} diff --git a/srcv3/ui/respawn.js b/srcv3/ui/respawn.js new file mode 100644 index 0000000..17ab794 --- /dev/null +++ b/srcv3/ui/respawn.js @@ -0,0 +1,56 @@ +// ==================== RESPAWN ==================== +import { state } from '../core/state.js'; +import { playSound } from '../audio/sound-engine.js'; +import { loadGame, applySave } from '../game/save.js'; + +export function initRespawn() { + document.getElementById('respawnBtn').onclick = async () => { + playSound('click'); // Звук клика по кнопке + + console.log('=== RESPAWN CLICKED ==='); + console.log('isMultiplayer:', state.isMultiplayer); + console.log('otherPlayers.size:', state.otherPlayers.size); + console.log('player.hp before respawn:', state.player.hp); + + const player = state.player; + const deathEl = state.deathEl; + + // В мультиплеере не загружаем сохранение, а возрождаемся в начальной точке + if (state.isMultiplayer && state.otherPlayers.size > 0) { + console.log('Мультиплеер режим - возрождение в начальной точке'); + player.hp = 100; + player.hunger = 100; + player.o2 = 100; + player.vx = player.vy = 0; + player.invuln = 0; + player.x = state.spawnPoint.x; + player.y = state.spawnPoint.y; + player.fallStartY = player.y; + console.log('Возрождение в начальной точке, HP:', player.hp); + } else { + console.log('Одиночный режим - загружаем последнее сохранение'); + // Одиночный режим - загружаем последнее сохранение + const loadedSave = await loadGame(); + if (loadedSave) { + await applySave(loadedSave); + console.log('Загружено последнее сохранение после смерти, final HP:', player.hp); + } else { + // Если сохранения нет, возрождаемся в начальной точке + player.hp = 100; + player.hunger = 100; + player.o2 = 100; + player.vx = player.vy = 0; + player.invuln = 0; + player.x = state.spawnPoint.x; + player.y = state.spawnPoint.y; + player.fallStartY = player.y; + console.log('Возрождение в начальной точке, HP:', player.hp); + } + } + + console.log('player.hp after respawn logic:', player.hp); + console.log('Hiding death screen...'); + deathEl.style.display = 'none'; + console.log('=== RESPAWN END ==='); + }; +} \ No newline at end of file diff --git a/srcv3/ui/save-controls.js b/srcv3/ui/save-controls.js new file mode 100644 index 0000000..dfba743 --- /dev/null +++ b/srcv3/ui/save-controls.js @@ -0,0 +1,156 @@ +// ==================== МЕНЮ ⋯ + СОХРАНЕНИЕ / СБРОС ==================== +import { state } from '../core/state.js'; +import { playSound } from '../audio/sound-engine.js'; +import { saveGame } from '../game/save.js'; +import { shareWorld } from './share.js'; +import { isTgWebApp } from './start-menu.js'; + +let dropdownOpen = false; + +export function initSaveControls() { + const menuBtn = document.getElementById('menuBtn'); + const menuDropdown = document.getElementById('menuDropdown'); + const saveItem = document.getElementById('saveItem'); + const resetItem = document.getElementById('resetItem'); + const marketItem = document.getElementById('marketItem'); + const shareItem = document.getElementById('shareItem'); + const settingsItem = document.getElementById('settingsItem'); + const quitItem = document.getElementById('quitItem'); + + // Убить возможный старый onclick + if (menuBtn) menuBtn.onclick = null; + + // Тогл меню + if (menuBtn) { + menuBtn.addEventListener('click', (e) => { + e.stopPropagation(); + dropdownOpen = !dropdownOpen; + if (menuDropdown) menuDropdown.classList.toggle('open', dropdownOpen); + }); + } + + // Закрытие по клику вне + document.addEventListener('click', () => { + if (dropdownOpen) { + dropdownOpen = false; + if (menuDropdown) menuDropdown.classList.remove('open'); + } + }); + + if (menuDropdown) { + menuDropdown.addEventListener('click', (e) => e.stopPropagation()); + } + + // 💾 Сохранить + if (saveItem) { + saveItem.addEventListener('click', () => { + playSound('click'); + saveGame(); + dropdownOpen = false; + if (menuDropdown) menuDropdown.classList.remove('open'); + alert('Игра сохранена!'); + }); + } + + // 🔄 Новый мир + if (resetItem) { + resetItem.addEventListener('click', () => { + if (confirm('Вы уверены, что хотите удалить сохранение и начать новую игру?')) { + playSound('click'); + try { localStorage.removeItem(state.SAVE_KEY); } catch(e) {} + state.inMemorySave = null; + state.worldId = Math.random().toString(36).substring(2, 10); + try { + const newUrl = new URL(window.location.href); + newUrl.searchParams.set('world', state.worldId); + window.history.replaceState(null, '', newUrl.toString()); + } catch(e) {} + location.reload(); + } else { + dropdownOpen = false; + if (menuDropdown) menuDropdown.classList.remove('open'); + } + }); + } + + // 🏪 Рынок + if (marketItem) { + marketItem.addEventListener('click', () => { + dropdownOpen = false; + if (menuDropdown) menuDropdown.classList.remove('open'); + document.dispatchEvent(new CustomEvent('toggleMarket')); + }); + } + + // 📤 Пригласить + if (shareItem) { + shareItem.addEventListener('click', () => { + dropdownOpen = false; + if (menuDropdown) menuDropdown.classList.remove('open'); + shareWorld(); + }); + } + + // ⚙️ Настройки + if (settingsItem) { + settingsItem.addEventListener('click', () => { + dropdownOpen = false; + menuDropdown.classList.remove('open'); + const panel = document.getElementById('settingsPanel'); + if (!panel) return; + // Sync controls with current state + const toggle = document.getElementById('dayNightToggle'); + const timeSlider = document.getElementById('timeSlider'); + const wSlider = document.getElementById('weatherIntensity'); + if (toggle) toggle.checked = state.dayNightCycle !== false; + if (timeSlider) timeSlider.value = Math.round(state.worldTime * 100); + if (wSlider) wSlider.value = Math.round(state.rainIntensity * 100); + // Sync weather buttons — highlight the active type + const wt = state.weatherType || (state.isRaining ? 'rain' : 'clear'); + const activeId = 'weather' + wt.charAt(0).toUpperCase() + wt.slice(1); + for (const bid of ['weatherClear','weatherRain','weatherSnow','weatherThunder']) { + const b = document.getElementById(bid); + if (b) b.style.background = (bid === activeId) ? '#3498db' : '#555'; + } + panel.style.display = 'block'; + }); + } + + // Закрытие настроек + const settingsClose = document.getElementById('settingsClose'); + if (settingsClose) { + settingsClose.addEventListener('click', () => { + const settingsPanel = document.getElementById('settingsPanel'); + if (settingsPanel) settingsPanel.style.display = 'none'; + }); + } + + // Тогл день/ночи + const dayNightToggle = document.getElementById('dayNightToggle'); + if (dayNightToggle) { + dayNightToggle.addEventListener('change', () => { + state.dayNightCycle = dayNightToggle.checked; + saveGame(); + }); + } + + // 🏠 Главное меню + if (quitItem) { + quitItem.addEventListener('click', () => { + dropdownOpen = false; + if (menuDropdown) menuDropdown.classList.remove('open'); + saveGame(); + try { + const url = new URL(window.location.href); + url.searchParams.delete('world'); + window.location.href = url.toString(); + } catch(e) { location.reload(); } + }); + } +} + +export function updateSaveButtonVisibility() { + const saveItem = document.getElementById('saveItem'); + if (!saveItem) return; + saveItem.style.display = (state.isMultiplayer && state.otherPlayers.size > 0) ? 'none' : 'block'; +} diff --git a/srcv3/ui/settings.js b/srcv3/ui/settings.js new file mode 100644 index 0000000..0edb1af --- /dev/null +++ b/srcv3/ui/settings.js @@ -0,0 +1,127 @@ +// Настройки +import { state } from '../core/state.js'; + +let settingsPanel = null; +let dayNightToggle = null; + +// Weather button IDs in order +const WEATHER_BTNS = ['weatherClear', 'weatherRain', 'weatherSnow', 'weatherThunder']; +const WEATHER_ACTIVE_BG = '#3498db'; +const WEATHER_INACTIVE_BG = '#555'; + +function setWeatherButtonActive(activeId) { + for (const id of WEATHER_BTNS) { + const btn = document.getElementById(id); + if (btn) { + btn.style.background = (id === activeId) ? WEATHER_ACTIVE_BG : WEATHER_INACTIVE_BG; + } + } +} + +export function initSettings() { + settingsPanel = document.getElementById('settingsPanel'); + dayNightToggle = document.getElementById('dayNightToggle'); + + if (!settingsPanel) return; + + const closeBtn = document.getElementById('settingsClose'); + if (closeBtn) { + closeBtn.addEventListener('click', () => { + settingsPanel.style.display = 'none'; + }); + } + + // Day/night cycle toggle + if (dayNightToggle) { + dayNightToggle.checked = state.dayNightCycle !== false; + dayNightToggle.addEventListener('change', () => { + state.dayNightCycle = dayNightToggle.checked; + }); + } + + // Time-of-day slider + const timeSlider = document.getElementById('timeSlider'); + if (timeSlider) { + timeSlider.value = Math.round(state.worldTime * 100); + timeSlider.addEventListener('input', () => { + state.worldTime = parseInt(timeSlider.value) / 100; + if (state.dayNightCycle) { + state.dayNightCycle = false; + if (dayNightToggle) dayNightToggle.checked = false; + } + }); + } + + // ====== Weather controls (Ясно / Дождь / Снег / Гроза) ====== + const weatherIntensitySlider = document.getElementById('weatherIntensity'); + + // Helper: apply a weather type + function applyWeather(type) { + state.weatherType = type; + state.weatherChangeInterval = Infinity; // Lock weather manually + + switch (type) { + case 'clear': + state.isRaining = false; + state.rainIntensity = 0; + if (weatherIntensitySlider) weatherIntensitySlider.value = 0; + break; + case 'rain': + state.isRaining = true; + state.rainIntensity = parseInt(weatherIntensitySlider?.value || 40) / 100; + break; + case 'snow': + state.isRaining = false; // snow uses its own rendering + state.rainIntensity = parseInt(weatherIntensitySlider?.value || 40) / 100; + break; + case 'thunder': + state.isRaining = true; // thunder includes rain + state.rainIntensity = parseInt(weatherIntensitySlider?.value || 60) / 100; + if (weatherIntensitySlider) weatherIntensitySlider.value = Math.round(state.rainIntensity * 100); + break; + } + + setWeatherButtonActive('weather' + type.charAt(0).toUpperCase() + type.slice(1)); + } + + // Button handlers + for (const id of WEATHER_BTNS) { + const btn = document.getElementById(id); + if (btn) { + const weatherType = btn.dataset.weather || id.replace('weather', '').toLowerCase(); + btn.addEventListener('click', () => applyWeather(weatherType)); + } + } + + // Intensity slider + if (weatherIntensitySlider) { + weatherIntensitySlider.addEventListener('input', () => { + const val = parseInt(weatherIntensitySlider.value) / 100; + if (state.weatherType !== 'clear') { + state.rainIntensity = val; + if (state.weatherType === 'thunder' || state.weatherType === 'rain') { + state.isRaining = true; + } + } + }); + } + + // Listen for toggleSettings custom event + document.addEventListener('toggleSettings', () => { + if (settingsPanel) { + // Sync all controls when panel opens + if (timeSlider) timeSlider.value = Math.round(state.worldTime * 100); + if (dayNightToggle) dayNightToggle.checked = state.dayNightCycle !== false; + if (weatherIntensitySlider) weatherIntensitySlider.value = Math.round(state.rainIntensity * 100); + // Highlight active weather button + const wt = state.weatherType || (state.isRaining ? 'rain' : 'clear'); + const activeId = 'weather' + wt.charAt(0).toUpperCase() + wt.slice(1); + setWeatherButtonActive(activeId); + settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none'; + } + }); +} + +export function isDayNightEnabled() { + return state.dayNightCycle !== false; +} diff --git a/srcv3/ui/share.js b/srcv3/ui/share.js new file mode 100644 index 0000000..5ad9de4 --- /dev/null +++ b/srcv3/ui/share.js @@ -0,0 +1,108 @@ +// ==================== ПОДЕЛИТЬСЯ МИРОМ ==================== +import { state } from '../core/state.js'; +import { isTgWebApp } from './start-menu.js'; +import { showToast } from './start-menu.js'; + +export function initShare() { + // Keep worldId click-to-copy as before + const worldIdEl = document.getElementById('worldId'); + if (worldIdEl) { + worldIdEl.onclick = () => shareWorld(); + } +} + +export function shareWorld() { + const shareUrl = new URL(window.location.href); + shareUrl.searchParams.set('world', state.worldId); + const url = shareUrl.toString(); + + // In Telegram WebApp — use native share + if (isTgWebApp && window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.switchInlineQuery) { + const inviteUrl = 'https://t.me/' + (state.TELEGRAM_BOT_USERNAME || 'grechka_mkn8n_bot') + '/app?startapp=' + encodeURIComponent(state.worldId); + Telegram.WebApp.openTelegramLink( + 'https://t.me/share/url?url=' + encodeURIComponent(inviteUrl) + + '&text=' + encodeURIComponent('🏗️ Заходи в GrechkaCraft! Код мира: ' + state.worldId) + ); + return; + } + + // Otherwise — show custom modal with URL + copy button + const overlay = document.createElement('div'); + overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:100000;display:flex;align-items:center;justify-content:center;'; + + const box = document.createElement('div'); + box.style.cssText = 'background:#C6C6C6;border:4px solid #000;padding:20px;max-width:400px;width:90%;box-shadow:4px 4px 0 #000;font-family:Courier New,monospace;'; + + const title = document.createElement('div'); + title.textContent = '📤 Пригласить друзей'; + title.style.cssText = 'font-size:18px;font-weight:bold;margin-bottom:12px;color:#222;'; + box.appendChild(title); + + const codeLabel = document.createElement('div'); + codeLabel.style.cssText = 'font-size:13px;color:#444;font-weight:bold;margin-bottom:4px;'; + codeLabel.textContent = 'Код мира:'; + box.appendChild(codeLabel); + + const codeBox = document.createElement('div'); + codeBox.style.cssText = 'font-size:20px;font-weight:bold;background:#fff;border:3px solid #000;padding:8px 12px;margin-bottom:12px;letter-spacing:2px;text-align:center;color:#5A8F3C;'; + codeBox.textContent = state.worldId; + box.appendChild(codeBox); + + const linkLabel = document.createElement('div'); + linkLabel.style.cssText = 'font-size:13px;color:#444;font-weight:bold;margin-bottom:4px;'; + linkLabel.textContent = 'Ссылка:'; + box.appendChild(linkLabel); + + const linkBox = document.createElement('input'); + linkBox.type = 'text'; + linkBox.readOnly = true; + linkBox.value = url; + linkBox.style.cssText = 'width:100%;padding:8px;font-family:Courier New,monospace;font-size:13px;background:#fff;border:3px solid #000;color:#222;margin-bottom:12px;box-sizing:border-box;'; + linkBox.onclick = () => linkBox.select(); + box.appendChild(linkBox); + + const btnRow = document.createElement('div'); + btnRow.style.cssText = 'display:flex;gap:8px;'; + + const btnCopy = document.createElement('button'); + btnCopy.textContent = '📋 Скопировать'; + btnCopy.style.cssText = 'flex:1;padding:10px;font-family:Courier New,monospace;font-weight:bold;cursor:pointer;border:3px solid #000;background:#5A8F3C;color:#fff;box-shadow:2px 2px 0 #000;'; + btnCopy.onclick = () => { + navigator.clipboard.writeText(url).then(() => { + showToast('📋 Ссылка скопирована!'); + overlay.remove(); + }).catch(() => { + linkBox.select(); + document.execCommand('copy'); + showToast('📋 Ссылка скопирована!'); + overlay.remove(); + }); + }; + btnRow.appendChild(btnCopy); + + const btnCode = document.createElement('button'); + btnCode.textContent = '📋 Код'; + btnCode.style.cssText = 'padding:10px;font-family:Courier New,monospace;font-weight:bold;cursor:pointer;border:3px solid #000;background:#3C5F9E;color:#fff;box-shadow:2px 2px 0 #000;'; + btnCode.onclick = () => { + navigator.clipboard.writeText(state.worldId).then(() => { + showToast('📋 Код скопирован: ' + state.worldId); + }).catch(() => { + showToast('Код: ' + state.worldId); + }); + }; + btnRow.appendChild(btnCode); + + const btnClose = document.createElement('button'); + btnClose.textContent = '✕'; + btnClose.style.cssText = 'padding:10px 14px;font-family:Courier New,monospace;font-weight:bold;cursor:pointer;border:3px solid #000;background:#9E2B2B;color:#fff;box-shadow:2px 2px 0 #000;'; + btnClose.onclick = () => overlay.remove(); + btnRow.appendChild(btnClose); + + box.appendChild(btnRow); + overlay.appendChild(box); + + // Close on click outside box + overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); }; + + document.body.appendChild(overlay); +} diff --git a/srcv3/ui/start-menu.js b/srcv3/ui/start-menu.js new file mode 100644 index 0000000..436d9e8 --- /dev/null +++ b/srcv3/ui/start-menu.js @@ -0,0 +1,575 @@ +// ==================== START MENU ==================== +import { state } from '../core/state.js'; + +// Detect Telegram WebApp +const isTgWebApp = !!(window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData); +let tgUser = null; +let tgSaveLoaded = false; + +if (isTgWebApp) { + try { + Telegram.WebApp.ready(); + Telegram.WebApp.expand(); + tgUser = Telegram.WebApp.initDataUnsafe?.user || null; + console.log('[TG] Mini App detected, user:', tgUser?.id, tgUser?.username); + // Set player name from TG + if (tgUser && tgUser.first_name) { + const tgName = tgUser.username || tgUser.first_name; + localStorage.setItem('minegrechka_playerName', tgName); + } + } catch (e) { + console.warn('[TG] WebApp init error:', e); + } +} + +// Export TG info for other modules +export { isTgWebApp, tgUser }; + +// ==================== HELPERS ==================== + +function generateShortWorldCode() { + // Generate readable 6-char code: consonants + digits (easy to copy/share) + const c = 'bcdfghjkmnpqrstvwxyz'; + const d = '23456789'; + let code = ''; + for (let i = 0; i < 3; i++) code += c[Math.floor(Math.random() * c.length)]; + for (let i = 0; i < 3; i++) code += d[Math.floor(Math.random() * d.length)]; + return code; +} + +export function showToast(msg, duration) { + duration = duration || 1800; + const t = document.createElement('div'); + t.textContent = msg; + t.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.85);color:#2ecc71;padding:10px 24px;border-radius:10px;font:bold 16px Arial,sans-serif;z-index:99999;pointer-events:none;transition:opacity 0.4s;'; + document.body.appendChild(t); + setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 400); }, duration); +} + +function customAlert(msg) { + const overlay = document.createElement('div'); + overlay.className = 'custom-modal-overlay'; + overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:100000;'; + const box = document.createElement('div'); + box.style.cssText = 'background:#C6C6C6;border:4px solid #000;padding:24px;max-width:360px;width:90%;box-shadow:4px 4px 0 #000;font-family:Courier New,monospace;'; + const text = document.createElement('div'); + text.textContent = msg; + text.style.marginBottom = '16px'; + text.style.fontWeight = 'bold'; + text.style.fontSize = '16px'; + const btn = document.createElement('button'); + btn.textContent = 'OK'; + btn.style.cssText = 'padding:8px 24px;font-family:Courier New,monospace;font-weight:bold;cursor:pointer;border:3px solid #000;background:#5A8F3C;color:#fff;box-shadow:2px 2px 0 #000;'; + btn.onclick = () => overlay.remove(); + box.appendChild(text); + box.appendChild(btn); + overlay.appendChild(box); + document.body.appendChild(overlay); +} + +// ==================== MAIN START MENU ==================== + +// Minecraft-style CSS for the start menu +const mcCSS = ` + .mc-overlay { position:fixed;top:0;left:0;width:100%;height:100%;z-index:99999; + display:flex;align-items:center;justify-content:center;font-family:'Courier New',monospace; + image-rendering:pixelated; } + .mc-bg { position:absolute;inset:0;background:#000;opacity:0.75; } + .mc-dirt { position:absolute;bottom:0;left:0;right:0;height:40%; + background:repeating-linear-gradient(0deg,#8B6914 0px,#8B6914 2px,#7A5B12 2px,#7A5B12 4px);opacity:0.3; } + .mc-box { position:relative;z-index:1;background:#C6C6C6;border:4px solid #000; + padding:0;max-width:420px;width:92%;box-shadow:4px 4px 0 #000;max-height:90vh;display:flex;flex-direction:column; } + .mc-titlebar { background:#1a1a2e;padding:12px 16px;text-align:center; + border-bottom:4px solid #000; } + .mc-title { font-size:24px;font-weight:bold;color:#FFAA00;text-shadow:2px 2px 0 #000; + letter-spacing:2px; } + .mc-subtitle { font-size:13px;color:#AAA;margin-top:4px; } + .mc-body { padding:16px;background:#C6C6C6;max-height:80vh;overflow-y:auto;-webkit-overflow-scrolling:touch; } + .mc-btn { display:block;width:100%;padding:10px 16px;margin-bottom:8px; + font-family:'Courier New',monospace;font-size:16px;font-weight:bold; + cursor:pointer;text-align:center;border:3px solid #000; + image-rendering:pixelated;box-shadow:3px 3px 0 #000; + transition:transform 0.05s,box-shadow 0.05s; } + .mc-btn:active { transform:translate(2px,2px);box-shadow:1px 1px 0 #000; } + .mc-btn-green { background:#5A8F3C;color:#E0E0E0;border-color:#3A5F1C;text-shadow:1px 1px 0 #222; } + .mc-btn-green:hover { background:#6DAF4E; } + .mc-btn-blue { background:#3C5F9E;color:#E0E0E0;border-color:#2A4070;text-shadow:1px 1px 0 #222; } + .mc-btn-blue:hover { background:#4B73BE; } + .mc-btn-purple { background:#7B3FA0;color:#E0E0E0;border-color:#5A2C78;text-shadow:1px 1px 0 #222; } + .mc-btn-purple:hover { background:#9555BE; } + .mc-btn-orange { background:#B86E00;color:#E0E0E0;border-color:#8A5200;text-shadow:1px 1px 0 #222; } + .mc-btn-orange:hover { background:#DA8500; } + .mc-btn-red { background:#9E2B2B;color:#E0E0E0;border-color:#6E1B1B;text-shadow:1px 1px 0 #222; } + .mc-btn-red:hover { background:#BE3B3B; } + .mc-btn-gray { background:#555;color:#AAA;border-color:#333;text-shadow:1px 1px 0 #111; } + .mc-btn-gray:hover { background:#666;color:#DDD; } + .mc-btn-outline { background:transparent;color:#555;border-color:#555; + text-shadow:none;box-shadow:2px 2px 0 #333; } + .mc-btn-outline:hover { background:#DDD;color:#222; } + .mc-input { width:100%;padding:8px 12px;font-family:'Courier New',monospace; + font-size:16px;background:#FFF;border:3px solid #000;color:#222; + box-shadow:inset 2px 2px 0 #999;margin:4px 0;box-sizing:border-box; } + .mc-input:focus { outline:none;border-color:#FFAA00; } + .mc-label { font-size:13px;color:#444;font-weight:bold;margin:8px 0 4px; } + .mc-divider { border:none;border-top:2px solid #999;margin:12px 0; } + .mc-friend-row { display:flex;justify-content:space-between;align-items:center; + padding:6px 8px;margin-bottom:4px;background:#E8E8E8;border:2px solid #999; + box-shadow:2px 2px 0 #999; } + .mc-friend-btn { padding:4px 10px;font-family:'Courier New',monospace; + font-size:13px;font-weight:bold;cursor:pointer;border:2px solid #000; + box-shadow:1px 1px 0 #000; } + .mc-friend-btn:active { transform:translate(1px,1px);box-shadow:none; } + .mc-friend-btn-green { background:#5A8F3C;color:#E0E0E0; } + .mc-friend-btn-red { background:#9E2B2B;color:#E0E0E0; } + .mc-checkbox-row { display:flex;align-items:center;gap:8px;font-size:13px;color:#444;cursor:pointer; } + .mc-checkbox { width:18px;height:18px;accent-color:#5A8F3C; } +`; + +export function showStartMenu() { + return new Promise(async (resolve) => { + const urlParams = new URLSearchParams(window.location.search); + const worldParam = urlParams.get('world'); + const tgStartParam = (isTgWebApp && Telegram.WebApp.initDataUnsafe?.start_param) + ? Telegram.WebApp.initDataUnsafe.start_param.trim() + : null; + + // If world is in URL or TG start param — skip menu, auto-join + if ((worldParam && worldParam.trim() !== '') || tgStartParam) { + const worldId = (worldParam && worldParam.trim()) || tgStartParam; + console.log('[Menu] Joining world from URL/startParam:', worldId); + + // Set player name from TG or localStorage or default + if (!state.playerName) { + if (isTgWebApp && tgUser) { + state.playerName = tgUser.username || tgUser.first_name || 'Игрок'; + } else { + state.playerName = localStorage.getItem('minegrechka_playerName') || 'Игрок'; + } + localStorage.setItem('minegrechka_playerName', state.playerName); + } + + resolve(worldId); + return; + } + + // Inject CSS + const styleEl = document.createElement('style'); + styleEl.textContent = mcCSS; + document.head.appendChild(styleEl); + + // Build overlay + const overlay = document.createElement('div'); + overlay.className = 'mc-overlay'; + overlay.id = 'startMenu'; + overlay.innerHTML = '
'; + + const box = document.createElement('div'); + box.className = 'mc-box'; + + // Title bar + const titleBar = document.createElement('div'); + titleBar.className = 'mc-titlebar'; + titleBar.innerHTML = '
⛏️ GrechkaCraft
' + + '
' + (isTgWebApp ? ('Привет, ' + (tgUser?.first_name || 'Игрок') + '!') : 'Voxel-мир в браузере') + '
'; + box.appendChild(titleBar); + + const body = document.createElement('div'); + body.className = 'mc-body'; + + // Mutable worldId holder + let selectedWorldId = null; + + // === Buttons === + + // 🌍 New World + const btnNew = document.createElement('button'); + btnNew.className = 'mc-btn mc-btn-green'; + btnNew.textContent = '🌍 Новый мир'; + btnNew.onclick = () => { + selectedWorldId = generateShortWorldCode(); + overlay.remove(); + styleEl.remove(); + resolve(selectedWorldId); + }; + body.appendChild(btnNew); + + // 🔗 Join by code + const btnJoin = document.createElement('button'); + btnJoin.className = 'mc-btn mc-btn-blue'; + btnJoin.textContent = '🔗 Вступить по коду'; + btnJoin.onclick = () => { + body.innerHTML = ''; + const lbl = document.createElement('div'); + lbl.className = 'mc-label'; + lbl.textContent = 'Код мира:'; + body.appendChild(lbl); + const codeInput = document.createElement('input'); + codeInput.className = 'mc-input'; + codeInput.type = 'text'; + codeInput.placeholder = 'bkh237'; + codeInput.maxLength = 12; + body.appendChild(codeInput); + const btnGo = document.createElement('button'); + btnGo.className = 'mc-btn mc-btn-blue'; + btnGo.textContent = 'Войти →'; + btnGo.onclick = () => { + const code = codeInput.value.trim().toLowerCase(); + if (code.length >= 3) { + selectedWorldId = code; + overlay.remove(); + styleEl.remove(); + resolve(selectedWorldId); + } + }; + body.appendChild(btnGo); + const btnBack = document.createElement('button'); + btnBack.className = 'mc-btn mc-btn-gray'; + btnBack.textContent = '← Назад'; + btnBack.onclick = () => { overlay.remove(); styleEl.remove(); showStartMenu().then(resolve); }; + body.appendChild(btnBack); + codeInput.focus(); + codeInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') btnGo.click(); }); + }; + body.appendChild(btnJoin); + + // 💾 Continue saved game + const hasLocalSave = !!localStorage.getItem(state.SAVE_KEY || 'minegrechka_save'); + if (isTgWebApp || hasLocalSave) { + const btnHistory = document.createElement('button'); + btnHistory.className = 'mc-btn mc-btn-purple'; + btnHistory.textContent = '💾 Продолжить игру'; + btnHistory.onclick = async () => { + if (isTgWebApp && tgUser) { + try { + const resp = await fetch(state.SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id); + const data = await resp.json(); + if (data.ok && data.world_id) { + selectedWorldId = data.world_id; + tgSaveLoaded = true; + overlay.remove(); + styleEl.remove(); + resolve(selectedWorldId); + return; + } + } catch (e) { console.warn('[TG] Load save failed:', e); } + } + if (hasLocalSave) { + // Continue with a new world code (the save will be loaded separately by initDB/loadGame) + selectedWorldId = generateShortWorldCode(); + overlay.remove(); + styleEl.remove(); + resolve(selectedWorldId); + return; + } + customAlert('Сохранений не найдено'); + }; + body.appendChild(btnHistory); + } + + // === TG-only buttons === + if (isTgWebApp && window.Telegram && Telegram.WebApp.switchInlineQuery) { + // 📤 Invite friends + const btnInvite = document.createElement('button'); + btnInvite.className = 'mc-btn mc-btn-outline'; + btnInvite.style.marginTop = '16px'; + btnInvite.textContent = '📤 Пригласить друзей'; + btnInvite.onclick = () => { + const code = selectedWorldId || generateShortWorldCode(); + const url = 'https://t.me/' + state.TELEGRAM_BOT_USERNAME + '/app?startapp=' + encodeURIComponent(code); + if (Telegram.WebApp && Telegram.WebApp.openTelegramLink) { + Telegram.WebApp.openTelegramLink('https://t.me/share/url?url=' + encodeURIComponent(url) + '&text=' + encodeURIComponent('🏗️ Заходи в GrechkaCraft! Мир: ' + code)); + } else { + navigator.clipboard.writeText(url).then(() => showToast('📋 Скопировано!')).catch(() => showToast(url)); + } + }; + body.appendChild(btnInvite); + + // 👥 Friends + const btnFriends = document.createElement('button'); + btnFriends.className = 'mc-btn mc-btn-orange'; + btnFriends.style.marginTop = '8px'; + btnFriends.textContent = '👥 Друзья'; + btnFriends.onclick = async () => { + try { + const resp = await fetch(state.SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id); + const data = await resp.json(); + body.innerHTML = ''; + + // Back button at top + const btnBackTop = document.createElement('button'); + btnBackTop.className = 'mc-btn mc-btn-gray'; + btnBackTop.textContent = '← Назад'; + btnBackTop.style.marginBottom = '12px'; + btnBackTop.onclick = () => { overlay.remove(); styleEl.remove(); showStartMenu().then(resolve); }; + body.appendChild(btnBackTop); + + // Nickname section + if (data.nickname) { + const nickDiv = document.createElement('div'); + nickDiv.className = 'mc-friend-row'; + nickDiv.innerHTML = '🔗 Твой ник: ' + data.nickname + ''; + nickDiv.style.background = '#D5E8C5'; + body.appendChild(nickDiv); + } else { + const nickLabel = document.createElement('div'); + nickLabel.className = 'mc-label'; + nickLabel.style.color = '#9E2B2B'; + nickLabel.textContent = '⚡ Задай ник, чтобы друзья могли тебя найти:'; + body.appendChild(nickLabel); + const nickRow = document.createElement('div'); + nickRow.style.display = 'flex'; + nickRow.style.gap = '4px'; + nickRow.style.marginBottom = '8px'; + const nickInput = document.createElement('input'); + nickInput.className = 'mc-input'; + nickInput.style.flex = '1'; + nickInput.type = 'text'; + nickInput.maxLength = 16; + nickInput.placeholder = 'Твой ник'; + const nickBtn = document.createElement('button'); + nickBtn.className = 'mc-btn mc-btn-green'; + nickBtn.style.width = 'auto'; + nickBtn.style.marginBottom = '0'; + nickBtn.style.padding = '8px 16px'; + nickBtn.textContent = '✓'; + nickBtn.onclick = async () => { + const n = nickInput.value.trim().toLowerCase(); + if (!n || n.length < 2) { showToast('❌ Минимум 2 символа'); return; } + const check = await fetch(state.SERVER_URL + '/api/nick/check?nick=' + encodeURIComponent(n)); + const cd = await check.json(); + if (!cd.available) { showToast('❌ Ник занят'); return; } + const setResp = await fetch(state.SERVER_URL + '/api/nick/set', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tg_id: tgUser.id, nickname: n }) + }); + const sd = await setResp.json(); + if (sd.ok) { showToast('✅ Ник: ' + sd.nickname); btnFriends.onclick(); } else { showToast('❌ Ошибка'); } + }; + nickRow.appendChild(nickInput); + nickRow.appendChild(nickBtn); + body.appendChild(nickRow); + } + + // Friends list + if (data.friends && data.friends.length) { + const listLabel = document.createElement('div'); + listLabel.className = 'mc-label'; + listLabel.textContent = '👥 Друзья:'; + body.appendChild(listLabel); + for (const f of data.friends) { + const fDiv = document.createElement('div'); + fDiv.className = 'mc-friend-row'; + const statusDot = f.online ? '🟢' : '⚫'; + let info = statusDot + ' ' + f.nickname + ''; + if (f.online && f.world_id) info += ' — мир ' + f.world_id; + const fInfo = document.createElement('div'); + fInfo.innerHTML = info; + fDiv.appendChild(fInfo); + if (f.online && f.world_id) { + const joinBtn = document.createElement('button'); + joinBtn.className = 'mc-friend-btn mc-friend-btn-green'; + joinBtn.textContent = '🎮 Вступить'; + joinBtn.onclick = () => { + selectedWorldId = f.world_id; + overlay.remove(); + styleEl.remove(); + resolve(selectedWorldId); + }; + fDiv.appendChild(joinBtn); + } + body.appendChild(fDiv); + } + } else if (data.friends && !data.friends.length) { + const noFriends = document.createElement('div'); + noFriends.className = 'mc-label'; + noFriends.style.color = '#888'; + noFriends.textContent = 'Пока нет друзей'; + body.appendChild(noFriends); + } + + // Pending friend requests + if (data.pending && data.pending.length) { + const pLabel = document.createElement('div'); + pLabel.className = 'mc-label'; + pLabel.style.color = '#B86E00'; + pLabel.textContent = '📨 Запросы в друзья:'; + body.appendChild(pLabel); + for (const p of data.pending) { + const pItem = document.createElement('div'); + pItem.className = 'mc-friend-row'; + const span = document.createElement('span'); + span.textContent = p.from; + pItem.appendChild(span); + const btns = document.createElement('div'); + btns.style.display = 'flex'; + btns.style.gap = '4px'; + const acceptBtn = document.createElement('button'); + acceptBtn.className = 'mc-friend-btn mc-friend-btn-green'; + acceptBtn.textContent = '✓'; + acceptBtn.onclick = async () => { + const r = await fetch(state.SERVER_URL + '/api/friends/accept', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tg_id: tgUser.id, request_id: p.id }) + }); + const d = await r.json(); + if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.onclick(); } else { showToast('❌ Ошибка'); } + }; + const declineBtn = document.createElement('button'); + declineBtn.className = 'mc-friend-btn mc-friend-btn-red'; + declineBtn.textContent = '✕'; + declineBtn.onclick = async () => { + await fetch(state.SERVER_URL + '/api/friends/decline', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tg_id: tgUser.id, request_id: p.id }) + }); + showToast('Отклонено'); + btnFriends.onclick(); + }; + btns.appendChild(acceptBtn); + btns.appendChild(declineBtn); + pItem.appendChild(btns); + body.appendChild(pItem); + } + } + + // Add friend + body.appendChild(document.createElement('hr')); + const addLabel = document.createElement('div'); + addLabel.className = 'mc-label'; + addLabel.textContent = '➕ Добавить друга по нику:'; + body.appendChild(addLabel); + const addRow = document.createElement('div'); + addRow.style.display = 'flex'; + addRow.style.gap = '4px'; + const addInput = document.createElement('input'); + addInput.className = 'mc-input'; + addInput.style.flex = '1'; + addInput.type = 'text'; + addInput.maxLength = 16; + addInput.placeholder = 'Ник друга'; + const addBtn = document.createElement('button'); + addBtn.className = 'mc-btn mc-btn-blue'; + addBtn.style.width = 'auto'; + addBtn.style.marginBottom = '0'; + addBtn.style.padding = '8px 16px'; + addBtn.textContent = '+'; + addBtn.onclick = async () => { + const nick = addInput.value.trim().toLowerCase(); + if (!nick || nick.length < 2) { showToast('❌ Минимум 2 символа'); return; } + const r = await fetch(state.SERVER_URL + '/api/friends/request', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tg_id: tgUser.id, nickname: nick }) + }); + const d = await r.json(); + if (d.ok) { showToast(d.message === 'Already friends' ? '✅ Уже друзья!' : '📨 Запрос отправлен!'); } + else { showToast('❌ ' + (d.error || 'Ошибка')); } + }; + addRow.appendChild(addInput); + addRow.appendChild(addBtn); + body.appendChild(addRow); + + // Privacy checkboxes + const settings = data.settings || { allow_requests: 1, notify_online: 1 }; + const settingsDiv = document.createElement('div'); + settingsDiv.style.marginTop = '12px'; + const reqLabel = document.createElement('label'); + reqLabel.className = 'mc-checkbox-row'; + const reqCb = document.createElement('input'); + reqCb.className = 'mc-checkbox'; + reqCb.type = 'checkbox'; + reqCb.checked = settings.allow_requests; + reqCb.onchange = async () => { + await fetch(state.SERVER_URL + '/api/friends/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + tg_id: tgUser.id, + allow_requests: reqCb.checked ? 1 : 0, + notify_online: settings.notify_online + }) + }); + }; + reqLabel.appendChild(reqCb); + reqLabel.appendChild(document.createTextNode(' Запросы в друзья')); + settingsDiv.appendChild(reqLabel); + body.appendChild(settingsDiv); + } catch (e) { showToast('❌ Ошибка загрузки друзей'); console.error(e); } + }; + body.appendChild(btnFriends); + } + + // Player name input — always visible, with Play button + { + const nameDivider = document.createElement('hr'); + nameDivider.className = 'mc-divider'; + body.appendChild(nameDivider); + const nameLabel = document.createElement('div'); + nameLabel.className = 'mc-label'; + nameLabel.textContent = 'Твоё имя:'; + body.appendChild(nameLabel); + const nameRow = document.createElement('div'); + nameRow.style.cssText = 'display:flex;gap:6px;'; + const nameInput = document.createElement('input'); + nameInput.className = 'mc-input'; + nameInput.style.flex = '1'; + nameInput.type = 'text'; + nameInput.placeholder = 'Игрок'; + nameInput.value = state.playerName || localStorage.getItem('minegrechka_playerName') || (tgUser ? (tgUser.username || tgUser.first_name || '') : ''); + nameInput.maxLength = 20; + nameRow.appendChild(nameInput); + + const playBtn = document.createElement('button'); + playBtn.className = 'mc-btn mc-btn-green'; + playBtn.style.cssText = 'width:auto;margin-bottom:0;padding:10px 20px;white-space:nowrap;'; + playBtn.textContent = '▶ Играть'; + playBtn.onclick = () => { + const name = nameInput.value.trim() || 'Игрок'; + state.playerName = name; + localStorage.setItem('minegrechka_playerName', name); + // If no worldId yet, generate one and start + if (!selectedWorldId && !state.worldId) { + selectedWorldId = generateShortWorldCode(); + } + overlay.remove(); + styleEl.remove(); + resolve(selectedWorldId || state.worldId); + }; + nameRow.appendChild(playBtn); + body.appendChild(nameRow); + + // Allow Enter key to start game + nameInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') playBtn.click(); + }); + + // Auto-focus input on mobile + setTimeout(() => nameInput.focus(), 100); + } + + box.appendChild(body); + overlay.appendChild(box); + document.body.appendChild(overlay); + + // Set default player name if still empty + if (!state.playerName) { + state.playerName = localStorage.getItem('minegrechka_playerName') || 'Игрок'; + localStorage.setItem('minegrechka_playerName', state.playerName); + } + + // Auto-load TG save info (background fetch) + if (isTgWebApp && tgUser && !worldParam) { + fetch(state.SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id) + .then(r => r.json()) + .then(data => { + if (data.ok && data.world_id) { + // could update UI but menu is already shown + } + }) + .catch(() => {}); + } + }); +} diff --git a/srcv3/ui/trade.js b/srcv3/ui/trade.js new file mode 100644 index 0000000..6cc19bd --- /dev/null +++ b/srcv3/ui/trade.js @@ -0,0 +1,277 @@ +// Trade UI — личная торговля (лицом к лицу) +import { state } from '../core/state.js'; +import { BLOCKS } from '../data/blocks.js'; +import { ITEMS } from '../data/items.js'; +import { TOOLS } from '../data/tools.js'; +import { playSound } from '../audio/sound-engine.js'; +import { addChatMessage } from './chat.js'; + +function itemName(id) { + return BLOCKS[id]?.n || TOOLS[id]?.n || ITEMS[id]?.n || id; +} +function itemIcon(id) { + if (BLOCKS[id]) return '🟫'; + if (TOOLS[id]) return TOOLS[id].icon || '🔧'; + if (ITEMS[id]) return ITEMS[id].icon || '📦'; + return '📦'; +} + +let tradeOpen = false; +let currentTradeId = null; +let myOffer = {}; +let partnerOffer = {}; +let iConfirmed = false; + +export function isTradeOpen() { return tradeOpen; } + +export function initTradeSocket(socket) { + // Incoming trade request + socket.on('trade_request', (data) => { + playSound('click'); + // Show trade request modal + const overlay = document.createElement('div'); + overlay.id = 'tradeRequestOverlay'; + overlay.className = 'custom-modal-overlay'; + overlay.innerHTML = ` +
+
🤝 ${data.from_name} хочет торговать
+ +
`; + document.getElementById('game').appendChild(overlay); + + document.getElementById('tradeAcceptBtn').addEventListener('click', () => { + overlay.remove(); + socket.emit('trade_accept', { from_socket_id: data.from }); + }); + document.getElementById('tradeDeclineBtn').addEventListener('click', () => { + overlay.remove(); + socket.emit('trade_decline', { from_socket_id: data.from }); + }); + }); + + socket.on('trade_start', (data) => { + currentTradeId = data.trade_id; + myOffer = {}; + partnerOffer = {}; + iConfirmed = false; + tradeOpen = true; + renderTradeUI(data.partner_name); + state.tradePanel.style.display = 'block'; + }); + + socket.on('trade_update', (data) => { + partnerOffer = data.player1Offer || data.player2Offer || {}; + // Determine which offer is the partner's + const isP1 = state.socket && state.socket.id === data.player1Offer; + // The partner's offer is the one we didn't send + // Actually the server sends both — we figure out which is ours + // Simpler: just use the data directly + if (state.mySocketId) { + // We need to know which offer in the update is ours vs partner's + // The trade_update event doesn't tell us, but we know myOffer already + // So partnerOffer = the one that's not myOffer + } + renderTradeOffers(); + }); + + socket.on('trade_status', (data) => { + // Update confirm buttons + const confirmBtn = document.getElementById('tradeConfirmBtn'); + if (confirmBtn) { + const amIPlayer1 = true; // We need to track this + if (iConfirmed) { + confirmBtn.textContent = '✅ Ожидание...'; + confirmBtn.disabled = true; + confirmBtn.style.background = '#555'; + } + } + // Show partner confirmation status + const partnerStatus = document.getElementById('partnerConfirmStatus'); + if (partnerStatus) { + const partnerConfirmed = data.player1Confirm || data.player2Confirm; + partnerStatus.textContent = partnerConfirmed ? '✅ Подтвердил' : '⏳ Ещё не подтвердил'; + } + }); + + socket.on('trade_complete', (data) => { + // Add received items to inventory + const received = data.received || {}; + for (const [item, qty] of Object.entries(received)) { + if (qty > 0) { + state.inv[item] = (state.inv[item] || 0) + qty; + } + } + // Remove sent items from inventory + const sent = data.sent || {}; + for (const [item, qty] of Object.entries(sent)) { + if (qty > 0) { + state.inv[item] = (state.inv[item] || 0) - qty; + if (state.inv[item] < 0) state.inv[item] = 0; + } + } + addChatMessage('Система', '🤝 Сделка завершена!'); + closeTrade(); + }); + + socket.on('trade_declined', () => { + addChatMessage('Система', 'Торговля отклонена'); + closeTrade(); + }); + + socket.on('trade_cancelled', () => { + addChatMessage('Система', 'Торговля отменена'); + closeTrade(); + }); + + socket.on('trade_error', (data) => { + addChatMessage('Система', '❌ ' + data.msg); + }); +} + +function renderTradeUI(partnerName) { + const panel = state.tradePanel; + const content = state.tradeContent; + content.innerHTML = ` +
🤝 Торговля с ${partnerName}
+
+
+
Вы предлагаете:
+
+ +
+
⇄
+
+
Партнёр предлагает:
+
+ Ожидание... +
+
+
+
⏳ Партнёр ещё не подтвердил
+
+ + +
`; + + document.getElementById('tradeAddItemBtn')?.addEventListener('click', showAddItemDialog); + document.getElementById('tradeConfirmBtn')?.addEventListener('click', confirmTrade); + document.getElementById('tradeCancelBtn')?.addEventListener('click', cancelTrade); +} + +function renderTradeOffers() { + const myItems = document.getElementById('myOfferItems'); + if (myItems) { + const keys = Object.keys(myOffer).filter(k => myOffer[k] > 0); + if (keys.length === 0) { + myItems.innerHTML = 'Ничего'; + } else { + myItems.innerHTML = keys.map(k => `
${itemIcon(k)} ${itemName(k)} x${myOffer[k]}
`).join(''); + } + } +} + +function showAddItemDialog() { + const inv = state.inv; + const items = Object.keys(inv).filter(id => inv[id] > 0); + if (items.length === 0) { + addChatMessage('Система', 'Инвентарь пуст!'); + return; + } + + const overlay = document.createElement('div'); + overlay.id = 'tradeItemPicker'; + overlay.className = 'custom-modal-overlay'; + let html = '
Что предложить?
'; + for (const id of items) { + // Show already offered count + const offered = myOffer[id] || 0; + const available = inv[id] - offered; + if (available <= 0) continue; + html += `
+ ${itemIcon(id)} + ${itemName(id)} + x${available} +
`; + } + html += '
'; + overlay.innerHTML = html; + document.getElementById('game').appendChild(overlay); + + overlay.querySelectorAll('.trade-pick-item').forEach(el => { + el.addEventListener('click', () => { + const itemId = el.dataset.item; + // Ask quantity + const available = inv[itemId] - (myOffer[itemId] || 0); + const qty = Math.min(available, available > 1 ? promptForQty(itemId, available) : 1); + if (qty > 0) { + myOffer[itemId] = (myOffer[itemId] || 0) + qty; + renderTradeOffers(); + // Reset confirm when offer changes + iConfirmed = false; + const btn = document.getElementById('tradeConfirmBtn'); + if (btn) { btn.textContent = '✅ Подтвердить'; btn.disabled = false; btn.style.background = '#2ecc71'; } + } + overlay.remove(); + }); + }); +} + +function promptForQty(itemId, max) { + // Simple: just use 1 for now, can enhance later with a proper dialog + const val = parseInt(prompt(`Сколько ${itemName(itemId)}? (макс: ${max})`, '1')); + return isNaN(val) || val < 1 ? 1 : Math.min(val, max); +} + +function confirmTrade() { + if (!currentTradeId || !state.socket) return; + + // Deduct offered items from inventory + for (const [item, qty] of Object.entries(myOffer)) { + if (qty > 0) { + if (state.inv[item] < qty) { + addChatMessage('Система', `Недостаточно ${itemName(item)}!`); + return; + } + } + } + + // Send offer + confirm + state.socket.emit('trade_offer', { trade_id: currentTradeId, offer: myOffer }); + + // Small delay then confirm + setTimeout(() => { + state.socket.emit('trade_confirm', { trade_id: currentTradeId }); + iConfirmed = true; + const btn = document.getElementById('tradeConfirmBtn'); + if (btn) { btn.textContent = '✅ Ожидание...'; btn.disabled = true; btn.style.background = '#555'; } + }, 200); +} + +function cancelTrade() { + if (!currentTradeId || !state.socket) return; + state.socket.emit('trade_cancel', { trade_id: currentTradeId }); + closeTrade(); +} + +export function closeTrade() { + tradeOpen = false; + currentTradeId = null; + myOffer = {}; + partnerOffer = {}; + iConfirmed = false; + if (state.tradePanel) state.tradePanel.style.display = 'none'; + // Remove any pick overlay + document.getElementById('tradeItemPicker')?.remove(); +} + +export function requestTradeWithPlayer(targetName) { + if (!state.socket || !state.isMultiplayer) { + addChatMessage('Система', 'Торговля доступна только в мультиплеере'); + return; + } + state.socket.emit('trade_request', { target_name: targetName }); + addChatMessage('Система', `Запрос отправлен ${targetName}...`); +} \ No newline at end of file diff --git a/srcv3/world/generation3d.js b/srcv3/world/generation3d.js new file mode 100644 index 0000000..a7effd1 --- /dev/null +++ b/srcv3/world/generation3d.js @@ -0,0 +1,238 @@ +// ==================== ГЕНЕРАЦИЯ 3D — GrechkaCraft v3 ==================== +// Объёмный мир: столбцы (x,z), рельеф из 2D-шума, пещеры, руды, растительность. +// Та же система биомов, что в 2D-игре, но по двум координатам. +import { TILE, SEA_GY, BEDROCK_GY, GEN_MARGIN_X } from '../core/constants.js'; +import { state } from '../core/state.js'; +import { getBlock, setBlock, k3 } from './world-storage3d.js'; + +// Радиус мира по Z — насколько далеко вбок генерируем вокруг игрока +export const WORLD_DEPTH = 20; // полу-ширина: z от -20 до +20 +const CHUNK = 8; // шаг генерации + +// ========== ШУМ (детерминированный, от worldSeed) ========== +function hash2(x, z) { + let h = x * 374761393 + z * 668265263 + state.worldSeed * 1274126177; + h = (h ^ (h >> 13)) * 1274126177; + return ((h ^ (h >> 16)) >>> 0) / 4294967295; +} + +function smooth(t) { return t * t * (3 - 2 * t); } + +function valueNoise2(x, z, scale) { + const fx = x / scale, fz = z / scale; + const x0 = Math.floor(fx), z0 = Math.floor(fz); + const tx = smooth(fx - x0), tz = smooth(fz - z0); + const a = hash2(x0, z0), b = hash2(x0 + 1, z0); + const c = hash2(x0, z0 + 1), d = hash2(x0 + 1, z0 + 1); + return (a * (1 - tx) + b * tx) * (1 - tz) + (c * (1 - tx) + d * tx) * tz; +} + +function fbm2(x, z, octaves = 4) { + let sum = 0, amp = 0.5, freq = 1, norm = 0; + for (let i = 0; i < octaves; i++) { + sum += valueNoise2(x * freq, z * freq, 1) * amp; + norm += amp; + amp *= 0.5; + freq *= 2; + } + return sum / norm; +} + +// ========== БИОМЫ (те же пять, что в 2D) ========== +export const BIOMES = { + plains: { name:'Равнина', surface:'grass', subsurface:'dirt', trees:true, treeChance:0.11 }, + desert: { name:'Пустыня', surface:'sand', subsurface:'sand', trees:false, treeChance:0 }, + tundra: { name:'Тундра', surface:'snow', subsurface:'dirt', trees:true, treeChance:0.05 }, + swamp: { name:'Болото', surface:'moss', subsurface:'dirt', trees:true, treeChance:0.09 }, + mountains: { name:'Горы', surface:'stone', subsurface:'stone', trees:false, treeChance:0 }, +}; + +export function getBiome(gx, gz) { + const s = state.worldSeed; + const temp = fbm2(gx * 0.6, gz * 0.6 + 11, 3) * 0.5 + Math.sin(gx * 0.003 + s * 0.01) * 0.25 + 0.4; + const humid = fbm2(gx * 0.5 + 500, gz * 0.5 + 700, 3) * 0.5 + Math.cos(gx * 0.006 + s * 0.02) * 0.2 + 0.4; + const mt = fbm2(gx * 0.25 + 900, gz * 0.25 + 300, 3); + if (mt > 0.78) return 'mountains'; + if (temp > 0.66) return 'desert'; + if (temp < 0.34) return 'tundra'; + if (humid > 0.66) return 'swamp'; + return 'plains'; +} + +const biomeCache = new Map(); +export function getCachedBiome(gx, gz) { + const cx = Math.floor(gx / CHUNK), cz = Math.floor(gz / CHUNK); + const key = cx + ',' + cz; + if (!biomeCache.has(key)) biomeCache.set(key, getBiome(cx * CHUNK, cz * CHUNK)); + return biomeCache.get(key); +} + +// ========== РЕЛЬЕФ ========== +// Высота поверхности в колонке (x,z). Одна и та же функция на сервере и клиенте. +export function surfaceGyAt(gx, gz) { + const biome = getCachedBiome(gx, gz); + const n1 = Math.sin(gx * 0.025 + state.worldSeed * 0.001) * 8; + const n2 = Math.sin(gz * 0.021 - state.worldSeed * 0.0015) * 7; + const n3 = Math.sin((gx + gz) * 0.012 + state.worldSeed * 0.002) * 10; + const n4 = fbm2(gx * 0.5, gz * 0.5, 3) * 9; + let h; + switch (biome) { + case 'desert': h = 8 + n1 * 0.5 + n3 * 0.3 + n4 * 0.2; break; + case 'mountains': h = 16 + n3 * 1.1 + n4 * 1.2; break; + case 'tundra': h = 9 + n1 * 0.6 + n3 * 0.5 + n4 * 0.4; break; + case 'swamp': h = 4 + n1 * 0.4 + n2 * 0.6 + n4 * 0.3; break; + default: h = 10 + n1 * 0.7 + n2 * 0.7 + n3 * 0.4 + n4 * 0.4; + } + return Math.max(2, Math.min(BEDROCK_GY - 12, Math.round(h))); +} + +export function seaLevel() { return SEA_GY; } + +// ========== ПЕЩЕРЫ ========== +function inCave(gx, gy, gz) { + if (gy < 3) return false; + const c1 = fbm2(gx * 0.9 + 11, gz * 0.9 + gy * 1.7, 2); + const c2 = fbm2(gx * 0.9 - 40, gz * 0.9 + gy * 1.7 + 90, 2); + // две «трубки» шума: пересечение даёт объёмные полости + return c1 > 0.62 && c2 > 0.60 && gy < surfaceGyAt(gx, gz) - 3; +} + +// ========== РУДЫ ========== +function oreAt(gx, gy, gz) { + if (gy < 2) return null; + const depth = gy / BEDROCK_GY; + const r = hash2(gx * 7 + gy, gz * 13); + const r2 = hash2(gx * 3 - gy, gz * 5 + 17); + if (depth < 0.30 && r > 0.982) return 'coal'; + if (depth < 0.26 && r > 0.990 && r2 > 0.6) return 'copper_ore'; + if (depth < 0.20 && r > 0.988 && r2 > 0.4) return 'iron_ore'; + if (depth < 0.13 && r > 0.994) return 'gold_ore'; + if (depth < 0.09 && r > 0.9965) return 'diamond_ore'; + return null; +} + +// ========== ДЕРЕВО (по XY, заполняет и Z) ========== +function growTree(gx, gy, gz, biome) { + const def = BIOMES[biome]; + if (!def || !def.trees) return; + if (hash2(gx * 31, gz * 17) > def.treeChance) return; + const h = 4 + Math.floor(hash2(gx * 5, gz * 11) * 3); + const leafType = biome === 'tundra' || biome === 'swamp' ? 'spruce_leaves' : 'leaves'; + for (let i = 1; i <= h; i++) { + if (!getBlock(gx, gy - i, gz)) setBlock(gx, gy - i, gz, 'wood'); + } + // крона объёмом: псевдосфера + const cy = gy - h; + const rad = 2; + for (let dy = -2; dy <= 1; dy++) { + for (let dz = -rad; dz <= rad; dz++) { + for (let dx = -rad; dx <= rad; dx++) { + const dist = dx * dx + dy * dy * 2 + dz * dz; + if (dist > rad * rad + 1) continue; + const yy = cy + dy; + if (!getBlock(gx + dx, yy, gz + dz)) setBlock(gx + dx, yy, gz + dz, leafType); + } + } + } +} + +function growDecor(gx, gy, gz, biome) { + const def = BIOMES[biome]; + const r = hash2(gx * 23 + 5, gz * 29 + 7); + if (def.flowers && r > 0.96 && !getBlock(gx, gy, gz)) setBlock(gx, gy, gz, 'flower'); + if ((biome === 'swamp' || biome === 'plains') && r > 0.985 && !getBlock(gx, gy, gz)) setBlock(gx, gy, gz, 'mushroom'); + if (biome === 'desert' && r > 0.978 && !getBlock(gx, gy, gz) && gy + 1 < surfaceGyAt(gx, gz)) setBlock(gx, gy, gz, 'cactus'); + if (biome === 'tundra' && r > 0.978 && !getBlock(gx, gy, gz) && gy + 2 < surfaceGyAt(gx, gz)) setBlock(gx, gy, gz, 'ice'); +} + +const generated = new Set(); // "cx,cz" — уже сгенерированные чанки + +// ========== ГЕНЕРАЦИЯ ЧАНКА ========== +export function genChunk(cx, cz) { + const key = cx + ',' + cz; + if (generated.has(key)) return false; + generated.add(key); + const x0 = cx * CHUNK, z0 = cz * CHUNK; + for (let x = x0; x < x0 + CHUNK; x++) { + for (let z = z0; z < z0 + CHUNK; z++) { + genColumn(x, z); + } + } + return true; +} + +export function genColumn(gx, gz) { + const biome = getCachedBiome(gx, gz); + const top = surfaceGyAt(gx, gz); + const sea = seaLevel(); + + // Генерируем до bedrock, но не ниже уровня «пещерной зоны» — экономим память + for (let gy = Math.min(BEDROCK_GY - 1, 44); gy >= 0; gy--) { + // бедрок внизу + if (gy >= BEDROCK_GY - 2) { + if (!getBlock(gx, gy, gz)) setBlock(gx, gy, gz, 'bedrock'); + continue; + } + if (gy > top) { + // над поверхностью: вода до уровня моря + if (gy <= sea) { + const t = (biome === 'swamp') ? 'swamp_water' : 'water'; + if (!getBlock(gx, gy, gz)) setBlock(gx, gy, gz, t); + } + continue; + } + // пещеры — только под поверхностью + if (gy < top - 2 && inCave(gx, gy, gz)) continue; + + const ore = oreAt(gx, gy, gz); + if (ore) { setBlock(gx, gy, gz, ore); continue; } + + const def = BIOMES[biome]; + if (gy === top) setBlock(gx, gy, gz, def.surface); + else if (gy > top - (biome === 'desert' || biome === 'mountains' ? 5 : 3)) setBlock(gx, gy, gz, def.subsurface); + else setBlock(gx, gy, gz, 'stone'); + } + + // вода в пещерах не заливаем — это гравитация + // растительность + const above = getBlock(gx, top, gz); + if (above && !BLOCKS_NO_SOLID(above.t)) growDecor(gx, top, gz, biome); + if (above && !BLOCKS_NO_SOLID(above.t) && hash2(gx * 13, gz * 17) > 0.93) growTree(gx, top, gz, biome); +} + +function BLOCKS_NO_SOLID(t) { return t === 'water' || t === 'swamp_water' || t === 'air'; } + +// =========─ ГЕНЕРАЦИЯ ВОКРУГ ИГРОКА =========─ +// Держим чанки сгенерированными вокруг позиции игрока по X и Z. +const genChunks = new Set(); +export function ensureGenAroundCamera() { + const px = Math.floor(state.player.x / TILE); + const pz = Math.floor(state.player.z / TILE); + const rx = 3, rz = Math.ceil(WORLD_DEPTH / CHUNK) + 1; + const cx = Math.floor(px / CHUNK), cz = Math.floor(pz / CHUNK); + for (let dx = -rx; dx <= rx; dx++) { + for (let dz = -rz; dz <= rz; dz++) { + const kx = cx + dx, kz = cz + dz; + const key = kx + ',' + kz; + if (genChunks.has(key)) continue; + genChunks.add(key); + genChunk(kx, kz); + } + } + // чистим слишком далёкие + for (const key of genChunks) { + const [a, b] = key.split(',').map(Number); + if (Math.abs(a - cx) > rx + 2 || Math.abs(b - cz) > rz + 2) { + genChunks.delete(key); + generated.delete(key); + } + } +} + +export function resetGenerationCache() { + generated.clear(); + genChunks.clear(); + biomeCache.clear(); +} + +export { CHUNK, k3, getBlock, setBlock }; diff --git a/srcv3/world/tnt3d.js b/srcv3/world/tnt3d.js new file mode 100644 index 0000000..8e4f886 --- /dev/null +++ b/srcv3/world/tnt3d.js @@ -0,0 +1,85 @@ +// ==================== TNT В 3D — GrechkaCraft v3 ==================== +// Взрыв сферический: сносит блоки в радиусе по всем трём осям. +import { BLOCKS } from '../data/blocks.js'; +import { state } from '../core/state.js'; +import { getBlock, setBlock, removeBlock } from '../world/world-storage3d.js'; +import { playSound } from '../audio/sound-engine.js'; + +export function activateTNT3D(b, fuseSeconds) { + b.active = true; + b.fuse = fuseSeconds; + playSound('fuse'); +} + +export function explodeTNT3D(b, st) { + const TILE = st.TILE; + const cx = b.gx, cy = b.gy, cz = b.gz || 0; + const radius = 3.2; + const r2 = radius * radius; + + // убрать сам блок + removeBlock(cx, cy, cz); + + const toRemove = []; + const r = Math.ceil(radius); + for (let dx = -r; dx <= r; dx++) { + for (let dy = -r; dy <= r; dy++) { + for (let dz = -r; dz <= r; dz++) { + const d2 = dx * dx + dy * dy + dz * dz; + if (d2 > r2) continue; + const blk = getBlock(cx + dx, cy + dy, cz + dz); + if (!blk) continue; + const def = BLOCKS[blk.t]; + if (!def) continue; + if (def.unbreakable) continue; + // взрывная сила падает с расстоянием + if (d2 > r2 * 0.55 && blk.t !== 'dirt' && blk.t !== 'sand' && blk.t !== 'grass') continue; + toRemove.push({ gx: cx + dx, gy: cy + dy, gz: cz + dz, t: blk.t }); + } + } + } + for (const rem of toRemove) removeBlock(rem.gx, rem.gy, rem.gz); + + // частицы + const parts = st.parts || (st.parts = []); + const n = 30; + for (let i = 0; i < n; i++) { + const a = Math.random() * Math.PI * 2; + const p = Math.acos(2 * Math.random() - 1); + const sp = 60 + Math.random() * 180; + parts.push({ + x: (cx + 0.5) * TILE, + y: (cy + 0.5) * TILE, + z: (cz + 0.5) * TILE, + vx: Math.sin(p) * Math.cos(a) * sp, + vy: Math.cos(p) * sp, + vz: Math.sin(p) * Math.sin(a) * sp, + life: 0.6 + Math.random() * 0.5, + c: Math.random() > 0.5 ? '#ff7a1a' : '#f1c40f', + }); + } + + // урон мобам и игроку в радиусе + const dmg = (e) => { + const ex = (e.x + e.w / 2) / TILE, ey = (e.y + e.h / 2) / TILE, ez = ((e.z || 0) + e.d / 2) / TILE; + const d = Math.hypot(ex - cx, ey - cy, ez - cz); + if (d < radius + 1) { + const f = 1 - d / (radius + 1); + e.hp -= 14 * f; + e.vx += (ex - cx) * 220; + e.vy -= 300 * f; + e.vz = (e.vz || 0) + (ez - cz) * 220; + } + }; + for (const m of st.mobs) dmg(m); + const p = st.player; + dmg(p); + if (p.hp <= 0 && p.invuln <= 0) { p.hp = 0; p.invuln = 1.5; } + + playSound('explode'); + + // взрывные частицы убрать по таймеру + st.explosionFlash = 0.4; + setBlock(cx, cy, cz, 'air'); + void setBlock; +} diff --git a/srcv3/world/weather.js b/srcv3/world/weather.js new file mode 100644 index 0000000..ad7ce12 --- /dev/null +++ b/srcv3/world/weather.js @@ -0,0 +1,219 @@ +// ==================== ПОГОДА (biome-aware) ==================== +import { state } from '../core/state.js'; +import { isNight } from '../entities/mob-ai3d.js'; +import { surfaceGyAt, getCachedBiome } from './generation3d.js'; +import { SEA_GY } from '../core/constants.js'; + +// Возможные погоды по биому +const BIOME_WEATHER = { + snow: ['clear', 'snow', 'blizzard'], + tundra: ['clear', 'snow', 'blizzard'], + forest: ['clear', 'rain', 'storm'], + plains: ['clear', 'rain'], + desert: ['clear', 'clear', 'rain'], + swamp: ['clear', 'rain', 'storm'], + mountains: ['clear', 'rain', 'snow'] +}; + +// Помощник интерполяции цветов +function lerpColor(a, b, t) { + const ar = parseInt(a.slice(1,3),16), ag = parseInt(a.slice(3,5),16), ab = parseInt(a.slice(5,7),16); + const br = parseInt(b.slice(1,3),16), bg = parseInt(b.slice(3,5),16), bb = parseInt(b.slice(5,7),16); + const r = Math.round(ar + (br - ar) * t); + const g = Math.round(ag + (bg - ag) * t); + const bl = Math.round(ab + (bb - ab) * t); + return '#' + ((1<<24)|(r<<16)|(g<<8)|bl).toString(16).slice(1); +} + +// Цвета неба по биому, погоде и времени суток +function skyColor(night, biome, weatherType, worldTime) { + function weatherSky() { + switch (weatherType) { + case 'blizzard': return '#8a9bae'; + case 'storm': return '#3d4f5f'; + case 'rain': return '#6B7B8D'; + case 'snow': return '#b8c6d4'; + case 'clear': return null; + default: return null; + } + } + + const dayColor = (biome === 'desert') ? '#f4d9a0' : (biome === 'tundra' || biome === 'snow') ? '#c4d4e0' : (biome === 'swamp') ? '#4a6741' : '#87CEEB'; + const dawnColor = '#FF9940'; + const duskMidColor = '#FF6B35'; + const duskEndColor = '#1a0533'; + const nightColor = '#070816'; + + let skyColorResult; + + if (worldTime < 0.05) { + const t = worldTime / 0.05; + skyColorResult = lerpColor(dawnColor, dayColor, t); + } else if (worldTime < 0.45) { + skyColorResult = dayColor; + } else if (worldTime < 0.55) { + const t = (worldTime - 0.45) / 0.1; + if (t < 0.5) { + skyColorResult = lerpColor(dayColor, duskMidColor, t * 2); + } else { + skyColorResult = lerpColor(duskMidColor, duskEndColor, (t - 0.5) * 2); + } + } else { + skyColorResult = nightColor; + if (weatherType === 'blizzard') return '#1a1a2e'; + if (weatherType === 'storm') return '#0a0a15'; + return nightColor; + } + + const wc = weatherSky(); + if (wc) { + const blendT = 0.6; + skyColorResult = lerpColor(skyColorResult, wc, blendT); + } + + return skyColorResult; +} + +export function updateWeather(dt) { + state.weatherTimer += dt; + if (state.weatherTimer >= state.weatherChangeInterval) { + state.weatherTimer = 0; + state.weatherChangeInterval = 60 + Math.random() * 120; + + // Определяем биом по позиции игрока + const playerGx = Math.floor(state.player.x / state.TILE); + const biome = getCachedBiome(playerGx, Math.floor((state.player.z || 0) / state.TILE)); + const weathers = BIOME_WEATHER[biome] || ['clear']; + + const night = isNight(); + const weights = weathers.map(w => { + if (w === 'clear') return night ? 2 : 4; + if (w === 'rain') return night ? 3 : 2; + if (w === 'snow') return night ? 2 : 3; + if (w === 'storm') return night ? 3 : 1; + if (w === 'blizzard') return night ? 3 : 1; + return 1; + }); + const total = weights.reduce((a, b) => a + b, 0); + let r = Math.random() * total; + state.weatherType = weathers[0]; + for (let i = 0; i < weathers.length; i++) { + r -= weights[i]; + if (r <= 0) { state.weatherType = weathers[i]; break; } + } + + state.isRaining = (state.weatherType === 'rain' || state.weatherType === 'storm'); + } + + // Плавная интерполяция интенсивности дождя + const rainTarget = (state.weatherType === 'rain' || state.weatherType === 'storm') ? 0.6 : 0; + state.rainIntensity += (rainTarget - state.rainIntensity) * dt * 0.5; + if (state.rainIntensity < 0.01) state.rainIntensity = 0; + + // Молния (только при storm) + if (state.weatherType === 'storm') { + state.lightningTimer -= dt; + if (state.lightningTimer <= 0) { + state.lightningFlash = 0.25; + state.lightningTimer = 4 + Math.random() * 12; + } + } + if (state.lightningFlash > 0) { + state.lightningFlash -= dt; + if (state.lightningFlash < 0) state.lightningFlash = 0; + } + + // Снежинки (при snow / blizzard) + if (state.weatherType === 'snow' || state.weatherType === 'blizzard') { + const spawnCount = state.weatherType === 'blizzard' ? Math.floor(80 * dt) : Math.floor(30 * dt); + const maxFlakes = state.weatherType === 'blizzard' ? 600 : 250; + for (let i = 0; i < spawnCount && state.snowflakes.length < maxFlakes; i++) { + state.snowflakes.push({ + x: state.camX + Math.random() * state.W, + y: state.camY - 20, + vy: 40 + Math.random() * 60, + vx: (state.weatherType === 'blizzard' ? -60 - Math.random() * 80 : -10 - Math.random() * 20), + size: 2 + Math.random() * 3, + wobble: Math.random() * Math.PI * 2 + }); + } + } + // Обновление снежинок + for (let i = state.snowflakes.length - 1; i >= 0; i--) { + const s = state.snowflakes[i]; + s.y += s.vy * dt; + s.x += s.vx * dt; + s.wobble += dt * 3; + s.x += Math.sin(s.wobble) * 15 * dt; + if (s.y > state.camY + state.H + 20 || s.x < state.camX - 50 || s.x > state.camX + state.W + 50) { + state.snowflakes.splice(i, 1); + } + } + // Убираем снег при смене погоды + if (state.weatherType !== 'snow' && state.weatherType !== 'blizzard' && !state.isRaining) { + if (state.snowflakes.length > 0) { + state.snowflakes.splice(0, Math.ceil(state.snowflakes.length * dt * 2)); + } + } +} + +export function updateRain(dt) { + if (!state.isRaining || state.rainIntensity < 0.01) { + if (state.raindrops.length > 0) { + state.raindrops.splice(0, Math.ceil(state.raindrops.length * dt * 3)); + } + return; + } + const spawnRate = Math.floor(state.rainIntensity * 60 * dt); + for (let i = 0; i < spawnRate && state.raindrops.length < state.MAX_RAINDROPS; i++) { + state.raindrops.push({ + x: state.camX + Math.random() * state.W, + y: state.camY - 20, + vy: 400 + Math.random() * 200, + len: 8 + Math.random() * 12 + }); + } + for (let i = state.raindrops.length - 1; i >= 0; i--) { + const d = state.raindrops[i]; + d.y += d.vy * dt; + d.x -= 30 * dt; + if (d.y > state.camY + state.H + 20) { + state.raindrops.splice(i, 1); + } + } +} + +export function drawWeather(ctx) { + if (state.raindrops.length > 0) { + ctx.save(); + ctx.strokeStyle = 'rgba(174,194,224,0.5)'; + ctx.lineWidth = 1.5; + ctx.beginPath(); + for (const d of state.raindrops) { + ctx.moveTo(d.x, d.y); + ctx.lineTo(d.x - 3, d.y + d.len); + } + ctx.stroke(); + ctx.restore(); + } + + if (state.snowflakes.length > 0) { + ctx.save(); + ctx.fillStyle = 'rgba(255,255,255,0.85)'; + for (const s of state.snowflakes) { + ctx.beginPath(); + ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2); + ctx.fill(); + } + ctx.restore(); + } + + if (state.lightningFlash > 0) { + ctx.save(); + ctx.fillStyle = `rgba(255,255,255,${Math.min(1, state.lightningFlash * 4)})`; + ctx.fillRect(0, 0, state.W, state.H); + ctx.restore(); + } +} + +export { skyColor }; diff --git a/srcv3/world/world-storage3d.js b/srcv3/world/world-storage3d.js new file mode 100644 index 0000000..619f000 --- /dev/null +++ b/srcv3/world/world-storage3d.js @@ -0,0 +1,65 @@ +// ==================== ХРАНИЛИЩЕ МИРА 3D — GrechkaCraft v3 ==================== +// Ключ блока: "x,y,z". Единый источник для рендера, физики, сети и сейвов. +// Мир по всем трём осям: X — вперёд, Y — вверх, Z — вбок (глубина мира). +import { BLOCKS } from '../data/blocks.js'; +import { state } from '../core/state.js'; + +const grid = state.grid; // Map: "x,y,z" => { gx, gy, gz, t, dead } +const blocks = state.blocks; // Array для перебора/рендера + +function k3(gx, gy, gz) { return gx + ',' + gy + ',' + gz; } + +function getBlock(gx, gy, gz) { + const b = grid.get(k3(gx, gy, gz)); + if (b && b.dead) return null; + return b || null; +} + +function hasBlock(gx, gy, gz) { return grid.has(k3(gx, gy, gz)); } + +function isSolid(gx, gy, gz) { + const b = getBlock(gx, gy, gz); + if (!b) return false; + const def = BLOCKS[b.t]; + return !!def.solid && !def.fluid && !def.decor && !def.climbable; +} + +// Непрозрачный ли блок (для отсечения внутренних граней не используется — мы рендерим кубы) +function isOpaque(gx, gy, gz) { + const b = getBlock(gx, gy, gz); + if (!b) return false; + const def = BLOCKS[b.t]; + return !!def.solid && !def.fluid && !def.decor && !def.climbable; +} + +function setBlock(gx, gy, gz, t, isPlayerPlaced = false) { + const key = k3(gx, gy, gz); + if (grid.has(key)) return false; + const b = { gx, gy, gz, t, dead: false, active: false, fuse: 0 }; + grid.set(key, b); + blocks.push(b); + if (isPlayerPlaced) { + state.placedBlocks.push({ gx, gy, gz, t }); + } + state.blocksVersion = (state.blocksVersion || 0) + 1; + return true; +} + +function removeBlock(gx, gy, gz) { + const key = k3(gx, gy, gz); + const b = grid.get(key); + if (!b) return null; + if (BLOCKS[b.t].unbreakable) return null; + grid.delete(key); + b.dead = true; + const wasPlayerPlaced = state.placedBlocks.some(pb => pb.gx === gx && pb.gy === gy && pb.gz === gz); + if (wasPlayerPlaced) { + state.placedBlocks = state.placedBlocks.filter(pb => !(pb.gx === gx && pb.gy === gy && pb.gz === gz)); + } else { + state.removedBlocks.push({ gx, gy, gz }); + } + state.blocksVersion = (state.blocksVersion || 0) + 1; + return b; +} + +export { grid, blocks, k3, getBlock, hasBlock, isSolid, isOpaque, setBlock, removeBlock }; diff --git a/style.css b/style.css index 54d8a5f..7d9a764 100644 --- a/style.css +++ b/style.css @@ -36,11 +36,10 @@ canvas { display:block; width:100%; height:100%; image-rendering:pixelated; } font-size:24px; cursor:pointer; pointer-events:auto; box-shadow:0 4px 0 rgba(0,0,0,0.5); } .rbtn:active { transform: translateY(4px); box-shadow:none; } #modeBtn { top:10px; background:#f39c12; } -#saveBtn { top:10px; right:70px !important; background:#27ae60; } -#resetBtn { top:10px; right:130px !important; background:#e74c3c; } -#mapToggle { top:10px; right:190px !important; background:#1abc9c; } +#mapToggle { top:10px; right:70px !important; background:#1abc9c; } #craftBtn { top:74px; right:10px !important; background:#9b59b6; } #invToggle { top:74px; right:70px !important; background:#3498db; } +#menuBtn { top:74px; right:130px !important; background:#555; } #chatToggle { display: none !important; } #chatPanel { display: none !important; } @@ -104,3 +103,14 @@ body.touch-device #hotbar { .custom-modal-box .btn-yes { background:#e74c3c; } .custom-modal-box .btn-no { background:#555; } .custom-modal-box .btn-ok { background:#2ecc71; } + +/* Menu dropdown ⋯ */ +.menu-dropdown { position:absolute; top:120px; right:58px; background:rgba(20,20,30,0.95); border:2px solid rgba(255,255,255,0.7); border-radius:10px; display:none; flex-direction:column; min-width:140px; z-index:300; pointer-events:auto; overflow:hidden; } +.menu-dropdown.open { display:flex; } +.menu-item { padding:10px 14px; color:#fff; font-size:14px; font-weight:700; cursor:pointer; border-bottom:1px solid rgba(255,255,255,0.1); } +.menu-item:last-child { border-bottom:none; } +.menu-item:hover { background:rgba(255,255,255,0.12); } +.menu-item:active { background:rgba(255,255,255,0.2); } + +/* Market panel */ +#marketPanel { display:none; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:360px; max-width:90%; background:rgba(10,10,12,0.95); border:2px solid rgba(255,255,255,0.85); border-radius:14px; pointer-events:auto; padding:12px; z-index:200; } diff --git a/backup/20260103_082922/backup/20260103_082922/style.css b/style.css.bak.v30 similarity index 52% rename from backup/20260103_082922/backup/20260103_082922/style.css rename to style.css.bak.v30 index af8325b..7d9a764 100644 --- a/backup/20260103_082922/backup/20260103_082922/style.css +++ b/style.css.bak.v30 @@ -1,8 +1,28 @@ /* Minegrechka Game Styles - v1.2 */ html, body { margin:0; padding:0; width:100%; height:100%; overflow:hidden; background:#111; font-family: system-ui, sans-serif; user-select:none; -webkit-user-select:none; touch-action:none; } -#game { position:absolute; top:0; left:0; right:0; bottom:140px; background:#87CEEB; overflow:hidden; } +#game { position:absolute; top:0; left:0; right:0; bottom:0; background:#87CEEB; overflow:hidden; } canvas { display:block; width:100%; height:100%; image-rendering:pixelated; } -#controls { position:absolute; left:0; right:0; bottom:0; height:140px; background:#222; border-top:4px solid #444; z-index:10; } +#controls { position:absolute; left:0; right:0; bottom:0; height:140px; background:#222; border-top:4px solid #444; z-index:100; display:none; } + +/* Стили для кнопок управления (.btn вместо .cbtn для совместимости с index.html) */ +.btn { position:absolute; top:50%; transform:translateY(-50%); width:74px; height:74px; border-radius:14px; + background:#333; border:3px solid #555; color:#fff; font-size:32px; display:flex; align-items:center; justify-content:center; + box-shadow:0 7px 0 #111; pointer-events:auto; z-index:101; } +.btn:active { transform:translateY(-46%); box-shadow:0 3px 0 #111; background:#444; } +#left { left:18px; } +#right { left:102px; } +#down { left:186px; background:#2980b9; border-color:#3498db; } +#jump { right:18px; background:#d35400; border-color:#e67e22; } + +/* Скрываем панель на десктопе (широкие экраны) */ +@media (min-width: 769px) { + #controls { display: none !important; } +} + +/* Показываем панель на мобильных устройствах (узкие экраны) */ +@media (max-width: 768px) { + #controls { display: block !important; } +} .ui { position:absolute; inset:0; pointer-events:none; z-index:20; } .pe { pointer-events:auto; } @@ -16,14 +36,21 @@ canvas { display:block; width:100%; height:100%; image-rendering:pixelated; } font-size:24px; cursor:pointer; pointer-events:auto; box-shadow:0 4px 0 rgba(0,0,0,0.5); } .rbtn:active { transform: translateY(4px); box-shadow:none; } #modeBtn { top:10px; background:#f39c12; } -#saveBtn { top:10px; right:70px !important; background:#27ae60; } -#resetBtn { top:10px; right:130px !important; background:#e74c3c; } +#mapToggle { top:10px; right:70px !important; background:#1abc9c; } #craftBtn { top:74px; right:10px !important; background:#9b59b6; } -#chatToggle { top:10px; right:190px !important; background:#9b59b6; } +#invToggle { top:74px; right:70px !important; background:#3498db; } +#menuBtn { top:74px; right:130px !important; background:#555; } +#chatToggle { display: none !important; } +#chatPanel { display: none !important; } #hotbar { position:absolute; left:50%; transform:translateX(-50%); bottom:10px; display:flex; gap:6px; background: rgba(0,0,0,0.60); padding:6px; border-radius:12px; pointer-events:auto; overflow-x: auto; overflow-y: hidden; max-width: 80%; } + +/* Поднимаем hotbar на тач-устройствах, чтобы не перекрывать кнопки */ +body.touch-device #hotbar { + bottom: 150px; +} .slot { width:38px; height:38px; border:2px solid rgba(255,255,255,0.22); border-radius:10px; position:relative; overflow:hidden; cursor:pointer; background: rgba(255,255,255,0.07); display:flex; align-items:center; justify-content:center; font-size:18px; } @@ -33,12 +60,12 @@ canvas { display:block; width:100%; height:100%; image-rendering:pixelated; } /* Craft modal */ #craftPanel { display:none; position:absolute; left:14px; right:14px; top:14px; bottom:14px; background: rgba(10,10,12,0.92); border:2px solid rgba(255,255,255,0.85); border-radius:14px; - pointer-events:auto; padding:12px; overflow:auto; } + pointer-events:auto; padding:12px; overflow:auto; z-index: 200; } /* Inventory modal */ #inventoryPanel { display:none; position:absolute; left:50%; top:50%; transform: translate(-50%, -50%); width: 420px; max-width: 90%; background: rgba(10,10,12,0.92); border:2px solid rgba(255,255,255,0.85); - border-radius:14px; pointer-events:auto; padding:12px; z-index: 100; } + border-radius:14px; pointer-events:auto; padding:12px; z-index: 200; } #inventoryPanel .top { display:flex; justify-content:space-between; align-items:center; color:#fff; margin-bottom:10px; } #inventoryGrid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin-top: 10px; } .inv-slot { width: 48px; height: 48px; border: 2px solid rgba(255,255,255,0.22); border-radius: 8px; @@ -46,9 +73,11 @@ canvas { display:block; width:100%; height:100%; image-rendering:pixelated; } display: flex; align-items: center; justify-content: center; font-size: 24px; } .inv-slot.sel { border-color: #f1c40f; box-shadow: 0 0 0 2px rgba(241,196,15,0.18) inset; } .inv-count { position: absolute; right: 3px; bottom: 1px; font-size: 12px; color: #fff; - font-weight: 900; text-shadow: 1px 1px 0 #000; background: rgba(0,0,0,0.5); padding: 1px 3px; border-radius: 4px; } + font-weight: 900; text-shadow: 1px 1px 0 #000; background: rgba(0,0,0,0.5); padding: 1px 3px; border-radius: 4px; } +.equipped-indicator { position: absolute; top: 2px; right: 2px; font-size: 14px; color: #2ecc71; font-weight: 900; text-shadow: 1px 1px 0 #000; } #craftPanel .top { display:flex; justify-content:space-between; align-items:center; color:#fff; margin-bottom:10px; } #craftPanel .close { background:#c0392b; border:none; color:#fff; font-weight:900; padding:8px 10px; border-radius:10px; cursor:pointer; } +#inventoryPanel .close { background:#c0392b; border:none; color:#fff; font-weight:900; padding:8px 10px; border-radius:10px; cursor:pointer; } .recipe { display:flex; align-items:center; gap:10px; padding:10px; border-radius:12px; background: rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.10); margin-bottom:8px; } .ricon { width:32px; height:32px; border-radius:8px; background-size:cover; image-rendering:pixelated; } @@ -58,15 +87,30 @@ canvas { display:block; width:100%; height:100%; image-rendering:pixelated; } .rcraft { background:#2ecc71; border:none; color:#fff; font-weight:900; padding:8px 10px; border-radius:10px; cursor:pointer; } .rcraft:disabled { background:#444; color:#888; cursor:not-allowed; } -/* Controls buttons */ -.cbtn { position:absolute; top:50%; transform:translateY(-50%); width:74px; height:74px; border-radius:14px; - background:#333; border:3px solid #555; color:#fff; font-size:32px; display:flex; align-items:center; justify-content:center; - box-shadow:0 7px 0 #111; pointer-events:auto; } -.cbtn:active { transform:translateY(-46%); box-shadow:0 3px 0 #111; background:#444; } -#left { left:18px; } -#right { left:102px; } -#jump { right:18px; background:#d35400; border-color:#e67e22; } - #death { display:none; position:absolute; inset:0; background: rgba(60,0,0,0.88); z-index:200; color:#fff; pointer-events:auto; align-items:center; justify-content:center; flex-direction:column; gap:12px; } #death button { padding:12px 18px; font-size:18px; font-weight:900; border:none; border-radius:12px; cursor:pointer; } + +/* Panel header + close button fix */ +.panel-header { display:flex; justify-content:space-between; align-items:center; color:#fff; font-weight:900; font-size:18px; margin-bottom:12px; padding-bottom:8px; border-bottom:1px solid rgba(255,255,255,0.15); } +.panel-header .close { background:#c0392b; border:none; color:#fff; font-weight:900; padding:8px 12px; border-radius:10px; cursor:pointer; font-size:16px; min-width:36px; text-align:center; flex-shrink:0; margin-left:12px; } + +/* Custom modal (alerts/confirms) */ +.custom-modal-overlay { position:absolute; inset:0; background:rgba(0,0,0,0.7); z-index:9999; display:flex; align-items:center; justify-content:center; } +.custom-modal-box { background:#1a1a2e; border:2px solid #e74c3c; border-radius:16px; padding:24px 32px; color:#fff; font-size:16px; font-weight:700; text-align:center; max-width:320px; box-shadow:0 8px 32px rgba(0,0,0,0.5); } +.custom-modal-box .modal-btns { display:flex; gap:10px; justify-content:center; margin-top:16px; } +.custom-modal-box button { font-weight:900; padding:10px 20px; border-radius:10px; font-size:15px; cursor:pointer; border:none; color:#fff; } +.custom-modal-box .btn-yes { background:#e74c3c; } +.custom-modal-box .btn-no { background:#555; } +.custom-modal-box .btn-ok { background:#2ecc71; } + +/* Menu dropdown ⋯ */ +.menu-dropdown { position:absolute; top:120px; right:58px; background:rgba(20,20,30,0.95); border:2px solid rgba(255,255,255,0.7); border-radius:10px; display:none; flex-direction:column; min-width:140px; z-index:300; pointer-events:auto; overflow:hidden; } +.menu-dropdown.open { display:flex; } +.menu-item { padding:10px 14px; color:#fff; font-size:14px; font-weight:700; cursor:pointer; border-bottom:1px solid rgba(255,255,255,0.1); } +.menu-item:last-child { border-bottom:none; } +.menu-item:hover { background:rgba(255,255,255,0.12); } +.menu-item:active { background:rgba(255,255,255,0.2); } + +/* Market panel */ +#marketPanel { display:none; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:360px; max-width:90%; background:rgba(10,10,12,0.95); border:2px solid rgba(255,255,255,0.85); border-radius:14px; pointer-events:auto; padding:12px; z-index:200; } diff --git a/three147.min.js b/three147.min.js new file mode 100644 index 0000000..8dc08f7 --- /dev/null +++ b/three147.min.js @@ -0,0 +1,6 @@ +/** + * @license + * Copyright 2010-2022 Three.js Authors + * SPDX-License-Identifier: MIT + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).THREE={})}(this,(function(t){"use strict";const e="147",i=100,n=300,r=301,s=302,a=303,o=304,l=306,c=1e3,h=1001,u=1002,d=1003,p=1004,m=1005,f=1006,g=1007,v=1008,x=1009,_=1012,y=1014,M=1015,b=1016,S=1020,w=1023,T=1026,A=1027,E=33776,C=33777,L=33778,R=33779,P=35840,I=35841,D=35842,N=35843,O=37492,z=37496,U=37808,B=37809,F=37810,k=37811,G=37812,V=37813,H=37814,W=37815,j=37816,q=37817,X=37818,Y=37819,Z=37820,J=37821,K=36492,$=2300,Q=2301,tt=2302,et=2400,it=2401,nt=2402,rt=2500,st=2501,at=3e3,ot=3001,lt="srgb",ct="srgb-linear",ht=7680,ut=35044,dt="300 es",pt=1035;class mt{addEventListener(t,e){void 0===this._listeners&&(this._listeners={});const i=this._listeners;void 0===i[t]&&(i[t]=[]),-1===i[t].indexOf(e)&&i[t].push(e)}hasEventListener(t,e){if(void 0===this._listeners)return!1;const i=this._listeners;return void 0!==i[t]&&-1!==i[t].indexOf(e)}removeEventListener(t,e){if(void 0===this._listeners)return;const i=this._listeners[t];if(void 0!==i){const t=i.indexOf(e);-1!==t&&i.splice(t,1)}}dispatchEvent(t){if(void 0===this._listeners)return;const e=this._listeners[t.type];if(void 0!==e){t.target=this;const i=e.slice(0);for(let e=0,n=i.length;e>8&255]+ft[t>>16&255]+ft[t>>24&255]+"-"+ft[255&e]+ft[e>>8&255]+"-"+ft[e>>16&15|64]+ft[e>>24&255]+"-"+ft[63&i|128]+ft[i>>8&255]+"-"+ft[i>>16&255]+ft[i>>24&255]+ft[255&n]+ft[n>>8&255]+ft[n>>16&255]+ft[n>>24&255]).toLowerCase()}function yt(t,e,i){return Math.max(e,Math.min(i,t))}function Mt(t,e){return(t%e+e)%e}function bt(t,e,i){return(1-i)*t+i*e}function St(t){return 0==(t&t-1)&&0!==t}function wt(t){return Math.pow(2,Math.ceil(Math.log(t)/Math.LN2))}function Tt(t){return Math.pow(2,Math.floor(Math.log(t)/Math.LN2))}function At(t,e){switch(e.constructor){case Float32Array:return t;case Uint16Array:return t/65535;case Uint8Array:return t/255;case Int16Array:return Math.max(t/32767,-1);case Int8Array:return Math.max(t/127,-1);default:throw new Error("Invalid component type.")}}function Et(t,e){switch(e.constructor){case Float32Array:return t;case Uint16Array:return Math.round(65535*t);case Uint8Array:return Math.round(255*t);case Int16Array:return Math.round(32767*t);case Int8Array:return Math.round(127*t);default:throw new Error("Invalid component type.")}}var Ct=Object.freeze({__proto__:null,DEG2RAD:vt,RAD2DEG:xt,generateUUID:_t,clamp:yt,euclideanModulo:Mt,mapLinear:function(t,e,i,n,r){return n+(t-e)*(r-n)/(i-e)},inverseLerp:function(t,e,i){return t!==e?(i-t)/(e-t):0},lerp:bt,damp:function(t,e,i,n){return bt(t,e,1-Math.exp(-i*n))},pingpong:function(t,e=1){return e-Math.abs(Mt(t,2*e)-e)},smoothstep:function(t,e,i){return t<=e?0:t>=i?1:(t=(t-e)/(i-e))*t*(3-2*t)},smootherstep:function(t,e,i){return t<=e?0:t>=i?1:(t=(t-e)/(i-e))*t*t*(t*(6*t-15)+10)},randInt:function(t,e){return t+Math.floor(Math.random()*(e-t+1))},randFloat:function(t,e){return t+Math.random()*(e-t)},randFloatSpread:function(t){return t*(.5-Math.random())},seededRandom:function(t){void 0!==t&&(gt=t);let e=gt+=1831565813;return e=Math.imul(e^e>>>15,1|e),e^=e+Math.imul(e^e>>>7,61|e),((e^e>>>14)>>>0)/4294967296},degToRad:function(t){return t*vt},radToDeg:function(t){return t*xt},isPowerOfTwo:St,ceilPowerOfTwo:wt,floorPowerOfTwo:Tt,setQuaternionFromProperEuler:function(t,e,i,n,r){const s=Math.cos,a=Math.sin,o=s(i/2),l=a(i/2),c=s((e+n)/2),h=a((e+n)/2),u=s((e-n)/2),d=a((e-n)/2),p=s((n-e)/2),m=a((n-e)/2);switch(r){case"XYX":t.set(o*h,l*u,l*d,o*c);break;case"YZY":t.set(l*d,o*h,l*u,o*c);break;case"ZXZ":t.set(l*u,l*d,o*h,o*c);break;case"XZX":t.set(o*h,l*m,l*p,o*c);break;case"YXY":t.set(l*p,o*h,l*m,o*c);break;case"ZYZ":t.set(l*m,l*p,o*h,o*c);break;default:console.warn("THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: "+r)}},normalize:Et,denormalize:At});class Lt{constructor(t=0,e=0){Lt.prototype.isVector2=!0,this.x=t,this.y=e}get width(){return this.x}set width(t){this.x=t}get height(){return this.y}set height(t){this.y=t}set(t,e){return this.x=t,this.y=e,this}setScalar(t){return this.x=t,this.y=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y)}copy(t){return this.x=t.x,this.y=t.y,this}add(t){return this.x+=t.x,this.y+=t.y,this}addScalar(t){return this.x+=t,this.y+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this}subScalar(t){return this.x-=t,this.y-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this}multiply(t){return this.x*=t.x,this.y*=t.y,this}multiplyScalar(t){return this.x*=t,this.y*=t,this}divide(t){return this.x/=t.x,this.y/=t.y,this}divideScalar(t){return this.multiplyScalar(1/t)}applyMatrix3(t){const e=this.x,i=this.y,n=t.elements;return this.x=n[0]*e+n[3]*i+n[6],this.y=n[1]*e+n[4]*i+n[7],this}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this}clamp(t,e){return this.x=Math.max(t.x,Math.min(e.x,this.x)),this.y=Math.max(t.y,Math.min(e.y,this.y)),this}clampScalar(t,e){return this.x=Math.max(t,Math.min(e,this.x)),this.y=Math.max(t,Math.min(e,this.y)),this}clampLength(t,e){const i=this.length();return this.divideScalar(i||1).multiplyScalar(Math.max(t,Math.min(e,i)))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this}roundToZero(){return this.x=this.x<0?Math.ceil(this.x):Math.floor(this.x),this.y=this.y<0?Math.ceil(this.y):Math.floor(this.y),this}negate(){return this.x=-this.x,this.y=-this.y,this}dot(t){return this.x*t.x+this.y*t.y}cross(t){return this.x*t.y-this.y*t.x}lengthSq(){return this.x*this.x+this.y*this.y}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)}normalize(){return this.divideScalar(this.length()||1)}angle(){return Math.atan2(-this.y,-this.x)+Math.PI}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,i=this.y-t.y;return e*e+i*i}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this}lerpVectors(t,e,i){return this.x=t.x+(e.x-t.x)*i,this.y=t.y+(e.y-t.y)*i,this}equals(t){return t.x===this.x&&t.y===this.y}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this}rotateAround(t,e){const i=Math.cos(e),n=Math.sin(e),r=this.x-t.x,s=this.y-t.y;return this.x=r*i-s*n+t.x,this.y=r*n+s*i+t.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y}}class Rt{constructor(){Rt.prototype.isMatrix3=!0,this.elements=[1,0,0,0,1,0,0,0,1]}set(t,e,i,n,r,s,a,o,l){const c=this.elements;return c[0]=t,c[1]=n,c[2]=a,c[3]=e,c[4]=r,c[5]=o,c[6]=i,c[7]=s,c[8]=l,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(t){const e=this.elements,i=t.elements;return e[0]=i[0],e[1]=i[1],e[2]=i[2],e[3]=i[3],e[4]=i[4],e[5]=i[5],e[6]=i[6],e[7]=i[7],e[8]=i[8],this}extractBasis(t,e,i){return t.setFromMatrix3Column(this,0),e.setFromMatrix3Column(this,1),i.setFromMatrix3Column(this,2),this}setFromMatrix4(t){const e=t.elements;return this.set(e[0],e[4],e[8],e[1],e[5],e[9],e[2],e[6],e[10]),this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const i=t.elements,n=e.elements,r=this.elements,s=i[0],a=i[3],o=i[6],l=i[1],c=i[4],h=i[7],u=i[2],d=i[5],p=i[8],m=n[0],f=n[3],g=n[6],v=n[1],x=n[4],_=n[7],y=n[2],M=n[5],b=n[8];return r[0]=s*m+a*v+o*y,r[3]=s*f+a*x+o*M,r[6]=s*g+a*_+o*b,r[1]=l*m+c*v+h*y,r[4]=l*f+c*x+h*M,r[7]=l*g+c*_+h*b,r[2]=u*m+d*v+p*y,r[5]=u*f+d*x+p*M,r[8]=u*g+d*_+p*b,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[3]*=t,e[6]*=t,e[1]*=t,e[4]*=t,e[7]*=t,e[2]*=t,e[5]*=t,e[8]*=t,this}determinant(){const t=this.elements,e=t[0],i=t[1],n=t[2],r=t[3],s=t[4],a=t[5],o=t[6],l=t[7],c=t[8];return e*s*c-e*a*l-i*r*c+i*a*o+n*r*l-n*s*o}invert(){const t=this.elements,e=t[0],i=t[1],n=t[2],r=t[3],s=t[4],a=t[5],o=t[6],l=t[7],c=t[8],h=c*s-a*l,u=a*o-c*r,d=l*r-s*o,p=e*h+i*u+n*d;if(0===p)return this.set(0,0,0,0,0,0,0,0,0);const m=1/p;return t[0]=h*m,t[1]=(n*l-c*i)*m,t[2]=(a*i-n*s)*m,t[3]=u*m,t[4]=(c*e-n*o)*m,t[5]=(n*r-a*e)*m,t[6]=d*m,t[7]=(i*o-l*e)*m,t[8]=(s*e-i*r)*m,this}transpose(){let t;const e=this.elements;return t=e[1],e[1]=e[3],e[3]=t,t=e[2],e[2]=e[6],e[6]=t,t=e[5],e[5]=e[7],e[7]=t,this}getNormalMatrix(t){return this.setFromMatrix4(t).invert().transpose()}transposeIntoArray(t){const e=this.elements;return t[0]=e[0],t[1]=e[3],t[2]=e[6],t[3]=e[1],t[4]=e[4],t[5]=e[7],t[6]=e[2],t[7]=e[5],t[8]=e[8],this}setUvTransform(t,e,i,n,r,s,a){const o=Math.cos(r),l=Math.sin(r);return this.set(i*o,i*l,-i*(o*s+l*a)+s+t,-n*l,n*o,-n*(-l*s+o*a)+a+e,0,0,1),this}scale(t,e){return this.premultiply(Pt.makeScale(t,e)),this}rotate(t){return this.premultiply(Pt.makeRotation(-t)),this}translate(t,e){return this.premultiply(Pt.makeTranslation(t,e)),this}makeTranslation(t,e){return this.set(1,0,t,0,1,e,0,0,1),this}makeRotation(t){const e=Math.cos(t),i=Math.sin(t);return this.set(e,-i,0,i,e,0,0,0,1),this}makeScale(t,e){return this.set(t,0,0,0,e,0,0,0,1),this}equals(t){const e=this.elements,i=t.elements;for(let t=0;t<9;t++)if(e[t]!==i[t])return!1;return!0}fromArray(t,e=0){for(let i=0;i<9;i++)this.elements[i]=t[i+e];return this}toArray(t=[],e=0){const i=this.elements;return t[e]=i[0],t[e+1]=i[1],t[e+2]=i[2],t[e+3]=i[3],t[e+4]=i[4],t[e+5]=i[5],t[e+6]=i[6],t[e+7]=i[7],t[e+8]=i[8],t}clone(){return(new this.constructor).fromArray(this.elements)}}const Pt=new Rt;function It(t){for(let e=t.length-1;e>=0;--e)if(t[e]>=65535)return!0;return!1}const Dt={Int8Array:Int8Array,Uint8Array:Uint8Array,Uint8ClampedArray:Uint8ClampedArray,Int16Array:Int16Array,Uint16Array:Uint16Array,Int32Array:Int32Array,Uint32Array:Uint32Array,Float32Array:Float32Array,Float64Array:Float64Array};function Nt(t,e){return new Dt[t](e)}function Ot(t){return document.createElementNS("http://www.w3.org/1999/xhtml",t)}function zt(t){return t<.04045?.0773993808*t:Math.pow(.9478672986*t+.0521327014,2.4)}function Ut(t){return t<.0031308?12.92*t:1.055*Math.pow(t,.41666)-.055}const Bt={[lt]:{[ct]:zt},[ct]:{[lt]:Ut}},Ft={legacyMode:!0,get workingColorSpace(){return ct},set workingColorSpace(t){console.warn("THREE.ColorManagement: .workingColorSpace is readonly.")},convert:function(t,e,i){if(this.legacyMode||e===i||!e||!i)return t;if(Bt[e]&&void 0!==Bt[e][i]){const n=Bt[e][i];return t.r=n(t.r),t.g=n(t.g),t.b=n(t.b),t}throw new Error("Unsupported color space conversion.")},fromWorkingColorSpace:function(t,e){return this.convert(t,this.workingColorSpace,e)},toWorkingColorSpace:function(t,e){return this.convert(t,e,this.workingColorSpace)}},kt={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},Gt={r:0,g:0,b:0},Vt={h:0,s:0,l:0},Ht={h:0,s:0,l:0};function Wt(t,e,i){return i<0&&(i+=1),i>1&&(i-=1),i<1/6?t+6*(e-t)*i:i<.5?e:i<2/3?t+6*(e-t)*(2/3-i):t}function jt(t,e){return e.r=t.r,e.g=t.g,e.b=t.b,e}class qt{constructor(t,e,i){return this.isColor=!0,this.r=1,this.g=1,this.b=1,void 0===e&&void 0===i?this.set(t):this.setRGB(t,e,i)}set(t){return t&&t.isColor?this.copy(t):"number"==typeof t?this.setHex(t):"string"==typeof t&&this.setStyle(t),this}setScalar(t){return this.r=t,this.g=t,this.b=t,this}setHex(t,e="srgb"){return t=Math.floor(t),this.r=(t>>16&255)/255,this.g=(t>>8&255)/255,this.b=(255&t)/255,Ft.toWorkingColorSpace(this,e),this}setRGB(t,e,i,n=Ft.workingColorSpace){return this.r=t,this.g=e,this.b=i,Ft.toWorkingColorSpace(this,n),this}setHSL(t,e,i,n=Ft.workingColorSpace){if(t=Mt(t,1),e=yt(e,0,1),i=yt(i,0,1),0===e)this.r=this.g=this.b=i;else{const n=i<=.5?i*(1+e):i+e-i*e,r=2*i-n;this.r=Wt(r,n,t+1/3),this.g=Wt(r,n,t),this.b=Wt(r,n,t-1/3)}return Ft.toWorkingColorSpace(this,n),this}setStyle(t,e="srgb"){function i(e){void 0!==e&&parseFloat(e)<1&&console.warn("THREE.Color: Alpha component of "+t+" will be ignored.")}let n;if(n=/^((?:rgb|hsl)a?)\(([^\)]*)\)/.exec(t)){let t;const r=n[1],s=n[2];switch(r){case"rgb":case"rgba":if(t=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(s))return this.r=Math.min(255,parseInt(t[1],10))/255,this.g=Math.min(255,parseInt(t[2],10))/255,this.b=Math.min(255,parseInt(t[3],10))/255,Ft.toWorkingColorSpace(this,e),i(t[4]),this;if(t=/^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(s))return this.r=Math.min(100,parseInt(t[1],10))/100,this.g=Math.min(100,parseInt(t[2],10))/100,this.b=Math.min(100,parseInt(t[3],10))/100,Ft.toWorkingColorSpace(this,e),i(t[4]),this;break;case"hsl":case"hsla":if(t=/^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(s)){const n=parseFloat(t[1])/360,r=parseFloat(t[2])/100,s=parseFloat(t[3])/100;return i(t[4]),this.setHSL(n,r,s,e)}}}else if(n=/^\#([A-Fa-f\d]+)$/.exec(t)){const t=n[1],i=t.length;if(3===i)return this.r=parseInt(t.charAt(0)+t.charAt(0),16)/255,this.g=parseInt(t.charAt(1)+t.charAt(1),16)/255,this.b=parseInt(t.charAt(2)+t.charAt(2),16)/255,Ft.toWorkingColorSpace(this,e),this;if(6===i)return this.r=parseInt(t.charAt(0)+t.charAt(1),16)/255,this.g=parseInt(t.charAt(2)+t.charAt(3),16)/255,this.b=parseInt(t.charAt(4)+t.charAt(5),16)/255,Ft.toWorkingColorSpace(this,e),this}return t&&t.length>0?this.setColorName(t,e):this}setColorName(t,e="srgb"){const i=kt[t.toLowerCase()];return void 0!==i?this.setHex(i,e):console.warn("THREE.Color: Unknown color "+t),this}clone(){return new this.constructor(this.r,this.g,this.b)}copy(t){return this.r=t.r,this.g=t.g,this.b=t.b,this}copySRGBToLinear(t){return this.r=zt(t.r),this.g=zt(t.g),this.b=zt(t.b),this}copyLinearToSRGB(t){return this.r=Ut(t.r),this.g=Ut(t.g),this.b=Ut(t.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(t="srgb"){return Ft.fromWorkingColorSpace(jt(this,Gt),t),yt(255*Gt.r,0,255)<<16^yt(255*Gt.g,0,255)<<8^yt(255*Gt.b,0,255)<<0}getHexString(t="srgb"){return("000000"+this.getHex(t).toString(16)).slice(-6)}getHSL(t,e=Ft.workingColorSpace){Ft.fromWorkingColorSpace(jt(this,Gt),e);const i=Gt.r,n=Gt.g,r=Gt.b,s=Math.max(i,n,r),a=Math.min(i,n,r);let o,l;const c=(a+s)/2;if(a===s)o=0,l=0;else{const t=s-a;switch(l=c<=.5?t/(s+a):t/(2-s-a),s){case i:o=(n-r)/t+(n2048||e.height>2048?(console.warn("THREE.ImageUtils.getDataURL: Image converted to jpg for performance reasons",t),e.toDataURL("image/jpeg",.6)):e.toDataURL("image/png")}static sRGBToLinear(t){if("undefined"!=typeof HTMLImageElement&&t instanceof HTMLImageElement||"undefined"!=typeof HTMLCanvasElement&&t instanceof HTMLCanvasElement||"undefined"!=typeof ImageBitmap&&t instanceof ImageBitmap){const e=Ot("canvas");e.width=t.width,e.height=t.height;const i=e.getContext("2d");i.drawImage(t,0,0,t.width,t.height);const n=i.getImageData(0,0,t.width,t.height),r=n.data;for(let t=0;t1)switch(this.wrapS){case c:t.x=t.x-Math.floor(t.x);break;case h:t.x=t.x<0?0:1;break;case u:1===Math.abs(Math.floor(t.x)%2)?t.x=Math.ceil(t.x)-t.x:t.x=t.x-Math.floor(t.x)}if(t.y<0||t.y>1)switch(this.wrapT){case c:t.y=t.y-Math.floor(t.y);break;case h:t.y=t.y<0?0:1;break;case u:1===Math.abs(Math.floor(t.y)%2)?t.y=Math.ceil(t.y)-t.y:t.y=t.y-Math.floor(t.y)}return this.flipY&&(t.y=1-t.y),t}set needsUpdate(t){!0===t&&(this.version++,this.source.needsUpdate=!0)}}$t.DEFAULT_IMAGE=null,$t.DEFAULT_MAPPING=n,$t.DEFAULT_ANISOTROPY=1;class Qt{constructor(t=0,e=0,i=0,n=1){Qt.prototype.isVector4=!0,this.x=t,this.y=e,this.z=i,this.w=n}get width(){return this.z}set width(t){this.z=t}get height(){return this.w}set height(t){this.w=t}set(t,e,i,n){return this.x=t,this.y=e,this.z=i,this.w=n,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this.w=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setW(t){return this.w=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;case 3:this.w=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z,this.w)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this.w=void 0!==t.w?t.w:1,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this.w+=t.w,this}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this.w+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this.w=t.w+e.w,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this.w+=t.w*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this.w-=t.w,this}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this.w-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this.w=t.w-e.w,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this.w*=t.w,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this.w*=t,this}applyMatrix4(t){const e=this.x,i=this.y,n=this.z,r=this.w,s=t.elements;return this.x=s[0]*e+s[4]*i+s[8]*n+s[12]*r,this.y=s[1]*e+s[5]*i+s[9]*n+s[13]*r,this.z=s[2]*e+s[6]*i+s[10]*n+s[14]*r,this.w=s[3]*e+s[7]*i+s[11]*n+s[15]*r,this}divideScalar(t){return this.multiplyScalar(1/t)}setAxisAngleFromQuaternion(t){this.w=2*Math.acos(t.w);const e=Math.sqrt(1-t.w*t.w);return e<1e-4?(this.x=1,this.y=0,this.z=0):(this.x=t.x/e,this.y=t.y/e,this.z=t.z/e),this}setAxisAngleFromRotationMatrix(t){let e,i,n,r;const s=.01,a=.1,o=t.elements,l=o[0],c=o[4],h=o[8],u=o[1],d=o[5],p=o[9],m=o[2],f=o[6],g=o[10];if(Math.abs(c-u)o&&t>v?tv?o=0?1:-1,n=1-e*e;if(n>Number.EPSILON){const r=Math.sqrt(n),s=Math.atan2(r,e*i);t=Math.sin(t*s)/r,a=Math.sin(a*s)/r}const r=a*i;if(o=o*t+u*r,l=l*t+d*r,c=c*t+p*r,h=h*t+m*r,t===1-a){const t=1/Math.sqrt(o*o+l*l+c*c+h*h);o*=t,l*=t,c*=t,h*=t}}t[e]=o,t[e+1]=l,t[e+2]=c,t[e+3]=h}static multiplyQuaternionsFlat(t,e,i,n,r,s){const a=i[n],o=i[n+1],l=i[n+2],c=i[n+3],h=r[s],u=r[s+1],d=r[s+2],p=r[s+3];return t[e]=a*p+c*h+o*d-l*u,t[e+1]=o*p+c*u+l*h-a*d,t[e+2]=l*p+c*d+a*u-o*h,t[e+3]=c*p-a*h-o*u-l*d,t}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get w(){return this._w}set w(t){this._w=t,this._onChangeCallback()}set(t,e,i,n){return this._x=t,this._y=e,this._z=i,this._w=n,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._w)}copy(t){return this._x=t.x,this._y=t.y,this._z=t.z,this._w=t.w,this._onChangeCallback(),this}setFromEuler(t,e){const i=t._x,n=t._y,r=t._z,s=t._order,a=Math.cos,o=Math.sin,l=a(i/2),c=a(n/2),h=a(r/2),u=o(i/2),d=o(n/2),p=o(r/2);switch(s){case"XYZ":this._x=u*c*h+l*d*p,this._y=l*d*h-u*c*p,this._z=l*c*p+u*d*h,this._w=l*c*h-u*d*p;break;case"YXZ":this._x=u*c*h+l*d*p,this._y=l*d*h-u*c*p,this._z=l*c*p-u*d*h,this._w=l*c*h+u*d*p;break;case"ZXY":this._x=u*c*h-l*d*p,this._y=l*d*h+u*c*p,this._z=l*c*p+u*d*h,this._w=l*c*h-u*d*p;break;case"ZYX":this._x=u*c*h-l*d*p,this._y=l*d*h+u*c*p,this._z=l*c*p-u*d*h,this._w=l*c*h+u*d*p;break;case"YZX":this._x=u*c*h+l*d*p,this._y=l*d*h+u*c*p,this._z=l*c*p-u*d*h,this._w=l*c*h-u*d*p;break;case"XZY":this._x=u*c*h-l*d*p,this._y=l*d*h-u*c*p,this._z=l*c*p+u*d*h,this._w=l*c*h+u*d*p;break;default:console.warn("THREE.Quaternion: .setFromEuler() encountered an unknown order: "+s)}return!1!==e&&this._onChangeCallback(),this}setFromAxisAngle(t,e){const i=e/2,n=Math.sin(i);return this._x=t.x*n,this._y=t.y*n,this._z=t.z*n,this._w=Math.cos(i),this._onChangeCallback(),this}setFromRotationMatrix(t){const e=t.elements,i=e[0],n=e[4],r=e[8],s=e[1],a=e[5],o=e[9],l=e[2],c=e[6],h=e[10],u=i+a+h;if(u>0){const t=.5/Math.sqrt(u+1);this._w=.25/t,this._x=(c-o)*t,this._y=(r-l)*t,this._z=(s-n)*t}else if(i>a&&i>h){const t=2*Math.sqrt(1+i-a-h);this._w=(c-o)/t,this._x=.25*t,this._y=(n+s)/t,this._z=(r+l)/t}else if(a>h){const t=2*Math.sqrt(1+a-i-h);this._w=(r-l)/t,this._x=(n+s)/t,this._y=.25*t,this._z=(o+c)/t}else{const t=2*Math.sqrt(1+h-i-a);this._w=(s-n)/t,this._x=(r+l)/t,this._y=(o+c)/t,this._z=.25*t}return this._onChangeCallback(),this}setFromUnitVectors(t,e){let i=t.dot(e)+1;return iMath.abs(t.z)?(this._x=-t.y,this._y=t.x,this._z=0,this._w=i):(this._x=0,this._y=-t.z,this._z=t.y,this._w=i)):(this._x=t.y*e.z-t.z*e.y,this._y=t.z*e.x-t.x*e.z,this._z=t.x*e.y-t.y*e.x,this._w=i),this.normalize()}angleTo(t){return 2*Math.acos(Math.abs(yt(this.dot(t),-1,1)))}rotateTowards(t,e){const i=this.angleTo(t);if(0===i)return this;const n=Math.min(1,e/i);return this.slerp(t,n),this}identity(){return this.set(0,0,0,1)}invert(){return this.conjugate()}conjugate(){return this._x*=-1,this._y*=-1,this._z*=-1,this._onChangeCallback(),this}dot(t){return this._x*t._x+this._y*t._y+this._z*t._z+this._w*t._w}lengthSq(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w}length(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)}normalize(){let t=this.length();return 0===t?(this._x=0,this._y=0,this._z=0,this._w=1):(t=1/t,this._x=this._x*t,this._y=this._y*t,this._z=this._z*t,this._w=this._w*t),this._onChangeCallback(),this}multiply(t){return this.multiplyQuaternions(this,t)}premultiply(t){return this.multiplyQuaternions(t,this)}multiplyQuaternions(t,e){const i=t._x,n=t._y,r=t._z,s=t._w,a=e._x,o=e._y,l=e._z,c=e._w;return this._x=i*c+s*a+n*l-r*o,this._y=n*c+s*o+r*a-i*l,this._z=r*c+s*l+i*o-n*a,this._w=s*c-i*a-n*o-r*l,this._onChangeCallback(),this}slerp(t,e){if(0===e)return this;if(1===e)return this.copy(t);const i=this._x,n=this._y,r=this._z,s=this._w;let a=s*t._w+i*t._x+n*t._y+r*t._z;if(a<0?(this._w=-t._w,this._x=-t._x,this._y=-t._y,this._z=-t._z,a=-a):this.copy(t),a>=1)return this._w=s,this._x=i,this._y=n,this._z=r,this;const o=1-a*a;if(o<=Number.EPSILON){const t=1-e;return this._w=t*s+e*this._w,this._x=t*i+e*this._x,this._y=t*n+e*this._y,this._z=t*r+e*this._z,this.normalize(),this._onChangeCallback(),this}const l=Math.sqrt(o),c=Math.atan2(l,a),h=Math.sin((1-e)*c)/l,u=Math.sin(e*c)/l;return this._w=s*h+this._w*u,this._x=i*h+this._x*u,this._y=n*h+this._y*u,this._z=r*h+this._z*u,this._onChangeCallback(),this}slerpQuaternions(t,e,i){return this.copy(t).slerp(e,i)}random(){const t=Math.random(),e=Math.sqrt(1-t),i=Math.sqrt(t),n=2*Math.PI*Math.random(),r=2*Math.PI*Math.random();return this.set(e*Math.cos(n),i*Math.sin(r),i*Math.cos(r),e*Math.sin(n))}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._w===this._w}fromArray(t,e=0){return this._x=t[e],this._y=t[e+1],this._z=t[e+2],this._w=t[e+3],this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._w,t}fromBufferAttribute(t,e){return this._x=t.getX(e),this._y=t.getY(e),this._z=t.getZ(e),this._w=t.getW(e),this}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._w}}class re{constructor(t=0,e=0,i=0){re.prototype.isVector3=!0,this.x=t,this.y=e,this.z=i}set(t,e,i){return void 0===i&&(i=this.z),this.x=t,this.y=e,this.z=i,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this}multiplyVectors(t,e){return this.x=t.x*e.x,this.y=t.y*e.y,this.z=t.z*e.z,this}applyEuler(t){return this.applyQuaternion(ae.setFromEuler(t))}applyAxisAngle(t,e){return this.applyQuaternion(ae.setFromAxisAngle(t,e))}applyMatrix3(t){const e=this.x,i=this.y,n=this.z,r=t.elements;return this.x=r[0]*e+r[3]*i+r[6]*n,this.y=r[1]*e+r[4]*i+r[7]*n,this.z=r[2]*e+r[5]*i+r[8]*n,this}applyNormalMatrix(t){return this.applyMatrix3(t).normalize()}applyMatrix4(t){const e=this.x,i=this.y,n=this.z,r=t.elements,s=1/(r[3]*e+r[7]*i+r[11]*n+r[15]);return this.x=(r[0]*e+r[4]*i+r[8]*n+r[12])*s,this.y=(r[1]*e+r[5]*i+r[9]*n+r[13])*s,this.z=(r[2]*e+r[6]*i+r[10]*n+r[14])*s,this}applyQuaternion(t){const e=this.x,i=this.y,n=this.z,r=t.x,s=t.y,a=t.z,o=t.w,l=o*e+s*n-a*i,c=o*i+a*e-r*n,h=o*n+r*i-s*e,u=-r*e-s*i-a*n;return this.x=l*o+u*-r+c*-a-h*-s,this.y=c*o+u*-s+h*-r-l*-a,this.z=h*o+u*-a+l*-s-c*-r,this}project(t){return this.applyMatrix4(t.matrixWorldInverse).applyMatrix4(t.projectionMatrix)}unproject(t){return this.applyMatrix4(t.projectionMatrixInverse).applyMatrix4(t.matrixWorld)}transformDirection(t){const e=this.x,i=this.y,n=this.z,r=t.elements;return this.x=r[0]*e+r[4]*i+r[8]*n,this.y=r[1]*e+r[5]*i+r[9]*n,this.z=r[2]*e+r[6]*i+r[10]*n,this.normalize()}divide(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this}divideScalar(t){return this.multiplyScalar(1/t)}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this.z=Math.min(this.z,t.z),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this.z=Math.max(this.z,t.z),this}clamp(t,e){return this.x=Math.max(t.x,Math.min(e.x,this.x)),this.y=Math.max(t.y,Math.min(e.y,this.y)),this.z=Math.max(t.z,Math.min(e.z,this.z)),this}clampScalar(t,e){return this.x=Math.max(t,Math.min(e,this.x)),this.y=Math.max(t,Math.min(e,this.y)),this.z=Math.max(t,Math.min(e,this.z)),this}clampLength(t,e){const i=this.length();return this.divideScalar(i||1).multiplyScalar(Math.max(t,Math.min(e,i)))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this}roundToZero(){return this.x=this.x<0?Math.ceil(this.x):Math.floor(this.x),this.y=this.y<0?Math.ceil(this.y):Math.floor(this.y),this.z=this.z<0?Math.ceil(this.z):Math.floor(this.z),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this}dot(t){return this.x*t.x+this.y*t.y+this.z*t.z}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)}normalize(){return this.divideScalar(this.length()||1)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this.z+=(t.z-this.z)*e,this}lerpVectors(t,e,i){return this.x=t.x+(e.x-t.x)*i,this.y=t.y+(e.y-t.y)*i,this.z=t.z+(e.z-t.z)*i,this}cross(t){return this.crossVectors(this,t)}crossVectors(t,e){const i=t.x,n=t.y,r=t.z,s=e.x,a=e.y,o=e.z;return this.x=n*o-r*a,this.y=r*s-i*o,this.z=i*a-n*s,this}projectOnVector(t){const e=t.lengthSq();if(0===e)return this.set(0,0,0);const i=t.dot(this)/e;return this.copy(t).multiplyScalar(i)}projectOnPlane(t){return se.copy(this).projectOnVector(t),this.sub(se)}reflect(t){return this.sub(se.copy(t).multiplyScalar(2*this.dot(t)))}angleTo(t){const e=Math.sqrt(this.lengthSq()*t.lengthSq());if(0===e)return Math.PI/2;const i=this.dot(t)/e;return Math.acos(yt(i,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,i=this.y-t.y,n=this.z-t.z;return e*e+i*i+n*n}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)+Math.abs(this.z-t.z)}setFromSpherical(t){return this.setFromSphericalCoords(t.radius,t.phi,t.theta)}setFromSphericalCoords(t,e,i){const n=Math.sin(e)*t;return this.x=n*Math.sin(i),this.y=Math.cos(e)*t,this.z=n*Math.cos(i),this}setFromCylindrical(t){return this.setFromCylindricalCoords(t.radius,t.theta,t.y)}setFromCylindricalCoords(t,e,i){return this.x=t*Math.sin(e),this.y=i,this.z=t*Math.cos(e),this}setFromMatrixPosition(t){const e=t.elements;return this.x=e[12],this.y=e[13],this.z=e[14],this}setFromMatrixScale(t){const e=this.setFromMatrixColumn(t,0).length(),i=this.setFromMatrixColumn(t,1).length(),n=this.setFromMatrixColumn(t,2).length();return this.x=e,this.y=i,this.z=n,this}setFromMatrixColumn(t,e){return this.fromArray(t.elements,4*e)}setFromMatrix3Column(t,e){return this.fromArray(t.elements,3*e)}setFromEuler(t){return this.x=t._x,this.y=t._y,this.z=t._z,this}equals(t){return t.x===this.x&&t.y===this.y&&t.z===this.z}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this.z=t[e+2],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t[e+2]=this.z,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this.z=t.getZ(e),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this}randomDirection(){const t=2*(Math.random()-.5),e=Math.random()*Math.PI*2,i=Math.sqrt(1-t**2);return this.x=i*Math.cos(e),this.y=i*Math.sin(e),this.z=t,this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z}}const se=new re,ae=new ne;class oe{constructor(t=new re(1/0,1/0,1/0),e=new re(-1/0,-1/0,-1/0)){this.isBox3=!0,this.min=t,this.max=e}set(t,e){return this.min.copy(t),this.max.copy(e),this}setFromArray(t){let e=1/0,i=1/0,n=1/0,r=-1/0,s=-1/0,a=-1/0;for(let o=0,l=t.length;or&&(r=l),c>s&&(s=c),h>a&&(a=h)}return this.min.set(e,i,n),this.max.set(r,s,a),this}setFromBufferAttribute(t){let e=1/0,i=1/0,n=1/0,r=-1/0,s=-1/0,a=-1/0;for(let o=0,l=t.count;or&&(r=l),c>s&&(s=c),h>a&&(a=h)}return this.min.set(e,i,n),this.max.set(r,s,a),this}setFromPoints(t){this.makeEmpty();for(let e=0,i=t.length;ethis.max.x||t.ythis.max.y||t.zthis.max.z)}containsBox(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y&&this.min.z<=t.min.z&&t.max.z<=this.max.z}getParameter(t,e){return e.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y),(t.z-this.min.z)/(this.max.z-this.min.z))}intersectsBox(t){return!(t.max.xthis.max.x||t.max.ythis.max.y||t.max.zthis.max.z)}intersectsSphere(t){return this.clampPoint(t.center,ce),ce.distanceToSquared(t.center)<=t.radius*t.radius}intersectsPlane(t){let e,i;return t.normal.x>0?(e=t.normal.x*this.min.x,i=t.normal.x*this.max.x):(e=t.normal.x*this.max.x,i=t.normal.x*this.min.x),t.normal.y>0?(e+=t.normal.y*this.min.y,i+=t.normal.y*this.max.y):(e+=t.normal.y*this.max.y,i+=t.normal.y*this.min.y),t.normal.z>0?(e+=t.normal.z*this.min.z,i+=t.normal.z*this.max.z):(e+=t.normal.z*this.max.z,i+=t.normal.z*this.min.z),e<=-t.constant&&i>=-t.constant}intersectsTriangle(t){if(this.isEmpty())return!1;this.getCenter(ve),xe.subVectors(this.max,ve),ue.subVectors(t.a,ve),de.subVectors(t.b,ve),pe.subVectors(t.c,ve),me.subVectors(de,ue),fe.subVectors(pe,de),ge.subVectors(ue,pe);let e=[0,-me.z,me.y,0,-fe.z,fe.y,0,-ge.z,ge.y,me.z,0,-me.x,fe.z,0,-fe.x,ge.z,0,-ge.x,-me.y,me.x,0,-fe.y,fe.x,0,-ge.y,ge.x,0];return!!Me(e,ue,de,pe,xe)&&(e=[1,0,0,0,1,0,0,0,1],!!Me(e,ue,de,pe,xe)&&(_e.crossVectors(me,fe),e=[_e.x,_e.y,_e.z],Me(e,ue,de,pe,xe)))}clampPoint(t,e){return e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return ce.copy(t).clamp(this.min,this.max).sub(t).length()}getBoundingSphere(t){return this.getCenter(t.center),t.radius=.5*this.getSize(ce).length(),t}intersect(t){return this.min.max(t.min),this.max.min(t.max),this.isEmpty()&&this.makeEmpty(),this}union(t){return this.min.min(t.min),this.max.max(t.max),this}applyMatrix4(t){return this.isEmpty()||(le[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(t),le[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(t),le[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(t),le[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(t),le[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(t),le[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(t),le[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(t),le[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(t),this.setFromPoints(le)),this}translate(t){return this.min.add(t),this.max.add(t),this}equals(t){return t.min.equals(this.min)&&t.max.equals(this.max)}}const le=[new re,new re,new re,new re,new re,new re,new re,new re],ce=new re,he=new oe,ue=new re,de=new re,pe=new re,me=new re,fe=new re,ge=new re,ve=new re,xe=new re,_e=new re,ye=new re;function Me(t,e,i,n,r){for(let s=0,a=t.length-3;s<=a;s+=3){ye.fromArray(t,s);const a=r.x*Math.abs(ye.x)+r.y*Math.abs(ye.y)+r.z*Math.abs(ye.z),o=e.dot(ye),l=i.dot(ye),c=n.dot(ye);if(Math.max(-Math.max(o,l,c),Math.min(o,l,c))>a)return!1}return!0}const be=new oe,Se=new re,we=new re;class Te{constructor(t=new re,e=-1){this.center=t,this.radius=e}set(t,e){return this.center.copy(t),this.radius=e,this}setFromPoints(t,e){const i=this.center;void 0!==e?i.copy(e):be.setFromPoints(t).getCenter(i);let n=0;for(let e=0,r=t.length;ethis.radius*this.radius&&(e.sub(this.center).normalize(),e.multiplyScalar(this.radius).add(this.center)),e}getBoundingBox(t){return this.isEmpty()?(t.makeEmpty(),t):(t.set(this.center,this.center),t.expandByScalar(this.radius),t)}applyMatrix4(t){return this.center.applyMatrix4(t),this.radius=this.radius*t.getMaxScaleOnAxis(),this}translate(t){return this.center.add(t),this}expandByPoint(t){if(this.isEmpty())return this.center.copy(t),this.radius=0,this;Se.subVectors(t,this.center);const e=Se.lengthSq();if(e>this.radius*this.radius){const t=Math.sqrt(e),i=.5*(t-this.radius);this.center.addScaledVector(Se,i/t),this.radius+=i}return this}union(t){return t.isEmpty()?this:this.isEmpty()?(this.copy(t),this):(!0===this.center.equals(t.center)?this.radius=Math.max(this.radius,t.radius):(we.subVectors(t.center,this.center).setLength(t.radius),this.expandByPoint(Se.copy(t.center).add(we)),this.expandByPoint(Se.copy(t.center).sub(we))),this)}equals(t){return t.center.equals(this.center)&&t.radius===this.radius}clone(){return(new this.constructor).copy(this)}}const Ae=new re,Ee=new re,Ce=new re,Le=new re,Re=new re,Pe=new re,Ie=new re;class De{constructor(t=new re,e=new re(0,0,-1)){this.origin=t,this.direction=e}set(t,e){return this.origin.copy(t),this.direction.copy(e),this}copy(t){return this.origin.copy(t.origin),this.direction.copy(t.direction),this}at(t,e){return e.copy(this.direction).multiplyScalar(t).add(this.origin)}lookAt(t){return this.direction.copy(t).sub(this.origin).normalize(),this}recast(t){return this.origin.copy(this.at(t,Ae)),this}closestPointToPoint(t,e){e.subVectors(t,this.origin);const i=e.dot(this.direction);return i<0?e.copy(this.origin):e.copy(this.direction).multiplyScalar(i).add(this.origin)}distanceToPoint(t){return Math.sqrt(this.distanceSqToPoint(t))}distanceSqToPoint(t){const e=Ae.subVectors(t,this.origin).dot(this.direction);return e<0?this.origin.distanceToSquared(t):(Ae.copy(this.direction).multiplyScalar(e).add(this.origin),Ae.distanceToSquared(t))}distanceSqToSegment(t,e,i,n){Ee.copy(t).add(e).multiplyScalar(.5),Ce.copy(e).sub(t).normalize(),Le.copy(this.origin).sub(Ee);const r=.5*t.distanceTo(e),s=-this.direction.dot(Ce),a=Le.dot(this.direction),o=-Le.dot(Ce),l=Le.lengthSq(),c=Math.abs(1-s*s);let h,u,d,p;if(c>0)if(h=s*o-a,u=s*a-o,p=r*c,h>=0)if(u>=-p)if(u<=p){const t=1/c;h*=t,u*=t,d=h*(h+s*u+2*a)+u*(s*h+u+2*o)+l}else u=r,h=Math.max(0,-(s*u+a)),d=-h*h+u*(u+2*o)+l;else u=-r,h=Math.max(0,-(s*u+a)),d=-h*h+u*(u+2*o)+l;else u<=-p?(h=Math.max(0,-(-s*r+a)),u=h>0?-r:Math.min(Math.max(-r,-o),r),d=-h*h+u*(u+2*o)+l):u<=p?(h=0,u=Math.min(Math.max(-r,-o),r),d=u*(u+2*o)+l):(h=Math.max(0,-(s*r+a)),u=h>0?r:Math.min(Math.max(-r,-o),r),d=-h*h+u*(u+2*o)+l);else u=s>0?-r:r,h=Math.max(0,-(s*u+a)),d=-h*h+u*(u+2*o)+l;return i&&i.copy(this.direction).multiplyScalar(h).add(this.origin),n&&n.copy(Ce).multiplyScalar(u).add(Ee),d}intersectSphere(t,e){Ae.subVectors(t.center,this.origin);const i=Ae.dot(this.direction),n=Ae.dot(Ae)-i*i,r=t.radius*t.radius;if(n>r)return null;const s=Math.sqrt(r-n),a=i-s,o=i+s;return a<0&&o<0?null:a<0?this.at(o,e):this.at(a,e)}intersectsSphere(t){return this.distanceSqToPoint(t.center)<=t.radius*t.radius}distanceToPlane(t){const e=t.normal.dot(this.direction);if(0===e)return 0===t.distanceToPoint(this.origin)?0:null;const i=-(this.origin.dot(t.normal)+t.constant)/e;return i>=0?i:null}intersectPlane(t,e){const i=this.distanceToPlane(t);return null===i?null:this.at(i,e)}intersectsPlane(t){const e=t.distanceToPoint(this.origin);if(0===e)return!0;return t.normal.dot(this.direction)*e<0}intersectBox(t,e){let i,n,r,s,a,o;const l=1/this.direction.x,c=1/this.direction.y,h=1/this.direction.z,u=this.origin;return l>=0?(i=(t.min.x-u.x)*l,n=(t.max.x-u.x)*l):(i=(t.max.x-u.x)*l,n=(t.min.x-u.x)*l),c>=0?(r=(t.min.y-u.y)*c,s=(t.max.y-u.y)*c):(r=(t.max.y-u.y)*c,s=(t.min.y-u.y)*c),i>s||r>n?null:((r>i||isNaN(i))&&(i=r),(s=0?(a=(t.min.z-u.z)*h,o=(t.max.z-u.z)*h):(a=(t.max.z-u.z)*h,o=(t.min.z-u.z)*h),i>o||a>n?null:((a>i||i!=i)&&(i=a),(o=0?i:n,e)))}intersectsBox(t){return null!==this.intersectBox(t,Ae)}intersectTriangle(t,e,i,n,r){Re.subVectors(e,t),Pe.subVectors(i,t),Ie.crossVectors(Re,Pe);let s,a=this.direction.dot(Ie);if(a>0){if(n)return null;s=1}else{if(!(a<0))return null;s=-1,a=-a}Le.subVectors(this.origin,t);const o=s*this.direction.dot(Pe.crossVectors(Le,Pe));if(o<0)return null;const l=s*this.direction.dot(Re.cross(Le));if(l<0)return null;if(o+l>a)return null;const c=-s*Le.dot(Ie);return c<0?null:this.at(c/a,r)}applyMatrix4(t){return this.origin.applyMatrix4(t),this.direction.transformDirection(t),this}equals(t){return t.origin.equals(this.origin)&&t.direction.equals(this.direction)}clone(){return(new this.constructor).copy(this)}}class Ne{constructor(){Ne.prototype.isMatrix4=!0,this.elements=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]}set(t,e,i,n,r,s,a,o,l,c,h,u,d,p,m,f){const g=this.elements;return g[0]=t,g[4]=e,g[8]=i,g[12]=n,g[1]=r,g[5]=s,g[9]=a,g[13]=o,g[2]=l,g[6]=c,g[10]=h,g[14]=u,g[3]=d,g[7]=p,g[11]=m,g[15]=f,this}identity(){return this.set(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1),this}clone(){return(new Ne).fromArray(this.elements)}copy(t){const e=this.elements,i=t.elements;return e[0]=i[0],e[1]=i[1],e[2]=i[2],e[3]=i[3],e[4]=i[4],e[5]=i[5],e[6]=i[6],e[7]=i[7],e[8]=i[8],e[9]=i[9],e[10]=i[10],e[11]=i[11],e[12]=i[12],e[13]=i[13],e[14]=i[14],e[15]=i[15],this}copyPosition(t){const e=this.elements,i=t.elements;return e[12]=i[12],e[13]=i[13],e[14]=i[14],this}setFromMatrix3(t){const e=t.elements;return this.set(e[0],e[3],e[6],0,e[1],e[4],e[7],0,e[2],e[5],e[8],0,0,0,0,1),this}extractBasis(t,e,i){return t.setFromMatrixColumn(this,0),e.setFromMatrixColumn(this,1),i.setFromMatrixColumn(this,2),this}makeBasis(t,e,i){return this.set(t.x,e.x,i.x,0,t.y,e.y,i.y,0,t.z,e.z,i.z,0,0,0,0,1),this}extractRotation(t){const e=this.elements,i=t.elements,n=1/Oe.setFromMatrixColumn(t,0).length(),r=1/Oe.setFromMatrixColumn(t,1).length(),s=1/Oe.setFromMatrixColumn(t,2).length();return e[0]=i[0]*n,e[1]=i[1]*n,e[2]=i[2]*n,e[3]=0,e[4]=i[4]*r,e[5]=i[5]*r,e[6]=i[6]*r,e[7]=0,e[8]=i[8]*s,e[9]=i[9]*s,e[10]=i[10]*s,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromEuler(t){const e=this.elements,i=t.x,n=t.y,r=t.z,s=Math.cos(i),a=Math.sin(i),o=Math.cos(n),l=Math.sin(n),c=Math.cos(r),h=Math.sin(r);if("XYZ"===t.order){const t=s*c,i=s*h,n=a*c,r=a*h;e[0]=o*c,e[4]=-o*h,e[8]=l,e[1]=i+n*l,e[5]=t-r*l,e[9]=-a*o,e[2]=r-t*l,e[6]=n+i*l,e[10]=s*o}else if("YXZ"===t.order){const t=o*c,i=o*h,n=l*c,r=l*h;e[0]=t+r*a,e[4]=n*a-i,e[8]=s*l,e[1]=s*h,e[5]=s*c,e[9]=-a,e[2]=i*a-n,e[6]=r+t*a,e[10]=s*o}else if("ZXY"===t.order){const t=o*c,i=o*h,n=l*c,r=l*h;e[0]=t-r*a,e[4]=-s*h,e[8]=n+i*a,e[1]=i+n*a,e[5]=s*c,e[9]=r-t*a,e[2]=-s*l,e[6]=a,e[10]=s*o}else if("ZYX"===t.order){const t=s*c,i=s*h,n=a*c,r=a*h;e[0]=o*c,e[4]=n*l-i,e[8]=t*l+r,e[1]=o*h,e[5]=r*l+t,e[9]=i*l-n,e[2]=-l,e[6]=a*o,e[10]=s*o}else if("YZX"===t.order){const t=s*o,i=s*l,n=a*o,r=a*l;e[0]=o*c,e[4]=r-t*h,e[8]=n*h+i,e[1]=h,e[5]=s*c,e[9]=-a*c,e[2]=-l*c,e[6]=i*h+n,e[10]=t-r*h}else if("XZY"===t.order){const t=s*o,i=s*l,n=a*o,r=a*l;e[0]=o*c,e[4]=-h,e[8]=l*c,e[1]=t*h+r,e[5]=s*c,e[9]=i*h-n,e[2]=n*h-i,e[6]=a*c,e[10]=r*h+t}return e[3]=0,e[7]=0,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromQuaternion(t){return this.compose(Ue,t,Be)}lookAt(t,e,i){const n=this.elements;return Ge.subVectors(t,e),0===Ge.lengthSq()&&(Ge.z=1),Ge.normalize(),Fe.crossVectors(i,Ge),0===Fe.lengthSq()&&(1===Math.abs(i.z)?Ge.x+=1e-4:Ge.z+=1e-4,Ge.normalize(),Fe.crossVectors(i,Ge)),Fe.normalize(),ke.crossVectors(Ge,Fe),n[0]=Fe.x,n[4]=ke.x,n[8]=Ge.x,n[1]=Fe.y,n[5]=ke.y,n[9]=Ge.y,n[2]=Fe.z,n[6]=ke.z,n[10]=Ge.z,this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const i=t.elements,n=e.elements,r=this.elements,s=i[0],a=i[4],o=i[8],l=i[12],c=i[1],h=i[5],u=i[9],d=i[13],p=i[2],m=i[6],f=i[10],g=i[14],v=i[3],x=i[7],_=i[11],y=i[15],M=n[0],b=n[4],S=n[8],w=n[12],T=n[1],A=n[5],E=n[9],C=n[13],L=n[2],R=n[6],P=n[10],I=n[14],D=n[3],N=n[7],O=n[11],z=n[15];return r[0]=s*M+a*T+o*L+l*D,r[4]=s*b+a*A+o*R+l*N,r[8]=s*S+a*E+o*P+l*O,r[12]=s*w+a*C+o*I+l*z,r[1]=c*M+h*T+u*L+d*D,r[5]=c*b+h*A+u*R+d*N,r[9]=c*S+h*E+u*P+d*O,r[13]=c*w+h*C+u*I+d*z,r[2]=p*M+m*T+f*L+g*D,r[6]=p*b+m*A+f*R+g*N,r[10]=p*S+m*E+f*P+g*O,r[14]=p*w+m*C+f*I+g*z,r[3]=v*M+x*T+_*L+y*D,r[7]=v*b+x*A+_*R+y*N,r[11]=v*S+x*E+_*P+y*O,r[15]=v*w+x*C+_*I+y*z,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[4]*=t,e[8]*=t,e[12]*=t,e[1]*=t,e[5]*=t,e[9]*=t,e[13]*=t,e[2]*=t,e[6]*=t,e[10]*=t,e[14]*=t,e[3]*=t,e[7]*=t,e[11]*=t,e[15]*=t,this}determinant(){const t=this.elements,e=t[0],i=t[4],n=t[8],r=t[12],s=t[1],a=t[5],o=t[9],l=t[13],c=t[2],h=t[6],u=t[10],d=t[14];return t[3]*(+r*o*h-n*l*h-r*a*u+i*l*u+n*a*d-i*o*d)+t[7]*(+e*o*d-e*l*u+r*s*u-n*s*d+n*l*c-r*o*c)+t[11]*(+e*l*h-e*a*d-r*s*h+i*s*d+r*a*c-i*l*c)+t[15]*(-n*a*c-e*o*h+e*a*u+n*s*h-i*s*u+i*o*c)}transpose(){const t=this.elements;let e;return e=t[1],t[1]=t[4],t[4]=e,e=t[2],t[2]=t[8],t[8]=e,e=t[6],t[6]=t[9],t[9]=e,e=t[3],t[3]=t[12],t[12]=e,e=t[7],t[7]=t[13],t[13]=e,e=t[11],t[11]=t[14],t[14]=e,this}setPosition(t,e,i){const n=this.elements;return t.isVector3?(n[12]=t.x,n[13]=t.y,n[14]=t.z):(n[12]=t,n[13]=e,n[14]=i),this}invert(){const t=this.elements,e=t[0],i=t[1],n=t[2],r=t[3],s=t[4],a=t[5],o=t[6],l=t[7],c=t[8],h=t[9],u=t[10],d=t[11],p=t[12],m=t[13],f=t[14],g=t[15],v=h*f*l-m*u*l+m*o*d-a*f*d-h*o*g+a*u*g,x=p*u*l-c*f*l-p*o*d+s*f*d+c*o*g-s*u*g,_=c*m*l-p*h*l+p*a*d-s*m*d-c*a*g+s*h*g,y=p*h*o-c*m*o-p*a*u+s*m*u+c*a*f-s*h*f,M=e*v+i*x+n*_+r*y;if(0===M)return this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);const b=1/M;return t[0]=v*b,t[1]=(m*u*r-h*f*r-m*n*d+i*f*d+h*n*g-i*u*g)*b,t[2]=(a*f*r-m*o*r+m*n*l-i*f*l-a*n*g+i*o*g)*b,t[3]=(h*o*r-a*u*r-h*n*l+i*u*l+a*n*d-i*o*d)*b,t[4]=x*b,t[5]=(c*f*r-p*u*r+p*n*d-e*f*d-c*n*g+e*u*g)*b,t[6]=(p*o*r-s*f*r-p*n*l+e*f*l+s*n*g-e*o*g)*b,t[7]=(s*u*r-c*o*r+c*n*l-e*u*l-s*n*d+e*o*d)*b,t[8]=_*b,t[9]=(p*h*r-c*m*r-p*i*d+e*m*d+c*i*g-e*h*g)*b,t[10]=(s*m*r-p*a*r+p*i*l-e*m*l-s*i*g+e*a*g)*b,t[11]=(c*a*r-s*h*r-c*i*l+e*h*l+s*i*d-e*a*d)*b,t[12]=y*b,t[13]=(c*m*n-p*h*n+p*i*u-e*m*u-c*i*f+e*h*f)*b,t[14]=(p*a*n-s*m*n-p*i*o+e*m*o+s*i*f-e*a*f)*b,t[15]=(s*h*n-c*a*n+c*i*o-e*h*o-s*i*u+e*a*u)*b,this}scale(t){const e=this.elements,i=t.x,n=t.y,r=t.z;return e[0]*=i,e[4]*=n,e[8]*=r,e[1]*=i,e[5]*=n,e[9]*=r,e[2]*=i,e[6]*=n,e[10]*=r,e[3]*=i,e[7]*=n,e[11]*=r,this}getMaxScaleOnAxis(){const t=this.elements,e=t[0]*t[0]+t[1]*t[1]+t[2]*t[2],i=t[4]*t[4]+t[5]*t[5]+t[6]*t[6],n=t[8]*t[8]+t[9]*t[9]+t[10]*t[10];return Math.sqrt(Math.max(e,i,n))}makeTranslation(t,e,i){return this.set(1,0,0,t,0,1,0,e,0,0,1,i,0,0,0,1),this}makeRotationX(t){const e=Math.cos(t),i=Math.sin(t);return this.set(1,0,0,0,0,e,-i,0,0,i,e,0,0,0,0,1),this}makeRotationY(t){const e=Math.cos(t),i=Math.sin(t);return this.set(e,0,i,0,0,1,0,0,-i,0,e,0,0,0,0,1),this}makeRotationZ(t){const e=Math.cos(t),i=Math.sin(t);return this.set(e,-i,0,0,i,e,0,0,0,0,1,0,0,0,0,1),this}makeRotationAxis(t,e){const i=Math.cos(e),n=Math.sin(e),r=1-i,s=t.x,a=t.y,o=t.z,l=r*s,c=r*a;return this.set(l*s+i,l*a-n*o,l*o+n*a,0,l*a+n*o,c*a+i,c*o-n*s,0,l*o-n*a,c*o+n*s,r*o*o+i,0,0,0,0,1),this}makeScale(t,e,i){return this.set(t,0,0,0,0,e,0,0,0,0,i,0,0,0,0,1),this}makeShear(t,e,i,n,r,s){return this.set(1,i,r,0,t,1,s,0,e,n,1,0,0,0,0,1),this}compose(t,e,i){const n=this.elements,r=e._x,s=e._y,a=e._z,o=e._w,l=r+r,c=s+s,h=a+a,u=r*l,d=r*c,p=r*h,m=s*c,f=s*h,g=a*h,v=o*l,x=o*c,_=o*h,y=i.x,M=i.y,b=i.z;return n[0]=(1-(m+g))*y,n[1]=(d+_)*y,n[2]=(p-x)*y,n[3]=0,n[4]=(d-_)*M,n[5]=(1-(u+g))*M,n[6]=(f+v)*M,n[7]=0,n[8]=(p+x)*b,n[9]=(f-v)*b,n[10]=(1-(u+m))*b,n[11]=0,n[12]=t.x,n[13]=t.y,n[14]=t.z,n[15]=1,this}decompose(t,e,i){const n=this.elements;let r=Oe.set(n[0],n[1],n[2]).length();const s=Oe.set(n[4],n[5],n[6]).length(),a=Oe.set(n[8],n[9],n[10]).length();this.determinant()<0&&(r=-r),t.x=n[12],t.y=n[13],t.z=n[14],ze.copy(this);const o=1/r,l=1/s,c=1/a;return ze.elements[0]*=o,ze.elements[1]*=o,ze.elements[2]*=o,ze.elements[4]*=l,ze.elements[5]*=l,ze.elements[6]*=l,ze.elements[8]*=c,ze.elements[9]*=c,ze.elements[10]*=c,e.setFromRotationMatrix(ze),i.x=r,i.y=s,i.z=a,this}makePerspective(t,e,i,n,r,s){const a=this.elements,o=2*r/(e-t),l=2*r/(i-n),c=(e+t)/(e-t),h=(i+n)/(i-n),u=-(s+r)/(s-r),d=-2*s*r/(s-r);return a[0]=o,a[4]=0,a[8]=c,a[12]=0,a[1]=0,a[5]=l,a[9]=h,a[13]=0,a[2]=0,a[6]=0,a[10]=u,a[14]=d,a[3]=0,a[7]=0,a[11]=-1,a[15]=0,this}makeOrthographic(t,e,i,n,r,s){const a=this.elements,o=1/(e-t),l=1/(i-n),c=1/(s-r),h=(e+t)*o,u=(i+n)*l,d=(s+r)*c;return a[0]=2*o,a[4]=0,a[8]=0,a[12]=-h,a[1]=0,a[5]=2*l,a[9]=0,a[13]=-u,a[2]=0,a[6]=0,a[10]=-2*c,a[14]=-d,a[3]=0,a[7]=0,a[11]=0,a[15]=1,this}equals(t){const e=this.elements,i=t.elements;for(let t=0;t<16;t++)if(e[t]!==i[t])return!1;return!0}fromArray(t,e=0){for(let i=0;i<16;i++)this.elements[i]=t[i+e];return this}toArray(t=[],e=0){const i=this.elements;return t[e]=i[0],t[e+1]=i[1],t[e+2]=i[2],t[e+3]=i[3],t[e+4]=i[4],t[e+5]=i[5],t[e+6]=i[6],t[e+7]=i[7],t[e+8]=i[8],t[e+9]=i[9],t[e+10]=i[10],t[e+11]=i[11],t[e+12]=i[12],t[e+13]=i[13],t[e+14]=i[14],t[e+15]=i[15],t}}const Oe=new re,ze=new Ne,Ue=new re(0,0,0),Be=new re(1,1,1),Fe=new re,ke=new re,Ge=new re,Ve=new Ne,He=new ne;class We{constructor(t=0,e=0,i=0,n=We.DefaultOrder){this.isEuler=!0,this._x=t,this._y=e,this._z=i,this._order=n}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get order(){return this._order}set order(t){this._order=t,this._onChangeCallback()}set(t,e,i,n=this._order){return this._x=t,this._y=e,this._z=i,this._order=n,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._order)}copy(t){return this._x=t._x,this._y=t._y,this._z=t._z,this._order=t._order,this._onChangeCallback(),this}setFromRotationMatrix(t,e=this._order,i=!0){const n=t.elements,r=n[0],s=n[4],a=n[8],o=n[1],l=n[5],c=n[9],h=n[2],u=n[6],d=n[10];switch(e){case"XYZ":this._y=Math.asin(yt(a,-1,1)),Math.abs(a)<.9999999?(this._x=Math.atan2(-c,d),this._z=Math.atan2(-s,r)):(this._x=Math.atan2(u,l),this._z=0);break;case"YXZ":this._x=Math.asin(-yt(c,-1,1)),Math.abs(c)<.9999999?(this._y=Math.atan2(a,d),this._z=Math.atan2(o,l)):(this._y=Math.atan2(-h,r),this._z=0);break;case"ZXY":this._x=Math.asin(yt(u,-1,1)),Math.abs(u)<.9999999?(this._y=Math.atan2(-h,d),this._z=Math.atan2(-s,l)):(this._y=0,this._z=Math.atan2(o,r));break;case"ZYX":this._y=Math.asin(-yt(h,-1,1)),Math.abs(h)<.9999999?(this._x=Math.atan2(u,d),this._z=Math.atan2(o,r)):(this._x=0,this._z=Math.atan2(-s,l));break;case"YZX":this._z=Math.asin(yt(o,-1,1)),Math.abs(o)<.9999999?(this._x=Math.atan2(-c,l),this._y=Math.atan2(-h,r)):(this._x=0,this._y=Math.atan2(a,d));break;case"XZY":this._z=Math.asin(-yt(s,-1,1)),Math.abs(s)<.9999999?(this._x=Math.atan2(u,l),this._y=Math.atan2(a,r)):(this._x=Math.atan2(-c,d),this._y=0);break;default:console.warn("THREE.Euler: .setFromRotationMatrix() encountered an unknown order: "+e)}return this._order=e,!0===i&&this._onChangeCallback(),this}setFromQuaternion(t,e,i){return Ve.makeRotationFromQuaternion(t),this.setFromRotationMatrix(Ve,e,i)}setFromVector3(t,e=this._order){return this.set(t.x,t.y,t.z,e)}reorder(t){return He.setFromEuler(this),this.setFromQuaternion(He,t)}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._order===this._order}fromArray(t){return this._x=t[0],this._y=t[1],this._z=t[2],void 0!==t[3]&&(this._order=t[3]),this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._order,t}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._order}toVector3(){console.error("THREE.Euler: .toVector3() has been removed. Use Vector3.setFromEuler() instead")}}We.DefaultOrder="XYZ",We.RotationOrders=["XYZ","YZX","ZXY","XZY","YXZ","ZYX"];class je{constructor(){this.mask=1}set(t){this.mask=(1<>>0}enable(t){this.mask|=1<1){for(let t=0;t1){for(let t=0;t0){n.children=[];for(let e=0;e0){n.animations=[];for(let e=0;e0&&(i.geometries=e),n.length>0&&(i.materials=n),r.length>0&&(i.textures=r),a.length>0&&(i.images=a),o.length>0&&(i.shapes=o),l.length>0&&(i.skeletons=l),c.length>0&&(i.animations=c),h.length>0&&(i.nodes=h)}return i.object=n,i;function s(t){const e=[];for(const i in t){const n=t[i];delete n.metadata,e.push(n)}return e}}clone(t){return(new this.constructor).copy(this,t)}copy(t,e=!0){if(this.name=t.name,this.up.copy(t.up),this.position.copy(t.position),this.rotation.order=t.rotation.order,this.quaternion.copy(t.quaternion),this.scale.copy(t.scale),this.matrix.copy(t.matrix),this.matrixWorld.copy(t.matrixWorld),this.matrixAutoUpdate=t.matrixAutoUpdate,this.matrixWorldNeedsUpdate=t.matrixWorldNeedsUpdate,this.matrixWorldAutoUpdate=t.matrixWorldAutoUpdate,this.layers.mask=t.layers.mask,this.visible=t.visible,this.castShadow=t.castShadow,this.receiveShadow=t.receiveShadow,this.frustumCulled=t.frustumCulled,this.renderOrder=t.renderOrder,this.userData=JSON.parse(JSON.stringify(t.userData)),!0===e)for(let e=0;e0?n.multiplyScalar(1/Math.sqrt(r)):n.set(0,0,0)}static getBarycoord(t,e,i,n,r){ai.subVectors(n,e),oi.subVectors(i,e),li.subVectors(t,e);const s=ai.dot(ai),a=ai.dot(oi),o=ai.dot(li),l=oi.dot(oi),c=oi.dot(li),h=s*l-a*a;if(0===h)return r.set(-2,-1,-1);const u=1/h,d=(l*o-a*c)*u,p=(s*c-a*o)*u;return r.set(1-d-p,p,d)}static containsPoint(t,e,i,n){return this.getBarycoord(t,e,i,n,ci),ci.x>=0&&ci.y>=0&&ci.x+ci.y<=1}static getUV(t,e,i,n,r,s,a,o){return this.getBarycoord(t,e,i,n,ci),o.set(0,0),o.addScaledVector(r,ci.x),o.addScaledVector(s,ci.y),o.addScaledVector(a,ci.z),o}static isFrontFacing(t,e,i,n){return ai.subVectors(i,e),oi.subVectors(t,e),ai.cross(oi).dot(n)<0}set(t,e,i){return this.a.copy(t),this.b.copy(e),this.c.copy(i),this}setFromPointsAndIndices(t,e,i,n){return this.a.copy(t[e]),this.b.copy(t[i]),this.c.copy(t[n]),this}setFromAttributeAndIndices(t,e,i,n){return this.a.fromBufferAttribute(t,e),this.b.fromBufferAttribute(t,i),this.c.fromBufferAttribute(t,n),this}clone(){return(new this.constructor).copy(this)}copy(t){return this.a.copy(t.a),this.b.copy(t.b),this.c.copy(t.c),this}getArea(){return ai.subVectors(this.c,this.b),oi.subVectors(this.a,this.b),.5*ai.cross(oi).length()}getMidpoint(t){return t.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(t){return gi.getNormal(this.a,this.b,this.c,t)}getPlane(t){return t.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(t,e){return gi.getBarycoord(t,this.a,this.b,this.c,e)}getUV(t,e,i,n,r){return gi.getUV(t,this.a,this.b,this.c,e,i,n,r)}containsPoint(t){return gi.containsPoint(t,this.a,this.b,this.c)}isFrontFacing(t){return gi.isFrontFacing(this.a,this.b,this.c,t)}intersectsBox(t){return t.intersectsTriangle(this)}closestPointToPoint(t,e){const i=this.a,n=this.b,r=this.c;let s,a;hi.subVectors(n,i),ui.subVectors(r,i),pi.subVectors(t,i);const o=hi.dot(pi),l=ui.dot(pi);if(o<=0&&l<=0)return e.copy(i);mi.subVectors(t,n);const c=hi.dot(mi),h=ui.dot(mi);if(c>=0&&h<=c)return e.copy(n);const u=o*h-c*l;if(u<=0&&o>=0&&c<=0)return s=o/(o-c),e.copy(i).addScaledVector(hi,s);fi.subVectors(t,r);const d=hi.dot(fi),p=ui.dot(fi);if(p>=0&&d<=p)return e.copy(r);const m=d*l-o*p;if(m<=0&&l>=0&&p<=0)return a=l/(l-p),e.copy(i).addScaledVector(ui,a);const f=c*p-d*h;if(f<=0&&h-c>=0&&d-p>=0)return di.subVectors(r,n),a=(h-c)/(h-c+(d-p)),e.copy(n).addScaledVector(di,a);const g=1/(f+m+u);return s=m*g,a=u*g,e.copy(i).addScaledVector(hi,s).addScaledVector(ui,a)}equals(t){return t.a.equals(this.a)&&t.b.equals(this.b)&&t.c.equals(this.c)}}let vi=0;class xi extends mt{constructor(){super(),this.isMaterial=!0,Object.defineProperty(this,"id",{value:vi++}),this.uuid=_t(),this.name="",this.type="Material",this.blending=1,this.side=0,this.vertexColors=!1,this.opacity=1,this.transparent=!1,this.blendSrc=204,this.blendDst=205,this.blendEquation=i,this.blendSrcAlpha=null,this.blendDstAlpha=null,this.blendEquationAlpha=null,this.depthFunc=3,this.depthTest=!0,this.depthWrite=!0,this.stencilWriteMask=255,this.stencilFunc=519,this.stencilRef=0,this.stencilFuncMask=255,this.stencilFail=ht,this.stencilZFail=ht,this.stencilZPass=ht,this.stencilWrite=!1,this.clippingPlanes=null,this.clipIntersection=!1,this.clipShadows=!1,this.shadowSide=null,this.colorWrite=!0,this.precision=null,this.polygonOffset=!1,this.polygonOffsetFactor=0,this.polygonOffsetUnits=0,this.dithering=!1,this.alphaToCoverage=!1,this.premultipliedAlpha=!1,this.visible=!0,this.toneMapped=!0,this.userData={},this.version=0,this._alphaTest=0}get alphaTest(){return this._alphaTest}set alphaTest(t){this._alphaTest>0!=t>0&&this.version++,this._alphaTest=t}onBuild(){}onBeforeRender(){}onBeforeCompile(){}customProgramCacheKey(){return this.onBeforeCompile.toString()}setValues(t){if(void 0!==t)for(const e in t){const i=t[e];if(void 0===i){console.warn("THREE.Material: '"+e+"' parameter is undefined.");continue}const n=this[e];void 0!==n?n&&n.isColor?n.set(i):n&&n.isVector3&&i&&i.isVector3?n.copy(i):this[e]=i:console.warn("THREE."+this.type+": '"+e+"' is not a property of this material.")}}toJSON(t){const e=void 0===t||"string"==typeof t;e&&(t={textures:{},images:{}});const i={metadata:{version:4.5,type:"Material",generator:"Material.toJSON"}};function n(t){const e=[];for(const i in t){const n=t[i];delete n.metadata,e.push(n)}return e}if(i.uuid=this.uuid,i.type=this.type,""!==this.name&&(i.name=this.name),this.color&&this.color.isColor&&(i.color=this.color.getHex()),void 0!==this.roughness&&(i.roughness=this.roughness),void 0!==this.metalness&&(i.metalness=this.metalness),void 0!==this.sheen&&(i.sheen=this.sheen),this.sheenColor&&this.sheenColor.isColor&&(i.sheenColor=this.sheenColor.getHex()),void 0!==this.sheenRoughness&&(i.sheenRoughness=this.sheenRoughness),this.emissive&&this.emissive.isColor&&(i.emissive=this.emissive.getHex()),this.emissiveIntensity&&1!==this.emissiveIntensity&&(i.emissiveIntensity=this.emissiveIntensity),this.specular&&this.specular.isColor&&(i.specular=this.specular.getHex()),void 0!==this.specularIntensity&&(i.specularIntensity=this.specularIntensity),this.specularColor&&this.specularColor.isColor&&(i.specularColor=this.specularColor.getHex()),void 0!==this.shininess&&(i.shininess=this.shininess),void 0!==this.clearcoat&&(i.clearcoat=this.clearcoat),void 0!==this.clearcoatRoughness&&(i.clearcoatRoughness=this.clearcoatRoughness),this.clearcoatMap&&this.clearcoatMap.isTexture&&(i.clearcoatMap=this.clearcoatMap.toJSON(t).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(i.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(t).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(i.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(t).uuid,i.clearcoatNormalScale=this.clearcoatNormalScale.toArray()),void 0!==this.iridescence&&(i.iridescence=this.iridescence),void 0!==this.iridescenceIOR&&(i.iridescenceIOR=this.iridescenceIOR),void 0!==this.iridescenceThicknessRange&&(i.iridescenceThicknessRange=this.iridescenceThicknessRange),this.iridescenceMap&&this.iridescenceMap.isTexture&&(i.iridescenceMap=this.iridescenceMap.toJSON(t).uuid),this.iridescenceThicknessMap&&this.iridescenceThicknessMap.isTexture&&(i.iridescenceThicknessMap=this.iridescenceThicknessMap.toJSON(t).uuid),this.map&&this.map.isTexture&&(i.map=this.map.toJSON(t).uuid),this.matcap&&this.matcap.isTexture&&(i.matcap=this.matcap.toJSON(t).uuid),this.alphaMap&&this.alphaMap.isTexture&&(i.alphaMap=this.alphaMap.toJSON(t).uuid),this.lightMap&&this.lightMap.isTexture&&(i.lightMap=this.lightMap.toJSON(t).uuid,i.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(i.aoMap=this.aoMap.toJSON(t).uuid,i.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(i.bumpMap=this.bumpMap.toJSON(t).uuid,i.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(i.normalMap=this.normalMap.toJSON(t).uuid,i.normalMapType=this.normalMapType,i.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(i.displacementMap=this.displacementMap.toJSON(t).uuid,i.displacementScale=this.displacementScale,i.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(i.roughnessMap=this.roughnessMap.toJSON(t).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(i.metalnessMap=this.metalnessMap.toJSON(t).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(i.emissiveMap=this.emissiveMap.toJSON(t).uuid),this.specularMap&&this.specularMap.isTexture&&(i.specularMap=this.specularMap.toJSON(t).uuid),this.specularIntensityMap&&this.specularIntensityMap.isTexture&&(i.specularIntensityMap=this.specularIntensityMap.toJSON(t).uuid),this.specularColorMap&&this.specularColorMap.isTexture&&(i.specularColorMap=this.specularColorMap.toJSON(t).uuid),this.envMap&&this.envMap.isTexture&&(i.envMap=this.envMap.toJSON(t).uuid,void 0!==this.combine&&(i.combine=this.combine)),void 0!==this.envMapIntensity&&(i.envMapIntensity=this.envMapIntensity),void 0!==this.reflectivity&&(i.reflectivity=this.reflectivity),void 0!==this.refractionRatio&&(i.refractionRatio=this.refractionRatio),this.gradientMap&&this.gradientMap.isTexture&&(i.gradientMap=this.gradientMap.toJSON(t).uuid),void 0!==this.transmission&&(i.transmission=this.transmission),this.transmissionMap&&this.transmissionMap.isTexture&&(i.transmissionMap=this.transmissionMap.toJSON(t).uuid),void 0!==this.thickness&&(i.thickness=this.thickness),this.thicknessMap&&this.thicknessMap.isTexture&&(i.thicknessMap=this.thicknessMap.toJSON(t).uuid),void 0!==this.attenuationDistance&&this.attenuationDistance!==1/0&&(i.attenuationDistance=this.attenuationDistance),void 0!==this.attenuationColor&&(i.attenuationColor=this.attenuationColor.getHex()),void 0!==this.size&&(i.size=this.size),null!==this.shadowSide&&(i.shadowSide=this.shadowSide),void 0!==this.sizeAttenuation&&(i.sizeAttenuation=this.sizeAttenuation),1!==this.blending&&(i.blending=this.blending),0!==this.side&&(i.side=this.side),this.vertexColors&&(i.vertexColors=!0),this.opacity<1&&(i.opacity=this.opacity),!0===this.transparent&&(i.transparent=this.transparent),i.depthFunc=this.depthFunc,i.depthTest=this.depthTest,i.depthWrite=this.depthWrite,i.colorWrite=this.colorWrite,i.stencilWrite=this.stencilWrite,i.stencilWriteMask=this.stencilWriteMask,i.stencilFunc=this.stencilFunc,i.stencilRef=this.stencilRef,i.stencilFuncMask=this.stencilFuncMask,i.stencilFail=this.stencilFail,i.stencilZFail=this.stencilZFail,i.stencilZPass=this.stencilZPass,void 0!==this.rotation&&0!==this.rotation&&(i.rotation=this.rotation),!0===this.polygonOffset&&(i.polygonOffset=!0),0!==this.polygonOffsetFactor&&(i.polygonOffsetFactor=this.polygonOffsetFactor),0!==this.polygonOffsetUnits&&(i.polygonOffsetUnits=this.polygonOffsetUnits),void 0!==this.linewidth&&1!==this.linewidth&&(i.linewidth=this.linewidth),void 0!==this.dashSize&&(i.dashSize=this.dashSize),void 0!==this.gapSize&&(i.gapSize=this.gapSize),void 0!==this.scale&&(i.scale=this.scale),!0===this.dithering&&(i.dithering=!0),this.alphaTest>0&&(i.alphaTest=this.alphaTest),!0===this.alphaToCoverage&&(i.alphaToCoverage=this.alphaToCoverage),!0===this.premultipliedAlpha&&(i.premultipliedAlpha=this.premultipliedAlpha),!0===this.wireframe&&(i.wireframe=this.wireframe),this.wireframeLinewidth>1&&(i.wireframeLinewidth=this.wireframeLinewidth),"round"!==this.wireframeLinecap&&(i.wireframeLinecap=this.wireframeLinecap),"round"!==this.wireframeLinejoin&&(i.wireframeLinejoin=this.wireframeLinejoin),!0===this.flatShading&&(i.flatShading=this.flatShading),!1===this.visible&&(i.visible=!1),!1===this.toneMapped&&(i.toneMapped=!1),!1===this.fog&&(i.fog=!1),"{}"!==JSON.stringify(this.userData)&&(i.userData=this.userData),e){const e=n(t.textures),r=n(t.images);e.length>0&&(i.textures=e),r.length>0&&(i.images=r)}return i}clone(){return(new this.constructor).copy(this)}copy(t){this.name=t.name,this.blending=t.blending,this.side=t.side,this.vertexColors=t.vertexColors,this.opacity=t.opacity,this.transparent=t.transparent,this.blendSrc=t.blendSrc,this.blendDst=t.blendDst,this.blendEquation=t.blendEquation,this.blendSrcAlpha=t.blendSrcAlpha,this.blendDstAlpha=t.blendDstAlpha,this.blendEquationAlpha=t.blendEquationAlpha,this.depthFunc=t.depthFunc,this.depthTest=t.depthTest,this.depthWrite=t.depthWrite,this.stencilWriteMask=t.stencilWriteMask,this.stencilFunc=t.stencilFunc,this.stencilRef=t.stencilRef,this.stencilFuncMask=t.stencilFuncMask,this.stencilFail=t.stencilFail,this.stencilZFail=t.stencilZFail,this.stencilZPass=t.stencilZPass,this.stencilWrite=t.stencilWrite;const e=t.clippingPlanes;let i=null;if(null!==e){const t=e.length;i=new Array(t);for(let n=0;n!==t;++n)i[n]=e[n].clone()}return this.clippingPlanes=i,this.clipIntersection=t.clipIntersection,this.clipShadows=t.clipShadows,this.shadowSide=t.shadowSide,this.colorWrite=t.colorWrite,this.precision=t.precision,this.polygonOffset=t.polygonOffset,this.polygonOffsetFactor=t.polygonOffsetFactor,this.polygonOffsetUnits=t.polygonOffsetUnits,this.dithering=t.dithering,this.alphaTest=t.alphaTest,this.alphaToCoverage=t.alphaToCoverage,this.premultipliedAlpha=t.premultipliedAlpha,this.visible=t.visible,this.toneMapped=t.toneMapped,this.userData=JSON.parse(JSON.stringify(t.userData)),this}dispose(){this.dispatchEvent({type:"dispose"})}set needsUpdate(t){!0===t&&this.version++}}class _i extends xi{constructor(t){super(),this.isMeshBasicMaterial=!0,this.type="MeshBasicMaterial",this.color=new qt(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.combine=0,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.specularMap=t.specularMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.combine=t.combine,this.reflectivity=t.reflectivity,this.refractionRatio=t.refractionRatio,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.fog=t.fog,this}}const yi=new re,Mi=new Lt;class bi{constructor(t,e,i){if(Array.isArray(t))throw new TypeError("THREE.BufferAttribute: array should be a Typed Array.");this.isBufferAttribute=!0,this.name="",this.array=t,this.itemSize=e,this.count=void 0!==t?t.length/e:0,this.normalized=!0===i,this.usage=ut,this.updateRange={offset:0,count:-1},this.version=0}onUploadCallback(){}set needsUpdate(t){!0===t&&this.version++}setUsage(t){return this.usage=t,this}copy(t){return this.name=t.name,this.array=new t.array.constructor(t.array),this.itemSize=t.itemSize,this.count=t.count,this.normalized=t.normalized,this.usage=t.usage,this}copyAt(t,e,i){t*=this.itemSize,i*=e.itemSize;for(let n=0,r=this.itemSize;n0&&(t.userData=this.userData),void 0!==this.parameters){const e=this.parameters;for(const i in e)void 0!==e[i]&&(t[i]=e[i]);return t}t.data={attributes:{}};const e=this.index;null!==e&&(t.data.index={type:e.array.constructor.name,array:Array.prototype.slice.call(e.array)});const i=this.attributes;for(const e in i){const n=i[e];t.data.attributes[e]=n.toJSON(t.data)}const n={};let r=!1;for(const e in this.morphAttributes){const i=this.morphAttributes[e],s=[];for(let e=0,n=i.length;e0&&(n[e]=s,r=!0)}r&&(t.data.morphAttributes=n,t.data.morphTargetsRelative=this.morphTargetsRelative);const s=this.groups;s.length>0&&(t.data.groups=JSON.parse(JSON.stringify(s)));const a=this.boundingSphere;return null!==a&&(t.data.boundingSphere={center:a.center.toArray(),radius:a.radius}),t}clone(){return(new this.constructor).copy(this)}copy(t){this.index=null,this.attributes={},this.morphAttributes={},this.groups=[],this.boundingBox=null,this.boundingSphere=null;const e={};this.name=t.name;const i=t.index;null!==i&&this.setIndex(i.clone(e));const n=t.attributes;for(const t in n){const i=n[t];this.setAttribute(t,i.clone(e))}const r=t.morphAttributes;for(const t in r){const i=[],n=r[t];for(let t=0,r=n.length;t0){const i=t[e[0]];if(void 0!==i){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let t=0,e=i.length;ti.far?null:{distance:c,point:Ji.clone(),object:t}}(t,e,i,n,Ui,Bi,Fi,Zi);if(p){o&&(qi.fromBufferAttribute(o,c),Xi.fromBufferAttribute(o,h),Yi.fromBufferAttribute(o,u),p.uv=gi.getUV(Zi,Ui,Bi,Fi,qi,Xi,Yi,new Lt)),l&&(qi.fromBufferAttribute(l,c),Xi.fromBufferAttribute(l,h),Yi.fromBufferAttribute(l,u),p.uv2=gi.getUV(Zi,Ui,Bi,Fi,qi,Xi,Yi,new Lt));const t={a:c,b:h,c:u,normal:new re,materialIndex:0};gi.getNormal(Ui,Bi,Fi,t.normal),p.face=t}return p}class Qi extends Di{constructor(t=1,e=1,i=1,n=1,r=1,s=1){super(),this.type="BoxGeometry",this.parameters={width:t,height:e,depth:i,widthSegments:n,heightSegments:r,depthSegments:s};const a=this;n=Math.floor(n),r=Math.floor(r),s=Math.floor(s);const o=[],l=[],c=[],h=[];let u=0,d=0;function p(t,e,i,n,r,s,p,m,f,g,v){const x=s/f,_=p/g,y=s/2,M=p/2,b=m/2,S=f+1,w=g+1;let T=0,A=0;const E=new re;for(let s=0;s0?1:-1,c.push(E.x,E.y,E.z),h.push(o/f),h.push(1-s/g),T+=1}}for(let t=0;t0&&(e.defines=this.defines),e.vertexShader=this.vertexShader,e.fragmentShader=this.fragmentShader;const i={};for(const t in this.extensions)!0===this.extensions[t]&&(i[t]=!0);return Object.keys(i).length>0&&(e.extensions=i),e}}class an extends si{constructor(){super(),this.isCamera=!0,this.type="Camera",this.matrixWorldInverse=new Ne,this.projectionMatrix=new Ne,this.projectionMatrixInverse=new Ne}copy(t,e){return super.copy(t,e),this.matrixWorldInverse.copy(t.matrixWorldInverse),this.projectionMatrix.copy(t.projectionMatrix),this.projectionMatrixInverse.copy(t.projectionMatrixInverse),this}getWorldDirection(t){this.updateWorldMatrix(!0,!1);const e=this.matrixWorld.elements;return t.set(-e[8],-e[9],-e[10]).normalize()}updateMatrixWorld(t){super.updateMatrixWorld(t),this.matrixWorldInverse.copy(this.matrixWorld).invert()}updateWorldMatrix(t,e){super.updateWorldMatrix(t,e),this.matrixWorldInverse.copy(this.matrixWorld).invert()}clone(){return(new this.constructor).copy(this)}}class on extends an{constructor(t=50,e=1,i=.1,n=2e3){super(),this.isPerspectiveCamera=!0,this.type="PerspectiveCamera",this.fov=t,this.zoom=1,this.near=i,this.far=n,this.focus=10,this.aspect=e,this.view=null,this.filmGauge=35,this.filmOffset=0,this.updateProjectionMatrix()}copy(t,e){return super.copy(t,e),this.fov=t.fov,this.zoom=t.zoom,this.near=t.near,this.far=t.far,this.focus=t.focus,this.aspect=t.aspect,this.view=null===t.view?null:Object.assign({},t.view),this.filmGauge=t.filmGauge,this.filmOffset=t.filmOffset,this}setFocalLength(t){const e=.5*this.getFilmHeight()/t;this.fov=2*xt*Math.atan(e),this.updateProjectionMatrix()}getFocalLength(){const t=Math.tan(.5*vt*this.fov);return.5*this.getFilmHeight()/t}getEffectiveFOV(){return 2*xt*Math.atan(Math.tan(.5*vt*this.fov)/this.zoom)}getFilmWidth(){return this.filmGauge*Math.min(this.aspect,1)}getFilmHeight(){return this.filmGauge/Math.max(this.aspect,1)}setViewOffset(t,e,i,n,r,s){this.aspect=t/e,null===this.view&&(this.view={enabled:!0,fullWidth:1,fullHeight:1,offsetX:0,offsetY:0,width:1,height:1}),this.view.enabled=!0,this.view.fullWidth=t,this.view.fullHeight=e,this.view.offsetX=i,this.view.offsetY=n,this.view.width=r,this.view.height=s,this.updateProjectionMatrix()}clearViewOffset(){null!==this.view&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){const t=this.near;let e=t*Math.tan(.5*vt*this.fov)/this.zoom,i=2*e,n=this.aspect*i,r=-.5*n;const s=this.view;if(null!==this.view&&this.view.enabled){const t=s.fullWidth,a=s.fullHeight;r+=s.offsetX*n/t,e-=s.offsetY*i/a,n*=s.width/t,i*=s.height/a}const a=this.filmOffset;0!==a&&(r+=t*a/this.getFilmWidth()),this.projectionMatrix.makePerspective(r,r+n,e,e-i,t,this.far),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(t){const e=super.toJSON(t);return e.object.fov=this.fov,e.object.zoom=this.zoom,e.object.near=this.near,e.object.far=this.far,e.object.focus=this.focus,e.object.aspect=this.aspect,null!==this.view&&(e.object.view=Object.assign({},this.view)),e.object.filmGauge=this.filmGauge,e.object.filmOffset=this.filmOffset,e}}const ln=-90;class cn extends si{constructor(t,e,i){super(),this.type="CubeCamera",this.renderTarget=i;const n=new on(ln,1,t,e);n.layers=this.layers,n.up.set(0,1,0),n.lookAt(1,0,0),this.add(n);const r=new on(ln,1,t,e);r.layers=this.layers,r.up.set(0,1,0),r.lookAt(-1,0,0),this.add(r);const s=new on(ln,1,t,e);s.layers=this.layers,s.up.set(0,0,-1),s.lookAt(0,1,0),this.add(s);const a=new on(ln,1,t,e);a.layers=this.layers,a.up.set(0,0,1),a.lookAt(0,-1,0),this.add(a);const o=new on(ln,1,t,e);o.layers=this.layers,o.up.set(0,1,0),o.lookAt(0,0,1),this.add(o);const l=new on(ln,1,t,e);l.layers=this.layers,l.up.set(0,1,0),l.lookAt(0,0,-1),this.add(l)}update(t,e){null===this.parent&&this.updateMatrixWorld();const i=this.renderTarget,[n,r,s,a,o,l]=this.children,c=t.getRenderTarget(),h=t.toneMapping,u=t.xr.enabled;t.toneMapping=0,t.xr.enabled=!1;const d=i.texture.generateMipmaps;i.texture.generateMipmaps=!1,t.setRenderTarget(i,0),t.render(e,n),t.setRenderTarget(i,1),t.render(e,r),t.setRenderTarget(i,2),t.render(e,s),t.setRenderTarget(i,3),t.render(e,a),t.setRenderTarget(i,4),t.render(e,o),i.texture.generateMipmaps=d,t.setRenderTarget(i,5),t.render(e,l),t.setRenderTarget(c),t.toneMapping=h,t.xr.enabled=u,i.texture.needsPMREMUpdate=!0}}class hn extends $t{constructor(t,e,i,n,s,a,o,l,c,h){super(t=void 0!==t?t:[],e=void 0!==e?e:r,i,n,s,a,o,l,c,h),this.isCubeTexture=!0,this.flipY=!1}get images(){return this.image}set images(t){this.image=t}}class un extends te{constructor(t=1,e={}){super(t,t,e),this.isWebGLCubeRenderTarget=!0;const i={width:t,height:t,depth:1},n=[i,i,i,i,i,i];this.texture=new hn(n,e.mapping,e.wrapS,e.wrapT,e.magFilter,e.minFilter,e.format,e.type,e.anisotropy,e.encoding),this.texture.isRenderTargetTexture=!0,this.texture.generateMipmaps=void 0!==e.generateMipmaps&&e.generateMipmaps,this.texture.minFilter=void 0!==e.minFilter?e.minFilter:f}fromEquirectangularTexture(t,e){this.texture.type=e.type,this.texture.encoding=e.encoding,this.texture.generateMipmaps=e.generateMipmaps,this.texture.minFilter=e.minFilter,this.texture.magFilter=e.magFilter;const i={uniforms:{tEquirect:{value:null}},vertexShader:"\n\n\t\t\t\tvarying vec3 vWorldDirection;\n\n\t\t\t\tvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\n\t\t\t\t\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n\n\t\t\t\t}\n\n\t\t\t\tvoid main() {\n\n\t\t\t\t\tvWorldDirection = transformDirection( position, modelMatrix );\n\n\t\t\t\t\t#include \n\t\t\t\t\t#include \n\n\t\t\t\t}\n\t\t\t",fragmentShader:"\n\n\t\t\t\tuniform sampler2D tEquirect;\n\n\t\t\t\tvarying vec3 vWorldDirection;\n\n\t\t\t\t#include \n\n\t\t\t\tvoid main() {\n\n\t\t\t\t\tvec3 direction = normalize( vWorldDirection );\n\n\t\t\t\t\tvec2 sampleUV = equirectUv( direction );\n\n\t\t\t\t\tgl_FragColor = texture2D( tEquirect, sampleUV );\n\n\t\t\t\t}\n\t\t\t"},n=new Qi(5,5,5),r=new sn({name:"CubemapFromEquirect",uniforms:tn(i.uniforms),vertexShader:i.vertexShader,fragmentShader:i.fragmentShader,side:1,blending:0});r.uniforms.tEquirect.value=e;const s=new Ki(n,r),a=e.minFilter;e.minFilter===v&&(e.minFilter=f);return new cn(1,10,this).update(t,s),e.minFilter=a,s.geometry.dispose(),s.material.dispose(),this}clear(t,e,i,n){const r=t.getRenderTarget();for(let r=0;r<6;r++)t.setRenderTarget(this,r),t.clear(e,i,n);t.setRenderTarget(r)}}const dn=new re,pn=new re,mn=new Rt;class fn{constructor(t=new re(1,0,0),e=0){this.isPlane=!0,this.normal=t,this.constant=e}set(t,e){return this.normal.copy(t),this.constant=e,this}setComponents(t,e,i,n){return this.normal.set(t,e,i),this.constant=n,this}setFromNormalAndCoplanarPoint(t,e){return this.normal.copy(t),this.constant=-e.dot(this.normal),this}setFromCoplanarPoints(t,e,i){const n=dn.subVectors(i,e).cross(pn.subVectors(t,e)).normalize();return this.setFromNormalAndCoplanarPoint(n,t),this}copy(t){return this.normal.copy(t.normal),this.constant=t.constant,this}normalize(){const t=1/this.normal.length();return this.normal.multiplyScalar(t),this.constant*=t,this}negate(){return this.constant*=-1,this.normal.negate(),this}distanceToPoint(t){return this.normal.dot(t)+this.constant}distanceToSphere(t){return this.distanceToPoint(t.center)-t.radius}projectPoint(t,e){return e.copy(this.normal).multiplyScalar(-this.distanceToPoint(t)).add(t)}intersectLine(t,e){const i=t.delta(dn),n=this.normal.dot(i);if(0===n)return 0===this.distanceToPoint(t.start)?e.copy(t.start):null;const r=-(t.start.dot(this.normal)+this.constant)/n;return r<0||r>1?null:e.copy(i).multiplyScalar(r).add(t.start)}intersectsLine(t){const e=this.distanceToPoint(t.start),i=this.distanceToPoint(t.end);return e<0&&i>0||i<0&&e>0}intersectsBox(t){return t.intersectsPlane(this)}intersectsSphere(t){return t.intersectsPlane(this)}coplanarPoint(t){return t.copy(this.normal).multiplyScalar(-this.constant)}applyMatrix4(t,e){const i=e||mn.getNormalMatrix(t),n=this.coplanarPoint(dn).applyMatrix4(t),r=this.normal.applyMatrix3(i).normalize();return this.constant=-n.dot(r),this}translate(t){return this.constant-=t.dot(this.normal),this}equals(t){return t.normal.equals(this.normal)&&t.constant===this.constant}clone(){return(new this.constructor).copy(this)}}const gn=new Te,vn=new re;class xn{constructor(t=new fn,e=new fn,i=new fn,n=new fn,r=new fn,s=new fn){this.planes=[t,e,i,n,r,s]}set(t,e,i,n,r,s){const a=this.planes;return a[0].copy(t),a[1].copy(e),a[2].copy(i),a[3].copy(n),a[4].copy(r),a[5].copy(s),this}copy(t){const e=this.planes;for(let i=0;i<6;i++)e[i].copy(t.planes[i]);return this}setFromProjectionMatrix(t){const e=this.planes,i=t.elements,n=i[0],r=i[1],s=i[2],a=i[3],o=i[4],l=i[5],c=i[6],h=i[7],u=i[8],d=i[9],p=i[10],m=i[11],f=i[12],g=i[13],v=i[14],x=i[15];return e[0].setComponents(a-n,h-o,m-u,x-f).normalize(),e[1].setComponents(a+n,h+o,m+u,x+f).normalize(),e[2].setComponents(a+r,h+l,m+d,x+g).normalize(),e[3].setComponents(a-r,h-l,m-d,x-g).normalize(),e[4].setComponents(a-s,h-c,m-p,x-v).normalize(),e[5].setComponents(a+s,h+c,m+p,x+v).normalize(),this}intersectsObject(t){const e=t.geometry;return null===e.boundingSphere&&e.computeBoundingSphere(),gn.copy(e.boundingSphere).applyMatrix4(t.matrixWorld),this.intersectsSphere(gn)}intersectsSprite(t){return gn.center.set(0,0,0),gn.radius=.7071067811865476,gn.applyMatrix4(t.matrixWorld),this.intersectsSphere(gn)}intersectsSphere(t){const e=this.planes,i=t.center,n=-t.radius;for(let t=0;t<6;t++){if(e[t].distanceToPoint(i)0?t.max.x:t.min.x,vn.y=n.normal.y>0?t.max.y:t.min.y,vn.z=n.normal.z>0?t.max.z:t.min.z,n.distanceToPoint(vn)<0)return!1}return!0}containsPoint(t){const e=this.planes;for(let i=0;i<6;i++)if(e[i].distanceToPoint(t)<0)return!1;return!0}clone(){return(new this.constructor).copy(this)}}function _n(){let t=null,e=!1,i=null,n=null;function r(e,s){i(e,s),n=t.requestAnimationFrame(r)}return{start:function(){!0!==e&&null!==i&&(n=t.requestAnimationFrame(r),e=!0)},stop:function(){t.cancelAnimationFrame(n),e=!1},setAnimationLoop:function(t){i=t},setContext:function(e){t=e}}}function yn(t,e){const i=e.isWebGL2,n=new WeakMap;return{get:function(t){return t.isInterleavedBufferAttribute&&(t=t.data),n.get(t)},remove:function(e){e.isInterleavedBufferAttribute&&(e=e.data);const i=n.get(e);i&&(t.deleteBuffer(i.buffer),n.delete(e))},update:function(e,r){if(e.isGLBufferAttribute){const t=n.get(e);return void((!t||t.version 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v;\n\treturn cross( v1, v2 ) * theta_sintheta;\n}\nvec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) {\n\tvec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ];\n\tvec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ];\n\tvec3 lightNormal = cross( v1, v2 );\n\tif( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 );\n\tvec3 T1, T2;\n\tT1 = normalize( V - N * dot( V, N ) );\n\tT2 = - cross( N, T1 );\n\tmat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) );\n\tvec3 coords[ 4 ];\n\tcoords[ 0 ] = mat * ( rectCoords[ 0 ] - P );\n\tcoords[ 1 ] = mat * ( rectCoords[ 1 ] - P );\n\tcoords[ 2 ] = mat * ( rectCoords[ 2 ] - P );\n\tcoords[ 3 ] = mat * ( rectCoords[ 3 ] - P );\n\tcoords[ 0 ] = normalize( coords[ 0 ] );\n\tcoords[ 1 ] = normalize( coords[ 1 ] );\n\tcoords[ 2 ] = normalize( coords[ 2 ] );\n\tcoords[ 3 ] = normalize( coords[ 3 ] );\n\tvec3 vectorFormFactor = vec3( 0.0 );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] );\n\tfloat result = LTC_ClippedSphereFormFactor( vectorFormFactor );\n\treturn vec3( result );\n}\nfloat G_BlinnPhong_Implicit( ) {\n\treturn 0.25;\n}\nfloat D_BlinnPhong( const in float shininess, const in float dotNH ) {\n\treturn RECIPROCAL_PI * ( shininess * 0.5 + 1.0 ) * pow( dotNH, shininess );\n}\nvec3 BRDF_BlinnPhong( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in vec3 specularColor, const in float shininess ) {\n\tvec3 halfDir = normalize( lightDir + viewDir );\n\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\tfloat dotVH = saturate( dot( viewDir, halfDir ) );\n\tvec3 F = F_Schlick( specularColor, 1.0, dotVH );\n\tfloat G = G_BlinnPhong_Implicit( );\n\tfloat D = D_BlinnPhong( shininess, dotNH );\n\treturn F * ( G * D );\n}\n#if defined( USE_SHEEN )\nfloat D_Charlie( float roughness, float dotNH ) {\n\tfloat alpha = pow2( roughness );\n\tfloat invAlpha = 1.0 / alpha;\n\tfloat cos2h = dotNH * dotNH;\n\tfloat sin2h = max( 1.0 - cos2h, 0.0078125 );\n\treturn ( 2.0 + invAlpha ) * pow( sin2h, invAlpha * 0.5 ) / ( 2.0 * PI );\n}\nfloat V_Neubelt( float dotNV, float dotNL ) {\n\treturn saturate( 1.0 / ( 4.0 * ( dotNL + dotNV - dotNL * dotNV ) ) );\n}\nvec3 BRDF_Sheen( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, vec3 sheenColor, const in float sheenRoughness ) {\n\tvec3 halfDir = normalize( lightDir + viewDir );\n\tfloat dotNL = saturate( dot( normal, lightDir ) );\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\tfloat D = D_Charlie( sheenRoughness, dotNH );\n\tfloat V = V_Neubelt( dotNV, dotNL );\n\treturn sheenColor * ( D * V );\n}\n#endif",iridescence_fragment:"#ifdef USE_IRIDESCENCE\n\tconst mat3 XYZ_TO_REC709 = mat3(\n\t\t 3.2404542, -0.9692660,\t0.0556434,\n\t\t-1.5371385,\t1.8760108, -0.2040259,\n\t\t-0.4985314,\t0.0415560,\t1.0572252\n\t);\n\tvec3 Fresnel0ToIor( vec3 fresnel0 ) {\n\t\tvec3 sqrtF0 = sqrt( fresnel0 );\n\t\treturn ( vec3( 1.0 ) + sqrtF0 ) / ( vec3( 1.0 ) - sqrtF0 );\n\t}\n\tvec3 IorToFresnel0( vec3 transmittedIor, float incidentIor ) {\n\t\treturn pow2( ( transmittedIor - vec3( incidentIor ) ) / ( transmittedIor + vec3( incidentIor ) ) );\n\t}\n\tfloat IorToFresnel0( float transmittedIor, float incidentIor ) {\n\t\treturn pow2( ( transmittedIor - incidentIor ) / ( transmittedIor + incidentIor ));\n\t}\n\tvec3 evalSensitivity( float OPD, vec3 shift ) {\n\t\tfloat phase = 2.0 * PI * OPD * 1.0e-9;\n\t\tvec3 val = vec3( 5.4856e-13, 4.4201e-13, 5.2481e-13 );\n\t\tvec3 pos = vec3( 1.6810e+06, 1.7953e+06, 2.2084e+06 );\n\t\tvec3 var = vec3( 4.3278e+09, 9.3046e+09, 6.6121e+09 );\n\t\tvec3 xyz = val * sqrt( 2.0 * PI * var ) * cos( pos * phase + shift ) * exp( - pow2( phase ) * var );\n\t\txyz.x += 9.7470e-14 * sqrt( 2.0 * PI * 4.5282e+09 ) * cos( 2.2399e+06 * phase + shift[ 0 ] ) * exp( - 4.5282e+09 * pow2( phase ) );\n\t\txyz /= 1.0685e-7;\n\t\tvec3 rgb = XYZ_TO_REC709 * xyz;\n\t\treturn rgb;\n\t}\n\tvec3 evalIridescence( float outsideIOR, float eta2, float cosTheta1, float thinFilmThickness, vec3 baseF0 ) {\n\t\tvec3 I;\n\t\tfloat iridescenceIOR = mix( outsideIOR, eta2, smoothstep( 0.0, 0.03, thinFilmThickness ) );\n\t\tfloat sinTheta2Sq = pow2( outsideIOR / iridescenceIOR ) * ( 1.0 - pow2( cosTheta1 ) );\n\t\tfloat cosTheta2Sq = 1.0 - sinTheta2Sq;\n\t\tif ( cosTheta2Sq < 0.0 ) {\n\t\t\t return vec3( 1.0 );\n\t\t}\n\t\tfloat cosTheta2 = sqrt( cosTheta2Sq );\n\t\tfloat R0 = IorToFresnel0( iridescenceIOR, outsideIOR );\n\t\tfloat R12 = F_Schlick( R0, 1.0, cosTheta1 );\n\t\tfloat R21 = R12;\n\t\tfloat T121 = 1.0 - R12;\n\t\tfloat phi12 = 0.0;\n\t\tif ( iridescenceIOR < outsideIOR ) phi12 = PI;\n\t\tfloat phi21 = PI - phi12;\n\t\tvec3 baseIOR = Fresnel0ToIor( clamp( baseF0, 0.0, 0.9999 ) );\t\tvec3 R1 = IorToFresnel0( baseIOR, iridescenceIOR );\n\t\tvec3 R23 = F_Schlick( R1, 1.0, cosTheta2 );\n\t\tvec3 phi23 = vec3( 0.0 );\n\t\tif ( baseIOR[ 0 ] < iridescenceIOR ) phi23[ 0 ] = PI;\n\t\tif ( baseIOR[ 1 ] < iridescenceIOR ) phi23[ 1 ] = PI;\n\t\tif ( baseIOR[ 2 ] < iridescenceIOR ) phi23[ 2 ] = PI;\n\t\tfloat OPD = 2.0 * iridescenceIOR * thinFilmThickness * cosTheta2;\n\t\tvec3 phi = vec3( phi21 ) + phi23;\n\t\tvec3 R123 = clamp( R12 * R23, 1e-5, 0.9999 );\n\t\tvec3 r123 = sqrt( R123 );\n\t\tvec3 Rs = pow2( T121 ) * R23 / ( vec3( 1.0 ) - R123 );\n\t\tvec3 C0 = R12 + Rs;\n\t\tI = C0;\n\t\tvec3 Cm = Rs - T121;\n\t\tfor ( int m = 1; m <= 2; ++ m ) {\n\t\t\tCm *= r123;\n\t\t\tvec3 Sm = 2.0 * evalSensitivity( float( m ) * OPD, float( m ) * phi );\n\t\t\tI += Cm * Sm;\n\t\t}\n\t\treturn max( I, vec3( 0.0 ) );\n\t}\n#endif",bumpmap_pars_fragment:"#ifdef USE_BUMPMAP\n\tuniform sampler2D bumpMap;\n\tuniform float bumpScale;\n\tvec2 dHdxy_fwd() {\n\t\tvec2 dSTdx = dFdx( vUv );\n\t\tvec2 dSTdy = dFdy( vUv );\n\t\tfloat Hll = bumpScale * texture2D( bumpMap, vUv ).x;\n\t\tfloat dBx = bumpScale * texture2D( bumpMap, vUv + dSTdx ).x - Hll;\n\t\tfloat dBy = bumpScale * texture2D( bumpMap, vUv + dSTdy ).x - Hll;\n\t\treturn vec2( dBx, dBy );\n\t}\n\tvec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy, float faceDirection ) {\n\t\tvec3 vSigmaX = dFdx( surf_pos.xyz );\n\t\tvec3 vSigmaY = dFdy( surf_pos.xyz );\n\t\tvec3 vN = surf_norm;\n\t\tvec3 R1 = cross( vSigmaY, vN );\n\t\tvec3 R2 = cross( vN, vSigmaX );\n\t\tfloat fDet = dot( vSigmaX, R1 ) * faceDirection;\n\t\tvec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );\n\t\treturn normalize( abs( fDet ) * surf_norm - vGrad );\n\t}\n#endif",clipping_planes_fragment:"#if NUM_CLIPPING_PLANES > 0\n\tvec4 plane;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\n\t\tplane = clippingPlanes[ i ];\n\t\tif ( dot( vClipPosition, plane.xyz ) > plane.w ) discard;\n\t}\n\t#pragma unroll_loop_end\n\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\n\t\tbool clipped = true;\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\n\t\t\tplane = clippingPlanes[ i ];\n\t\t\tclipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t\tif ( clipped ) discard;\n\t#endif\n#endif",clipping_planes_pars_fragment:"#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n\tuniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ];\n#endif",clipping_planes_pars_vertex:"#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n#endif",clipping_planes_vertex:"#if NUM_CLIPPING_PLANES > 0\n\tvClipPosition = - mvPosition.xyz;\n#endif",color_fragment:"#if defined( USE_COLOR_ALPHA )\n\tdiffuseColor *= vColor;\n#elif defined( USE_COLOR )\n\tdiffuseColor.rgb *= vColor;\n#endif",color_pars_fragment:"#if defined( USE_COLOR_ALPHA )\n\tvarying vec4 vColor;\n#elif defined( USE_COLOR )\n\tvarying vec3 vColor;\n#endif",color_pars_vertex:"#if defined( USE_COLOR_ALPHA )\n\tvarying vec4 vColor;\n#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR )\n\tvarying vec3 vColor;\n#endif",color_vertex:"#if defined( USE_COLOR_ALPHA )\n\tvColor = vec4( 1.0 );\n#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR )\n\tvColor = vec3( 1.0 );\n#endif\n#ifdef USE_COLOR\n\tvColor *= color;\n#endif\n#ifdef USE_INSTANCING_COLOR\n\tvColor.xyz *= instanceColor.xyz;\n#endif",common:"#define PI 3.141592653589793\n#define PI2 6.283185307179586\n#define PI_HALF 1.5707963267948966\n#define RECIPROCAL_PI 0.3183098861837907\n#define RECIPROCAL_PI2 0.15915494309189535\n#define EPSILON 1e-6\n#ifndef saturate\n#define saturate( a ) clamp( a, 0.0, 1.0 )\n#endif\n#define whiteComplement( a ) ( 1.0 - saturate( a ) )\nfloat pow2( const in float x ) { return x*x; }\nvec3 pow2( const in vec3 x ) { return x*x; }\nfloat pow3( const in float x ) { return x*x*x; }\nfloat pow4( const in float x ) { float x2 = x*x; return x2*x2; }\nfloat max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); }\nfloat average( const in vec3 v ) { return dot( v, vec3( 0.3333333 ) ); }\nhighp float rand( const in vec2 uv ) {\n\tconst highp float a = 12.9898, b = 78.233, c = 43758.5453;\n\thighp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );\n\treturn fract( sin( sn ) * c );\n}\n#ifdef HIGH_PRECISION\n\tfloat precisionSafeLength( vec3 v ) { return length( v ); }\n#else\n\tfloat precisionSafeLength( vec3 v ) {\n\t\tfloat maxComponent = max3( abs( v ) );\n\t\treturn length( v / maxComponent ) * maxComponent;\n\t}\n#endif\nstruct IncidentLight {\n\tvec3 color;\n\tvec3 direction;\n\tbool visible;\n};\nstruct ReflectedLight {\n\tvec3 directDiffuse;\n\tvec3 directSpecular;\n\tvec3 indirectDiffuse;\n\tvec3 indirectSpecular;\n};\nstruct GeometricContext {\n\tvec3 position;\n\tvec3 normal;\n\tvec3 viewDir;\n#ifdef USE_CLEARCOAT\n\tvec3 clearcoatNormal;\n#endif\n};\nvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n}\nvec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );\n}\nmat3 transposeMat3( const in mat3 m ) {\n\tmat3 tmp;\n\ttmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x );\n\ttmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y );\n\ttmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z );\n\treturn tmp;\n}\nfloat luminance( const in vec3 rgb ) {\n\tconst vec3 weights = vec3( 0.2126729, 0.7151522, 0.0721750 );\n\treturn dot( weights, rgb );\n}\nbool isPerspectiveMatrix( mat4 m ) {\n\treturn m[ 2 ][ 3 ] == - 1.0;\n}\nvec2 equirectUv( in vec3 dir ) {\n\tfloat u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5;\n\tfloat v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5;\n\treturn vec2( u, v );\n}",cube_uv_reflection_fragment:"#ifdef ENVMAP_TYPE_CUBE_UV\n\t#define cubeUV_minMipLevel 4.0\n\t#define cubeUV_minTileSize 16.0\n\tfloat getFace( vec3 direction ) {\n\t\tvec3 absDirection = abs( direction );\n\t\tfloat face = - 1.0;\n\t\tif ( absDirection.x > absDirection.z ) {\n\t\t\tif ( absDirection.x > absDirection.y )\n\t\t\t\tface = direction.x > 0.0 ? 0.0 : 3.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t} else {\n\t\t\tif ( absDirection.z > absDirection.y )\n\t\t\t\tface = direction.z > 0.0 ? 2.0 : 5.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t}\n\t\treturn face;\n\t}\n\tvec2 getUV( vec3 direction, float face ) {\n\t\tvec2 uv;\n\t\tif ( face == 0.0 ) {\n\t\t\tuv = vec2( direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 1.0 ) {\n\t\t\tuv = vec2( - direction.x, - direction.z ) / abs( direction.y );\n\t\t} else if ( face == 2.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.y ) / abs( direction.z );\n\t\t} else if ( face == 3.0 ) {\n\t\t\tuv = vec2( - direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 4.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.z ) / abs( direction.y );\n\t\t} else {\n\t\t\tuv = vec2( direction.x, direction.y ) / abs( direction.z );\n\t\t}\n\t\treturn 0.5 * ( uv + 1.0 );\n\t}\n\tvec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) {\n\t\tfloat face = getFace( direction );\n\t\tfloat filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 );\n\t\tmipInt = max( mipInt, cubeUV_minMipLevel );\n\t\tfloat faceSize = exp2( mipInt );\n\t\tvec2 uv = getUV( direction, face ) * ( faceSize - 2.0 ) + 1.0;\n\t\tif ( face > 2.0 ) {\n\t\t\tuv.y += faceSize;\n\t\t\tface -= 3.0;\n\t\t}\n\t\tuv.x += face * faceSize;\n\t\tuv.x += filterInt * 3.0 * cubeUV_minTileSize;\n\t\tuv.y += 4.0 * ( exp2( CUBEUV_MAX_MIP ) - faceSize );\n\t\tuv.x *= CUBEUV_TEXEL_WIDTH;\n\t\tuv.y *= CUBEUV_TEXEL_HEIGHT;\n\t\t#ifdef texture2DGradEXT\n\t\t\treturn texture2DGradEXT( envMap, uv, vec2( 0.0 ), vec2( 0.0 ) ).rgb;\n\t\t#else\n\t\t\treturn texture2D( envMap, uv ).rgb;\n\t\t#endif\n\t}\n\t#define cubeUV_r0 1.0\n\t#define cubeUV_v0 0.339\n\t#define cubeUV_m0 - 2.0\n\t#define cubeUV_r1 0.8\n\t#define cubeUV_v1 0.276\n\t#define cubeUV_m1 - 1.0\n\t#define cubeUV_r4 0.4\n\t#define cubeUV_v4 0.046\n\t#define cubeUV_m4 2.0\n\t#define cubeUV_r5 0.305\n\t#define cubeUV_v5 0.016\n\t#define cubeUV_m5 3.0\n\t#define cubeUV_r6 0.21\n\t#define cubeUV_v6 0.0038\n\t#define cubeUV_m6 4.0\n\tfloat roughnessToMip( float roughness ) {\n\t\tfloat mip = 0.0;\n\t\tif ( roughness >= cubeUV_r1 ) {\n\t\t\tmip = ( cubeUV_r0 - roughness ) * ( cubeUV_m1 - cubeUV_m0 ) / ( cubeUV_r0 - cubeUV_r1 ) + cubeUV_m0;\n\t\t} else if ( roughness >= cubeUV_r4 ) {\n\t\t\tmip = ( cubeUV_r1 - roughness ) * ( cubeUV_m4 - cubeUV_m1 ) / ( cubeUV_r1 - cubeUV_r4 ) + cubeUV_m1;\n\t\t} else if ( roughness >= cubeUV_r5 ) {\n\t\t\tmip = ( cubeUV_r4 - roughness ) * ( cubeUV_m5 - cubeUV_m4 ) / ( cubeUV_r4 - cubeUV_r5 ) + cubeUV_m4;\n\t\t} else if ( roughness >= cubeUV_r6 ) {\n\t\t\tmip = ( cubeUV_r5 - roughness ) * ( cubeUV_m6 - cubeUV_m5 ) / ( cubeUV_r5 - cubeUV_r6 ) + cubeUV_m5;\n\t\t} else {\n\t\t\tmip = - 2.0 * log2( 1.16 * roughness );\t\t}\n\t\treturn mip;\n\t}\n\tvec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) {\n\t\tfloat mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP );\n\t\tfloat mipF = fract( mip );\n\t\tfloat mipInt = floor( mip );\n\t\tvec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt );\n\t\tif ( mipF == 0.0 ) {\n\t\t\treturn vec4( color0, 1.0 );\n\t\t} else {\n\t\t\tvec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 );\n\t\t\treturn vec4( mix( color0, color1, mipF ), 1.0 );\n\t\t}\n\t}\n#endif",defaultnormal_vertex:"vec3 transformedNormal = objectNormal;\n#ifdef USE_INSTANCING\n\tmat3 m = mat3( instanceMatrix );\n\ttransformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );\n\ttransformedNormal = m * transformedNormal;\n#endif\ntransformedNormal = normalMatrix * transformedNormal;\n#ifdef FLIP_SIDED\n\ttransformedNormal = - transformedNormal;\n#endif\n#ifdef USE_TANGENT\n\tvec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;\n\t#ifdef FLIP_SIDED\n\t\ttransformedTangent = - transformedTangent;\n\t#endif\n#endif",displacementmap_pars_vertex:"#ifdef USE_DISPLACEMENTMAP\n\tuniform sampler2D displacementMap;\n\tuniform float displacementScale;\n\tuniform float displacementBias;\n#endif",displacementmap_vertex:"#ifdef USE_DISPLACEMENTMAP\n\ttransformed += normalize( objectNormal ) * ( texture2D( displacementMap, vUv ).x * displacementScale + displacementBias );\n#endif",emissivemap_fragment:"#ifdef USE_EMISSIVEMAP\n\tvec4 emissiveColor = texture2D( emissiveMap, vUv );\n\ttotalEmissiveRadiance *= emissiveColor.rgb;\n#endif",emissivemap_pars_fragment:"#ifdef USE_EMISSIVEMAP\n\tuniform sampler2D emissiveMap;\n#endif",encodings_fragment:"gl_FragColor = linearToOutputTexel( gl_FragColor );",encodings_pars_fragment:"vec4 LinearToLinear( in vec4 value ) {\n\treturn value;\n}\nvec4 LinearTosRGB( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );\n}",envmap_fragment:"#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvec3 cameraToFrag;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToFrag = normalize( vWorldPosition - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvec3 reflectVec = reflect( cameraToFrag, worldNormal );\n\t\t#else\n\t\t\tvec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio );\n\t\t#endif\n\t#else\n\t\tvec3 reflectVec = vReflect;\n\t#endif\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tvec4 envColor = textureCube( envMap, vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) );\n\t#else\n\t\tvec4 envColor = vec4( 0.0 );\n\t#endif\n\t#ifdef ENVMAP_BLENDING_MULTIPLY\n\t\toutgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_MIX )\n\t\toutgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_ADD )\n\t\toutgoingLight += envColor.xyz * specularStrength * reflectivity;\n\t#endif\n#endif",envmap_common_pars_fragment:"#ifdef USE_ENVMAP\n\tuniform float envMapIntensity;\n\tuniform float flipEnvMap;\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tuniform samplerCube envMap;\n\t#else\n\t\tuniform sampler2D envMap;\n\t#endif\n\t\n#endif",envmap_pars_fragment:"#ifdef USE_ENVMAP\n\tuniform float reflectivity;\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\tvarying vec3 vWorldPosition;\n\t\tuniform float refractionRatio;\n\t#else\n\t\tvarying vec3 vReflect;\n\t#endif\n#endif",envmap_pars_vertex:"#ifdef USE_ENVMAP\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\t\n\t\tvarying vec3 vWorldPosition;\n\t#else\n\t\tvarying vec3 vReflect;\n\t\tuniform float refractionRatio;\n\t#endif\n#endif",envmap_physical_pars_fragment:"#if defined( USE_ENVMAP )\n\tvec3 getIBLIrradiance( const in vec3 normal ) {\n\t\t#if defined( ENVMAP_TYPE_CUBE_UV )\n\t\t\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\t\t\tvec4 envMapColor = textureCubeUV( envMap, worldNormal, 1.0 );\n\t\t\treturn PI * envMapColor.rgb * envMapIntensity;\n\t\t#else\n\t\t\treturn vec3( 0.0 );\n\t\t#endif\n\t}\n\tvec3 getIBLRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness ) {\n\t\t#if defined( ENVMAP_TYPE_CUBE_UV )\n\t\t\tvec3 reflectVec = reflect( - viewDir, normal );\n\t\t\treflectVec = normalize( mix( reflectVec, normal, roughness * roughness) );\n\t\t\treflectVec = inverseTransformDirection( reflectVec, viewMatrix );\n\t\t\tvec4 envMapColor = textureCubeUV( envMap, reflectVec, roughness );\n\t\t\treturn envMapColor.rgb * envMapIntensity;\n\t\t#else\n\t\t\treturn vec3( 0.0 );\n\t\t#endif\n\t}\n#endif",envmap_vertex:"#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvWorldPosition = worldPosition.xyz;\n\t#else\n\t\tvec3 cameraToVertex;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToVertex = normalize( worldPosition.xyz - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvReflect = reflect( cameraToVertex, worldNormal );\n\t\t#else\n\t\t\tvReflect = refract( cameraToVertex, worldNormal, refractionRatio );\n\t\t#endif\n\t#endif\n#endif",fog_vertex:"#ifdef USE_FOG\n\tvFogDepth = - mvPosition.z;\n#endif",fog_pars_vertex:"#ifdef USE_FOG\n\tvarying float vFogDepth;\n#endif",fog_fragment:"#ifdef USE_FOG\n\t#ifdef FOG_EXP2\n\t\tfloat fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth );\n\t#else\n\t\tfloat fogFactor = smoothstep( fogNear, fogFar, vFogDepth );\n\t#endif\n\tgl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );\n#endif",fog_pars_fragment:"#ifdef USE_FOG\n\tuniform vec3 fogColor;\n\tvarying float vFogDepth;\n\t#ifdef FOG_EXP2\n\t\tuniform float fogDensity;\n\t#else\n\t\tuniform float fogNear;\n\t\tuniform float fogFar;\n\t#endif\n#endif",gradientmap_pars_fragment:"#ifdef USE_GRADIENTMAP\n\tuniform sampler2D gradientMap;\n#endif\nvec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) {\n\tfloat dotNL = dot( normal, lightDirection );\n\tvec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 );\n\t#ifdef USE_GRADIENTMAP\n\t\treturn vec3( texture2D( gradientMap, coord ).r );\n\t#else\n\t\tvec2 fw = fwidth( coord ) * 0.5;\n\t\treturn mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( 0.7 - fw.x, 0.7 + fw.x, coord.x ) );\n\t#endif\n}",lightmap_fragment:"#ifdef USE_LIGHTMAP\n\tvec4 lightMapTexel = texture2D( lightMap, vUv2 );\n\tvec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity;\n\treflectedLight.indirectDiffuse += lightMapIrradiance;\n#endif",lightmap_pars_fragment:"#ifdef USE_LIGHTMAP\n\tuniform sampler2D lightMap;\n\tuniform float lightMapIntensity;\n#endif",lights_lambert_fragment:"LambertMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.specularStrength = specularStrength;",lights_lambert_pars_fragment:"varying vec3 vViewPosition;\nstruct LambertMaterial {\n\tvec3 diffuseColor;\n\tfloat specularStrength;\n};\nvoid RE_Direct_Lambert( const in IncidentLight directLight, const in GeometricContext geometry, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometry.normal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in GeometricContext geometry, const in LambertMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_Lambert\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Lambert",lights_pars_begin:"uniform bool receiveShadow;\nuniform vec3 ambientLightColor;\nuniform vec3 lightProbe[ 9 ];\nvec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {\n\tfloat x = normal.x, y = normal.y, z = normal.z;\n\tvec3 result = shCoefficients[ 0 ] * 0.886227;\n\tresult += shCoefficients[ 1 ] * 2.0 * 0.511664 * y;\n\tresult += shCoefficients[ 2 ] * 2.0 * 0.511664 * z;\n\tresult += shCoefficients[ 3 ] * 2.0 * 0.511664 * x;\n\tresult += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y;\n\tresult += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z;\n\tresult += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 );\n\tresult += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z;\n\tresult += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y );\n\treturn result;\n}\nvec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) {\n\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\tvec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe );\n\treturn irradiance;\n}\nvec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {\n\tvec3 irradiance = ambientLightColor;\n\treturn irradiance;\n}\nfloat getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) {\n\t#if defined ( PHYSICALLY_CORRECT_LIGHTS )\n\t\tfloat distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 );\n\t\tif ( cutoffDistance > 0.0 ) {\n\t\t\tdistanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) );\n\t\t}\n\t\treturn distanceFalloff;\n\t#else\n\t\tif ( cutoffDistance > 0.0 && decayExponent > 0.0 ) {\n\t\t\treturn pow( saturate( - lightDistance / cutoffDistance + 1.0 ), decayExponent );\n\t\t}\n\t\treturn 1.0;\n\t#endif\n}\nfloat getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) {\n\treturn smoothstep( coneCosine, penumbraCosine, angleCosine );\n}\n#if NUM_DIR_LIGHTS > 0\n\tstruct DirectionalLight {\n\t\tvec3 direction;\n\t\tvec3 color;\n\t};\n\tuniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];\n\tvoid getDirectionalLightInfo( const in DirectionalLight directionalLight, const in GeometricContext geometry, out IncidentLight light ) {\n\t\tlight.color = directionalLight.color;\n\t\tlight.direction = directionalLight.direction;\n\t\tlight.visible = true;\n\t}\n#endif\n#if NUM_POINT_LIGHTS > 0\n\tstruct PointLight {\n\t\tvec3 position;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t};\n\tuniform PointLight pointLights[ NUM_POINT_LIGHTS ];\n\tvoid getPointLightInfo( const in PointLight pointLight, const in GeometricContext geometry, out IncidentLight light ) {\n\t\tvec3 lVector = pointLight.position - geometry.position;\n\t\tlight.direction = normalize( lVector );\n\t\tfloat lightDistance = length( lVector );\n\t\tlight.color = pointLight.color;\n\t\tlight.color *= getDistanceAttenuation( lightDistance, pointLight.distance, pointLight.decay );\n\t\tlight.visible = ( light.color != vec3( 0.0 ) );\n\t}\n#endif\n#if NUM_SPOT_LIGHTS > 0\n\tstruct SpotLight {\n\t\tvec3 position;\n\t\tvec3 direction;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t\tfloat coneCos;\n\t\tfloat penumbraCos;\n\t};\n\tuniform SpotLight spotLights[ NUM_SPOT_LIGHTS ];\n\tvoid getSpotLightInfo( const in SpotLight spotLight, const in GeometricContext geometry, out IncidentLight light ) {\n\t\tvec3 lVector = spotLight.position - geometry.position;\n\t\tlight.direction = normalize( lVector );\n\t\tfloat angleCos = dot( light.direction, spotLight.direction );\n\t\tfloat spotAttenuation = getSpotAttenuation( spotLight.coneCos, spotLight.penumbraCos, angleCos );\n\t\tif ( spotAttenuation > 0.0 ) {\n\t\t\tfloat lightDistance = length( lVector );\n\t\t\tlight.color = spotLight.color * spotAttenuation;\n\t\t\tlight.color *= getDistanceAttenuation( lightDistance, spotLight.distance, spotLight.decay );\n\t\t\tlight.visible = ( light.color != vec3( 0.0 ) );\n\t\t} else {\n\t\t\tlight.color = vec3( 0.0 );\n\t\t\tlight.visible = false;\n\t\t}\n\t}\n#endif\n#if NUM_RECT_AREA_LIGHTS > 0\n\tstruct RectAreaLight {\n\t\tvec3 color;\n\t\tvec3 position;\n\t\tvec3 halfWidth;\n\t\tvec3 halfHeight;\n\t};\n\tuniform sampler2D ltc_1;\tuniform sampler2D ltc_2;\n\tuniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ];\n#endif\n#if NUM_HEMI_LIGHTS > 0\n\tstruct HemisphereLight {\n\t\tvec3 direction;\n\t\tvec3 skyColor;\n\t\tvec3 groundColor;\n\t};\n\tuniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ];\n\tvec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) {\n\t\tfloat dotNL = dot( normal, hemiLight.direction );\n\t\tfloat hemiDiffuseWeight = 0.5 * dotNL + 0.5;\n\t\tvec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight );\n\t\treturn irradiance;\n\t}\n#endif",lights_toon_fragment:"ToonMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;",lights_toon_pars_fragment:"varying vec3 vViewPosition;\nstruct ToonMaterial {\n\tvec3 diffuseColor;\n};\nvoid RE_Direct_Toon( const in IncidentLight directLight, const in GeometricContext geometry, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\n\tvec3 irradiance = getGradientIrradiance( geometry.normal, directLight.direction ) * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in GeometricContext geometry, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_Toon\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Toon",lights_phong_fragment:"BlinnPhongMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.specularColor = specular;\nmaterial.specularShininess = shininess;\nmaterial.specularStrength = specularStrength;",lights_phong_pars_fragment:"varying vec3 vViewPosition;\nstruct BlinnPhongMaterial {\n\tvec3 diffuseColor;\n\tvec3 specularColor;\n\tfloat specularShininess;\n\tfloat specularStrength;\n};\nvoid RE_Direct_BlinnPhong( const in IncidentLight directLight, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometry.normal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n\treflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometry.viewDir, geometry.normal, material.specularColor, material.specularShininess ) * material.specularStrength;\n}\nvoid RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_BlinnPhong\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_BlinnPhong",lights_physical_fragment:"PhysicalMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb * ( 1.0 - metalnessFactor );\nvec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );\nfloat geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );\nmaterial.roughness = max( roughnessFactor, 0.0525 );material.roughness += geometryRoughness;\nmaterial.roughness = min( material.roughness, 1.0 );\n#ifdef IOR\n\tmaterial.ior = ior;\n\t#ifdef SPECULAR\n\t\tfloat specularIntensityFactor = specularIntensity;\n\t\tvec3 specularColorFactor = specularColor;\n\t\t#ifdef USE_SPECULARINTENSITYMAP\n\t\t\tspecularIntensityFactor *= texture2D( specularIntensityMap, vUv ).a;\n\t\t#endif\n\t\t#ifdef USE_SPECULARCOLORMAP\n\t\t\tspecularColorFactor *= texture2D( specularColorMap, vUv ).rgb;\n\t\t#endif\n\t\tmaterial.specularF90 = mix( specularIntensityFactor, 1.0, metalnessFactor );\n\t#else\n\t\tfloat specularIntensityFactor = 1.0;\n\t\tvec3 specularColorFactor = vec3( 1.0 );\n\t\tmaterial.specularF90 = 1.0;\n\t#endif\n\tmaterial.specularColor = mix( min( pow2( ( material.ior - 1.0 ) / ( material.ior + 1.0 ) ) * specularColorFactor, vec3( 1.0 ) ) * specularIntensityFactor, diffuseColor.rgb, metalnessFactor );\n#else\n\tmaterial.specularColor = mix( vec3( 0.04 ), diffuseColor.rgb, metalnessFactor );\n\tmaterial.specularF90 = 1.0;\n#endif\n#ifdef USE_CLEARCOAT\n\tmaterial.clearcoat = clearcoat;\n\tmaterial.clearcoatRoughness = clearcoatRoughness;\n\tmaterial.clearcoatF0 = vec3( 0.04 );\n\tmaterial.clearcoatF90 = 1.0;\n\t#ifdef USE_CLEARCOATMAP\n\t\tmaterial.clearcoat *= texture2D( clearcoatMap, vUv ).x;\n\t#endif\n\t#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\t\tmaterial.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vUv ).y;\n\t#endif\n\tmaterial.clearcoat = saturate( material.clearcoat );\tmaterial.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 );\n\tmaterial.clearcoatRoughness += geometryRoughness;\n\tmaterial.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 );\n#endif\n#ifdef USE_IRIDESCENCE\n\tmaterial.iridescence = iridescence;\n\tmaterial.iridescenceIOR = iridescenceIOR;\n\t#ifdef USE_IRIDESCENCEMAP\n\t\tmaterial.iridescence *= texture2D( iridescenceMap, vUv ).r;\n\t#endif\n\t#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\t\tmaterial.iridescenceThickness = (iridescenceThicknessMaximum - iridescenceThicknessMinimum) * texture2D( iridescenceThicknessMap, vUv ).g + iridescenceThicknessMinimum;\n\t#else\n\t\tmaterial.iridescenceThickness = iridescenceThicknessMaximum;\n\t#endif\n#endif\n#ifdef USE_SHEEN\n\tmaterial.sheenColor = sheenColor;\n\t#ifdef USE_SHEENCOLORMAP\n\t\tmaterial.sheenColor *= texture2D( sheenColorMap, vUv ).rgb;\n\t#endif\n\tmaterial.sheenRoughness = clamp( sheenRoughness, 0.07, 1.0 );\n\t#ifdef USE_SHEENROUGHNESSMAP\n\t\tmaterial.sheenRoughness *= texture2D( sheenRoughnessMap, vUv ).a;\n\t#endif\n#endif",lights_physical_pars_fragment:"struct PhysicalMaterial {\n\tvec3 diffuseColor;\n\tfloat roughness;\n\tvec3 specularColor;\n\tfloat specularF90;\n\t#ifdef USE_CLEARCOAT\n\t\tfloat clearcoat;\n\t\tfloat clearcoatRoughness;\n\t\tvec3 clearcoatF0;\n\t\tfloat clearcoatF90;\n\t#endif\n\t#ifdef USE_IRIDESCENCE\n\t\tfloat iridescence;\n\t\tfloat iridescenceIOR;\n\t\tfloat iridescenceThickness;\n\t\tvec3 iridescenceFresnel;\n\t\tvec3 iridescenceF0;\n\t#endif\n\t#ifdef USE_SHEEN\n\t\tvec3 sheenColor;\n\t\tfloat sheenRoughness;\n\t#endif\n\t#ifdef IOR\n\t\tfloat ior;\n\t#endif\n\t#ifdef USE_TRANSMISSION\n\t\tfloat transmission;\n\t\tfloat transmissionAlpha;\n\t\tfloat thickness;\n\t\tfloat attenuationDistance;\n\t\tvec3 attenuationColor;\n\t#endif\n};\nvec3 clearcoatSpecular = vec3( 0.0 );\nvec3 sheenSpecular = vec3( 0.0 );\nfloat IBLSheenBRDF( const in vec3 normal, const in vec3 viewDir, const in float roughness ) {\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat r2 = roughness * roughness;\n\tfloat a = roughness < 0.25 ? -339.2 * r2 + 161.4 * roughness - 25.9 : -8.48 * r2 + 14.3 * roughness - 9.95;\n\tfloat b = roughness < 0.25 ? 44.0 * r2 - 23.7 * roughness + 3.26 : 1.97 * r2 - 3.27 * roughness + 0.72;\n\tfloat DG = exp( a * dotNV + b ) + ( roughness < 0.25 ? 0.0 : 0.1 * ( roughness - 0.25 ) );\n\treturn saturate( DG * RECIPROCAL_PI );\n}\nvec2 DFGApprox( const in vec3 normal, const in vec3 viewDir, const in float roughness ) {\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tconst vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 );\n\tconst vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 );\n\tvec4 r = roughness * c0 + c1;\n\tfloat a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y;\n\tvec2 fab = vec2( - 1.04, 1.04 ) * a004 + r.zw;\n\treturn fab;\n}\nvec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) {\n\tvec2 fab = DFGApprox( normal, viewDir, roughness );\n\treturn specularColor * fab.x + specularF90 * fab.y;\n}\n#ifdef USE_IRIDESCENCE\nvoid computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {\n#else\nvoid computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {\n#endif\n\tvec2 fab = DFGApprox( normal, viewDir, roughness );\n\t#ifdef USE_IRIDESCENCE\n\t\tvec3 Fr = mix( specularColor, iridescenceF0, iridescence );\n\t#else\n\t\tvec3 Fr = specularColor;\n\t#endif\n\tvec3 FssEss = Fr * fab.x + specularF90 * fab.y;\n\tfloat Ess = fab.x + fab.y;\n\tfloat Ems = 1.0 - Ess;\n\tvec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619;\tvec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg );\n\tsingleScatter += FssEss;\n\tmultiScatter += Fms * Ems;\n}\n#if NUM_RECT_AREA_LIGHTS > 0\n\tvoid RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\t\tvec3 normal = geometry.normal;\n\t\tvec3 viewDir = geometry.viewDir;\n\t\tvec3 position = geometry.position;\n\t\tvec3 lightPos = rectAreaLight.position;\n\t\tvec3 halfWidth = rectAreaLight.halfWidth;\n\t\tvec3 halfHeight = rectAreaLight.halfHeight;\n\t\tvec3 lightColor = rectAreaLight.color;\n\t\tfloat roughness = material.roughness;\n\t\tvec3 rectCoords[ 4 ];\n\t\trectCoords[ 0 ] = lightPos + halfWidth - halfHeight;\t\trectCoords[ 1 ] = lightPos - halfWidth - halfHeight;\n\t\trectCoords[ 2 ] = lightPos - halfWidth + halfHeight;\n\t\trectCoords[ 3 ] = lightPos + halfWidth + halfHeight;\n\t\tvec2 uv = LTC_Uv( normal, viewDir, roughness );\n\t\tvec4 t1 = texture2D( ltc_1, uv );\n\t\tvec4 t2 = texture2D( ltc_2, uv );\n\t\tmat3 mInv = mat3(\n\t\t\tvec3( t1.x, 0, t1.y ),\n\t\t\tvec3(\t\t0, 1,\t\t0 ),\n\t\t\tvec3( t1.z, 0, t1.w )\n\t\t);\n\t\tvec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y );\n\t\treflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords );\n\t\treflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords );\n\t}\n#endif\nvoid RE_Direct_Physical( const in IncidentLight directLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometry.normal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\t#ifdef USE_CLEARCOAT\n\t\tfloat dotNLcc = saturate( dot( geometry.clearcoatNormal, directLight.direction ) );\n\t\tvec3 ccIrradiance = dotNLcc * directLight.color;\n\t\tclearcoatSpecular += ccIrradiance * BRDF_GGX( directLight.direction, geometry.viewDir, geometry.clearcoatNormal, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness );\n\t#endif\n\t#ifdef USE_SHEEN\n\t\tsheenSpecular += irradiance * BRDF_Sheen( directLight.direction, geometry.viewDir, geometry.normal, material.sheenColor, material.sheenRoughness );\n\t#endif\n\t#ifdef USE_IRIDESCENCE\n\t\treflectedLight.directSpecular += irradiance * BRDF_GGX_Iridescence( directLight.direction, geometry.viewDir, geometry.normal, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness );\n\t#else\n\t\treflectedLight.directSpecular += irradiance * BRDF_GGX( directLight.direction, geometry.viewDir, geometry.normal, material.specularColor, material.specularF90, material.roughness );\n\t#endif\n\treflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) {\n\t#ifdef USE_CLEARCOAT\n\t\tclearcoatSpecular += clearcoatRadiance * EnvironmentBRDF( geometry.clearcoatNormal, geometry.viewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness );\n\t#endif\n\t#ifdef USE_SHEEN\n\t\tsheenSpecular += irradiance * material.sheenColor * IBLSheenBRDF( geometry.normal, geometry.viewDir, material.sheenRoughness );\n\t#endif\n\tvec3 singleScattering = vec3( 0.0 );\n\tvec3 multiScattering = vec3( 0.0 );\n\tvec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI;\n\t#ifdef USE_IRIDESCENCE\n\t\tcomputeMultiscatteringIridescence( geometry.normal, geometry.viewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness, singleScattering, multiScattering );\n\t#else\n\t\tcomputeMultiscattering( geometry.normal, geometry.viewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering );\n\t#endif\n\tvec3 totalScattering = singleScattering + multiScattering;\n\tvec3 diffuse = material.diffuseColor * ( 1.0 - max( max( totalScattering.r, totalScattering.g ), totalScattering.b ) );\n\treflectedLight.indirectSpecular += radiance * singleScattering;\n\treflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance;\n\treflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance;\n}\n#define RE_Direct\t\t\t\tRE_Direct_Physical\n#define RE_Direct_RectArea\t\tRE_Direct_RectArea_Physical\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Physical\n#define RE_IndirectSpecular\t\tRE_IndirectSpecular_Physical\nfloat computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) {\n\treturn saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion );\n}",lights_fragment_begin:"\nGeometricContext geometry;\ngeometry.position = - vViewPosition;\ngeometry.normal = normal;\ngeometry.viewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition );\n#ifdef USE_CLEARCOAT\n\tgeometry.clearcoatNormal = clearcoatNormal;\n#endif\n#ifdef USE_IRIDESCENCE\n\tfloat dotNVi = saturate( dot( normal, geometry.viewDir ) );\n\tif ( material.iridescenceThickness == 0.0 ) {\n\t\tmaterial.iridescence = 0.0;\n\t} else {\n\t\tmaterial.iridescence = saturate( material.iridescence );\n\t}\n\tif ( material.iridescence > 0.0 ) {\n\t\tmaterial.iridescenceFresnel = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor );\n\t\tmaterial.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi );\n\t}\n#endif\nIncidentLight directLight;\n#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct )\n\tPointLight pointLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0\n\tPointLightShadow pointLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\n\t\tpointLight = pointLights[ i ];\n\t\tgetPointLightInfo( pointLight, geometry, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS )\n\t\tpointLightShadow = pointLightShadows[ i ];\n\t\tdirectLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometry, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct )\n\tSpotLight spotLight;\n\tvec4 spotColor;\n\tvec3 spotLightCoord;\n\tbool inSpotLightMap;\n\t#if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0\n\tSpotLightShadow spotLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\n\t\tspotLight = spotLights[ i ];\n\t\tgetSpotLightInfo( spotLight, geometry, directLight );\n\t\t#if ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS )\n\t\t#define SPOT_LIGHT_MAP_INDEX UNROLLED_LOOP_INDEX\n\t\t#elif ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\t#define SPOT_LIGHT_MAP_INDEX NUM_SPOT_LIGHT_MAPS\n\t\t#else\n\t\t#define SPOT_LIGHT_MAP_INDEX ( UNROLLED_LOOP_INDEX - NUM_SPOT_LIGHT_SHADOWS + NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS )\n\t\t#endif\n\t\t#if ( SPOT_LIGHT_MAP_INDEX < NUM_SPOT_LIGHT_MAPS )\n\t\t\tspotLightCoord = vSpotLightCoord[ i ].xyz / vSpotLightCoord[ i ].w;\n\t\t\tinSpotLightMap = all( lessThan( abs( spotLightCoord * 2. - 1. ), vec3( 1.0 ) ) );\n\t\t\tspotColor = texture2D( spotLightMap[ SPOT_LIGHT_MAP_INDEX ], spotLightCoord.xy );\n\t\t\tdirectLight.color = inSpotLightMap ? directLight.color * spotColor.rgb : directLight.color;\n\t\t#endif\n\t\t#undef SPOT_LIGHT_MAP_INDEX\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\tspotLightShadow = spotLightShadows[ i ];\n\t\tdirectLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotLightCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometry, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )\n\tDirectionalLight directionalLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0\n\tDirectionalLightShadow directionalLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n\t\tdirectionalLight = directionalLights[ i ];\n\t\tgetDirectionalLightInfo( directionalLight, geometry, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )\n\t\tdirectionalLightShadow = directionalLightShadows[ i ];\n\t\tdirectLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometry, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )\n\tRectAreaLight rectAreaLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) {\n\t\trectAreaLight = rectAreaLights[ i ];\n\t\tRE_Direct_RectArea( rectAreaLight, geometry, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if defined( RE_IndirectDiffuse )\n\tvec3 iblIrradiance = vec3( 0.0 );\n\tvec3 irradiance = getAmbientLightIrradiance( ambientLightColor );\n\tirradiance += getLightProbeIrradiance( lightProbe, geometry.normal );\n\t#if ( NUM_HEMI_LIGHTS > 0 )\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {\n\t\t\tirradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry.normal );\n\t\t}\n\t\t#pragma unroll_loop_end\n\t#endif\n#endif\n#if defined( RE_IndirectSpecular )\n\tvec3 radiance = vec3( 0.0 );\n\tvec3 clearcoatRadiance = vec3( 0.0 );\n#endif",lights_fragment_maps:"#if defined( RE_IndirectDiffuse )\n\t#ifdef USE_LIGHTMAP\n\t\tvec4 lightMapTexel = texture2D( lightMap, vUv2 );\n\t\tvec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity;\n\t\tirradiance += lightMapIrradiance;\n\t#endif\n\t#if defined( USE_ENVMAP ) && defined( STANDARD ) && defined( ENVMAP_TYPE_CUBE_UV )\n\t\tiblIrradiance += getIBLIrradiance( geometry.normal );\n\t#endif\n#endif\n#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular )\n\tradiance += getIBLRadiance( geometry.viewDir, geometry.normal, material.roughness );\n\t#ifdef USE_CLEARCOAT\n\t\tclearcoatRadiance += getIBLRadiance( geometry.viewDir, geometry.clearcoatNormal, material.clearcoatRoughness );\n\t#endif\n#endif",lights_fragment_end:"#if defined( RE_IndirectDiffuse )\n\tRE_IndirectDiffuse( irradiance, geometry, material, reflectedLight );\n#endif\n#if defined( RE_IndirectSpecular )\n\tRE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometry, material, reflectedLight );\n#endif",logdepthbuf_fragment:"#if defined( USE_LOGDEPTHBUF ) && defined( USE_LOGDEPTHBUF_EXT )\n\tgl_FragDepthEXT = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;\n#endif",logdepthbuf_pars_fragment:"#if defined( USE_LOGDEPTHBUF ) && defined( USE_LOGDEPTHBUF_EXT )\n\tuniform float logDepthBufFC;\n\tvarying float vFragDepth;\n\tvarying float vIsPerspective;\n#endif",logdepthbuf_pars_vertex:"#ifdef USE_LOGDEPTHBUF\n\t#ifdef USE_LOGDEPTHBUF_EXT\n\t\tvarying float vFragDepth;\n\t\tvarying float vIsPerspective;\n\t#else\n\t\tuniform float logDepthBufFC;\n\t#endif\n#endif",logdepthbuf_vertex:"#ifdef USE_LOGDEPTHBUF\n\t#ifdef USE_LOGDEPTHBUF_EXT\n\t\tvFragDepth = 1.0 + gl_Position.w;\n\t\tvIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) );\n\t#else\n\t\tif ( isPerspectiveMatrix( projectionMatrix ) ) {\n\t\t\tgl_Position.z = log2( max( EPSILON, gl_Position.w + 1.0 ) ) * logDepthBufFC - 1.0;\n\t\t\tgl_Position.z *= gl_Position.w;\n\t\t}\n\t#endif\n#endif",map_fragment:"#ifdef USE_MAP\n\tvec4 sampledDiffuseColor = texture2D( map, vUv );\n\t#ifdef DECODE_VIDEO_TEXTURE\n\t\tsampledDiffuseColor = vec4( mix( pow( sampledDiffuseColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), sampledDiffuseColor.rgb * 0.0773993808, vec3( lessThanEqual( sampledDiffuseColor.rgb, vec3( 0.04045 ) ) ) ), sampledDiffuseColor.w );\n\t#endif\n\tdiffuseColor *= sampledDiffuseColor;\n#endif",map_pars_fragment:"#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif",map_particle_fragment:"#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\n\tvec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy;\n#endif\n#ifdef USE_MAP\n\tdiffuseColor *= texture2D( map, uv );\n#endif\n#ifdef USE_ALPHAMAP\n\tdiffuseColor.a *= texture2D( alphaMap, uv ).g;\n#endif",map_particle_pars_fragment:"#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\n\tuniform mat3 uvTransform;\n#endif\n#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif\n#ifdef USE_ALPHAMAP\n\tuniform sampler2D alphaMap;\n#endif",metalnessmap_fragment:"float metalnessFactor = metalness;\n#ifdef USE_METALNESSMAP\n\tvec4 texelMetalness = texture2D( metalnessMap, vUv );\n\tmetalnessFactor *= texelMetalness.b;\n#endif",metalnessmap_pars_fragment:"#ifdef USE_METALNESSMAP\n\tuniform sampler2D metalnessMap;\n#endif",morphcolor_vertex:"#if defined( USE_MORPHCOLORS ) && defined( MORPHTARGETS_TEXTURE )\n\tvColor *= morphTargetBaseInfluence;\n\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\t#if defined( USE_COLOR_ALPHA )\n\t\t\tif ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ) * morphTargetInfluences[ i ];\n\t\t#elif defined( USE_COLOR )\n\t\t\tif ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb * morphTargetInfluences[ i ];\n\t\t#endif\n\t}\n#endif",morphnormal_vertex:"#ifdef USE_MORPHNORMALS\n\tobjectNormal *= morphTargetBaseInfluence;\n\t#ifdef MORPHTARGETS_TEXTURE\n\t\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\t\tif ( morphTargetInfluences[ i ] != 0.0 ) objectNormal += getMorph( gl_VertexID, i, 1 ).xyz * morphTargetInfluences[ i ];\n\t\t}\n\t#else\n\t\tobjectNormal += morphNormal0 * morphTargetInfluences[ 0 ];\n\t\tobjectNormal += morphNormal1 * morphTargetInfluences[ 1 ];\n\t\tobjectNormal += morphNormal2 * morphTargetInfluences[ 2 ];\n\t\tobjectNormal += morphNormal3 * morphTargetInfluences[ 3 ];\n\t#endif\n#endif",morphtarget_pars_vertex:"#ifdef USE_MORPHTARGETS\n\tuniform float morphTargetBaseInfluence;\n\t#ifdef MORPHTARGETS_TEXTURE\n\t\tuniform float morphTargetInfluences[ MORPHTARGETS_COUNT ];\n\t\tuniform sampler2DArray morphTargetsTexture;\n\t\tuniform ivec2 morphTargetsTextureSize;\n\t\tvec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) {\n\t\t\tint texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset;\n\t\t\tint y = texelIndex / morphTargetsTextureSize.x;\n\t\t\tint x = texelIndex - y * morphTargetsTextureSize.x;\n\t\t\tivec3 morphUV = ivec3( x, y, morphTargetIndex );\n\t\t\treturn texelFetch( morphTargetsTexture, morphUV, 0 );\n\t\t}\n\t#else\n\t\t#ifndef USE_MORPHNORMALS\n\t\t\tuniform float morphTargetInfluences[ 8 ];\n\t\t#else\n\t\t\tuniform float morphTargetInfluences[ 4 ];\n\t\t#endif\n\t#endif\n#endif",morphtarget_vertex:"#ifdef USE_MORPHTARGETS\n\ttransformed *= morphTargetBaseInfluence;\n\t#ifdef MORPHTARGETS_TEXTURE\n\t\tfor ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) {\n\t\t\tif ( morphTargetInfluences[ i ] != 0.0 ) transformed += getMorph( gl_VertexID, i, 0 ).xyz * morphTargetInfluences[ i ];\n\t\t}\n\t#else\n\t\ttransformed += morphTarget0 * morphTargetInfluences[ 0 ];\n\t\ttransformed += morphTarget1 * morphTargetInfluences[ 1 ];\n\t\ttransformed += morphTarget2 * morphTargetInfluences[ 2 ];\n\t\ttransformed += morphTarget3 * morphTargetInfluences[ 3 ];\n\t\t#ifndef USE_MORPHNORMALS\n\t\t\ttransformed += morphTarget4 * morphTargetInfluences[ 4 ];\n\t\t\ttransformed += morphTarget5 * morphTargetInfluences[ 5 ];\n\t\t\ttransformed += morphTarget6 * morphTargetInfluences[ 6 ];\n\t\t\ttransformed += morphTarget7 * morphTargetInfluences[ 7 ];\n\t\t#endif\n\t#endif\n#endif",normal_fragment_begin:"float faceDirection = gl_FrontFacing ? 1.0 : - 1.0;\n#ifdef FLAT_SHADED\n\tvec3 fdx = dFdx( vViewPosition );\n\tvec3 fdy = dFdy( vViewPosition );\n\tvec3 normal = normalize( cross( fdx, fdy ) );\n#else\n\tvec3 normal = normalize( vNormal );\n\t#ifdef DOUBLE_SIDED\n\t\tnormal = normal * faceDirection;\n\t#endif\n\t#ifdef USE_TANGENT\n\t\tvec3 tangent = normalize( vTangent );\n\t\tvec3 bitangent = normalize( vBitangent );\n\t\t#ifdef DOUBLE_SIDED\n\t\t\ttangent = tangent * faceDirection;\n\t\t\tbitangent = bitangent * faceDirection;\n\t\t#endif\n\t\t#if defined( TANGENTSPACE_NORMALMAP ) || defined( USE_CLEARCOAT_NORMALMAP )\n\t\t\tmat3 vTBN = mat3( tangent, bitangent, normal );\n\t\t#endif\n\t#endif\n#endif\nvec3 geometryNormal = normal;",normal_fragment_maps:"#ifdef OBJECTSPACE_NORMALMAP\n\tnormal = texture2D( normalMap, vUv ).xyz * 2.0 - 1.0;\n\t#ifdef FLIP_SIDED\n\t\tnormal = - normal;\n\t#endif\n\t#ifdef DOUBLE_SIDED\n\t\tnormal = normal * faceDirection;\n\t#endif\n\tnormal = normalize( normalMatrix * normal );\n#elif defined( TANGENTSPACE_NORMALMAP )\n\tvec3 mapN = texture2D( normalMap, vUv ).xyz * 2.0 - 1.0;\n\tmapN.xy *= normalScale;\n\t#ifdef USE_TANGENT\n\t\tnormal = normalize( vTBN * mapN );\n\t#else\n\t\tnormal = perturbNormal2Arb( - vViewPosition, normal, mapN, faceDirection );\n\t#endif\n#elif defined( USE_BUMPMAP )\n\tnormal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection );\n#endif",normal_pars_fragment:"#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif",normal_pars_vertex:"#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif",normal_vertex:"#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n\t#ifdef USE_TANGENT\n\t\tvTangent = normalize( transformedTangent );\n\t\tvBitangent = normalize( cross( vNormal, vTangent ) * tangent.w );\n\t#endif\n#endif",normalmap_pars_fragment:"#ifdef USE_NORMALMAP\n\tuniform sampler2D normalMap;\n\tuniform vec2 normalScale;\n#endif\n#ifdef OBJECTSPACE_NORMALMAP\n\tuniform mat3 normalMatrix;\n#endif\n#if ! defined ( USE_TANGENT ) && ( defined ( TANGENTSPACE_NORMALMAP ) || defined ( USE_CLEARCOAT_NORMALMAP ) )\n\tvec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm, vec3 mapN, float faceDirection ) {\n\t\tvec3 q0 = dFdx( eye_pos.xyz );\n\t\tvec3 q1 = dFdy( eye_pos.xyz );\n\t\tvec2 st0 = dFdx( vUv.st );\n\t\tvec2 st1 = dFdy( vUv.st );\n\t\tvec3 N = surf_norm;\n\t\tvec3 q1perp = cross( q1, N );\n\t\tvec3 q0perp = cross( N, q0 );\n\t\tvec3 T = q1perp * st0.x + q0perp * st1.x;\n\t\tvec3 B = q1perp * st0.y + q0perp * st1.y;\n\t\tfloat det = max( dot( T, T ), dot( B, B ) );\n\t\tfloat scale = ( det == 0.0 ) ? 0.0 : faceDirection * inversesqrt( det );\n\t\treturn normalize( T * ( mapN.x * scale ) + B * ( mapN.y * scale ) + N * mapN.z );\n\t}\n#endif",clearcoat_normal_fragment_begin:"#ifdef USE_CLEARCOAT\n\tvec3 clearcoatNormal = geometryNormal;\n#endif",clearcoat_normal_fragment_maps:"#ifdef USE_CLEARCOAT_NORMALMAP\n\tvec3 clearcoatMapN = texture2D( clearcoatNormalMap, vUv ).xyz * 2.0 - 1.0;\n\tclearcoatMapN.xy *= clearcoatNormalScale;\n\t#ifdef USE_TANGENT\n\t\tclearcoatNormal = normalize( vTBN * clearcoatMapN );\n\t#else\n\t\tclearcoatNormal = perturbNormal2Arb( - vViewPosition, clearcoatNormal, clearcoatMapN, faceDirection );\n\t#endif\n#endif",clearcoat_pars_fragment:"#ifdef USE_CLEARCOATMAP\n\tuniform sampler2D clearcoatMap;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tuniform sampler2D clearcoatRoughnessMap;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tuniform sampler2D clearcoatNormalMap;\n\tuniform vec2 clearcoatNormalScale;\n#endif",iridescence_pars_fragment:"#ifdef USE_IRIDESCENCEMAP\n\tuniform sampler2D iridescenceMap;\n#endif\n#ifdef USE_IRIDESCENCE_THICKNESSMAP\n\tuniform sampler2D iridescenceThicknessMap;\n#endif",output_fragment:"#ifdef OPAQUE\ndiffuseColor.a = 1.0;\n#endif\n#ifdef USE_TRANSMISSION\ndiffuseColor.a *= material.transmissionAlpha + 0.1;\n#endif\ngl_FragColor = vec4( outgoingLight, diffuseColor.a );",packing:"vec3 packNormalToRGB( const in vec3 normal ) {\n\treturn normalize( normal ) * 0.5 + 0.5;\n}\nvec3 unpackRGBToNormal( const in vec3 rgb ) {\n\treturn 2.0 * rgb.xyz - 1.0;\n}\nconst float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;\nconst vec3 PackFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );\nconst vec4 UnpackFactors = UnpackDownscale / vec4( PackFactors, 1. );\nconst float ShiftRight8 = 1. / 256.;\nvec4 packDepthToRGBA( const in float v ) {\n\tvec4 r = vec4( fract( v * PackFactors ), v );\n\tr.yzw -= r.xyz * ShiftRight8;\treturn r * PackUpscale;\n}\nfloat unpackRGBAToDepth( const in vec4 v ) {\n\treturn dot( v, UnpackFactors );\n}\nvec2 packDepthToRG( in highp float v ) {\n\treturn packDepthToRGBA( v ).yx;\n}\nfloat unpackRGToDepth( const in highp vec2 v ) {\n\treturn unpackRGBAToDepth( vec4( v.xy, 0.0, 0.0 ) );\n}\nvec4 pack2HalfToRGBA( vec2 v ) {\n\tvec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ) );\n\treturn vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w );\n}\nvec2 unpackRGBATo2Half( vec4 v ) {\n\treturn vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) );\n}\nfloat viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn ( viewZ + near ) / ( near - far );\n}\nfloat orthographicDepthToViewZ( const in float linearClipZ, const in float near, const in float far ) {\n\treturn linearClipZ * ( near - far ) - near;\n}\nfloat viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn ( ( near + viewZ ) * far ) / ( ( far - near ) * viewZ );\n}\nfloat perspectiveDepthToViewZ( const in float invClipZ, const in float near, const in float far ) {\n\treturn ( near * far ) / ( ( far - near ) * invClipZ - far );\n}",premultiplied_alpha_fragment:"#ifdef PREMULTIPLIED_ALPHA\n\tgl_FragColor.rgb *= gl_FragColor.a;\n#endif",project_vertex:"vec4 mvPosition = vec4( transformed, 1.0 );\n#ifdef USE_INSTANCING\n\tmvPosition = instanceMatrix * mvPosition;\n#endif\nmvPosition = modelViewMatrix * mvPosition;\ngl_Position = projectionMatrix * mvPosition;",dithering_fragment:"#ifdef DITHERING\n\tgl_FragColor.rgb = dithering( gl_FragColor.rgb );\n#endif",dithering_pars_fragment:"#ifdef DITHERING\n\tvec3 dithering( vec3 color ) {\n\t\tfloat grid_position = rand( gl_FragCoord.xy );\n\t\tvec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 );\n\t\tdither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position );\n\t\treturn color + dither_shift_RGB;\n\t}\n#endif",roughnessmap_fragment:"float roughnessFactor = roughness;\n#ifdef USE_ROUGHNESSMAP\n\tvec4 texelRoughness = texture2D( roughnessMap, vUv );\n\troughnessFactor *= texelRoughness.g;\n#endif",roughnessmap_pars_fragment:"#ifdef USE_ROUGHNESSMAP\n\tuniform sampler2D roughnessMap;\n#endif",shadowmap_pars_fragment:"#if NUM_SPOT_LIGHT_COORDS > 0\n\tvarying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ];\n#endif\n#if NUM_SPOT_LIGHT_MAPS > 0\n\tuniform sampler2D spotLightMap[ NUM_SPOT_LIGHT_MAPS ];\n#endif\n#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\tstruct SpotLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t\tfloat shadowCameraNear;\n\t\t\tfloat shadowCameraFar;\n\t\t};\n\t\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\n\t#endif\n\tfloat texture2DCompare( sampler2D depths, vec2 uv, float compare ) {\n\t\treturn step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) );\n\t}\n\tvec2 texture2DDistribution( sampler2D shadow, vec2 uv ) {\n\t\treturn unpackRGBATo2Half( texture2D( shadow, uv ) );\n\t}\n\tfloat VSMShadow (sampler2D shadow, vec2 uv, float compare ){\n\t\tfloat occlusion = 1.0;\n\t\tvec2 distribution = texture2DDistribution( shadow, uv );\n\t\tfloat hard_shadow = step( compare , distribution.x );\n\t\tif (hard_shadow != 1.0 ) {\n\t\t\tfloat distance = compare - distribution.x ;\n\t\t\tfloat variance = max( 0.00000, distribution.y * distribution.y );\n\t\t\tfloat softness_probability = variance / (variance + distance * distance );\t\t\tsoftness_probability = clamp( ( softness_probability - 0.3 ) / ( 0.95 - 0.3 ), 0.0, 1.0 );\t\t\tocclusion = clamp( max( hard_shadow, softness_probability ), 0.0, 1.0 );\n\t\t}\n\t\treturn occlusion;\n\t}\n\tfloat getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\n\t\tfloat shadow = 1.0;\n\t\tshadowCoord.xyz /= shadowCoord.w;\n\t\tshadowCoord.z += shadowBias;\n\t\tbvec4 inFrustumVec = bvec4 ( shadowCoord.x >= 0.0, shadowCoord.x <= 1.0, shadowCoord.y >= 0.0, shadowCoord.y <= 1.0 );\n\t\tbool inFrustum = all( inFrustumVec );\n\t\tbvec2 frustumTestVec = bvec2( inFrustum, shadowCoord.z <= 1.0 );\n\t\tbool frustumTest = all( frustumTestVec );\n\t\tif ( frustumTest ) {\n\t\t#if defined( SHADOWMAP_TYPE_PCF )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx0 = - texelSize.x * shadowRadius;\n\t\t\tfloat dy0 = - texelSize.y * shadowRadius;\n\t\t\tfloat dx1 = + texelSize.x * shadowRadius;\n\t\t\tfloat dy1 = + texelSize.y * shadowRadius;\n\t\t\tfloat dx2 = dx0 / 2.0;\n\t\t\tfloat dy2 = dy0 / 2.0;\n\t\t\tfloat dx3 = dx1 / 2.0;\n\t\t\tfloat dy3 = dy1 / 2.0;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z )\n\t\t\t) * ( 1.0 / 17.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_PCF_SOFT )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx = texelSize.x;\n\t\t\tfloat dy = texelSize.y;\n\t\t\tvec2 uv = shadowCoord.xy;\n\t\t\tvec2 f = fract( uv * shadowMapSize + 0.5 );\n\t\t\tuv -= f * texelSize;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, uv, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( dx, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( 0.0, dy ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + texelSize, shadowCoord.z ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( -dx, 0.0 ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 0.0 ), shadowCoord.z ),\n\t\t\t\t\t f.x ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( -dx, dy ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, dy ), shadowCoord.z ),\n\t\t\t\t\t f.x ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( 0.0, -dy ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 0.0, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t f.y ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t f.y ) +\n\t\t\t\tmix( mix( texture2DCompare( shadowMap, uv + vec2( -dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( 2.0 * dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t\t\tf.x ),\n\t\t\t\t\t mix( texture2DCompare( shadowMap, uv + vec2( -dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t\t\tf.x ),\n\t\t\t\t\t f.y )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_VSM )\n\t\t\tshadow = VSMShadow( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#else\n\t\t\tshadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#endif\n\t\t}\n\t\treturn shadow;\n\t}\n\tvec2 cubeToUV( vec3 v, float texelSizeY ) {\n\t\tvec3 absV = abs( v );\n\t\tfloat scaleToCube = 1.0 / max( absV.x, max( absV.y, absV.z ) );\n\t\tabsV *= scaleToCube;\n\t\tv *= scaleToCube * ( 1.0 - 2.0 * texelSizeY );\n\t\tvec2 planar = v.xy;\n\t\tfloat almostATexel = 1.5 * texelSizeY;\n\t\tfloat almostOne = 1.0 - almostATexel;\n\t\tif ( absV.z >= almostOne ) {\n\t\t\tif ( v.z > 0.0 )\n\t\t\t\tplanar.x = 4.0 - v.x;\n\t\t} else if ( absV.x >= almostOne ) {\n\t\t\tfloat signX = sign( v.x );\n\t\t\tplanar.x = v.z * signX + 2.0 * signX;\n\t\t} else if ( absV.y >= almostOne ) {\n\t\t\tfloat signY = sign( v.y );\n\t\t\tplanar.x = v.x + 2.0 * signY + 2.0;\n\t\t\tplanar.y = v.z * signY - 2.0;\n\t\t}\n\t\treturn vec2( 0.125, 0.25 ) * planar + vec2( 0.375, 0.75 );\n\t}\n\tfloat getPointShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) {\n\t\tvec2 texelSize = vec2( 1.0 ) / ( shadowMapSize * vec2( 4.0, 2.0 ) );\n\t\tvec3 lightToPosition = shadowCoord.xyz;\n\t\tfloat dp = ( length( lightToPosition ) - shadowCameraNear ) / ( shadowCameraFar - shadowCameraNear );\t\tdp += shadowBias;\n\t\tvec3 bd3D = normalize( lightToPosition );\n\t\t#if defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_PCF_SOFT ) || defined( SHADOWMAP_TYPE_VSM )\n\t\t\tvec2 offset = vec2( - 1, 1 ) * shadowRadius * texelSize.y;\n\t\t\treturn (\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxx, texelSize.y ), dp )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#else\n\t\t\treturn texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp );\n\t\t#endif\n\t}\n#endif",shadowmap_pars_vertex:"#if NUM_SPOT_LIGHT_COORDS > 0\n\tuniform mat4 spotLightMatrix[ NUM_SPOT_LIGHT_COORDS ];\n\tvarying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ];\n#endif\n#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\tuniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t\tstruct SpotLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\tuniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t\tfloat shadowCameraNear;\n\t\t\tfloat shadowCameraFar;\n\t\t};\n\t\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\n\t#endif\n#endif",shadowmap_vertex:"#if defined( USE_SHADOWMAP ) || ( NUM_SPOT_LIGHT_COORDS > 0 )\n\t#if NUM_DIR_LIGHT_SHADOWS > 0 || NUM_SPOT_LIGHT_COORDS > 0 || NUM_POINT_LIGHT_SHADOWS > 0\n\t\tvec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\n\t\tvec4 shadowWorldPosition;\n\t#endif\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\n\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );\n\t\tvDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_SPOT_LIGHT_COORDS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHT_COORDS; i ++ ) {\n\t\tshadowWorldPosition = worldPosition;\n\t\t#if ( defined( USE_SHADOWMAP ) && UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\t\tshadowWorldPosition.xyz += shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias;\n\t\t#endif\n\t\tvSpotLightCoord[ i ] = spotLightMatrix[ i ] * shadowWorldPosition;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\n\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 );\n\t\tvPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n#endif",shadowmask_pars_fragment:"float getShadowMask() {\n\tfloat shadow = 1.0;\n\t#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\tDirectionalLightShadow directionalLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\n\t\tdirectionalLight = directionalLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\tSpotLightShadow spotLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) {\n\t\tspotLight = spotLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowBias, spotLight.shadowRadius, vSpotLightCoord[ i ] ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\tPointLightShadow pointLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\n\t\tpointLight = pointLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#endif\n\treturn shadow;\n}",skinbase_vertex:"#ifdef USE_SKINNING\n\tmat4 boneMatX = getBoneMatrix( skinIndex.x );\n\tmat4 boneMatY = getBoneMatrix( skinIndex.y );\n\tmat4 boneMatZ = getBoneMatrix( skinIndex.z );\n\tmat4 boneMatW = getBoneMatrix( skinIndex.w );\n#endif",skinning_pars_vertex:"#ifdef USE_SKINNING\n\tuniform mat4 bindMatrix;\n\tuniform mat4 bindMatrixInverse;\n\tuniform highp sampler2D boneTexture;\n\tuniform int boneTextureSize;\n\tmat4 getBoneMatrix( const in float i ) {\n\t\tfloat j = i * 4.0;\n\t\tfloat x = mod( j, float( boneTextureSize ) );\n\t\tfloat y = floor( j / float( boneTextureSize ) );\n\t\tfloat dx = 1.0 / float( boneTextureSize );\n\t\tfloat dy = 1.0 / float( boneTextureSize );\n\t\ty = dy * ( y + 0.5 );\n\t\tvec4 v1 = texture2D( boneTexture, vec2( dx * ( x + 0.5 ), y ) );\n\t\tvec4 v2 = texture2D( boneTexture, vec2( dx * ( x + 1.5 ), y ) );\n\t\tvec4 v3 = texture2D( boneTexture, vec2( dx * ( x + 2.5 ), y ) );\n\t\tvec4 v4 = texture2D( boneTexture, vec2( dx * ( x + 3.5 ), y ) );\n\t\tmat4 bone = mat4( v1, v2, v3, v4 );\n\t\treturn bone;\n\t}\n#endif",skinning_vertex:"#ifdef USE_SKINNING\n\tvec4 skinVertex = bindMatrix * vec4( transformed, 1.0 );\n\tvec4 skinned = vec4( 0.0 );\n\tskinned += boneMatX * skinVertex * skinWeight.x;\n\tskinned += boneMatY * skinVertex * skinWeight.y;\n\tskinned += boneMatZ * skinVertex * skinWeight.z;\n\tskinned += boneMatW * skinVertex * skinWeight.w;\n\ttransformed = ( bindMatrixInverse * skinned ).xyz;\n#endif",skinnormal_vertex:"#ifdef USE_SKINNING\n\tmat4 skinMatrix = mat4( 0.0 );\n\tskinMatrix += skinWeight.x * boneMatX;\n\tskinMatrix += skinWeight.y * boneMatY;\n\tskinMatrix += skinWeight.z * boneMatZ;\n\tskinMatrix += skinWeight.w * boneMatW;\n\tskinMatrix = bindMatrixInverse * skinMatrix * bindMatrix;\n\tobjectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz;\n\t#ifdef USE_TANGENT\n\t\tobjectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz;\n\t#endif\n#endif",specularmap_fragment:"float specularStrength;\n#ifdef USE_SPECULARMAP\n\tvec4 texelSpecular = texture2D( specularMap, vUv );\n\tspecularStrength = texelSpecular.r;\n#else\n\tspecularStrength = 1.0;\n#endif",specularmap_pars_fragment:"#ifdef USE_SPECULARMAP\n\tuniform sampler2D specularMap;\n#endif",tonemapping_fragment:"#if defined( TONE_MAPPING )\n\tgl_FragColor.rgb = toneMapping( gl_FragColor.rgb );\n#endif",tonemapping_pars_fragment:"#ifndef saturate\n#define saturate( a ) clamp( a, 0.0, 1.0 )\n#endif\nuniform float toneMappingExposure;\nvec3 LinearToneMapping( vec3 color ) {\n\treturn toneMappingExposure * color;\n}\nvec3 ReinhardToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\treturn saturate( color / ( vec3( 1.0 ) + color ) );\n}\nvec3 OptimizedCineonToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\tcolor = max( vec3( 0.0 ), color - 0.004 );\n\treturn pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) );\n}\nvec3 RRTAndODTFit( vec3 v ) {\n\tvec3 a = v * ( v + 0.0245786 ) - 0.000090537;\n\tvec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081;\n\treturn a / b;\n}\nvec3 ACESFilmicToneMapping( vec3 color ) {\n\tconst mat3 ACESInputMat = mat3(\n\t\tvec3( 0.59719, 0.07600, 0.02840 ),\t\tvec3( 0.35458, 0.90834, 0.13383 ),\n\t\tvec3( 0.04823, 0.01566, 0.83777 )\n\t);\n\tconst mat3 ACESOutputMat = mat3(\n\t\tvec3(\t1.60475, -0.10208, -0.00327 ),\t\tvec3( -0.53108,\t1.10813, -0.07276 ),\n\t\tvec3( -0.07367, -0.00605,\t1.07602 )\n\t);\n\tcolor *= toneMappingExposure / 0.6;\n\tcolor = ACESInputMat * color;\n\tcolor = RRTAndODTFit( color );\n\tcolor = ACESOutputMat * color;\n\treturn saturate( color );\n}\nvec3 CustomToneMapping( vec3 color ) { return color; }",transmission_fragment:"#ifdef USE_TRANSMISSION\n\tmaterial.transmission = transmission;\n\tmaterial.transmissionAlpha = 1.0;\n\tmaterial.thickness = thickness;\n\tmaterial.attenuationDistance = attenuationDistance;\n\tmaterial.attenuationColor = attenuationColor;\n\t#ifdef USE_TRANSMISSIONMAP\n\t\tmaterial.transmission *= texture2D( transmissionMap, vUv ).r;\n\t#endif\n\t#ifdef USE_THICKNESSMAP\n\t\tmaterial.thickness *= texture2D( thicknessMap, vUv ).g;\n\t#endif\n\tvec3 pos = vWorldPosition;\n\tvec3 v = normalize( cameraPosition - pos );\n\tvec3 n = inverseTransformDirection( normal, viewMatrix );\n\tvec4 transmission = getIBLVolumeRefraction(\n\t\tn, v, material.roughness, material.diffuseColor, material.specularColor, material.specularF90,\n\t\tpos, modelMatrix, viewMatrix, projectionMatrix, material.ior, material.thickness,\n\t\tmaterial.attenuationColor, material.attenuationDistance );\n\tmaterial.transmissionAlpha = mix( material.transmissionAlpha, transmission.a, material.transmission );\n\ttotalDiffuse = mix( totalDiffuse, transmission.rgb, material.transmission );\n#endif",transmission_pars_fragment:"#ifdef USE_TRANSMISSION\n\tuniform float transmission;\n\tuniform float thickness;\n\tuniform float attenuationDistance;\n\tuniform vec3 attenuationColor;\n\t#ifdef USE_TRANSMISSIONMAP\n\t\tuniform sampler2D transmissionMap;\n\t#endif\n\t#ifdef USE_THICKNESSMAP\n\t\tuniform sampler2D thicknessMap;\n\t#endif\n\tuniform vec2 transmissionSamplerSize;\n\tuniform sampler2D transmissionSamplerMap;\n\tuniform mat4 modelMatrix;\n\tuniform mat4 projectionMatrix;\n\tvarying vec3 vWorldPosition;\n\tvec3 getVolumeTransmissionRay( const in vec3 n, const in vec3 v, const in float thickness, const in float ior, const in mat4 modelMatrix ) {\n\t\tvec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior );\n\t\tvec3 modelScale;\n\t\tmodelScale.x = length( vec3( modelMatrix[ 0 ].xyz ) );\n\t\tmodelScale.y = length( vec3( modelMatrix[ 1 ].xyz ) );\n\t\tmodelScale.z = length( vec3( modelMatrix[ 2 ].xyz ) );\n\t\treturn normalize( refractionVector ) * thickness * modelScale;\n\t}\n\tfloat applyIorToRoughness( const in float roughness, const in float ior ) {\n\t\treturn roughness * clamp( ior * 2.0 - 2.0, 0.0, 1.0 );\n\t}\n\tvec4 getTransmissionSample( const in vec2 fragCoord, const in float roughness, const in float ior ) {\n\t\tfloat framebufferLod = log2( transmissionSamplerSize.x ) * applyIorToRoughness( roughness, ior );\n\t\t#ifdef texture2DLodEXT\n\t\t\treturn texture2DLodEXT( transmissionSamplerMap, fragCoord.xy, framebufferLod );\n\t\t#else\n\t\t\treturn texture2D( transmissionSamplerMap, fragCoord.xy, framebufferLod );\n\t\t#endif\n\t}\n\tvec3 applyVolumeAttenuation( const in vec3 radiance, const in float transmissionDistance, const in vec3 attenuationColor, const in float attenuationDistance ) {\n\t\tif ( isinf( attenuationDistance ) ) {\n\t\t\treturn radiance;\n\t\t} else {\n\t\t\tvec3 attenuationCoefficient = -log( attenuationColor ) / attenuationDistance;\n\t\t\tvec3 transmittance = exp( - attenuationCoefficient * transmissionDistance );\t\t\treturn transmittance * radiance;\n\t\t}\n\t}\n\tvec4 getIBLVolumeRefraction( const in vec3 n, const in vec3 v, const in float roughness, const in vec3 diffuseColor,\n\t\tconst in vec3 specularColor, const in float specularF90, const in vec3 position, const in mat4 modelMatrix,\n\t\tconst in mat4 viewMatrix, const in mat4 projMatrix, const in float ior, const in float thickness,\n\t\tconst in vec3 attenuationColor, const in float attenuationDistance ) {\n\t\tvec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix );\n\t\tvec3 refractedRayExit = position + transmissionRay;\n\t\tvec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 );\n\t\tvec2 refractionCoords = ndcPos.xy / ndcPos.w;\n\t\trefractionCoords += 1.0;\n\t\trefractionCoords /= 2.0;\n\t\tvec4 transmittedLight = getTransmissionSample( refractionCoords, roughness, ior );\n\t\tvec3 attenuatedColor = applyVolumeAttenuation( transmittedLight.rgb, length( transmissionRay ), attenuationColor, attenuationDistance );\n\t\tvec3 F = EnvironmentBRDF( n, v, specularColor, specularF90, roughness );\n\t\treturn vec4( ( 1.0 - F ) * attenuatedColor * diffuseColor, transmittedLight.a );\n\t}\n#endif",uv_pars_fragment:"#if ( defined( USE_UV ) && ! defined( UVS_VERTEX_ONLY ) )\n\tvarying vec2 vUv;\n#endif",uv_pars_vertex:"#ifdef USE_UV\n\t#ifdef UVS_VERTEX_ONLY\n\t\tvec2 vUv;\n\t#else\n\t\tvarying vec2 vUv;\n\t#endif\n\tuniform mat3 uvTransform;\n#endif",uv_vertex:"#ifdef USE_UV\n\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n#endif",uv2_pars_fragment:"#if defined( USE_LIGHTMAP ) || defined( USE_AOMAP )\n\tvarying vec2 vUv2;\n#endif",uv2_pars_vertex:"#if defined( USE_LIGHTMAP ) || defined( USE_AOMAP )\n\tattribute vec2 uv2;\n\tvarying vec2 vUv2;\n\tuniform mat3 uv2Transform;\n#endif",uv2_vertex:"#if defined( USE_LIGHTMAP ) || defined( USE_AOMAP )\n\tvUv2 = ( uv2Transform * vec3( uv2, 1 ) ).xy;\n#endif",worldpos_vertex:"#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0\n\tvec4 worldPosition = vec4( transformed, 1.0 );\n\t#ifdef USE_INSTANCING\n\t\tworldPosition = instanceMatrix * worldPosition;\n\t#endif\n\tworldPosition = modelMatrix * worldPosition;\n#endif",background_vert:"varying vec2 vUv;\nuniform mat3 uvTransform;\nvoid main() {\n\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n\tgl_Position = vec4( position.xy, 1.0, 1.0 );\n}",background_frag:"uniform sampler2D t2D;\nuniform float backgroundIntensity;\nvarying vec2 vUv;\nvoid main() {\n\tvec4 texColor = texture2D( t2D, vUv );\n\t#ifdef DECODE_VIDEO_TEXTURE\n\t\ttexColor = vec4( mix( pow( texColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), texColor.rgb * 0.0773993808, vec3( lessThanEqual( texColor.rgb, vec3( 0.04045 ) ) ) ), texColor.w );\n\t#endif\n\ttexColor.rgb *= backgroundIntensity;\n\tgl_FragColor = texColor;\n\t#include \n\t#include \n}",backgroundCube_vert:"varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n\tgl_Position.z = gl_Position.w;\n}",backgroundCube_frag:"#ifdef ENVMAP_TYPE_CUBE\n\tuniform samplerCube envMap;\n#elif defined( ENVMAP_TYPE_CUBE_UV )\n\tuniform sampler2D envMap;\n#endif\nuniform float flipEnvMap;\nuniform float backgroundBlurriness;\nuniform float backgroundIntensity;\nvarying vec3 vWorldDirection;\n#include \nvoid main() {\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tvec4 texColor = textureCube( envMap, vec3( flipEnvMap * vWorldDirection.x, vWorldDirection.yz ) );\n\t#elif defined( ENVMAP_TYPE_CUBE_UV )\n\t\tvec4 texColor = textureCubeUV( envMap, vWorldDirection, backgroundBlurriness );\n\t#else\n\t\tvec4 texColor = vec4( 0.0, 0.0, 0.0, 1.0 );\n\t#endif\n\ttexColor.rgb *= backgroundIntensity;\n\tgl_FragColor = texColor;\n\t#include \n\t#include \n}",cube_vert:"varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n\tgl_Position.z = gl_Position.w;\n}",cube_frag:"uniform samplerCube tCube;\nuniform float tFlip;\nuniform float opacity;\nvarying vec3 vWorldDirection;\nvoid main() {\n\tvec4 texColor = textureCube( tCube, vec3( tFlip * vWorldDirection.x, vWorldDirection.yz ) );\n\tgl_FragColor = texColor;\n\tgl_FragColor.a *= opacity;\n\t#include \n\t#include \n}",depth_vert:"#include \n#include \n#include \n#include \n#include \n#include \n#include \nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\t#include \n\t#include \n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvHighPrecisionZW = gl_Position.zw;\n}",depth_frag:"#if DEPTH_PACKING == 3200\n\tuniform float opacity;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( 1.0 );\n\t#if DEPTH_PACKING == 3200\n\t\tdiffuseColor.a = opacity;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\tfloat fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;\n\t#if DEPTH_PACKING == 3200\n\t\tgl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );\n\t#elif DEPTH_PACKING == 3201\n\t\tgl_FragColor = packDepthToRGBA( fragCoordZ );\n\t#endif\n}",distanceRGBA_vert:"#define DISTANCE\nvarying vec3 vWorldPosition;\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvWorldPosition = worldPosition.xyz;\n}",distanceRGBA_frag:"#define DISTANCE\nuniform vec3 referencePosition;\nuniform float nearDistance;\nuniform float farDistance;\nvarying vec3 vWorldPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main () {\n\t#include \n\tvec4 diffuseColor = vec4( 1.0 );\n\t#include \n\t#include \n\t#include \n\tfloat dist = length( vWorldPosition - referencePosition );\n\tdist = ( dist - nearDistance ) / ( farDistance - nearDistance );\n\tdist = saturate( dist );\n\tgl_FragColor = packDepthToRGBA( dist );\n}",equirect_vert:"varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n}",equirect_frag:"uniform sampler2D tEquirect;\nvarying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvec3 direction = normalize( vWorldDirection );\n\tvec2 sampleUV = equirectUv( direction );\n\tgl_FragColor = texture2D( tEquirect, sampleUV );\n\t#include \n\t#include \n}",linedashed_vert:"uniform float scale;\nattribute float lineDistance;\nvarying float vLineDistance;\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvLineDistance = scale * lineDistance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",linedashed_frag:"uniform vec3 diffuse;\nuniform float opacity;\nuniform float dashSize;\nuniform float totalSize;\nvarying float vLineDistance;\n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tif ( mod( vLineDistance, totalSize ) > dashSize ) {\n\t\tdiscard;\n\t}\n\tvec3 outgoingLight = vec3( 0.0 );\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshbasic_vert:"#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#if defined ( USE_ENVMAP ) || defined ( USE_SKINNING )\n\t\t#include \n\t\t#include \n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshbasic_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\t#ifdef USE_LIGHTMAP\n\t\tvec4 lightMapTexel = texture2D( lightMap, vUv2 );\n\t\treflectedLight.indirectDiffuse += lightMapTexel.rgb * lightMapIntensity * RECIPROCAL_PI;\n\t#else\n\t\treflectedLight.indirectDiffuse += vec3( 1.0 );\n\t#endif\n\t#include \n\treflectedLight.indirectDiffuse *= diffuseColor.rgb;\n\tvec3 outgoingLight = reflectedLight.indirectDiffuse;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshlambert_vert:"#define LAMBERT\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n\t#include \n}",meshlambert_frag:"#define LAMBERT\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshmatcap_vert:"#define MATCAP\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n}",meshmatcap_frag:"#define MATCAP\nuniform vec3 diffuse;\nuniform float opacity;\nuniform sampler2D matcap;\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 viewDir = normalize( vViewPosition );\n\tvec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) );\n\tvec3 y = cross( viewDir, x );\n\tvec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5;\n\t#ifdef USE_MATCAP\n\t\tvec4 matcapColor = texture2D( matcap, uv );\n\t#else\n\t\tvec4 matcapColor = vec4( vec3( mix( 0.2, 0.8, uv.y ) ), 1.0 );\n\t#endif\n\tvec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshnormal_vert:"#define NORMAL\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( TANGENTSPACE_NORMALMAP )\n\tvarying vec3 vViewPosition;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( TANGENTSPACE_NORMALMAP )\n\tvViewPosition = - mvPosition.xyz;\n#endif\n}",meshnormal_frag:"#define NORMAL\nuniform float opacity;\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( TANGENTSPACE_NORMALMAP )\n\tvarying vec3 vViewPosition;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\tgl_FragColor = vec4( packNormalToRGB( normal ), opacity );\n\t#ifdef OPAQUE\n\t\tgl_FragColor.a = 1.0;\n\t#endif\n}",meshphong_vert:"#define PHONG\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n\t#include \n}",meshphong_frag:"#define PHONG\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform vec3 specular;\nuniform float shininess;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshphysical_vert:"#define STANDARD\nvarying vec3 vViewPosition;\n#ifdef USE_TRANSMISSION\n\tvarying vec3 vWorldPosition;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n#ifdef USE_TRANSMISSION\n\tvWorldPosition = worldPosition.xyz;\n#endif\n}",meshphysical_frag:"#define STANDARD\n#ifdef PHYSICAL\n\t#define IOR\n\t#define SPECULAR\n#endif\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float roughness;\nuniform float metalness;\nuniform float opacity;\n#ifdef IOR\n\tuniform float ior;\n#endif\n#ifdef SPECULAR\n\tuniform float specularIntensity;\n\tuniform vec3 specularColor;\n\t#ifdef USE_SPECULARINTENSITYMAP\n\t\tuniform sampler2D specularIntensityMap;\n\t#endif\n\t#ifdef USE_SPECULARCOLORMAP\n\t\tuniform sampler2D specularColorMap;\n\t#endif\n#endif\n#ifdef USE_CLEARCOAT\n\tuniform float clearcoat;\n\tuniform float clearcoatRoughness;\n#endif\n#ifdef USE_IRIDESCENCE\n\tuniform float iridescence;\n\tuniform float iridescenceIOR;\n\tuniform float iridescenceThicknessMinimum;\n\tuniform float iridescenceThicknessMaximum;\n#endif\n#ifdef USE_SHEEN\n\tuniform vec3 sheenColor;\n\tuniform float sheenRoughness;\n\t#ifdef USE_SHEENCOLORMAP\n\t\tuniform sampler2D sheenColorMap;\n\t#endif\n\t#ifdef USE_SHEENROUGHNESSMAP\n\t\tuniform sampler2D sheenRoughnessMap;\n\t#endif\n#endif\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse;\n\tvec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular;\n\t#include \n\tvec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance;\n\t#ifdef USE_SHEEN\n\t\tfloat sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor );\n\t\toutgoingLight = outgoingLight * sheenEnergyComp + sheenSpecular;\n\t#endif\n\t#ifdef USE_CLEARCOAT\n\t\tfloat dotNVcc = saturate( dot( geometry.clearcoatNormal, geometry.viewDir ) );\n\t\tvec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc );\n\t\toutgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + clearcoatSpecular * material.clearcoat;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshtoon_vert:"#define TOON\nvarying vec3 vViewPosition;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n}",meshtoon_frag:"#define TOON\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",points_vert:"uniform float size;\nuniform float scale;\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tgl_PointSize = size;\n\t#ifdef USE_SIZEATTENUATION\n\t\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\n\t\tif ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z );\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n}",points_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec3 outgoingLight = vec3( 0.0 );\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",shadow_vert:"#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",shadow_frag:"uniform vec3 color;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tgl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) );\n\t#include \n\t#include \n\t#include \n}",sprite_vert:"uniform float rotation;\nuniform vec2 center;\n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 );\n\tvec2 scale;\n\tscale.x = length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) );\n\tscale.y = length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) );\n\t#ifndef USE_SIZEATTENUATION\n\t\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\n\t\tif ( isPerspective ) scale *= - mvPosition.z;\n\t#endif\n\tvec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale;\n\tvec2 rotatedPosition;\n\trotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;\n\trotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;\n\tmvPosition.xy += rotatedPosition;\n\tgl_Position = projectionMatrix * mvPosition;\n\t#include \n\t#include \n\t#include \n}",sprite_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec3 outgoingLight = vec3( 0.0 );\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\t#include \n\t#include \n\t#include \n\t#include \n}"},Sn={common:{diffuse:{value:new qt(16777215)},opacity:{value:1},map:{value:null},uvTransform:{value:new Rt},uv2Transform:{value:new Rt},alphaMap:{value:null},alphaTest:{value:0}},specularmap:{specularMap:{value:null}},envmap:{envMap:{value:null},flipEnvMap:{value:-1},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1}},emissivemap:{emissiveMap:{value:null}},bumpmap:{bumpMap:{value:null},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalScale:{value:new Lt(1,1)}},displacementmap:{displacementMap:{value:null},displacementScale:{value:1},displacementBias:{value:0}},roughnessmap:{roughnessMap:{value:null}},metalnessmap:{metalnessMap:{value:null}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new qt(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMap:{value:[]},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotLightMap:{value:[]},spotShadowMap:{value:[]},spotLightMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMap:{value:[]},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null}},points:{diffuse:{value:new qt(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaTest:{value:0},uvTransform:{value:new Rt}},sprite:{diffuse:{value:new qt(16777215)},opacity:{value:1},center:{value:new Lt(.5,.5)},rotation:{value:0},map:{value:null},alphaMap:{value:null},alphaTest:{value:0},uvTransform:{value:new Rt}}},wn={basic:{uniforms:en([Sn.common,Sn.specularmap,Sn.envmap,Sn.aomap,Sn.lightmap,Sn.fog]),vertexShader:bn.meshbasic_vert,fragmentShader:bn.meshbasic_frag},lambert:{uniforms:en([Sn.common,Sn.specularmap,Sn.envmap,Sn.aomap,Sn.lightmap,Sn.emissivemap,Sn.bumpmap,Sn.normalmap,Sn.displacementmap,Sn.fog,Sn.lights,{emissive:{value:new qt(0)}}]),vertexShader:bn.meshlambert_vert,fragmentShader:bn.meshlambert_frag},phong:{uniforms:en([Sn.common,Sn.specularmap,Sn.envmap,Sn.aomap,Sn.lightmap,Sn.emissivemap,Sn.bumpmap,Sn.normalmap,Sn.displacementmap,Sn.fog,Sn.lights,{emissive:{value:new qt(0)},specular:{value:new qt(1118481)},shininess:{value:30}}]),vertexShader:bn.meshphong_vert,fragmentShader:bn.meshphong_frag},standard:{uniforms:en([Sn.common,Sn.envmap,Sn.aomap,Sn.lightmap,Sn.emissivemap,Sn.bumpmap,Sn.normalmap,Sn.displacementmap,Sn.roughnessmap,Sn.metalnessmap,Sn.fog,Sn.lights,{emissive:{value:new qt(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:bn.meshphysical_vert,fragmentShader:bn.meshphysical_frag},toon:{uniforms:en([Sn.common,Sn.aomap,Sn.lightmap,Sn.emissivemap,Sn.bumpmap,Sn.normalmap,Sn.displacementmap,Sn.gradientmap,Sn.fog,Sn.lights,{emissive:{value:new qt(0)}}]),vertexShader:bn.meshtoon_vert,fragmentShader:bn.meshtoon_frag},matcap:{uniforms:en([Sn.common,Sn.bumpmap,Sn.normalmap,Sn.displacementmap,Sn.fog,{matcap:{value:null}}]),vertexShader:bn.meshmatcap_vert,fragmentShader:bn.meshmatcap_frag},points:{uniforms:en([Sn.points,Sn.fog]),vertexShader:bn.points_vert,fragmentShader:bn.points_frag},dashed:{uniforms:en([Sn.common,Sn.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:bn.linedashed_vert,fragmentShader:bn.linedashed_frag},depth:{uniforms:en([Sn.common,Sn.displacementmap]),vertexShader:bn.depth_vert,fragmentShader:bn.depth_frag},normal:{uniforms:en([Sn.common,Sn.bumpmap,Sn.normalmap,Sn.displacementmap,{opacity:{value:1}}]),vertexShader:bn.meshnormal_vert,fragmentShader:bn.meshnormal_frag},sprite:{uniforms:en([Sn.sprite,Sn.fog]),vertexShader:bn.sprite_vert,fragmentShader:bn.sprite_frag},background:{uniforms:{uvTransform:{value:new Rt},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:bn.background_vert,fragmentShader:bn.background_frag},backgroundCube:{uniforms:{envMap:{value:null},flipEnvMap:{value:-1},backgroundBlurriness:{value:0},backgroundIntensity:{value:1}},vertexShader:bn.backgroundCube_vert,fragmentShader:bn.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:bn.cube_vert,fragmentShader:bn.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:bn.equirect_vert,fragmentShader:bn.equirect_frag},distanceRGBA:{uniforms:en([Sn.common,Sn.displacementmap,{referencePosition:{value:new re},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:bn.distanceRGBA_vert,fragmentShader:bn.distanceRGBA_frag},shadow:{uniforms:en([Sn.lights,Sn.fog,{color:{value:new qt(0)},opacity:{value:1}}]),vertexShader:bn.shadow_vert,fragmentShader:bn.shadow_frag}};wn.physical={uniforms:en([wn.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatNormalScale:{value:new Lt(1,1)},clearcoatNormalMap:{value:null},iridescence:{value:0},iridescenceMap:{value:null},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},sheen:{value:0},sheenColor:{value:new qt(0)},sheenColorMap:{value:null},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},transmission:{value:0},transmissionMap:{value:null},transmissionSamplerSize:{value:new Lt},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},attenuationDistance:{value:0},attenuationColor:{value:new qt(0)},specularIntensity:{value:1},specularIntensityMap:{value:null},specularColor:{value:new qt(1,1,1)},specularColorMap:{value:null}}]),vertexShader:bn.meshphysical_vert,fragmentShader:bn.meshphysical_frag};const Tn={r:0,b:0,g:0};function An(t,e,i,n,r,s,a){const o=new qt(0);let c,h,u=!0===s?0:1,d=null,p=0,m=null;function f(e,i){e.getRGB(Tn,nn(t)),n.buffers.color.setClear(Tn.r,Tn.g,Tn.b,i,a)}return{getClearColor:function(){return o},setClearColor:function(t,e=1){o.set(t),u=e,f(o,u)},getClearAlpha:function(){return u},setClearAlpha:function(t){u=t,f(o,u)},render:function(n,s){let a=!1,g=!0===s.isScene?s.background:null;if(g&&g.isTexture){g=(s.backgroundBlurriness>0?i:e).get(g)}const v=t.xr,x=v.getSession&&v.getSession();x&&"additive"===x.environmentBlendMode&&(g=null),null===g?f(o,u):g&&g.isColor&&(f(g,1),a=!0),(t.autoClear||a)&&t.clear(t.autoClearColor,t.autoClearDepth,t.autoClearStencil),g&&(g.isCubeTexture||g.mapping===l)?(void 0===h&&(h=new Ki(new Qi(1,1,1),new sn({name:"BackgroundCubeMaterial",uniforms:tn(wn.backgroundCube.uniforms),vertexShader:wn.backgroundCube.vertexShader,fragmentShader:wn.backgroundCube.fragmentShader,side:1,depthTest:!1,depthWrite:!1,fog:!1})),h.geometry.deleteAttribute("normal"),h.geometry.deleteAttribute("uv"),h.onBeforeRender=function(t,e,i){this.matrixWorld.copyPosition(i.matrixWorld)},Object.defineProperty(h.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),r.update(h)),h.material.uniforms.envMap.value=g,h.material.uniforms.flipEnvMap.value=g.isCubeTexture&&!1===g.isRenderTargetTexture?-1:1,h.material.uniforms.backgroundBlurriness.value=s.backgroundBlurriness,h.material.uniforms.backgroundIntensity.value=s.backgroundIntensity,d===g&&p===g.version&&m===t.toneMapping||(h.material.needsUpdate=!0,d=g,p=g.version,m=t.toneMapping),h.layers.enableAll(),n.unshift(h,h.geometry,h.material,0,0,null)):g&&g.isTexture&&(void 0===c&&(c=new Ki(new Mn(2,2),new sn({name:"BackgroundMaterial",uniforms:tn(wn.background.uniforms),vertexShader:wn.background.vertexShader,fragmentShader:wn.background.fragmentShader,side:0,depthTest:!1,depthWrite:!1,fog:!1})),c.geometry.deleteAttribute("normal"),Object.defineProperty(c.material,"map",{get:function(){return this.uniforms.t2D.value}}),r.update(c)),c.material.uniforms.t2D.value=g,c.material.uniforms.backgroundIntensity.value=s.backgroundIntensity,!0===g.matrixAutoUpdate&&g.updateMatrix(),c.material.uniforms.uvTransform.value.copy(g.matrix),d===g&&p===g.version&&m===t.toneMapping||(c.material.needsUpdate=!0,d=g,p=g.version,m=t.toneMapping),c.layers.enableAll(),n.unshift(c,c.geometry,c.material,0,0,null))}}}function En(t,e,i,n){const r=t.getParameter(34921),s=n.isWebGL2?null:e.get("OES_vertex_array_object"),a=n.isWebGL2||null!==s,o={},l=p(null);let c=l,h=!1;function u(e){return n.isWebGL2?t.bindVertexArray(e):s.bindVertexArrayOES(e)}function d(e){return n.isWebGL2?t.deleteVertexArray(e):s.deleteVertexArrayOES(e)}function p(t){const e=[],i=[],n=[];for(let t=0;t=0){const i=r[e];let n=s[e];if(void 0===n&&("instanceMatrix"===e&&t.instanceMatrix&&(n=t.instanceMatrix),"instanceColor"===e&&t.instanceColor&&(n=t.instanceColor)),void 0===i)return!0;if(i.attribute!==n)return!0;if(n&&i.data!==n.data)return!0;a++}}return c.attributesNum!==a||c.index!==n}(r,_,d,y),M&&function(t,e,i,n){const r={},s=e.attributes;let a=0;const o=i.getAttributes();for(const e in o){if(o[e].location>=0){let i=s[e];void 0===i&&("instanceMatrix"===e&&t.instanceMatrix&&(i=t.instanceMatrix),"instanceColor"===e&&t.instanceColor&&(i=t.instanceColor));const n={};n.attribute=i,i&&i.data&&(n.data=i.data),r[e]=n,a++}}c.attributes=r,c.attributesNum=a,c.index=n}(r,_,d,y)}else{const t=!0===l.wireframe;c.geometry===_.id&&c.program===d.id&&c.wireframe===t||(c.geometry=_.id,c.program=d.id,c.wireframe=t,M=!0)}null!==y&&i.update(y,34963),(M||h)&&(h=!1,function(r,s,a,o){if(!1===n.isWebGL2&&(r.isInstancedMesh||o.isInstancedBufferGeometry)&&null===e.get("ANGLE_instanced_arrays"))return;m();const l=o.attributes,c=a.getAttributes(),h=s.defaultAttributeValues;for(const e in c){const n=c[e];if(n.location>=0){let s=l[e];if(void 0===s&&("instanceMatrix"===e&&r.instanceMatrix&&(s=r.instanceMatrix),"instanceColor"===e&&r.instanceColor&&(s=r.instanceColor)),void 0!==s){const e=s.normalized,a=s.itemSize,l=i.get(s);if(void 0===l)continue;const c=l.buffer,h=l.type,u=l.bytesPerElement;if(s.isInterleavedBufferAttribute){const i=s.data,l=i.stride,d=s.offset;if(i.isInstancedInterleavedBuffer){for(let t=0;t0&&t.getShaderPrecisionFormat(35632,36338).precision>0)return"highp";e="mediump"}return"mediump"===e&&t.getShaderPrecisionFormat(35633,36337).precision>0&&t.getShaderPrecisionFormat(35632,36337).precision>0?"mediump":"lowp"}const s="undefined"!=typeof WebGL2RenderingContext&&t instanceof WebGL2RenderingContext||"undefined"!=typeof WebGL2ComputeRenderingContext&&t instanceof WebGL2ComputeRenderingContext;let a=void 0!==i.precision?i.precision:"highp";const o=r(a);o!==a&&(console.warn("THREE.WebGLRenderer:",a,"not supported, using",o,"instead."),a=o);const l=s||e.has("WEBGL_draw_buffers"),c=!0===i.logarithmicDepthBuffer,h=t.getParameter(34930),u=t.getParameter(35660),d=t.getParameter(3379),p=t.getParameter(34076),m=t.getParameter(34921),f=t.getParameter(36347),g=t.getParameter(36348),v=t.getParameter(36349),x=u>0,_=s||e.has("OES_texture_float");return{isWebGL2:s,drawBuffers:l,getMaxAnisotropy:function(){if(void 0!==n)return n;if(!0===e.has("EXT_texture_filter_anisotropic")){const i=e.get("EXT_texture_filter_anisotropic");n=t.getParameter(i.MAX_TEXTURE_MAX_ANISOTROPY_EXT)}else n=0;return n},getMaxPrecision:r,precision:a,logarithmicDepthBuffer:c,maxTextures:h,maxVertexTextures:u,maxTextureSize:d,maxCubemapSize:p,maxAttributes:m,maxVertexUniforms:f,maxVaryings:g,maxFragmentUniforms:v,vertexTextures:x,floatFragmentTextures:_,floatVertexTextures:x&&_,maxSamples:s?t.getParameter(36183):0}}function Rn(t){const e=this;let i=null,n=0,r=!1,s=!1;const a=new fn,o=new Rt,l={value:null,needsUpdate:!1};function c(){l.value!==i&&(l.value=i,l.needsUpdate=n>0),e.numPlanes=n,e.numIntersection=0}function h(t,i,n,r){const s=null!==t?t.length:0;let c=null;if(0!==s){if(c=l.value,!0!==r||null===c){const e=n+4*s,r=i.matrixWorldInverse;o.getNormalMatrix(r),(null===c||c.length0){const a=new un(s.height/2);return a.fromEquirectangularTexture(t,r),e.set(r,a),r.addEventListener("dispose",n),i(a.texture,r.mapping)}return null}}}return r},dispose:function(){e=new WeakMap}}}class In extends an{constructor(t=-1,e=1,i=1,n=-1,r=.1,s=2e3){super(),this.isOrthographicCamera=!0,this.type="OrthographicCamera",this.zoom=1,this.view=null,this.left=t,this.right=e,this.top=i,this.bottom=n,this.near=r,this.far=s,this.updateProjectionMatrix()}copy(t,e){return super.copy(t,e),this.left=t.left,this.right=t.right,this.top=t.top,this.bottom=t.bottom,this.near=t.near,this.far=t.far,this.zoom=t.zoom,this.view=null===t.view?null:Object.assign({},t.view),this}setViewOffset(t,e,i,n,r,s){null===this.view&&(this.view={enabled:!0,fullWidth:1,fullHeight:1,offsetX:0,offsetY:0,width:1,height:1}),this.view.enabled=!0,this.view.fullWidth=t,this.view.fullHeight=e,this.view.offsetX=i,this.view.offsetY=n,this.view.width=r,this.view.height=s,this.updateProjectionMatrix()}clearViewOffset(){null!==this.view&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){const t=(this.right-this.left)/(2*this.zoom),e=(this.top-this.bottom)/(2*this.zoom),i=(this.right+this.left)/2,n=(this.top+this.bottom)/2;let r=i-t,s=i+t,a=n+e,o=n-e;if(null!==this.view&&this.view.enabled){const t=(this.right-this.left)/this.view.fullWidth/this.zoom,e=(this.top-this.bottom)/this.view.fullHeight/this.zoom;r+=t*this.view.offsetX,s=r+t*this.view.width,a-=e*this.view.offsetY,o=a-e*this.view.height}this.projectionMatrix.makeOrthographic(r,s,a,o,this.near,this.far),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(t){const e=super.toJSON(t);return e.object.zoom=this.zoom,e.object.left=this.left,e.object.right=this.right,e.object.top=this.top,e.object.bottom=this.bottom,e.object.near=this.near,e.object.far=this.far,null!==this.view&&(e.object.view=Object.assign({},this.view)),e}}const Dn=[.125,.215,.35,.446,.526,.582],Nn=20,On=new In,zn=new qt;let Un=null;const Bn=(1+Math.sqrt(5))/2,Fn=1/Bn,kn=[new re(1,1,1),new re(-1,1,1),new re(1,1,-1),new re(-1,1,-1),new re(0,Bn,Fn),new re(0,Bn,-Fn),new re(Fn,0,Bn),new re(-Fn,0,Bn),new re(Bn,Fn,0),new re(-Bn,Fn,0)];class Gn{constructor(t){this._renderer=t,this._pingPongRenderTarget=null,this._lodMax=0,this._cubeSize=0,this._lodPlanes=[],this._sizeLods=[],this._sigmas=[],this._blurMaterial=null,this._cubemapMaterial=null,this._equirectMaterial=null,this._compileMaterial(this._blurMaterial)}fromScene(t,e=0,i=.1,n=100){Un=this._renderer.getRenderTarget(),this._setSize(256);const r=this._allocateTargets();return r.depthBuffer=!0,this._sceneToCubeUV(t,i,n,r),e>0&&this._blur(r,0,0,e),this._applyPMREM(r),this._cleanup(r),r}fromEquirectangular(t,e=null){return this._fromTexture(t,e)}fromCubemap(t,e=null){return this._fromTexture(t,e)}compileCubemapShader(){null===this._cubemapMaterial&&(this._cubemapMaterial=jn(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){null===this._equirectMaterial&&(this._equirectMaterial=Wn(),this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),null!==this._cubemapMaterial&&this._cubemapMaterial.dispose(),null!==this._equirectMaterial&&this._equirectMaterial.dispose()}_setSize(t){this._lodMax=Math.floor(Math.log2(t)),this._cubeSize=Math.pow(2,this._lodMax)}_dispose(){null!==this._blurMaterial&&this._blurMaterial.dispose(),null!==this._pingPongRenderTarget&&this._pingPongRenderTarget.dispose();for(let t=0;tt-4?o=Dn[a-t+4-1]:0===a&&(o=0),n.push(o);const l=1/(s-2),c=-l,h=1+l,u=[c,c,h,c,h,h,c,c,h,h,c,h],d=6,p=6,m=3,f=2,g=1,v=new Float32Array(m*p*d),x=new Float32Array(f*p*d),_=new Float32Array(g*p*d);for(let t=0;t2?0:-1,n=[e,i,0,e+2/3,i,0,e+2/3,i+1,0,e,i,0,e+2/3,i+1,0,e,i+1,0];v.set(n,m*p*t),x.set(u,f*p*t);const r=[t,t,t,t,t,t];_.set(r,g*p*t)}const y=new Di;y.setAttribute("position",new bi(v,m)),y.setAttribute("uv",new bi(x,f)),y.setAttribute("faceIndex",new bi(_,g)),e.push(y),r>4&&r--}return{lodPlanes:e,sizeLods:i,sigmas:n}}(n)),this._blurMaterial=function(t,e,i){const n=new Float32Array(Nn),r=new re(0,1,0);return new sn({name:"SphericalGaussianBlur",defines:{n:Nn,CUBEUV_TEXEL_WIDTH:1/e,CUBEUV_TEXEL_HEIGHT:1/i,CUBEUV_MAX_MIP:`${t}.0`},uniforms:{envMap:{value:null},samples:{value:1},weights:{value:n},latitudinal:{value:!1},dTheta:{value:0},mipInt:{value:0},poleAxis:{value:r}},vertexShader:qn(),fragmentShader:"\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform sampler2D envMap;\n\t\t\tuniform int samples;\n\t\t\tuniform float weights[ n ];\n\t\t\tuniform bool latitudinal;\n\t\t\tuniform float dTheta;\n\t\t\tuniform float mipInt;\n\t\t\tuniform vec3 poleAxis;\n\n\t\t\t#define ENVMAP_TYPE_CUBE_UV\n\t\t\t#include \n\n\t\t\tvec3 getSample( float theta, vec3 axis ) {\n\n\t\t\t\tfloat cosTheta = cos( theta );\n\t\t\t\t// Rodrigues' axis-angle rotation\n\t\t\t\tvec3 sampleDirection = vOutputDirection * cosTheta\n\t\t\t\t\t+ cross( axis, vOutputDirection ) * sin( theta )\n\t\t\t\t\t+ axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta );\n\n\t\t\t\treturn bilinearCubeUV( envMap, sampleDirection, mipInt );\n\n\t\t\t}\n\n\t\t\tvoid main() {\n\n\t\t\t\tvec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection );\n\n\t\t\t\tif ( all( equal( axis, vec3( 0.0 ) ) ) ) {\n\n\t\t\t\t\taxis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x );\n\n\t\t\t\t}\n\n\t\t\t\taxis = normalize( axis );\n\n\t\t\t\tgl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );\n\t\t\t\tgl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis );\n\n\t\t\t\tfor ( int i = 1; i < n; i++ ) {\n\n\t\t\t\t\tif ( i >= samples ) {\n\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tfloat theta = dTheta * float( i );\n\t\t\t\t\tgl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis );\n\t\t\t\t\tgl_FragColor.rgb += weights[ i ] * getSample( theta, axis );\n\n\t\t\t\t}\n\n\t\t\t}\n\t\t",blending:0,depthTest:!1,depthWrite:!1})}(n,t,e)}return n}_compileMaterial(t){const e=new Ki(this._lodPlanes[0],t);this._renderer.compile(e,On)}_sceneToCubeUV(t,e,i,n){const r=new on(90,1,e,i),s=[1,-1,1,1,1,1],a=[1,1,1,-1,-1,-1],o=this._renderer,l=o.autoClear,c=o.toneMapping;o.getClearColor(zn),o.toneMapping=0,o.autoClear=!1;const h=new _i({name:"PMREM.Background",side:1,depthWrite:!1,depthTest:!1}),u=new Ki(new Qi,h);let d=!1;const p=t.background;p?p.isColor&&(h.color.copy(p),t.background=null,d=!0):(h.color.copy(zn),d=!0);for(let e=0;e<6;e++){const i=e%3;0===i?(r.up.set(0,s[e],0),r.lookAt(a[e],0,0)):1===i?(r.up.set(0,0,s[e]),r.lookAt(0,a[e],0)):(r.up.set(0,s[e],0),r.lookAt(0,0,a[e]));const l=this._cubeSize;Hn(n,i*l,e>2?l:0,l,l),o.setRenderTarget(n),d&&o.render(u,r),o.render(t,r)}u.geometry.dispose(),u.material.dispose(),o.toneMapping=c,o.autoClear=l,t.background=p}_textureToCubeUV(t,e){const i=this._renderer,n=t.mapping===r||t.mapping===s;n?(null===this._cubemapMaterial&&(this._cubemapMaterial=jn()),this._cubemapMaterial.uniforms.flipEnvMap.value=!1===t.isRenderTargetTexture?-1:1):null===this._equirectMaterial&&(this._equirectMaterial=Wn());const a=n?this._cubemapMaterial:this._equirectMaterial,o=new Ki(this._lodPlanes[0],a);a.uniforms.envMap.value=t;const l=this._cubeSize;Hn(e,0,0,3*l,2*l),i.setRenderTarget(e),i.render(o,On)}_applyPMREM(t){const e=this._renderer,i=e.autoClear;e.autoClear=!1;for(let e=1;eNn&&console.warn(`sigmaRadians, ${r}, is too large and will clip, as it requested ${m} samples when the maximum is set to 20`);const f=[];let g=0;for(let t=0;tv-4?n-v+4:0),4*(this._cubeSize-x),3*x,2*x),o.setRenderTarget(e),o.render(c,On)}}function Vn(t,e,i){const n=new te(t,e,i);return n.texture.mapping=l,n.texture.name="PMREM.cubeUv",n.scissorTest=!0,n}function Hn(t,e,i,n,r){t.viewport.set(e,i,n,r),t.scissor.set(e,i,n,r)}function Wn(){return new sn({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null}},vertexShader:qn(),fragmentShader:"\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform sampler2D envMap;\n\n\t\t\t#include \n\n\t\t\tvoid main() {\n\n\t\t\t\tvec3 outputDirection = normalize( vOutputDirection );\n\t\t\t\tvec2 uv = equirectUv( outputDirection );\n\n\t\t\t\tgl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 );\n\n\t\t\t}\n\t\t",blending:0,depthTest:!1,depthWrite:!1})}function jn(){return new sn({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:qn(),fragmentShader:"\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tuniform float flipEnvMap;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform samplerCube envMap;\n\n\t\t\tvoid main() {\n\n\t\t\t\tgl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) );\n\n\t\t\t}\n\t\t",blending:0,depthTest:!1,depthWrite:!1})}function qn(){return"\n\n\t\tprecision mediump float;\n\t\tprecision mediump int;\n\n\t\tattribute float faceIndex;\n\n\t\tvarying vec3 vOutputDirection;\n\n\t\t// RH coordinate system; PMREM face-indexing convention\n\t\tvec3 getDirection( vec2 uv, float face ) {\n\n\t\t\tuv = 2.0 * uv - 1.0;\n\n\t\t\tvec3 direction = vec3( uv, 1.0 );\n\n\t\t\tif ( face == 0.0 ) {\n\n\t\t\t\tdirection = direction.zyx; // ( 1, v, u ) pos x\n\n\t\t\t} else if ( face == 1.0 ) {\n\n\t\t\t\tdirection = direction.xzy;\n\t\t\t\tdirection.xz *= -1.0; // ( -u, 1, -v ) pos y\n\n\t\t\t} else if ( face == 2.0 ) {\n\n\t\t\t\tdirection.x *= -1.0; // ( -u, v, 1 ) pos z\n\n\t\t\t} else if ( face == 3.0 ) {\n\n\t\t\t\tdirection = direction.zyx;\n\t\t\t\tdirection.xz *= -1.0; // ( -1, v, -u ) neg x\n\n\t\t\t} else if ( face == 4.0 ) {\n\n\t\t\t\tdirection = direction.xzy;\n\t\t\t\tdirection.xy *= -1.0; // ( -u, -1, v ) neg y\n\n\t\t\t} else if ( face == 5.0 ) {\n\n\t\t\t\tdirection.z *= -1.0; // ( u, v, -1 ) neg z\n\n\t\t\t}\n\n\t\t\treturn direction;\n\n\t\t}\n\n\t\tvoid main() {\n\n\t\t\tvOutputDirection = getDirection( uv, faceIndex );\n\t\t\tgl_Position = vec4( position, 1.0 );\n\n\t\t}\n\t"}function Xn(t){let e=new WeakMap,i=null;function n(t){const i=t.target;i.removeEventListener("dispose",n);const r=e.get(i);void 0!==r&&(e.delete(i),r.dispose())}return{get:function(l){if(l&&l.isTexture){const c=l.mapping,h=c===a||c===o,u=c===r||c===s;if(h||u){if(l.isRenderTargetTexture&&!0===l.needsPMREMUpdate){l.needsPMREMUpdate=!1;let n=e.get(l);return null===i&&(i=new Gn(t)),n=h?i.fromEquirectangular(l,n):i.fromCubemap(l,n),e.set(l,n),n.texture}if(e.has(l))return e.get(l).texture;{const r=l.image;if(h&&r&&r.height>0||u&&r&&function(t){let e=0;const i=6;for(let n=0;ne.maxTextureSize&&(E=Math.ceil(A/e.maxTextureSize),A=e.maxTextureSize);const C=new Float32Array(A*E*4*m),L=new ee(C,A,E,m);L.type=M,L.needsUpdate=!0;const R=4*T;for(let I=0;I0)return t;const r=e*i;let s=ar[r];if(void 0===s&&(s=new Float32Array(r),ar[r]=s),0!==e){n.toArray(s,0);for(let n=1,r=0;n!==e;++n)r+=i,t[n].toArray(s,r)}return s}function dr(t,e){if(t.length!==e.length)return!1;for(let i=0,n=t.length;i":" "} ${r}: ${i[t]}`)}return n.join("\n")}(t.getShaderSource(e),n)}return r}function cs(t,e){const i=function(t){switch(t){case at:return["Linear","( value )"];case ot:return["sRGB","( value )"];default:return console.warn("THREE.WebGLProgram: Unsupported encoding:",t),["Linear","( value )"]}}(e);return"vec4 "+t+"( vec4 value ) { return LinearTo"+i[0]+i[1]+"; }"}function hs(t,e){let i;switch(e){case 1:i="Linear";break;case 2:i="Reinhard";break;case 3:i="OptimizedCineon";break;case 4:i="ACESFilmic";break;case 5:i="Custom";break;default:console.warn("THREE.WebGLProgram: Unsupported toneMapping:",e),i="Linear"}return"vec3 "+t+"( vec3 color ) { return "+i+"ToneMapping( color ); }"}function us(t){return""!==t}function ds(t,e){const i=e.numSpotLightShadows+e.numSpotLightMaps-e.numSpotLightShadowsWithMaps;return t.replace(/NUM_DIR_LIGHTS/g,e.numDirLights).replace(/NUM_SPOT_LIGHTS/g,e.numSpotLights).replace(/NUM_SPOT_LIGHT_MAPS/g,e.numSpotLightMaps).replace(/NUM_SPOT_LIGHT_COORDS/g,i).replace(/NUM_RECT_AREA_LIGHTS/g,e.numRectAreaLights).replace(/NUM_POINT_LIGHTS/g,e.numPointLights).replace(/NUM_HEMI_LIGHTS/g,e.numHemiLights).replace(/NUM_DIR_LIGHT_SHADOWS/g,e.numDirLightShadows).replace(/NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS/g,e.numSpotLightShadowsWithMaps).replace(/NUM_SPOT_LIGHT_SHADOWS/g,e.numSpotLightShadows).replace(/NUM_POINT_LIGHT_SHADOWS/g,e.numPointLightShadows)}function ps(t,e){return t.replace(/NUM_CLIPPING_PLANES/g,e.numClippingPlanes).replace(/UNION_CLIPPING_PLANES/g,e.numClippingPlanes-e.numClipIntersection)}const ms=/^[ \t]*#include +<([\w\d./]+)>/gm;function fs(t){return t.replace(ms,gs)}function gs(t,e){const i=bn[e];if(void 0===i)throw new Error("Can not resolve #include <"+e+">");return fs(i)}const vs=/#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;function xs(t){return t.replace(vs,_s)}function _s(t,e,i,n){let r="";for(let t=parseInt(e);t0&&(_+="\n"),y=[g,v].filter(us).join("\n"),y.length>0&&(y+="\n")):(_=[ys(i),"#define SHADER_NAME "+i.shaderName,v,i.instancing?"#define USE_INSTANCING":"",i.instancingColor?"#define USE_INSTANCING_COLOR":"",i.supportsVertexTextures?"#define VERTEX_TEXTURES":"",i.useFog&&i.fog?"#define USE_FOG":"",i.useFog&&i.fogExp2?"#define FOG_EXP2":"",i.map?"#define USE_MAP":"",i.envMap?"#define USE_ENVMAP":"",i.envMap?"#define "+p:"",i.lightMap?"#define USE_LIGHTMAP":"",i.aoMap?"#define USE_AOMAP":"",i.emissiveMap?"#define USE_EMISSIVEMAP":"",i.bumpMap?"#define USE_BUMPMAP":"",i.normalMap?"#define USE_NORMALMAP":"",i.normalMap&&i.objectSpaceNormalMap?"#define OBJECTSPACE_NORMALMAP":"",i.normalMap&&i.tangentSpaceNormalMap?"#define TANGENTSPACE_NORMALMAP":"",i.clearcoatMap?"#define USE_CLEARCOATMAP":"",i.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",i.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",i.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",i.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",i.displacementMap&&i.supportsVertexTextures?"#define USE_DISPLACEMENTMAP":"",i.specularMap?"#define USE_SPECULARMAP":"",i.specularIntensityMap?"#define USE_SPECULARINTENSITYMAP":"",i.specularColorMap?"#define USE_SPECULARCOLORMAP":"",i.roughnessMap?"#define USE_ROUGHNESSMAP":"",i.metalnessMap?"#define USE_METALNESSMAP":"",i.alphaMap?"#define USE_ALPHAMAP":"",i.transmission?"#define USE_TRANSMISSION":"",i.transmissionMap?"#define USE_TRANSMISSIONMAP":"",i.thicknessMap?"#define USE_THICKNESSMAP":"",i.sheenColorMap?"#define USE_SHEENCOLORMAP":"",i.sheenRoughnessMap?"#define USE_SHEENROUGHNESSMAP":"",i.vertexTangents?"#define USE_TANGENT":"",i.vertexColors?"#define USE_COLOR":"",i.vertexAlphas?"#define USE_COLOR_ALPHA":"",i.vertexUvs?"#define USE_UV":"",i.uvsVertexOnly?"#define UVS_VERTEX_ONLY":"",i.flatShading?"#define FLAT_SHADED":"",i.skinning?"#define USE_SKINNING":"",i.morphTargets?"#define USE_MORPHTARGETS":"",i.morphNormals&&!1===i.flatShading?"#define USE_MORPHNORMALS":"",i.morphColors&&i.isWebGL2?"#define USE_MORPHCOLORS":"",i.morphTargetsCount>0&&i.isWebGL2?"#define MORPHTARGETS_TEXTURE":"",i.morphTargetsCount>0&&i.isWebGL2?"#define MORPHTARGETS_TEXTURE_STRIDE "+i.morphTextureStride:"",i.morphTargetsCount>0&&i.isWebGL2?"#define MORPHTARGETS_COUNT "+i.morphTargetsCount:"",i.doubleSided?"#define DOUBLE_SIDED":"",i.flipSided?"#define FLIP_SIDED":"",i.shadowMapEnabled?"#define USE_SHADOWMAP":"",i.shadowMapEnabled?"#define "+u:"",i.sizeAttenuation?"#define USE_SIZEATTENUATION":"",i.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",i.logarithmicDepthBuffer&&i.rendererExtensionFragDepth?"#define USE_LOGDEPTHBUF_EXT":"","uniform mat4 modelMatrix;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform mat4 viewMatrix;","uniform mat3 normalMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;","#ifdef USE_INSTANCING","\tattribute mat4 instanceMatrix;","#endif","#ifdef USE_INSTANCING_COLOR","\tattribute vec3 instanceColor;","#endif","attribute vec3 position;","attribute vec3 normal;","attribute vec2 uv;","#ifdef USE_TANGENT","\tattribute vec4 tangent;","#endif","#if defined( USE_COLOR_ALPHA )","\tattribute vec4 color;","#elif defined( USE_COLOR )","\tattribute vec3 color;","#endif","#if ( defined( USE_MORPHTARGETS ) && ! defined( MORPHTARGETS_TEXTURE ) )","\tattribute vec3 morphTarget0;","\tattribute vec3 morphTarget1;","\tattribute vec3 morphTarget2;","\tattribute vec3 morphTarget3;","\t#ifdef USE_MORPHNORMALS","\t\tattribute vec3 morphNormal0;","\t\tattribute vec3 morphNormal1;","\t\tattribute vec3 morphNormal2;","\t\tattribute vec3 morphNormal3;","\t#else","\t\tattribute vec3 morphTarget4;","\t\tattribute vec3 morphTarget5;","\t\tattribute vec3 morphTarget6;","\t\tattribute vec3 morphTarget7;","\t#endif","#endif","#ifdef USE_SKINNING","\tattribute vec4 skinIndex;","\tattribute vec4 skinWeight;","#endif","\n"].filter(us).join("\n"),y=[g,ys(i),"#define SHADER_NAME "+i.shaderName,v,i.useFog&&i.fog?"#define USE_FOG":"",i.useFog&&i.fogExp2?"#define FOG_EXP2":"",i.map?"#define USE_MAP":"",i.matcap?"#define USE_MATCAP":"",i.envMap?"#define USE_ENVMAP":"",i.envMap?"#define "+d:"",i.envMap?"#define "+p:"",i.envMap?"#define "+m:"",f?"#define CUBEUV_TEXEL_WIDTH "+f.texelWidth:"",f?"#define CUBEUV_TEXEL_HEIGHT "+f.texelHeight:"",f?"#define CUBEUV_MAX_MIP "+f.maxMip+".0":"",i.lightMap?"#define USE_LIGHTMAP":"",i.aoMap?"#define USE_AOMAP":"",i.emissiveMap?"#define USE_EMISSIVEMAP":"",i.bumpMap?"#define USE_BUMPMAP":"",i.normalMap?"#define USE_NORMALMAP":"",i.normalMap&&i.objectSpaceNormalMap?"#define OBJECTSPACE_NORMALMAP":"",i.normalMap&&i.tangentSpaceNormalMap?"#define TANGENTSPACE_NORMALMAP":"",i.clearcoat?"#define USE_CLEARCOAT":"",i.clearcoatMap?"#define USE_CLEARCOATMAP":"",i.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",i.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",i.iridescence?"#define USE_IRIDESCENCE":"",i.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",i.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",i.specularMap?"#define USE_SPECULARMAP":"",i.specularIntensityMap?"#define USE_SPECULARINTENSITYMAP":"",i.specularColorMap?"#define USE_SPECULARCOLORMAP":"",i.roughnessMap?"#define USE_ROUGHNESSMAP":"",i.metalnessMap?"#define USE_METALNESSMAP":"",i.alphaMap?"#define USE_ALPHAMAP":"",i.alphaTest?"#define USE_ALPHATEST":"",i.sheen?"#define USE_SHEEN":"",i.sheenColorMap?"#define USE_SHEENCOLORMAP":"",i.sheenRoughnessMap?"#define USE_SHEENROUGHNESSMAP":"",i.transmission?"#define USE_TRANSMISSION":"",i.transmissionMap?"#define USE_TRANSMISSIONMAP":"",i.thicknessMap?"#define USE_THICKNESSMAP":"",i.decodeVideoTexture?"#define DECODE_VIDEO_TEXTURE":"",i.vertexTangents?"#define USE_TANGENT":"",i.vertexColors||i.instancingColor?"#define USE_COLOR":"",i.vertexAlphas?"#define USE_COLOR_ALPHA":"",i.vertexUvs?"#define USE_UV":"",i.uvsVertexOnly?"#define UVS_VERTEX_ONLY":"",i.gradientMap?"#define USE_GRADIENTMAP":"",i.flatShading?"#define FLAT_SHADED":"",i.doubleSided?"#define DOUBLE_SIDED":"",i.flipSided?"#define FLIP_SIDED":"",i.shadowMapEnabled?"#define USE_SHADOWMAP":"",i.shadowMapEnabled?"#define "+u:"",i.premultipliedAlpha?"#define PREMULTIPLIED_ALPHA":"",i.physicallyCorrectLights?"#define PHYSICALLY_CORRECT_LIGHTS":"",i.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",i.logarithmicDepthBuffer&&i.rendererExtensionFragDepth?"#define USE_LOGDEPTHBUF_EXT":"","uniform mat4 viewMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;",0!==i.toneMapping?"#define TONE_MAPPING":"",0!==i.toneMapping?bn.tonemapping_pars_fragment:"",0!==i.toneMapping?hs("toneMapping",i.toneMapping):"",i.dithering?"#define DITHERING":"",i.opaque?"#define OPAQUE":"",bn.encodings_pars_fragment,cs("linearToOutputTexel",i.outputEncoding),i.useDepthPacking?"#define DEPTH_PACKING "+i.depthPacking:"","\n"].filter(us).join("\n")),c=fs(c),c=ds(c,i),c=ps(c,i),h=fs(h),h=ds(h,i),h=ps(h,i),c=xs(c),h=xs(h),i.isWebGL2&&!0!==i.isRawShaderMaterial&&(M="#version 300 es\n",_=["precision mediump sampler2DArray;","#define attribute in","#define varying out","#define texture2D texture"].join("\n")+"\n"+_,y=["#define varying in",i.glslVersion===dt?"":"layout(location = 0) out highp vec4 pc_fragColor;",i.glslVersion===dt?"":"#define gl_FragColor pc_fragColor","#define gl_FragDepthEXT gl_FragDepth","#define texture2D texture","#define textureCube texture","#define texture2DProj textureProj","#define texture2DLodEXT textureLod","#define texture2DProjLodEXT textureProjLod","#define textureCubeLodEXT textureLod","#define texture2DGradEXT textureGrad","#define texture2DProjGradEXT textureProjGrad","#define textureCubeGradEXT textureGrad"].join("\n")+"\n"+y);const b=M+y+h,S=as(a,35633,M+_+c),w=as(a,35632,b);if(a.attachShader(x,S),a.attachShader(x,w),void 0!==i.index0AttributeName?a.bindAttribLocation(x,0,i.index0AttributeName):!0===i.morphTargets&&a.bindAttribLocation(x,0,"position"),a.linkProgram(x),t.debug.checkShaderErrors){const t=a.getProgramInfoLog(x).trim(),e=a.getShaderInfoLog(S).trim(),i=a.getShaderInfoLog(w).trim();let n=!0,r=!0;if(!1===a.getProgramParameter(x,35714)){n=!1;const e=ls(a,S,"vertex"),i=ls(a,w,"fragment");console.error("THREE.WebGLProgram: Shader Error "+a.getError()+" - VALIDATE_STATUS "+a.getProgramParameter(x,35715)+"\n\nProgram Info Log: "+t+"\n"+e+"\n"+i)}else""!==t?console.warn("THREE.WebGLProgram: Program Info Log:",t):""!==e&&""!==i||(r=!1);r&&(this.diagnostics={runnable:n,programLog:t,vertexShader:{log:e,prefix:_},fragmentShader:{log:i,prefix:y}})}let T,A;return a.deleteShader(S),a.deleteShader(w),this.getUniforms=function(){return void 0===T&&(T=new ss(a,x)),T},this.getAttributes=function(){return void 0===A&&(A=function(t,e){const i={},n=t.getProgramParameter(e,35721);for(let r=0;r0,D=s.clearcoat>0,N=s.iridescence>0;return{isWebGL2:u,shaderID:S,shaderName:s.type,vertexShader:A,fragmentShader:E,defines:s.defines,customVertexShaderID:C,customFragmentShaderID:L,isRawShaderMaterial:!0===s.isRawShaderMaterial,glslVersion:s.glslVersion,precision:m,instancing:!0===v.isInstancedMesh,instancingColor:!0===v.isInstancedMesh&&null!==v.instanceColor,supportsVertexTextures:p,outputEncoding:null===P?t.outputEncoding:!0===P.isXRRenderTarget?P.texture.encoding:at,map:!!s.map,matcap:!!s.matcap,envMap:!!M,envMapMode:M&&M.mapping,envMapCubeUVHeight:b,lightMap:!!s.lightMap,aoMap:!!s.aoMap,emissiveMap:!!s.emissiveMap,bumpMap:!!s.bumpMap,normalMap:!!s.normalMap,objectSpaceNormalMap:1===s.normalMapType,tangentSpaceNormalMap:0===s.normalMapType,decodeVideoTexture:!!s.map&&!0===s.map.isVideoTexture&&s.map.encoding===ot,clearcoat:D,clearcoatMap:D&&!!s.clearcoatMap,clearcoatRoughnessMap:D&&!!s.clearcoatRoughnessMap,clearcoatNormalMap:D&&!!s.clearcoatNormalMap,iridescence:N,iridescenceMap:N&&!!s.iridescenceMap,iridescenceThicknessMap:N&&!!s.iridescenceThicknessMap,displacementMap:!!s.displacementMap,roughnessMap:!!s.roughnessMap,metalnessMap:!!s.metalnessMap,specularMap:!!s.specularMap,specularIntensityMap:!!s.specularIntensityMap,specularColorMap:!!s.specularColorMap,opaque:!1===s.transparent&&1===s.blending,alphaMap:!!s.alphaMap,alphaTest:I,gradientMap:!!s.gradientMap,sheen:s.sheen>0,sheenColorMap:!!s.sheenColorMap,sheenRoughnessMap:!!s.sheenRoughnessMap,transmission:s.transmission>0,transmissionMap:!!s.transmissionMap,thicknessMap:!!s.thicknessMap,combine:s.combine,vertexTangents:!!s.normalMap&&!!_.attributes.tangent,vertexColors:s.vertexColors,vertexAlphas:!0===s.vertexColors&&!!_.attributes.color&&4===_.attributes.color.itemSize,vertexUvs:!!(s.map||s.bumpMap||s.normalMap||s.specularMap||s.alphaMap||s.emissiveMap||s.roughnessMap||s.metalnessMap||s.clearcoatMap||s.clearcoatRoughnessMap||s.clearcoatNormalMap||s.iridescenceMap||s.iridescenceThicknessMap||s.displacementMap||s.transmissionMap||s.thicknessMap||s.specularIntensityMap||s.specularColorMap||s.sheenColorMap||s.sheenRoughnessMap),uvsVertexOnly:!(s.map||s.bumpMap||s.normalMap||s.specularMap||s.alphaMap||s.emissiveMap||s.roughnessMap||s.metalnessMap||s.clearcoatNormalMap||s.iridescenceMap||s.iridescenceThicknessMap||s.transmission>0||s.transmissionMap||s.thicknessMap||s.specularIntensityMap||s.specularColorMap||s.sheen>0||s.sheenColorMap||s.sheenRoughnessMap||!s.displacementMap),fog:!!x,useFog:!0===s.fog,fogExp2:x&&x.isFogExp2,flatShading:!!s.flatShading,sizeAttenuation:s.sizeAttenuation,logarithmicDepthBuffer:d,skinning:!0===v.isSkinnedMesh,morphTargets:void 0!==_.morphAttributes.position,morphNormals:void 0!==_.morphAttributes.normal,morphColors:void 0!==_.morphAttributes.color,morphTargetsCount:T,morphTextureStride:R,numDirLights:o.directional.length,numPointLights:o.point.length,numSpotLights:o.spot.length,numSpotLightMaps:o.spotLightMap.length,numRectAreaLights:o.rectArea.length,numHemiLights:o.hemi.length,numDirLightShadows:o.directionalShadowMap.length,numPointLightShadows:o.pointShadowMap.length,numSpotLightShadows:o.spotShadowMap.length,numSpotLightShadowsWithMaps:o.numSpotLightShadowsWithMaps,numClippingPlanes:a.numPlanes,numClipIntersection:a.numIntersection,dithering:s.dithering,shadowMapEnabled:t.shadowMap.enabled&&h.length>0,shadowMapType:t.shadowMap.type,toneMapping:s.toneMapped?t.toneMapping:0,physicallyCorrectLights:t.physicallyCorrectLights,premultipliedAlpha:s.premultipliedAlpha,doubleSided:2===s.side,flipSided:1===s.side,useDepthPacking:!!s.depthPacking,depthPacking:s.depthPacking||0,index0AttributeName:s.index0AttributeName,extensionDerivatives:s.extensions&&s.extensions.derivatives,extensionFragDepth:s.extensions&&s.extensions.fragDepth,extensionDrawBuffers:s.extensions&&s.extensions.drawBuffers,extensionShaderTextureLOD:s.extensions&&s.extensions.shaderTextureLOD,rendererExtensionFragDepth:u||n.has("EXT_frag_depth"),rendererExtensionDrawBuffers:u||n.has("WEBGL_draw_buffers"),rendererExtensionShaderTextureLod:u||n.has("EXT_shader_texture_lod"),customProgramCacheKey:s.customProgramCacheKey()}},getProgramCacheKey:function(e){const i=[];if(e.shaderID?i.push(e.shaderID):(i.push(e.customVertexShaderID),i.push(e.customFragmentShaderID)),void 0!==e.defines)for(const t in e.defines)i.push(t),i.push(e.defines[t]);return!1===e.isRawShaderMaterial&&(!function(t,e){t.push(e.precision),t.push(e.outputEncoding),t.push(e.envMapMode),t.push(e.envMapCubeUVHeight),t.push(e.combine),t.push(e.vertexUvs),t.push(e.fogExp2),t.push(e.sizeAttenuation),t.push(e.morphTargetsCount),t.push(e.morphAttributeCount),t.push(e.numDirLights),t.push(e.numPointLights),t.push(e.numSpotLights),t.push(e.numSpotLightMaps),t.push(e.numHemiLights),t.push(e.numRectAreaLights),t.push(e.numDirLightShadows),t.push(e.numPointLightShadows),t.push(e.numSpotLightShadows),t.push(e.numSpotLightShadowsWithMaps),t.push(e.shadowMapType),t.push(e.toneMapping),t.push(e.numClippingPlanes),t.push(e.numClipIntersection),t.push(e.depthPacking)}(i,e),function(t,e){o.disableAll(),e.isWebGL2&&o.enable(0);e.supportsVertexTextures&&o.enable(1);e.instancing&&o.enable(2);e.instancingColor&&o.enable(3);e.map&&o.enable(4);e.matcap&&o.enable(5);e.envMap&&o.enable(6);e.lightMap&&o.enable(7);e.aoMap&&o.enable(8);e.emissiveMap&&o.enable(9);e.bumpMap&&o.enable(10);e.normalMap&&o.enable(11);e.objectSpaceNormalMap&&o.enable(12);e.tangentSpaceNormalMap&&o.enable(13);e.clearcoat&&o.enable(14);e.clearcoatMap&&o.enable(15);e.clearcoatRoughnessMap&&o.enable(16);e.clearcoatNormalMap&&o.enable(17);e.iridescence&&o.enable(18);e.iridescenceMap&&o.enable(19);e.iridescenceThicknessMap&&o.enable(20);e.displacementMap&&o.enable(21);e.specularMap&&o.enable(22);e.roughnessMap&&o.enable(23);e.metalnessMap&&o.enable(24);e.gradientMap&&o.enable(25);e.alphaMap&&o.enable(26);e.alphaTest&&o.enable(27);e.vertexColors&&o.enable(28);e.vertexAlphas&&o.enable(29);e.vertexUvs&&o.enable(30);e.vertexTangents&&o.enable(31);e.uvsVertexOnly&&o.enable(32);t.push(o.mask),o.disableAll(),e.fog&&o.enable(0);e.useFog&&o.enable(1);e.flatShading&&o.enable(2);e.logarithmicDepthBuffer&&o.enable(3);e.skinning&&o.enable(4);e.morphTargets&&o.enable(5);e.morphNormals&&o.enable(6);e.morphColors&&o.enable(7);e.premultipliedAlpha&&o.enable(8);e.shadowMapEnabled&&o.enable(9);e.physicallyCorrectLights&&o.enable(10);e.doubleSided&&o.enable(11);e.flipSided&&o.enable(12);e.useDepthPacking&&o.enable(13);e.dithering&&o.enable(14);e.specularIntensityMap&&o.enable(15);e.specularColorMap&&o.enable(16);e.transmission&&o.enable(17);e.transmissionMap&&o.enable(18);e.thicknessMap&&o.enable(19);e.sheen&&o.enable(20);e.sheenColorMap&&o.enable(21);e.sheenRoughnessMap&&o.enable(22);e.decodeVideoTexture&&o.enable(23);e.opaque&&o.enable(24);t.push(o.mask)}(i,e),i.push(t.outputEncoding)),i.push(e.customProgramCacheKey),i.join()},getUniforms:function(t){const e=f[t.type];let i;if(e){const t=wn[e];i=rn.clone(t.uniforms)}else i=t.uniforms;return i},acquireProgram:function(e,i){let n;for(let t=0,e=h.length;t0?n.push(h):!0===a.transparent?r.push(h):i.push(h)},unshift:function(t,e,a,o,l,c){const h=s(t,e,a,o,l,c);a.transmission>0?n.unshift(h):!0===a.transparent?r.unshift(h):i.unshift(h)},finish:function(){for(let i=e,n=t.length;i1&&i.sort(t||Es),n.length>1&&n.sort(e||Cs),r.length>1&&r.sort(e||Cs)}}}function Rs(){let t=new WeakMap;return{get:function(e,i){const n=t.get(e);let r;return void 0===n?(r=new Ls,t.set(e,[r])):i>=n.length?(r=new Ls,n.push(r)):r=n[i],r},dispose:function(){t=new WeakMap}}}function Ps(){const t={};return{get:function(e){if(void 0!==t[e.id])return t[e.id];let i;switch(e.type){case"DirectionalLight":i={direction:new re,color:new qt};break;case"SpotLight":i={position:new re,direction:new re,color:new qt,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":i={position:new re,color:new qt,distance:0,decay:0};break;case"HemisphereLight":i={direction:new re,skyColor:new qt,groundColor:new qt};break;case"RectAreaLight":i={color:new qt,position:new re,halfWidth:new re,halfHeight:new re}}return t[e.id]=i,i}}}let Is=0;function Ds(t,e){return(e.castShadow?2:0)-(t.castShadow?2:0)+(e.map?1:0)-(t.map?1:0)}function Ns(t,e){const i=new Ps,n=function(){const t={};return{get:function(e){if(void 0!==t[e.id])return t[e.id];let i;switch(e.type){case"DirectionalLight":case"SpotLight":i={shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Lt};break;case"PointLight":i={shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Lt,shadowCameraNear:1,shadowCameraFar:1e3}}return t[e.id]=i,i}}}(),r={version:0,hash:{directionalLength:-1,pointLength:-1,spotLength:-1,rectAreaLength:-1,hemiLength:-1,numDirectionalShadows:-1,numPointShadows:-1,numSpotShadows:-1,numSpotMaps:-1},ambient:[0,0,0],probe:[],directional:[],directionalShadow:[],directionalShadowMap:[],directionalShadowMatrix:[],spot:[],spotLightMap:[],spotShadow:[],spotShadowMap:[],spotLightMatrix:[],rectArea:[],rectAreaLTC1:null,rectAreaLTC2:null,point:[],pointShadow:[],pointShadowMap:[],pointShadowMatrix:[],hemi:[],numSpotLightShadowsWithMaps:0};for(let t=0;t<9;t++)r.probe.push(new re);const s=new re,a=new Ne,o=new Ne;return{setup:function(s,a){let o=0,l=0,c=0;for(let t=0;t<9;t++)r.probe[t].set(0,0,0);let h=0,u=0,d=0,p=0,m=0,f=0,g=0,v=0,x=0,_=0;s.sort(Ds);const y=!0!==a?Math.PI:1;for(let t=0,e=s.length;t0&&(e.isWebGL2||!0===t.has("OES_texture_float_linear")?(r.rectAreaLTC1=Sn.LTC_FLOAT_1,r.rectAreaLTC2=Sn.LTC_FLOAT_2):!0===t.has("OES_texture_half_float_linear")?(r.rectAreaLTC1=Sn.LTC_HALF_1,r.rectAreaLTC2=Sn.LTC_HALF_2):console.error("THREE.WebGLRenderer: Unable to use RectAreaLight. Missing WebGL extensions.")),r.ambient[0]=o,r.ambient[1]=l,r.ambient[2]=c;const M=r.hash;M.directionalLength===h&&M.pointLength===u&&M.spotLength===d&&M.rectAreaLength===p&&M.hemiLength===m&&M.numDirectionalShadows===f&&M.numPointShadows===g&&M.numSpotShadows===v&&M.numSpotMaps===x||(r.directional.length=h,r.spot.length=d,r.rectArea.length=p,r.point.length=u,r.hemi.length=m,r.directionalShadow.length=f,r.directionalShadowMap.length=f,r.pointShadow.length=g,r.pointShadowMap.length=g,r.spotShadow.length=v,r.spotShadowMap.length=v,r.directionalShadowMatrix.length=f,r.pointShadowMatrix.length=g,r.spotLightMatrix.length=v+x-_,r.spotLightMap.length=x,r.numSpotLightShadowsWithMaps=_,M.directionalLength=h,M.pointLength=u,M.spotLength=d,M.rectAreaLength=p,M.hemiLength=m,M.numDirectionalShadows=f,M.numPointShadows=g,M.numSpotShadows=v,M.numSpotMaps=x,r.version=Is++)},setupView:function(t,e){let i=0,n=0,l=0,c=0,h=0;const u=e.matrixWorldInverse;for(let e=0,d=t.length;e=s.length?(a=new Os(t,e),s.push(a)):a=s[r],a},dispose:function(){i=new WeakMap}}}class Us extends xi{constructor(t){super(),this.isMeshDepthMaterial=!0,this.type="MeshDepthMaterial",this.depthPacking=3200,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.setValues(t)}copy(t){return super.copy(t),this.depthPacking=t.depthPacking,this.map=t.map,this.alphaMap=t.alphaMap,this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this}}class Bs extends xi{constructor(t){super(),this.isMeshDistanceMaterial=!0,this.type="MeshDistanceMaterial",this.referencePosition=new re,this.nearDistance=1,this.farDistance=1e3,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.setValues(t)}copy(t){return super.copy(t),this.referencePosition.copy(t.referencePosition),this.nearDistance=t.nearDistance,this.farDistance=t.farDistance,this.map=t.map,this.alphaMap=t.alphaMap,this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this}}function Fs(t,e,i){let n=new xn;const r=new Lt,s=new Lt,a=new Qt,o=new Us({depthPacking:3201}),l=new Bs,c={},h=i.maxTextureSize,u={0:1,1:0,2:2},p=new sn({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new Lt},radius:{value:4}},vertexShader:"void main() {\n\tgl_Position = vec4( position, 1.0 );\n}",fragmentShader:"uniform sampler2D shadow_pass;\nuniform vec2 resolution;\nuniform float radius;\n#include \nvoid main() {\n\tconst float samples = float( VSM_SAMPLES );\n\tfloat mean = 0.0;\n\tfloat squared_mean = 0.0;\n\tfloat uvStride = samples <= 1.0 ? 0.0 : 2.0 / ( samples - 1.0 );\n\tfloat uvStart = samples <= 1.0 ? 0.0 : - 1.0;\n\tfor ( float i = 0.0; i < samples; i ++ ) {\n\t\tfloat uvOffset = uvStart + i * uvStride;\n\t\t#ifdef HORIZONTAL_PASS\n\t\t\tvec2 distribution = unpackRGBATo2Half( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( uvOffset, 0.0 ) * radius ) / resolution ) );\n\t\t\tmean += distribution.x;\n\t\t\tsquared_mean += distribution.y * distribution.y + distribution.x * distribution.x;\n\t\t#else\n\t\t\tfloat depth = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, uvOffset ) * radius ) / resolution ) );\n\t\t\tmean += depth;\n\t\t\tsquared_mean += depth * depth;\n\t\t#endif\n\t}\n\tmean = mean / samples;\n\tsquared_mean = squared_mean / samples;\n\tfloat std_dev = sqrt( squared_mean - mean * mean );\n\tgl_FragColor = pack2HalfToRGBA( vec2( mean, std_dev ) );\n}"}),m=p.clone();m.defines.HORIZONTAL_PASS=1;const f=new Di;f.setAttribute("position",new bi(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));const g=new Ki(f,p),v=this;function x(i,n){const s=e.update(g);p.defines.VSM_SAMPLES!==i.blurSamples&&(p.defines.VSM_SAMPLES=i.blurSamples,m.defines.VSM_SAMPLES=i.blurSamples,p.needsUpdate=!0,m.needsUpdate=!0),null===i.mapPass&&(i.mapPass=new te(r.x,r.y)),p.uniforms.shadow_pass.value=i.map.texture,p.uniforms.resolution.value=i.mapSize,p.uniforms.radius.value=i.radius,t.setRenderTarget(i.mapPass),t.clear(),t.renderBufferDirect(n,null,s,p,g,null),m.uniforms.shadow_pass.value=i.mapPass.texture,m.uniforms.resolution.value=i.mapSize,m.uniforms.radius.value=i.radius,t.setRenderTarget(i.map),t.clear(),t.renderBufferDirect(n,null,s,m,g,null)}function _(e,i,n,r,s,a){let h=null;const d=!0===n.isPointLight?e.customDistanceMaterial:e.customDepthMaterial;if(h=void 0!==d?d:!0===n.isPointLight?l:o,t.localClippingEnabled&&!0===i.clipShadows&&Array.isArray(i.clippingPlanes)&&0!==i.clippingPlanes.length||i.displacementMap&&0!==i.displacementScale||i.alphaMap&&i.alphaTest>0||i.map&&i.alphaTest>0){const t=h.uuid,e=i.uuid;let n=c[t];void 0===n&&(n={},c[t]=n);let r=n[e];void 0===r&&(r=h.clone(),n[e]=r),h=r}return h.visible=i.visible,h.wireframe=i.wireframe,h.side=3===a?null!==i.shadowSide?i.shadowSide:i.side:null!==i.shadowSide?i.shadowSide:u[i.side],h.alphaMap=i.alphaMap,h.alphaTest=i.alphaTest,h.map=i.map,h.clipShadows=i.clipShadows,h.clippingPlanes=i.clippingPlanes,h.clipIntersection=i.clipIntersection,h.displacementMap=i.displacementMap,h.displacementScale=i.displacementScale,h.displacementBias=i.displacementBias,h.wireframeLinewidth=i.wireframeLinewidth,h.linewidth=i.linewidth,!0===n.isPointLight&&!0===h.isMeshDistanceMaterial&&(h.referencePosition.setFromMatrixPosition(n.matrixWorld),h.nearDistance=r,h.farDistance=s),h}function y(i,r,s,a,o){if(!1===i.visible)return;if(i.layers.test(r.layers)&&(i.isMesh||i.isLine||i.isPoints)&&(i.castShadow||i.receiveShadow&&3===o)&&(!i.frustumCulled||n.intersectsObject(i))){i.modelViewMatrix.multiplyMatrices(s.matrixWorldInverse,i.matrixWorld);const n=e.update(i),r=i.material;if(Array.isArray(r)){const e=n.groups;for(let l=0,c=e.length;lh||r.y>h)&&(r.x>h&&(s.x=Math.floor(h/m.x),r.x=s.x*m.x,u.mapSize.x=s.x),r.y>h&&(s.y=Math.floor(h/m.y),r.y=s.y*m.y,u.mapSize.y=s.y)),null===u.map){const t=3!==this.type?{minFilter:d,magFilter:d}:{};u.map=new te(r.x,r.y,t),u.map.texture.name=c.name+".shadowMap",u.camera.updateProjectionMatrix()}t.setRenderTarget(u.map),t.clear();const f=u.getViewportCount();for(let t=0;t=1):-1!==I.indexOf("OpenGL ES")&&(P=parseFloat(/^OpenGL ES (\d)/.exec(I)[1]),R=P>=2);let D=null,N={};const O=t.getParameter(3088),z=t.getParameter(2978),U=(new Qt).fromArray(O),B=(new Qt).fromArray(z);function F(e,i,n){const r=new Uint8Array(4),s=t.createTexture();t.bindTexture(e,s),t.texParameteri(e,10241,9728),t.texParameteri(e,10240,9728);for(let e=0;en||t.height>n)&&(r=n/Math.max(t.width,t.height)),r<1||!0===e){if("undefined"!=typeof HTMLImageElement&&t instanceof HTMLImageElement||"undefined"!=typeof HTMLCanvasElement&&t instanceof HTMLCanvasElement||"undefined"!=typeof ImageBitmap&&t instanceof ImageBitmap){const n=e?Tt:Math.floor,s=n(r*t.width),a=n(r*t.height);void 0===D&&(D=z(s,a));const o=i?z(s,a):D;o.width=s,o.height=a;return o.getContext("2d").drawImage(t,0,0,s,a),console.warn("THREE.WebGLRenderer: Texture has been resized from ("+t.width+"x"+t.height+") to ("+s+"x"+a+")."),o}return"data"in t&&console.warn("THREE.WebGLRenderer: Image in DataTexture is too big ("+t.width+"x"+t.height+")."),t}return t}function B(t){return St(t.width)&&St(t.height)}function F(t,e){return t.generateMipmaps&&e&&t.minFilter!==d&&t.minFilter!==f}function k(e){t.generateMipmap(e)}function G(i,n,r,s,a=!1){if(!1===o)return n;if(null!==i){if(void 0!==t[i])return t[i];console.warn("THREE.WebGLRenderer: Attempt to use non-existing WebGL internal format '"+i+"'")}let l=n;return 6403===n&&(5126===r&&(l=33326),5131===r&&(l=33325),5121===r&&(l=33321)),33319===n&&(5126===r&&(l=33328),5131===r&&(l=33327),5121===r&&(l=33323)),6408===n&&(5126===r&&(l=34836),5131===r&&(l=34842),5121===r&&(l=s===ot&&!1===a?35907:32856),32819===r&&(l=32854),32820===r&&(l=32855)),33325!==l&&33326!==l&&33327!==l&&33328!==l&&34842!==l&&34836!==l||e.get("EXT_color_buffer_float"),l}function V(t,e,i){return!0===F(t,i)||t.isFramebufferTexture&&t.minFilter!==d&&t.minFilter!==f?Math.log2(Math.max(e.width,e.height))+1:void 0!==t.mipmaps&&t.mipmaps.length>0?t.mipmaps.length:t.isCompressedTexture&&Array.isArray(t.image)?e.mipmaps.length:1}function H(t){return t===d||t===p||t===m?9728:9729}function W(t){const e=t.target;e.removeEventListener("dispose",W),function(t){const e=n.get(t);if(void 0===e.__webglInit)return;const i=t.source,r=N.get(i);if(r){const n=r[e.__cacheKey];n.usedTimes--,0===n.usedTimes&&q(t),0===Object.keys(r).length&&N.delete(i)}n.remove(t)}(e),e.isVideoTexture&&I.delete(e)}function j(e){const i=e.target;i.removeEventListener("dispose",j),function(e){const i=e.texture,r=n.get(e),s=n.get(i);void 0!==s.__webglTexture&&(t.deleteTexture(s.__webglTexture),a.memory.textures--);e.depthTexture&&e.depthTexture.dispose();if(e.isWebGLCubeRenderTarget)for(let e=0;e<6;e++)t.deleteFramebuffer(r.__webglFramebuffer[e]),r.__webglDepthbuffer&&t.deleteRenderbuffer(r.__webglDepthbuffer[e]);else{if(t.deleteFramebuffer(r.__webglFramebuffer),r.__webglDepthbuffer&&t.deleteRenderbuffer(r.__webglDepthbuffer),r.__webglMultisampledFramebuffer&&t.deleteFramebuffer(r.__webglMultisampledFramebuffer),r.__webglColorRenderbuffer)for(let e=0;e0&&r.__version!==t.version){const i=t.image;if(null===i)console.warn("THREE.WebGLRenderer: Texture marked for update but no image data found.");else{if(!1!==i.complete)return void Q(r,t,e);console.warn("THREE.WebGLRenderer: Texture marked for update but image is incomplete")}}i.bindTexture(3553,r.__webglTexture,33984+e)}const Z={[c]:10497,[h]:33071,[u]:33648},J={[d]:9728,[p]:9984,[m]:9986,[f]:9729,[g]:9985,[v]:9987};function K(i,s,a){if(a?(t.texParameteri(i,10242,Z[s.wrapS]),t.texParameteri(i,10243,Z[s.wrapT]),32879!==i&&35866!==i||t.texParameteri(i,32882,Z[s.wrapR]),t.texParameteri(i,10240,J[s.magFilter]),t.texParameteri(i,10241,J[s.minFilter])):(t.texParameteri(i,10242,33071),t.texParameteri(i,10243,33071),32879!==i&&35866!==i||t.texParameteri(i,32882,33071),s.wrapS===h&&s.wrapT===h||console.warn("THREE.WebGLRenderer: Texture is not power of two. Texture.wrapS and Texture.wrapT should be set to THREE.ClampToEdgeWrapping."),t.texParameteri(i,10240,H(s.magFilter)),t.texParameteri(i,10241,H(s.minFilter)),s.minFilter!==d&&s.minFilter!==f&&console.warn("THREE.WebGLRenderer: Texture is not power of two. Texture.minFilter should be set to THREE.NearestFilter or THREE.LinearFilter.")),!0===e.has("EXT_texture_filter_anisotropic")){const a=e.get("EXT_texture_filter_anisotropic");if(s.type===M&&!1===e.has("OES_texture_float_linear"))return;if(!1===o&&s.type===b&&!1===e.has("OES_texture_half_float_linear"))return;(s.anisotropy>1||n.get(s).__currentAnisotropy)&&(t.texParameterf(i,a.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(s.anisotropy,r.getMaxAnisotropy())),n.get(s).__currentAnisotropy=s.anisotropy)}}function $(e,i){let n=!1;void 0===e.__webglInit&&(e.__webglInit=!0,i.addEventListener("dispose",W));const r=i.source;let s=N.get(r);void 0===s&&(s={},N.set(r,s));const o=function(t){const e=[];return e.push(t.wrapS),e.push(t.wrapT),e.push(t.wrapR||0),e.push(t.magFilter),e.push(t.minFilter),e.push(t.anisotropy),e.push(t.internalFormat),e.push(t.format),e.push(t.type),e.push(t.generateMipmaps),e.push(t.premultiplyAlpha),e.push(t.flipY),e.push(t.unpackAlignment),e.push(t.encoding),e.join()}(i);if(o!==e.__cacheKey){void 0===s[o]&&(s[o]={texture:t.createTexture(),usedTimes:0},a.memory.textures++,n=!0),s[o].usedTimes++;const r=s[e.__cacheKey];void 0!==r&&(s[e.__cacheKey].usedTimes--,0===r.usedTimes&&q(i)),e.__cacheKey=o,e.__webglTexture=s[o].texture}return n}function Q(e,r,a){let l=3553;(r.isDataArrayTexture||r.isCompressedArrayTexture)&&(l=35866),r.isData3DTexture&&(l=32879);const c=$(e,r),u=r.source;i.bindTexture(l,e.__webglTexture,33984+a);const p=n.get(u);if(u.version!==p.__version||!0===c){i.activeTexture(33984+a),t.pixelStorei(37440,r.flipY),t.pixelStorei(37441,r.premultiplyAlpha),t.pixelStorei(3317,r.unpackAlignment),t.pixelStorei(37443,0);const e=function(t){return!o&&(t.wrapS!==h||t.wrapT!==h||t.minFilter!==d&&t.minFilter!==f)}(r)&&!1===B(r.image);let n=U(r.image,e,!1,C);n=st(r,n);const m=B(n)||o,g=s.convert(r.format,r.encoding);let v,x=s.convert(r.type),b=G(r.internalFormat,g,x,r.encoding,r.isVideoTexture);K(l,r,m);const E=r.mipmaps,L=o&&!0!==r.isVideoTexture,R=void 0===p.__version||!0===c,P=V(r,n,m);if(r.isDepthTexture)b=6402,o?b=r.type===M?36012:r.type===y?33190:r.type===S?35056:33189:r.type===M&&console.error("WebGLRenderer: Floating point depth texture requires WebGL2."),r.format===T&&6402===b&&r.type!==_&&r.type!==y&&(console.warn("THREE.WebGLRenderer: Use UnsignedShortType or UnsignedIntType for DepthFormat DepthTexture."),r.type=y,x=s.convert(r.type)),r.format===A&&6402===b&&(b=34041,r.type!==S&&(console.warn("THREE.WebGLRenderer: Use UnsignedInt248Type for DepthStencilFormat DepthTexture."),r.type=S,x=s.convert(r.type))),R&&(L?i.texStorage2D(3553,1,b,n.width,n.height):i.texImage2D(3553,0,b,n.width,n.height,0,g,x,null));else if(r.isDataTexture)if(E.length>0&&m){L&&R&&i.texStorage2D(3553,P,b,E[0].width,E[0].height);for(let t=0,e=E.length;t>=1,e>>=1}}else if(E.length>0&&m){L&&R&&i.texStorage2D(3553,P,b,E[0].width,E[0].height);for(let t=0,e=E.length;t=34069&&l<=34074)&&t.framebufferTexture2D(36160,o,l,n.get(a).__webglTexture,0),i.bindFramebuffer(36160,null)}function et(e,i,n){if(t.bindRenderbuffer(36161,e),i.depthBuffer&&!i.stencilBuffer){let r=33189;if(n||rt(i)){const e=i.depthTexture;e&&e.isDepthTexture&&(e.type===M?r=36012:e.type===y&&(r=33190));const n=nt(i);rt(i)?R.renderbufferStorageMultisampleEXT(36161,n,r,i.width,i.height):t.renderbufferStorageMultisample(36161,n,r,i.width,i.height)}else t.renderbufferStorage(36161,r,i.width,i.height);t.framebufferRenderbuffer(36160,36096,36161,e)}else if(i.depthBuffer&&i.stencilBuffer){const r=nt(i);n&&!1===rt(i)?t.renderbufferStorageMultisample(36161,r,35056,i.width,i.height):rt(i)?R.renderbufferStorageMultisampleEXT(36161,r,35056,i.width,i.height):t.renderbufferStorage(36161,34041,i.width,i.height),t.framebufferRenderbuffer(36160,33306,36161,e)}else{const e=!0===i.isWebGLMultipleRenderTargets?i.texture:[i.texture];for(let r=0;r0&&!0===e.has("WEBGL_multisampled_render_to_texture")&&!1!==i.__useRenderToTexture}function st(t,i){const n=t.encoding,r=t.format,s=t.type;return!0===t.isCompressedTexture||!0===t.isVideoTexture||t.format===pt||n!==at&&(n===ot?!1===o?!0===e.has("EXT_sRGB")&&r===w?(t.format=pt,t.minFilter=f,t.generateMipmaps=!1):i=Yt.sRGBToLinear(i):r===w&&s===x||console.warn("THREE.WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType."):console.error("THREE.WebGLTextures: Unsupported texture encoding:",n)),i}this.allocateTextureUnit=function(){const t=X;return t>=l&&console.warn("THREE.WebGLTextures: Trying to use "+t+" texture units while this GPU supports only "+l),X+=1,t},this.resetTextureUnits=function(){X=0},this.setTexture2D=Y,this.setTexture2DArray=function(t,e){const r=n.get(t);t.version>0&&r.__version!==t.version?Q(r,t,e):i.bindTexture(35866,r.__webglTexture,33984+e)},this.setTexture3D=function(t,e){const r=n.get(t);t.version>0&&r.__version!==t.version?Q(r,t,e):i.bindTexture(32879,r.__webglTexture,33984+e)},this.setTextureCube=function(e,r){const a=n.get(e);e.version>0&&a.__version!==e.version?function(e,r,a){if(6!==r.image.length)return;const l=$(e,r),c=r.source;i.bindTexture(34067,e.__webglTexture,33984+a);const h=n.get(c);if(c.version!==h.__version||!0===l){i.activeTexture(33984+a),t.pixelStorei(37440,r.flipY),t.pixelStorei(37441,r.premultiplyAlpha),t.pixelStorei(3317,r.unpackAlignment),t.pixelStorei(37443,0);const e=r.isCompressedTexture||r.image[0].isCompressedTexture,n=r.image[0]&&r.image[0].isDataTexture,u=[];for(let t=0;t<6;t++)u[t]=e||n?n?r.image[t].image:r.image[t]:U(r.image[t],!1,!0,E),u[t]=st(r,u[t]);const d=u[0],p=B(d)||o,m=s.convert(r.format,r.encoding),f=s.convert(r.type),g=G(r.internalFormat,m,f,r.encoding),v=o&&!0!==r.isVideoTexture,x=void 0===h.__version||!0===l;let _,y=V(r,d,p);if(K(34067,r,p),e){v&&x&&i.texStorage2D(34067,y,g,d.width,d.height);for(let t=0;t<6;t++){_=u[t].mipmaps;for(let e=0;e<_.length;e++){const n=_[e];r.format!==w?null!==m?v?i.compressedTexSubImage2D(34069+t,e,0,0,n.width,n.height,m,n.data):i.compressedTexImage2D(34069+t,e,g,n.width,n.height,0,n.data):console.warn("THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .setTextureCube()"):v?i.texSubImage2D(34069+t,e,0,0,n.width,n.height,m,f,n.data):i.texImage2D(34069+t,e,g,n.width,n.height,0,m,f,n.data)}}}else{_=r.mipmaps,v&&x&&(_.length>0&&y++,i.texStorage2D(34067,y,g,u[0].width,u[0].height));for(let t=0;t<6;t++)if(n){v?i.texSubImage2D(34069+t,0,0,0,u[t].width,u[t].height,m,f,u[t].data):i.texImage2D(34069+t,0,g,u[t].width,u[t].height,0,m,f,u[t].data);for(let e=0;e<_.length;e++){const n=_[e].image[t].image;v?i.texSubImage2D(34069+t,e+1,0,0,n.width,n.height,m,f,n.data):i.texImage2D(34069+t,e+1,g,n.width,n.height,0,m,f,n.data)}}else{v?i.texSubImage2D(34069+t,0,0,0,m,f,u[t]):i.texImage2D(34069+t,0,g,m,f,u[t]);for(let e=0;e<_.length;e++){const n=_[e];v?i.texSubImage2D(34069+t,e+1,0,0,m,f,n.image[t]):i.texImage2D(34069+t,e+1,g,m,f,n.image[t])}}}F(r,p)&&k(34067),h.__version=c.version,r.onUpdate&&r.onUpdate(r)}e.__version=r.version}(a,e,r):i.bindTexture(34067,a.__webglTexture,33984+r)},this.rebindTextures=function(t,e,i){const r=n.get(t);void 0!==e&&tt(r.__webglFramebuffer,t,t.texture,36064,3553),void 0!==i&&it(t)},this.setupRenderTarget=function(e){const l=e.texture,c=n.get(e),h=n.get(l);e.addEventListener("dispose",j),!0!==e.isWebGLMultipleRenderTargets&&(void 0===h.__webglTexture&&(h.__webglTexture=t.createTexture()),h.__version=l.version,a.memory.textures++);const u=!0===e.isWebGLCubeRenderTarget,d=!0===e.isWebGLMultipleRenderTargets,p=B(e)||o;if(u){c.__webglFramebuffer=[];for(let e=0;e<6;e++)c.__webglFramebuffer[e]=t.createFramebuffer()}else{if(c.__webglFramebuffer=t.createFramebuffer(),d)if(r.drawBuffers){const i=e.texture;for(let e=0,r=i.length;e0&&!1===rt(e)){const n=d?l:[l];c.__webglMultisampledFramebuffer=t.createFramebuffer(),c.__webglColorRenderbuffer=[],i.bindFramebuffer(36160,c.__webglMultisampledFramebuffer);for(let i=0;i0&&!1===rt(e)){const r=e.isWebGLMultipleRenderTargets?e.texture:[e.texture],s=e.width,a=e.height;let o=16384;const l=[],c=e.stencilBuffer?33306:36096,h=n.get(e),u=!0===e.isWebGLMultipleRenderTargets;if(u)for(let e=0;eo+c?(l.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:t.handedness,target:this})):!l.inputState.pinching&&a<=o-c&&(l.inputState.pinching=!0,this.dispatchEvent({type:"pinchstart",handedness:t.handedness,target:this}))}else null!==o&&t.gripSpace&&(r=e.getPose(t.gripSpace,i),null!==r&&(o.matrix.fromArray(r.transform.matrix),o.matrix.decompose(o.position,o.rotation,o.scale),r.linearVelocity?(o.hasLinearVelocity=!0,o.linearVelocity.copy(r.linearVelocity)):o.hasLinearVelocity=!1,r.angularVelocity?(o.hasAngularVelocity=!0,o.angularVelocity.copy(r.angularVelocity)):o.hasAngularVelocity=!1));null!==a&&(n=e.getPose(t.targetRaySpace,i),null===n&&null!==r&&(n=r),null!==n&&(a.matrix.fromArray(n.transform.matrix),a.matrix.decompose(a.position,a.rotation,a.scale),n.linearVelocity?(a.hasLinearVelocity=!0,a.linearVelocity.copy(n.linearVelocity)):a.hasLinearVelocity=!1,n.angularVelocity?(a.hasAngularVelocity=!0,a.angularVelocity.copy(n.angularVelocity)):a.hasAngularVelocity=!1,this.dispatchEvent(js)))}return null!==a&&(a.visible=null!==n),null!==o&&(o.visible=null!==r),null!==l&&(l.visible=null!==s),this}_getHandJoint(t,e){if(void 0===t.joints[e.jointName]){const i=new Ws;i.matrixAutoUpdate=!1,i.visible=!1,t.joints[e.jointName]=i,t.add(i)}return t.joints[e.jointName]}}class Xs extends $t{constructor(t,e,i,n,r,s,a,o,l,c){if((c=void 0!==c?c:T)!==T&&c!==A)throw new Error("DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat");void 0===i&&c===T&&(i=y),void 0===i&&c===A&&(i=S),super(null,n,r,s,a,o,c,i,l),this.isDepthTexture=!0,this.image={width:t,height:e},this.magFilter=void 0!==a?a:d,this.minFilter=void 0!==o?o:d,this.flipY=!1,this.generateMipmaps=!1}}class Ys extends mt{constructor(t,e){super();const i=this;let n=null,r=1,s=null,a="local-floor",o=null,l=null,c=null,h=null,u=null,d=null;const p=e.getContextAttributes();let m=null,f=null;const g=[],v=[],_=new Set,M=new Map,b=new on;b.layers.enable(1),b.viewport=new Qt;const E=new on;E.layers.enable(2),E.viewport=new Qt;const C=[b,E],L=new Hs;L.layers.enable(1),L.layers.enable(2);let R=null,P=null;function I(t){const e=v.indexOf(t.inputSource);if(-1===e)return;const i=g[e];void 0!==i&&i.dispatchEvent({type:t.type,data:t.inputSource})}function D(){n.removeEventListener("select",I),n.removeEventListener("selectstart",I),n.removeEventListener("selectend",I),n.removeEventListener("squeeze",I),n.removeEventListener("squeezestart",I),n.removeEventListener("squeezeend",I),n.removeEventListener("end",D),n.removeEventListener("inputsourceschange",N);for(let t=0;t=0&&(v[n]=null,g[n].disconnect(i))}for(let e=0;e=v.length){v.push(i),n=t;break}if(null===v[t]){v[t]=i,n=t;break}}if(-1===n)break}const r=g[n];r&&r.connect(i)}}this.cameraAutoUpdate=!0,this.enabled=!1,this.isPresenting=!1,this.getController=function(t){let e=g[t];return void 0===e&&(e=new qs,g[t]=e),e.getTargetRaySpace()},this.getControllerGrip=function(t){let e=g[t];return void 0===e&&(e=new qs,g[t]=e),e.getGripSpace()},this.getHand=function(t){let e=g[t];return void 0===e&&(e=new qs,g[t]=e),e.getHandSpace()},this.setFramebufferScaleFactor=function(t){r=t,!0===i.isPresenting&&console.warn("THREE.WebXRManager: Cannot change framebuffer scale while presenting.")},this.setReferenceSpaceType=function(t){a=t,!0===i.isPresenting&&console.warn("THREE.WebXRManager: Cannot change reference space type while presenting.")},this.getReferenceSpace=function(){return o||s},this.setReferenceSpace=function(t){o=t},this.getBaseLayer=function(){return null!==h?h:u},this.getBinding=function(){return c},this.getFrame=function(){return d},this.getSession=function(){return n},this.setSession=async function(l){if(n=l,null!==n){if(m=t.getRenderTarget(),n.addEventListener("select",I),n.addEventListener("selectstart",I),n.addEventListener("selectend",I),n.addEventListener("squeeze",I),n.addEventListener("squeezestart",I),n.addEventListener("squeezeend",I),n.addEventListener("end",D),n.addEventListener("inputsourceschange",N),!0!==p.xrCompatible&&await e.makeXRCompatible(),void 0===n.renderState.layers||!1===t.capabilities.isWebGL2){const i={antialias:void 0!==n.renderState.layers||p.antialias,alpha:p.alpha,depth:p.depth,stencil:p.stencil,framebufferScaleFactor:r};u=new XRWebGLLayer(n,e,i),n.updateRenderState({baseLayer:u}),f=new te(u.framebufferWidth,u.framebufferHeight,{format:w,type:x,encoding:t.outputEncoding,stencilBuffer:p.stencil})}else{let i=null,s=null,a=null;p.depth&&(a=p.stencil?35056:33190,i=p.stencil?A:T,s=p.stencil?S:y);const o={colorFormat:32856,depthFormat:a,scaleFactor:r};c=new XRWebGLBinding(n,e),h=c.createProjectionLayer(o),n.updateRenderState({layers:[h]}),f=new te(h.textureWidth,h.textureHeight,{format:w,type:x,depthTexture:new Xs(h.textureWidth,h.textureHeight,s,void 0,void 0,void 0,void 0,void 0,void 0,i),stencilBuffer:p.stencil,encoding:t.outputEncoding,samples:p.antialias?4:0});t.properties.get(f).__ignoreDepthValues=h.ignoreDepthValues}f.isXRRenderTarget=!0,this.setFoveation(1),o=null,s=await n.requestReferenceSpace(a),F.setContext(n),F.start(),i.isPresenting=!0,i.dispatchEvent({type:"sessionstart"})}};const O=new re,z=new re;function U(t,e){null===e?t.matrixWorld.copy(t.matrix):t.matrixWorld.multiplyMatrices(e.matrixWorld,t.matrix),t.matrixWorldInverse.copy(t.matrixWorld).invert()}this.updateCamera=function(t){if(null===n)return;L.near=E.near=b.near=t.near,L.far=E.far=b.far=t.far,R===L.near&&P===L.far||(n.updateRenderState({depthNear:L.near,depthFar:L.far}),R=L.near,P=L.far);const e=t.parent,i=L.cameras;U(L,e);for(let t=0;te&&(M.set(t,t.lastChangedTime),i.dispatchEvent({type:"planechanged",data:t}))}else _.add(t),M.set(t,n.lastChangedTime),i.dispatchEvent({type:"planeadded",data:t})}d=null})),this.setAnimationLoop=function(t){B=t},this.dispose=function(){}}}function Zs(t,e){function i(i,n){i.opacity.value=n.opacity,n.color&&i.diffuse.value.copy(n.color),n.emissive&&i.emissive.value.copy(n.emissive).multiplyScalar(n.emissiveIntensity),n.map&&(i.map.value=n.map),n.alphaMap&&(i.alphaMap.value=n.alphaMap),n.bumpMap&&(i.bumpMap.value=n.bumpMap,i.bumpScale.value=n.bumpScale,1===n.side&&(i.bumpScale.value*=-1)),n.displacementMap&&(i.displacementMap.value=n.displacementMap,i.displacementScale.value=n.displacementScale,i.displacementBias.value=n.displacementBias),n.emissiveMap&&(i.emissiveMap.value=n.emissiveMap),n.normalMap&&(i.normalMap.value=n.normalMap,i.normalScale.value.copy(n.normalScale),1===n.side&&i.normalScale.value.negate()),n.specularMap&&(i.specularMap.value=n.specularMap),n.alphaTest>0&&(i.alphaTest.value=n.alphaTest);const r=e.get(n).envMap;if(r&&(i.envMap.value=r,i.flipEnvMap.value=r.isCubeTexture&&!1===r.isRenderTargetTexture?-1:1,i.reflectivity.value=n.reflectivity,i.ior.value=n.ior,i.refractionRatio.value=n.refractionRatio),n.lightMap){i.lightMap.value=n.lightMap;const e=!0!==t.physicallyCorrectLights?Math.PI:1;i.lightMapIntensity.value=n.lightMapIntensity*e}let s,a;n.aoMap&&(i.aoMap.value=n.aoMap,i.aoMapIntensity.value=n.aoMapIntensity),n.map?s=n.map:n.specularMap?s=n.specularMap:n.displacementMap?s=n.displacementMap:n.normalMap?s=n.normalMap:n.bumpMap?s=n.bumpMap:n.roughnessMap?s=n.roughnessMap:n.metalnessMap?s=n.metalnessMap:n.alphaMap?s=n.alphaMap:n.emissiveMap?s=n.emissiveMap:n.clearcoatMap?s=n.clearcoatMap:n.clearcoatNormalMap?s=n.clearcoatNormalMap:n.clearcoatRoughnessMap?s=n.clearcoatRoughnessMap:n.iridescenceMap?s=n.iridescenceMap:n.iridescenceThicknessMap?s=n.iridescenceThicknessMap:n.specularIntensityMap?s=n.specularIntensityMap:n.specularColorMap?s=n.specularColorMap:n.transmissionMap?s=n.transmissionMap:n.thicknessMap?s=n.thicknessMap:n.sheenColorMap?s=n.sheenColorMap:n.sheenRoughnessMap&&(s=n.sheenRoughnessMap),void 0!==s&&(s.isWebGLRenderTarget&&(s=s.texture),!0===s.matrixAutoUpdate&&s.updateMatrix(),i.uvTransform.value.copy(s.matrix)),n.aoMap?a=n.aoMap:n.lightMap&&(a=n.lightMap),void 0!==a&&(a.isWebGLRenderTarget&&(a=a.texture),!0===a.matrixAutoUpdate&&a.updateMatrix(),i.uv2Transform.value.copy(a.matrix))}return{refreshFogUniforms:function(e,i){i.color.getRGB(e.fogColor.value,nn(t)),i.isFog?(e.fogNear.value=i.near,e.fogFar.value=i.far):i.isFogExp2&&(e.fogDensity.value=i.density)},refreshMaterialUniforms:function(t,n,r,s,a){n.isMeshBasicMaterial||n.isMeshLambertMaterial?i(t,n):n.isMeshToonMaterial?(i(t,n),function(t,e){e.gradientMap&&(t.gradientMap.value=e.gradientMap)}(t,n)):n.isMeshPhongMaterial?(i(t,n),function(t,e){t.specular.value.copy(e.specular),t.shininess.value=Math.max(e.shininess,1e-4)}(t,n)):n.isMeshStandardMaterial?(i(t,n),function(t,i){t.roughness.value=i.roughness,t.metalness.value=i.metalness,i.roughnessMap&&(t.roughnessMap.value=i.roughnessMap);i.metalnessMap&&(t.metalnessMap.value=i.metalnessMap);e.get(i).envMap&&(t.envMapIntensity.value=i.envMapIntensity)}(t,n),n.isMeshPhysicalMaterial&&function(t,e,i){t.ior.value=e.ior,e.sheen>0&&(t.sheenColor.value.copy(e.sheenColor).multiplyScalar(e.sheen),t.sheenRoughness.value=e.sheenRoughness,e.sheenColorMap&&(t.sheenColorMap.value=e.sheenColorMap),e.sheenRoughnessMap&&(t.sheenRoughnessMap.value=e.sheenRoughnessMap));e.clearcoat>0&&(t.clearcoat.value=e.clearcoat,t.clearcoatRoughness.value=e.clearcoatRoughness,e.clearcoatMap&&(t.clearcoatMap.value=e.clearcoatMap),e.clearcoatRoughnessMap&&(t.clearcoatRoughnessMap.value=e.clearcoatRoughnessMap),e.clearcoatNormalMap&&(t.clearcoatNormalScale.value.copy(e.clearcoatNormalScale),t.clearcoatNormalMap.value=e.clearcoatNormalMap,1===e.side&&t.clearcoatNormalScale.value.negate()));e.iridescence>0&&(t.iridescence.value=e.iridescence,t.iridescenceIOR.value=e.iridescenceIOR,t.iridescenceThicknessMinimum.value=e.iridescenceThicknessRange[0],t.iridescenceThicknessMaximum.value=e.iridescenceThicknessRange[1],e.iridescenceMap&&(t.iridescenceMap.value=e.iridescenceMap),e.iridescenceThicknessMap&&(t.iridescenceThicknessMap.value=e.iridescenceThicknessMap));e.transmission>0&&(t.transmission.value=e.transmission,t.transmissionSamplerMap.value=i.texture,t.transmissionSamplerSize.value.set(i.width,i.height),e.transmissionMap&&(t.transmissionMap.value=e.transmissionMap),t.thickness.value=e.thickness,e.thicknessMap&&(t.thicknessMap.value=e.thicknessMap),t.attenuationDistance.value=e.attenuationDistance,t.attenuationColor.value.copy(e.attenuationColor));t.specularIntensity.value=e.specularIntensity,t.specularColor.value.copy(e.specularColor),e.specularIntensityMap&&(t.specularIntensityMap.value=e.specularIntensityMap);e.specularColorMap&&(t.specularColorMap.value=e.specularColorMap)}(t,n,a)):n.isMeshMatcapMaterial?(i(t,n),function(t,e){e.matcap&&(t.matcap.value=e.matcap)}(t,n)):n.isMeshDepthMaterial?i(t,n):n.isMeshDistanceMaterial?(i(t,n),function(t,e){t.referencePosition.value.copy(e.referencePosition),t.nearDistance.value=e.nearDistance,t.farDistance.value=e.farDistance}(t,n)):n.isMeshNormalMaterial?i(t,n):n.isLineBasicMaterial?(function(t,e){t.diffuse.value.copy(e.color),t.opacity.value=e.opacity}(t,n),n.isLineDashedMaterial&&function(t,e){t.dashSize.value=e.dashSize,t.totalSize.value=e.dashSize+e.gapSize,t.scale.value=e.scale}(t,n)):n.isPointsMaterial?function(t,e,i,n){t.diffuse.value.copy(e.color),t.opacity.value=e.opacity,t.size.value=e.size*i,t.scale.value=.5*n,e.map&&(t.map.value=e.map);e.alphaMap&&(t.alphaMap.value=e.alphaMap);e.alphaTest>0&&(t.alphaTest.value=e.alphaTest);let r;e.map?r=e.map:e.alphaMap&&(r=e.alphaMap);void 0!==r&&(!0===r.matrixAutoUpdate&&r.updateMatrix(),t.uvTransform.value.copy(r.matrix))}(t,n,r,s):n.isSpriteMaterial?function(t,e){t.diffuse.value.copy(e.color),t.opacity.value=e.opacity,t.rotation.value=e.rotation,e.map&&(t.map.value=e.map);e.alphaMap&&(t.alphaMap.value=e.alphaMap);e.alphaTest>0&&(t.alphaTest.value=e.alphaTest);let i;e.map?i=e.map:e.alphaMap&&(i=e.alphaMap);void 0!==i&&(!0===i.matrixAutoUpdate&&i.updateMatrix(),t.uvTransform.value.copy(i.matrix))}(t,n):n.isShadowMaterial?(t.color.value.copy(n.color),t.opacity.value=n.opacity):n.isShaderMaterial&&(n.uniformsNeedUpdate=!1)}}}function Js(t,e,i,n){let r={},s={},a=[];const o=i.isWebGL2?t.getParameter(35375):0;function l(t,e,i){const n=t.value;if(void 0===i[e])return i[e]="number"==typeof n?n:n.clone(),!0;if("number"==typeof n){if(i[e]!==n)return i[e]=n,!0}else{const t=i[e];if(!1===t.equals(n))return t.copy(n),!0}return!1}function c(t){const e=t.value,i={boundary:0,storage:0};return"number"==typeof e?(i.boundary=4,i.storage=4):e.isVector2?(i.boundary=8,i.storage=8):e.isVector3||e.isColor?(i.boundary=16,i.storage=12):e.isVector4?(i.boundary=16,i.storage=16):e.isMatrix3?(i.boundary=48,i.storage=48):e.isMatrix4?(i.boundary=64,i.storage=64):e.isTexture?console.warn("THREE.WebGLRenderer: Texture samplers can not be part of an uniforms group."):console.warn("THREE.WebGLRenderer: Unsupported uniform value type.",e),i}function h(e){const i=e.target;i.removeEventListener("dispose",h);const n=a.indexOf(i.__bindingPointIndex);a.splice(n,1),t.deleteBuffer(r[i.id]),delete r[i.id],delete s[i.id]}return{bind:function(t,e){const i=e.program;n.uniformBlockBinding(t,i)},update:function(i,u){let d=r[i.id];void 0===d&&(!function(t){const e=t.uniforms;let i=0;const n=16;let r=0;for(let t=0,s=e.length;t0){r=i%n;const t=n-r;0!==r&&t-a.boundary<0&&(i+=n-r,s.__offset=i)}i+=a.storage}r=i%n,r>0&&(i+=n-r);t.__size=i,t.__cache={}}(i),d=function(e){const i=function(){for(let t=0;t0&&function(t,e,i){const n=Y.isWebGL2;null===G&&(G=new te(1,1,{generateMipmaps:!0,type:X.has("EXT_color_buffer_half_float")?b:x,minFilter:v,samples:n&&!0===s?4:0}));f.getDrawingBufferSize(H),n?G.setSize(H.x,H.y):G.setSize(Tt(H.x),Tt(H.y));const r=f.getRenderTarget();f.setRenderTarget(G),f.clear();const a=f.toneMapping;f.toneMapping=0,Nt(t,e,i),f.toneMapping=a,$.updateMultisampleRenderTarget(G),$.updateRenderTargetMipmap(G),f.setRenderTarget(r)}(r,e,i),n&&Z.viewport(E.copy(n)),r.length>0&&Nt(r,e,i),a.length>0&&Nt(a,e,i),o.length>0&&Nt(o,e,i),Z.buffers.depth.setTest(!0),Z.buffers.depth.setMask(!0),Z.buffers.color.setMask(!0),Z.setPolygonOffset(!1)}function Nt(t,e,i){const n=!0===e.isScene?e.overrideMaterial:null;for(let r=0,s=t.length;r0?m[m.length-1]:null,p.pop(),u=p.length>0?p[p.length-1]:null},this.getActiveCubeFace=function(){return _},this.getActiveMipmapLevel=function(){return y},this.getRenderTarget=function(){return S},this.setRenderTargetTextures=function(t,e,i){K.get(t.texture).__webglTexture=e,K.get(t.depthTexture).__webglTexture=i;const n=K.get(t);n.__hasExternalTextures=!0,n.__hasExternalTextures&&(n.__autoAllocateDepthBuffer=void 0===i,n.__autoAllocateDepthBuffer||!0===X.has("WEBGL_multisampled_render_to_texture")&&(console.warn("THREE.WebGLRenderer: Render-to-texture extension was disabled because an external texture was provided"),n.__useRenderToTexture=!1))},this.setRenderTargetFramebuffer=function(t,e){const i=K.get(t);i.__webglFramebuffer=e,i.__useDefaultFramebuffer=void 0===e},this.setRenderTarget=function(t,e=0,i=0){S=t,_=e,y=i;let n=!0,r=null,s=!1,a=!1;if(t){const i=K.get(t);void 0!==i.__useDefaultFramebuffer?(Z.bindFramebuffer(36160,null),n=!1):void 0===i.__webglFramebuffer?$.setupRenderTarget(t):i.__hasExternalTextures&&$.rebindTextures(t,K.get(t.texture).__webglTexture,K.get(t.depthTexture).__webglTexture);const o=t.texture;(o.isData3DTexture||o.isDataArrayTexture||o.isCompressedArrayTexture)&&(a=!0);const l=K.get(t).__webglFramebuffer;t.isWebGLCubeRenderTarget?(r=l[e],s=!0):r=Y.isWebGL2&&t.samples>0&&!1===$.useMultisampledRTT(t)?K.get(t).__webglMultisampledFramebuffer:l,E.copy(t.viewport),C.copy(t.scissor),L=t.scissorTest}else E.copy(O).multiplyScalar(I).floor(),C.copy(z).multiplyScalar(I).floor(),L=U;if(Z.bindFramebuffer(36160,r)&&Y.drawBuffers&&n&&Z.drawBuffers(t,r),Z.viewport(E),Z.scissor(C),Z.setScissorTest(L),s){const n=K.get(t.texture);xt.framebufferTexture2D(36160,36064,34069+e,n.__webglTexture,i)}else if(a){const n=K.get(t.texture),r=e||0;xt.framebufferTextureLayer(36160,36064,n.__webglTexture,i||0,r)}T=-1},this.readRenderTargetPixels=function(t,e,i,n,r,s,a){if(!t||!t.isWebGLRenderTarget)return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let o=K.get(t).__webglFramebuffer;if(t.isWebGLCubeRenderTarget&&void 0!==a&&(o=o[a]),o){Z.bindFramebuffer(36160,o);try{const a=t.texture,o=a.format,l=a.type;if(o!==w&&ft.convert(o)!==xt.getParameter(35739))return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");const c=l===b&&(X.has("EXT_color_buffer_half_float")||Y.isWebGL2&&X.has("EXT_color_buffer_float"));if(!(l===x||ft.convert(l)===xt.getParameter(35738)||l===M&&(Y.isWebGL2||X.has("OES_texture_float")||X.has("WEBGL_color_buffer_float"))||c))return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.");e>=0&&e<=t.width-n&&i>=0&&i<=t.height-r&&xt.readPixels(e,i,n,r,ft.convert(o),ft.convert(l),s)}finally{const t=null!==S?K.get(S).__webglFramebuffer:null;Z.bindFramebuffer(36160,t)}}},this.copyFramebufferToTexture=function(t,e,i=0){const n=Math.pow(2,-i),r=Math.floor(e.image.width*n),s=Math.floor(e.image.height*n);$.setTexture2D(e,0),xt.copyTexSubImage2D(3553,i,0,0,t.x,t.y,r,s),Z.unbindTexture()},this.copyTextureToTexture=function(t,e,i,n=0){const r=e.image.width,s=e.image.height,a=ft.convert(i.format),o=ft.convert(i.type);$.setTexture2D(i,0),xt.pixelStorei(37440,i.flipY),xt.pixelStorei(37441,i.premultiplyAlpha),xt.pixelStorei(3317,i.unpackAlignment),e.isDataTexture?xt.texSubImage2D(3553,n,t.x,t.y,r,s,a,o,e.image.data):e.isCompressedTexture?xt.compressedTexSubImage2D(3553,n,t.x,t.y,e.mipmaps[0].width,e.mipmaps[0].height,a,e.mipmaps[0].data):xt.texSubImage2D(3553,n,t.x,t.y,a,o,e.image),0===n&&i.generateMipmaps&&xt.generateMipmap(3553),Z.unbindTexture()},this.copyTextureToTexture3D=function(t,e,i,n,r=0){if(f.isWebGL1Renderer)return void console.warn("THREE.WebGLRenderer.copyTextureToTexture3D: can only be used with WebGL2.");const s=t.max.x-t.min.x+1,a=t.max.y-t.min.y+1,o=t.max.z-t.min.z+1,l=ft.convert(n.format),c=ft.convert(n.type);let h;if(n.isData3DTexture)$.setTexture3D(n,0),h=32879;else{if(!n.isDataArrayTexture)return void console.warn("THREE.WebGLRenderer.copyTextureToTexture3D: only supports THREE.DataTexture3D and THREE.DataTexture2DArray.");$.setTexture2DArray(n,0),h=35866}xt.pixelStorei(37440,n.flipY),xt.pixelStorei(37441,n.premultiplyAlpha),xt.pixelStorei(3317,n.unpackAlignment);const u=xt.getParameter(3314),d=xt.getParameter(32878),p=xt.getParameter(3316),m=xt.getParameter(3315),g=xt.getParameter(32877),v=i.isCompressedTexture?i.mipmaps[0]:i.image;xt.pixelStorei(3314,v.width),xt.pixelStorei(32878,v.height),xt.pixelStorei(3316,t.min.x),xt.pixelStorei(3315,t.min.y),xt.pixelStorei(32877,t.min.z),i.isDataTexture||i.isData3DTexture?xt.texSubImage3D(h,r,e.x,e.y,e.z,s,a,o,l,c,v.data):i.isCompressedArrayTexture?(console.warn("THREE.WebGLRenderer.copyTextureToTexture3D: untested support for compressed srcTexture."),xt.compressedTexSubImage3D(h,r,e.x,e.y,e.z,s,a,o,l,v.data)):xt.texSubImage3D(h,r,e.x,e.y,e.z,s,a,o,l,c,v),xt.pixelStorei(3314,u),xt.pixelStorei(32878,d),xt.pixelStorei(3316,p),xt.pixelStorei(3315,m),xt.pixelStorei(32877,g),0===r&&n.generateMipmaps&&xt.generateMipmap(h),Z.unbindTexture()},this.initTexture=function(t){t.isCubeTexture?$.setTextureCube(t,0):t.isData3DTexture?$.setTexture3D(t,0):t.isDataArrayTexture||t.isCompressedArrayTexture?$.setTexture2DArray(t,0):$.setTexture2D(t,0),Z.unbindTexture()},this.resetState=function(){_=0,y=0,S=null,Z.reset(),gt.reset()},"undefined"!=typeof __THREE_DEVTOOLS__&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}class $s extends Ks{}$s.prototype.isWebGL1Renderer=!0;class Qs{constructor(t,e=25e-5){this.isFogExp2=!0,this.name="",this.color=new qt(t),this.density=e}clone(){return new Qs(this.color,this.density)}toJSON(){return{type:"FogExp2",color:this.color.getHex(),density:this.density}}}class ta{constructor(t,e=1,i=1e3){this.isFog=!0,this.name="",this.color=new qt(t),this.near=e,this.far=i}clone(){return new ta(this.color,this.near,this.far)}toJSON(){return{type:"Fog",color:this.color.getHex(),near:this.near,far:this.far}}}class ea extends si{constructor(){super(),this.isScene=!0,this.type="Scene",this.background=null,this.environment=null,this.fog=null,this.backgroundBlurriness=0,this.backgroundIntensity=1,this.overrideMaterial=null,"undefined"!=typeof __THREE_DEVTOOLS__&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}copy(t,e){return super.copy(t,e),null!==t.background&&(this.background=t.background.clone()),null!==t.environment&&(this.environment=t.environment.clone()),null!==t.fog&&(this.fog=t.fog.clone()),this.backgroundBlurriness=t.backgroundBlurriness,this.backgroundIntensity=t.backgroundIntensity,null!==t.overrideMaterial&&(this.overrideMaterial=t.overrideMaterial.clone()),this.matrixAutoUpdate=t.matrixAutoUpdate,this}toJSON(t){const e=super.toJSON(t);return null!==this.fog&&(e.object.fog=this.fog.toJSON()),this.backgroundBlurriness>0&&(e.backgroundBlurriness=this.backgroundBlurriness),1!==this.backgroundIntensity&&(e.backgroundIntensity=this.backgroundIntensity),e}get autoUpdate(){return console.warn("THREE.Scene: autoUpdate was renamed to matrixWorldAutoUpdate in r144."),this.matrixWorldAutoUpdate}set autoUpdate(t){console.warn("THREE.Scene: autoUpdate was renamed to matrixWorldAutoUpdate in r144."),this.matrixWorldAutoUpdate=t}}class ia{constructor(t,e){this.isInterleavedBuffer=!0,this.array=t,this.stride=e,this.count=void 0!==t?t.length/e:0,this.usage=ut,this.updateRange={offset:0,count:-1},this.version=0,this.uuid=_t()}onUploadCallback(){}set needsUpdate(t){!0===t&&this.version++}setUsage(t){return this.usage=t,this}copy(t){return this.array=new t.array.constructor(t.array),this.count=t.count,this.stride=t.stride,this.usage=t.usage,this}copyAt(t,e,i){t*=this.stride,i*=e.stride;for(let n=0,r=this.stride;nt.far||e.push({distance:o,point:oa.clone(),uv:gi.getUV(oa,pa,ma,fa,ga,va,xa,new Lt),face:null,object:this})}copy(t,e){return super.copy(t,e),void 0!==t.center&&this.center.copy(t.center),this.material=t.material,this}}function ya(t,e,i,n,r,s){ha.subVectors(t,i).addScalar(.5).multiply(n),void 0!==r?(ua.x=s*ha.x-r*ha.y,ua.y=r*ha.x+s*ha.y):ua.copy(ha),t.copy(e),t.x+=ua.x,t.y+=ua.y,t.applyMatrix4(da)}const Ma=new re,ba=new re;class Sa extends si{constructor(){super(),this._currentLevel=0,this.type="LOD",Object.defineProperties(this,{levels:{enumerable:!0,value:[]},isLOD:{value:!0}}),this.autoUpdate=!0}copy(t){super.copy(t,!1);const e=t.levels;for(let t=0,i=e.length;t0){let i,n;for(i=1,n=e.length;i0){Ma.setFromMatrixPosition(this.matrixWorld);const i=t.ray.origin.distanceTo(Ma);this.getObjectForDistance(i).raycast(t,e)}}update(t){const e=this.levels;if(e.length>1){Ma.setFromMatrixPosition(t.matrixWorld),ba.setFromMatrixPosition(this.matrixWorld);const i=Ma.distanceTo(ba)/t.zoom;let n,r;for(e[0].object.visible=!0,n=1,r=e.length;n=t))break;e[n-1].object.visible=!1,e[n].object.visible=!0}for(this._currentLevel=n-1;no)continue;u.applyMatrix4(this.matrixWorld);const s=t.ray.origin.distanceTo(u);st.far||e.push({distance:s,point:h.clone().applyMatrix4(this.matrixWorld),index:i,face:null,faceIndex:null,object:this})}}else{for(let i=Math.max(0,s.start),n=Math.min(m.count,s.start+s.count)-1;io)continue;u.applyMatrix4(this.matrixWorld);const n=t.ray.origin.distanceTo(u);nt.far||e.push({distance:n,point:h.clone().applyMatrix4(this.matrixWorld),index:i,face:null,faceIndex:null,object:this})}}}updateMorphTargets(){const t=this.geometry.morphAttributes,e=Object.keys(t);if(e.length>0){const i=t[e[0]];if(void 0!==i){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let t=0,e=i.length;t0){const i=t[e[0]];if(void 0!==i){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let t=0,e=i.length;tr.far)return;s.push({distance:l,distanceToRay:Math.sqrt(o),point:i,index:e,face:null,object:a})}}class ao extends $t{constructor(t,e,i,n,r,s,a,o,l,c,h,u){super(null,s,a,o,l,c,n,r,h,u),this.isCompressedTexture=!0,this.image={width:e,height:i},this.mipmaps=t,this.flipY=!1,this.generateMipmaps=!1}}class oo{constructor(){this.type="Curve",this.arcLengthDivisions=200}getPoint(){return console.warn("THREE.Curve: .getPoint() not implemented."),null}getPointAt(t,e){const i=this.getUtoTmapping(t);return this.getPoint(i,e)}getPoints(t=5){const e=[];for(let i=0;i<=t;i++)e.push(this.getPoint(i/t));return e}getSpacedPoints(t=5){const e=[];for(let i=0;i<=t;i++)e.push(this.getPointAt(i/t));return e}getLength(){const t=this.getLengths();return t[t.length-1]}getLengths(t=this.arcLengthDivisions){if(this.cacheArcLengths&&this.cacheArcLengths.length===t+1&&!this.needsUpdate)return this.cacheArcLengths;this.needsUpdate=!1;const e=[];let i,n=this.getPoint(0),r=0;e.push(0);for(let s=1;s<=t;s++)i=this.getPoint(s/t),r+=i.distanceTo(n),e.push(r),n=i;return this.cacheArcLengths=e,e}updateArcLengths(){this.needsUpdate=!0,this.getLengths()}getUtoTmapping(t,e){const i=this.getLengths();let n=0;const r=i.length;let s;s=e||t*i[r-1];let a,o=0,l=r-1;for(;o<=l;)if(n=Math.floor(o+(l-o)/2),a=i[n]-s,a<0)o=n+1;else{if(!(a>0)){l=n;break}l=n-1}if(n=l,i[n]===s)return n/(r-1);const c=i[n];return(n+(s-c)/(i[n+1]-c))/(r-1)}getTangent(t,e){const i=1e-4;let n=t-i,r=t+i;n<0&&(n=0),r>1&&(r=1);const s=this.getPoint(n),a=this.getPoint(r),o=e||(s.isVector2?new Lt:new re);return o.copy(a).sub(s).normalize(),o}getTangentAt(t,e){const i=this.getUtoTmapping(t);return this.getTangent(i,e)}computeFrenetFrames(t,e){const i=new re,n=[],r=[],s=[],a=new re,o=new Ne;for(let e=0;e<=t;e++){const i=e/t;n[e]=this.getTangentAt(i,new re)}r[0]=new re,s[0]=new re;let l=Number.MAX_VALUE;const c=Math.abs(n[0].x),h=Math.abs(n[0].y),u=Math.abs(n[0].z);c<=l&&(l=c,i.set(1,0,0)),h<=l&&(l=h,i.set(0,1,0)),u<=l&&i.set(0,0,1),a.crossVectors(n[0],i).normalize(),r[0].crossVectors(n[0],a),s[0].crossVectors(n[0],r[0]);for(let e=1;e<=t;e++){if(r[e]=r[e-1].clone(),s[e]=s[e-1].clone(),a.crossVectors(n[e-1],n[e]),a.length()>Number.EPSILON){a.normalize();const t=Math.acos(yt(n[e-1].dot(n[e]),-1,1));r[e].applyMatrix4(o.makeRotationAxis(a,t))}s[e].crossVectors(n[e],r[e])}if(!0===e){let e=Math.acos(yt(r[0].dot(r[t]),-1,1));e/=t,n[0].dot(a.crossVectors(r[0],r[t]))>0&&(e=-e);for(let i=1;i<=t;i++)r[i].applyMatrix4(o.makeRotationAxis(n[i],e*i)),s[i].crossVectors(n[i],r[i])}return{tangents:n,normals:r,binormals:s}}clone(){return(new this.constructor).copy(this)}copy(t){return this.arcLengthDivisions=t.arcLengthDivisions,this}toJSON(){const t={metadata:{version:4.5,type:"Curve",generator:"Curve.toJSON"}};return t.arcLengthDivisions=this.arcLengthDivisions,t.type=this.type,t}fromJSON(t){return this.arcLengthDivisions=t.arcLengthDivisions,this}}class lo extends oo{constructor(t=0,e=0,i=1,n=1,r=0,s=2*Math.PI,a=!1,o=0){super(),this.isEllipseCurve=!0,this.type="EllipseCurve",this.aX=t,this.aY=e,this.xRadius=i,this.yRadius=n,this.aStartAngle=r,this.aEndAngle=s,this.aClockwise=a,this.aRotation=o}getPoint(t,e){const i=e||new Lt,n=2*Math.PI;let r=this.aEndAngle-this.aStartAngle;const s=Math.abs(r)n;)r-=n;r0?0:(Math.floor(Math.abs(l)/r)+1)*r:0===c&&l===r-1&&(l=r-2,c=1),this.closed||l>0?a=n[(l-1)%r]:(uo.subVectors(n[0],n[1]).add(n[0]),a=uo);const h=n[l%r],u=n[(l+1)%r];if(this.closed||l+2n.length-2?n.length-1:s+1],h=n[s>n.length-3?n.length-1:s+2];return i.set(vo(a,o.x,l.x,c.x,h.x),vo(a,o.y,l.y,c.y,h.y)),i}copy(t){super.copy(t),this.points=[];for(let e=0,i=t.points.length;e=i){const t=n[r]-i,s=this.curves[r],a=s.getLength(),o=0===a?0:1-t/a;return s.getPointAt(o,e)}r++}return null}getLength(){const t=this.getCurveLengths();return t[t.length-1]}updateArcLengths(){this.needsUpdate=!0,this.cacheLengths=null,this.getCurveLengths()}getCurveLengths(){if(this.cacheLengths&&this.cacheLengths.length===this.curves.length)return this.cacheLengths;const t=[];let e=0;for(let i=0,n=this.curves.length;i1&&!e[e.length-1].equals(e[0])&&e.push(e[0]),e}copy(t){super.copy(t),this.curves=[];for(let e=0,i=t.curves.length;e0){const t=l.getPoint(0);t.equals(this.currentPoint)||this.lineTo(t.x,t.y)}this.curves.push(l);const c=l.getPoint(1);return this.currentPoint.copy(c),this}copy(t){return super.copy(t),this.currentPoint.copy(t.currentPoint),this}toJSON(){const t=super.toJSON();return t.currentPoint=this.currentPoint.toArray(),t}fromJSON(t){return super.fromJSON(t),this.currentPoint.fromArray(t.currentPoint),this}}class Ro extends Di{constructor(t=[new Lt(0,-.5),new Lt(.5,0),new Lt(0,.5)],e=12,i=0,n=2*Math.PI){super(),this.type="LatheGeometry",this.parameters={points:t,segments:e,phiStart:i,phiLength:n},e=Math.floor(e),n=yt(n,0,2*Math.PI);const r=[],s=[],a=[],o=[],l=[],c=1/e,h=new re,u=new Lt,d=new re,p=new re,m=new re;let f=0,g=0;for(let e=0;e<=t.length-1;e++)switch(e){case 0:f=t[e+1].x-t[e].x,g=t[e+1].y-t[e].y,d.x=1*g,d.y=-f,d.z=0*g,m.copy(d),d.normalize(),o.push(d.x,d.y,d.z);break;case t.length-1:o.push(m.x,m.y,m.z);break;default:f=t[e+1].x-t[e].x,g=t[e+1].y-t[e].y,d.x=1*g,d.y=-f,d.z=0*g,p.copy(d),d.x+=m.x,d.y+=m.y,d.z+=m.z,d.normalize(),o.push(d.x,d.y,d.z),m.copy(p)}for(let r=0;r<=e;r++){const d=i+r*c*n,p=Math.sin(d),m=Math.cos(d);for(let i=0;i<=t.length-1;i++){h.x=t[i].x*p,h.y=t[i].y,h.z=t[i].x*m,s.push(h.x,h.y,h.z),u.x=r/e,u.y=i/(t.length-1),a.push(u.x,u.y);const n=o[3*i+0]*p,c=o[3*i+1],d=o[3*i+0]*m;l.push(n,c,d)}}for(let i=0;i0&&v(!0),e>0&&v(!1)),this.setIndex(c),this.setAttribute("position",new Ti(h,3)),this.setAttribute("normal",new Ti(u,3)),this.setAttribute("uv",new Ti(d,2))}static fromJSON(t){return new Do(t.radiusTop,t.radiusBottom,t.height,t.radialSegments,t.heightSegments,t.openEnded,t.thetaStart,t.thetaLength)}}class No extends Do{constructor(t=1,e=1,i=8,n=1,r=!1,s=0,a=2*Math.PI){super(0,t,e,i,n,r,s,a),this.type="ConeGeometry",this.parameters={radius:t,height:e,radialSegments:i,heightSegments:n,openEnded:r,thetaStart:s,thetaLength:a}}static fromJSON(t){return new No(t.radius,t.height,t.radialSegments,t.heightSegments,t.openEnded,t.thetaStart,t.thetaLength)}}class Oo extends Di{constructor(t=[],e=[],i=1,n=0){super(),this.type="PolyhedronGeometry",this.parameters={vertices:t,indices:e,radius:i,detail:n};const r=[],s=[];function a(t,e,i,n){const r=n+1,s=[];for(let n=0;n<=r;n++){s[n]=[];const a=t.clone().lerp(i,n/r),o=e.clone().lerp(i,n/r),l=r-n;for(let t=0;t<=l;t++)s[n][t]=0===t&&n===r?a:a.clone().lerp(o,t/l)}for(let t=0;t.9&&a<.1&&(e<.2&&(s[t+0]+=1),i<.2&&(s[t+2]+=1),n<.2&&(s[t+4]+=1))}}()}(),this.setAttribute("position",new Ti(r,3)),this.setAttribute("normal",new Ti(r.slice(),3)),this.setAttribute("uv",new Ti(s,2)),0===n?this.computeVertexNormals():this.normalizeNormals()}static fromJSON(t){return new Oo(t.vertices,t.indices,t.radius,t.details)}}class zo extends Oo{constructor(t=1,e=0){const i=(1+Math.sqrt(5))/2,n=1/i;super([-1,-1,-1,-1,-1,1,-1,1,-1,-1,1,1,1,-1,-1,1,-1,1,1,1,-1,1,1,1,0,-n,-i,0,-n,i,0,n,-i,0,n,i,-n,-i,0,-n,i,0,n,-i,0,n,i,0,-i,0,-n,i,0,-n,-i,0,n,i,0,n],[3,11,7,3,7,15,3,15,13,7,19,17,7,17,6,7,6,15,17,4,8,17,8,10,17,10,6,8,0,16,8,16,2,8,2,10,0,12,1,0,1,18,0,18,16,6,10,2,6,2,13,6,13,15,2,16,18,2,18,3,2,3,13,18,1,9,18,9,11,18,11,3,4,14,12,4,12,0,4,0,8,11,9,5,11,5,19,11,19,7,19,5,14,19,14,4,19,4,17,1,12,14,1,14,5,1,5,9],t,e),this.type="DodecahedronGeometry",this.parameters={radius:t,detail:e}}static fromJSON(t){return new zo(t.radius,t.detail)}}const Uo=new re,Bo=new re,Fo=new re,ko=new gi;class Go extends Di{constructor(t=null,e=1){if(super(),this.type="EdgesGeometry",this.parameters={geometry:t,thresholdAngle:e},null!==t){const i=4,n=Math.pow(10,i),r=Math.cos(vt*e),s=t.getIndex(),a=t.getAttribute("position"),o=s?s.count:a.count,l=[0,0,0],c=["a","b","c"],h=new Array(3),u={},d=[];for(let t=0;t80*i){o=c=t[0],l=h=t[1];for(let e=i;ec&&(c=u),d>h&&(h=d);p=Math.max(c-o,h-l),p=0!==p?32767/p:0}return qo(s,a,i,o,l,p,0),a};function Wo(t,e,i,n,r){let s,a;if(r===function(t,e,i,n){let r=0;for(let s=e,a=i-n;s0)for(s=e;s=e;s-=n)a=ul(s,t[s],t[s+1],a);return a&&sl(a,a.next)&&(dl(a),a=a.next),a}function jo(t,e){if(!t)return t;e||(e=t);let i,n=t;do{if(i=!1,n.steiner||!sl(n,n.next)&&0!==rl(n.prev,n,n.next))n=n.next;else{if(dl(n),n=e=n.prev,n===n.next)break;i=!0}}while(i||n!==e);return e}function qo(t,e,i,n,r,s,a){if(!t)return;!a&&s&&function(t,e,i,n){let r=t;do{0===r.z&&(r.z=tl(r.x,r.y,e,i,n)),r.prevZ=r.prev,r.nextZ=r.next,r=r.next}while(r!==t);r.prevZ.nextZ=null,r.prevZ=null,function(t){let e,i,n,r,s,a,o,l,c=1;do{for(i=t,t=null,s=null,a=0;i;){for(a++,n=i,o=0,e=0;e0||l>0&&n;)0!==o&&(0===l||!n||i.z<=n.z)?(r=i,i=i.nextZ,o--):(r=n,n=n.nextZ,l--),s?s.nextZ=r:t=r,r.prevZ=s,s=r;i=n}s.nextZ=null,c*=2}while(a>1)}(r)}(t,n,r,s);let o,l,c=t;for(;t.prev!==t.next;)if(o=t.prev,l=t.next,s?Yo(t,n,r,s):Xo(t))e.push(o.i/i|0),e.push(t.i/i|0),e.push(l.i/i|0),dl(t),t=l.next,c=l.next;else if((t=l)===c){a?1===a?qo(t=Zo(jo(t),e,i),e,i,n,r,s,2):2===a&&Jo(t,e,i,n,r,s):qo(jo(t),e,i,n,r,s,1);break}}function Xo(t){const e=t.prev,i=t,n=t.next;if(rl(e,i,n)>=0)return!1;const r=e.x,s=i.x,a=n.x,o=e.y,l=i.y,c=n.y,h=rs?r>a?r:a:s>a?s:a,p=o>l?o>c?o:c:l>c?l:c;let m=n.next;for(;m!==e;){if(m.x>=h&&m.x<=d&&m.y>=u&&m.y<=p&&il(r,o,s,l,a,c,m.x,m.y)&&rl(m.prev,m,m.next)>=0)return!1;m=m.next}return!0}function Yo(t,e,i,n){const r=t.prev,s=t,a=t.next;if(rl(r,s,a)>=0)return!1;const o=r.x,l=s.x,c=a.x,h=r.y,u=s.y,d=a.y,p=ol?o>c?o:c:l>c?l:c,g=h>u?h>d?h:d:u>d?u:d,v=tl(p,m,e,i,n),x=tl(f,g,e,i,n);let _=t.prevZ,y=t.nextZ;for(;_&&_.z>=v&&y&&y.z<=x;){if(_.x>=p&&_.x<=f&&_.y>=m&&_.y<=g&&_!==r&&_!==a&&il(o,h,l,u,c,d,_.x,_.y)&&rl(_.prev,_,_.next)>=0)return!1;if(_=_.prevZ,y.x>=p&&y.x<=f&&y.y>=m&&y.y<=g&&y!==r&&y!==a&&il(o,h,l,u,c,d,y.x,y.y)&&rl(y.prev,y,y.next)>=0)return!1;y=y.nextZ}for(;_&&_.z>=v;){if(_.x>=p&&_.x<=f&&_.y>=m&&_.y<=g&&_!==r&&_!==a&&il(o,h,l,u,c,d,_.x,_.y)&&rl(_.prev,_,_.next)>=0)return!1;_=_.prevZ}for(;y&&y.z<=x;){if(y.x>=p&&y.x<=f&&y.y>=m&&y.y<=g&&y!==r&&y!==a&&il(o,h,l,u,c,d,y.x,y.y)&&rl(y.prev,y,y.next)>=0)return!1;y=y.nextZ}return!0}function Zo(t,e,i){let n=t;do{const r=n.prev,s=n.next.next;!sl(r,s)&&al(r,n,n.next,s)&&cl(r,s)&&cl(s,r)&&(e.push(r.i/i|0),e.push(n.i/i|0),e.push(s.i/i|0),dl(n),dl(n.next),n=t=s),n=n.next}while(n!==t);return jo(n)}function Jo(t,e,i,n,r,s){let a=t;do{let t=a.next.next;for(;t!==a.prev;){if(a.i!==t.i&&nl(a,t)){let o=hl(a,t);return a=jo(a,a.next),o=jo(o,o.next),qo(a,e,i,n,r,s,0),void qo(o,e,i,n,r,s,0)}t=t.next}a=a.next}while(a!==t)}function Ko(t,e){return t.x-e.x}function $o(t,e){const i=function(t,e){let i,n=e,r=-1/0;const s=t.x,a=t.y;do{if(a<=n.y&&a>=n.next.y&&n.next.y!==n.y){const t=n.x+(a-n.y)*(n.next.x-n.x)/(n.next.y-n.y);if(t<=s&&t>r&&(r=t,i=n.x=n.x&&n.x>=l&&s!==n.x&&il(ai.x||n.x===i.x&&Qo(i,n)))&&(i=n,u=h)),n=n.next}while(n!==o);return i}(t,e);if(!i)return e;const n=hl(i,t);return jo(n,n.next),jo(i,i.next)}function Qo(t,e){return rl(t.prev,t,e.prev)<0&&rl(e.next,t,t.next)<0}function tl(t,e,i,n,r){return(t=1431655765&((t=858993459&((t=252645135&((t=16711935&((t=(t-i)*r|0)|t<<8))|t<<4))|t<<2))|t<<1))|(e=1431655765&((e=858993459&((e=252645135&((e=16711935&((e=(e-n)*r|0)|e<<8))|e<<4))|e<<2))|e<<1))<<1}function el(t){let e=t,i=t;do{(e.x=(t-a)*(s-o)&&(t-a)*(n-o)>=(i-a)*(e-o)&&(i-a)*(s-o)>=(r-a)*(n-o)}function nl(t,e){return t.next.i!==e.i&&t.prev.i!==e.i&&!function(t,e){let i=t;do{if(i.i!==t.i&&i.next.i!==t.i&&i.i!==e.i&&i.next.i!==e.i&&al(i,i.next,t,e))return!0;i=i.next}while(i!==t);return!1}(t,e)&&(cl(t,e)&&cl(e,t)&&function(t,e){let i=t,n=!1;const r=(t.x+e.x)/2,s=(t.y+e.y)/2;do{i.y>s!=i.next.y>s&&i.next.y!==i.y&&r<(i.next.x-i.x)*(s-i.y)/(i.next.y-i.y)+i.x&&(n=!n),i=i.next}while(i!==t);return n}(t,e)&&(rl(t.prev,t,e.prev)||rl(t,e.prev,e))||sl(t,e)&&rl(t.prev,t,t.next)>0&&rl(e.prev,e,e.next)>0)}function rl(t,e,i){return(e.y-t.y)*(i.x-e.x)-(e.x-t.x)*(i.y-e.y)}function sl(t,e){return t.x===e.x&&t.y===e.y}function al(t,e,i,n){const r=ll(rl(t,e,i)),s=ll(rl(t,e,n)),a=ll(rl(i,n,t)),o=ll(rl(i,n,e));return r!==s&&a!==o||(!(0!==r||!ol(t,i,e))||(!(0!==s||!ol(t,n,e))||(!(0!==a||!ol(i,t,n))||!(0!==o||!ol(i,e,n)))))}function ol(t,e,i){return e.x<=Math.max(t.x,i.x)&&e.x>=Math.min(t.x,i.x)&&e.y<=Math.max(t.y,i.y)&&e.y>=Math.min(t.y,i.y)}function ll(t){return t>0?1:t<0?-1:0}function cl(t,e){return rl(t.prev,t,t.next)<0?rl(t,e,t.next)>=0&&rl(t,t.prev,e)>=0:rl(t,e,t.prev)<0||rl(t,t.next,e)<0}function hl(t,e){const i=new pl(t.i,t.x,t.y),n=new pl(e.i,e.x,e.y),r=t.next,s=e.prev;return t.next=e,e.prev=t,i.next=r,r.prev=i,n.next=i,i.prev=n,s.next=n,n.prev=s,n}function ul(t,e,i,n){const r=new pl(t,e,i);return n?(r.next=n.next,r.prev=n,n.next.prev=r,n.next=r):(r.prev=r,r.next=r),r}function dl(t){t.next.prev=t.prev,t.prev.next=t.next,t.prevZ&&(t.prevZ.nextZ=t.nextZ),t.nextZ&&(t.nextZ.prevZ=t.prevZ)}function pl(t,e,i){this.i=t,this.x=e,this.y=i,this.prev=null,this.next=null,this.z=0,this.prevZ=null,this.nextZ=null,this.steiner=!1}class ml{static area(t){const e=t.length;let i=0;for(let n=e-1,r=0;r2&&t[e-1].equals(t[0])&&t.pop()}function gl(t,e){for(let i=0;iNumber.EPSILON){const u=Math.sqrt(h),d=Math.sqrt(l*l+c*c),p=e.x-o/u,m=e.y+a/u,f=((i.x-c/d-p)*c-(i.y+l/d-m)*l)/(a*c-o*l);n=p+a*f-t.x,r=m+o*f-t.y;const g=n*n+r*r;if(g<=2)return new Lt(n,r);s=Math.sqrt(g/2)}else{let t=!1;a>Number.EPSILON?l>Number.EPSILON&&(t=!0):a<-Number.EPSILON?l<-Number.EPSILON&&(t=!0):Math.sign(o)===Math.sign(c)&&(t=!0),t?(n=-o,r=a,s=Math.sqrt(h)):(n=a,r=o,s=Math.sqrt(h/2))}return new Lt(n/s,r/s)}const P=[];for(let t=0,e=A.length,i=e-1,n=t+1;t=0;t--){const e=t/p,i=h*Math.cos(e*Math.PI/2),n=u*Math.sin(e*Math.PI/2)+d;for(let t=0,e=A.length;t=0;){const n=i;let r=i-1;r<0&&(r=t.length-1);for(let t=0,i=o+2*p;t0)&&d.push(e,r,l),(t!==i-1||o0!=t>0&&this.version++,this._sheen=t}get clearcoat(){return this._clearcoat}set clearcoat(t){this._clearcoat>0!=t>0&&this.version++,this._clearcoat=t}get iridescence(){return this._iridescence}set iridescence(t){this._iridescence>0!=t>0&&this.version++,this._iridescence=t}get transmission(){return this._transmission}set transmission(t){this._transmission>0!=t>0&&this.version++,this._transmission=t}copy(t){return super.copy(t),this.defines={STANDARD:"",PHYSICAL:""},this.clearcoat=t.clearcoat,this.clearcoatMap=t.clearcoatMap,this.clearcoatRoughness=t.clearcoatRoughness,this.clearcoatRoughnessMap=t.clearcoatRoughnessMap,this.clearcoatNormalMap=t.clearcoatNormalMap,this.clearcoatNormalScale.copy(t.clearcoatNormalScale),this.ior=t.ior,this.iridescence=t.iridescence,this.iridescenceMap=t.iridescenceMap,this.iridescenceIOR=t.iridescenceIOR,this.iridescenceThicknessRange=[...t.iridescenceThicknessRange],this.iridescenceThicknessMap=t.iridescenceThicknessMap,this.sheen=t.sheen,this.sheenColor.copy(t.sheenColor),this.sheenColorMap=t.sheenColorMap,this.sheenRoughness=t.sheenRoughness,this.sheenRoughnessMap=t.sheenRoughnessMap,this.transmission=t.transmission,this.transmissionMap=t.transmissionMap,this.thickness=t.thickness,this.thicknessMap=t.thicknessMap,this.attenuationDistance=t.attenuationDistance,this.attenuationColor.copy(t.attenuationColor),this.specularIntensity=t.specularIntensity,this.specularIntensityMap=t.specularIntensityMap,this.specularColor.copy(t.specularColor),this.specularColorMap=t.specularColorMap,this}}class Ol extends xi{constructor(t){super(),this.isMeshPhongMaterial=!0,this.type="MeshPhongMaterial",this.color=new qt(16777215),this.specular=new qt(1118481),this.shininess=30,this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new qt(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new Lt(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.combine=0,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.flatShading=!1,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.specular.copy(t.specular),this.shininess=t.shininess,this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.emissive.copy(t.emissive),this.emissiveMap=t.emissiveMap,this.emissiveIntensity=t.emissiveIntensity,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.specularMap=t.specularMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.combine=t.combine,this.reflectivity=t.reflectivity,this.refractionRatio=t.refractionRatio,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.flatShading=t.flatShading,this.fog=t.fog,this}}class zl extends xi{constructor(t){super(),this.isMeshToonMaterial=!0,this.defines={TOON:""},this.type="MeshToonMaterial",this.color=new qt(16777215),this.map=null,this.gradientMap=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new qt(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new Lt(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.alphaMap=null,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.gradientMap=t.gradientMap,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.emissive.copy(t.emissive),this.emissiveMap=t.emissiveMap,this.emissiveIntensity=t.emissiveIntensity,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.alphaMap=t.alphaMap,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.fog=t.fog,this}}class Ul extends xi{constructor(t){super(),this.isMeshNormalMaterial=!0,this.type="MeshNormalMaterial",this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new Lt(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.flatShading=!1,this.setValues(t)}copy(t){return super.copy(t),this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.flatShading=t.flatShading,this}}class Bl extends xi{constructor(t){super(),this.isMeshLambertMaterial=!0,this.type="MeshLambertMaterial",this.color=new qt(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.emissive=new qt(0),this.emissiveIntensity=1,this.emissiveMap=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new Lt(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.combine=0,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.flatShading=!1,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.emissive.copy(t.emissive),this.emissiveMap=t.emissiveMap,this.emissiveIntensity=t.emissiveIntensity,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.specularMap=t.specularMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.combine=t.combine,this.reflectivity=t.reflectivity,this.refractionRatio=t.refractionRatio,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.flatShading=t.flatShading,this.fog=t.fog,this}}class Fl extends xi{constructor(t){super(),this.isMeshMatcapMaterial=!0,this.defines={MATCAP:""},this.type="MeshMatcapMaterial",this.color=new qt(16777215),this.matcap=null,this.map=null,this.bumpMap=null,this.bumpScale=1,this.normalMap=null,this.normalMapType=0,this.normalScale=new Lt(1,1),this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.alphaMap=null,this.flatShading=!1,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.defines={MATCAP:""},this.color.copy(t.color),this.matcap=t.matcap,this.map=t.map,this.bumpMap=t.bumpMap,this.bumpScale=t.bumpScale,this.normalMap=t.normalMap,this.normalMapType=t.normalMapType,this.normalScale.copy(t.normalScale),this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.alphaMap=t.alphaMap,this.flatShading=t.flatShading,this.fog=t.fog,this}}class kl extends Va{constructor(t){super(),this.isLineDashedMaterial=!0,this.type="LineDashedMaterial",this.scale=1,this.dashSize=3,this.gapSize=1,this.setValues(t)}copy(t){return super.copy(t),this.scale=t.scale,this.dashSize=t.dashSize,this.gapSize=t.gapSize,this}}function Gl(t,e,i){return Hl(t)?new t.constructor(t.subarray(e,void 0!==i?i:t.length)):t.slice(e,i)}function Vl(t,e,i){return!t||!i&&t.constructor===e?t:"number"==typeof e.BYTES_PER_ELEMENT?new e(t):Array.prototype.slice.call(t)}function Hl(t){return ArrayBuffer.isView(t)&&!(t instanceof DataView)}function Wl(t){const e=t.length,i=new Array(e);for(let t=0;t!==e;++t)i[t]=t;return i.sort((function(e,i){return t[e]-t[i]})),i}function jl(t,e,i){const n=t.length,r=new t.constructor(n);for(let s=0,a=0;a!==n;++s){const n=i[s]*e;for(let i=0;i!==e;++i)r[a++]=t[n+i]}return r}function ql(t,e,i,n){let r=1,s=t[0];for(;void 0!==s&&void 0===s[n];)s=t[r++];if(void 0===s)return;let a=s[n];if(void 0!==a)if(Array.isArray(a))do{a=s[n],void 0!==a&&(e.push(s.time),i.push.apply(i,a)),s=t[r++]}while(void 0!==s);else if(void 0!==a.toArray)do{a=s[n],void 0!==a&&(e.push(s.time),a.toArray(i,i.length)),s=t[r++]}while(void 0!==s);else do{a=s[n],void 0!==a&&(e.push(s.time),i.push(a)),s=t[r++]}while(void 0!==s)}var Xl=Object.freeze({__proto__:null,arraySlice:Gl,convertArray:Vl,isTypedArray:Hl,getKeyframeOrder:Wl,sortedArray:jl,flattenJSON:ql,subclip:function(t,e,i,n,r=30){const s=t.clone();s.name=e;const a=[];for(let t=0;t=n)){l.push(e.times[t]);for(let i=0;is.tracks[t].times[0]&&(o=s.tracks[t].times[0]);for(let t=0;t=n.times[u]){const t=u*l+o,e=t+l-o;d=Gl(n.values,t,e)}else{const t=n.createInterpolant(),e=o,i=l-o;t.evaluate(s),d=Gl(t.resultBuffer,e,i)}if("quaternion"===r){(new ne).fromArray(d).normalize().conjugate().toArray(d)}const p=a.times.length;for(let t=0;t=r)break t;{const a=e[1];t=r)break e}s=i,i=0}}for(;i>>1;te;)--s;if(++s,0!==r||s!==n){r>=s&&(s=Math.max(s,1),r=s-1);const t=this.getValueSize();this.times=Gl(i,r,s),this.values=Gl(this.values,r*t,s*t)}return this}validate(){let t=!0;const e=this.getValueSize();e-Math.floor(e)!=0&&(console.error("THREE.KeyframeTrack: Invalid value size in track.",this),t=!1);const i=this.times,n=this.values,r=i.length;0===r&&(console.error("THREE.KeyframeTrack: Track is empty.",this),t=!1);let s=null;for(let e=0;e!==r;e++){const n=i[e];if("number"==typeof n&&isNaN(n)){console.error("THREE.KeyframeTrack: Time is not a valid number.",this,e,n),t=!1;break}if(null!==s&&s>n){console.error("THREE.KeyframeTrack: Out of order keys.",this,e,n,s),t=!1;break}s=n}if(void 0!==n&&Hl(n))for(let e=0,i=n.length;e!==i;++e){const i=n[e];if(isNaN(i)){console.error("THREE.KeyframeTrack: Value is not a valid number.",this,e,i),t=!1;break}}return t}optimize(){const t=Gl(this.times),e=Gl(this.values),i=this.getValueSize(),n=this.getInterpolation()===tt,r=t.length-1;let s=1;for(let a=1;a0){t[s]=t[r];for(let t=r*i,n=s*i,a=0;a!==i;++a)e[n+a]=e[t+a];++s}return s!==t.length?(this.times=Gl(t,0,s),this.values=Gl(e,0,s*i)):(this.times=t,this.values=e),this}clone(){const t=Gl(this.times,0),e=Gl(this.values,0),i=new(0,this.constructor)(this.name,t,e);return i.createInterpolant=this.createInterpolant,i}}$l.prototype.TimeBufferType=Float32Array,$l.prototype.ValueBufferType=Float32Array,$l.prototype.DefaultInterpolation=Q;class Ql extends $l{}Ql.prototype.ValueTypeName="bool",Ql.prototype.ValueBufferType=Array,Ql.prototype.DefaultInterpolation=$,Ql.prototype.InterpolantFactoryMethodLinear=void 0,Ql.prototype.InterpolantFactoryMethodSmooth=void 0;class tc extends $l{}tc.prototype.ValueTypeName="color";class ec extends $l{}ec.prototype.ValueTypeName="number";class ic extends Yl{constructor(t,e,i,n){super(t,e,i,n)}interpolate_(t,e,i,n){const r=this.resultBuffer,s=this.sampleValues,a=this.valueSize,o=(i-e)/(n-e);let l=t*a;for(let t=l+a;l!==t;l+=4)ne.slerpFlat(r,0,s,l-a,s,l,o);return r}}class nc extends $l{InterpolantFactoryMethodLinear(t){return new ic(this.times,this.values,this.getValueSize(),t)}}nc.prototype.ValueTypeName="quaternion",nc.prototype.DefaultInterpolation=Q,nc.prototype.InterpolantFactoryMethodSmooth=void 0;class rc extends $l{}rc.prototype.ValueTypeName="string",rc.prototype.ValueBufferType=Array,rc.prototype.DefaultInterpolation=$,rc.prototype.InterpolantFactoryMethodLinear=void 0,rc.prototype.InterpolantFactoryMethodSmooth=void 0;class sc extends $l{}sc.prototype.ValueTypeName="vector";class ac{constructor(t,e=-1,i,n=2500){this.name=t,this.tracks=i,this.duration=e,this.blendMode=n,this.uuid=_t(),this.duration<0&&this.resetDuration()}static parse(t){const e=[],i=t.tracks,n=1/(t.fps||1);for(let t=0,r=i.length;t!==r;++t)e.push(oc(i[t]).scale(n));const r=new this(t.name,t.duration,e,t.blendMode);return r.uuid=t.uuid,r}static toJSON(t){const e=[],i=t.tracks,n={name:t.name,duration:t.duration,tracks:e,uuid:t.uuid,blendMode:t.blendMode};for(let t=0,n=i.length;t!==n;++t)e.push($l.toJSON(i[t]));return n}static CreateFromMorphTargetSequence(t,e,i,n){const r=e.length,s=[];for(let t=0;t1){const t=s[1];let e=n[t];e||(n[t]=e=[]),e.push(i)}}const s=[];for(const t in n)s.push(this.CreateFromMorphTargetSequence(t,n[t],e,i));return s}static parseAnimation(t,e){if(!t)return console.error("THREE.AnimationClip: No animation in JSONLoader data."),null;const i=function(t,e,i,n,r){if(0!==i.length){const s=[],a=[];ql(i,s,a,n),0!==s.length&&r.push(new t(e,s,a))}},n=[],r=t.name||"default",s=t.fps||30,a=t.blendMode;let o=t.length||-1;const l=t.hierarchy||[];for(let t=0;t{e&&e(r),this.manager.itemEnd(t)}),0),r;if(void 0!==dc[t])return void dc[t].push({onLoad:e,onProgress:i,onError:n});dc[t]=[],dc[t].push({onLoad:e,onProgress:i,onError:n});const s=new Request(t,{headers:new Headers(this.requestHeader),credentials:this.withCredentials?"include":"same-origin"}),a=this.mimeType,o=this.responseType;fetch(s).then((e=>{if(200===e.status||0===e.status){if(0===e.status&&console.warn("THREE.FileLoader: HTTP Status 0 received."),"undefined"==typeof ReadableStream||void 0===e.body||void 0===e.body.getReader)return e;const i=dc[t],n=e.body.getReader(),r=e.headers.get("Content-Length")||e.headers.get("X-File-Size"),s=r?parseInt(r):0,a=0!==s;let o=0;const l=new ReadableStream({start(t){!function e(){n.read().then((({done:n,value:r})=>{if(n)t.close();else{o+=r.byteLength;const n=new ProgressEvent("progress",{lengthComputable:a,loaded:o,total:s});for(let t=0,e=i.length;t{switch(o){case"arraybuffer":return t.arrayBuffer();case"blob":return t.blob();case"document":return t.text().then((t=>(new DOMParser).parseFromString(t,a)));case"json":return t.json();default:if(void 0===a)return t.text();{const e=/charset="?([^;"\s]*)"?/i.exec(a),i=e&&e[1]?e[1].toLowerCase():void 0,n=new TextDecoder(i);return t.arrayBuffer().then((t=>n.decode(t)))}}})).then((e=>{lc.add(t,e);const i=dc[t];delete dc[t];for(let t=0,n=i.length;t{const i=dc[t];if(void 0===i)throw this.manager.itemError(t),e;delete dc[t];for(let t=0,n=i.length;t{this.manager.itemEnd(t)})),this.manager.itemStart(t)}setResponseType(t){return this.responseType=t,this}setMimeType(t){return this.mimeType=t,this}}class fc extends uc{constructor(t){super(t)}load(t,e,i,n){void 0!==this.path&&(t=this.path+t),t=this.manager.resolveURL(t);const r=this,s=lc.get(t);if(void 0!==s)return r.manager.itemStart(t),setTimeout((function(){e&&e(s),r.manager.itemEnd(t)}),0),s;const a=Ot("img");function o(){c(),lc.add(t,this),e&&e(this),r.manager.itemEnd(t)}function l(e){c(),n&&n(e),r.manager.itemError(t),r.manager.itemEnd(t)}function c(){a.removeEventListener("load",o,!1),a.removeEventListener("error",l,!1)}return a.addEventListener("load",o,!1),a.addEventListener("error",l,!1),"data:"!==t.slice(0,5)&&void 0!==this.crossOrigin&&(a.crossOrigin=this.crossOrigin),r.manager.itemStart(t),a.src=t,a}}class gc extends si{constructor(t,e=1){super(),this.isLight=!0,this.type="Light",this.color=new qt(t),this.intensity=e}dispose(){}copy(t,e){return super.copy(t,e),this.color.copy(t.color),this.intensity=t.intensity,this}toJSON(t){const e=super.toJSON(t);return e.object.color=this.color.getHex(),e.object.intensity=this.intensity,void 0!==this.groundColor&&(e.object.groundColor=this.groundColor.getHex()),void 0!==this.distance&&(e.object.distance=this.distance),void 0!==this.angle&&(e.object.angle=this.angle),void 0!==this.decay&&(e.object.decay=this.decay),void 0!==this.penumbra&&(e.object.penumbra=this.penumbra),void 0!==this.shadow&&(e.object.shadow=this.shadow.toJSON()),e}}class vc extends gc{constructor(t,e,i){super(t,i),this.isHemisphereLight=!0,this.type="HemisphereLight",this.position.copy(si.DefaultUp),this.updateMatrix(),this.groundColor=new qt(e)}copy(t,e){return super.copy(t,e),this.groundColor.copy(t.groundColor),this}}const xc=new Ne,_c=new re,yc=new re;class Mc{constructor(t){this.camera=t,this.bias=0,this.normalBias=0,this.radius=1,this.blurSamples=8,this.mapSize=new Lt(512,512),this.map=null,this.mapPass=null,this.matrix=new Ne,this.autoUpdate=!0,this.needsUpdate=!1,this._frustum=new xn,this._frameExtents=new Lt(1,1),this._viewportCount=1,this._viewports=[new Qt(0,0,1,1)]}getViewportCount(){return this._viewportCount}getFrustum(){return this._frustum}updateMatrices(t){const e=this.camera,i=this.matrix;_c.setFromMatrixPosition(t.matrixWorld),e.position.copy(_c),yc.setFromMatrixPosition(t.target.matrixWorld),e.lookAt(yc),e.updateMatrixWorld(),xc.multiplyMatrices(e.projectionMatrix,e.matrixWorldInverse),this._frustum.setFromProjectionMatrix(xc),i.set(.5,0,0,.5,0,.5,0,.5,0,0,.5,.5,0,0,0,1),i.multiply(xc)}getViewport(t){return this._viewports[t]}getFrameExtents(){return this._frameExtents}dispose(){this.map&&this.map.dispose(),this.mapPass&&this.mapPass.dispose()}copy(t){return this.camera=t.camera.clone(),this.bias=t.bias,this.radius=t.radius,this.mapSize.copy(t.mapSize),this}clone(){return(new this.constructor).copy(this)}toJSON(){const t={};return 0!==this.bias&&(t.bias=this.bias),0!==this.normalBias&&(t.normalBias=this.normalBias),1!==this.radius&&(t.radius=this.radius),512===this.mapSize.x&&512===this.mapSize.y||(t.mapSize=this.mapSize.toArray()),t.camera=this.camera.toJSON(!1).object,delete t.camera.matrix,t}}class bc extends Mc{constructor(){super(new on(50,1,.5,500)),this.isSpotLightShadow=!0,this.focus=1}updateMatrices(t){const e=this.camera,i=2*xt*t.angle*this.focus,n=this.mapSize.width/this.mapSize.height,r=t.distance||e.far;i===e.fov&&n===e.aspect&&r===e.far||(e.fov=i,e.aspect=n,e.far=r,e.updateProjectionMatrix()),super.updateMatrices(t)}copy(t){return super.copy(t),this.focus=t.focus,this}}class Sc extends gc{constructor(t,e,i=0,n=Math.PI/3,r=0,s=2){super(t,e),this.isSpotLight=!0,this.type="SpotLight",this.position.copy(si.DefaultUp),this.updateMatrix(),this.target=new si,this.distance=i,this.angle=n,this.penumbra=r,this.decay=s,this.map=null,this.shadow=new bc}get power(){return this.intensity*Math.PI}set power(t){this.intensity=t/Math.PI}dispose(){this.shadow.dispose()}copy(t,e){return super.copy(t,e),this.distance=t.distance,this.angle=t.angle,this.penumbra=t.penumbra,this.decay=t.decay,this.target=t.target.clone(),this.shadow=t.shadow.clone(),this}}const wc=new Ne,Tc=new re,Ac=new re;class Ec extends Mc{constructor(){super(new on(90,1,.5,500)),this.isPointLightShadow=!0,this._frameExtents=new Lt(4,2),this._viewportCount=6,this._viewports=[new Qt(2,1,1,1),new Qt(0,1,1,1),new Qt(3,1,1,1),new Qt(1,1,1,1),new Qt(3,0,1,1),new Qt(1,0,1,1)],this._cubeDirections=[new re(1,0,0),new re(-1,0,0),new re(0,0,1),new re(0,0,-1),new re(0,1,0),new re(0,-1,0)],this._cubeUps=[new re(0,1,0),new re(0,1,0),new re(0,1,0),new re(0,1,0),new re(0,0,1),new re(0,0,-1)]}updateMatrices(t,e=0){const i=this.camera,n=this.matrix,r=t.distance||i.far;r!==i.far&&(i.far=r,i.updateProjectionMatrix()),Tc.setFromMatrixPosition(t.matrixWorld),i.position.copy(Tc),Ac.copy(i.position),Ac.add(this._cubeDirections[e]),i.up.copy(this._cubeUps[e]),i.lookAt(Ac),i.updateMatrixWorld(),n.makeTranslation(-Tc.x,-Tc.y,-Tc.z),wc.multiplyMatrices(i.projectionMatrix,i.matrixWorldInverse),this._frustum.setFromProjectionMatrix(wc)}}class Cc extends gc{constructor(t,e,i=0,n=2){super(t,e),this.isPointLight=!0,this.type="PointLight",this.distance=i,this.decay=n,this.shadow=new Ec}get power(){return 4*this.intensity*Math.PI}set power(t){this.intensity=t/(4*Math.PI)}dispose(){this.shadow.dispose()}copy(t,e){return super.copy(t,e),this.distance=t.distance,this.decay=t.decay,this.shadow=t.shadow.clone(),this}}class Lc extends Mc{constructor(){super(new In(-5,5,5,-5,.5,500)),this.isDirectionalLightShadow=!0}}class Rc extends gc{constructor(t,e){super(t,e),this.isDirectionalLight=!0,this.type="DirectionalLight",this.position.copy(si.DefaultUp),this.updateMatrix(),this.target=new si,this.shadow=new Lc}dispose(){this.shadow.dispose()}copy(t){return super.copy(t),this.target=t.target.clone(),this.shadow=t.shadow.clone(),this}}class Pc extends gc{constructor(t,e){super(t,e),this.isAmbientLight=!0,this.type="AmbientLight"}}class Ic extends gc{constructor(t,e,i=10,n=10){super(t,e),this.isRectAreaLight=!0,this.type="RectAreaLight",this.width=i,this.height=n}get power(){return this.intensity*this.width*this.height*Math.PI}set power(t){this.intensity=t/(this.width*this.height*Math.PI)}copy(t){return super.copy(t),this.width=t.width,this.height=t.height,this}toJSON(t){const e=super.toJSON(t);return e.object.width=this.width,e.object.height=this.height,e}}class Dc{constructor(){this.isSphericalHarmonics3=!0,this.coefficients=[];for(let t=0;t<9;t++)this.coefficients.push(new re)}set(t){for(let e=0;e<9;e++)this.coefficients[e].copy(t[e]);return this}zero(){for(let t=0;t<9;t++)this.coefficients[t].set(0,0,0);return this}getAt(t,e){const i=t.x,n=t.y,r=t.z,s=this.coefficients;return e.copy(s[0]).multiplyScalar(.282095),e.addScaledVector(s[1],.488603*n),e.addScaledVector(s[2],.488603*r),e.addScaledVector(s[3],.488603*i),e.addScaledVector(s[4],i*n*1.092548),e.addScaledVector(s[5],n*r*1.092548),e.addScaledVector(s[6],.315392*(3*r*r-1)),e.addScaledVector(s[7],i*r*1.092548),e.addScaledVector(s[8],.546274*(i*i-n*n)),e}getIrradianceAt(t,e){const i=t.x,n=t.y,r=t.z,s=this.coefficients;return e.copy(s[0]).multiplyScalar(.886227),e.addScaledVector(s[1],1.023328*n),e.addScaledVector(s[2],1.023328*r),e.addScaledVector(s[3],1.023328*i),e.addScaledVector(s[4],.858086*i*n),e.addScaledVector(s[5],.858086*n*r),e.addScaledVector(s[6],.743125*r*r-.247708),e.addScaledVector(s[7],.858086*i*r),e.addScaledVector(s[8],.429043*(i*i-n*n)),e}add(t){for(let e=0;e<9;e++)this.coefficients[e].add(t.coefficients[e]);return this}addScaledSH(t,e){for(let i=0;i<9;i++)this.coefficients[i].addScaledVector(t.coefficients[i],e);return this}scale(t){for(let e=0;e<9;e++)this.coefficients[e].multiplyScalar(t);return this}lerp(t,e){for(let i=0;i<9;i++)this.coefficients[i].lerp(t.coefficients[i],e);return this}equals(t){for(let e=0;e<9;e++)if(!this.coefficients[e].equals(t.coefficients[e]))return!1;return!0}copy(t){return this.set(t.coefficients)}clone(){return(new this.constructor).copy(this)}fromArray(t,e=0){const i=this.coefficients;for(let n=0;n<9;n++)i[n].fromArray(t,e+3*n);return this}toArray(t=[],e=0){const i=this.coefficients;for(let n=0;n<9;n++)i[n].toArray(t,e+3*n);return t}static getBasisAt(t,e){const i=t.x,n=t.y,r=t.z;e[0]=.282095,e[1]=.488603*n,e[2]=.488603*r,e[3]=.488603*i,e[4]=1.092548*i*n,e[5]=1.092548*n*r,e[6]=.315392*(3*r*r-1),e[7]=1.092548*i*r,e[8]=.546274*(i*i-n*n)}}class Nc extends gc{constructor(t=new Dc,e=1){super(void 0,e),this.isLightProbe=!0,this.sh=t}copy(t){return super.copy(t),this.sh.copy(t.sh),this}fromJSON(t){return this.intensity=t.intensity,this.sh.fromArray(t.sh),this}toJSON(t){const e=super.toJSON(t);return e.object.sh=this.sh.toArray(),e}}class Oc extends uc{constructor(t){super(t),this.textures={}}load(t,e,i,n){const r=this,s=new mc(r.manager);s.setPath(r.path),s.setRequestHeader(r.requestHeader),s.setWithCredentials(r.withCredentials),s.load(t,(function(i){try{e(r.parse(JSON.parse(i)))}catch(e){n?n(e):console.error(e),r.manager.itemError(t)}}),i,n)}parse(t){const e=this.textures;function i(t){return void 0===e[t]&&console.warn("THREE.MaterialLoader: Undefined texture",t),e[t]}const n=Oc.createMaterialFromType(t.type);if(void 0!==t.uuid&&(n.uuid=t.uuid),void 0!==t.name&&(n.name=t.name),void 0!==t.color&&void 0!==n.color&&n.color.setHex(t.color),void 0!==t.roughness&&(n.roughness=t.roughness),void 0!==t.metalness&&(n.metalness=t.metalness),void 0!==t.sheen&&(n.sheen=t.sheen),void 0!==t.sheenColor&&(n.sheenColor=(new qt).setHex(t.sheenColor)),void 0!==t.sheenRoughness&&(n.sheenRoughness=t.sheenRoughness),void 0!==t.emissive&&void 0!==n.emissive&&n.emissive.setHex(t.emissive),void 0!==t.specular&&void 0!==n.specular&&n.specular.setHex(t.specular),void 0!==t.specularIntensity&&(n.specularIntensity=t.specularIntensity),void 0!==t.specularColor&&void 0!==n.specularColor&&n.specularColor.setHex(t.specularColor),void 0!==t.shininess&&(n.shininess=t.shininess),void 0!==t.clearcoat&&(n.clearcoat=t.clearcoat),void 0!==t.clearcoatRoughness&&(n.clearcoatRoughness=t.clearcoatRoughness),void 0!==t.iridescence&&(n.iridescence=t.iridescence),void 0!==t.iridescenceIOR&&(n.iridescenceIOR=t.iridescenceIOR),void 0!==t.iridescenceThicknessRange&&(n.iridescenceThicknessRange=t.iridescenceThicknessRange),void 0!==t.transmission&&(n.transmission=t.transmission),void 0!==t.thickness&&(n.thickness=t.thickness),void 0!==t.attenuationDistance&&(n.attenuationDistance=t.attenuationDistance),void 0!==t.attenuationColor&&void 0!==n.attenuationColor&&n.attenuationColor.setHex(t.attenuationColor),void 0!==t.fog&&(n.fog=t.fog),void 0!==t.flatShading&&(n.flatShading=t.flatShading),void 0!==t.blending&&(n.blending=t.blending),void 0!==t.combine&&(n.combine=t.combine),void 0!==t.side&&(n.side=t.side),void 0!==t.shadowSide&&(n.shadowSide=t.shadowSide),void 0!==t.opacity&&(n.opacity=t.opacity),void 0!==t.transparent&&(n.transparent=t.transparent),void 0!==t.alphaTest&&(n.alphaTest=t.alphaTest),void 0!==t.depthTest&&(n.depthTest=t.depthTest),void 0!==t.depthWrite&&(n.depthWrite=t.depthWrite),void 0!==t.colorWrite&&(n.colorWrite=t.colorWrite),void 0!==t.stencilWrite&&(n.stencilWrite=t.stencilWrite),void 0!==t.stencilWriteMask&&(n.stencilWriteMask=t.stencilWriteMask),void 0!==t.stencilFunc&&(n.stencilFunc=t.stencilFunc),void 0!==t.stencilRef&&(n.stencilRef=t.stencilRef),void 0!==t.stencilFuncMask&&(n.stencilFuncMask=t.stencilFuncMask),void 0!==t.stencilFail&&(n.stencilFail=t.stencilFail),void 0!==t.stencilZFail&&(n.stencilZFail=t.stencilZFail),void 0!==t.stencilZPass&&(n.stencilZPass=t.stencilZPass),void 0!==t.wireframe&&(n.wireframe=t.wireframe),void 0!==t.wireframeLinewidth&&(n.wireframeLinewidth=t.wireframeLinewidth),void 0!==t.wireframeLinecap&&(n.wireframeLinecap=t.wireframeLinecap),void 0!==t.wireframeLinejoin&&(n.wireframeLinejoin=t.wireframeLinejoin),void 0!==t.rotation&&(n.rotation=t.rotation),1!==t.linewidth&&(n.linewidth=t.linewidth),void 0!==t.dashSize&&(n.dashSize=t.dashSize),void 0!==t.gapSize&&(n.gapSize=t.gapSize),void 0!==t.scale&&(n.scale=t.scale),void 0!==t.polygonOffset&&(n.polygonOffset=t.polygonOffset),void 0!==t.polygonOffsetFactor&&(n.polygonOffsetFactor=t.polygonOffsetFactor),void 0!==t.polygonOffsetUnits&&(n.polygonOffsetUnits=t.polygonOffsetUnits),void 0!==t.dithering&&(n.dithering=t.dithering),void 0!==t.alphaToCoverage&&(n.alphaToCoverage=t.alphaToCoverage),void 0!==t.premultipliedAlpha&&(n.premultipliedAlpha=t.premultipliedAlpha),void 0!==t.visible&&(n.visible=t.visible),void 0!==t.toneMapped&&(n.toneMapped=t.toneMapped),void 0!==t.userData&&(n.userData=t.userData),void 0!==t.vertexColors&&("number"==typeof t.vertexColors?n.vertexColors=t.vertexColors>0:n.vertexColors=t.vertexColors),void 0!==t.uniforms)for(const e in t.uniforms){const r=t.uniforms[e];switch(n.uniforms[e]={},r.type){case"t":n.uniforms[e].value=i(r.value);break;case"c":n.uniforms[e].value=(new qt).setHex(r.value);break;case"v2":n.uniforms[e].value=(new Lt).fromArray(r.value);break;case"v3":n.uniforms[e].value=(new re).fromArray(r.value);break;case"v4":n.uniforms[e].value=(new Qt).fromArray(r.value);break;case"m3":n.uniforms[e].value=(new Rt).fromArray(r.value);break;case"m4":n.uniforms[e].value=(new Ne).fromArray(r.value);break;default:n.uniforms[e].value=r.value}}if(void 0!==t.defines&&(n.defines=t.defines),void 0!==t.vertexShader&&(n.vertexShader=t.vertexShader),void 0!==t.fragmentShader&&(n.fragmentShader=t.fragmentShader),void 0!==t.glslVersion&&(n.glslVersion=t.glslVersion),void 0!==t.extensions)for(const e in t.extensions)n.extensions[e]=t.extensions[e];if(void 0!==t.size&&(n.size=t.size),void 0!==t.sizeAttenuation&&(n.sizeAttenuation=t.sizeAttenuation),void 0!==t.map&&(n.map=i(t.map)),void 0!==t.matcap&&(n.matcap=i(t.matcap)),void 0!==t.alphaMap&&(n.alphaMap=i(t.alphaMap)),void 0!==t.bumpMap&&(n.bumpMap=i(t.bumpMap)),void 0!==t.bumpScale&&(n.bumpScale=t.bumpScale),void 0!==t.normalMap&&(n.normalMap=i(t.normalMap)),void 0!==t.normalMapType&&(n.normalMapType=t.normalMapType),void 0!==t.normalScale){let e=t.normalScale;!1===Array.isArray(e)&&(e=[e,e]),n.normalScale=(new Lt).fromArray(e)}return void 0!==t.displacementMap&&(n.displacementMap=i(t.displacementMap)),void 0!==t.displacementScale&&(n.displacementScale=t.displacementScale),void 0!==t.displacementBias&&(n.displacementBias=t.displacementBias),void 0!==t.roughnessMap&&(n.roughnessMap=i(t.roughnessMap)),void 0!==t.metalnessMap&&(n.metalnessMap=i(t.metalnessMap)),void 0!==t.emissiveMap&&(n.emissiveMap=i(t.emissiveMap)),void 0!==t.emissiveIntensity&&(n.emissiveIntensity=t.emissiveIntensity),void 0!==t.specularMap&&(n.specularMap=i(t.specularMap)),void 0!==t.specularIntensityMap&&(n.specularIntensityMap=i(t.specularIntensityMap)),void 0!==t.specularColorMap&&(n.specularColorMap=i(t.specularColorMap)),void 0!==t.envMap&&(n.envMap=i(t.envMap)),void 0!==t.envMapIntensity&&(n.envMapIntensity=t.envMapIntensity),void 0!==t.reflectivity&&(n.reflectivity=t.reflectivity),void 0!==t.refractionRatio&&(n.refractionRatio=t.refractionRatio),void 0!==t.lightMap&&(n.lightMap=i(t.lightMap)),void 0!==t.lightMapIntensity&&(n.lightMapIntensity=t.lightMapIntensity),void 0!==t.aoMap&&(n.aoMap=i(t.aoMap)),void 0!==t.aoMapIntensity&&(n.aoMapIntensity=t.aoMapIntensity),void 0!==t.gradientMap&&(n.gradientMap=i(t.gradientMap)),void 0!==t.clearcoatMap&&(n.clearcoatMap=i(t.clearcoatMap)),void 0!==t.clearcoatRoughnessMap&&(n.clearcoatRoughnessMap=i(t.clearcoatRoughnessMap)),void 0!==t.clearcoatNormalMap&&(n.clearcoatNormalMap=i(t.clearcoatNormalMap)),void 0!==t.clearcoatNormalScale&&(n.clearcoatNormalScale=(new Lt).fromArray(t.clearcoatNormalScale)),void 0!==t.iridescenceMap&&(n.iridescenceMap=i(t.iridescenceMap)),void 0!==t.iridescenceThicknessMap&&(n.iridescenceThicknessMap=i(t.iridescenceThicknessMap)),void 0!==t.transmissionMap&&(n.transmissionMap=i(t.transmissionMap)),void 0!==t.thicknessMap&&(n.thicknessMap=i(t.thicknessMap)),void 0!==t.sheenColorMap&&(n.sheenColorMap=i(t.sheenColorMap)),void 0!==t.sheenRoughnessMap&&(n.sheenRoughnessMap=i(t.sheenRoughnessMap)),n}setTextures(t){return this.textures=t,this}static createMaterialFromType(t){return new{ShadowMaterial:Pl,SpriteMaterial:sa,RawShaderMaterial:Il,ShaderMaterial:sn,PointsMaterial:Qa,MeshPhysicalMaterial:Nl,MeshStandardMaterial:Dl,MeshPhongMaterial:Ol,MeshToonMaterial:zl,MeshNormalMaterial:Ul,MeshLambertMaterial:Bl,MeshDepthMaterial:Us,MeshDistanceMaterial:Bs,MeshBasicMaterial:_i,MeshMatcapMaterial:Fl,LineDashedMaterial:kl,LineBasicMaterial:Va,Material:xi}[t]}}class zc{static decodeText(t){if("undefined"!=typeof TextDecoder)return(new TextDecoder).decode(t);let e="";for(let i=0,n=t.length;i0){this.source.connect(this.filters[0]);for(let t=1,e=this.filters.length;t0){this.source.disconnect(this.filters[0]);for(let t=1,e=this.filters.length;t0&&this._mixBufferRegionAdditive(i,n,this._addIndex*e,1,e);for(let t=e,r=e+e;t!==r;++t)if(i[t]!==i[t+e]){a.setValue(i,n);break}}saveOriginalState(){const t=this.binding,e=this.buffer,i=this.valueSize,n=i*this._origIndex;t.getValue(e,n);for(let t=i,r=n;t!==r;++t)e[t]=e[n+t%i];this._setIdentity(),this.cumulativeWeight=0,this.cumulativeWeightAdditive=0}restoreOriginalState(){const t=3*this.valueSize;this.binding.setValue(this.buffer,t)}_setAdditiveIdentityNumeric(){const t=this._addIndex*this.valueSize,e=t+this.valueSize;for(let i=t;i=.5)for(let n=0;n!==r;++n)t[e+n]=t[i+n]}_slerp(t,e,i,n){ne.slerpFlat(t,e,t,e,t,i,n)}_slerpAdditive(t,e,i,n,r){const s=this._workIndex*r;ne.multiplyQuaternionsFlat(t,s,t,e,t,i),ne.slerpFlat(t,e,t,e,t,s,n)}_lerp(t,e,i,n,r){const s=1-n;for(let a=0;a!==r;++a){const r=e+a;t[r]=t[r]*s+t[i+a]*n}}_lerpAdditive(t,e,i,n,r){for(let s=0;s!==r;++s){const r=e+s;t[r]=t[r]+t[i+s]*n}}}const sh="\\[\\]\\.:\\/",ah=new RegExp("[\\[\\]\\.:\\/]","g"),oh="[^\\[\\]\\.:\\/]",lh="[^"+sh.replace("\\.","")+"]",ch=new RegExp("^"+/((?:WC+[\/:])*)/.source.replace("WC",oh)+/(WCOD+)?/.source.replace("WCOD",lh)+/(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace("WC",oh)+/\.(WC+)(?:\[(.+)\])?/.source.replace("WC",oh)+"$"),hh=["material","materials","bones","map"];class uh{constructor(t,e,i){this.path=e,this.parsedPath=i||uh.parseTrackName(e),this.node=uh.findNode(t,this.parsedPath.nodeName)||t,this.rootNode=t,this.getValue=this._getValue_unbound,this.setValue=this._setValue_unbound}static create(t,e,i){return t&&t.isAnimationObjectGroup?new uh.Composite(t,e,i):new uh(t,e,i)}static sanitizeNodeName(t){return t.replace(/\s/g,"_").replace(ah,"")}static parseTrackName(t){const e=ch.exec(t);if(null===e)throw new Error("PropertyBinding: Cannot parse trackName: "+t);const i={nodeName:e[2],objectName:e[3],objectIndex:e[4],propertyName:e[5],propertyIndex:e[6]},n=i.nodeName&&i.nodeName.lastIndexOf(".");if(void 0!==n&&-1!==n){const t=i.nodeName.substring(n+1);-1!==hh.indexOf(t)&&(i.nodeName=i.nodeName.substring(0,n),i.objectName=t)}if(null===i.propertyName||0===i.propertyName.length)throw new Error("PropertyBinding: can not parse propertyName from trackName: "+t);return i}static findNode(t,e){if(void 0===e||""===e||"."===e||-1===e||e===t.name||e===t.uuid)return t;if(t.skeleton){const i=t.skeleton.getBoneByName(e);if(void 0!==i)return i}if(t.children){const i=function(t){for(let n=0;n0){const t=this._interpolants,e=this._propertyBindings;if(this.blendMode===st)for(let i=0,n=t.length;i!==n;++i)t[i].evaluate(s),e[i].accumulateAdditive(a);else for(let i=0,r=t.length;i!==r;++i)t[i].evaluate(s),e[i].accumulate(n,a)}}_updateWeight(t){let e=0;if(this.enabled){e=this.weight;const i=this._weightInterpolant;if(null!==i){const n=i.evaluate(t)[0];e*=n,t>i.parameterPositions[1]&&(this.stopFading(),0===n&&(this.enabled=!1))}}return this._effectiveWeight=e,e}_updateTimeScale(t){let e=0;if(!this.paused){e=this.timeScale;const i=this._timeScaleInterpolant;if(null!==i){e*=i.evaluate(t)[0],t>i.parameterPositions[1]&&(this.stopWarping(),0===e?this.paused=!0:this.timeScale=e)}}return this._effectiveTimeScale=e,e}_updateTime(t){const e=this._clip.duration,i=this.loop;let n=this.time+t,r=this._loopCount;const s=2202===i;if(0===t)return-1===r?n:s&&1==(1&r)?e-n:n;if(2200===i){-1===r&&(this._loopCount=0,this._setEndings(!0,!0,!1));t:{if(n>=e)n=e;else{if(!(n<0)){this.time=n;break t}n=0}this.clampWhenFinished?this.paused=!0:this.enabled=!1,this.time=n,this._mixer.dispatchEvent({type:"finished",action:this,direction:t<0?-1:1})}}else{if(-1===r&&(t>=0?(r=0,this._setEndings(!0,0===this.repetitions,s)):this._setEndings(0===this.repetitions,!0,s)),n>=e||n<0){const i=Math.floor(n/e);n-=e*i,r+=Math.abs(i);const a=this.repetitions-r;if(a<=0)this.clampWhenFinished?this.paused=!0:this.enabled=!1,n=t>0?e:0,this.time=n,this._mixer.dispatchEvent({type:"finished",action:this,direction:t>0?1:-1});else{if(1===a){const e=t<0;this._setEndings(e,!e,s)}else this._setEndings(!1,!1,s);this._loopCount=r,this.time=n,this._mixer.dispatchEvent({type:"loop",action:this,loopDelta:i})}}else this.time=n;if(s&&1==(1&r))return e-n}return n}_setEndings(t,e,i){const n=this._interpolantSettings;i?(n.endingStart=it,n.endingEnd=it):(n.endingStart=t?this.zeroSlopeAtStart?it:et:nt,n.endingEnd=e?this.zeroSlopeAtEnd?it:et:nt)}_scheduleFading(t,e,i){const n=this._mixer,r=n.time;let s=this._weightInterpolant;null===s&&(s=n._lendControlInterpolant(),this._weightInterpolant=s);const a=s.parameterPositions,o=s.sampleValues;return a[0]=r,o[0]=e,a[1]=r+t,o[1]=i,this}}const ph=new Float32Array(1);class mh{constructor(t){this.value=t}clone(){return new mh(void 0===this.value.clone?this.value:this.value.clone())}}let fh=0;function gh(t,e){return t.distance-e.distance}function vh(t,e,i,n){if(t.layers.test(e.layers)&&t.raycast(e,i),!0===n){const n=t.children;for(let t=0,r=n.length;t>-e-14,n[256|t]=1024>>-e-14|32768,r[t]=-e-1,r[256|t]=-e-1):e<=15?(n[t]=e+15<<10,n[256|t]=e+15<<10|32768,r[t]=13,r[256|t]=13):e<128?(n[t]=31744,n[256|t]=64512,r[t]=24,r[256|t]=24):(n[t]=31744,n[256|t]=64512,r[t]=13,r[256|t]=13)}const s=new Uint32Array(2048),a=new Uint32Array(64),o=new Uint32Array(64);for(let t=1;t<1024;++t){let e=t<<13,i=0;for(;0==(8388608&e);)e<<=1,i-=8388608;e&=-8388609,i+=947912704,s[t]=e|i}for(let t=1024;t<2048;++t)s[t]=939524096+(t-1024<<13);for(let t=1;t<31;++t)a[t]=t<<23;a[31]=1199570944,a[32]=2147483648;for(let t=33;t<63;++t)a[t]=2147483648+(t-32<<23);a[63]=3347054592;for(let t=1;t<64;++t)32!==t&&(o[t]=1024);return{floatView:e,uint32View:i,baseTable:n,shiftTable:r,mantissaTable:s,exponentTable:a,offsetTable:o}}var Gh=Object.freeze({__proto__:null,toHalfFloat:function(t){Math.abs(t)>65504&&console.warn("THREE.DataUtils.toHalfFloat(): Value out of range."),t=yt(t,-65504,65504),Fh.floatView[0]=t;const e=Fh.uint32View[0],i=e>>23&511;return Fh.baseTable[i]+((8388607&e)>>Fh.shiftTable[i])},fromHalfFloat:function(t){const e=t>>10;return Fh.uint32View[0]=Fh.mantissaTable[Fh.offsetTable[e]+(1023&t)]+Fh.exponentTable[e],Fh.floatView[0]}});"undefined"!=typeof __THREE_DEVTOOLS__&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("register",{detail:{revision:e}})),"undefined"!=typeof window&&(window.__THREE__?console.warn("WARNING: Multiple instances of Three.js being imported."):window.__THREE__=e),t.ACESFilmicToneMapping=4,t.AddEquation=i,t.AddOperation=2,t.AdditiveAnimationBlendMode=st,t.AdditiveBlending=2,t.AlphaFormat=1021,t.AlwaysDepth=1,t.AlwaysStencilFunc=519,t.AmbientLight=Pc,t.AmbientLightProbe=class extends Nc{constructor(t,e=1){super(void 0,e),this.isAmbientLightProbe=!0;const i=(new qt).set(t);this.sh.coefficients[0].set(i.r,i.g,i.b).multiplyScalar(2*Math.sqrt(Math.PI))}},t.AnimationClip=ac,t.AnimationLoader=class extends uc{constructor(t){super(t)}load(t,e,i,n){const r=this,s=new mc(this.manager);s.setPath(this.path),s.setRequestHeader(this.requestHeader),s.setWithCredentials(this.withCredentials),s.load(t,(function(i){try{e(r.parse(JSON.parse(i)))}catch(e){n?n(e):console.error(e),r.manager.itemError(t)}}),i,n)}parse(t){const e=[];for(let i=0;i=0;--e)t[e].stop();return this}update(t){t*=this.timeScale;const e=this._actions,i=this._nActiveActions,n=this.time+=t,r=Math.sign(t),s=this._accuIndex^=1;for(let a=0;a!==i;++a){e[a]._update(n,t,r,s)}const a=this._bindings,o=this._nActiveBindings;for(let t=0;t!==o;++t)a[t].apply(s);return this}setTime(t){this.time=0;for(let t=0;t=r){const s=r++,c=t[s];e[c.uuid]=l,t[l]=c,e[o]=s,t[s]=a;for(let t=0,e=n;t!==e;++t){const e=i[t],n=e[s],r=e[l];e[l]=n,e[s]=r}}}this.nCachedObjects_=r}uncache(){const t=this._objects,e=this._indicesByUUID,i=this._bindings,n=i.length;let r=this.nCachedObjects_,s=t.length;for(let a=0,o=arguments.length;a!==o;++a){const o=arguments[a].uuid,l=e[o];if(void 0!==l)if(delete e[o],l0&&(e[a.uuid]=l),t[l]=a,t.pop();for(let t=0,e=n;t!==e;++t){const e=i[t];e[l]=e[r],e.pop()}}}this.nCachedObjects_=r}subscribe_(t,e){const i=this._bindingsIndicesByPath;let n=i[t];const r=this._bindings;if(void 0!==n)return r[n];const s=this._paths,a=this._parsedPaths,o=this._objects,l=o.length,c=this.nCachedObjects_,h=new Array(l);n=r.length,i[t]=n,s.push(t),a.push(e),r.push(h);for(let i=c,n=o.length;i!==n;++i){const n=o[i];h[i]=new uh(n,t,e)}return h}unsubscribe_(t){const e=this._bindingsIndicesByPath,i=e[t];if(void 0!==i){const n=this._paths,r=this._parsedPaths,s=this._bindings,a=s.length-1,o=s[a];e[t[a]]=i,s[i]=o,s.pop(),r[i]=r[a],r.pop(),n[i]=n[a],n.pop()}}},t.AnimationUtils=Xl,t.ArcCurve=co,t.ArrayCamera=Hs,t.ArrowHelper=class extends si{constructor(t=new re(0,0,1),e=new re(0,0,0),i=1,n=16776960,r=.2*i,s=.2*r){super(),this.type="ArrowHelper",void 0===Uh&&(Uh=new Di,Uh.setAttribute("position",new Ti([0,0,0,0,1,0],3)),Bh=new Do(0,.5,1,5,1),Bh.translate(0,-.5,0)),this.position.copy(e),this.line=new Ya(Uh,new Va({color:n,toneMapped:!1})),this.line.matrixAutoUpdate=!1,this.add(this.line),this.cone=new Ki(Bh,new _i({color:n,toneMapped:!1})),this.cone.matrixAutoUpdate=!1,this.add(this.cone),this.setDirection(t),this.setLength(i,r,s)}setDirection(t){if(t.y>.99999)this.quaternion.set(0,0,0,1);else if(t.y<-.99999)this.quaternion.set(1,0,0,0);else{zh.set(t.z,0,-t.x).normalize();const e=Math.acos(t.y);this.quaternion.setFromAxisAngle(zh,e)}}setLength(t,e=.2*t,i=.2*e){this.line.scale.set(1,Math.max(1e-4,t-e),1),this.line.updateMatrix(),this.cone.scale.set(i,e,i),this.cone.position.y=t,this.cone.updateMatrix()}setColor(t){this.line.material.color.set(t),this.cone.material.color.set(t)}copy(t){return super.copy(t,!1),this.line.copy(t.line),this.cone.copy(t.cone),this}dispose(){this.line.geometry.dispose(),this.line.material.dispose(),this.cone.geometry.dispose(),this.cone.material.dispose()}},t.Audio=Qc,t.AudioAnalyser=class{constructor(t,e=2048){this.analyser=t.context.createAnalyser(),this.analyser.fftSize=e,this.data=new Uint8Array(this.analyser.frequencyBinCount),t.getOutput().connect(this.analyser)}getFrequencyData(){return this.analyser.getByteFrequencyData(this.data),this.data}getAverageFrequency(){let t=0;const e=this.getFrequencyData();for(let i=0;ithis.max.x||t.ythis.max.y)}containsBox(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y}getParameter(t,e){return e.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y))}intersectsBox(t){return!(t.max.xthis.max.x||t.max.ythis.max.y)}clampPoint(t,e){return e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return xh.copy(t).clamp(this.min,this.max).sub(t).length()}intersect(t){return this.min.max(t.min),this.max.min(t.max),this}union(t){return this.min.min(t.min),this.max.max(t.max),this}translate(t){return this.min.add(t),this.max.add(t),this}equals(t){return t.min.equals(this.min)&&t.max.equals(this.max)}},t.Box3=oe,t.Box3Helper=class extends Ka{constructor(t,e=16776960){const i=new Uint16Array([0,1,1,2,2,3,3,0,4,5,5,6,6,7,7,4,0,4,1,5,2,6,3,7]),n=new Di;n.setIndex(new bi(i,1)),n.setAttribute("position",new Ti([1,1,1,-1,1,1,-1,-1,1,1,-1,1,1,1,-1,-1,1,-1,-1,-1,-1,1,-1,-1],3)),super(n,new Va({color:e,toneMapped:!1})),this.box=t,this.type="Box3Helper",this.geometry.computeBoundingSphere()}updateMatrixWorld(t){const e=this.box;e.isEmpty()||(e.getCenter(this.position),e.getSize(this.scale),this.scale.multiplyScalar(.5),super.updateMatrixWorld(t))}dispose(){this.geometry.dispose(),this.material.dispose()}},t.BoxBufferGeometry=class extends Qi{constructor(t,e,i,n,r,s){console.warn("THREE.BoxBufferGeometry has been renamed to THREE.BoxGeometry."),super(t,e,i,n,r,s)}},t.BoxGeometry=Qi,t.BoxHelper=class extends Ka{constructor(t,e=16776960){const i=new Uint16Array([0,1,1,2,2,3,3,0,4,5,5,6,6,7,7,4,0,4,1,5,2,6,3,7]),n=new Float32Array(24),r=new Di;r.setIndex(new bi(i,1)),r.setAttribute("position",new bi(n,3)),super(r,new Va({color:e,toneMapped:!1})),this.object=t,this.type="BoxHelper",this.matrixAutoUpdate=!1,this.update()}update(t){if(void 0!==t&&console.warn("THREE.BoxHelper: .update() has no longer arguments."),void 0!==this.object&&Oh.setFromObject(this.object),Oh.isEmpty())return;const e=Oh.min,i=Oh.max,n=this.geometry.attributes.position,r=n.array;r[0]=i.x,r[1]=i.y,r[2]=i.z,r[3]=e.x,r[4]=i.y,r[5]=i.z,r[6]=e.x,r[7]=e.y,r[8]=i.z,r[9]=i.x,r[10]=e.y,r[11]=i.z,r[12]=i.x,r[13]=i.y,r[14]=e.z,r[15]=e.x,r[16]=i.y,r[17]=e.z,r[18]=e.x,r[19]=e.y,r[20]=e.z,r[21]=i.x,r[22]=e.y,r[23]=e.z,n.needsUpdate=!0,this.geometry.computeBoundingSphere()}setFromObject(t){return this.object=t,this.update(),this}copy(t,e){return super.copy(t,e),this.object=t.object,this}dispose(){this.geometry.dispose(),this.material.dispose()}},t.BufferAttribute=bi,t.BufferGeometry=Di,t.BufferGeometryLoader=Bc,t.ByteType=1010,t.Cache=lc,t.Camera=an,t.CameraHelper=class extends Ka{constructor(t){const e=new Di,i=new Va({color:16777215,vertexColors:!0,toneMapped:!1}),n=[],r=[],s={};function a(t,e){o(t),o(e)}function o(t){n.push(0,0,0),r.push(0,0,0),void 0===s[t]&&(s[t]=[]),s[t].push(n.length/3-1)}a("n1","n2"),a("n2","n4"),a("n4","n3"),a("n3","n1"),a("f1","f2"),a("f2","f4"),a("f4","f3"),a("f3","f1"),a("n1","f1"),a("n2","f2"),a("n3","f3"),a("n4","f4"),a("p","n1"),a("p","n2"),a("p","n3"),a("p","n4"),a("u1","u2"),a("u2","u3"),a("u3","u1"),a("c","t"),a("p","c"),a("cn1","cn2"),a("cn3","cn4"),a("cf1","cf2"),a("cf3","cf4"),e.setAttribute("position",new Ti(n,3)),e.setAttribute("color",new Ti(r,3)),super(e,i),this.type="CameraHelper",this.camera=t,this.camera.updateProjectionMatrix&&this.camera.updateProjectionMatrix(),this.matrix=t.matrixWorld,this.matrixAutoUpdate=!1,this.pointMap=s,this.update();const l=new qt(16755200),c=new qt(16711680),h=new qt(43775),u=new qt(16777215),d=new qt(3355443);this.setColors(l,c,h,u,d)}setColors(t,e,i,n,r){const s=this.geometry.getAttribute("color");s.setXYZ(0,t.r,t.g,t.b),s.setXYZ(1,t.r,t.g,t.b),s.setXYZ(2,t.r,t.g,t.b),s.setXYZ(3,t.r,t.g,t.b),s.setXYZ(4,t.r,t.g,t.b),s.setXYZ(5,t.r,t.g,t.b),s.setXYZ(6,t.r,t.g,t.b),s.setXYZ(7,t.r,t.g,t.b),s.setXYZ(8,t.r,t.g,t.b),s.setXYZ(9,t.r,t.g,t.b),s.setXYZ(10,t.r,t.g,t.b),s.setXYZ(11,t.r,t.g,t.b),s.setXYZ(12,t.r,t.g,t.b),s.setXYZ(13,t.r,t.g,t.b),s.setXYZ(14,t.r,t.g,t.b),s.setXYZ(15,t.r,t.g,t.b),s.setXYZ(16,t.r,t.g,t.b),s.setXYZ(17,t.r,t.g,t.b),s.setXYZ(18,t.r,t.g,t.b),s.setXYZ(19,t.r,t.g,t.b),s.setXYZ(20,t.r,t.g,t.b),s.setXYZ(21,t.r,t.g,t.b),s.setXYZ(22,t.r,t.g,t.b),s.setXYZ(23,t.r,t.g,t.b),s.setXYZ(24,e.r,e.g,e.b),s.setXYZ(25,e.r,e.g,e.b),s.setXYZ(26,e.r,e.g,e.b),s.setXYZ(27,e.r,e.g,e.b),s.setXYZ(28,e.r,e.g,e.b),s.setXYZ(29,e.r,e.g,e.b),s.setXYZ(30,e.r,e.g,e.b),s.setXYZ(31,e.r,e.g,e.b),s.setXYZ(32,i.r,i.g,i.b),s.setXYZ(33,i.r,i.g,i.b),s.setXYZ(34,i.r,i.g,i.b),s.setXYZ(35,i.r,i.g,i.b),s.setXYZ(36,i.r,i.g,i.b),s.setXYZ(37,i.r,i.g,i.b),s.setXYZ(38,n.r,n.g,n.b),s.setXYZ(39,n.r,n.g,n.b),s.setXYZ(40,r.r,r.g,r.b),s.setXYZ(41,r.r,r.g,r.b),s.setXYZ(42,r.r,r.g,r.b),s.setXYZ(43,r.r,r.g,r.b),s.setXYZ(44,r.r,r.g,r.b),s.setXYZ(45,r.r,r.g,r.b),s.setXYZ(46,r.r,r.g,r.b),s.setXYZ(47,r.r,r.g,r.b),s.setXYZ(48,r.r,r.g,r.b),s.setXYZ(49,r.r,r.g,r.b),s.needsUpdate=!0}update(){const t=this.geometry,e=this.pointMap;Dh.projectionMatrixInverse.copy(this.camera.projectionMatrixInverse),Nh("c",e,t,Dh,0,0,-1),Nh("t",e,t,Dh,0,0,1),Nh("n1",e,t,Dh,-1,-1,-1),Nh("n2",e,t,Dh,1,-1,-1),Nh("n3",e,t,Dh,-1,1,-1),Nh("n4",e,t,Dh,1,1,-1),Nh("f1",e,t,Dh,-1,-1,1),Nh("f2",e,t,Dh,1,-1,1),Nh("f3",e,t,Dh,-1,1,1),Nh("f4",e,t,Dh,1,1,1),Nh("u1",e,t,Dh,.7,1.1,-1),Nh("u2",e,t,Dh,-.7,1.1,-1),Nh("u3",e,t,Dh,0,2,-1),Nh("cf1",e,t,Dh,-1,0,1),Nh("cf2",e,t,Dh,1,0,1),Nh("cf3",e,t,Dh,0,-1,1),Nh("cf4",e,t,Dh,0,1,1),Nh("cn1",e,t,Dh,-1,0,-1),Nh("cn2",e,t,Dh,1,0,-1),Nh("cn3",e,t,Dh,0,-1,-1),Nh("cn4",e,t,Dh,0,1,-1),t.getAttribute("position").needsUpdate=!0}dispose(){this.geometry.dispose(),this.material.dispose()}},t.CanvasTexture=class extends $t{constructor(t,e,i,n,r,s,a,o,l){super(t,e,i,n,r,s,a,o,l),this.isCanvasTexture=!0,this.needsUpdate=!0}},t.CapsuleBufferGeometry=class extends Po{constructor(t,e,i,n){console.warn("THREE.CapsuleBufferGeometry has been renamed to THREE.CapsuleGeometry."),super(t,e,i,n)}},t.CapsuleGeometry=Po,t.CatmullRomCurve3=go,t.CineonToneMapping=3,t.CircleBufferGeometry=class extends Io{constructor(t,e,i,n){console.warn("THREE.CircleBufferGeometry has been renamed to THREE.CircleGeometry."),super(t,e,i,n)}},t.CircleGeometry=Io,t.ClampToEdgeWrapping=h,t.Clock=Xc,t.Color=qt,t.ColorKeyframeTrack=tc,t.ColorManagement=Ft,t.CompressedArrayTexture=class extends ao{constructor(t,e,i,n,r,s){super(t,e,i,r,s),this.isCompressedArrayTexture=!0,this.image.depth=n,this.wrapR=h}},t.CompressedTexture=ao,t.CompressedTextureLoader=class extends uc{constructor(t){super(t)}load(t,e,i,n){const r=this,s=[],a=new ao,o=new mc(this.manager);o.setPath(this.path),o.setResponseType("arraybuffer"),o.setRequestHeader(this.requestHeader),o.setWithCredentials(r.withCredentials);let l=0;function c(c){o.load(t[c],(function(t){const i=r.parse(t,!0);s[c]={width:i.width,height:i.height,format:i.format,mipmaps:i.mipmaps},l+=1,6===l&&(1===i.mipmapCount&&(a.minFilter=f),a.image=s,a.format=i.format,a.needsUpdate=!0,e&&e(a))}),i,n)}if(Array.isArray(t))for(let e=0,i=t.length;e0){const i=new cc(e);r=new fc(i),r.setCrossOrigin(this.crossOrigin);for(let e=0,i=t.length;e0){n=new fc(this.manager),n.setCrossOrigin(this.crossOrigin);for(let e=0,n=t.length;e1)for(let i=0;iNumber.EPSILON){if(l<0&&(i=e[s],o=-o,a=e[r],l=-l),t.ya.y)continue;if(t.y===i.y){if(t.x===i.x)return!0}else{const e=l*(t.x-i.x)-o*(t.y-i.y);if(0===e)return!0;if(e<0)continue;n=!n}}else{if(t.y!==i.y)continue;if(a.x<=t.x&&t.x<=i.x||i.x<=t.x&&t.x<=a.x)return!0}}return n}const i=ml.isClockWise,n=this.subPaths;if(0===n.length)return[];let r,s,a;const o=[];if(1===n.length)return s=n[0],a=new Vo,a.curves=s.curves,o.push(a),o;let l=!i(n[0].getPoints());l=t?!l:l;const c=[],h=[];let u,d,p=[],m=0;h[m]=void 0,p[m]=[];for(let e=0,a=n.length;e1){let t=!1,i=0;for(let t=0,e=h.length;t0&&!1===t&&(p=c)}for(let t=0,e=h.length;t=t.HAVE_CURRENT_DATA&&(this.needsUpdate=!0)}},t.WebGL1Renderer=$s,t.WebGL3DRenderTarget=class extends te{constructor(t=1,e=1,i=1){super(t,e),this.isWebGL3DRenderTarget=!0,this.depth=i,this.texture=new ie(null,t,e,i),this.texture.isRenderTargetTexture=!0}},t.WebGLArrayRenderTarget=class extends te{constructor(t=1,e=1,i=1){super(t,e),this.isWebGLArrayRenderTarget=!0,this.depth=i,this.texture=new ee(null,t,e,i),this.texture.isRenderTargetTexture=!0}},t.WebGLCubeRenderTarget=un,t.WebGLMultipleRenderTargets=class extends te{constructor(t=1,e=1,i=1,n={}){super(t,e,n),this.isWebGLMultipleRenderTargets=!0;const r=this.texture;this.texture=[];for(let t=0;t