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
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",