extremely claude-assisted go game based on atproto! working on cleaning up and giving a more unique design, still has a bit of a slop vibe to it.
1import { browser } from '$app/environment';
2
3export type SoundEffect =
4 | 'capture' // When you capture opponent's stones
5 | 'captured' // When your stones get captured
6 | 'move_made' // When someone makes a move in your game (homepage notification)
7 | 'opened_game' // When you open/load a game
8 | 'played_stone'; // When placing a stone on the board
9
10export class SoundManager {
11 private sounds: Map<SoundEffect, HTMLAudioElement> = new Map();
12 private enabled: boolean = true;
13
14 constructor() {
15 if (!browser) return;
16
17 // Load SFX preference from cookie
18 this.enabled = this.getSfxEnabled();
19
20 // Preload all sound effects
21 this.preloadSound('capture', '/sfx/capture.wav');
22 this.preloadSound('captured', '/sfx/captured.wav');
23 this.preloadSound('move_made', '/sfx/move_made.wav');
24 this.preloadSound('opened_game', '/sfx/opened_game.wav');
25 this.preloadSound('played_stone', '/sfx/played_stone.wav');
26 }
27
28 private preloadSound(name: SoundEffect, path: string) {
29 if (!browser) return;
30
31 const audio = new Audio(path);
32 audio.preload = 'auto';
33 audio.volume = 0.5; // Default volume at 50%
34 this.sounds.set(name, audio);
35 }
36
37 play(sound: SoundEffect) {
38 if (!browser || !this.enabled) return;
39
40 const audio = this.sounds.get(sound);
41 if (audio) {
42 // Clone the audio to allow overlapping sounds
43 const clone = audio.cloneNode() as HTMLAudioElement;
44 clone.volume = audio.volume;
45 clone.play().catch(err => {
46 console.warn('Failed to play sound:', sound, err);
47 });
48 }
49 }
50
51 setEnabled(enabled: boolean) {
52 this.enabled = enabled;
53 this.setSfxCookie(enabled);
54 }
55
56 isEnabled(): boolean {
57 return this.enabled;
58 }
59
60 setVolume(volume: number) {
61 if (!browser) return;
62
63 // Clamp volume between 0 and 1
64 volume = Math.max(0, Math.min(1, volume));
65
66 for (const audio of this.sounds.values()) {
67 audio.volume = volume;
68 }
69 }
70
71 private getSfxEnabled(): boolean {
72 if (!browser) return true;
73
74 const value = this.getCookie('sfx-enabled');
75 if (value === null) return true; // Default to enabled
76 return value === 'true';
77 }
78
79 private setSfxCookie(enabled: boolean) {
80 if (!browser) return;
81
82 const expires = new Date();
83 expires.setFullYear(expires.getFullYear() + 1); // Expire in 1 year
84 document.cookie = `sfx-enabled=${enabled};expires=${expires.toUTCString()};path=/`;
85 }
86
87 private getCookie(name: string): string | null {
88 if (!browser) return null;
89
90 const value = `; ${document.cookie}`;
91 const parts = value.split(`; ${name}=`);
92 if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
93 return null;
94 }
95}
96
97// Global sound manager instance
98let soundManager: SoundManager | null = null;
99
100export function getSoundManager(): SoundManager {
101 if (!soundManager) {
102 soundManager = new SoundManager();
103 }
104 return soundManager;
105}