diff --git a/web/assets/css/game.css b/web/assets/css/game.css index cfb9c32..ff2335d 100644 --- a/web/assets/css/game.css +++ b/web/assets/css/game.css @@ -20,6 +20,71 @@ body { user-select: none; } +.level-menu-shell { + position: fixed; + top: 24px; + right: 24px; + z-index: 1100; + display: flex; + align-items: flex-start; + gap: 10px; +} + +.level-menu-toggle { + border: none; + border-radius: 999px; + background: #223; + color: #fff; + padding: 12px 18px; + font-size: 0.95rem; + font-weight: 700; + cursor: pointer; + box-shadow: 0 8px 20px rgba(34, 51, 68, 0.18); +} + +.level-menu-panel { + min-width: 180px; + background: rgba(255, 255, 255, 0.96); + border-radius: 18px; + padding: 14px; + box-shadow: 0 18px 40px rgba(34, 51, 68, 0.18); + border: 1px solid rgba(34, 51, 68, 0.08); +} + +.level-menu-panel h2 { + font-size: 0.95rem; + color: #223; + margin-bottom: 10px; +} + +.level-menu-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.level-menu-item { + border: none; + border-radius: 10px; + padding: 10px 12px; + text-align: left; + background: #dfe5f8; + color: #223; + font-weight: 700; + cursor: pointer; +} + +.level-menu-item.is-current { + background: #223; + color: #fff; +} + +.level-menu-item:disabled { + background: #eceff7; + color: #8a92a3; + cursor: not-allowed; +} + main { display: flex; flex-direction: column; diff --git a/web/assets/js/game.js b/web/assets/js/game.js index c5668f2..ad0ce73 100644 --- a/web/assets/js/game.js +++ b/web/assets/js/game.js @@ -162,6 +162,8 @@ let rotatorIntervals = {}; let toggledDoors = {}; let poweredCaptors = {}; let draggedGlassColor = null; +let highestUnlockedLevelIndex = 0; +const unlockedLevelsStorageKey = "mirror-game-highest-unlocked-level"; function getCurrentLevel() { return levels[currentLevelIndex]; @@ -171,6 +173,107 @@ function getCurrentLevelConfig(configByLevel) { return configByLevel[currentLevelIndex] || {}; } +function loadUnlockedLevels() { + const savedValue = window.localStorage.getItem(unlockedLevelsStorageKey); + const parsedValue = Number.parseInt(savedValue || "0", 10); + + if (Number.isNaN(parsedValue)) { + highestUnlockedLevelIndex = 0; + return; + } + + highestUnlockedLevelIndex = Math.max(0, Math.min(parsedValue, levels.length - 1)); +} + +function saveUnlockedLevels() { + window.localStorage.setItem(unlockedLevelsStorageKey, String(highestUnlockedLevelIndex)); +} + +function unlockLevel(levelIndex) { + if (levelIndex <= highestUnlockedLevelIndex || levelIndex >= levels.length) { + return; + } + + highestUnlockedLevelIndex = levelIndex; + saveUnlockedLevels(); + renderLevelMenu(); +} + +function toggleLevelMenu() { + const levelMenuPanel = document.getElementById("level-menu-panel"); + const levelMenuToggle = document.getElementById("level-menu-toggle"); + + if (!levelMenuPanel || !levelMenuToggle) { + return; + } + + const isOpening = levelMenuPanel.hasAttribute("hidden"); + + if (isOpening) { + levelMenuPanel.removeAttribute("hidden"); + } else { + levelMenuPanel.setAttribute("hidden", ""); + } + + levelMenuToggle.setAttribute("aria-expanded", String(isOpening)); +} + +function renderLevelMenu() { + const levelMenuList = document.getElementById("level-menu-list"); + + if (!levelMenuList) { + return; + } + + levelMenuList.innerHTML = ""; + + for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) { + const levelButton = document.createElement("button"); + const isUnlocked = levelIndex <= highestUnlockedLevelIndex; + const isCurrentLevel = levelIndex === currentLevelIndex; + + levelButton.type = "button"; + levelButton.classList.add("level-menu-item"); + levelButton.textContent = isUnlocked ? `Level ${levelIndex + 1}` : `Level ${levelIndex + 1} - Locked`; + levelButton.disabled = !isUnlocked; + + if (isCurrentLevel) { + levelButton.classList.add("is-current"); + } + + levelButton.addEventListener("click", () => { + if (!isUnlocked) { + return; + } + + goToLevel(levelIndex); + const levelMenuPanel = document.getElementById("level-menu-panel"); + const levelMenuToggle = document.getElementById("level-menu-toggle"); + + if (levelMenuPanel) { + levelMenuPanel.setAttribute("hidden", ""); + } + + if (levelMenuToggle) { + levelMenuToggle.setAttribute("aria-expanded", "false"); + } + }); + + levelMenuList.appendChild(levelButton); + } +} + +function setupLevelMenu() { + const levelMenuToggle = document.getElementById("level-menu-toggle"); + + if (!levelMenuToggle) { + return; + } + + levelMenuToggle.addEventListener("click", toggleLevelMenu); + renderLevelMenu(); +} + function getCurrentGlassOptions() { return glassOptions[currentLevelIndex] || []; } @@ -892,22 +995,19 @@ function traceLaser() { } function finish() { + unlockLevel(currentLevelIndex + 1); + setTimeout(() => { const winOverlay = document.querySelector(".win-overlay"); winOverlay.style.visibility = "visible"; }, 100); } -function nextLevel() { - currentLevelIndex++; - +function goToLevel(levelIndex) { + currentLevelIndex = levelIndex; isLevelFinished = false; stopAllRotatorButtons(); - if (currentLevelIndex >= levels.length) { - currentLevelIndex = 0; - } - initializeMirrorOrientations(); glassPlacements = {}; resetGlassInventory(); @@ -923,10 +1023,18 @@ function nextLevel() { const winOverlay = document.querySelector(".win-overlay"); winOverlay.style.visibility = "hidden"; + renderLevelMenu(); } +function nextLevel() { + const nextLevelIndex = currentLevelIndex + 1 >= levels.length ? 0 : currentLevelIndex + 1; + goToLevel(nextLevelIndex); +} + +loadUnlockedLevels(); resetGlassInventory(); createPalette(); initializeMirrorOrientations(); blockBrowserDrop(); +setupLevelMenu(); traceLaser(); diff --git a/web/templates/view/game.html b/web/templates/view/game.html index 8ee4b57..a0351a0 100644 --- a/web/templates/view/game.html +++ b/web/templates/view/game.html @@ -15,6 +15,16 @@ + +