use timer based mechanic instead

This commit is contained in:
pobadoba
2026-05-10 17:13:54 +09:00
parent 9b68630764
commit c28a1d1f6a
6 changed files with 79 additions and 12 deletions

View File

@@ -78,6 +78,22 @@ canvas {
line-height: 1; line-height: 1;
color: #eef5ff; color: #eef5ff;
text-shadow: 0 0 12px rgba(121, 174, 242, 0.35); 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 { .canvas-hud-row {

View File

@@ -10,11 +10,11 @@
<body class="maze-page"> <body class="maze-page">
<main class="maze-layout"> <main class="maze-layout">
<section class="panel"> <section class="panel">
<div class="panel-label">Babylon Scene</div> <div class="panel-label">UNTITLED MAZE GAME</div>
<div class="canvas-stage"> <div class="canvas-stage">
<canvas id="renderCanvas"></canvas> <canvas id="renderCanvas"></canvas>
<div class="canvas-hud"> <div class="canvas-hud">
<div class="canvas-hud-label">Elapsed time</div> <div class="canvas-hud-label">Time left</div>
<div id="canvas-time" class="canvas-hud-value">0.0s</div> <div id="canvas-time" class="canvas-hud-value">0.0s</div>
<div class="canvas-hud-row"><span>Has key</span><strong id="canvas-key">no</strong></div> <div class="canvas-hud-row"><span>Has key</span><strong id="canvas-key">no</strong></div>
<div class="canvas-hud-row"><span>Rounds</span><strong id="canvas-rounds">0</strong></div> <div class="canvas-hud-row"><span>Rounds</span><strong id="canvas-rounds">0</strong></div>
@@ -66,7 +66,9 @@
<div class="status-display"> <div class="status-display">
<div class="status-line"><strong>Seed:</strong> <span id="status-seed">0</span></div> <div class="status-line"><strong>Seed:</strong> <span id="status-seed">0</span></div>
<div class="status-line"><strong>Level:</strong> <span id="status-level">1</span></div> <div class="status-line"><strong>Level:</strong> <span id="status-level">1</span></div>
<div class="status-line"><strong>Time:</strong> <span id="status-time">0.0</span></div> <div class="status-line"><strong>Maze:</strong> <span id="status-maze-size">11x11</span></div>
<div class="status-line"><strong>Chests:</strong> <span id="status-chests">2</span></div>
<div class="status-line"><strong>Time left:</strong> <span id="status-time">60.0</span></div>
<div class="status-line"><strong>Key:</strong> <span id="status-key">no</span></div> <div class="status-line"><strong>Key:</strong> <span id="status-key">no</span></div>
<div class="status-line"><strong>Rounds:</strong> <span id="status-rounds">0</span></div> <div class="status-line"><strong>Rounds:</strong> <span id="status-rounds">0</span></div>
<div class="status-message" id="status-message">Adjust settings, then start a run.</div> <div class="status-message" id="status-message">Adjust settings, then start a run.</div>

View File

@@ -57,6 +57,7 @@ let lastFootstepPosition = null;
let footstepAccumulator = 0; let footstepAccumulator = 0;
let footstepElapsed = 0; let footstepElapsed = 0;
let gameOverActive = false; let gameOverActive = false;
let lowTimeAlertPlayed = false;
scene.gravity = new BABYLON.Vector3(0, -0.2, 0); scene.gravity = new BABYLON.Vector3(0, -0.2, 0);
scene.collisionsEnabled = true; scene.collisionsEnabled = true;
@@ -95,7 +96,7 @@ function restartRunFromGameOver() {
sharedState.runtime.runActive = true; sharedState.runtime.runActive = true;
sharedState.runtime.hasKey = false; sharedState.runtime.hasKey = false;
sharedState.runtime.roundsCompleted = 0; sharedState.runtime.roundsCompleted = 0;
sharedState.runtime.elapsedSeconds = 0; sharedState.runtime.elapsedSeconds = ROUND_TIME_SECONDS;
sharedState.runtime.message = "Run restarted."; sharedState.runtime.message = "Run restarted.";
sharedState.config.level = 1; sharedState.config.level = 1;
hideGameOverScreen(); hideGameOverScreen();
@@ -179,6 +180,7 @@ let spawnGridPos = null; // track spawn grid position for validation
let spawnMarker = null; let spawnMarker = null;
let highlightedChest = null; let highlightedChest = null;
const cellSize = 2; const cellSize = 2;
const ROUND_TIME_SECONDS = 60;
function setChestHighlight(mesh) { function setChestHighlight(mesh) {
if (highlightedChest === mesh) { if (highlightedChest === mesh) {
@@ -391,15 +393,21 @@ function spawnCameraAt(grid) {
function generateLevel() { function generateLevel() {
hideGameOverScreen(); hideGameOverScreen();
lowTimeAlertPlayed = false;
if (canvasTime) {
canvasTime.classList.remove("low-time");
}
const cfg = sharedState.config; const cfg = sharedState.config;
const seed = cfg.seed; const seed = cfg.seed;
const w = Math.max(9, cfg.mazeWidth); const roundScale = Math.max(0, cfg.level - 1);
const h = Math.max(9, cfg.mazeHeight); 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); const grid = generateMazeGrid(w, h, seed + cfg.level);
updateOverviewCameraForMaze(w, h); updateOverviewCameraForMaze(w, h);
const dead = findDeadEnds(grid); const dead = findDeadEnds(grid);
buildLevelFromGrid(grid); buildLevelFromGrid(grid);
placeChestsOnDeadEnds(grid, dead, cfg.minChestDeadEnds, seed + cfg.level); placeChestsOnDeadEnds(grid, dead, chestCount, seed + cfg.level);
placeExit(grid, seed + cfg.level); placeExit(grid, seed + cfg.level);
spawnCameraAt(grid); spawnCameraAt(grid);
@@ -428,6 +436,7 @@ window.mazeGameApi = { generateLevel };
// Pointer interaction for chests // Pointer interaction for chests
scene.onPointerObservable.add((pi) => { scene.onPointerObservable.add((pi) => {
if (pi.type !== BABYLON.PointerEventTypes.POINTERDOWN) return; if (pi.type !== BABYLON.PointerEventTypes.POINTERDOWN) return;
if (!sharedState.runtime.runActive || gameOverActive) return;
const pick = scene.pick(scene.pointerX, scene.pointerY); const pick = scene.pick(scene.pointerX, scene.pointerY);
if (!pick || !pick.hit || !pick.pickedMesh) return; if (!pick || !pick.hit || !pick.pickedMesh) return;
const m = pick.pickedMesh; const m = pick.pickedMesh;
@@ -467,7 +476,31 @@ scene.registerBeforeRender(() => {
} }
if (sharedState.runtime.runActive) { 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) { if (sharedState.runtime.runActive && cameraMode === "fp" && camera && camera.position && document.pointerLockElement === canvas) {
const currentPosition = camera.position; const currentPosition = camera.position;
@@ -496,7 +529,7 @@ scene.registerBeforeRender(() => {
sharedState.config.level += 1; sharedState.config.level += 1;
sharedState.runtime.hasKey = false; sharedState.runtime.hasKey = false;
sharedState.runtime.roundsCompleted += 1; sharedState.runtime.roundsCompleted += 1;
sharedState.runtime.elapsedSeconds = 0; sharedState.runtime.elapsedSeconds = ROUND_TIME_SECONDS;
sharedState.runtime.message = `Level ${sharedState.config.level} starting.`; sharedState.runtime.message = `Level ${sharedState.config.level} starting.`;
generateLevel(); generateLevel();
} }

View File

@@ -10,7 +10,7 @@ export const sharedState = (window.mazeGameState ??= {
runActive: false, runActive: false,
hasKey: false, hasKey: false,
roundsCompleted: 0, roundsCompleted: 0,
elapsedSeconds: 0, elapsedSeconds: 60,
message: "Adjust settings, then start a run.", message: "Adjust settings, then start a run.",
}, },
}); });

View File

@@ -6,7 +6,7 @@ function resetRun(message) {
sharedState.runtime.runActive = true; sharedState.runtime.runActive = true;
sharedState.runtime.hasKey = false; sharedState.runtime.hasKey = false;
sharedState.runtime.roundsCompleted = 0; sharedState.runtime.roundsCompleted = 0;
sharedState.runtime.elapsedSeconds = 0; sharedState.runtime.elapsedSeconds = 60;
sharedState.runtime.message = message; sharedState.runtime.message = message;
sharedState.config.level = 1; sharedState.config.level = 1;
try { window.mazeGameApi.generateLevel(); } catch (e) { console.warn(e); } try { window.mazeGameApi.generateLevel(); } catch (e) { console.warn(e); }
@@ -15,7 +15,7 @@ function resetRun(message) {
function restartLevel(message) { function restartLevel(message) {
sharedState.runtime.hasKey = false; sharedState.runtime.hasKey = false;
sharedState.runtime.elapsedSeconds = 0; sharedState.runtime.elapsedSeconds = 60;
sharedState.runtime.message = message; sharedState.runtime.message = message;
try { window.mazeGameApi.generateLevel(); } catch (e) { console.warn(e); } try { window.mazeGameApi.generateLevel(); } catch (e) { console.warn(e); }
updateDisplay(); updateDisplay();
@@ -33,8 +33,16 @@ function updateDisplay() {
document.getElementById("value-height").textContent = sharedState.config.mazeHeight; document.getElementById("value-height").textContent = sharedState.config.mazeHeight;
document.getElementById("value-deadends").textContent = sharedState.config.minChestDeadEnds; 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-seed").textContent = sharedState.config.seed;
document.getElementById("status-level").textContent = sharedState.config.level; 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-time").textContent = sharedState.runtime.elapsedSeconds.toFixed(1);
document.getElementById("status-key").textContent = sharedState.runtime.hasKey ? "yes" : "no"; document.getElementById("status-key").textContent = sharedState.runtime.hasKey ? "yes" : "no";
document.getElementById("status-rounds").textContent = sharedState.runtime.roundsCompleted; 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 // Update status display on game loop
setInterval(() => { setInterval(() => {
if (sharedState.runtime.runActive) { 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-time").textContent = sharedState.runtime.elapsedSeconds.toFixed(1);
document.getElementById("status-key").textContent = sharedState.runtime.hasKey ? "yes" : "no"; document.getElementById("status-key").textContent = sharedState.runtime.hasKey ? "yes" : "no";
document.getElementById("status-rounds").textContent = sharedState.runtime.roundsCompleted; document.getElementById("status-rounds").textContent = sharedState.runtime.roundsCompleted;

View File

@@ -2,6 +2,7 @@ const soundFiles = {
chestClose: "/sfx/sfx_chest_close.wav", chestClose: "/sfx/sfx_chest_close.wav",
chestOpen: "/sfx/sfx_chest_open.wav", chestOpen: "/sfx/sfx_chest_open.wav",
click: "/sfx/sfx_click.wav", click: "/sfx/sfx_click.wav",
clock: "/sfx/sfx_clock.wav",
key: "/sfx/sfx_key.wav", key: "/sfx/sfx_key.wav",
lose: "/sfx/sfx_lose.wav", lose: "/sfx/sfx_lose.wav",
step: "/sfx/sfx_step.wav", step: "/sfx/sfx_step.wav",