// ==================== 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; } .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;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 (only if not already set) if (!state.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()) { state.playerName = nameInput.value.trim(); localStorage.setItem('minegrechka_playerName', state.playerName); } }); body.appendChild(nameInput); } box.appendChild(body); overlay.appendChild(box); document.body.appendChild(overlay); // Pre-fill player name from TG if available if (isTgWebApp && tgUser && !state.playerName) { state.playerName = tgUser.username || tgUser.first_name || 'Игрок'; 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(() => {}); } }); }