feat: minecraft-style start menu with proper spacing

This commit is contained in:
Mk 2026-05-27 04:54:39 +00:00
parent f14b61d7d9
commit 547ad86989
2 changed files with 211 additions and 121 deletions

330
game.js
View File

@ -106,87 +106,143 @@ function customConfirm(msg, onYes) {
return; 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'); const overlay = document.createElement('div');
overlay.className = 'mc-overlay';
overlay.id = 'startMenu'; 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;'; overlay.innerHTML = '<div class="mc-bg"></div><div class="mc-dirt"></div>';
const box = document.createElement('div'); 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;'; box.className = 'mc-box';
// Title // Title bar
const title = document.createElement('div'); const titleBar = document.createElement('div');
title.style.cssText = 'font-size:28px;font-weight:bold;margin-bottom:8px;color:#e94560;'; titleBar.className = 'mc-titlebar';
title.textContent = '🏗️ GrechkaCraft'; titleBar.innerHTML = '<div class="mc-title">⛏️ GrechkaCraft</div>' +
box.appendChild(title); '<div class="mc-subtitle">' + (isTgWebApp ? ('Привет, ' + (tgUser?.first_name || 'Игрок') + '!') : 'Voxel-мир в браузере') + '</div>';
box.appendChild(titleBar);
const subtitle = document.createElement('div'); const body = document.createElement('div');
subtitle.style.cssText = 'font-size:14px;color:#888;margin-bottom:24px;'; body.className = 'mc-body';
subtitle.textContent = isTgWebApp ? ('Привет, ' + (tgUser?.first_name || 'Игрок') + '!') : 'Voxel-мир в браузере';
box.appendChild(subtitle);
// New World button // === Buttons ===
// New World
const btnNew = document.createElement('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.className = 'mc-btn mc-btn-green';
btnNew.textContent = '🌍 Новый мир'; btnNew.textContent = '🌍 Новый мир';
btnNew.onmouseover = () => btnNew.style.background = '#2ecc71';
btnNew.onmouseout = () => btnNew.style.background = '#27ae60';
btnNew.onclick = () => { btnNew.onclick = () => {
worldId = generateShortWorldCode(); worldId = generateShortWorldCode();
overlay.remove(); overlay.remove();
styleEl.remove();
resolve(worldId); resolve(worldId);
}; };
box.appendChild(btnNew); body.appendChild(btnNew);
// Join World button // Join by code
const btnJoin = document.createElement('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.className = 'mc-btn mc-btn-blue';
btnJoin.textContent = '🔗 Вступить по коду'; btnJoin.textContent = '🔗 Вступить по коду';
btnJoin.onmouseover = () => btnJoin.style.background = '#3498db';
btnJoin.onmouseout = () => btnJoin.style.background = '#2980b9';
btnJoin.onclick = () => { btnJoin.onclick = () => {
body.innerHTML = '';
const lbl = document.createElement('div');
lbl.className = 'mc-label';
lbl.textContent = 'Код мира:';
body.appendChild(lbl);
const codeInput = document.createElement('input'); const codeInput = document.createElement('input');
codeInput.className = 'mc-input';
codeInput.type = 'text'; codeInput.type = 'text';
codeInput.placeholder = 'Введите код мира (например: bkh237)'; 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; codeInput.maxLength = 12;
box.innerHTML = ''; body.appendChild(codeInput);
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'); 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.className = 'mc-btn mc-btn-blue';
btnGo.textContent = 'Войти'; btnGo.textContent = 'Войти →';
btnGo.onclick = () => { btnGo.onclick = () => {
const code = codeInput.value.trim().toLowerCase(); const code = codeInput.value.trim().toLowerCase();
if (code.length >= 3) { if (code.length >= 3) {
worldId = code; worldId = code;
overlay.remove(); overlay.remove();
styleEl.remove();
resolve(worldId); resolve(worldId);
} }
}; };
box.appendChild(btnGo); body.appendChild(btnGo);
const btnBack = document.createElement('button'); 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.className = 'mc-btn mc-btn-gray';
btnBack.textContent = '← Назад'; btnBack.textContent = '← Назад';
btnBack.onclick = () => { overlay.remove(); showStartMenu().then(resolve); }; btnBack.onclick = () => { overlay.remove(); styleEl.remove(); showStartMenu().then(resolve); };
box.appendChild(btnBack); body.appendChild(btnBack);
codeInput.focus(); codeInput.focus();
codeInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') btnGo.click(); }); codeInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') btnGo.click(); });
}; };
box.appendChild(btnJoin); body.appendChild(btnJoin);
// History / Continue button (only if TG user with saved game OR localStorage save) // Continue saved game
const hasLocalSave = !!localStorage.getItem('minegrechka_save'); const hasLocalSave = !!localStorage.getItem('minegrechka_save');
if (isTgWebApp || hasLocalSave) { if (isTgWebApp || hasLocalSave) {
const btnHistory = document.createElement('button'); 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.className = 'mc-btn mc-btn-purple';
btnHistory.textContent = '💾 Продолжить игру'; btnHistory.textContent = '💾 Продолжить игру';
btnHistory.onmouseover = () => btnHistory.style.background = '#9b59b6';
btnHistory.onmouseout = () => btnHistory.style.background = '#8e44ad';
btnHistory.onclick = async () => { btnHistory.onclick = async () => {
// TG user: load save from server
if (isTgWebApp && tgUser) { if (isTgWebApp && tgUser) {
try { try {
const resp = await fetch(SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id); const resp = await fetch(SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id);
@ -195,75 +251,89 @@ function customConfirm(msg, onYes) {
worldId = data.world_id; worldId = data.world_id;
tgSaveLoaded = true; tgSaveLoaded = true;
overlay.remove(); overlay.remove();
styleEl.remove();
resolve(worldId); resolve(worldId);
return; return;
} }
} catch(e) { console.warn('[TG] Load save failed:', e); } } catch(e) { console.warn('[TG] Load save failed:', e); }
} }
// Fallback: load from latest localStorage save
if (hasLocalSave) { if (hasLocalSave) {
// Generate world from save (worldSeed based)
worldId = generateShortWorldCode(); worldId = generateShortWorldCode();
overlay.remove(); overlay.remove();
styleEl.remove();
resolve(worldId); resolve(worldId);
return; return;
} }
customAlert('Сохранений не найдено'); customAlert('Сохранений не найдено');
}; };
box.appendChild(btnHistory); body.appendChild(btnHistory);
} }
// TG Share button (only in TG context) // === TG-only buttons ===
if (isTgWebApp && window.Telegram && Telegram.WebApp.switchInlineQuery) { if (isTgWebApp && window.Telegram && Telegram.WebApp.switchInlineQuery) {
// Invite friends — with margin
const btnInvite = document.createElement('button'); 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.className = 'mc-btn mc-btn-outline';
btnInvite.style.marginTop = '16px';
btnInvite.textContent = '📤 Пригласить друзей'; btnInvite.textContent = '📤 Пригласить друзей';
btnInvite.onclick = () => { btnInvite.onclick = () => {
const code = worldId || generateShortWorldCode(); const code = worldId || generateShortWorldCode();
// Share: open TG share dialog with game link
const url = 'https://t.me/Grechkacraft_bot/app?startapp=' + encodeURIComponent(code); const url = 'https://t.me/Grechkacraft_bot/app?startapp=' + encodeURIComponent(code);
if (Telegram.WebApp && Telegram.WebApp.openTelegramLink) { if (Telegram.WebApp && Telegram.WebApp.openTelegramLink) {
Telegram.WebApp.openTelegramLink('https://t.me/share/url?url=' + encodeURIComponent(url) + '&text=' + encodeURIComponent('🏗️ Заходи в GrechkaCraft! Мир: ' + code)); Telegram.WebApp.openTelegramLink('https://t.me/share/url?url=' + encodeURIComponent(url) + '&text=' + encodeURIComponent('🏗️ Заходи в GrechkaCraft! Мир: ' + code));
} else { } else {
// Fallback: copy invite link
navigator.clipboard.writeText(url).then(() => showToast('📋 Скопировано!')).catch(() => showToast(url)); navigator.clipboard.writeText(url).then(() => showToast('📋 Скопировано!')).catch(() => showToast(url));
} }
}; };
box.appendChild(btnInvite); body.appendChild(btnInvite);
// Friends button (TG only) // Friends — with margin
const btnFriends = document.createElement('button'); const btnFriends = document.createElement('button');
btnFriends.style.cssText = 'width:100%;padding:14px;margin-bottom:12px;background:#f39c12;color:#fff;border:none;border-radius:10px;font-size:18px;cursor:pointer;font-weight:bold;'; btnFriends.className = 'mc-btn mc-btn-orange';
btnFriends.style.marginTop = '8px';
btnFriends.textContent = '👥 Друзья'; btnFriends.textContent = '👥 Друзья';
btnFriends.onmouseover = () => btnFriends.style.background = '#e67e22';
btnFriends.onmouseout = () => btnFriends.style.background = '#f39c12';
btnFriends.onclick = async () => { btnFriends.onclick = async () => {
try { try {
const resp = await fetch(SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id); const resp = await fetch(SERVER_URL + '/api/tg/friends?tg_id=' + tgUser.id);
const data = await resp.json(); const data = await resp.json();
box.innerHTML = ''; body.innerHTML = '';
const ft = document.createElement('div');
ft.style.cssText = 'font-size:20px;font-weight:bold;margin-bottom:8px;color:#f39c12;'; // Back button at top
ft.textContent = '👥 Друзья'; const btnBackTop = document.createElement('button');
box.appendChild(ft); btnBackTop.className = 'mc-btn mc-btn-gray';
// Show nickname btnBackTop.textContent = '← Назад';
btnBackTop.style.marginBottom = '12px';
btnBackTop.onclick = () => { overlay.remove(); styleEl.remove(); showStartMenu().then(resolve); };
body.appendChild(btnBackTop);
// Nickname section
if (data.nickname) { if (data.nickname) {
const nickDiv = document.createElement('div'); const nickDiv = document.createElement('div');
nickDiv.style.cssText = 'color:#2ecc71;font-size:14px;margin-bottom:12px;'; nickDiv.className = 'mc-friend-row';
nickDiv.textContent = '🔗 Твой ник: ' + data.nickname; nickDiv.innerHTML = '🔗 Твой ник: <b>' + data.nickname + '</b>';
box.appendChild(nickDiv); nickDiv.style.background = '#D5E8C5';
body.appendChild(nickDiv);
} else { } else {
// No nickname yet — set one const nickLabel = document.createElement('div');
const nickDiv = document.createElement('div'); nickLabel.className = 'mc-label';
nickDiv.style.cssText = 'margin-bottom:12px;'; nickLabel.style.color = '#9E2B2B';
nickDiv.innerHTML = '<div style="color:#e74c3c;font-size:14px;margin-bottom:8px;">Задай ник, чтобы друзья могли тебя найти:</div>'; 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'); const nickInput = document.createElement('input');
nickInput.className = 'mc-input';
nickInput.style.flex = '1';
nickInput.type = 'text'; nickInput.type = 'text';
nickInput.maxLength = 16; nickInput.maxLength = 16;
nickInput.placeholder = 'Твой ник'; nickInput.placeholder = 'Твой ник';
nickInput.style.cssText = 'width:65%;padding:10px;border:2px solid #f39c12;border-radius:8px;background:#16213e;color:#fff;font-size:16px;';
const nickBtn = document.createElement('button'); const nickBtn = document.createElement('button');
nickBtn.style.cssText = 'width:28%;padding:10px;background:#f39c12;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-left:4px;'; nickBtn.className = 'mc-btn mc-btn-green';
nickBtn.style.width = 'auto';
nickBtn.style.marginBottom = '0';
nickBtn.style.padding = '8px 16px';
nickBtn.textContent = '✓'; nickBtn.textContent = '✓';
nickBtn.onclick = async () => { nickBtn.onclick = async () => {
const n = nickInput.value.trim().toLowerCase(); const n = nickInput.value.trim().toLowerCase();
@ -275,50 +345,61 @@ function customConfirm(msg, onYes) {
const sd = await set.json(); const sd = await set.json();
if (sd.ok) { showToast('✅ Ник: ' + sd.nickname); btnFriends.onclick(); } else { showToast('❌ Ошибка'); } if (sd.ok) { showToast('✅ Ник: ' + sd.nickname); btnFriends.onclick(); } else { showToast('❌ Ошибка'); }
}; };
nickDiv.appendChild(nickInput); nickRow.appendChild(nickInput);
nickDiv.appendChild(nickBtn); nickRow.appendChild(nickBtn);
box.appendChild(nickDiv); body.appendChild(nickRow);
} }
// Friends list // Friends list
if (data.friends && data.friends.length) { if (data.friends && data.friends.length) {
const list = document.createElement('div'); const listLabel = document.createElement('div');
list.style.cssText = 'margin-bottom:12px;'; listLabel.className = 'mc-label';
listLabel.textContent = '👥 Друзья:';
body.appendChild(listLabel);
for (const f of data.friends) { for (const f of data.friends) {
const fDiv = document.createElement('div'); const fDiv = document.createElement('div');
fDiv.style.cssText = 'padding:6px 0;border-bottom:1px solid #333;display:flex;justify-content:space-between;align-items:center;'; fDiv.className = 'mc-friend-row';
const statusDot = f.online ? '🟢' : '⚫'; const statusDot = f.online ? '🟢' : '⚫';
const info = statusDot + ' <b>' + f.nickname + '</b>' + (f.world_id ? ' — мир ' + f.world_id : ''); let info = statusDot + ' <b>' + f.nickname + '</b>';
fDiv.innerHTML = '<div>' + info + '</div>'; 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) { if (f.online && f.world_id) {
const joinBtn = document.createElement('button'); const joinBtn = document.createElement('button');
joinBtn.style.cssText = 'padding:4px 10px;background:#27ae60;color:#fff;border:none;border-radius:6px;font-size:12px;cursor:pointer;'; joinBtn.className = 'mc-friend-btn mc-friend-btn-green';
joinBtn.textContent = '🎮 Вступить'; joinBtn.textContent = '🎮 Вступить';
joinBtn.onclick = () => { worldId = f.world_id; overlay.remove(); resolve(worldId); }; joinBtn.onclick = () => { worldId = f.world_id; overlay.remove(); styleEl.remove(); resolve(worldId); };
fDiv.appendChild(joinBtn); fDiv.appendChild(joinBtn);
} }
list.appendChild(fDiv); body.appendChild(fDiv);
} }
box.appendChild(list);
} else if (data.friends && !data.friends.length) { } else if (data.friends && !data.friends.length) {
const noFriends = document.createElement('div'); const noFriends = document.createElement('div');
noFriends.style.cssText = 'color:#888;font-size:14px;margin-bottom:12px;'; noFriends.className = 'mc-label';
noFriends.style.color = '#888';
noFriends.textContent = 'Пока нет друзей'; noFriends.textContent = 'Пока нет друзей';
box.appendChild(noFriends); body.appendChild(noFriends);
} }
// Pending friend requests
// Pending requests
if (data.pending && data.pending.length) { if (data.pending && data.pending.length) {
const pDiv = document.createElement('div'); const pLabel = document.createElement('div');
pDiv.style.cssText = 'margin-bottom:12px;'; pLabel.className = 'mc-label';
pDiv.innerHTML = '<div style="color:#f39c12;font-weight:bold;margin-bottom:6px;">📨 Запросы в друзья:</div>'; pLabel.style.color = '#B86E00';
pLabel.textContent = '📨 Запросы в друзья:';
body.appendChild(pLabel);
for (const p of data.pending) { for (const p of data.pending) {
const pItem = document.createElement('div'); const pItem = document.createElement('div');
pItem.style.cssText = 'display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid #222;'; pItem.className = 'mc-friend-row';
const span = document.createElement('span'); const span = document.createElement('span');
span.textContent = p.from; span.textContent = p.from;
pItem.appendChild(span); pItem.appendChild(span);
const btns = document.createElement('div'); const btns = document.createElement('div');
btns.style.display = 'flex';
btns.style.gap = '4px';
const acceptBtn = document.createElement('button'); const acceptBtn = document.createElement('button');
acceptBtn.style.cssText = 'padding:4px 8px;background:#27ae60;color:#fff;border:none;border-radius:4px;font-size:12px;cursor:pointer;margin-right:4px;'; acceptBtn.className = 'mc-friend-btn mc-friend-btn-green';
acceptBtn.textContent = '✓'; acceptBtn.textContent = '✓';
acceptBtn.onclick = async () => { 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 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})});
@ -326,7 +407,7 @@ function customConfirm(msg, onYes) {
if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.onclick(); } else { showToast('❌ Ошибка'); } if (d.ok) { showToast('✅ Друг добавлен!'); btnFriends.onclick(); } else { showToast('❌ Ошибка'); }
}; };
const declineBtn = document.createElement('button'); const declineBtn = document.createElement('button');
declineBtn.style.cssText = 'padding:4px 8px;background:#e74c3c;color:#fff;border:none;border-radius:4px;font-size:12px;cursor:pointer;'; declineBtn.className = 'mc-friend-btn mc-friend-btn-red';
declineBtn.textContent = '✕'; declineBtn.textContent = '✕';
declineBtn.onclick = async () => { 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})}); 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})});
@ -336,21 +417,31 @@ function customConfirm(msg, onYes) {
btns.appendChild(acceptBtn); btns.appendChild(acceptBtn);
btns.appendChild(declineBtn); btns.appendChild(declineBtn);
pItem.appendChild(btns); pItem.appendChild(btns);
pDiv.appendChild(pItem); body.appendChild(pItem);
} }
box.appendChild(pDiv);
} }
// Add friend by nickname
const addDiv = document.createElement('div'); // Add friend
addDiv.style.cssText = 'margin-top:12px;'; body.appendChild(document.createElement('hr'));
addDiv.innerHTML = '<div style="color:#888;font-size:14px;margin-bottom:8px;">Добавить друга по нику:</div>'; 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'); const addInput = document.createElement('input');
addInput.className = 'mc-input';
addInput.style.flex = '1';
addInput.type = 'text'; addInput.type = 'text';
addInput.maxLength = 16; addInput.maxLength = 16;
addInput.placeholder = 'Ник друга'; addInput.placeholder = 'Ник друга';
addInput.style.cssText = 'width:65%;padding:10px;border:2px solid #f39c12;border-radius:8px;background:#16213e;color:#fff;font-size:16px;';
const addBtn = document.createElement('button'); const addBtn = document.createElement('button');
addBtn.style.cssText = 'width:28%;padding:10px;background:#3498db;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-left:4px;'; addBtn.className = 'mc-btn mc-btn-blue';
addBtn.style.width = 'auto';
addBtn.style.marginBottom = '0';
addBtn.style.padding = '8px 16px';
addBtn.textContent = '+'; addBtn.textContent = '+';
addBtn.onclick = async () => { addBtn.onclick = async () => {
const nick = addInput.value.trim().toLowerCase(); const nick = addInput.value.trim().toLowerCase();
@ -360,68 +451,67 @@ function customConfirm(msg, onYes) {
if (d.ok) { showToast(d.message === 'Already friends' ? '✅ Уже друзья!' : '📨 Запрос отправлен!'); } if (d.ok) { showToast(d.message === 'Already friends' ? '✅ Уже друзья!' : '📨 Запрос отправлен!'); }
else { showToast('❌ ' + (d.error || 'Ошибка')); } else { showToast('❌ ' + (d.error || 'Ошибка')); }
}; };
addDiv.appendChild(addInput); addRow.appendChild(addInput);
addDiv.appendChild(addBtn); addRow.appendChild(addBtn);
box.appendChild(addDiv); body.appendChild(addRow);
// Privacy settings
// Privacy checkboxes
const settings = data.settings || { allow_requests: 1, notify_online: 1 }; const settings = data.settings || { allow_requests: 1, notify_online: 1 };
const settingsDiv = document.createElement('div'); const settingsDiv = document.createElement('div');
settingsDiv.style.cssText = 'margin-top:12px;display:flex;gap:12px;font-size:13px;color:#888;'; settingsDiv.style.marginTop = '12px';
const reqLabel = document.createElement('label'); const reqLabel = document.createElement('label');
reqLabel.style.cssText = 'cursor:pointer;'; reqLabel.className = 'mc-checkbox-row';
const reqCb = document.createElement('input'); const reqCb = document.createElement('input');
reqCb.className = 'mc-checkbox';
reqCb.type = 'checkbox'; reqCb.type = 'checkbox';
reqCb.checked = settings.allow_requests; reqCb.checked = settings.allow_requests;
reqCb.style.cssText = 'margin-right:4px;';
reqCb.onchange = async () => { 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})}); 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(reqCb);
reqLabel.appendChild(document.createTextNode(' Запросы в друзья')); reqLabel.appendChild(document.createTextNode(' Запросы в друзья'));
settingsDiv.appendChild(reqLabel); settingsDiv.appendChild(reqLabel);
box.appendChild(settingsDiv); body.appendChild(settingsDiv);
// Back button
const btnBackF = document.createElement('button');
btnBackF.style.cssText = 'width:100%;padding:10px;margin-top:12px;background:transparent;color:#888;border:1px solid #444;border-radius:8px;cursor:pointer;font-size:14px;';
btnBackF.textContent = '← Назад';
btnBackF.onclick = () => { overlay.remove(); showStartMenu().then(resolve); };
box.appendChild(btnBackF);
} catch(e) { showToast('❌ Ошибка загрузки друзей'); console.error(e); } } catch(e) { showToast('❌ Ошибка загрузки друзей'); console.error(e); }
}; };
box.appendChild(btnFriends); body.appendChild(btnFriends);
} }
// Player name input (if not set) // Player name input
if (!playerName) { 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'); const nameInput = document.createElement('input');
nameInput.className = 'mc-input';
nameInput.type = 'text'; nameInput.type = 'text';
nameInput.placeholder = 'Ваше имя'; 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.value = tgUser ? (tgUser.username || tgUser.first_name || '') : '';
nameInput.maxLength = 20; nameInput.maxLength = 20;
box.appendChild(nameInput);
// Update playerName on input
nameInput.addEventListener('input', () => { nameInput.addEventListener('input', () => {
if (nameInput.value.trim()) { if (nameInput.value.trim()) {
playerName = nameInput.value.trim(); playerName = nameInput.value.trim();
localStorage.setItem('minegrechka_playerName', playerName); localStorage.setItem('minegrechka_playerName', playerName);
} }
}); });
box.appendChild(document.createElement('br')); body.appendChild(nameInput);
} }
box.appendChild(body);
overlay.appendChild(box); overlay.appendChild(box);
document.body.appendChild(overlay); document.body.appendChild(overlay);
// If TG user without world in URL, also try to auto-load their save // Auto-load TG save
if (isTgWebApp && tgUser && !worldParam) { if (isTgWebApp && tgUser && !worldParam) {
fetch(SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id) fetch(SERVER_URL + '/api/tg/save?tg_id=' + tgUser.id)
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
if (data.ok && data.world_id) { if (data.ok && data.world_id) {
// Update the "Continue" button to show the world code // could update UI but menu is already shown
const historyBtn = box.querySelector('button:nth-child(5)'); // rough
} }
}) })
.catch(() => {}); .catch(() => {});

View File

@ -95,6 +95,6 @@
</div> </div>
</div> </div>
<script src="game.js?v=50"></script> <script src="game.js?v=51"></script>
</body> </body>
</html> </html>