Files
RPG_FromClaude/menu.js
Maxim Dolgolyov ac1f348311 Initial commit: RPG game project
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-25 01:01:02 +03:00

410 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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 = `
<div class="sc-num">Слот ${sl + 1}</div>
<div class="sc-icon">${meta.icon}</div>
<div class="sc-class">${meta.className}</div>
<div class="sc-info">
⭐ Уровень ${meta.level}<br>
📍 ${meta.mapName} · День ${meta.days}<br>
⚔️ Убийств: ${meta.kills}<br>
🕐 ${meta.playTime}
</div>
<div class="sc-date">${meta.date} ${meta.saveTime}</div>
<div class="sc-play">▶ Играть</div>
<div class="sc-del" onclick="menuDeleteSlot(${sl}, event)">🗑 Удалить</div>`;
card.onclick = e => {
if (e.target.classList.contains('sc-del')) return;
_menuStartMusic();
Game.loadAndStart(sl);
};
} else {
card.innerHTML = `
<div class="sc-num">Слот ${sl + 1}</div>
<div class="sc-icon" style="opacity:.3">📂</div>
<div class="sc-class" style="color:#444;font-size:12px">Пусто</div>
<div class="sc-info" style="margin-top:10px;color:#333">Нажмите чтобы<br>начать новую игру</div>`;
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 = `
<div class="cb-icon">${cls.icon}</div>
<div class="cb-name">${cls.name}</div>
<div class="cb-desc">${cls.desc}</div>
<div class="cb-stats">HP:${cls.hp} MP:${cls.mp} СИЛ:${cls.str} ЗАЩ:${cls.def}</div>`;
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());
}