Add level selection menu and persistence
Introduce a level selection UI and persistence for unlocked levels. Adds CSS styles and HTML markup for a floating level menu, and JS to load/save highest unlocked level to localStorage (key: mirror-game-highest-unlocked-level). Implements functions to render/toggle the menu, unlock levels on finish, and navigate to arbitrary levels via goToLevel. nextLevel now delegates to goToLevel, and setup calls loadUnlockedLevels() and setupLevelMenu() so the menu reflects progress immediately.
This commit is contained in:
@@ -20,6 +20,71 @@ body {
|
|||||||
user-select: none;
|
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 {
|
main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -162,6 +162,8 @@ let rotatorIntervals = {};
|
|||||||
let toggledDoors = {};
|
let toggledDoors = {};
|
||||||
let poweredCaptors = {};
|
let poweredCaptors = {};
|
||||||
let draggedGlassColor = null;
|
let draggedGlassColor = null;
|
||||||
|
let highestUnlockedLevelIndex = 0;
|
||||||
|
const unlockedLevelsStorageKey = "mirror-game-highest-unlocked-level";
|
||||||
|
|
||||||
function getCurrentLevel() {
|
function getCurrentLevel() {
|
||||||
return levels[currentLevelIndex];
|
return levels[currentLevelIndex];
|
||||||
@@ -171,6 +173,107 @@ function getCurrentLevelConfig(configByLevel) {
|
|||||||
return configByLevel[currentLevelIndex] || {};
|
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() {
|
function getCurrentGlassOptions() {
|
||||||
return glassOptions[currentLevelIndex] || [];
|
return glassOptions[currentLevelIndex] || [];
|
||||||
}
|
}
|
||||||
@@ -892,22 +995,19 @@ function traceLaser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function finish() {
|
function finish() {
|
||||||
|
unlockLevel(currentLevelIndex + 1);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const winOverlay = document.querySelector(".win-overlay");
|
const winOverlay = document.querySelector(".win-overlay");
|
||||||
winOverlay.style.visibility = "visible";
|
winOverlay.style.visibility = "visible";
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextLevel() {
|
function goToLevel(levelIndex) {
|
||||||
currentLevelIndex++;
|
currentLevelIndex = levelIndex;
|
||||||
|
|
||||||
isLevelFinished = false;
|
isLevelFinished = false;
|
||||||
stopAllRotatorButtons();
|
stopAllRotatorButtons();
|
||||||
|
|
||||||
if (currentLevelIndex >= levels.length) {
|
|
||||||
currentLevelIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeMirrorOrientations();
|
initializeMirrorOrientations();
|
||||||
glassPlacements = {};
|
glassPlacements = {};
|
||||||
resetGlassInventory();
|
resetGlassInventory();
|
||||||
@@ -923,10 +1023,18 @@ function nextLevel() {
|
|||||||
|
|
||||||
const winOverlay = document.querySelector(".win-overlay");
|
const winOverlay = document.querySelector(".win-overlay");
|
||||||
winOverlay.style.visibility = "hidden";
|
winOverlay.style.visibility = "hidden";
|
||||||
|
renderLevelMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function nextLevel() {
|
||||||
|
const nextLevelIndex = currentLevelIndex + 1 >= levels.length ? 0 : currentLevelIndex + 1;
|
||||||
|
goToLevel(nextLevelIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadUnlockedLevels();
|
||||||
resetGlassInventory();
|
resetGlassInventory();
|
||||||
createPalette();
|
createPalette();
|
||||||
initializeMirrorOrientations();
|
initializeMirrorOrientations();
|
||||||
blockBrowserDrop();
|
blockBrowserDrop();
|
||||||
|
setupLevelMenu();
|
||||||
traceLaser();
|
traceLaser();
|
||||||
|
|||||||
@@ -15,6 +15,16 @@
|
|||||||
<button class="win-button" onclick="nextLevel()">Next Level</button>
|
<button class="win-button" onclick="nextLevel()">Next Level</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<aside class="level-menu-shell">
|
||||||
|
<button id="level-menu-toggle" class="level-menu-toggle" type="button" aria-expanded="false" aria-controls="level-menu-panel">
|
||||||
|
Levels
|
||||||
|
</button>
|
||||||
|
<div id="level-menu-panel" class="level-menu-panel" hidden>
|
||||||
|
<h2>Choose Level</h2>
|
||||||
|
<div id="level-menu-list" class="level-menu-list"></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
atOptions = {
|
atOptions = {
|
||||||
'key' : '72b6ba1a1c26b9671167b66063c7e699',
|
'key' : '72b6ba1a1c26b9671167b66063c7e699',
|
||||||
|
|||||||
Reference in New Issue
Block a user