fix: v44 — menu dropdown, settings panel, market fix, voice mode toggle
- Replace saveBtn/resetBtn crash (null.onclick) with menuBtn dropdown - Add ⚙️ Настройки to dropdown + settingsPanel with day/night cycle toggle - Fix market: use state.inv instead of state.player.inventory - Add market_create/market_list/market_cancel server handlers - Add voice mode toggle (📢near/🌍world) to voice-chat.js module - dayNightCycleEnabled froze worldTime when unchecked - Bump cache v=44
This commit is contained in:
parent
2a5f65d393
commit
afb5b8a7c6
265
game.js
265
game.js
|
|
@ -2395,60 +2395,225 @@ registerProcessor('voice-playback', VoicePlaybackProcessor);
|
||||||
inventoryPanel.style.display = 'none';
|
inventoryPanel.style.display = 'none';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Кнопка сохранения игры (только для одиночного режима)
|
// Меню ⋯ (dropdown)
|
||||||
const saveBtn = document.getElementById('saveBtn');
|
let dropdownOpen = false;
|
||||||
saveBtn.onclick = () => {
|
const menuBtn = document.getElementById('menuBtn');
|
||||||
playSound('click');
|
const menuDropdown = document.getElementById('menuDropdown');
|
||||||
saveGame();
|
const saveItem = document.getElementById('saveItem');
|
||||||
customAlert('Игра сохранена!');
|
const resetItem = document.getElementById('resetItem');
|
||||||
};
|
const marketItem = document.getElementById('marketItem');
|
||||||
|
const settingsItem = document.getElementById('settingsItem');
|
||||||
|
|
||||||
// Кнопка сброса игры (удаление сохранения и создание нового мира)
|
if (menuBtn) {
|
||||||
const resetBtn = document.getElementById('resetBtn');
|
menuBtn.onclick = null;
|
||||||
resetBtn.onclick = () => {
|
menuBtn.addEventListener('click', (e) => {
|
||||||
customConfirm('Вы уверены, что хотите удалить сохранение и начать новую игру?', () => {
|
e.stopPropagation();
|
||||||
playSound('click');
|
dropdownOpen = !dropdownOpen;
|
||||||
|
menuDropdown.classList.toggle('open', dropdownOpen);
|
||||||
// Удаляем сохранение из 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();
|
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
document.addEventListener('click', () => {
|
||||||
|
if (dropdownOpen) {
|
||||||
|
dropdownOpen = false;
|
||||||
|
menuDropdown.classList.remove('open');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
menuDropdown && menuDropdown.addEventListener('click', (e) => e.stopPropagation());
|
||||||
|
|
||||||
|
// 💾 Сохранить
|
||||||
|
if (saveItem) {
|
||||||
|
saveItem.addEventListener('click', () => {
|
||||||
|
playSound('click');
|
||||||
|
saveGame();
|
||||||
|
dropdownOpen = false;
|
||||||
|
menuDropdown.classList.remove('open');
|
||||||
|
customAlert('Игра сохранена!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔄 Новый мир
|
||||||
|
if (resetItem) {
|
||||||
|
resetItem.addEventListener('click', () => {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка обновления URL:', e);
|
||||||
|
}
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🏪 Рынок
|
||||||
|
if (marketItem) {
|
||||||
|
marketItem.addEventListener('click', () => {
|
||||||
|
dropdownOpen = false;
|
||||||
|
menuDropdown.classList.remove('open');
|
||||||
|
toggleMarket();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚙ Настройки
|
||||||
|
if (settingsItem) {
|
||||||
|
settingsItem.addEventListener('click', () => {
|
||||||
|
dropdownOpen = false;
|
||||||
|
menuDropdown.classList.remove('open');
|
||||||
|
toggleSettingsPanel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Показываем кнопку сохранения только если играем одни
|
// Показываем кнопку сохранения только если играем одни
|
||||||
function updateSaveButtonVisibility() {
|
function updateSaveButtonVisibility() {
|
||||||
if (isMultiplayer && otherPlayers.size > 0) {
|
if (saveItem) {
|
||||||
saveBtn.style.display = 'none';
|
saveItem.style.display = (isMultiplayer && otherPlayers.size > 0) ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== РЫНОК ====================
|
||||||
|
const marketPanel = document.getElementById('marketPanel');
|
||||||
|
const marketClose = document.getElementById('marketClose');
|
||||||
|
const marketContent = document.getElementById('marketContent');
|
||||||
|
|
||||||
|
if (marketClose) {
|
||||||
|
marketClose.addEventListener('click', () => { marketPanel.style.display = 'none'; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMarket() {
|
||||||
|
if (marketPanel.style.display === 'none' || !marketPanel.style.display) {
|
||||||
|
openMarket();
|
||||||
} else {
|
} else {
|
||||||
saveBtn.style.display = 'flex';
|
marketPanel.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMarket() {
|
||||||
|
marketPanel.style.display = 'block';
|
||||||
|
renderMarket();
|
||||||
|
}
|
||||||
|
|
||||||
|
let marketOrders = []; // локальный кэш ордеров
|
||||||
|
|
||||||
|
function renderMarket() {
|
||||||
|
if (!socket) {
|
||||||
|
marketContent.innerHTML = '<p style="color:#aaa;text-align:center;">Подключение к серверу...</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.emit('market_list', {}, (orders) => {
|
||||||
|
if (!orders || !orders.length) {
|
||||||
|
marketOrders = [];
|
||||||
|
marketContent.innerHTML =
|
||||||
|
'<p style="color:#aaa;text-align:center;">Предложений пока нет</p>' +
|
||||||
|
'<div style="margin-top:12px;">' +
|
||||||
|
'<button id="createOrderBtn" style="width:100%;padding:10px;background:#4a8c4a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">' +
|
||||||
|
'+ Выставить предмет</button></div>';
|
||||||
|
const btn = document.getElementById('createOrderBtn');
|
||||||
|
if (btn) btn.addEventListener('click', showCreateOrder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
marketOrders = orders;
|
||||||
|
let html = '<div style="margin-bottom:8px;"><button id="createOrderBtn" style="padding:8px 14px;background:#4a8c4a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">+ Выставить предмет</button></div>';
|
||||||
|
html += orders.map(o =>
|
||||||
|
'<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.1);">' +
|
||||||
|
'<div><span style="color:#ffd700;">' + o.offer_item + '</span> ×' + o.offer_qty +
|
||||||
|
' <span style="color:#888;">⇄</span> ' +
|
||||||
|
'<span style="color:#7ecfff;">' + o.want_item + '</span> ×' + o.want_qty +
|
||||||
|
'</div><div style="font-size:11px;color:#888;">' + (o.seller || 'Аноним') + '</div></div>'
|
||||||
|
).join('');
|
||||||
|
marketContent.innerHTML = html;
|
||||||
|
const btn = document.getElementById('createOrderBtn');
|
||||||
|
if (btn) btn.addEventListener('click', showCreateOrder);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCreateOrder() {
|
||||||
|
const invKeys = Object.keys(inv).filter(id => inv[id] > 0);
|
||||||
|
marketContent.innerHTML =
|
||||||
|
'<div style="padding:8px;">' +
|
||||||
|
'<h3 style="margin:0 0 10px;font-size:14px;">Выставить предмет</h3>' +
|
||||||
|
'<label style="font-size:12px;color:#aaa;">Отдаю:</label>' +
|
||||||
|
'<select id="offerItem" style="width:100%;margin-bottom:8px;padding:6px;background:#222;color:#fff;border:1px solid #555;border-radius:4px;">' +
|
||||||
|
invKeys.map(id => '<option value="' + id + '">' + id + ' (×' + inv[id] + ')</option>').join('') +
|
||||||
|
'<option value="">— Пусто —</option></select>' +
|
||||||
|
'<label style="font-size:12px;color:#aaa;">Хочу:</label>' +
|
||||||
|
'<input id="wantItem" placeholder="Название предмета" style="width:100%;margin-bottom:8px;padding:6px;background:#222;color:#fff;border:1px solid #555;border-radius:4px;" />' +
|
||||||
|
'<div style="display:flex;gap:8px;">' +
|
||||||
|
'<button id="submitOrder" style="flex:1;padding:8px;background:#4a8c4a;color:#fff;border:none;border-radius:6px;cursor:pointer;">Выставить</button>' +
|
||||||
|
'<button id="cancelOrder" style="flex:1;padding:8px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;">Отмена</button>' +
|
||||||
|
'</div></div>';
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== НАСТРОЙКИ ====================
|
||||||
|
let dayNightCycleEnabled = true;
|
||||||
|
const settingsPanel = document.getElementById('settingsPanel');
|
||||||
|
|
||||||
|
function toggleSettingsPanel() {
|
||||||
|
if (!settingsPanel) return;
|
||||||
|
if (settingsPanel.style.display === 'none' || !settingsPanel.style.display) {
|
||||||
|
settingsPanel.style.display = 'block';
|
||||||
|
renderSettings();
|
||||||
|
} else {
|
||||||
|
settingsPanel.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSettings() {
|
||||||
|
if (!settingsPanel) return;
|
||||||
|
const contentEl = document.getElementById('settingsContent');
|
||||||
|
if (!contentEl) return;
|
||||||
|
contentEl.innerHTML =
|
||||||
|
'<div style="margin-bottom:16px;">' +
|
||||||
|
'<label style="display:flex;align-items:center;gap:10px;color:#fff;font-size:14px;cursor:pointer;">' +
|
||||||
|
'<input type="checkbox" id="dayNightToggle" ' + (dayNightCycleEnabled ? 'checked' : '') + ' style="width:20px;height:20px;cursor:pointer;" />' +
|
||||||
|
'Цикл день/ночь</label>' +
|
||||||
|
'<p style="color:#888;font-size:11px;margin:4px 0 0 30px;">Отключите, чтобы заморозить время суток</p>' +
|
||||||
|
'</div>';
|
||||||
|
const toggle = document.getElementById('dayNightToggle');
|
||||||
|
if (toggle) {
|
||||||
|
toggle.addEventListener('change', (e) => {
|
||||||
|
dayNightCycleEnabled = e.target.checked;
|
||||||
|
console.log('dayNightCycle:', dayNightCycleEnabled);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsPanel) {
|
||||||
|
const settingsClose = document.getElementById('settingsClose');
|
||||||
|
if (settingsClose) {
|
||||||
|
settingsClose.addEventListener('click', () => { settingsPanel.style.display = 'none'; });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4212,7 +4377,7 @@ registerProcessor('voice-playback', VoicePlaybackProcessor);
|
||||||
|
|
||||||
// Ускорение времени во время сна
|
// Ускорение времени во время сна
|
||||||
if(player.sleeping && isNight()){
|
if(player.sleeping && isNight()){
|
||||||
worldTime += dt * 8 / DAY_LEN; // В 8 раз быстрее
|
if(dayNightCycleEnabled) worldTime += dt * 8 / DAY_LEN; // В 8 раз быстрее
|
||||||
// Восстанавливаем здоровье во время сна
|
// Восстанавливаем здоровье во время сна
|
||||||
player.hp = Math.min(100, player.hp + dt * 20);
|
player.hp = Math.min(100, player.hp + dt * 20);
|
||||||
// Автоматическое пробуждение когда наступает день
|
// Автоматическое пробуждение когда наступает день
|
||||||
|
|
@ -4220,7 +4385,7 @@ registerProcessor('voice-playback', VoicePlaybackProcessor);
|
||||||
player.sleeping = false;
|
player.sleeping = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
worldTime += dt / DAY_LEN;
|
if(dayNightCycleEnabled) worldTime += dt / DAY_LEN;
|
||||||
}
|
}
|
||||||
if(worldTime >= 1) worldTime -= 1;
|
if(worldTime >= 1) worldTime -= 1;
|
||||||
|
|
||||||
|
|
|
||||||
27
index.html
27
index.html
|
|
@ -25,13 +25,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="modeBtn" class="rbtn pe">⛏️</div>
|
<div id="modeBtn" class="rbtn pe">⛏️</div>
|
||||||
<div id="saveBtn" class="rbtn pe">💾</div>
|
|
||||||
<div id="craftBtn" class="rbtn pe">🔨</div>
|
<div id="craftBtn" class="rbtn pe">🔨</div>
|
||||||
<div id="resetBtn" class="rbtn pe">🔄</div>
|
|
||||||
|
|
||||||
<div id="chatToggle" class="rbtn pe">💬</div>
|
<div id="chatToggle" class="rbtn pe">💬</div>
|
||||||
<div id="invToggle" class="rbtn pe">📦</div>
|
<div id="invToggle" class="rbtn pe">📦</div>
|
||||||
<div id="mapToggle" class="rbtn pe">🗺️</div>
|
<div id="mapToggle" class="rbtn pe">🗺️</div>
|
||||||
|
<div id="menuBtn" class="rbtn pe">⋯</div>
|
||||||
|
<div id="menuDropdown" class="menu-dropdown">
|
||||||
|
<div id="saveItem" class="menu-item">💾 Сохранить</div>
|
||||||
|
<div id="resetItem" class="menu-item">🔄 Новый мир</div>
|
||||||
|
<div id="marketItem" class="menu-item">🏪 Рынок</div>
|
||||||
|
<div id="settingsItem" class="menu-item">⚙️ Настройки</div>
|
||||||
|
</div>
|
||||||
<div id="hotbar" class="pe"></div>
|
<div id="hotbar" class="pe"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -91,8 +96,24 @@
|
||||||
<button id="respawnBtn" class="respawn-btn">Возродиться</button>
|
<button id="respawnBtn" class="respawn-btn">Возродиться</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="marketPanel" class="panel" style="display:none;">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>🏪 Рынок</span>
|
||||||
|
<span id="marketClose" class="close" style="cursor:pointer;">✕</span>
|
||||||
|
</div>
|
||||||
|
<div id="marketContent" style="padding:8px;max-height:60vh;overflow-y:auto;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="settingsPanel" class="panel" style="display:none;">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span>⚙️ Настройки</span>
|
||||||
|
<span id="settingsClose" class="close" style="cursor:pointer;">✕</span>
|
||||||
|
</div>
|
||||||
|
<div id="settingsContent" style="padding:8px;max-height:60vh;overflow-y:auto;"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="game.js?v=24"></script>
|
<script src="game.js?v=44"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
374
src/main.js
374
src/main.js
|
|
@ -1,278 +1,228 @@
|
||||||
// ==================== ENTRY POINT ====================
|
// ==================== ENTRY POINT ====================
|
||||||
import { state } from './core/state.js';
|
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 { initSocket } from './multiplayer/socket.js';
|
||||||
import { sendBlockChange, sendPlayerPosition } from './multiplayer/socket-helpers.js';
|
|
||||||
import { loadSound, playSound } from './audio/sound-engine.js';
|
import { loadSound, playSound } from './audio/sound-engine.js';
|
||||||
import { BLOCKS } from './data/blocks.js';
|
import { initTextures, tex, itemTex } from './render/textures.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 { getBlock, setBlock, removeBlock } from './world/world-storage.js';
|
||||||
import { genColumn, surfaceGyAt, ensureGenAroundCamera, regenerateVisibleChunks } from './world/generation.js';
|
import { genColumn, surfaceGyAt } from './world/generation.js';
|
||||||
import { initDB, loadGame, applySave, saveGame } from './game/save.js';
|
import { initDB, loadGame, applySave, saveGame } from './game/save.js';
|
||||||
import { loop } from './game/loop.js';
|
import { loop } from './game/loop.js';
|
||||||
import { initChat } from './ui/chat.js';
|
import { initChat } from './ui/chat.js';
|
||||||
import { initFurnace } from './ui/furnace.js';
|
import { initFurnace } from './ui/furnace.js';
|
||||||
import { initMinimap } from './ui/minimap.js';
|
import { initMinimap } from './ui/minimap.js';
|
||||||
import { initSaveControls, updateSaveButtonVisibility } from './ui/save-controls.js';
|
import { initSaveControls, updateSaveButtonVisibility } from './ui/save-controls.js';
|
||||||
|
import { initMarket } from './ui/market.js';
|
||||||
import { initRespawn } from './ui/respawn.js';
|
import { initRespawn } from './ui/respawn.js';
|
||||||
import { initShare } from './ui/share.js';
|
import { initShare } from './ui/share.js';
|
||||||
import { initControls } from './input/controls.js';
|
import { initControls } from './input/controls.js';
|
||||||
import { initMouseHandlers } from './input/mouse-handler.js';
|
import { initMouseHandlers } from './input/mouse-handler.js';
|
||||||
import { initModes } from './game/modes.js';
|
import { initModes } from './game/modes.js';
|
||||||
import { initVoice } from './multiplayer/voice-chat.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 { rebuildHotbar } from './ui/hotbar.js';
|
||||||
|
import { toggleCraft, closeCraft, toggleInventory, closeInventory } from './ui/craft.js';
|
||||||
|
|
||||||
// ==================== 1) CONFIG — parse URL/getName ====================
|
// ==================== 1) CONFIG — parse URL ====================
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
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_BOT_USERNAME = 'Grechkacraft_bot';
|
||||||
state.TELEGRAM_APP_SHORT_NAME = 'minegrechka';
|
state.TELEGRAM_APP_SHORT_NAME = 'minegrechka';
|
||||||
|
|
||||||
// Защита от mixed content
|
// Set TG-related state
|
||||||
if (location.protocol === 'https:' && state.SERVER_URL.startsWith('http://')) {
|
if (isTgWebApp) {
|
||||||
console.warn('⚠️ Mixed content warning: page is HTTPS but server URL is HTTP');
|
state.myTgId = tgUser?.id || null;
|
||||||
alert('⚠️ Предупреждение: страница загружена по HTTPS, но сервер использует HTTP. Это может вызвать проблемы.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Player name
|
state.heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png';
|
||||||
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
|
// ==================== 2) START MENU ====================
|
||||||
console.log('Current URL:', window.location.href);
|
// Show the start menu before initializing the game.
|
||||||
const worldParam = urlParams.get('world');
|
// The menu returns a Promise that resolves with the worldId.
|
||||||
console.log('world param:', worldParam);
|
async function startGame() {
|
||||||
|
const worldId = await showStartMenu();
|
||||||
|
|
||||||
state.worldId = (worldParam && worldParam.trim() !== '') ? worldParam : null;
|
// Set worldId and playerName
|
||||||
console.log('worldId after params:', state.worldId, 'type:', typeof state.worldId);
|
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);
|
||||||
|
|
||||||
if (!state.worldId) {
|
// Validate TG initData if in TG context
|
||||||
state.worldId = Math.random().toString(36).substring(2, 10);
|
if (isTgWebApp && window.Telegram && Telegram.WebApp.initData) {
|
||||||
console.log('Generated worldId:', state.worldId);
|
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 {
|
try {
|
||||||
const newUrl = new URL(window.location.href);
|
const newUrl = new URL(window.location.href);
|
||||||
newUrl.searchParams.set('world', state.worldId);
|
newUrl.searchParams.set('world', state.worldId);
|
||||||
const newUrlString = newUrl.toString();
|
if (typeof history !== 'undefined' && history.replaceState) {
|
||||||
console.log('New URL to set:', newUrlString);
|
history.replaceState(null, '', newUrl.toString());
|
||||||
|
|
||||||
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) {
|
} catch (e) {}
|
||||||
console.error('Error updating URL:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Generated new worldId for browser:', state.worldId);
|
// ==================== 3) CANVAS — init from canvas module ====================
|
||||||
}
|
initCanvas();
|
||||||
|
|
||||||
console.log('Final worldId:', state.worldId, 'Player name:', state.playerName);
|
// ==================== 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');
|
||||||
|
|
||||||
// ==================== 2) CANVAS — get elements, resize ====================
|
// ==================== 5) TEXTURES — initTextures() ====================
|
||||||
state.gameEl = document.getElementById('game');
|
initTextures();
|
||||||
state.canvas = document.getElementById('c');
|
state.tex = tex;
|
||||||
state.ctx = state.canvas.getContext('2d');
|
state.itemTex = itemTex;
|
||||||
|
|
||||||
// offscreen light map
|
// ==================== 6) CRAFT/INVENTORY BUTTONS ====================
|
||||||
state.lightC = document.createElement('canvas');
|
const craftBtn = document.getElementById('craftBtn');
|
||||||
state.lightCtx = state.lightC.getContext('2d');
|
const invToggle = document.getElementById('invToggle');
|
||||||
|
const craftClose = document.getElementById('craftClose');
|
||||||
|
const inventoryClose = document.getElementById('inventoryClose');
|
||||||
|
|
||||||
state.dpr = Math.max(1, window.devicePixelRatio || 1);
|
if (craftBtn) craftBtn.onclick = () => toggleCraft();
|
||||||
|
if (craftClose) craftClose.onclick = () => closeCraft();
|
||||||
|
if (invToggle) invToggle.onclick = () => toggleInventory();
|
||||||
|
if (inventoryClose) inventoryClose.onclick = () => closeInventory();
|
||||||
|
|
||||||
function resize() {
|
// ==================== 7) CONTROLS — initControls ====================
|
||||||
state.W = state.gameEl.clientWidth;
|
initControls();
|
||||||
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 ====================
|
// ==================== 8) MOUSE — initMouseHandlers ====================
|
||||||
state.otherPlayers = new Map();
|
initMouseHandlers();
|
||||||
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
|
// ==================== 9) UI MODULES — init all UI ====================
|
||||||
state.clouds = Array.from({ length: 10 }, () => ({
|
initChat();
|
||||||
x: Math.random() * 2000,
|
initFurnace();
|
||||||
y: -200 - Math.random() * 260,
|
initMinimap();
|
||||||
w: 80 + Math.random() * 120,
|
initSaveControls();
|
||||||
s: 12 + Math.random() * 20
|
initMarket();
|
||||||
}));
|
initRespawn();
|
||||||
|
initShare();
|
||||||
|
initModes();
|
||||||
|
|
||||||
// Weather
|
// ==================== 10) SOCKET — initSocket ====================
|
||||||
state.weatherTimer = 0;
|
initSocket();
|
||||||
state.weatherChangeInterval = 60 + Math.random() * 120;
|
|
||||||
|
|
||||||
// Player spawn
|
// ==================== 11) VOICE — initVoice ====================
|
||||||
state.player.x = 6 * state.TILE;
|
initVoice();
|
||||||
state.player.y = 0;
|
|
||||||
state.spawnPoint.x = 6 * state.TILE;
|
|
||||||
state.spawnPoint.y = 0;
|
|
||||||
|
|
||||||
// Hero image
|
// ==================== 12) SAVE — loadGame, applySave ====================
|
||||||
state.heroImg = new Image();
|
rebuildHotbar();
|
||||||
state.heroImg.src = 'https://supamg.mkn8n.ru/storage/v1/object/public/img//grechka.png';
|
|
||||||
|
|
||||||
// UI elements
|
initDB().then(async () => {
|
||||||
state.hpEl = document.getElementById('hp');
|
const loadedSave = await loadGame();
|
||||||
state.foodEl = document.getElementById('food');
|
if (loadedSave) {
|
||||||
state.sxEl = document.getElementById('sx');
|
await applySave(loadedSave);
|
||||||
state.syEl = document.getElementById('sy');
|
console.log('Загружено сохранение, HP:', state.player.hp);
|
||||||
state.todEl = document.getElementById('tod');
|
|
||||||
state.worldIdEl = document.getElementById('worldId');
|
|
||||||
state.playerCountEl = document.getElementById('playerCount');
|
|
||||||
state.deathEl = document.getElementById('death');
|
|
||||||
|
|
||||||
// ==================== 4) TEXTURES — initTextures() ====================
|
if (state.player.hp <= 0) {
|
||||||
initTextures();
|
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('Сохранение не найдено, начинаем новую игру');
|
||||||
|
|
||||||
// ==================== 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();
|
|
||||||
|
|
||||||
// ==================== 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.hp = 100;
|
||||||
state.player.hunger = 100;
|
state.player.hunger = 100;
|
||||||
state.player.o2 = 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.vx = state.player.vy = 0;
|
||||||
state.player.invuln = 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.player.fallStartY = state.player.y;
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Сохранение не найдено, начинаем новую игру');
|
|
||||||
|
|
||||||
state.player.hp = 100;
|
state.spawnPoint.x = state.player.x;
|
||||||
state.player.hunger = 100;
|
state.spawnPoint.y = state.player.y;
|
||||||
state.player.o2 = 100;
|
|
||||||
state.player.vx = state.player.vy = 0;
|
|
||||||
state.player.invuln = 0;
|
|
||||||
|
|
||||||
// старт — на поверхности
|
console.log('Новая игра: startGX=', startGX, 'surfaceY=', surfaceY, 'player.y=', state.player.y);
|
||||||
const startGX = 6;
|
|
||||||
for (let dx = -3; dx <= 3; dx++) genColumn(startGX + dx);
|
// Генерируем карту вокруг стартовой позиции
|
||||||
const surfaceY = surfaceGyAt(startGX);
|
for (let gx = startGX - 50; gx <= startGX + 50; gx++) {
|
||||||
let safeGY = surfaceY - 1;
|
genColumn(gx);
|
||||||
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;
|
// Автосейв при скрытии страницы
|
||||||
|
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;
|
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++) {
|
for (let gx = startGX - 50; gx <= startGX + 50; gx++) {
|
||||||
genColumn(gx);
|
genColumn(gx);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Автосейв при скрытии страницы
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.hidden) {
|
|
||||||
saveGame();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Автосейв перед закрытием страницы
|
// ==================== 13) LOOP — startLoop ====================
|
||||||
window.addEventListener('beforeunload', () => {
|
requestAnimationFrame(loop);
|
||||||
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++) {
|
// Start the game
|
||||||
genColumn(gx);
|
startGame().catch(err => {
|
||||||
}
|
console.error('Fatal error starting game:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==================== 13) LOOP — startLoop ====================
|
|
||||||
requestAnimationFrame(loop);
|
|
||||||
|
|
@ -7,6 +7,7 @@ let voiceStream = null;
|
||||||
let audioCtx = null;
|
let audioCtx = null;
|
||||||
let voiceProcessor = null;
|
let voiceProcessor = null;
|
||||||
let voiceActive = false;
|
let voiceActive = false;
|
||||||
|
let voiceMode = 'near'; // 'near' — 600px radius, 'world' — global
|
||||||
const VOICE_SERVER = 'https://voicegrech.mkn8n.ru';
|
const VOICE_SERVER = 'https://voicegrech.mkn8n.ru';
|
||||||
|
|
||||||
// Кнопка микрофона
|
// Кнопка микрофона
|
||||||
|
|
@ -16,6 +17,30 @@ 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;';
|
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);
|
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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Индикатор говорящего
|
// Индикатор говорящего
|
||||||
const speakingIndicator = document.createElement('div');
|
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.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;';
|
||||||
|
|
@ -64,7 +89,7 @@ voiceBtn.onclick = async () => {
|
||||||
// Подключаемся к голосовому серверу
|
// Подключаемся к голосовому серверу
|
||||||
voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] });
|
voiceSocket = io(VOICE_SERVER, { transports: ['websocket'] });
|
||||||
voiceSocket.on('connect', () => {
|
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: worldId, x: state.player.x, y: state.player.y, name: state.playerName || 'Игрок', mode: voiceMode });
|
||||||
});
|
});
|
||||||
|
|
||||||
voiceSocket.on('voice_in', (payload) => {
|
voiceSocket.on('voice_in', (payload) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
// ==================== РЫНОК ПРЕДЛОЖЕНИЙ ====================
|
||||||
|
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 = '<p style="color:#aaa;">Подключение к серверу...</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запрашиваем список ордеров с сервера
|
||||||
|
state.socket.emit('market_list', {}, (orders) => {
|
||||||
|
if (!orders || !orders.length) {
|
||||||
|
marketContent.innerHTML = `
|
||||||
|
<p style="color:#aaa;text-align:center;">Предложений пока нет</p>
|
||||||
|
<div style="margin-top:12px;">
|
||||||
|
<button id="createOrderBtn" style="width:100%;padding:10px;background:#4a8c4a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:14px;">
|
||||||
|
+ Выставить предмет
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
const btn = document.getElementById('createOrderBtn');
|
||||||
|
if (btn) btn.addEventListener('click', showCreateOrder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div style="margin-bottom:8px;"><button id="createOrderBtn" style="padding:8px 14px;background:#4a8c4a;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">+ Выставить предмет</button></div>';
|
||||||
|
|
||||||
|
html += orders.map(o => `
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.1);">
|
||||||
|
<div>
|
||||||
|
<span style="color:#ffd700;">${o.offer_item}</span> ×${o.offer_qty}
|
||||||
|
<span style="color:#888;"> ⇄ </span>
|
||||||
|
<span style="color:#7ecfff;">${o.want_item}</span> ×${o.want_qty}
|
||||||
|
</div>
|
||||||
|
<div style="font-size:11px;color:#888;">${o.seller || 'Аноним'}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
marketContent.innerHTML = html;
|
||||||
|
const btn = document.getElementById('createOrderBtn');
|
||||||
|
if (btn) btn.addEventListener('click', showCreateOrder);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCreateOrder() {
|
||||||
|
const invItems = Object.keys(state.inv).filter(id => state.inv[id] > 0);
|
||||||
|
|
||||||
|
marketContent.innerHTML = `
|
||||||
|
<div style="padding:8px;">
|
||||||
|
<h3 style="margin:0 0 10px;font-size:14px;">Выставить предмет</h3>
|
||||||
|
<label style="font-size:12px;color:#aaa;">Отдаю:</label>
|
||||||
|
<select id="offerItem" style="width:100%;margin-bottom:8px;padding:6px;background:#222;color:#fff;border:1px solid #555;border-radius:4px;">
|
||||||
|
${invItems.map(id => `<option value="${id}">${id} (×${state.inv[id]})</option>`).join('')}
|
||||||
|
<option value="">— Пусто —</option>
|
||||||
|
</select>
|
||||||
|
<label style="font-size:12px;color:#aaa;">Хочу:</label>
|
||||||
|
<input id="wantItem" placeholder="Название предмета" style="width:100%;margin-bottom:8px;padding:6px;background:#222;color:#fff;border:1px solid #555;border-radius:4px;" />
|
||||||
|
<div style="display:flex;gap:8px;">
|
||||||
|
<button id="submitOrder" style="flex:1;padding:8px;background:#4a8c4a;color:#fff;border:none;border-radius:6px;cursor:pointer;">Выставить</button>
|
||||||
|
<button id="cancelOrder" style="flex:1;padding:8px;background:#555;color:#fff;border:none;border-radius:6px;cursor:pointer;">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,55 @@
|
||||||
// ==================== КНОПКИ СОХРАНЕНИЯ / СБРОСА ====================
|
// ==================== МЕНЮ ⋯ + СОХРАНЕНИЕ / СБРОС ====================
|
||||||
import { state } from '../core/state.js';
|
import { state } from '../core/state.js';
|
||||||
import { playSound } from '../audio/sound-engine.js';
|
import { playSound } from '../audio/sound-engine.js';
|
||||||
import { saveGame } from '../game/save.js';
|
import { saveGame } from '../game/save.js';
|
||||||
|
|
||||||
|
let dropdownOpen = false;
|
||||||
|
|
||||||
export function initSaveControls() {
|
export function initSaveControls() {
|
||||||
const saveBtn = document.getElementById('saveBtn');
|
const menuBtn = document.getElementById('menuBtn');
|
||||||
saveBtn.onclick = () => {
|
const menuDropdown = document.getElementById('menuDropdown');
|
||||||
|
const saveItem = document.getElementById('saveItem');
|
||||||
|
const resetItem = document.getElementById('resetItem');
|
||||||
|
const marketItem = document.getElementById('marketItem');
|
||||||
|
const settingsItem = document.getElementById('settingsItem');
|
||||||
|
|
||||||
|
// Убить возможный старый 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');
|
playSound('click');
|
||||||
saveGame();
|
saveGame();
|
||||||
|
dropdownOpen = false;
|
||||||
|
menuDropdown.classList.remove('open');
|
||||||
alert('Игра сохранена!');
|
alert('Игра сохранена!');
|
||||||
};
|
});
|
||||||
|
|
||||||
const resetBtn = document.getElementById('resetBtn');
|
// 🔄 Новый мир
|
||||||
resetBtn.onclick = () => {
|
resetItem.addEventListener('click', () => {
|
||||||
if (confirm('Вы уверены, что хотите удалить сохранение и начать новую игру?')) {
|
if (confirm('Вы уверены, что хотите удалить сохранение и начать новую игру?')) {
|
||||||
playSound('click');
|
playSound('click');
|
||||||
|
|
||||||
// Удаляем сохранение из localStorage
|
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem(state.SAVE_KEY);
|
localStorage.removeItem(state.SAVE_KEY);
|
||||||
console.log('Сохранение удалено из localStorage');
|
console.log('Сохранение удалено из localStorage');
|
||||||
|
|
@ -24,39 +57,53 @@ export function initSaveControls() {
|
||||||
console.warn('Ошибка удаления сохранения:', e);
|
console.warn('Ошибка удаления сохранения:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сбрасываем in-memory сохранение
|
|
||||||
state.inMemorySave = null;
|
state.inMemorySave = null;
|
||||||
|
|
||||||
// Генерируем новый worldId
|
|
||||||
state.worldId = Math.random().toString(36).substring(2, 10);
|
state.worldId = Math.random().toString(36).substring(2, 10);
|
||||||
console.log('Новый worldId после сброса:', state.worldId);
|
console.log('Новый worldId после сброса:', state.worldId);
|
||||||
|
|
||||||
// Обновляем URL
|
|
||||||
try {
|
try {
|
||||||
const newUrl = new URL(window.location.href);
|
const newUrl = new URL(window.location.href);
|
||||||
newUrl.searchParams.set('world', state.worldId);
|
newUrl.searchParams.set('world', state.worldId);
|
||||||
const newUrlString = newUrl.toString();
|
|
||||||
|
|
||||||
if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') {
|
if (typeof window.history !== 'undefined' && typeof window.history.replaceState === 'function') {
|
||||||
window.history.replaceState(null, '', newUrlString);
|
window.history.replaceState(null, '', newUrl.toString());
|
||||||
console.log('URL обновлён:', newUrlString);
|
console.log('URL обновлён:', newUrl.toString());
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Ошибка обновления URL:', e);
|
console.error('Ошибка обновления URL:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Перезагружаем страницу
|
|
||||||
location.reload();
|
location.reload();
|
||||||
|
} else {
|
||||||
|
dropdownOpen = false;
|
||||||
|
menuDropdown.classList.remove('open');
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Показываем кнопку сохранения только если играем одни
|
// 🏪 Рынок
|
||||||
export function updateSaveButtonVisibility() {
|
marketItem.addEventListener('click', () => {
|
||||||
const saveBtn = document.getElementById('saveBtn');
|
dropdownOpen = false;
|
||||||
if (state.isMultiplayer && state.otherPlayers.size > 0) {
|
menuDropdown.classList.remove('open');
|
||||||
saveBtn.style.display = 'none';
|
// toggleMarket импортируется в main.js, вызовем через кастомное событие
|
||||||
} else {
|
document.dispatchEvent(new CustomEvent('toggleMarket'));
|
||||||
saveBtn.style.display = 'flex';
|
});
|
||||||
|
|
||||||
|
// ⚙️ Настройки
|
||||||
|
if (settingsItem) {
|
||||||
|
settingsItem.addEventListener('click', () => {
|
||||||
|
dropdownOpen = false;
|
||||||
|
menuDropdown.classList.remove('open');
|
||||||
|
document.dispatchEvent(new CustomEvent('toggleSettings'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показываем/скрываем пункт сохранения в зависимости от режима
|
||||||
|
export 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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,551 @@
|
||||||
|
// ==================== 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 = '<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 (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(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
16
style.css
16
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); }
|
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; }
|
.rbtn:active { transform: translateY(4px); box-shadow:none; }
|
||||||
#modeBtn { top:10px; background:#f39c12; }
|
#modeBtn { top:10px; background:#f39c12; }
|
||||||
#saveBtn { top:10px; right:70px !important; background:#27ae60; }
|
#mapToggle { top:10px; right:70px !important; background:#1abc9c; }
|
||||||
#resetBtn { top:10px; right:130px !important; background:#e74c3c; }
|
|
||||||
#mapToggle { top:10px; right:190px !important; background:#1abc9c; }
|
|
||||||
#craftBtn { top:74px; right:10px !important; background:#9b59b6; }
|
#craftBtn { top:74px; right:10px !important; background:#9b59b6; }
|
||||||
#invToggle { top:74px; right:70px !important; background:#3498db; }
|
#invToggle { top:74px; right:70px !important; background:#3498db; }
|
||||||
|
#menuBtn { top:74px; right:130px !important; background:#555; }
|
||||||
#chatToggle { display: none !important; }
|
#chatToggle { display: none !important; }
|
||||||
#chatPanel { 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-yes { background:#e74c3c; }
|
||||||
.custom-modal-box .btn-no { background:#555; }
|
.custom-modal-box .btn-no { background:#555; }
|
||||||
.custom-modal-box .btn-ok { background:#2ecc71; }
|
.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; }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue