Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 139 additions & 32 deletions web-app/js/audioManager.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,165 @@
const AudioManager = (() => {
let _ctx = null;
let soundLocks = new Set();

function ctx() {
if (!_ctx) _ctx = new (window.AudioContext || window.webkitAudioContext)();
if (_ctx.state === 'suspended') _ctx.resume();
return _ctx;
}

function enabled() {
const v = localStorage.getItem('soundEnabled');
return v === null ? true : v === 'true';
}
function beep(freq, dur, type='sine', gain=0.3, t=null) {
const c = ctx(); const now = t ?? c.currentTime;
const osc = c.createOscillator(), g = c.createGain();
osc.connect(g); g.connect(c.destination);
osc.type = type; osc.frequency.setValueAtTime(freq, now);

function beep(freq, dur, type = 'sine', gain = 0.3, t = null) {
const c = ctx();
const now = t ?? c.currentTime;

const osc = c.createOscillator(),
g = c.createGain();

osc.connect(g);
g.connect(c.destination);

osc.type = type;
osc.frequency.setValueAtTime(freq, now);

g.gain.setValueAtTime(0, now);
g.gain.linearRampToValueAtTime(gain, now+0.01);
g.gain.exponentialRampToValueAtTime(0.001, now+dur);
osc.start(now); osc.stop(now+dur+0.01);
g.gain.linearRampToValueAtTime(gain, now + 0.01);
g.gain.exponentialRampToValueAtTime(0.001, now + dur);

osc.start(now);
osc.stop(now + dur + 0.01);
}
function noise(dur=0.1, gain=0.2) {
const c = ctx(), buf = c.createBuffer(1, c.sampleRate*dur, c.sampleRate);

function noise(dur = 0.1, gain = 0.2) {
const c = ctx(),
buf = c.createBuffer(1, c.sampleRate * dur, c.sampleRate);

const d = buf.getChannelData(0);
for (let i=0;i<d.length;i++) d[i]=Math.random()*2-1;
const src = c.createBufferSource(); src.buffer = buf;
const g = c.createGain(), f = c.createBiquadFilter();
f.type='highpass'; f.frequency.value=1000;
src.connect(f); f.connect(g); g.connect(c.destination);
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;

const src = c.createBufferSource();
src.buffer = buf;

const g = c.createGain(),
f = c.createBiquadFilter();

f.type = 'highpass';
f.frequency.value = 1000;

src.connect(f);
f.connect(g);
g.connect(c.destination);

const t = c.currentTime;
g.gain.setValueAtTime(gain, t);
g.gain.exponentialRampToValueAtTime(0.001, t+dur);
src.start(t); src.stop(t+dur+0.01);
g.gain.exponentialRampToValueAtTime(0.001, t + dur);

src.start(t);
src.stop(t + dur + 0.01);
}

const sounds = {
snake_eat() { const c=ctx(),n=c.currentTime; beep(300,.06,'square',.25,n); beep(600,.08,'square',.20,n+.05); },
snake_die() { const c=ctx(),n=c.currentTime; beep(400,.12,'sawtooth',.3,n); beep(200,.20,'sawtooth',.25,n+.10); beep(100,.30,'sine',.20,n+.25); },
mole_hit() { noise(.08,.35); const c=ctx(); beep(180,.10,'sine',.2,c.currentTime+.02); },
card_flip() { noise(.05,.12); const c=ctx(); beep(800,.04,'sine',.08,c.currentTime+.01); },
card_deal() { noise(.07,.18); const c=ctx(); beep(300,.06,'sine',.12,c.currentTime+.02); },
game_win() { const c=ctx(),n=c.currentTime; [523,659,784,1047].forEach((f,i)=>beep(f,.15,'sine',.25,n+i*.12)); },
game_over() { const c=ctx(),n=c.currentTime; beep(440,.15,'sawtooth',.3,n); beep(349,.20,'sawtooth',.28,n+.18); beep(262,.35,'sine',.25,n+.40); },
score_point() { beep(880,.08,'sine',.2); },
wrong() { beep(150,.15,'sawtooth',.2); },
click() { beep(600,.04,'sine',.1); },
snake_eat() {
const c = ctx(),
n = c.currentTime;
beep(300, 0.06, 'square', 0.25, n);
beep(600, 0.08, 'square', 0.2, n + 0.05);
},

snake_die() {
const c = ctx(),
n = c.currentTime;
beep(400, 0.12, 'sawtooth', 0.3, n);
beep(200, 0.2, 'sawtooth', 0.25, n + 0.1);
beep(100, 0.3, 'sine', 0.2, n + 0.25);
},

mole_hit() {
noise(0.08, 0.35);
const c = ctx();
beep(180, 0.1, 'sine', 0.2, c.currentTime + 0.02);
},

card_flip() {
noise(0.05, 0.12);
const c = ctx();
beep(800, 0.04, 'sine', 0.08, c.currentTime + 0.01);
},

card_deal() {
noise(0.07, 0.18);
const c = ctx();
beep(300, 0.06, 'sine', 0.12, c.currentTime + 0.02);
},

game_win() {
const c = ctx(),
n = c.currentTime;
[523, 659, 784, 1047].forEach((f, i) =>
beep(f, 0.15, 'sine', 0.25, n + i * 0.12)
);
},

game_over() {
const c = ctx(),
n = c.currentTime;
beep(440, 0.15, 'sawtooth', 0.3, n);
beep(349, 0.2, 'sawtooth', 0.28, n + 0.18);
beep(262, 0.35, 'sine', 0.25, n + 0.4);
},

score_point() {
beep(880, 0.08, 'sine', 0.2);
},

wrong() {
beep(150, 0.15, 'sawtooth', 0.2);
},

click() {
beep(600, 0.04, 'sine', 0.1);
},
};

return {
play(name) {
if (!enabled()) return false;
if (sounds[name]) { try { sounds[name](); } catch(e){} return true; }

// 🛑 Strict permanent lock until game reset explicitly clears it
if (name === "snake_die" || name === "game_over") {
if (soundLocks.has(name)) return false;
soundLocks.add(name);
}

if (sounds[name]) {
try {
sounds[name]();
} catch (e) {}
return true;
}

return false;
},
setEnabled(v) { localStorage.setItem('soundEnabled', String(!!v)); },
isEnabled() { return enabled(); },
list() { return Object.keys(sounds); },

setEnabled(v) {
localStorage.setItem('soundEnabled', String(!!v));
},

isEnabled() {
return enabled();
},

list() {
return Object.keys(sounds);
},

reset() {
soundLocks.clear();
}
};
})();
window.AudioManager = AudioManager;
window.AudioManager = AudioManager;
22 changes: 17 additions & 5 deletions web-app/js/projects/snake.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,12 @@ function getSnakeGameHTML() {

// --- GAME LOGIC STATE ---
let direction = { x: 0, y: 0 };
let speed = 9; // System baseline operational configuration state
let scoreMultiplier = 2; // Baseline multiplier scalar state
let speed = 9;
let scoreMultiplier = 2;
let score = 0;
let lastPaintTime = 0;
let isPaused = false;
let isGameOver = false; // 👈 Added tracking state flag
let snakeArr = [{ x: 13, y: 10 }];
let food = { x: 6, y: 7 };

Expand All @@ -215,7 +216,7 @@ const CONFIG_DIFFICULTY = {

function main(ctime) {
const canvas = document.getElementById('snakeCanvas');
if (!canvas) return; // Exit loop if the game modal has been closed
if (!canvas) return;

window.requestAnimationFrame(main);
if ((ctime - lastPaintTime) / 1000 < 1 / speed) {
Expand Down Expand Up @@ -268,7 +269,12 @@ function gameEngine() {
direction = { x: 0, y: 0 };
document.getElementById('final-score').innerHTML = score;
document.getElementById("game-over-overlay").classList.remove("hidden");
if (window.AudioManager) AudioManager.play("snake_die");

// 🚨 CRITICAL FIX: Only play the sound ONCE right when impact happens
if (!isGameOver) {
if (window.AudioManager) AudioManager.play("snake_die");
isGameOver = true;
}

// Execute persistent local evaluations
checkAndSaveHighScore();
Expand All @@ -282,7 +288,7 @@ function gameEngine() {

// Eating food
if (snakeArr[0].y === food.y && snakeArr[0].x === food.x) {
score += (1 * scoreMultiplier); // Scaled multiplier calculations
score += (1 * scoreMultiplier);
document.getElementById('score').innerHTML = score;
snakeArr.unshift({ x: snakeArr[0].x + direction.x, y: snakeArr[0].y + direction.y });
if (window.AudioManager) AudioManager.play("snake_eat");
Expand Down Expand Up @@ -332,8 +338,14 @@ function restartGame() {
snakeArr = [{ x: 13, y: 10 }];
score = 0;
isPaused = false;
isGameOver = false; // 👈 Reset the tracking flag state
lastPaintTime = 0;

// 🚨 CRITICAL FIX: Clear the AudioManager permanent sound locks on restart
if (window.AudioManager) {
window.AudioManager.reset();
}

// Reset UI
const pauseBtn = document.getElementById('pauseGameBtn');
if (pauseBtn) pauseBtn.textContent = 'Pause';
Expand Down
Loading