Skip to content

Commit 3f8c289

Browse files
committed
Add sound mute toggle with persistent preference
- Mute/unmute button visible on menu and during gameplay - Saves preference to localStorage - All audio functions respect muted state
1 parent 8baaf33 commit 3f8c289

File tree

5 files changed

+78
-2
lines changed

5 files changed

+78
-2
lines changed

src/client/audio.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,20 @@
44
*/
55

66
let ctx: AudioContext | null = null;
7+
let muted = false;
78

8-
function getCtx(): AudioContext {
9+
export function isMuted(): boolean {
10+
return muted;
11+
}
12+
13+
export function setMuted(m: boolean) {
14+
muted = m;
15+
// Persist preference
16+
try { localStorage.setItem('delta-v-mute', m ? '1' : '0'); } catch {}
17+
}
18+
19+
function getCtx(): AudioContext | null {
20+
if (muted) return null;
921
if (!ctx) {
1022
ctx = new AudioContext();
1123
}
@@ -14,6 +26,12 @@ function getCtx(): AudioContext {
1426

1527
/** Resume audio context after user gesture (required by browsers). */
1628
export function initAudio() {
29+
// Load saved mute preference
30+
try {
31+
const saved = localStorage.getItem('delta-v-mute');
32+
if (saved === '1') muted = true;
33+
} catch {}
34+
1735
const resume = () => {
1836
if (ctx?.state === 'suspended') {
1937
ctx.resume();
@@ -28,6 +46,7 @@ export function initAudio() {
2846
/** Short blip for UI interactions (button clicks, selections). */
2947
export function playSelect() {
3048
const ac = getCtx();
49+
if (!ac) return;
3150
const osc = ac.createOscillator();
3251
const gain = ac.createGain();
3352
osc.connect(gain);
@@ -44,6 +63,7 @@ export function playSelect() {
4463
/** Confirm/submit sound — ascending tone. */
4564
export function playConfirm() {
4665
const ac = getCtx();
66+
if (!ac) return;
4767
const osc = ac.createOscillator();
4868
const gain = ac.createGain();
4969
osc.connect(gain);
@@ -60,6 +80,7 @@ export function playConfirm() {
6080
/** Thruster sound for movement. */
6181
export function playThrust() {
6282
const ac = getCtx();
83+
if (!ac) return;
6384
const bufSize = ac.sampleRate * 0.3;
6485
const buf = ac.createBuffer(1, bufSize, ac.sampleRate);
6586
const data = buf.getChannelData(0);
@@ -84,6 +105,7 @@ export function playThrust() {
84105
/** Laser/beam sound for combat. */
85106
export function playCombat() {
86107
const ac = getCtx();
108+
if (!ac) return;
87109
const osc = ac.createOscillator();
88110
const gain = ac.createGain();
89111
osc.connect(gain);
@@ -100,6 +122,7 @@ export function playCombat() {
100122
/** Explosion sound for ship destruction or detonation. */
101123
export function playExplosion() {
102124
const ac = getCtx();
125+
if (!ac) return;
103126
const bufSize = ac.sampleRate * 0.5;
104127
const buf = ac.createBuffer(1, bufSize, ac.sampleRate);
105128
const data = buf.getChannelData(0);
@@ -124,6 +147,7 @@ export function playExplosion() {
124147
/** Alert tone for phase changes. */
125148
export function playPhaseChange() {
126149
const ac = getCtx();
150+
if (!ac) return;
127151
const osc = ac.createOscillator();
128152
const gain = ac.createGain();
129153
osc.connect(gain);
@@ -150,6 +174,7 @@ export function playPhaseChange() {
150174
/** Victory fanfare. */
151175
export function playVictory() {
152176
const ac = getCtx();
177+
if (!ac) return;
153178
const notes = [523, 659, 784, 1047]; // C5, E5, G5, C6
154179
notes.forEach((freq, i) => {
155180
const osc = ac.createOscillator();
@@ -169,6 +194,7 @@ export function playVictory() {
169194
/** Defeat sound. */
170195
export function playDefeat() {
171196
const ac = getCtx();
197+
if (!ac) return;
172198
const notes = [400, 350, 300, 200]; // Descending
173199
notes.forEach((freq, i) => {
174200
const osc = ac.createOscillator();

src/client/main.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { aiAstrogation, aiOrdnance, aiCombat, type AIDifficulty } from '../share
88
import { Renderer, HEX_SIZE } from './renderer';
99
import { InputHandler } from './input';
1010
import { UIManager } from './ui';
11-
import { initAudio, playSelect, playConfirm, playThrust, playCombat, playExplosion, playPhaseChange, playVictory, playDefeat } from './audio';
11+
import { initAudio, playSelect, playConfirm, playThrust, playCombat, playExplosion, playPhaseChange, playVictory, playDefeat, isMuted, setMuted } from './audio';
1212

1313
type ClientState =
1414
| 'menu'
@@ -141,6 +141,14 @@ class GameClient {
141141
document.getElementById('helpCloseBtn')!.addEventListener('click', () => this.toggleHelp());
142142
document.getElementById('helpBtn')!.addEventListener('click', () => this.toggleHelp());
143143

144+
// Sound toggle
145+
const soundBtn = document.getElementById('soundBtn')!;
146+
this.updateSoundButton();
147+
soundBtn.addEventListener('click', () => {
148+
setMuted(!isMuted());
149+
this.updateSoundButton();
150+
});
151+
144152
// Ship hover tooltip
145153
this.canvas.addEventListener('mousemove', (e) => this.updateTooltip(e.clientX, e.clientY));
146154
this.canvas.addEventListener('mouseleave', () => { this.tooltipEl.style.display = 'none'; });
@@ -955,6 +963,13 @@ class GameClient {
955963
helpOverlay.style.display = helpOverlay.style.display === 'none' ? 'flex' : 'none';
956964
}
957965

966+
private updateSoundButton() {
967+
const btn = document.getElementById('soundBtn')!;
968+
const m = isMuted();
969+
btn.textContent = m ? '🔇' : '🔊';
970+
btn.classList.toggle('muted', m);
971+
}
972+
958973
private logLandings(movements: ShipMovement[]) {
959974
if (!this.gameState) return;
960975
for (const m of movements) {

src/client/ui.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,14 @@ export class UIManager {
146146
this.gameLogEl.style.display = 'none';
147147
this.logShowBtn.style.display = 'none';
148148
document.getElementById('helpBtn')!.style.display = 'none';
149+
document.getElementById('soundBtn')!.style.display = 'none';
149150
document.getElementById('helpOverlay')!.style.display = 'none';
150151
}
151152

152153
showMenu() {
153154
this.hideAll();
154155
this.menuEl.style.display = 'flex';
156+
document.getElementById('soundBtn')!.style.display = 'flex';
155157
// Reset state
156158
document.getElementById('difficultySelect')!.style.display = 'none';
157159
this.pendingAIGame = false;
@@ -181,6 +183,7 @@ export class UIManager {
181183
this.hudEl.style.display = 'block';
182184
this.shipListEl.style.display = 'flex';
183185
document.getElementById('helpBtn')!.style.display = 'flex';
186+
document.getElementById('soundBtn')!.style.display = 'flex';
184187
if (this.logVisible) {
185188
this.gameLogEl.style.display = 'flex';
186189
} else {

static/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ <h3>Combat</h3>
141141

142142
<!-- Help button -->
143143
<button id="helpBtn" class="help-btn" style="display:none">?</button>
144+
<!-- Sound toggle button -->
145+
<button id="soundBtn" class="sound-btn" style="display:none" title="Toggle sound"></button>
144146

145147
<!-- Ship tooltip -->
146148
<div id="shipTooltip" class="ship-tooltip" style="display:none"></div>

static/style.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,36 @@ body {
597597
color: #ccc;
598598
}
599599

600+
.sound-btn {
601+
position: fixed;
602+
bottom: 6.5rem;
603+
right: 0.5rem;
604+
z-index: 5;
605+
width: 28px;
606+
height: 28px;
607+
border-radius: 50%;
608+
background: rgba(10, 10, 26, 0.75);
609+
border: 1px solid rgba(255, 255, 255, 0.15);
610+
color: #888;
611+
font-family: inherit;
612+
font-size: 0.75rem;
613+
cursor: pointer;
614+
backdrop-filter: blur(4px);
615+
display: flex;
616+
align-items: center;
617+
justify-content: center;
618+
}
619+
620+
.sound-btn:hover {
621+
background: rgba(255, 255, 255, 0.08);
622+
color: #ccc;
623+
}
624+
625+
.sound-btn.muted {
626+
color: #ff5252;
627+
border-color: rgba(255, 82, 82, 0.3);
628+
}
629+
600630
/* Game Over */
601631
#gameOver h2 {
602632
font-size: 2rem;

0 commit comments

Comments
 (0)