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;
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 {

View File

@@ -10,11 +10,11 @@
<body class="maze-page">
<main class="maze-layout">
<section class="panel">
<div class="panel-label">Babylon Scene</div>
<div class="panel-label">UNTITLED MAZE GAME</div>
<div class="canvas-stage">
<canvas id="renderCanvas"></canvas>
<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 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>
@@ -66,7 +66,9 @@
<div class="status-display">
<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>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>Rounds:</strong> <span id="status-rounds">0</span></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 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();
}

View File

@@ -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.",
},
});

View File

@@ -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;

View File

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