1159 lines
37 KiB
JavaScript
1159 lines
37 KiB
JavaScript
// Legend of grid case
|
|
|
|
const legend = {
|
|
empty: 0,
|
|
laser: 1,
|
|
coloredLaser: 2,
|
|
mirror: 3,
|
|
door: 4,
|
|
button: 5,
|
|
wall: 6,
|
|
target: 7,
|
|
ligthLaser: 8,
|
|
demiWallCornerUpLeft: 9,
|
|
demiWallCornerUpRight: 10,
|
|
demiWallCornerDownLeft: 11,
|
|
demiWallCornerDownRight: 12,
|
|
doorOpen: 13,
|
|
button2: 14,
|
|
captor: 15,
|
|
cable: 16,
|
|
captorTurn: 17,
|
|
cableVertical: 18,
|
|
captorTurnReturn: 19,
|
|
rotatorButton: 20,
|
|
cableTurn: 21,
|
|
horizontalSemi: 22,
|
|
cableTurnHorizontale : 23,
|
|
cableTurnHorizontale2 : 24,
|
|
captorTurnHorizontal : 25,
|
|
wallSemiAngle: 26,
|
|
};
|
|
|
|
const laserColors = {
|
|
white: "white",
|
|
red: "red",
|
|
blue: "blue",
|
|
yellow: "yellow",
|
|
};
|
|
|
|
const glassOptions = [
|
|
[
|
|
{ color: laserColors.red, maxAmount: 1, currentAmount: 1 },
|
|
{ color: laserColors.blue, maxAmount: 1, currentAmount: 1 },
|
|
{ color: laserColors.yellow, maxAmount: 0, currentAmount: 0 },
|
|
],
|
|
[
|
|
{ color: laserColors.red, maxAmount: 1, currentAmount: 1 },
|
|
{ color: laserColors.blue, maxAmount: 1, currentAmount: 1 },
|
|
{ color: laserColors.yellow, maxAmount: 1, currentAmount: 1 },
|
|
],
|
|
[
|
|
{ color: laserColors.red, maxAmount: 1, currentAmount: 1 },
|
|
{ color: laserColors.blue, maxAmount: 2, currentAmount:2},
|
|
{ color: laserColors.yellow, maxAmount: 1, currentAmount: 1 },
|
|
],
|
|
[
|
|
{ color: laserColors.red, maxAmount: 2, currentAmount: 2 },
|
|
{ color: laserColors.blue, maxAmount: 1, currentAmount: 1 },
|
|
{ color: laserColors.yellow, maxAmount: 2, currentAmount: 2 },
|
|
],
|
|
];
|
|
|
|
let levels = [
|
|
[
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 1, 0, 5, 4, 0, 10, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 7, 6, 6, 6, 6, 0, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 3, 0, 0, 0, 0, 12, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
],
|
|
|
|
[
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
[6, 6, 1, 0, 0, 0, 17, 0, 0, 3, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 18, 6, 6, 0, 6, 6, 6, 6, 6],
|
|
[6, 6, 7, 0, 0, 0, 4, 0, 0, 12, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
[6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
|
|
],
|
|
|
|
[
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 11, 0, 0, 0, 0],
|
|
[7, 1, 0, 0, 0, 4, 0, 0, 0, 3, 21, 0, 0, 0, 0],
|
|
[0, 6, 6, 6, 6, 18, 6, 6, 6, 0, 18, 0, 0, 0, 0],
|
|
[0, 6, 9, 0, 0, 19, 0, 0, 0, 12, 18, 0, 0, 0, 0],
|
|
[0, 6, 0, 6, 6, 6, 6, 6, 6, 24, 23, 0, 0, 0, 0],
|
|
[0, 6, 11, 0, 0, 0, 0, 0, 20, 23, 9, 0, 0, 0, 0],
|
|
[0, 10, 6, 6, 6, 6, 6, 6, 6, 9, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
],
|
|
|
|
[
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 6, 6, 6, 6, 6, 6, 11, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 1, 0, 0, 0, 0, 10, 6, 0, 0, 0, 0],
|
|
[0, 0, 0, 3, 0, 16, 16, 20, 0, 3, 6, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 6, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 12, 6, 6, 6, 6, 6, 9, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 6, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 6, 0, 0, 3, 0, 0, 7, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 6, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 10, 6, 6, 6, 6, 6, 0, 0, 0, 0, 0],
|
|
],
|
|
];
|
|
/*
|
|
[
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
], */
|
|
let currentLevelIndex = 0;
|
|
|
|
const initialMirrorAngles = [
|
|
{
|
|
"6,4": 315,
|
|
},
|
|
{},
|
|
{
|
|
"2,9": 225,
|
|
},
|
|
{}
|
|
];
|
|
|
|
const buttonGroups = [
|
|
{
|
|
"4,6": 1,
|
|
},
|
|
{},
|
|
{
|
|
"9,4": 1,
|
|
},
|
|
];
|
|
|
|
const doorGroups = [
|
|
{
|
|
"4,7": 1,
|
|
},
|
|
{
|
|
"4,6": 1,
|
|
},
|
|
{},
|
|
];
|
|
|
|
const captorGroups = [
|
|
{},
|
|
{
|
|
"2,6": 1,
|
|
},
|
|
{},
|
|
];
|
|
|
|
const rotatorButtons = [
|
|
{},
|
|
{},
|
|
{
|
|
"6,8": { mirrorX: 9, mirrorY: 2, step: 22.5, intervalMs: 1000 },
|
|
},
|
|
{
|
|
"3,7": { mirrorX: 7, mirrorY: 7, step: -22.5, intervalMs: 1000 },
|
|
},
|
|
];
|
|
|
|
let laserDirection = { dx: 0, dy: 0 };
|
|
let laserSegments = {};
|
|
let mirrorOrientations = {};
|
|
let glassPlacements = {};
|
|
let activatedButtons = {};
|
|
let openedDoors = {};
|
|
let isLevelFinished = false;
|
|
let activeRotatorButtons = {};
|
|
let rotatorIntervals = {};
|
|
let toggledDoors = {};
|
|
let poweredCaptors = {};
|
|
let draggedGlassColor = null;
|
|
let highestUnlockedLevelIndex = 0;
|
|
const unlockedLevelsStorageKey = "mirror-game-highest-unlocked-level";
|
|
|
|
function getCurrentLevel() {
|
|
return levels[currentLevelIndex];
|
|
}
|
|
|
|
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] || [];
|
|
}
|
|
|
|
function getGlassOptionByColor(color) {
|
|
const currentGlassOptions = getCurrentGlassOptions();
|
|
|
|
for (let i = 0; i < currentGlassOptions.length; i++) {
|
|
if (currentGlassOptions[i].color === color) {
|
|
return currentGlassOptions[i];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function resetGlassInventory() {
|
|
const currentGlassOptions = getCurrentGlassOptions();
|
|
|
|
for (let i = 0; i < currentGlassOptions.length; i++) {
|
|
currentGlassOptions[i].currentAmount = currentGlassOptions[i].maxAmount;
|
|
}
|
|
}
|
|
|
|
function normalizeLaserDirection(dx, dy) {
|
|
const epsilon = 0.0001;
|
|
const normalizedDx = Math.abs(dx) < epsilon ? 0 : Math.sign(dx);
|
|
const normalizedDy = Math.abs(dy) < epsilon ? 0 : Math.sign(dy);
|
|
|
|
return { dx: normalizedDx, dy: normalizedDy };
|
|
}
|
|
|
|
function reflectLaser(direction, mirrorAngle) {
|
|
const mirrorRadians = mirrorAngle * (Math.PI / 180);
|
|
const mirrorVectorX = Math.cos(mirrorRadians);
|
|
const mirrorVectorY = Math.sin(mirrorRadians);
|
|
const dotProduct = (direction.dx * mirrorVectorX) + (direction.dy * mirrorVectorY);
|
|
const reflectedDx = (2 * dotProduct * mirrorVectorX) - direction.dx;
|
|
const reflectedDy = (2 * dotProduct * mirrorVectorY) - direction.dy;
|
|
|
|
return normalizeLaserDirection(reflectedDx, reflectedDy);
|
|
}
|
|
|
|
function reverseLaser(direction) {
|
|
return {
|
|
dx: direction.dx * -1,
|
|
dy: direction.dy * -1,
|
|
};
|
|
}
|
|
|
|
function getLaserSegmentClass(segmentDirection) {
|
|
if (!segmentDirection) {
|
|
return "laser-horizontal";
|
|
}
|
|
|
|
if (segmentDirection.dx === 0) {
|
|
return "laser-vertical";
|
|
}
|
|
|
|
if (segmentDirection.dy === 0) {
|
|
return "laser-horizontal";
|
|
}
|
|
|
|
if (segmentDirection.dx === segmentDirection.dy) {
|
|
return "laser-diagonal-down";
|
|
}
|
|
|
|
return "laser-diagonal-up";
|
|
}
|
|
|
|
function getLaserColorClass(color) {
|
|
return `laser-color-${color || laserColors.white}`;
|
|
}
|
|
|
|
function getButtonGroup(x, y) {
|
|
const cellType = getCurrentLevel()[y][x];
|
|
|
|
if (cellType === legend.button2) {
|
|
return 2;
|
|
}
|
|
|
|
return getCurrentLevelConfig(buttonGroups)[`${y},${x}`] || 1;
|
|
}
|
|
|
|
function getDoorGroup(x, y) {
|
|
return getCurrentLevelConfig(doorGroups)[`${y},${x}`] || 1;
|
|
}
|
|
|
|
function getCaptorGroup(x, y) {
|
|
return getCurrentLevelConfig(captorGroups)[`${y},${x}`] || getDoorGroup(x, y);
|
|
}
|
|
|
|
function openDoorsFromButton(x, y) {
|
|
const buttonGroup = getButtonGroup(x, y);
|
|
const level = getCurrentLevel();
|
|
|
|
for (let doorY = 0; doorY < level.length; doorY++) {
|
|
for (let doorX = 0; doorX < level[doorY].length; doorX++) {
|
|
if (level[doorY][doorX] === legend.door && getDoorGroup(doorX, doorY) === buttonGroup) {
|
|
openedDoors[`${doorY},${doorX}`] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggleDoorsFromCaptor(x, y) {
|
|
const captorGroup = getCaptorGroup(x, y);
|
|
const level = getCurrentLevel();
|
|
|
|
for (let doorY = 0; doorY < level.length; doorY++) {
|
|
for (let doorX = 0; doorX < level[doorY].length; doorX++) {
|
|
const coordKey = `${doorY},${doorX}`;
|
|
|
|
if (level[doorY][doorX] === legend.door && getDoorGroup(doorX, doorY) === captorGroup) {
|
|
toggledDoors[coordKey] = !toggledDoors[coordKey];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function getRotatorButtonConfig(x, y) {
|
|
return getCurrentLevelConfig(rotatorButtons)[`${y},${x}`];
|
|
}
|
|
|
|
function isMirrorControlledByButton(x, y) {
|
|
const rotatorEntries = Object.values(getCurrentLevelConfig(rotatorButtons));
|
|
|
|
for (let i = 0; i < rotatorEntries.length; i++) {
|
|
const rotatorConfig = rotatorEntries[i];
|
|
|
|
if (rotatorConfig.mirrorX === x && rotatorConfig.mirrorY === y) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function rotateMirrorStep(x, y, angleStep) {
|
|
const coordKey = `${y},${x}`;
|
|
|
|
if (getCurrentLevel()[y][x] !== legend.mirror) {
|
|
return;
|
|
}
|
|
|
|
let currentAngle = mirrorOrientations[coordKey] || 0;
|
|
currentAngle = (currentAngle + angleStep) % 360;
|
|
|
|
if (currentAngle < 0) {
|
|
currentAngle += 360;
|
|
}
|
|
|
|
mirrorOrientations[coordKey] = currentAngle;
|
|
}
|
|
|
|
function syncRotatorButtons() {
|
|
const currentLevelRotatorButtons = getCurrentLevelConfig(rotatorButtons);
|
|
const rotatorKeys = Object.keys(currentLevelRotatorButtons);
|
|
|
|
for (let i = 0; i < rotatorKeys.length; i++) {
|
|
const key = rotatorKeys[i];
|
|
const config = currentLevelRotatorButtons[key];
|
|
const isActive = activeRotatorButtons[key] === true;
|
|
|
|
if (isActive && !rotatorIntervals[key]) {
|
|
rotatorIntervals[key] = window.setInterval(() => {
|
|
rotateMirrorStep(config.mirrorX, config.mirrorY, -22.5);
|
|
traceLaser();
|
|
}, 1000);
|
|
}
|
|
|
|
if (!isActive && rotatorIntervals[key]) {
|
|
window.clearInterval(rotatorIntervals[key]);
|
|
delete rotatorIntervals[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
function stopAllRotatorButtons() {
|
|
const intervalKeys = Object.keys(rotatorIntervals);
|
|
|
|
for (let i = 0; i < intervalKeys.length; i++) {
|
|
const key = intervalKeys[i];
|
|
window.clearInterval(rotatorIntervals[key]);
|
|
}
|
|
|
|
rotatorIntervals = {};
|
|
}
|
|
|
|
function saveLaserSegment(x, y, direction, color) {
|
|
laserSegments[`${y},${x}`] = {
|
|
direction: { ...direction },
|
|
color: color,
|
|
};
|
|
}
|
|
|
|
function isGlassOnCell(x, y) {
|
|
return glassPlacements[`${y},${x}`] !== undefined;
|
|
}
|
|
|
|
function drawGlassInCell(cell, x, y) {
|
|
const glassColor = glassPlacements[`${y},${x}`];
|
|
|
|
if (!glassColor) {
|
|
return;
|
|
}
|
|
|
|
const glassDiv = document.createElement("div");
|
|
glassDiv.classList.add("cell-glass", `glass-${glassColor}`);
|
|
cell.appendChild(glassDiv);
|
|
}
|
|
|
|
function drawLaserInCell(cell, segmentData) {
|
|
if (!segmentData) {
|
|
return;
|
|
}
|
|
|
|
const laserDiv = document.createElement("div");
|
|
laserDiv.classList.add("laser-overlay");
|
|
laserDiv.classList.add(getLaserSegmentClass(segmentData.direction));
|
|
laserDiv.classList.add(getLaserColorClass(segmentData.color));
|
|
cell.appendChild(laserDiv);
|
|
}
|
|
|
|
function createPalette() {
|
|
const palette = document.getElementById("glass-palette");
|
|
|
|
if (!palette) {
|
|
return;
|
|
}
|
|
|
|
palette.innerHTML = "";
|
|
|
|
const currentGlassOptions = getCurrentGlassOptions();
|
|
|
|
for (let i = 0; i < currentGlassOptions.length; i++) {
|
|
const glassOption = currentGlassOptions[i];
|
|
const glassColor = glassOption.color;
|
|
const glassButton = document.createElement("button");
|
|
glassButton.type = "button";
|
|
glassButton.classList.add("glass-item", `glass-${glassColor}`);
|
|
glassButton.draggable = glassOption.currentAmount > 0;
|
|
|
|
if (glassOption.currentAmount <= 0) {
|
|
glassButton.disabled = true;
|
|
}
|
|
|
|
const glassLabel = document.createElement("span");
|
|
glassLabel.classList.add("glass-item-label");
|
|
glassLabel.textContent = `${glassOption.currentAmount}/${glassOption.maxAmount}`;
|
|
glassButton.appendChild(glassLabel);
|
|
|
|
glassButton.addEventListener("dragstart", (event) => {
|
|
if (glassOption.currentAmount <= 0) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
draggedGlassColor = glassColor;
|
|
event.dataTransfer.effectAllowed = "copy";
|
|
event.dataTransfer.setData("application/x-glass-color", glassColor);
|
|
});
|
|
|
|
glassButton.addEventListener("dragend", () => {
|
|
draggedGlassColor = null;
|
|
});
|
|
|
|
palette.appendChild(glassButton);
|
|
}
|
|
}
|
|
|
|
function addDropEvents(cell, x, y) {
|
|
cell.addEventListener("dragover", (event) => {
|
|
if (isLevelFinished) {
|
|
return;
|
|
}
|
|
|
|
const transferTypes = event.dataTransfer ? Array.from(event.dataTransfer.types || []) : [];
|
|
if (!draggedGlassColor) {
|
|
return;
|
|
}
|
|
|
|
const cellType = getCurrentLevel()[y][x];
|
|
if (cellType !== legend.empty) {
|
|
return;
|
|
}
|
|
|
|
const selectedColor = draggedGlassColor;
|
|
const glassOption = getGlassOptionByColor(selectedColor);
|
|
|
|
if (!glassOption || glassOption.currentAmount <= 0) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
cell.classList.add("can-drop");
|
|
});
|
|
|
|
cell.addEventListener("dragleave", () => {
|
|
cell.classList.remove("can-drop");
|
|
});
|
|
|
|
cell.addEventListener("drop", (event) => {
|
|
if (isLevelFinished) {
|
|
return;
|
|
}
|
|
|
|
const transferredColor = event.dataTransfer ? event.dataTransfer.getData("application/x-glass-color") : "";
|
|
const selectedColor = transferredColor || draggedGlassColor;
|
|
cell.classList.remove("can-drop");
|
|
|
|
if (!selectedColor) {
|
|
return;
|
|
}
|
|
|
|
if (getCurrentLevel()[y][x] !== legend.empty) {
|
|
return;
|
|
}
|
|
|
|
if (isGlassOnCell(x, y)) {
|
|
return;
|
|
}
|
|
|
|
const glassOption = getGlassOptionByColor(selectedColor);
|
|
|
|
if (!glassOption || glassOption.currentAmount <= 0) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
glassPlacements[`${y},${x}`] = selectedColor;
|
|
glassOption.currentAmount--;
|
|
draggedGlassColor = null;
|
|
createPalette();
|
|
traceLaser();
|
|
});
|
|
|
|
cell.addEventListener("dblclick", () => {
|
|
if (isLevelFinished) {
|
|
return;
|
|
}
|
|
|
|
if (isGlassOnCell(x, y)) {
|
|
const glassColor = glassPlacements[`${y},${x}`];
|
|
const glassOption = getGlassOptionByColor(glassColor);
|
|
|
|
delete glassPlacements[`${y},${x}`];
|
|
|
|
if (glassOption) {
|
|
glassOption.currentAmount = Math.min(glassOption.currentAmount + 1, glassOption.maxAmount);
|
|
}
|
|
|
|
createPalette();
|
|
traceLaser();
|
|
}
|
|
});
|
|
}
|
|
|
|
function blockBrowserDrop() {
|
|
document.addEventListener("dragover", (event) => {
|
|
event.preventDefault();
|
|
});
|
|
|
|
document.addEventListener("drop", (event) => {
|
|
event.preventDefault();
|
|
});
|
|
}
|
|
|
|
function initializeMirrorOrientations() {
|
|
mirrorOrientations = {};
|
|
const level = getCurrentLevel();
|
|
|
|
for (let y = 0; y < level.length; y++) {
|
|
for (let x = 0; x < level[y].length; x++) {
|
|
if (level[y][x] === legend.mirror) {
|
|
mirrorOrientations[`${y},${x}`] = getCurrentLevelConfig(initialMirrorAngles)[`${y},${x}`] || 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function loadGrid() {
|
|
const mapDiv = document.getElementById("map");
|
|
const level = getCurrentLevel();
|
|
mapDiv.innerHTML = "";
|
|
|
|
for (let y = 0; y < level.length; y++) {
|
|
const lign = document.createElement("div");
|
|
lign.classList.add("lign");
|
|
|
|
for (let x = 0; x < level[y].length; x++) {
|
|
const cell = document.createElement("div");
|
|
cell.classList.add("cell");
|
|
addDropEvents(cell, x, y);
|
|
|
|
switch (level[y][x]) {
|
|
case legend.empty:
|
|
cell.classList.add("empty");
|
|
break;
|
|
case legend.laser:
|
|
cell.classList.add("laser");
|
|
break;
|
|
case legend.coloredLaser:
|
|
cell.classList.add("colored-laser");
|
|
break;
|
|
case legend.mirror:
|
|
cell.classList.add("mirror");
|
|
const currentAngle = mirrorOrientations[`${y},${x}`] || 0;
|
|
const img = document.createElement("img");
|
|
img.src = "/web/assets/img/tiles/Mirror.svg";
|
|
img.classList.add("mirror-img");
|
|
img.style.transform = `rotate(${currentAngle}deg)`;
|
|
|
|
if (isMirrorControlledByButton(x, y)) {
|
|
const mirrorDisplay = document.createElement("div");
|
|
mirrorDisplay.classList.add("btn-mirror", "btn-mirror-locked");
|
|
mirrorDisplay.appendChild(img);
|
|
cell.appendChild(mirrorDisplay);
|
|
} else {
|
|
const btnMirror = document.createElement("button");
|
|
btnMirror.classList.add("btn-mirror");
|
|
btnMirror.type = "button";
|
|
btnMirror.appendChild(img);
|
|
btnMirror.onmousedown = (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (!isLevelFinished) {
|
|
rotateMirror(x, y, event.button === 2);
|
|
}
|
|
};
|
|
btnMirror.oncontextmenu = (event) => event.preventDefault();
|
|
cell.appendChild(btnMirror);
|
|
}
|
|
break;
|
|
case legend.door:
|
|
cell.classList.add("door");
|
|
if (openedDoors[`${y},${x}`]) {
|
|
cell.classList.add("door-open");
|
|
}
|
|
break;
|
|
case legend.doorOpen:
|
|
cell.classList.add("door", "door-open");
|
|
break;
|
|
case legend.button:
|
|
cell.classList.add("button");
|
|
if (activatedButtons[`${y},${x}`]) {
|
|
cell.classList.add("button-active");
|
|
}
|
|
break;
|
|
case legend.button2:
|
|
cell.classList.add("button", "button-2");
|
|
if (activatedButtons[`${y},${x}`]) {
|
|
cell.classList.add("button-active");
|
|
}
|
|
break;
|
|
case legend.rotatorButton:
|
|
cell.classList.add("button", "button-rotator");
|
|
if (activatedButtons[`${y},${x}`]) {
|
|
cell.classList.add("button-active");
|
|
}
|
|
break;
|
|
case legend.wall:
|
|
cell.classList.add("wall");
|
|
break;
|
|
case legend.target:
|
|
cell.classList.add("target");
|
|
break;
|
|
case legend.demiWallCornerUpLeft:
|
|
cell.classList.add("demi-wall-corner-up-left");
|
|
break;
|
|
case legend.demiWallCornerUpRight:
|
|
cell.classList.add("demi-wall-corner-up-right");
|
|
break;
|
|
case legend.demiWallCornerDownLeft:
|
|
cell.classList.add("demi-wall-corner-down-left");
|
|
break;
|
|
case legend.demiWallCornerDownRight:
|
|
cell.classList.add("demi-wall-corner-down-right");
|
|
break;
|
|
case legend.captor:
|
|
cell.classList.add("captor");
|
|
break;
|
|
case legend.cable:
|
|
cell.classList.add("cable");
|
|
break;
|
|
case legend.captorTurn:
|
|
cell.classList.add("captor-turn");
|
|
break;
|
|
case legend.cableVertical:
|
|
cell.classList.add("cable-vertical");
|
|
break;
|
|
case legend.captorTurnReturn:
|
|
cell.classList.add("captor-turn-reverse");
|
|
break;
|
|
case legend.cableTurn:
|
|
cell.classList.add("cable-turn");
|
|
break;
|
|
case legend.horizontalSemi:
|
|
cell.classList.add("horizontal-semi");
|
|
break;
|
|
case legend.cableTurnHorizontale:
|
|
cell.classList.add("cable-turn-horizontale");
|
|
break;
|
|
case legend.cableTurnHorizontale2:
|
|
cell.classList.add("cable-turn-horizontale2");
|
|
break;
|
|
case legend.captorTurnHorizontal:
|
|
cell.classList.add("captor-turn-horizontale2");
|
|
break;
|
|
case legend.wallSemiAngle:
|
|
cell.classList.add("wall-semi-angle");
|
|
break;
|
|
}
|
|
|
|
drawLaserInCell(cell, laserSegments[`${y},${x}`]);
|
|
drawGlassInCell(cell, x, y);
|
|
lign.appendChild(cell);
|
|
}
|
|
|
|
mapDiv.appendChild(lign);
|
|
}
|
|
}
|
|
|
|
function rotateMirror(x, y, isRightClick) {
|
|
rotateMirrorStep(x, y, isRightClick ? 22.5 : -22.5);
|
|
traceLaser();
|
|
}
|
|
|
|
function traceLaser() {
|
|
laserSegments = {};
|
|
activatedButtons = {};
|
|
openedDoors = { ...toggledDoors };
|
|
activeRotatorButtons = {};
|
|
isLevelFinished = false;
|
|
const nextPoweredCaptors = {};
|
|
|
|
const level = getCurrentLevel();
|
|
let startLaserX;
|
|
let startLaserY;
|
|
|
|
for (let y = 0; y < level.length; y++) {
|
|
for (let x = 0; x < level[y].length; x++) {
|
|
if (level[y][x] === legend.laser) {
|
|
startLaserX = x;
|
|
startLaserY = y;
|
|
laserDirection = { dx: 1, dy: 0 };
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (startLaserX !== undefined) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (startLaserX === undefined) {
|
|
return;
|
|
}
|
|
|
|
let currentX = startLaserX;
|
|
let currentY = startLaserY;
|
|
let currentLaserColor = laserColors.white;
|
|
let laserActive = true;
|
|
let iterations = 0;
|
|
const maxIterations = 1000;
|
|
|
|
while (laserActive && iterations < maxIterations) {
|
|
iterations++;
|
|
currentX += laserDirection.dx;
|
|
currentY += laserDirection.dy;
|
|
|
|
if (currentX < 0 || currentX >= level[0].length || currentY < 0 || currentY >= level.length) {
|
|
laserActive = false;
|
|
break;
|
|
}
|
|
|
|
const cellType = level[currentY][currentX];
|
|
const glassColor = glassPlacements[`${currentY},${currentX}`];
|
|
|
|
if (glassColor) {
|
|
currentLaserColor = glassColor;
|
|
}
|
|
|
|
if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
|
|
if (cellType === legend.target) {
|
|
laserActive = false;
|
|
isLevelFinished = true;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
switch (cellType) {
|
|
case legend.laser:
|
|
case legend.coloredLaser:
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
break;
|
|
|
|
case legend.empty:
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
break;
|
|
|
|
case legend.target:
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
laserActive = false;
|
|
isLevelFinished = true;
|
|
break;
|
|
|
|
case legend.mirror:
|
|
if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else {
|
|
const mirrorAngle = mirrorOrientations[`${currentY},${currentX}`] || 0;
|
|
laserDirection = reflectLaser(laserDirection, mirrorAngle);
|
|
}
|
|
break;
|
|
|
|
case legend.wall:
|
|
if (currentLaserColor === laserColors.blue) {
|
|
laserDirection = reverseLaser(laserDirection);
|
|
} else if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else {
|
|
laserActive = false;
|
|
}
|
|
break;
|
|
case legend.wallSemiAngle:
|
|
if (currentLaserColor === laserColors.blue) {
|
|
laserDirection = reverseLaser(laserDirection);
|
|
} else if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else {
|
|
laserActive = false;
|
|
}
|
|
break;
|
|
case legend.horizontalSemi:
|
|
if (currentLaserColor === laserColors.blue) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
laserDirection = reflectLaser(laserDirection, 0);
|
|
} else if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else {
|
|
laserActive = false;
|
|
}
|
|
break;
|
|
case legend.cableVertical:
|
|
case legend.cableTurn:
|
|
case legend.door:
|
|
case legend.doorOpen:
|
|
if (openedDoors[`${currentY},${currentX}`] || cellType === legend.doorOpen) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else if (currentLaserColor === laserColors.blue) {
|
|
laserDirection = reverseLaser(laserDirection);
|
|
} else if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else {
|
|
laserActive = false;
|
|
}
|
|
break;
|
|
case legend.button:
|
|
case legend.button2:
|
|
if (currentLaserColor === laserColors.red) {
|
|
activatedButtons[`${currentY},${currentX}`] = true;
|
|
openDoorsFromButton(currentX, currentY);
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else if (currentLaserColor === laserColors.blue) {
|
|
laserDirection = reverseLaser(laserDirection);
|
|
} else {
|
|
laserActive = false;
|
|
}
|
|
break;
|
|
|
|
case legend.captor:
|
|
if (currentLaserColor === laserColors.blue) {
|
|
laserDirection = reverseLaser(laserDirection);
|
|
} else if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else {
|
|
laserActive = false;
|
|
}
|
|
break;
|
|
case legend.captorTurn:
|
|
if (currentLaserColor === laserColors.red) {
|
|
const captorKey = `${currentY},${currentX}`;
|
|
activatedButtons[captorKey] = true;
|
|
nextPoweredCaptors[captorKey] = true;
|
|
|
|
if (!poweredCaptors[captorKey]) {
|
|
toggleDoorsFromCaptor(currentX, currentY);
|
|
openedDoors = { ...toggledDoors };
|
|
}
|
|
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
laserActive = false;
|
|
}else if(currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else {
|
|
laserActive = false;
|
|
}
|
|
break;
|
|
case legend.captorTurnReturn:
|
|
if (currentLaserColor === laserColors.red) {
|
|
const captorKey = `${currentY},${currentX}`;
|
|
activatedButtons[captorKey] = true;
|
|
nextPoweredCaptors[captorKey] = true;
|
|
|
|
if (!poweredCaptors[captorKey]) {
|
|
toggleDoorsFromCaptor(currentX, currentY);
|
|
openedDoors = { ...toggledDoors };
|
|
}
|
|
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
laserActive = false;
|
|
}else if(currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else {
|
|
laserActive = false;
|
|
}
|
|
break;
|
|
case legend.captorTurnHorizontal:
|
|
if (currentLaserColor === laserColors.red) {
|
|
const captorKey = `${currentY},${currentX}`;
|
|
activatedButtons[captorKey] = true;
|
|
nextPoweredCaptors[captorKey] = true;
|
|
|
|
if (!poweredCaptors[captorKey]) {
|
|
toggleDoorsFromCaptor(currentX, currentY);
|
|
openedDoors = { ...toggledDoors };
|
|
}
|
|
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
laserActive = false;
|
|
}else if(currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else {
|
|
laserActive = false;
|
|
}
|
|
break;
|
|
case legend.rotatorButton:
|
|
if (currentLaserColor === laserColors.red) {
|
|
const rotatorKey = `${currentY},${currentX}`;
|
|
activatedButtons[rotatorKey] = true;
|
|
activeRotatorButtons[rotatorKey] = true;
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
laserActive = false;
|
|
} else if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else if (currentLaserColor === laserColors.blue) {
|
|
laserDirection = reverseLaser(laserDirection);
|
|
laserActive = false;
|
|
} else {
|
|
laserActive = false;
|
|
}
|
|
break;
|
|
|
|
case legend.demiWallCornerUpLeft:
|
|
if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else if (currentLaserColor === laserColors.blue) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
laserDirection = reflectLaser(laserDirection, 135);
|
|
}
|
|
break;
|
|
|
|
case legend.demiWallCornerUpRight:
|
|
if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else if (currentLaserColor === laserColors.blue) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
laserDirection = reflectLaser(laserDirection, 45);
|
|
}
|
|
break;
|
|
|
|
case legend.demiWallCornerDownLeft:
|
|
if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else if (currentLaserColor === laserColors.blue) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
laserDirection = reflectLaser(laserDirection, 225);
|
|
}
|
|
break;
|
|
|
|
case legend.demiWallCornerDownRight:
|
|
if (currentLaserColor === laserColors.yellow) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
} else if (currentLaserColor === laserColors.blue) {
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
laserDirection = reflectLaser(laserDirection, 315);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
|
|
break;
|
|
}
|
|
}
|
|
|
|
poweredCaptors = nextPoweredCaptors;
|
|
syncRotatorButtons();
|
|
loadGrid();
|
|
|
|
if (isLevelFinished) {
|
|
finish();
|
|
}
|
|
}
|
|
|
|
function finish() {
|
|
unlockLevel(currentLevelIndex + 1);
|
|
|
|
setTimeout(() => {
|
|
const winOverlay = document.querySelector(".win-overlay");
|
|
winOverlay.style.visibility = "visible";
|
|
}, 100);
|
|
}
|
|
|
|
function goToLevel(levelIndex) {
|
|
currentLevelIndex = levelIndex;
|
|
isLevelFinished = false;
|
|
stopAllRotatorButtons();
|
|
|
|
mirrorOrientations = {};
|
|
laserSegments = {};
|
|
activatedButtons = {};
|
|
openedDoors = {};
|
|
toggledDoors = {};
|
|
poweredCaptors = {};
|
|
initializeMirrorOrientations();
|
|
glassPlacements = {};
|
|
resetGlassInventory();
|
|
createPalette();
|
|
loadGrid();
|
|
traceLaser();
|
|
|
|
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();
|