grechka-game/src3d/ui/start-menu.js

576 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==================== 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 = '<div class="mc-bg"></div><div class="mc-dirt"></div>';
const box = document.createElement('div');
box.className = 'mc-box';
// Title bar
const titleBar = document.createElement('div');
titleBar.className = 'mc-titlebar';
titleBar.innerHTML = '<div class="mc-title">⛏️ GrechkaCraft</div>' +
'<div class="mc-subtitle">' + (isTgWebApp ? ('Привет, ' + (tgUser?.first_name || 'Игрок') + '!') : 'Voxel-мир в браузере') + '</div>';
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 = '🔗 Твой ник: <b>' + data.nickname + '</b>';
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 + ' <b>' + f.nickname + '</b>';
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(() => {});
}
});
}