diff --git a/css/style.css b/css/style.css index a677bf5..f2663fa 100644 --- a/css/style.css +++ b/css/style.css @@ -78,6 +78,22 @@ canvas { line-height: 1; color: #eef5ff; text-shadow: 0 0 12px rgba(121, 174, 242, 0.35); + transition: all 0.2s ease; +} + +.canvas-hud-value.low-time { + color: #ff4444; + text-shadow: 0 0 16px rgba(255, 68, 68, 0.6); + animation: pulse-warning 0.6s infinite; +} + +@keyframes pulse-warning { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.7; + } } .canvas-hud-row { diff --git a/index.html b/index.html index 2b0fd5b..a125440 100644 --- a/index.html +++ b/index.html @@ -10,11 +10,11 @@
-
Babylon Scene
+
UNTITLED MAZE GAME
-
Elapsed time
+
Time left
0.0s
Has keyno
Rounds0
@@ -66,7 +66,9 @@
Seed: 0
Level: 1
-
Time: 0.0
+
Maze: 11x11
+
Chests: 2
+
Time left: 60.0
Key: no
Rounds: 0
Adjust settings, then start a run.
diff --git a/src/babylon_setup.js b/src/babylon_setup.js index b832441..79f79fd 100644 --- a/src/babylon_setup.js +++ b/src/babylon_setup.js @@ -57,6 +57,7 @@ let lastFootstepPosition = null; let footstepAccumulator = 0; let footstepElapsed = 0; let gameOverActive = false; +let lowTimeAlertPlayed = false; scene.gravity = new BABYLON.Vector3(0, -0.2, 0); scene.collisionsEnabled = true; @@ -95,7 +96,7 @@ function restartRunFromGameOver() { sharedState.runtime.runActive = true; sharedState.runtime.hasKey = false; sharedState.runtime.roundsCompleted = 0; - sharedState.runtime.elapsedSeconds = 0; + sharedState.runtime.elapsedSeconds = ROUND_TIME_SECONDS; sharedState.runtime.message = "Run restarted."; sharedState.config.level = 1; hideGameOverScreen(); @@ -179,6 +180,7 @@ let spawnGridPos = null; // track spawn grid position for validation let spawnMarker = null; let highlightedChest = null; const cellSize = 2; +const ROUND_TIME_SECONDS = 60; function setChestHighlight(mesh) { if (highlightedChest === mesh) { @@ -391,15 +393,21 @@ function spawnCameraAt(grid) { function generateLevel() { hideGameOverScreen(); + lowTimeAlertPlayed = false; + if (canvasTime) { + canvasTime.classList.remove("low-time"); + } const cfg = sharedState.config; const seed = cfg.seed; - const w = Math.max(9, cfg.mazeWidth); - const h = Math.max(9, cfg.mazeHeight); + const roundScale = Math.max(0, cfg.level - 1); + const w = Math.max(9, cfg.mazeWidth + roundScale * 2); + const h = Math.max(9, cfg.mazeHeight + roundScale * 2); + const chestCount = Math.max(1, cfg.minChestDeadEnds + roundScale); const grid = generateMazeGrid(w, h, seed + cfg.level); updateOverviewCameraForMaze(w, h); const dead = findDeadEnds(grid); buildLevelFromGrid(grid); - placeChestsOnDeadEnds(grid, dead, cfg.minChestDeadEnds, seed + cfg.level); + placeChestsOnDeadEnds(grid, dead, chestCount, seed + cfg.level); placeExit(grid, seed + cfg.level); spawnCameraAt(grid); @@ -428,6 +436,7 @@ window.mazeGameApi = { generateLevel }; // Pointer interaction for chests scene.onPointerObservable.add((pi) => { if (pi.type !== BABYLON.PointerEventTypes.POINTERDOWN) return; + if (!sharedState.runtime.runActive || gameOverActive) return; const pick = scene.pick(scene.pointerX, scene.pointerY); if (!pick || !pick.hit || !pick.pickedMesh) return; const m = pick.pickedMesh; @@ -467,7 +476,31 @@ scene.registerBeforeRender(() => { } if (sharedState.runtime.runActive) { - sharedState.runtime.elapsedSeconds += engine.getDeltaTime() / 1000; + const dt = engine.getDeltaTime() / 1000; + sharedState.runtime.elapsedSeconds = Math.max(0, sharedState.runtime.elapsedSeconds - dt); + + const isLowTime = sharedState.runtime.elapsedSeconds < 10; + if (isLowTime && !lowTimeAlertPlayed) { + lowTimeAlertPlayed = true; + playSfx("clock", 0.75); + if (canvasTime) { + canvasTime.classList.add("low-time"); + } + } + if (!isLowTime && lowTimeAlertPlayed) { + lowTimeAlertPlayed = false; + if (canvasTime) { + canvasTime.classList.remove("low-time"); + } + } + + if (sharedState.runtime.elapsedSeconds <= 0) { + sharedState.runtime.runActive = false; + sharedState.runtime.message = "Time up — game over."; + playSfx("lose", 0.85); + showGameOverScreen(); + return; + } } if (sharedState.runtime.runActive && cameraMode === "fp" && camera && camera.position && document.pointerLockElement === canvas) { const currentPosition = camera.position; @@ -496,7 +529,7 @@ scene.registerBeforeRender(() => { sharedState.config.level += 1; sharedState.runtime.hasKey = false; sharedState.runtime.roundsCompleted += 1; - sharedState.runtime.elapsedSeconds = 0; + sharedState.runtime.elapsedSeconds = ROUND_TIME_SECONDS; sharedState.runtime.message = `Level ${sharedState.config.level} starting.`; generateLevel(); } diff --git a/src/game/state.js b/src/game/state.js index 3bee3ed..0899680 100644 --- a/src/game/state.js +++ b/src/game/state.js @@ -10,7 +10,7 @@ export const sharedState = (window.mazeGameState ??= { runActive: false, hasKey: false, roundsCompleted: 0, - elapsedSeconds: 0, + elapsedSeconds: 60, message: "Adjust settings, then start a run.", }, }); diff --git a/src/html_panel.js b/src/html_panel.js index 9b27135..729ce5e 100644 --- a/src/html_panel.js +++ b/src/html_panel.js @@ -6,7 +6,7 @@ function resetRun(message) { sharedState.runtime.runActive = true; sharedState.runtime.hasKey = false; sharedState.runtime.roundsCompleted = 0; - sharedState.runtime.elapsedSeconds = 0; + sharedState.runtime.elapsedSeconds = 60; sharedState.runtime.message = message; sharedState.config.level = 1; try { window.mazeGameApi.generateLevel(); } catch (e) { console.warn(e); } @@ -15,7 +15,7 @@ function resetRun(message) { function restartLevel(message) { sharedState.runtime.hasKey = false; - sharedState.runtime.elapsedSeconds = 0; + sharedState.runtime.elapsedSeconds = 60; sharedState.runtime.message = message; try { window.mazeGameApi.generateLevel(); } catch (e) { console.warn(e); } updateDisplay(); @@ -33,8 +33,16 @@ function updateDisplay() { document.getElementById("value-height").textContent = sharedState.config.mazeHeight; document.getElementById("value-deadends").textContent = sharedState.config.minChestDeadEnds; + // Calculate effective maze size and chest count based on current level + const roundScale = Math.max(0, sharedState.config.level - 1); + const effectiveWidth = Math.max(9, sharedState.config.mazeWidth + roundScale * 2); + const effectiveHeight = Math.max(9, sharedState.config.mazeHeight + roundScale * 2); + const effectiveChests = Math.max(1, sharedState.config.minChestDeadEnds + roundScale); + document.getElementById("status-seed").textContent = sharedState.config.seed; document.getElementById("status-level").textContent = sharedState.config.level; + document.getElementById("status-maze-size").textContent = `${effectiveWidth}x${effectiveHeight}`; + document.getElementById("status-chests").textContent = effectiveChests; document.getElementById("status-time").textContent = sharedState.runtime.elapsedSeconds.toFixed(1); document.getElementById("status-key").textContent = sharedState.runtime.hasKey ? "yes" : "no"; document.getElementById("status-rounds").textContent = sharedState.runtime.roundsCompleted; @@ -84,6 +92,13 @@ document.getElementById("slider-deadends").addEventListener("input", (e) => { // Update status display on game loop setInterval(() => { if (sharedState.runtime.runActive) { + const roundScale = Math.max(0, sharedState.config.level - 1); + const effectiveWidth = Math.max(9, sharedState.config.mazeWidth + roundScale * 2); + const effectiveHeight = Math.max(9, sharedState.config.mazeHeight + roundScale * 2); + const effectiveChests = Math.max(1, sharedState.config.minChestDeadEnds + roundScale); + + document.getElementById("status-maze-size").textContent = `${effectiveWidth}x${effectiveHeight}`; + document.getElementById("status-chests").textContent = effectiveChests; document.getElementById("status-time").textContent = sharedState.runtime.elapsedSeconds.toFixed(1); document.getElementById("status-key").textContent = sharedState.runtime.hasKey ? "yes" : "no"; document.getElementById("status-rounds").textContent = sharedState.runtime.roundsCompleted; diff --git a/src/sfx.js b/src/sfx.js index 7fc9c18..77e84b0 100644 --- a/src/sfx.js +++ b/src/sfx.js @@ -2,6 +2,7 @@ const soundFiles = { chestClose: "/sfx/sfx_chest_close.wav", chestOpen: "/sfx/sfx_chest_open.wav", click: "/sfx/sfx_click.wav", + clock: "/sfx/sfx_clock.wav", key: "/sfx/sfx_key.wav", lose: "/sfx/sfx_lose.wav", step: "/sfx/sfx_step.wav",