diff --git a/index.html b/index.html
index 29b4d90..ad30756 100644
--- a/index.html
+++ b/index.html
@@ -15,6 +15,7 @@
⚠️ Red squares are deadly - avoid them!
🔊 Hidden dangers end the game - listen for directions!
🗣️ Speech directions: "left", "right", "up", "down"
+ 👁️ Hold Shift + direction to detect invisible dangers!
diff --git a/src/game.js b/src/game.js
index 232da01..f54bf86 100644
--- a/src/game.js
+++ b/src/game.js
@@ -137,6 +137,10 @@ export class Game {
this.inputHandler.onMove = (direction) => {
this.movePlayer(direction);
};
+
+ this.inputHandler.onProbe = (direction) => {
+ this.probeDirection(direction);
+ };
}
movePlayer(direction) {
@@ -160,6 +164,11 @@ export class Game {
this.handleAudioDanger();
}
+ // Check if player stepped on a hidden danger square
+ if (this.maze.isDangerousHidden(newX, newY)) {
+ this.handleHiddenDanger();
+ }
+
// Check if player reached the exit
if (this.maze.isExit(newX, newY)) {
this.handleWin();
@@ -167,6 +176,21 @@ export class Game {
}
}
+ probeDirection(direction) {
+ const probeX = this.player.x + direction.x;
+ const probeY = this.player.y + direction.y;
+
+ // Check if probe position is within bounds
+ if (probeX < 0 || probeX >= this.maze.cols || probeY < 0 || probeY >= this.maze.rows) {
+ return;
+ }
+
+ // Check if the probed square has a hidden danger
+ if (this.maze.isDangerousHidden(probeX, probeY)) {
+ this.handleHiddenDangerDetected();
+ }
+ }
+
trackMove(x, y) {
// Track player move with timestamp
const timestamp = Date.now();
@@ -188,6 +212,25 @@ export class Game {
this.gameOver();
}
+ handleHiddenDanger() {
+ // Hidden danger squares end the game
+ this.gameOver();
+ }
+
+ handleHiddenDangerDetected() {
+ // Player detected a hidden danger with Shift+direction
+ console.log('Hidden danger detected!');
+
+ // Try vibration first
+ if (this.vibrationEnabled && navigator.vibrate) {
+ navigator.vibrate([200, 100, 200]); // Danger detection pattern
+ } else {
+ // Fallback to caps lock toggle
+ this.inputHandler.toggleCapsLock();
+ console.log('Caps Lock toggled as vibration fallback');
+ }
+ }
+
gameOver() {
if (this.soundEnabled) {
this.audioSystem.playGameOverSound();
diff --git a/src/input.js b/src/input.js
index 9351b02..a39b8a5 100644
--- a/src/input.js
+++ b/src/input.js
@@ -2,6 +2,7 @@ export class InputHandler {
constructor() {
this.keys = new Set();
this.onMove = null; // Callback for movement
+ this.onProbe = null; // Callback for probing with Shift+direction
this.moveDelay = 150; // Milliseconds between moves
this.lastMoveTime = 0;
@@ -36,6 +37,7 @@ export class InputHandler {
}
let direction = null;
+ const isShiftHeld = this.keys.has('ShiftLeft') || this.keys.has('ShiftRight');
// Check for movement keys (WASD or Arrow keys)
if (this.keys.has('KeyW') || this.keys.has('ArrowUp')) {
@@ -48,9 +50,16 @@ export class InputHandler {
direction = { x: 1, y: 0 };
}
- if (direction && this.onMove) {
- this.onMove(direction);
- this.lastMoveTime = now;
+ if (direction) {
+ if (isShiftHeld && this.onProbe) {
+ // Shift is held, this is a probe action
+ this.onProbe(direction);
+ this.lastMoveTime = now;
+ } else if (!isShiftHeld && this.onMove) {
+ // Normal movement
+ this.onMove(direction);
+ this.lastMoveTime = now;
+ }
}
}
@@ -68,9 +77,33 @@ export class InputHandler {
down: this.keys.has('KeyS') || this.keys.has('ArrowDown'),
left: this.keys.has('KeyA') || this.keys.has('ArrowLeft'),
right: this.keys.has('KeyD') || this.keys.has('ArrowRight'),
+ shift: this.keys.has('ShiftLeft') || this.keys.has('ShiftRight'),
};
}
+ // Toggle Caps Lock as fallback for vibration
+ toggleCapsLock() {
+ // Create a temporary input to simulate caps lock toggle
+ const temp = document.createElement('input');
+ temp.style.position = 'absolute';
+ temp.style.left = '-9999px';
+ document.body.appendChild(temp);
+ temp.focus();
+
+ // Simulate caps lock key press
+ const event = new KeyboardEvent('keydown', {
+ key: 'CapsLock',
+ code: 'CapsLock',
+ keyCode: 20,
+ which: 20
+ });
+ temp.dispatchEvent(event);
+
+ setTimeout(() => {
+ document.body.removeChild(temp);
+ }, 100);
+ }
+
// Clean up event listeners
destroy() {
document.removeEventListener('keydown', this.handleKeyDown);
diff --git a/src/maze.js b/src/maze.js
index f19ec2b..0e7f8cf 100644
--- a/src/maze.js
+++ b/src/maze.js
@@ -12,7 +12,8 @@ export class Maze {
this.SAFE = 1;
this.DANGEROUS_VISUAL = 2; // Visually dangerous, no audio
this.DANGEROUS_AUDIO = 3; // Looks safe, has audio when near
- this.EXIT = 4;
+ this.DANGEROUS_HIDDEN = 4; // Invisible, only detectable with Shift+direction
+ this.EXIT = 5;
}
generate() {
@@ -89,16 +90,18 @@ export class Maze {
}
}
- // Calculate target number of dangerous squares (30% of inner safe squares)
- const totalDangerousCount = Math.floor(innerSafeSquares.length * 0.3);
- const visualDangerousCount = Math.floor(totalDangerousCount * 0.5);
- const audioDangerousCount = totalDangerousCount - visualDangerousCount;
+ // Calculate target number of dangerous squares (40% of inner safe squares)
+ const totalDangerousCount = Math.floor(innerSafeSquares.length * 0.4);
+ const visualDangerousCount = Math.floor(totalDangerousCount * 0.33);
+ const audioDangerousCount = Math.floor(totalDangerousCount * 0.33);
+ const hiddenDangerousCount = totalDangerousCount - visualDangerousCount - audioDangerousCount;
this.shuffle(innerSafeSquares);
let addedDangerous = 0;
let addedVisual = 0;
let addedAudio = 0;
+ let addedHidden = 0;
// Add dangerous squares one by one, checking path after each addition
for (let i = 0; i < innerSafeSquares.length && addedDangerous < totalDangerousCount; i++) {
@@ -110,6 +113,8 @@ export class Maze {
dangerType = this.DANGEROUS_VISUAL;
} else if (addedAudio < audioDangerousCount) {
dangerType = this.DANGEROUS_AUDIO;
+ } else if (addedHidden < hiddenDangerousCount) {
+ dangerType = this.DANGEROUS_HIDDEN;
} else {
break; // We've added enough dangerous squares
}
@@ -125,8 +130,10 @@ export class Maze {
addedDangerous++;
if (dangerType === this.DANGEROUS_VISUAL) {
addedVisual++;
- } else {
+ } else if (dangerType === this.DANGEROUS_AUDIO) {
addedAudio++;
+ } else {
+ addedHidden++;
}
} else {
// Path blocked, revert this square to safe
@@ -220,6 +227,10 @@ export class Maze {
return this.grid[y][x] === this.DANGEROUS_AUDIO;
}
+ isDangerousHidden(x, y) {
+ return this.grid[y][x] === this.DANGEROUS_HIDDEN;
+ }
+
isNearAudioDanger(playerX, playerY, range = 1) {
// Check if player is within range of any audio danger squares
for (let dy = -range; dy <= range; dy++) {
@@ -335,6 +346,41 @@ export class Maze {
}
break;
+ case this.DANGEROUS_HIDDEN:
+ if (debugMode) {
+ // In debug mode, show hidden danger squares with a purple overlay
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
+
+ // Add purple debug overlay with pulsing effect
+ const hiddenTime = Date.now() * 0.004;
+ const hiddenPulse = (Math.sin(hiddenTime + x + y) + 1) * 0.5;
+ const hiddenAlpha = 0.3 + hiddenPulse * 0.4;
+ ctx.fillStyle = `rgba(128, 0, 128, ${hiddenAlpha})`;
+ ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
+
+ // Add debug border
+ ctx.strokeStyle = '#800080';
+ ctx.lineWidth = 2;
+ ctx.strokeRect(pixelX, pixelY, cellSize, cellSize);
+
+ // Add hidden symbol
+ ctx.fillStyle = '#ffffff';
+ ctx.font = `${cellSize * 0.4}px Arial`;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText('👁️', pixelX + cellSize / 2, pixelY + cellSize / 2);
+ } else {
+ // In normal mode, hidden danger squares look identical to safe squares
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(pixelX, pixelY, cellSize, cellSize);
+ // Add subtle grid lines
+ ctx.strokeStyle = '#e0e0e0';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(pixelX, pixelY, cellSize, cellSize);
+ }
+ break;
+
case this.DANGEROUS_VISUAL:
// Visual-only danger squares - animated red appearance
const time = Date.now() * 0.003;
diff --git a/src/style.css b/src/style.css
index 4ee8228..2096ad3 100644
--- a/src/style.css
+++ b/src/style.css
@@ -80,6 +80,14 @@ h1 {
opacity: 0.8;
}
+.probe-hint {
+ color: #dda0dd !important;
+ font-size: 0.9em;
+ font-style: italic;
+ opacity: 0.8;
+ font-weight: bold;
+}
+
@keyframes pulse {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }