// ============================================================ // MENU.JS — Стартовый экран: анимация, слоты, выбор класса // ============================================================ let _menuSlot = null; window.onload = async function () { try { await DataLoader.load(); } catch (e) { // Ошибка уже показана баннером в DataLoader._showError() return; } Audio.init(); menuBuildClassGrid(); menuBuildSlots(); // Показываем сплэш-экран; start-screen скрыт через style="display:none" в HTML splashStartAnim(); }; // ── Переход со сплэша в главное меню ────────────────────── function splashEnter() { const splash = document.getElementById('splash-screen'); if (!splash) return; // Запускаем музыку (первый клик пользователя — браузер разрешает) const bgm = document.getElementById('menu-bgm'); if (bgm) bgm.play().catch(() => {}); // Сначала показываем главное меню позади сплэша — чтобы не мелькал игровой интерфейс const ss = document.getElementById('start-screen'); if (ss) ss.style.display = ''; menuStartAnim(); // Затем плавно убираем сплэш поверх уже готового меню splash.style.opacity = '0'; splash.style.pointerEvents = 'none'; setTimeout(() => { splash.style.display = 'none'; }, 750); } // ── Анимация сплэш-экрана ───────────────────────────────── function splashStartAnim() { const mc = document.getElementById('splash-canvas'); if (!mc) return; const ctx = mc.getContext('2d'); mc.width = 900; mc.height = 600; const RUNES = ['ᚠ','ᚢ','ᚦ','ᚨ','ᚱ','ᚷ','ᚹ','ᛗ','ᛟ','ᚾ','ᛁ','ᛃ','ᚲ','ᛏ','ᛚ']; // Три слоя звёзд (параллакс) const starLayers = [ Array.from({length: 110}, () => ({ x: Math.random()*900, y: Math.random()*600, r: Math.random()*0.7+0.1, v: 0.04, a: Math.random()*0.4+0.1 })), Array.from({length: 55}, () => ({ x: Math.random()*900, y: Math.random()*600, r: Math.random()*1.1+0.3, v: 0.10, a: Math.random()*0.5+0.2 })), Array.from({length: 22}, () => ({ x: Math.random()*900, y: Math.random()*600, r: Math.random()*1.6+0.5, v: 0.18, a: Math.random()*0.6+0.3 })), ]; const runes = []; let lastRune = 0; let angle = 0; function frame(ts) { const el = document.getElementById('splash-screen'); if (!el || el.style.display === 'none' || el.style.opacity === '0') return; requestAnimationFrame(frame); // Фон ctx.fillStyle = '#02020a'; ctx.fillRect(0, 0, 900, 600); // Центральное свечение const cx = 450, cy = 288; const glow = ctx.createRadialGradient(cx, cy, 0, cx, cy, 420); glow.addColorStop(0, 'rgba(35,8,75,0.55)'); glow.addColorStop(0.45,'rgba(15,4,38,0.28)'); glow.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = glow; ctx.fillRect(0, 0, 900, 600); // Звёзды starLayers.forEach((layer, li) => { layer.forEach(s => { s.y -= s.v; if (s.y < -2) { s.y = 602; s.x = Math.random() * 900; } const tw = 0.65 + Math.sin(ts / 900 + s.x * 0.05) * 0.35; ctx.fillStyle = li === 2 ? `rgba(255,220,140,${s.a * tw})` : `rgba(190,190,255,${s.a * tw})`; ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fill(); }); }); // Магический круг angle += 0.0015; ctx.save(); ctx.translate(cx, cy); // Внешнее кольцо ctx.rotate(angle); ctx.beginPath(); ctx.arc(0, 0, 215, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(80,40,160,0.18)'; ctx.lineWidth = 1; ctx.stroke(); // Внутреннее кольцо ctx.rotate(-angle * 2.3); ctx.beginPath(); ctx.arc(0, 0, 155, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(100,55,185,0.14)'; ctx.lineWidth = 1; ctx.stroke(); // Шестиугольник ctx.rotate(angle * 1.5); ctx.strokeStyle = 'rgba(110,65,195,0.10)'; ctx.lineWidth = 0.8; for (let i = 0; i < 6; i++) { const a1 = (i / 6) * Math.PI * 2; const a2 = ((i + 2) / 6) * Math.PI * 2; ctx.beginPath(); ctx.moveTo(Math.cos(a1) * 155, Math.sin(a1) * 155); ctx.lineTo(Math.cos(a2) * 155, Math.sin(a2) * 155); ctx.stroke(); } // Точки на кольце ctx.rotate(-angle * 0.5); for (let i = 0; i < 8; i++) { const a = (i / 8) * Math.PI * 2 + angle * 3; const px = Math.cos(a) * 215, py = Math.sin(a) * 215; ctx.beginPath(); ctx.arc(px, py, 2.5, 0, Math.PI * 2); ctx.fillStyle = `rgba(150,80,220,${0.4 + Math.sin(ts/600 + i) * 0.2})`; ctx.fill(); } ctx.restore(); // Лучи света ctx.save(); for (let i = 0; i < 8; i++) { const a = (i / 8) * Math.PI * 2 + angle * 1.8; const pulse = 0.06 + Math.sin(ts / 1100 + i) * 0.02; const grad = ctx.createLinearGradient(cx, cy, cx + Math.cos(a) * 380, cy + Math.sin(a) * 380); grad.addColorStop(0, `rgba(110,50,210,${pulse})`); grad.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.moveTo(cx, cy); const w = 0.07; ctx.arc(cx, cy, 380, a - w, a + w); ctx.closePath(); ctx.fill(); } ctx.restore(); // Пульсирующая сфера в центре const orbP = Math.sin(ts / 800); const orb = ctx.createRadialGradient(cx, cy, 0, cx, cy, 65 + orbP * 6); orb.addColorStop(0, `rgba(190,130,255,${0.14 + orbP * 0.05})`); orb.addColorStop(0.5, `rgba(90,40,190,${0.07 + orbP * 0.02})`); orb.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = orb; ctx.beginPath(); ctx.arc(cx, cy, 65 + orbP * 6, 0, Math.PI * 2); ctx.fill(); // Силуэты мечей ctx.save(); ctx.globalAlpha = 0.09 + Math.sin(ts / 3200) * 0.02; ctx.strokeStyle = '#c8a020'; ctx.lineWidth = 1.6; // Левый меч ctx.save(); ctx.translate(185, 295); ctx.rotate(-0.28 + Math.sin(ts / 4000) * 0.015); ctx.beginPath(); ctx.moveTo(0, -140); ctx.lineTo(0, 110); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-22, -55); ctx.lineTo(22, -55); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, -140); ctx.lineTo(-6, -118); ctx.moveTo(0, -140); ctx.lineTo(6, -118); ctx.stroke(); ctx.restore(); // Правый меч ctx.save(); ctx.translate(715, 295); ctx.rotate(0.28 - Math.sin(ts / 4000) * 0.015); ctx.beginPath(); ctx.moveTo(0, -140); ctx.lineTo(0, 110); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-22, -55); ctx.lineTo(22, -55); ctx.stroke(); ctx.beginPath(); ctx.moveTo(0, -140); ctx.lineTo(-6, -118); ctx.moveTo(0, -140); ctx.lineTo(6, -118); ctx.stroke(); ctx.restore(); ctx.restore(); // Руны if (ts - lastRune > 420) { lastRune = ts; const col = Math.random() < 0.35 ? '#c8a020' : Math.random() < 0.5 ? '#9966cc' : '#4a4aaa'; runes.push({ x: 40 + Math.random() * 820, y: 620, ch: RUNES[Math.floor(Math.random() * RUNES.length)], a: 0.75, vy: -(0.38 + Math.random() * 0.52), sz: 10 + Math.random() * 15, col, dx: (Math.random() - 0.5) * 0.28, }); } runes.forEach(r => { r.y += r.vy; r.x += r.dx; r.a -= 0.0022; }); for (let i = runes.length - 1; i >= 0; i--) { const r = runes[i]; if (r.a <= 0 || r.y < -10) { runes.splice(i, 1); continue; } ctx.globalAlpha = r.a; ctx.fillStyle = r.col; ctx.font = `${r.sz}px serif`; ctx.textAlign = 'left'; ctx.fillText(r.ch, r.x, r.y); } ctx.globalAlpha = 1; ctx.textAlign = 'left'; } requestAnimationFrame(frame); } // Вызывается из любой точки взаимодействия с меню (браузер разрешает play() только после клика) function _menuStartMusic() { const el = document.getElementById('menu-bgm'); if (el && el.paused) el.play().catch(() => {}); } // Вызывается из game.js при старте игры function _stopMenuBgm() { const el = document.getElementById('menu-bgm'); if (el && !el.paused) { el.pause(); el.currentTime = 0; } } // ── Анимация фона главного меню ─────────────────────────── function menuStartAnim() { const mc = document.getElementById('menu-canvas'); if (!mc) return; const ctx = mc.getContext('2d'); mc.width = 900; mc.height = 600; const stars = Array.from({ length: 180 }, () => ({ x: Math.random() * 900, y: Math.random() * 600, r: Math.random() * 1.4 + 0.2, vx: (Math.random() - .5) * 0.1, vy: (Math.random() - .5) * 0.05, a: Math.random() * 0.7 + 0.2, })); const runes = []; const RUNES = ['ᚠ','ᚢ','ᚦ','ᚨ','ᚱ','ᚷ','ᚹ','ᛗ','ᛟ','ᚾ','ᛁ','ᛃ','ᚲ','ᛏ','ᛚ']; let _lr = 0; function frame(ts) { const ss = document.getElementById('start-screen'); if (!ss || ss.style.display === 'none') return; requestAnimationFrame(frame); ctx.fillStyle = '#03030b'; ctx.fillRect(0, 0, 900, 600); const grd = ctx.createRadialGradient(450, 300, 0, 450, 300, 430); grd.addColorStop(0, 'rgba(50,20,90,0.28)'); grd.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = grd; ctx.fillRect(0, 0, 900, 600); // Звёзды stars.forEach(s => { s.x = (s.x + s.vx + 900) % 900; s.y = (s.y + s.vy + 600) % 600; ctx.fillStyle = `rgba(200,200,255,${s.a})`; ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fill(); }); // Руны if (ts - _lr > 650) { _lr = ts; runes.push({ x: 80 + Math.random() * 740, y: 590 + Math.random() * 20, ch: RUNES[Math.floor(Math.random() * RUNES.length)], a: 0.65, vy: -(0.35 + Math.random() * 0.35), sz: 11 + Math.random() * 13, col: Math.random() < 0.3 ? '#c8a020' : '#4848a8', }); } runes.forEach(r => { r.y += r.vy; r.a -= 0.0025; }); for (let i = runes.length - 1; i >= 0; i--) { const r = runes[i]; if (r.a <= 0) { runes.splice(i, 1); continue; } ctx.globalAlpha = r.a; ctx.fillStyle = r.col; ctx.font = `${r.sz}px serif`; ctx.fillText(r.ch, r.x, r.y); } ctx.globalAlpha = 1; } requestAnimationFrame(frame); } // ── Слоты сохранений ────────────────────────────────────── function menuBuildSlots() { const cont = document.getElementById('s-slots'); if (!cont) return; cont.innerHTML = ''; for (let sl = 0; sl < 3; sl++) { const meta = RPG.getSaveMeta(sl); const card = document.createElement('div'); card.className = 'slot-card ' + (meta ? 'filled' : 'empty'); card.id = 'slot-card-' + sl; if (meta) { card.innerHTML = `
Слот ${sl + 1}
${meta.icon}
${meta.className}
⭐ Уровень ${meta.level}
📍 ${meta.mapName} · День ${meta.days}
⚔️ Убийств: ${meta.kills}
🕐 ${meta.playTime}
${meta.date} ${meta.saveTime}
▶ Играть
🗑 Удалить
`; card.onclick = e => { if (e.target.classList.contains('sc-del')) return; _menuStartMusic(); Game.loadAndStart(sl); }; } else { card.innerHTML = `
Слот ${sl + 1}
📂
Пусто
Нажмите чтобы
начать новую игру
`; card.onclick = () => menuShowClassSelect(sl); } cont.appendChild(card); } } // ── Выбор класса ────────────────────────────────────────── function menuBuildClassGrid() { const grid = document.getElementById('cls-grid'); if (!grid) return; Object.entries(RPG.CLASSES).forEach(([id, cls]) => { const btn = document.createElement('button'); btn.className = 'cls-btn'; btn.innerHTML = `
${cls.icon}
${cls.name}
${cls.desc}
HP:${cls.hp} MP:${cls.mp} СИЛ:${cls.str} ЗАЩ:${cls.def}
`; btn.onclick = () => { const slot = _menuSlot !== null ? _menuSlot : _pickFreeSlot(); Game.start(id, slot); }; grid.appendChild(btn); }); } function _pickFreeSlot() { for (let i = 0; i < 3; i++) if (!RPG.hasSave(i)) return i; return 0; } // ── Навигация меню ──────────────────────────────────────── function menuShowClassSelect(slot) { _menuStartMusic(); _menuSlot = slot; document.getElementById('menu-main').style.display = 'none'; document.getElementById('menu-class').style.display = 'flex'; const hint = document.getElementById('cls-slot-hint'); if (hint) hint.textContent = slot !== null ? `Новая игра — Слот ${slot + 1}` : 'Выберите класс'; } function menuBack() { _menuStartMusic(); document.getElementById('menu-class').style.display = 'none'; document.getElementById('menu-main').style.display = 'flex'; _menuSlot = null; } function menuDeleteSlot(slot, event) { event.stopPropagation(); if (!confirm(`Удалить сохранение в слоте ${slot + 1}?`)) return; RPG.deleteSave(slot); menuBuildSlots(); } // ── Выбор папки сохранений ──────────────────────────────── async function menuSelectSaveFolder() { if (typeof SaveFS === 'undefined') { alert('File System API недоступен в этом браузере.'); return; } if (!SaveFS.isSupported()) { alert('Ваш браузер не поддерживает сохранение в файлы. Используйте Chrome/Edge.'); return; } const ok = await SaveFS.selectDir(); if (ok) { const name = SaveFS.getDirName(); const btn = document.getElementById('btn-save-folder'); if (btn) btn.textContent = '📁 ' + name; menuBuildSlots(); // обновить слоты из файлов } } // Экспортировать сохранения в уже выбранную папку async function menuExportSaves() { if (typeof SaveFS === 'undefined' || !SaveFS.hasDir()) { alert('Сначала выберите папку!'); return; } await SaveFS.exportAll(); alert('Сохранения экспортированы в папку: ' + SaveFS.getDirName()); }