Skip to content

Commit 7471761

Browse files
feat: Add interactive game-specific action sounds via Web Audio API (steam-bell-92#730)
* feat: Add centralized AudioManager utility using Web Audio API * feat(snake): Add eat and death sound effects using AudioManager * feat(whack-a-mole): Add hit, miss and game-over sound effects using AudioManager * feat(coin-flip): Add flip, win and wrong prediction sound effects using AudioManager * feat(2048): Add tile merge, game-over and 2048 win sound effects using AudioManager * chore: Load audioManager.js in index.html before projects.js * chore: Add assets/sounds directory for future audio clip support * fix(snake): Correct AudioManager hook placement in snake.js * fix(whack-a-mole): Correct AudioManager hook placement in whack-a-mole.js * fix(coin-flip): Correct AudioManager hook placement in coin-flip.js * fix(2048): Correct AudioManager hook placement in 2048-game.js * fix: Move audioManager.js script tag before project files in index.html * fix: Correct script load order for audioManager in index.html
1 parent e3490dd commit 7471761

7 files changed

Lines changed: 70 additions & 1 deletion

File tree

web-app/assets/sounds/.gitkeep

Whitespace-only changes.

web-app/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ <h3>Legal</h3>
645645
<!-- ── SCRIPTS ──────────────────────────────────────────────── -->
646646
<script src="https://unpkg.com/lucide@latest"></script>
647647
<script src="js/audio.js"></script>
648+
<script src="js/audioManager.js"></script>
648649
<script src="js/projects/rock-paper-scissor.js"></script>
649650
<script src="js/projects/dice-rolling.js"></script>
650651
<script src="js/projects/coin-flip.js"></script>

web-app/js/audioManager.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const AudioManager = (() => {
2+
let _ctx = null;
3+
function ctx() {
4+
if (!_ctx) _ctx = new (window.AudioContext || window.webkitAudioContext)();
5+
if (_ctx.state === 'suspended') _ctx.resume();
6+
return _ctx;
7+
}
8+
function enabled() {
9+
const v = localStorage.getItem('soundEnabled');
10+
return v === null ? true : v === 'true';
11+
}
12+
function beep(freq, dur, type='sine', gain=0.3, t=null) {
13+
const c = ctx(); const now = t ?? c.currentTime;
14+
const osc = c.createOscillator(), g = c.createGain();
15+
osc.connect(g); g.connect(c.destination);
16+
osc.type = type; osc.frequency.setValueAtTime(freq, now);
17+
g.gain.setValueAtTime(0, now);
18+
g.gain.linearRampToValueAtTime(gain, now+0.01);
19+
g.gain.exponentialRampToValueAtTime(0.001, now+dur);
20+
osc.start(now); osc.stop(now+dur+0.01);
21+
}
22+
function noise(dur=0.1, gain=0.2) {
23+
const c = ctx(), buf = c.createBuffer(1, c.sampleRate*dur, c.sampleRate);
24+
const d = buf.getChannelData(0);
25+
for (let i=0;i<d.length;i++) d[i]=Math.random()*2-1;
26+
const src = c.createBufferSource(); src.buffer = buf;
27+
const g = c.createGain(), f = c.createBiquadFilter();
28+
f.type='highpass'; f.frequency.value=1000;
29+
src.connect(f); f.connect(g); g.connect(c.destination);
30+
const t = c.currentTime;
31+
g.gain.setValueAtTime(gain, t);
32+
g.gain.exponentialRampToValueAtTime(0.001, t+dur);
33+
src.start(t); src.stop(t+dur+0.01);
34+
}
35+
const sounds = {
36+
snake_eat() { const c=ctx(),n=c.currentTime; beep(300,.06,'square',.25,n); beep(600,.08,'square',.20,n+.05); },
37+
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); },
38+
mole_hit() { noise(.08,.35); const c=ctx(); beep(180,.10,'sine',.2,c.currentTime+.02); },
39+
card_flip() { noise(.05,.12); const c=ctx(); beep(800,.04,'sine',.08,c.currentTime+.01); },
40+
card_deal() { noise(.07,.18); const c=ctx(); beep(300,.06,'sine',.12,c.currentTime+.02); },
41+
game_win() { const c=ctx(),n=c.currentTime; [523,659,784,1047].forEach((f,i)=>beep(f,.15,'sine',.25,n+i*.12)); },
42+
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); },
43+
score_point() { beep(880,.08,'sine',.2); },
44+
wrong() { beep(150,.15,'sawtooth',.2); },
45+
click() { beep(600,.04,'sine',.1); },
46+
};
47+
return {
48+
play(name) {
49+
if (!enabled()) return false;
50+
if (sounds[name]) { try { sounds[name](); } catch(e){} return true; }
51+
return false;
52+
},
53+
setEnabled(v) { localStorage.setItem('soundEnabled', String(!!v)); },
54+
isEnabled() { return enabled(); },
55+
list() { return Object.keys(sounds); },
56+
};
57+
})();
58+
window.AudioManager = AudioManager;

web-app/js/projects/2048-game.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ function init2048Game() {
291291
for (let i = 0; i < 3; i++) {
292292
if (row[i] && row[i] === row[i + 1]) {
293293
row[i] *= 2;
294+
if (window.AudioManager) AudioManager.play("score_point");
294295
score += row[i];
295296
row[i + 1] = 0;
296297
}
@@ -380,6 +381,7 @@ function init2048Game() {
380381

381382
if (checkGameOver()) {
382383
gameMessage.textContent = "GAME OVER!";
384+
if (window.AudioManager) AudioManager.play("game_over");
383385
}
384386

385387
drawBoard();
@@ -388,6 +390,7 @@ function init2048Game() {
388390

389391
if (checkGameOver()) {
390392
gameMessage.textContent = "GAME OVER!";
393+
if (window.AudioManager) AudioManager.play("game_over");
391394
} else {
392395
gameMessage.textContent =
393396
"No move possible in this direction!";

web-app/js/projects/coin-flip.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ function initCoinFlip() {
502502
flipBtn.disabled = true;
503503
predictionWrapper.classList.add('disabled-cards');
504504
result.textContent = 'Flipping...';
505+
if (window.AudioManager) AudioManager.play("card_flip");
505506
result.className = 'coin-result flipping';
506507
coinScene.classList.add('rolling');
507508

@@ -526,6 +527,7 @@ function initCoinFlip() {
526527
}
527528

528529
if (isCorrect) {
530+
if (window.AudioManager) AudioManager.play("game_win");
529531
wins++;
530532
streak++;
531533
if (streak > bestStreak) {
@@ -536,6 +538,7 @@ function initCoinFlip() {
536538
} else {
537539
losses++;
538540
streak = 0;
541+
if (window.AudioManager) AudioManager.play("wrong");
539542
result.textContent = isHeads ? '👑 Heads! Wrong prediction. 😢' : '🦅 Tails! Wrong prediction. 😢';
540543
result.className = 'coin-result lose';
541544
}

web-app/js/projects/snake.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ function gameEngine() {
267267
if (isCollide(snakeArr)) {
268268
direction = { x: 0, y: 0 };
269269
document.getElementById('final-score').innerHTML = score;
270-
document.getElementById('game-over-overlay').classList.remove('hidden');
270+
document.getElementById("game-over-overlay").classList.remove("hidden");
271+
if (window.AudioManager) AudioManager.play("snake_die");
271272

272273
// Execute persistent local evaluations
273274
checkAndSaveHighScore();
@@ -284,6 +285,7 @@ function gameEngine() {
284285
score += (1 * scoreMultiplier); // Scaled multiplier calculations
285286
document.getElementById('score').innerHTML = score;
286287
snakeArr.unshift({ x: snakeArr[0].x + direction.x, y: snakeArr[0].y + direction.y });
288+
if (window.AudioManager) AudioManager.play("snake_eat");
287289
let a = 2, b = 16;
288290
food = { x: Math.round(a + (b - a) * Math.random()), y: Math.round(a + (b - a) * Math.random()) };
289291
}

web-app/js/projects/whack-a-mole.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function initWhackaMole() {
6262
score += 1;
6363
scoreEl.textContent = String(score);
6464
messageEl.textContent = 'Hit! 🔨';
65+
if (window.AudioManager) AudioManager.play("mole_hit");
6566
button.textContent = '💥';
6667
button.classList.remove('active');
6768

@@ -117,6 +118,7 @@ function initWhackaMole() {
117118
hole.classList.remove('active');
118119
hole.textContent = '🕳️';
119120
});
121+
if (window.AudioManager) AudioManager.play("game_over");
120122
messageEl.textContent = finalMessage;
121123
startBtn.disabled = false;
122124
}

0 commit comments

Comments
 (0)