From e90a1fc49719306d3d9d7df7388f88886aa07ee5 Mon Sep 17 00:00:00 2001 From: Sysy's Date: Tue, 31 Mar 2026 10:53:18 +0200 Subject: [PATCH] Add glass palette, colored lasers & UI layout Introduce tinted glass mechanic and colored lasers with drag-and-drop palette, plus UI layout and styling. CSS: new game-layout, toolbox, glass-palette, glass-item, cell-glass, laser color classes and many visual tweaks (user-select, drop outline, door/button states). JS: add laserColors, glassOptions, glassPlacements, palette creation, drag/drop/dblclick handlers, block browser drop, saveLaserSegment helper, colored laser tracing (red/blue/yellow/white) including mirror/door/button interactions, button/door grouping and initial mirror angles, and updates to loadGrid to render glass and colorized laser segments. HTML: move map into new main layout and add toolbox palette container. Overall enables placing colored glass to influence laser behavior and updates visuals/interaction accordingly. --- web/assets/css/game.css | 132 +++++++++++++++- web/assets/js/game.js | 298 ++++++++++++++++++++++++++++++++--- web/templates/view/game.html | 9 +- 3 files changed, 408 insertions(+), 31 deletions(-) diff --git a/web/assets/css/game.css b/web/assets/css/game.css index a668e5a..a6e9fd3 100644 --- a/web/assets/css/game.css +++ b/web/assets/css/game.css @@ -17,6 +17,7 @@ body { align-items: center; justify-content: center; font-family: 'Arial', sans-serif; + user-select: none; } main { @@ -28,6 +29,36 @@ main { border-radius: 10px; min-width: fit-content; flex-shrink: 0; + gap: 16px; +} + +.game-layout { + width: min(95vw, 1000px); +} + +.toolbox { + width: 100%; + background: #dfe5f8; + border-radius: 10px; + padding: 14px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.toolbox h2 { + font-size: 1rem; +} + +.toolbox p { + font-size: 0.9rem; + color: #334; +} + +.glass-palette { + display: flex; + gap: 10px; + flex-wrap: wrap; } /* ================================ @@ -63,6 +94,11 @@ main { margin: 0; position: relative; background-color: #2a2a2a; + user-select: none; +} + +.cell.can-drop { + outline: 2px dashed rgba(0, 0, 0, 0.2); } /* ================================ @@ -78,7 +114,12 @@ main { } .laser { - background-color: #FFD700; + background-color: #f5f5f5; + border: 2px solid #d8d8d8; +} + +.colored-laser { + background-color: #ffa726; } .mirror { @@ -92,6 +133,26 @@ main { background-color: #0729c0; } +.door { + background-color: #6d4c41; +} + +.door-open { + background-color: #bca89c; +} + +.button { + background-color: #7cb342; +} + +.button-2 { + background-color: #ff8f00; +} + +.button-active { + background-color: #c6ff00; +} + .target { background: #00FF00; } @@ -105,19 +166,39 @@ main { } .laser-horizontal { - background: linear-gradient(to bottom, transparent 0%, transparent 45%, red 45%, red 55%, transparent 55%, transparent 100%), #DADEEF + --laser-color: red; + background: linear-gradient(to bottom, transparent 0%, transparent 45%, var(--laser-color) 45%, var(--laser-color) 55%, transparent 55%, transparent 100%), #DADEEF } .laser-vertical { - background: linear-gradient(to right, transparent 0%, transparent 45%, red 45%, red 55%, transparent 55%, transparent 100%), #DADEEF; + --laser-color: red; + background: linear-gradient(to right, transparent 0%, transparent 45%, var(--laser-color) 45%, var(--laser-color) 55%, transparent 55%, transparent 100%), #DADEEF; } .laser-diagonal-down { - background: linear-gradient(45deg, transparent 0%, transparent 46%, red 46%, red 54%, transparent 54%, transparent 100%), #DADEEF; + --laser-color: red; + background: linear-gradient(45deg, transparent 0%, transparent 46%, var(--laser-color) 46%, var(--laser-color) 54%, transparent 54%, transparent 100%), #DADEEF; } .laser-diagonal-up { - background: linear-gradient(135deg, transparent 0%, transparent 46%, red 46%, red 54%, transparent 54%, transparent 100%), #DADEEF; + --laser-color: red; + background: linear-gradient(135deg, transparent 0%, transparent 46%, var(--laser-color) 46%, var(--laser-color) 54%, transparent 54%, transparent 100%), #DADEEF; +} + +.laser-color-white { + --laser-color: #f8f8f8; +} + +.laser-color-red { + --laser-color: #ff3b30; +} + +.laser-color-blue { + --laser-color: #2d7ff9; +} + +.laser-color-yellow { + --laser-color: #ffd400; } /* ================================ @@ -143,6 +224,47 @@ main { transform-origin: center; } +.glass-item { + width: 54px; + height: 54px; + border: none; + border-radius: 10px; + cursor: grab; + position: relative; + box-shadow: inset 0 0 0 2px rgba(0, 0, 0, 0.1); + user-select: none; +} + +.glass-item::after, +.cell-glass::after { + content: ''; + position: absolute; + inset: 10px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.45); + border: 1px solid rgba(255, 255, 255, 0.8); +} + +.glass-red { + background: rgba(255, 59, 48, 0.85); +} + +.glass-blue { + background: rgba(45, 127, 249, 0.85); +} + +.glass-yellow { + background: rgba(255, 212, 0, 0.9); +} + +.cell-glass { + position: absolute; + inset: 5px; + border-radius: 8px; + opacity: 0.9; + pointer-events: none; +} + /* ================================ RESPONSIVE ================================ */ diff --git a/web/assets/js/game.js b/web/assets/js/game.js index 90d526e..5831b0b 100644 --- a/web/assets/js/game.js +++ b/web/assets/js/game.js @@ -11,27 +11,60 @@ const legend = { demiWall: 7, target: 8, ligthLaser: 9, + button2: 10, } +const laserColors = { + white: "white", + red: "red", + blue: "blue", + yellow: "yellow", +}; + +const glassOptions = [ + laserColors.red, + laserColors.blue, + laserColors.yellow, +]; + // Grid test let level1 = [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 3, 0, 0, 0, 8, 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, 3, 4, 0, 0, 3, 0, 7, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 5, 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, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 3, 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], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0], + [1, 0, 0, 0, 3, 5, 3, 0, 8, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ] -// Function to save initial orientation of mirrors +const initialMirrorAngles = { + "8,4": 315, + "2,4": 45, + "2,8": 315, + "6,8": 45, +}; + +const buttonGroups = { + "4,4": 1, + "8,5": 2, +}; + +const doorGroups = { + "2,5": 1, + "2,6": 2, +}; + let laserDirection = { dx: 0, dy: 0 }; let laserSegments = {}; let mirrorOrientations = {}; +let glassPlacements = {}; +let activatedButtons = {}; +let openedDoors = {}; function normalizeLaserDirection(dx, dy) { const epsilon = 0.0001; @@ -72,12 +105,166 @@ function getLaserSegmentClass(segmentDirection) { return "laser-diagonal-up"; } +function getLaserColorClass(color) { + return `laser-color-${color || laserColors.white}`; +} + +function reverseLaser(direction) { + return { + dx: direction.dx * -1, + dy: direction.dy * -1, + }; +} + +function getButtonGroup(x, y) { + const cellType = level1[y][x]; + + if (cellType === legend.button2) { + return 2; + } + + return buttonGroups[`${y},${x}`] || 1; +} + +function getDoorGroup(x, y) { + return doorGroups[`${y},${x}`] || 1; +} + +function openDoorsFromButton(x, y) { + const buttonGroup = getButtonGroup(x, y); + + for (let doorY = 0; doorY < level1.length; doorY++) { + for (let doorX = 0; doorX < level1[doorY].length; doorX++) { + if (level1[doorY][doorX] === legend.door && getDoorGroup(doorX, doorY) === buttonGroup) { + openedDoors[`${doorY},${doorX}`] = true; + } + } + } +} + +function saveLaserSegment(x, y, direction, color) { + laserSegments[`${y},${x}`] = { + direction: { ...direction }, + color: color, + }; + + if (level1[y][x] === legend.empty) { + level1[y][x] = legend.ligthLaser; + } +} + +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 createPalette() { + const palette = document.getElementById("glass-palette"); + + if (!palette) { + return; + } + + palette.innerHTML = ""; + + for (let i = 0; i < glassOptions.length; i++) { + const glassColor = glassOptions[i]; + const glassButton = document.createElement("button"); + glassButton.type = "button"; + glassButton.classList.add("glass-item", `glass-${glassColor}`); + glassButton.draggable = true; + glassButton.addEventListener("dragstart", (event) => { + event.dataTransfer.effectAllowed = "copy"; + event.dataTransfer.setData("text/plain", glassColor); + event.dataTransfer.setData("application/x-glass-color", glassColor); + }); + + palette.appendChild(glassButton); + } +} + +function addDropEvents(cell, x, y) { + cell.addEventListener("dragover", (event) => { + if (isLevelFinished) { + return; + } + + if (!event.dataTransfer.types.includes("application/x-glass-color")) { + return; + } + + if (level1[y][x] !== legend.empty && level1[y][x] !== legend.ligthLaser) { + return; + } + + event.preventDefault(); + cell.classList.add("can-drop"); + }); + + cell.addEventListener("dragleave", () => { + cell.classList.remove("can-drop"); + }); + + cell.addEventListener("drop", (event) => { + if (isLevelFinished) { + return; + } + + const selectedColor = event.dataTransfer.getData("application/x-glass-color"); + cell.classList.remove("can-drop"); + + if (!selectedColor) { + return; + } + + if (level1[y][x] !== legend.empty && level1[y][x] !== legend.ligthLaser) { + return; + } + + event.preventDefault(); + glassPlacements[`${y},${x}`] = selectedColor; + traceLaser(); + }); + + cell.addEventListener("dblclick", () => { + if (isLevelFinished) { + return; + } + + if (isGlassOnCell(x, y)) { + delete glassPlacements[`${y},${x}`]; + traceLaser(); + } + }); +} + +function blockBrowserDrop() { + document.addEventListener("dragover", (event) => { + event.preventDefault(); + }); + + document.addEventListener("drop", (event) => { + event.preventDefault(); + }); +} + function initializeMirrorOrientations() { mirrorOrientations = {}; // Reset for (let y = 0; y < level1.length; y++) { for (let x = 0; x < level1[y].length; x++) { if (level1[y][x] === legend.mirror) { - mirrorOrientations[`${y},${x}`] = 0; // Default angle + mirrorOrientations[`${y},${x}`] = initialMirrorAngles[`${y},${x}`] || 0; } } } @@ -98,6 +285,7 @@ function loadGrid() { for (let x = 0; x < level1[y].length; x++) { const cell = document.createElement("div"); cell.classList.add("cell"); + addDropEvents(cell, x, y); switch (level1[y][x]) { case legend.empty: @@ -130,9 +318,21 @@ function loadGrid() { break; case legend.door: cell.classList.add("door"); + if (openedDoors[`${y},${x}`]) { + cell.classList.add("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.wall: cell.classList.add("wall"); @@ -144,12 +344,20 @@ function loadGrid() { cell.classList.add("target"); break; case legend.ligthLaser: - cell.classList.add("light-laser"); - const segmentDirection = laserSegments[`${y},${x}`]; - cell.classList.add(getLaserSegmentClass(segmentDirection)); + cell.classList.add("empty"); break; } + const segmentData = laserSegments[`${y},${x}`]; + if (segmentData) { + const segmentDirection = segmentData.direction; + cell.classList.add("light-laser"); + cell.classList.add(getLaserSegmentClass(segmentDirection)); + cell.classList.add(getLaserColorClass(segmentData.color)); + } + + drawGlassInCell(cell, x, y); + lign.appendChild(cell); } @@ -191,6 +399,8 @@ let isLevelFinished = false; function traceLaser() { // Reset light laser from previous trace laserSegments = {}; + activatedButtons = {}; + openedDoors = {}; for (let y = 0; y < level1.length; y++) { for (let x = 0; x < level1[y].length; x++) { if (level1[y][x] === legend.ligthLaser) { @@ -223,8 +433,10 @@ function traceLaser() { let currentX = startLaserX; let currentY = startLaserY; let laserActive = true; + let currentLaserColor = laserColors.white; const maxIterations = 1000; // Prevent infinite loops let iterations = 0; + isLevelFinished = false; while (laserActive && iterations < maxIterations) { iterations++; @@ -239,6 +451,11 @@ function traceLaser() { } const cellType = level1[currentY][currentX]; + const glassColor = glassPlacements[`${currentY},${currentX}`]; + + if (glassColor) { + currentLaserColor = glassColor; + } switch (cellType) { case legend.laser: @@ -247,43 +464,73 @@ function traceLaser() { break; case legend.empty: - level1[currentY][currentX] = legend.ligthLaser; - laserSegments[`${currentY},${currentX}`] = { ...laserDirection }; + saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor); break; case legend.target: - level1[currentY][currentX] = legend.ligthLaser; - laserSegments[`${currentY},${currentX}`] = { ...laserDirection }; + saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor); laserActive = false; isLevelFinished = true; break; case legend.mirror: - const mirrorAngle = mirrorOrientations[`${currentY},${currentX}`] || 0; - laserDirection = reflectLaser(laserDirection, mirrorAngle); + if (currentLaserColor === laserColors.yellow) { + saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor); + } else { + const mirrorAngle = mirrorOrientations[`${currentY},${currentX}`] || 0; + laserDirection = reflectLaser(laserDirection, mirrorAngle); + } break; case legend.wall: - laserActive = false; + if (currentLaserColor === laserColors.blue) { + laserDirection = reverseLaser(laserDirection); + } else if (currentLaserColor === laserColors.yellow) { + saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor); + } else { + laserActive = false; + } break; case legend.demiWall: - laserActive = false; + if (currentLaserColor === laserColors.blue) { + laserDirection = reverseLaser(laserDirection); + } else if (currentLaserColor === laserColors.yellow) { + saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor); + } else { + laserActive = false; + } break; case legend.door: - laserActive = false; + if (openedDoors[`${currentY},${currentX}`]) { + 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: - level1[currentY][currentX] = legend.ligthLaser; - laserSegments[`${currentY},${currentX}`] = { ...laserDirection }; - laserActive = false; + 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; default: - level1[currentY][currentX] = legend.ligthLaser; - laserSegments[`${currentY},${currentX}`] = { ...laserDirection }; + saveLaserSegment(currentX, currentY, laserDirection, currentLaserColor); break; } } @@ -295,6 +542,9 @@ function traceLaser() { } } +createPalette(); +initializeMirrorOrientations(); +blockBrowserDrop(); traceLaser(); // If level finishh -> call this function @@ -303,5 +553,3 @@ function finish() { alert("Réussi !"); }, 100); } - - diff --git a/web/templates/view/game.html b/web/templates/view/game.html index 472bb98..970a953 100644 --- a/web/templates/view/game.html +++ b/web/templates/view/game.html @@ -9,7 +9,6 @@ -
+
+
+ +
+

Vitres tintées

+
+
+