From 528a698cf0da705511166e132dbfbb37ceb2adc9 Mon Sep 17 00:00:00 2001 From: Mk Date: Tue, 26 May 2026 20:33:39 +0000 Subject: [PATCH] feat: TG Mini App + start menu (new world, join by code, continue game) + short world codes + TG save/load + share invites --- game.js | 328 ++++++++++++++++++++++++++++++++++++++++++++--------- index.html | 3 +- 2 files changed, 274 insertions(+), 57 deletions(-) diff --git a/game.js b/game.js index fd4151a..1a7ea26 100644 --- a/game.js +++ b/game.js @@ -55,73 +55,260 @@ function customConfirm(msg, onYes) { 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; - // Запрашиваем имя игрока, если его нет - if (!playerName) { - playerName = prompt('Введите ваше имя для игры:') || 'Игрок'; - localStorage.setItem('minegrechka_playerName', playerName); - console.log('Player name set:', playerName); + // 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; } - // Берём worldId из URL или генерируем новый - console.log('Current URL:', window.location.href); - const worldParam = urlParams.get('world'); - console.log('world param:', worldParam); + 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; + } + + const overlay = document.createElement('div'); + overlay.id = 'startMenu'; + overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;z-index:99999;font-family:Arial,sans-serif;'; + + const box = document.createElement('div'); + box.style.cssText = 'background:#1a1a2e;border:2px solid #e94560;border-radius:16px;padding:32px;max-width:400px;width:90%;text-align:center;color:#eee;'; + + // Title + const title = document.createElement('div'); + title.style.cssText = 'font-size:28px;font-weight:bold;margin-bottom:8px;color:#e94560;'; + title.textContent = '🏗️ GrechkaCraft'; + box.appendChild(title); + + const subtitle = document.createElement('div'); + subtitle.style.cssText = 'font-size:14px;color:#888;margin-bottom:24px;'; + subtitle.textContent = isTgWebApp ? ('Привет, ' + (tgUser?.first_name || 'Игрок') + '!') : 'Voxel-мир в браузере'; + box.appendChild(subtitle); + + // New World button + const btnNew = document.createElement('button'); + btnNew.style.cssText = 'width:100%;padding:14px;margin-bottom:12px;background:#27ae60;color:#fff;border:none;border-radius:10px;font-size:18px;cursor:pointer;font-weight:bold;'; + btnNew.textContent = '🌍 Новый мир'; + btnNew.onmouseover = () => btnNew.style.background = '#2ecc71'; + btnNew.onmouseout = () => btnNew.style.background = '#27ae60'; + btnNew.onclick = () => { + worldId = generateShortWorldCode(); + overlay.remove(); + resolve(worldId); + }; + box.appendChild(btnNew); + + // Join World button + const btnJoin = document.createElement('button'); + btnJoin.style.cssText = 'width:100%;padding:14px;margin-bottom:12px;background:#2980b9;color:#fff;border:none;border-radius:10px;font-size:18px;cursor:pointer;font-weight:bold;'; + btnJoin.textContent = '🔗 Вступить по коду'; + btnJoin.onmouseover = () => btnJoin.style.background = '#3498db'; + btnJoin.onmouseout = () => btnJoin.style.background = '#2980b9'; + btnJoin.onclick = () => { + const codeInput = document.createElement('input'); + codeInput.type = 'text'; + codeInput.placeholder = 'Введите код мира (например: bkh237)'; + codeInput.style.cssText = 'width:90%;padding:12px;margin:12px 0;border:2px solid #3498db;border-radius:8px;background:#16213e;color:#fff;font-size:16px;text-align:center;text-transform:lowercase;'; + codeInput.maxLength = 12; + box.innerHTML = ''; + const backTitle = document.createElement('div'); + backTitle.style.cssText = 'font-size:20px;font-weight:bold;margin-bottom:16px;color:#3498db;'; + backTitle.textContent = '🔗 Вступить в мир'; + box.appendChild(backTitle); + box.appendChild(codeInput); + const btnGo = document.createElement('button'); + btnGo.style.cssText = 'width:100%;padding:14px;margin-top:12px;background:#2980b9;color:#fff;border:none;border-radius:10px;font-size:18px;cursor:pointer;font-weight:bold;'; + btnGo.textContent = 'Войти'; + btnGo.onclick = () => { + const code = codeInput.value.trim().toLowerCase(); + if (code.length >= 3) { + worldId = code; + overlay.remove(); + resolve(worldId); + } + }; + box.appendChild(btnGo); + const btnBack = document.createElement('button'); + btnBack.style.cssText = 'width:100%;padding:10px;margin-top:8px;background:transparent;color:#888;border:1px solid #444;border-radius:8px;cursor:pointer;font-size:14px;'; + btnBack.textContent = '← Назад'; + btnBack.onclick = () => { overlay.remove(); showStartMenu().then(resolve); }; + box.appendChild(btnBack); + codeInput.focus(); + codeInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') btnGo.click(); }); + }; + box.appendChild(btnJoin); + + // History / Continue button (only if TG user with saved game OR localStorage save) + const hasLocalSave = !!localStorage.getItem('minegrechka_save'); + if (isTgWebApp || hasLocalSave) { + const btnHistory = document.createElement('button'); + btnHistory.style.cssText = 'width:100%;padding:14px;margin-bottom:12px;background:#8e44ad;color:#fff;border:none;border-radius:10px;font-size:18px;cursor:pointer;font-weight:bold;'; + btnHistory.textContent = '💾 Продолжить игру'; + btnHistory.onmouseover = () => btnHistory.style.background = '#9b59b6'; + btnHistory.onmouseout = () => btnHistory.style.background = '#8e44ad'; + btnHistory.onclick = async () => { + // TG user: load save from server + 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(); + resolve(worldId); + return; + } + } catch(e) { console.warn('[TG] Load save failed:', e); } + } + // Fallback: load from latest localStorage save + if (hasLocalSave) { + // Generate world from save (worldSeed based) + worldId = generateShortWorldCode(); + overlay.remove(); + resolve(worldId); + return; + } + customAlert('Сохранений не найдено'); + }; + box.appendChild(btnHistory); + } + + // TG Share button (only in TG context) + if (isTgWebApp && window.Telegram && Telegram.WebApp.switchInlineQuery) { + const btnInvite = document.createElement('button'); + btnInvite.style.cssText = 'width:100%;padding:10px;margin-top:8px;background:transparent;color:#3498db;border:1px solid #3498db;border-radius:8px;cursor:pointer;font-size:14px;'; + btnInvite.textContent = '📤 Пригласить друзей'; + btnInvite.onclick = () => { + const code = worldId || generateShortWorldCode(); + Telegram.WebApp.switchInlineQuery('play ' + code); + }; + box.appendChild(btnInvite); + } + + // Player name input (if not set) + if (!playerName) { + const nameInput = document.createElement('input'); + nameInput.type = 'text'; + nameInput.placeholder = 'Ваше имя'; + nameInput.style.cssText = 'width:90%;padding:10px;margin:16px 0 8px;border:2px solid #e94560;border-radius:8px;background:#16213e;color:#fff;font-size:16px;text-align:center;'; + nameInput.value = tgUser ? (tgUser.username || tgUser.first_name || '') : ''; + nameInput.maxLength = 20; + box.appendChild(nameInput); + + // Update playerName on input + nameInput.addEventListener('input', () => { + if (nameInput.value.trim()) { + playerName = nameInput.value.trim(); + localStorage.setItem('minegrechka_playerName', playerName); + } + }); + box.appendChild(document.createElement('br')); + } + + overlay.appendChild(box); + document.body.appendChild(overlay); + + // If TG user without world in URL, also try to auto-load their 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) { + // Update the "Continue" button to show the world code + const historyBtn = box.querySelector('button:nth-child(5)'); // rough + } + }) + .catch(() => {}); + } + }); + } - // Проверяем на null, undefined или пустую строку - worldId = (worldParam && worldParam.trim() !== '') ? worldParam : null; + // ==================== GAME LAUNCH ==================== + // Override the old auto-join logic + let gameLaunched = false; - 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); + 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); - 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!'); + if (typeof history !== 'undefined' && history.replaceState) { + history.replaceState(null, '', newUrl.toString()); } - } catch (e) { - console.error('Error updating URL:', e); - } + } catch(e) {} - console.log('Generated new worldId for browser:', worldId); + 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')); } - - 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; @@ -190,7 +377,7 @@ function customConfirm(msg, onYes) { isMultiplayer = true; // Присоединяемся к миру - socket.emit('join_world', { world_id: worldId, player_name: playerName }); + socket.emit('join_world', { world_id: worldId, player_name: playerName, tg_id: (tgUser && tgUser.id) ? tgUser.id : null }); // Показываем в UI worldIdEl.textContent = worldId; @@ -576,8 +763,13 @@ function customConfirm(msg, onYes) { }); // ==================== ИНИЦИАЛИЗАЦИЯ СОКЕТА ==================== - // Инициализируем socket - initSocket(); + // Show start menu first, then init socket + showStartMenu().then((wid) => { + worldId = wid; + // Send tg_id if TG user + initSocket(); + launchGame(); + }); // ==================== ЗВУКОВОЙ ДВИЖОК ==================== const sounds = {}; @@ -2756,6 +2948,16 @@ registerProcessor('voice-playback', VoicePlaybackProcessor); 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); @@ -2766,7 +2968,21 @@ registerProcessor('voice-playback', VoicePlaybackProcessor); } function loadGame(){ - return new Promise((resolve, reject) => { + 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); diff --git a/index.html b/index.html index a6e3b6b..ca0b9fc 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,7 @@ GrechkaCraft: Multiplayer + @@ -94,6 +95,6 @@ - +