mirror of
https://github.com/kuhyx/slavic_game_jam.git
synced 2026-07-04 13:23:08 +02:00
voice
This commit is contained in:
parent
684646e53e
commit
4532e4a66c
@ -13,12 +13,13 @@
|
|||||||
<div class="game-info">
|
<div class="game-info">
|
||||||
<p>Use WASD or Arrow Keys to move</p>
|
<p>Use WASD or Arrow Keys to move</p>
|
||||||
<p class="visual-warning">⚠️ Red squares are deadly - avoid them!</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-warning">🔊 Hidden dangers end the game - listen for directions!</p>
|
||||||
<p class="audio-hint">🎧 Use headphones for best directional audio experience</p>
|
<p class="audio-hint">🗣️ Speech directions: "left", "right", "up", "down"</p>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button id="soundToggle">🔊 Sound: ON</button>
|
<button id="soundToggle">🔊 Sound: ON</button>
|
||||||
|
<button id="speechToggle">🗣️ Speech: ON</button>
|
||||||
<button id="vibrationToggle">📳 Vibration: ON</button>
|
<button id="vibrationToggle">📳 Vibration: ON</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
87
src/audio.js
87
src/audio.js
@ -4,6 +4,8 @@ export class AudioSystem {
|
|||||||
this.gainNode = null;
|
this.gainNode = null;
|
||||||
this.activeSounds = new Set();
|
this.activeSounds = new Set();
|
||||||
this.initialized = false;
|
this.initialized = false;
|
||||||
|
this.lastSpokenDirection = '';
|
||||||
|
this.speechEnabled = true;
|
||||||
|
|
||||||
this.initializeAudio();
|
this.initializeAudio();
|
||||||
}
|
}
|
||||||
@ -127,39 +129,68 @@ export class AudioSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async playDirectionalProximitySound(directions) {
|
async playDirectionalProximitySound(directions) {
|
||||||
await this.ensureAudioContext();
|
// Use text-to-speech instead of sounds
|
||||||
|
this.speakDirections(directions);
|
||||||
// Calculate the dominant direction for panning
|
}
|
||||||
let panValue = 0;
|
|
||||||
let pitchModifier = 0;
|
speakDirections(directions) {
|
||||||
|
if (!this.speechEnabled || !('speechSynthesis' in window)) {
|
||||||
// Horizontal panning (left/right)
|
return;
|
||||||
if (directions.left && !directions.right) {
|
|
||||||
panValue = -0.7; // Pan left
|
|
||||||
} else if (directions.right && !directions.left) {
|
|
||||||
panValue = 0.7; // Pan right
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine which directions to announce
|
||||||
|
const activeDirections = [];
|
||||||
|
|
||||||
// Vertical pitch variation (up/down)
|
if (directions.up) activeDirections.push('up');
|
||||||
if (directions.up && !directions.down) {
|
if (directions.down) activeDirections.push('down');
|
||||||
pitchModifier = 50; // Higher pitch for above
|
if (directions.left) activeDirections.push('left');
|
||||||
} else if (directions.down && !directions.up) {
|
if (directions.right) activeDirections.push('right');
|
||||||
pitchModifier = -50; // Lower pitch for below
|
|
||||||
|
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
|
// Clear the last spoken direction when speech ends
|
||||||
this.createOscillator(300, 'sine', 0.15, panValue, pitchModifier);
|
utterance.onend = () => {
|
||||||
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
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.createOscillator(350, 'sine', 0.05, panValue, pitchModifier);
|
this.lastSpokenDirection = '';
|
||||||
}, 80);
|
}, 500); // Small delay to prevent immediate repetition
|
||||||
} else if (directions.down) {
|
};
|
||||||
// Slower, deeper sound for below
|
|
||||||
this.createOscillator(200, 'sine', 0.2, panValue, pitchModifier);
|
// 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
|
// Game state
|
||||||
this.soundEnabled = true;
|
this.soundEnabled = true;
|
||||||
|
this.speechEnabled = true;
|
||||||
this.vibrationEnabled = true;
|
this.vibrationEnabled = true;
|
||||||
this.lastProximityWarning = 0;
|
this.lastProximityWarning = 0;
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ export class Game {
|
|||||||
|
|
||||||
setupControls() {
|
setupControls() {
|
||||||
const soundToggle = document.getElementById('soundToggle');
|
const soundToggle = document.getElementById('soundToggle');
|
||||||
|
const speechToggle = document.getElementById('speechToggle');
|
||||||
const vibrationToggle = document.getElementById('vibrationToggle');
|
const vibrationToggle = document.getElementById('vibrationToggle');
|
||||||
|
|
||||||
soundToggle.addEventListener('click', () => {
|
soundToggle.addEventListener('click', () => {
|
||||||
@ -41,6 +43,12 @@ export class Game {
|
|||||||
this.audioSystem.stopAll();
|
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', () => {
|
vibrationToggle.addEventListener('click', () => {
|
||||||
this.vibrationEnabled = !this.vibrationEnabled;
|
this.vibrationEnabled = !this.vibrationEnabled;
|
||||||
@ -127,20 +135,20 @@ export class Game {
|
|||||||
update(deltaTime) {
|
update(deltaTime) {
|
||||||
this.player.update(deltaTime);
|
this.player.update(deltaTime);
|
||||||
|
|
||||||
// Check proximity to audio danger squares and play directional warning sounds
|
// Check proximity to audio danger squares and play directional warning speech
|
||||||
if (this.soundEnabled) {
|
if (this.speechEnabled) {
|
||||||
const directions = this.maze.getAudioDangerDirections(this.player.x, this.player.y);
|
const directions = this.maze.getAudioDangerDirections(this.player.x, this.player.y);
|
||||||
if (directions.squares.length > 0) {
|
if (directions.squares.length > 0) {
|
||||||
this.playDirectionalProximityWarning(directions);
|
this.playDirectionalProximityWarning(directions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// Use a timer to avoid playing too frequently
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (!this.lastProximityWarning || now - this.lastProximityWarning > 1000) {
|
if (!this.lastProximityWarning || now - this.lastProximityWarning > 1500) {
|
||||||
this.audioSystem.playDirectionalProximitySound(directions);
|
this.audioSystem.playDirectionalProximitySound(directions);
|
||||||
this.lastProximityWarning = now;
|
this.lastProximityWarning = now;
|
||||||
}
|
}
|
||||||
|
|||||||
158
src/maze.js
158
src/maze.js
@ -23,14 +23,13 @@ export class Maze {
|
|||||||
// Create outer boundary walls only
|
// Create outer boundary walls only
|
||||||
this.createOuterWalls();
|
this.createOuterWalls();
|
||||||
|
|
||||||
// Add dangerous squares (about 30% of inner safe squares)
|
|
||||||
this.addDangerousSquares();
|
|
||||||
|
|
||||||
// Set exit (ensure it's not on the outer wall)
|
// Set exit (ensure it's not on the outer wall)
|
||||||
this.exitX = this.cols - 2;
|
this.exitX = this.cols - 2;
|
||||||
this.exitY = this.rows - 2;
|
this.exitY = this.rows - 2;
|
||||||
this.grid[this.exitY][this.exitX] = this.EXIT;
|
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() {
|
createOuterWalls() {
|
||||||
@ -77,6 +76,123 @@ export class Maze {
|
|||||||
this.dangerousSquares.add(`${x},${y}`);
|
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) {
|
shuffle(array) {
|
||||||
for (let i = array.length - 1; i > 0; i--) {
|
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) {
|
if (x < 0 || x >= this.cols || y < 0 || y >= this.rows) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this.grid[y][x] !== this.WALL;
|
return this.isWalkable(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
isDangerous(x, y) {
|
isDangerous(x, y) {
|
||||||
@ -121,7 +237,7 @@ export class Maze {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAudioDangerDirections(playerX, playerY, range = 1) {
|
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 = {
|
const directions = {
|
||||||
left: false,
|
left: false,
|
||||||
right: false,
|
right: false,
|
||||||
@ -130,22 +246,23 @@ export class Maze {
|
|||||||
squares: []
|
squares: []
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let dy = -range; dy <= range; dy++) {
|
// Only check cardinal directions (no corners)
|
||||||
for (let dx = -range; dx <= range; dx++) {
|
const cardinalDirections = [
|
||||||
if (dx === 0 && dy === 0) continue; // Skip player's current position
|
{ dx: -1, dy: 0, dir: 'left' }, // left
|
||||||
|
{ dx: 1, dy: 0, dir: 'right' }, // right
|
||||||
const checkX = playerX + dx;
|
{ dx: 0, dy: -1, dir: 'up' }, // up
|
||||||
const checkY = playerY + dy;
|
{ 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 (checkX >= 0 && checkX < this.cols && checkY >= 0 && checkY < this.rows) {
|
||||||
if (this.grid[checkY][checkX] === this.DANGEROUS_AUDIO) {
|
if (this.grid[checkY][checkX] === this.DANGEROUS_AUDIO) {
|
||||||
directions.squares.push({ x: checkX, y: checkY, dx, dy });
|
directions.squares.push({ x: checkX, y: checkY, dx: dx * dist, dy: dy * dist });
|
||||||
|
directions[dir] = true;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,6 +300,9 @@ export class Maze {
|
|||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.strokeRect(pixelX, pixelY, cellSize, cellSize);
|
ctx.strokeRect(pixelX, pixelY, cellSize, cellSize);
|
||||||
break;
|
break;
|
||||||
|
//debug
|
||||||
|
// case this.DANGEROUS_AUDIO:
|
||||||
|
|
||||||
|
|
||||||
case this.DANGEROUS_VISUAL:
|
case this.DANGEROUS_VISUAL:
|
||||||
// Visual-only danger squares - animated red appearance
|
// Visual-only danger squares - animated red appearance
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user