diff --git a/apps/web/package.json b/apps/web/package.json
index cee5109..0fbc352 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -25,6 +25,7 @@
"cookie": "^1.0.2",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
+ "howler": "^2.2.4",
"react": "^19.1.0",
"react-countup": "^6.5.3",
"react-dom": "^19.1.0",
@@ -38,6 +39,7 @@
"devDependencies": {
"@eslint/js": "^9.30.1",
"@hex-hunt-app/types": "*",
+ "@types/howler": "^2.2.12",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
diff --git a/apps/web/public/audio/next-move.mp3 b/apps/web/public/audio/next-move.mp3
new file mode 100644
index 0000000..56a3e61
Binary files /dev/null and b/apps/web/public/audio/next-move.mp3 differ
diff --git a/apps/web/public/audio/sci-fi-gun-shot.mp3 b/apps/web/public/audio/sci-fi-gun-shot.mp3
new file mode 100644
index 0000000..f197053
Binary files /dev/null and b/apps/web/public/audio/sci-fi-gun-shot.mp3 differ
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 2de8bef..85b831c 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -12,6 +12,7 @@ import GameProvider from './providers/GameProvider';
import WindowSizeProvider from './providers/WindowSizeProvider';
import PrivacyPolicyPage from './pages/PrivacyPolicyPage';
import GamePage from './pages/GamePage';
+import AudioProvider from './providers/AudioProvider';
const queryClient = new QueryClient();
@@ -23,22 +24,27 @@ function App() {
-
-
- } />
- Select Game
}
- />
- } />
- } />
- }
- />
- Error page
} />
- {' '}
-
+
+
+
+ } />
+ Select Game
}
+ />
+ } />
+ } />
+ }
+ />
+ Error page
}
+ />
+ {' '}
+
+
diff --git a/apps/web/src/hooks/game.ts b/apps/web/src/hooks/game.ts
index 5d5d0e6..7a705e5 100644
--- a/apps/web/src/hooks/game.ts
+++ b/apps/web/src/hooks/game.ts
@@ -15,6 +15,7 @@ import { EventType } from '../components/AnimatedPopup/AnimatedPopup';
import type { GameData } from '../utils/GameData';
import { PlayerType } from '../utils/Player';
import type { Hex } from '../utils/Hex';
+import { SoundSource, useAudio } from '../providers/AudioProvider';
export type ImgRef = {
astronaut: HTMLImageElement | null;
@@ -72,6 +73,7 @@ export const useInitializeSockets = (
) => {
const socketRef = useRef(null);
const [, navigate] = useLocation();
+ const { setSound, setMove } = useAudio();
useEffect(() => {
socketRef.current = io(import.meta.env.VITE_API_URL, {
@@ -117,6 +119,18 @@ export const useInitializeSockets = (
setGameState(data.game);
});
socketRef.current.on('gameState', (data: GameData) => {
+ const someoneDied = data.players.some(
+ (p) => p.diedAtMove === data.moves - 1,
+ );
+
+ if (someoneDied) {
+ setSound(SoundSource.SCI_FI_GUN_SHOT);
+ } else {
+ setSound(SoundSource.NEW_TURN);
+ }
+
+ setMove(data.moves);
+
toast('Next move', {
style: {
background: '#1b1f2d',
@@ -186,6 +200,8 @@ export const useInitializeSockets = (
setShowPopup,
setPopupEvents,
setShouldResetEventDate,
+ setSound,
+ setMove,
]);
return socketRef;
diff --git a/apps/web/src/providers/AudioProvider.tsx b/apps/web/src/providers/AudioProvider.tsx
new file mode 100644
index 0000000..703ee85
--- /dev/null
+++ b/apps/web/src/providers/AudioProvider.tsx
@@ -0,0 +1,69 @@
+import { createContext, useContext, useEffect, useRef, useState } from 'react';
+import { Howl } from 'howler';
+
+// eslint-disable-next-line react-refresh/only-export-components
+export enum SoundSource {
+ NEW_TURN = '/audio/next-move.mp3',
+ SCI_FI_GUN_SHOT = '/audio/sci-fi-gun-shot.mp3',
+}
+
+type AudioContextType = {
+ setSound: (src: string | null) => void;
+ setMove: (move: number) => void;
+};
+
+const initialContextValue: AudioContextType = {
+ setSound: () => {},
+ setMove: () => {},
+};
+
+const AudioContext = createContext(initialContextValue);
+
+type Props = {
+ children: React.ReactNode;
+};
+
+const AudioProvider: React.FC = ({ children }) => {
+ const soundRef = useRef(null);
+ const [sound, setSound] = useState(null);
+ const [move, setMove] = useState(0);
+
+ const playNewSound = (src: string) => {
+ const newSound = new Howl({
+ src: [src],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ onload: () => console.log('sound loaded'),
+ onplayerror: (id, error) => console.error('sound play error', id, error),
+ onloaderror: (id, error) => console.error('sound load error', id, error),
+ });
+
+ newSound.play();
+ soundRef.current = newSound;
+ };
+
+ useEffect(() => {
+ if (soundRef.current) {
+ const oldSound = soundRef.current;
+ oldSound.stop();
+ oldSound.unload();
+ }
+
+ if (sound) playNewSound(sound);
+ }, [sound, move]);
+
+ const value = {
+ setSound,
+ setMove,
+ };
+
+ return (
+ {children}
+ );
+};
+
+export default AudioProvider;
+
+// eslint-disable-next-line react-refresh/only-export-components
+export const useAudio = () => useContext(AudioContext);
diff --git a/yarn.lock b/yarn.lock
index b9a6be8..11cad20 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5052,6 +5052,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/howler@npm:^2.2.12":
+ version: 2.2.12
+ resolution: "@types/howler@npm:2.2.12"
+ checksum: 0f8b88abd9b0607db50c4fbc91cc6646a9eee4a337f3e8b9c39696e78eff1eb0b3f5913bf32cccc7a96d2be2af8544376ce3afa5de4b1ba9873d0287abe97a5b
+ languageName: node
+ linkType: hard
+
"@types/http-cache-semantics@npm:^4.0.2":
version: 4.0.4
resolution: "@types/http-cache-semantics@npm:4.0.4"
@@ -10046,6 +10053,13 @@ __metadata:
languageName: node
linkType: hard
+"howler@npm:^2.2.4":
+ version: 2.2.4
+ resolution: "howler@npm:2.2.4"
+ checksum: 14ad7ed8825ac4439a63429d25f3ba6f44daca1d6df4ad6b04f33e342269b80db5608d9f1e6e980e2d47027c7ceba7811068eba7f56e8e75f2601086e07f0b73
+ languageName: node
+ linkType: hard
+
"html-escaper@npm:^2.0.0":
version: 2.0.2
resolution: "html-escaper@npm:2.0.2"
@@ -15745,6 +15759,7 @@ __metadata:
"@solana/wallet-adapter-wallets": ^0.19.37
"@solana/web3.js": ^1.98.4
"@tanstack/react-query": ^5.84.1
+ "@types/howler": ^2.2.12
"@types/react": ^19.1.8
"@types/react-dom": ^19.1.6
"@types/socket.io": ^3.0.2
@@ -15759,6 +15774,7 @@ __metadata:
eslint-plugin-react-hooks: ^5.2.0
eslint-plugin-react-refresh: ^0.4.20
globals: ^16.3.0
+ howler: ^2.2.4
react: ^19.1.0
react-countup: ^6.5.3
react-dom: ^19.1.0