This commit is contained in:
Sebastian Szczepanski 2025-08-01 17:24:25 +02:00
parent 684646e53e
commit 4532e4a66c
4 changed files with 214 additions and 54 deletions

View File

@ -13,12 +13,13 @@
<div class="game-info">
<p>Use WASD or Arrow Keys to move</p>
<p class="visual-warning">⚠️ Red squares are deadly - avoid them!</p>
<p class="audio-warning">🔊 Hidden dangers end the game - listen carefully!</p>
<p class="audio-hint">🎧 Use headphones for best directional audio experience</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>
</div>
<canvas id="gameCanvas" width="800" height="600"></canvas>
<div class="controls">
<button id="soundToggle">🔊 Sound: ON</button>
<button id="speechToggle">🗣️ Speech: ON</button>
<button id="vibrationToggle">📳 Vibration: ON</button>
</div>
</div>

View File

@ -4,6 +4,8 @@ export class AudioSystem {
this.gainNode = null;
this.activeSounds = new Set();
this.initialized = false;
this.lastSpokenDirection = '';
this.speechEnabled = true;
this.initializeAudio();
}
@ -127,39 +129,68 @@ export class AudioSystem {
}
async playDirectionalProximitySound(directions) {
await this.ensureAudioContext();
// Calculate the dominant direction for panning
let panValue = 0;
let pitchModifier = 0;
// Horizontal panning (left/right)
if (directions.left && !directions.right) {
panValue = -0.7; // Pan left
} else if (directions.right && !directions.left) {
panValue = 0.7; // Pan right
// Use text-to-speech instead of sounds
this.speakDirections(directions);
}
speakDirections(directions) {
if (!this.speechEnabled || !('speechSynthesis' in window)) {
return;
}
// Determine which directions to announce
const activeDirections = [];
// Vertical pitch variation (up/down)
if (directions.up && !directions.down) {
pitchModifier = 50; // Higher pitch for above
} else if (directions.down && !directions.up) {
pitchModifier = -50; // Lower pitch for below
if (directions.up) activeDirections.push('up');
if (directions.down) activeDirections.push('down');
if (directions.left) activeDirections.push('left');
if (directions.right) activeDirections.push('right');
if (activeDirections.length === 0) {
return;
}
// Create the text to speak
let textToSpeak;
if (activeDirections.length === 1) {
textToSpeak = activeDirections[0];
} else if (activeDirections.length === 2) {
textToSpeak = activeDirections.join(' and ');
} else {
textToSpeak = activeDirections.slice(0, -1).join(', ') + ', and ' + activeDirections[activeDirections.length - 1];
}
// Avoid repeating the same direction announcement
if (textToSpeak === this.lastSpokenDirection) {
return;
}
this.lastSpokenDirection = textToSpeak;
// Stop any currently playing speech
speechSynthesis.cancel();
// Create and configure the speech utterance
const utterance = new SpeechSynthesisUtterance(textToSpeak);
utterance.rate = 1.2; // Slightly faster speech
utterance.pitch = 1.0;
utterance.volume = 0.8;
// Create directional warning sounds
this.createOscillator(300, 'sine', 0.15, panValue, pitchModifier);
this.createOscillator(250, 'sine', 0.15, panValue, pitchModifier * 0.5);
// Add subtle rhythmic variation based on direction
if (directions.up) {
// Quick double beep for above
// Clear the last spoken direction when speech ends
utterance.onend = () => {
setTimeout(() => {
this.createOscillator(350, 'sine', 0.05, panValue, pitchModifier);
}, 80);
} else if (directions.down) {
// Slower, deeper sound for below
this.createOscillator(200, 'sine', 0.2, panValue, pitchModifier);
this.lastSpokenDirection = '';
}, 500); // Small delay to prevent immediate repetition
};
// Speak the directions
speechSynthesis.speak(utterance);
}
setSpeechEnabled(enabled) {
this.speechEnabled = enabled;
if (!enabled) {
speechSynthesis.cancel();
this.lastSpokenDirection = '';
}
}

View File

@ -23,6 +23,7 @@ export class Game {
// Game state
this.soundEnabled = true;
this.speechEnabled = true;
this.vibrationEnabled = true;
this.lastProximityWarning = 0;
@ -32,6 +33,7 @@ export class Game {
setupControls() {
const soundToggle = document.getElementById('soundToggle');
const speechToggle = document.getElementById('speechToggle');
const vibrationToggle = document.getElementById('vibrationToggle');
soundToggle.addEventListener('click', () => {
@ -41,6 +43,12 @@ export class Game {
this.audioSystem.stopAll();
}
});
speechToggle.addEventListener('click', () => {
this.speechEnabled = !this.speechEnabled;
speechToggle.textContent = `🗣️ Speech: ${this.speechEnabled ? 'ON' : 'OFF'}`;
this.audioSystem.setSpeechEnabled(this.speechEnabled);
});
vibrationToggle.addEventListener('click', () => {
this.vibrationEnabled = !this.vibrationEnabled;
@ -127,20 +135,20 @@ export class Game {
update(deltaTime) {
this.player.update(deltaTime);
// Check proximity to audio danger squares and play directional warning sounds
if (this.soundEnabled) {
// Check proximity to audio danger squares and play directional warning speech
if (this.speechEnabled) {
const directions = this.maze.getAudioDangerDirections(this.player.x, this.player.y);
if (directions.squares.length > 0) {
this.playDirectionalProximityWarning(directions);
}
}
}
playDirectionalProximityWarning(directions) {
// Play a directional warning sound when near audio danger squares
// Play a directional warning speech when near audio danger squares
// Use a timer to avoid playing too frequently
const now = Date.now();
if (!this.lastProximityWarning || now - this.lastProximityWarning > 1000) {
if (!this.lastProximityWarning || now - this.lastProximityWarning > 1500) {
this.audioSystem.playDirectionalProximitySound(directions);
this.lastProximityWarning = now;
}

View File

@ -23,14 +23,13 @@ export class Maze {
// Create outer boundary walls only
this.createOuterWalls();
// Add dangerous squares (about 30% of inner safe squares)
this.addDangerousSquares();
// Set exit (ensure it's not on the outer wall)
this.exitX = this.cols - 2;
this.exitY = this.rows - 2;
this.grid[this.exitY][this.exitX] = this.EXIT;
this.dangerousSquares.delete(`${this.exitX},${this.exitY}`);
// Add dangerous squares while ensuring a path exists
this.addDangerousSquaresWithPathGuarantee();
}
createOuterWalls() {
@ -77,6 +76,123 @@ export class Maze {
this.dangerousSquares.add(`${x},${y}`);
}
}
addDangerousSquaresWithPathGuarantee() {
const innerSafeSquares = [];
// Find all inner safe squares (excluding outer wall boundary, player start, and exit)
for (let y = 1; y < this.rows - 1; y++) {
for (let x = 1; x < this.cols - 1; x++) {
if (this.grid[y][x] === this.SAFE && !(x === 1 && y === 1) && !(x === this.exitX && y === this.exitY)) {
innerSafeSquares.push({ x, y });
}
}
}
// 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;
this.shuffle(innerSafeSquares);
let addedDangerous = 0;
let addedVisual = 0;
let addedAudio = 0;
// Add dangerous squares one by one, checking path after each addition
for (let i = 0; i < innerSafeSquares.length && addedDangerous < totalDangerousCount; i++) {
const { x, y } = innerSafeSquares[i];
// Determine what type of dangerous square to add
let dangerType;
if (addedVisual < visualDangerousCount) {
dangerType = this.DANGEROUS_VISUAL;
} else if (addedAudio < audioDangerousCount) {
dangerType = this.DANGEROUS_AUDIO;
} else {
break; // We've added enough dangerous squares
}
// Temporarily place the dangerous square
const originalType = this.grid[y][x];
this.grid[y][x] = dangerType;
// Check if a path still exists from start to exit
if (this.hasPathToExit(1, 1)) {
// Path exists, keep this dangerous square
this.dangerousSquares.add(`${x},${y}`);
addedDangerous++;
if (dangerType === this.DANGEROUS_VISUAL) {
addedVisual++;
} else {
addedAudio++;
}
} else {
// Path blocked, revert this square to safe
this.grid[y][x] = originalType;
}
}
}
// Breadth-first search to check if a safe path exists from start to exit
hasPathToExit(startX, startY) {
const visited = new Set();
const queue = [{ x: startX, y: startY }];
const targetKey = `${this.exitX},${this.exitY}`;
while (queue.length > 0) {
const { x, y } = queue.shift();
const key = `${x},${y}`;
if (key === targetKey) {
return true; // Found path to exit
}
if (visited.has(key)) {
continue;
}
visited.add(key);
// Check all four directions
const directions = [
{ dx: 0, dy: -1 }, // up
{ dx: 0, dy: 1 }, // down
{ dx: -1, dy: 0 }, // left
{ dx: 1, dy: 0 } // right
];
for (const { dx, dy } of directions) {
const newX = x + dx;
const newY = y + dy;
const newKey = `${newX},${newY}`;
// Check if the new position is valid and safe to walk
if (newX >= 0 && newX < this.cols &&
newY >= 0 && newY < this.rows &&
!visited.has(newKey) &&
this.isSafeToWalk(newX, newY)) {
queue.push({ x: newX, y: newY });
}
}
}
return false; // No safe path found
}
// Check if a position is safe to walk (only safe squares and exit)
isSafeToWalk(x, y) {
const cellType = this.grid[y][x];
return cellType === this.SAFE || cellType === this.EXIT;
}
// Check if a position is walkable (not a wall - for game movement)
isWalkable(x, y) {
const cellType = this.grid[y][x];
// Player can walk on all squares except walls
// Dangerous squares end the game but are still walkable for movement
return cellType !== this.WALL;
}
shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
@ -89,7 +205,7 @@ export class Maze {
if (x < 0 || x >= this.cols || y < 0 || y >= this.rows) {
return false;
}
return this.grid[y][x] !== this.WALL;
return this.isWalkable(x, y);
}
isDangerous(x, y) {
@ -121,7 +237,7 @@ export class Maze {
}
getAudioDangerDirections(playerX, playerY, range = 1) {
// Get all directions where audio danger squares are located
// Get all directions where audio danger squares are located (cardinal directions only)
const directions = {
left: false,
right: false,
@ -130,22 +246,23 @@ export class Maze {
squares: []
};
for (let dy = -range; dy <= range; dy++) {
for (let dx = -range; dx <= range; dx++) {
if (dx === 0 && dy === 0) continue; // Skip player's current position
const checkX = playerX + dx;
const checkY = playerY + dy;
// Only check cardinal directions (no corners)
const cardinalDirections = [
{ dx: -1, dy: 0, dir: 'left' }, // left
{ dx: 1, dy: 0, dir: 'right' }, // right
{ dx: 0, dy: -1, dir: 'up' }, // up
{ dx: 0, dy: 1, dir: 'down' } // down
];
for (const { dx, dy, dir } of cardinalDirections) {
for (let dist = 1; dist <= range; dist++) {
const checkX = playerX + (dx * dist);
const checkY = playerY + (dy * dist);
if (checkX >= 0 && checkX < this.cols && checkY >= 0 && checkY < this.rows) {
if (this.grid[checkY][checkX] === this.DANGEROUS_AUDIO) {
directions.squares.push({ x: checkX, y: checkY, dx, dy });
// Determine primary directions
if (dx < 0) directions.left = true;
if (dx > 0) directions.right = true;
if (dy < 0) directions.up = true;
if (dy > 0) directions.down = true;
directions.squares.push({ x: checkX, y: checkY, dx: dx * dist, dy: dy * dist });
directions[dir] = true;
}
}
}
@ -183,6 +300,9 @@ export class Maze {
ctx.lineWidth = 1;
ctx.strokeRect(pixelX, pixelY, cellSize, cellSize);
break;
//debug
// case this.DANGEROUS_AUDIO:
case this.DANGEROUS_VISUAL:
// Visual-only danger squares - animated red appearance