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