8 Commits

Author SHA1 Message Date
Sysy's
954af3cded Fix relative paths 2026-03-31 14:55:00 +02:00
Sysy's
cfeded5079 X 2026-03-31 14:53:26 +02:00
Sysy's
1eb3aec524 Update game.js 2026-03-31 14:35:01 +02:00
1145d26e9d add lvl 4 2026-03-31 14:27:06 +02:00
Sysy's
f4c821ae97 Replace void with walls 2026-03-31 14:16:05 +02:00
Sysy's
90a0de0429 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.
2026-03-31 14:10:37 +02:00
Sysy's
34d213d5f4 Support limited glass inventory and yellow laser
Introduce per-level glass inventory and UI for draggable glass pieces, including disabled state and count labels. glassOptions now hold objects with color, maxAmount and currentAmount; palette creation uses these values to enable/disable dragging, show remaining counts, and update on place/remove. Drag-and-drop logic now tracks a draggedGlassColor fallback, prevents placing when inventory is empty, decrements/increments inventory on place/remove, and rebuilds the palette. Reset inventory on level start and when advancing levels. Also adjust laser tracing: yellow lasers are saved as segments and terminate on target, and yellow interacts with demi-wall corners by saving the segment instead of reflecting (blue still reflects). Add CSS for .glass-item:disabled and .glass-item-label. Overall fixes inventory handling and yellow-laser behavior.
2026-03-31 14:05:35 +02:00
6229fe7b9e Level 3 2026-03-31 13:49:02 +02:00
8 changed files with 613 additions and 89 deletions

View File

@@ -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;
@@ -108,7 +173,7 @@ main {
.laser {
background-color: #DADEEF;
background-image: url("../img/tiles/Laser.svg");
background-image: url("/web/assets/img/tiles/Laser.svg");
background-size: 80%;
background-repeat: no-repeat;
background-position: center;
@@ -117,7 +182,7 @@ main {
.colored-laser {
background-color: #DADEEF;
background-image: url("../img/tiles/Prisme.svg");
background-image: url("/web/assets/img/tiles/Prisme.svg");
background-size: 80%;
background-repeat: no-repeat;
background-position: center;
@@ -137,15 +202,31 @@ main {
.wall {
background-color: #DADEEF;
background-image: url("../img/tiles/Tuile.svg");
background-image: url("/web/assets/img/tiles/Tuile.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
}
.wall-semi-angle {
background-color: #DADEEF;
background-image: url("/web/assets/img/tiles/BottomLeft.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
}
.horizontal-semi {
background-color: #DADEEF;
background-image: url("/web/assets/img/tiles/HorizontaleSemi.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: bottom;
}
.captor {
background-color: #DADEEF;
background-image: url("../img/tiles/Capteur-1.svg");
background-image: url("/web/assets/img/tiles/Capteur-1.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
@@ -154,7 +235,24 @@ main {
.captor-turn {
background-color: #DADEEF;
background-image: url("../img/tiles/Capteur-2.svg");
background-image: url("/web/assets/img/tiles/Capteur-2.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
}
.captor-turn-reverse {
background-color: #DADEEF;
background-image: url("/web/assets/img/tiles/Capteur-2.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
transform: rotate(180deg);
}
.captor-turn-horizontale2 {
background-color: #DADEEF;
background-image: url("/web/assets/img/tiles/Capteur-1.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
@@ -162,31 +260,55 @@ main {
.cable {
background-color: #DADEEF;
background-image: url("../img/tiles/CableV.svg");
background-image: url("/web/assets/img/tiles/CableH.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
}
.cable-turn {
background-color: #DADEEF;
background-image: url("/web/assets/img/tiles/CableBottomLeft.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
transform: rotate(90deg);
}
.cable-vertical {
background-color: #DADEEF;
background-image: url("../img/tiles/CableV.svg");
background-image: url("/web/assets/img/tiles/CableV.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
}
.cable-turn-horizontale {
background-color: #DADEEF;
background-image: url("/web/assets/img/tiles/CableTopLeft.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
}
.cable-turn-horizontale2 {
background-color: #DADEEF;
background-image: url("/web/assets/img/tiles/CableTopLeft.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
transform: rotate(180deg);
}
.door {
background-color: #DADEEF;
background-image: url("../img/tiles/WoodenDoor.svg");
background-image: url("/web/assets/img/tiles/WoodenDoor.svg");
background-size: contain;
background-repeat: no-repeat;
background-position: end;
}
.door-open {
background-image: url("../img/tiles/WoodenDoor_openned.svg");
background-image: url("/web/assets/img/tiles/WoodenDoor_openned.svg");
background-size: contain;
background-repeat: no-repeat;
background-position: end;
@@ -194,7 +316,7 @@ main {
.button {
background-color: #DADEEF;
background-image: url("../img/tiles/ButtonComplete.svg"), url("../img/tiles/Tuile.svg");
background-image: url("/web/assets/img/tiles/ButtonComplete.svg"), url("/web/assets/img/tiles/Tuile.svg");
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
@@ -202,7 +324,7 @@ main {
.button-2 {
background-color: #DADEEF;
background-image: url("../img/tiles/ButtonQuarter.svg"), url("../img/tiles/Tuile.svg");
background-image: url("/web/assets/img/tiles/ButtonQuarter.svg"), url("/web/assets/img/tiles/Tuile.svg");
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
@@ -210,7 +332,7 @@ main {
.button-rotator {
background-color: #DADEEF;
background-image: url("../img/tiles/ButtonProfile.svg"), url("../img/tiles/Tuile.svg");
background-image: url("/web/assets/img/tiles/ButtonProfile.svg"), url("/web/assets/img/tiles/Tuile.svg");
background-size: 100% 100%;
background-repeat: no-repeat;
background-position: center;
@@ -222,7 +344,7 @@ main {
.target {
background-color: #DADEEF;
background-image: url("../img/tiles/Trigger.svg");
background-image: url("/web/assets/img/tiles/Trigger.svg");
background-size: 80%;
background-repeat: no-repeat;
background-position: center;
@@ -230,7 +352,7 @@ main {
.demi-wall-corner-up-left {
background-color: #DADEEF;
background-image: url("../img/tiles/TopLeft.svg");
background-image: url("/web/assets/img/tiles/TopLeft.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
@@ -238,7 +360,7 @@ main {
.demi-wall-corner-up-right {
background-color: #DADEEF;
background-image: url("../img/tiles/TopRight.svg");
background-image: url("/web/assets/img/tiles/TopRight.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
@@ -246,7 +368,7 @@ main {
.demi-wall-corner-down-left {
background-color: #DADEEF;
background-image: url("../img/tiles/BottomLeft.svg");
background-image: url("/web/assets/img/tiles/BottomLeftAngle.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
@@ -254,7 +376,7 @@ main {
.demi-wall-corner-down-right {
background-color: #DADEEF;
background-image: url("../img/tiles/BottomRight.svg");
background-image: url("/web/assets/img/tiles/BottomRightAngle.svg");
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
@@ -341,6 +463,24 @@ main {
font-weight: bold;
}
.glass-item:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.glass-item-label {
position: absolute;
right: 4px;
bottom: 3px;
z-index: 2;
font-size: 0.7rem;
font-weight: 700;
color: #223;
background: rgba(255, 255, 255, 0.88);
padding: 1px 5px;
border-radius: 999px;
}
.glass-item::after,
.cell-glass::after {
content: "";

View File

@@ -1,17 +1,19 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 99L1 2.41406L97.5859 99L1 99Z" fill="url(#paint0_linear_17_89)" stroke="url(#paint1_linear_17_89)" stroke-width="2"/>
<path d="M1 99L1 1.61816L99 50.6182V99L1 99Z" fill="url(#paint0_linear_35_19)" stroke="url(#paint1_linear_35_19)" stroke-width="2"/>
<circle cx="5.5" cy="94.5" r="1.5" transform="rotate(-180 5.5 94.5)" fill="#4A4A4A"/>
<circle cx="5.5" cy="94.5" r="1" transform="rotate(-180 5.5 94.5)" fill="#898989"/>
<circle cx="5.5" cy="13.5" r="1.5" transform="rotate(-180 5.5 13.5)" fill="#4A4A4A"/>
<circle cx="5.5" cy="13.5" r="1" transform="rotate(-180 5.5 13.5)" fill="#898989"/>
<circle cx="86.5" cy="94.5" r="1.5" transform="rotate(-180 86.5 94.5)" fill="#4A4A4A"/>
<circle cx="86.5" cy="94.5" r="1" transform="rotate(-180 86.5 94.5)" fill="#898989"/>
<circle cx="4.5" cy="8.5" r="1.5" transform="rotate(-180 4.5 8.5)" fill="#4A4A4A"/>
<circle cx="4.5" cy="8.5" r="1" transform="rotate(-180 4.5 8.5)" fill="#898989"/>
<circle cx="94.5" cy="94.5" r="1.5" transform="rotate(-180 94.5 94.5)" fill="#4A4A4A"/>
<circle cx="94.5" cy="94.5" r="1" transform="rotate(-180 94.5 94.5)" fill="#898989"/>
<circle cx="94.5" cy="53.5" r="1.5" transform="rotate(-180 94.5 53.5)" fill="#4A4A4A"/>
<circle cx="94.5" cy="53.5" r="1" transform="rotate(-180 94.5 53.5)" fill="#898989"/>
<defs>
<linearGradient id="paint0_linear_17_89" x1="0" y1="0" x2="100" y2="100" gradientUnits="userSpaceOnUse">
<linearGradient id="paint0_linear_35_19" x1="0" y1="0" x2="40" y2="80" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#747474"/>
</linearGradient>
<linearGradient id="paint1_linear_17_89" x1="100" y1="100" x2="0" y2="0" gradientUnits="userSpaceOnUse">
<linearGradient id="paint1_linear_35_19" x1="100" y1="50" x2="60" y2="-30" gradientUnits="userSpaceOnUse">
<stop stop-color="#A8A8A8"/>
<stop offset="1" stop-color="#848484"/>
</linearGradient>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,19 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 99L1 2.41406L97.5859 99L1 99Z" fill="url(#paint0_linear_17_89)" stroke="url(#paint1_linear_17_89)" stroke-width="2"/>
<circle cx="5.5" cy="94.5" r="1.5" transform="rotate(-180 5.5 94.5)" fill="#4A4A4A"/>
<circle cx="5.5" cy="94.5" r="1" transform="rotate(-180 5.5 94.5)" fill="#898989"/>
<circle cx="5.5" cy="13.5" r="1.5" transform="rotate(-180 5.5 13.5)" fill="#4A4A4A"/>
<circle cx="5.5" cy="13.5" r="1" transform="rotate(-180 5.5 13.5)" fill="#898989"/>
<circle cx="86.5" cy="94.5" r="1.5" transform="rotate(-180 86.5 94.5)" fill="#4A4A4A"/>
<circle cx="86.5" cy="94.5" r="1" transform="rotate(-180 86.5 94.5)" fill="#898989"/>
<defs>
<linearGradient id="paint0_linear_17_89" x1="0" y1="0" x2="100" y2="100" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#747474"/>
</linearGradient>
<linearGradient id="paint1_linear_17_89" x1="100" y1="100" x2="0" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#A8A8A8"/>
<stop offset="1" stop-color="#848484"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,17 +1,19 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M99 99H2.41406L99 2.41406V99Z" fill="url(#paint0_linear_19_190)" stroke="url(#paint1_linear_19_190)" stroke-width="2"/>
<path d="M99 99H1V50.6182L99 1.61816V99Z" fill="url(#paint0_linear_35_30)" stroke="url(#paint1_linear_35_30)" stroke-width="2"/>
<circle cx="94.5" cy="94.5" r="1.5" transform="rotate(90 94.5 94.5)" fill="#4A4A4A"/>
<circle cx="94.5" cy="94.5" r="1" transform="rotate(90 94.5 94.5)" fill="#898989"/>
<circle cx="13.5" cy="94.5" r="1.5" transform="rotate(90 13.5 94.5)" fill="#4A4A4A"/>
<circle cx="13.5" cy="94.5" r="1" transform="rotate(90 13.5 94.5)" fill="#898989"/>
<circle cx="94.5" cy="13.5" r="1.5" transform="rotate(90 94.5 13.5)" fill="#4A4A4A"/>
<circle cx="94.5" cy="13.5" r="1" transform="rotate(90 94.5 13.5)" fill="#898989"/>
<circle cx="5.5" cy="94.5" r="1.5" transform="rotate(90 5.5 94.5)" fill="#4A4A4A"/>
<circle cx="5.5" cy="94.5" r="1" transform="rotate(90 5.5 94.5)" fill="#898989"/>
<circle cx="5.5" cy="53.5" r="1.5" transform="rotate(90 5.5 53.5)" fill="#4A4A4A"/>
<circle cx="5.5" cy="53.5" r="1" transform="rotate(90 5.5 53.5)" fill="#898989"/>
<circle cx="94.5" cy="9.5" r="1.5" transform="rotate(90 94.5 9.5)" fill="#4A4A4A"/>
<circle cx="94.5" cy="9.5" r="1" transform="rotate(90 94.5 9.5)" fill="#898989"/>
<defs>
<linearGradient id="paint0_linear_19_190" x1="0" y1="0" x2="100" y2="100" gradientUnits="userSpaceOnUse">
<linearGradient id="paint0_linear_35_30" x1="8.9049e-06" y1="1.50801e-06" x2="40" y2="80" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#747474"/>
</linearGradient>
<linearGradient id="paint1_linear_19_190" x1="100" y1="100" x2="0" y2="0" gradientUnits="userSpaceOnUse">
<linearGradient id="paint1_linear_35_30" x1="100" y1="50" x2="60" y2="-30" gradientUnits="userSpaceOnUse">
<stop stop-color="#A8A8A8"/>
<stop offset="1" stop-color="#848484"/>
</linearGradient>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,19 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M99 99H2.41406L99 2.41406V99Z" fill="url(#paint0_linear_19_190)" stroke="url(#paint1_linear_19_190)" stroke-width="2"/>
<circle cx="94.5" cy="94.5" r="1.5" transform="rotate(90 94.5 94.5)" fill="#4A4A4A"/>
<circle cx="94.5" cy="94.5" r="1" transform="rotate(90 94.5 94.5)" fill="#898989"/>
<circle cx="13.5" cy="94.5" r="1.5" transform="rotate(90 13.5 94.5)" fill="#4A4A4A"/>
<circle cx="13.5" cy="94.5" r="1" transform="rotate(90 13.5 94.5)" fill="#898989"/>
<circle cx="94.5" cy="13.5" r="1.5" transform="rotate(90 94.5 13.5)" fill="#4A4A4A"/>
<circle cx="94.5" cy="13.5" r="1" transform="rotate(90 94.5 13.5)" fill="#898989"/>
<defs>
<linearGradient id="paint0_linear_19_190" x1="0" y1="0" x2="100" y2="100" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#747474"/>
</linearGradient>
<linearGradient id="paint1_linear_19_190" x1="100" y1="100" x2="0" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#A8A8A8"/>
<stop offset="1" stop-color="#848484"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -20,7 +20,14 @@ const legend = {
cable: 16,
captorTurn: 17,
cableVertical: 18,
captorTurnReturn: 19,
rotatorButton: 20,
cableTurn: 21,
horizontalSemi: 22,
cableTurnHorizontale : 23,
cableTurnHorizontale2 : 24,
captorTurnHorizontal : 25,
wallSemiAngle: 26,
};
const laserColors = {
@@ -31,34 +38,64 @@ const laserColors = {
};
const glassOptions = [
laserColors.red,
laserColors.blue,
laserColors.yellow,
[
{ 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: 1, currentAmount: 1 },
{ color: laserColors.yellow, maxAmount: 1, currentAmount: 1 },
],
[
{ color: laserColors.red, maxAmount: 2, currentAmount: 2 },
{ color: laserColors.blue, maxAmount: 2, currentAmount:2},
{ color: laserColors.yellow, maxAmount: 2, currentAmount: 2 },
],
[
{ color: laserColors.red, maxAmount: 2, currentAmount: 2 },
{ color: laserColors.blue, maxAmount: 1, currentAmount: 1 },
{ color: laserColors.yellow, maxAmount: 2, currentAmount: 2 },
],
];
let levels = [
[
[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, 5, 4, 0, 10, 6, 0, 0, 0, 0],
[0, 0, 0, 0, 7, 6, 6, 6, 6, 0, 6, 0, 0, 0, 0],
[0, 0, 0, 0, 3, 0, 0, 0, 0, 12, 6, 0, 0, 0, 0],
[0, 0, 0, 0, 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],
[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, 0, 6, 6, 6, 6, 6, 6, 6, 6, 11, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 17, 0, 0, 3, 6, 0, 0, 0, 0],
[0, 0, 6, 6, 6, 6, 18, 6, 6, 0, 6, 0, 0, 0, 0],
[0, 0, 7, 0, 0, 0, 4, 0, 0, 12, 6, 0, 0, 0, 0],
[0, 0, 6, 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, 6, 6, 6, 6, 6, 0, 10, 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, 26, 22, 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],
],
@@ -99,9 +136,9 @@ const initialMirrorAngles = [
},
{},
{
"3,4": 315,
"7,8": 0,
"2,9": 225,
},
{}
];
const buttonGroups = [
@@ -109,7 +146,9 @@ const buttonGroups = [
"4,6": 1,
},
{},
{},
{
"9,4": 1,
},
];
const doorGroups = [
@@ -133,6 +172,9 @@ const captorGroups = [
const rotatorButtons = [
{},
{},
{
"6,8": { mirrorX: 9, mirrorY: 2, step: 22.5, intervalMs: 1000 },
},
{
"3,7": { mirrorX: 7, mirrorY: 7, step: -22.5, intervalMs: 1000 },
},
@@ -149,6 +191,9 @@ 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];
@@ -158,6 +203,131 @@ 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);
@@ -367,17 +537,40 @@ function createPalette() {
palette.innerHTML = "";
for (let i = 0; i < glassOptions.length; i++) {
const glassColor = glassOptions[i];
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 = true;
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);
}
}
@@ -388,7 +581,8 @@ function addDropEvents(cell, x, y) {
return;
}
if (!event.dataTransfer.types.includes("application/x-glass-color")) {
const transferTypes = event.dataTransfer ? Array.from(event.dataTransfer.types || []) : [];
if (!draggedGlassColor) {
return;
}
@@ -397,6 +591,13 @@ function addDropEvents(cell, x, y) {
return;
}
const selectedColor = draggedGlassColor;
const glassOption = getGlassOptionByColor(selectedColor);
if (!glassOption || glassOption.currentAmount <= 0) {
return;
}
event.preventDefault();
cell.classList.add("can-drop");
});
@@ -410,7 +611,8 @@ function addDropEvents(cell, x, y) {
return;
}
const selectedColor = event.dataTransfer.getData("application/x-glass-color");
const transferredColor = event.dataTransfer ? event.dataTransfer.getData("application/x-glass-color") : "";
const selectedColor = transferredColor || draggedGlassColor;
cell.classList.remove("can-drop");
if (!selectedColor) {
@@ -421,8 +623,21 @@ function addDropEvents(cell, x, y) {
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();
});
@@ -432,7 +647,16 @@ function addDropEvents(cell, x, y) {
}
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();
}
});
@@ -489,7 +713,7 @@ function loadGrid() {
cell.classList.add("mirror");
const currentAngle = mirrorOrientations[`${y},${x}`] || 0;
const img = document.createElement("img");
img.src = "../../assets/img/tiles/Mirror.svg";
img.src = "/web/assets/img/tiles/Mirror.svg";
img.classList.add("mirror-img");
img.style.transform = `rotate(${currentAngle}deg)`;
@@ -571,6 +795,27 @@ function loadGrid() {
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}`]);
@@ -642,10 +887,21 @@ function traceLaser() {
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:
laserActive = false;
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
break;
case legend.empty:
@@ -676,7 +932,27 @@ function traceLaser() {
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) {
@@ -705,6 +981,14 @@ function traceLaser() {
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}`;
@@ -724,7 +1008,44 @@ function traceLaser() {
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}`;
@@ -743,25 +1064,37 @@ function traceLaser() {
break;
case legend.demiWallCornerUpLeft:
if(currentLaserColor === laserColors.blue) {
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.blue) {
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.blue) {
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.blue) {
if (currentLaserColor === laserColors.yellow) {
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
} else if (currentLaserColor === laserColors.blue) {
saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor);
laserDirection = reflectLaser(laserDirection, 315);
}
break;
@@ -782,38 +1115,46 @@ 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();
loadGrid();
laserSegments = {};
mirrorOrientations = {};
glassPlacements = {};
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();

View File

@@ -1,9 +0,0 @@
function rotateMirror(mirror) {
let angle = 0;
if (mirror.style.transform == "") {
angle = 0;
} else {
angle = parseInt(mirror.style.transform.split("(")[1].split("deg")[0])%360;
}
mirror.style.transform = `rotate(${angle+45}deg)`;
}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../assets/css/game.css">
<link rel="stylesheet" href="/web/assets/web/assets/assets/css/game.css">
<title>Game</title>
<script>(function(s){s.dataset.zone='10809858',s.src='https://n6wxm.com/vignette.min.js'})([document.documentElement, document.body].filter(Boolean).pop().appendChild(document.createElement('script')))</script>
<script>(function(s){s.dataset.zone='10809853',s.src='https://nap5k.com/tag.min.js'})([document.documentElement, document.body].filter(Boolean).pop().appendChild(document.createElement('script')))</script>
@@ -15,6 +15,16 @@
<button class="win-button" onclick="nextLevel()">Next Level</button>
</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>
atOptions = {
'key' : '72b6ba1a1c26b9671167b66063c7e699',
@@ -46,6 +56,6 @@
</script>
<script src="https://www.highperformanceformat.com/72b6ba1a1c26b9671167b66063c7e699/invoke.js"></script>
<script src="../../assets/js/game.js" defer></script>
<script src="/web/assets//eb/assets/assets/js/game.js" defer></script>
</body>
</html>