mirror of
https://github.com/kuhyx/slavic_game_jam.git
synced 2026-07-04 11:43:04 +02:00
voice
This commit is contained in:
parent
684646e53e
commit
4532e4a66c
@ -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>
|
||||
|
||||
87
src/audio.js
87
src/audio.js
@ -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 = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
src/game.js
18
src/game.js
@ -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;
|
||||
}
|
||||
|
||||
158
src/maze.js
158
src/maze.js
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user