wip: vibrate danger

This commit is contained in:
Krzysztof Rudnicki 2025-08-02 13:01:45 +02:00
parent 13123234da
commit 89b689e2f0
5 changed files with 140 additions and 9 deletions

View File

@ -15,6 +15,7 @@
<p class="visual-warning">⚠️ Red squares are deadly - avoid them!</p>
<p class="audio-warning">🔊 Hidden dangers end the game - listen for directions!</p>
<p class="audio-hint">🗣️ Speech directions: "left", "right", "up", "down"</p>
<p class="probe-hint">👁️ Hold Shift + direction to detect invisible dangers!</p>
</div>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="controls">

View File

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

View File

@ -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,11 +50,18 @@ export class InputHandler {
direction = { x: 1, y: 0 };
}
if (direction && this.onMove) {
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;
}
}
}
// Method to handle continuous key holding
update() {
@ -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);

View File

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

View File

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