diff --git a/packages/webapp-next/assets/sounds/correct.wav b/packages/webapp-next/assets/sounds/correct.wav new file mode 100644 index 0000000..ff43ffc Binary files /dev/null and b/packages/webapp-next/assets/sounds/correct.wav differ diff --git a/packages/webapp-next/assets/sounds/error.wav b/packages/webapp-next/assets/sounds/error.wav new file mode 100644 index 0000000..7188d56 Binary files /dev/null and b/packages/webapp-next/assets/sounds/error.wav differ diff --git a/packages/webapp-next/common/components/overlays/SettingsOverlay.tsx b/packages/webapp-next/common/components/overlays/SettingsOverlay.tsx index d81e309..0077846 100644 --- a/packages/webapp-next/common/components/overlays/SettingsOverlay.tsx +++ b/packages/webapp-next/common/components/overlays/SettingsOverlay.tsx @@ -9,6 +9,7 @@ import { import { toggleDefaultRaceIsPublic, toggleSyntaxHighlightning, + toggleSound, useSettingsStore, } from "../../../modules/play2/state/settings-store"; import { Overlay } from "../Overlay"; @@ -24,6 +25,7 @@ export const SettingsOverlay: React.FC = ({ (s) => s.syntaxHighlighting ); const isPublicRaceByDefault = useSettingsStore((s) => s.defaultIsPublic); + const isSoundEnabled = useSettingsStore((s) => s.sound); return ( <> @@ -50,6 +52,12 @@ export const SettingsOverlay: React.FC = ({ checked={isPublicRaceByDefault} toggleEnabled={toggleDefaultRaceIsPublic} /> + diff --git a/packages/webapp-next/modules/play2/components/HiddenCodeInput.tsx b/packages/webapp-next/modules/play2/components/HiddenCodeInput.tsx index 928f315..857316b 100644 --- a/packages/webapp-next/modules/play2/components/HiddenCodeInput.tsx +++ b/packages/webapp-next/modules/play2/components/HiddenCodeInput.tsx @@ -7,9 +7,17 @@ import { useState, } from "react"; -import { isSkippable, useCodeStore } from "../state/code-store"; +import { isSkippable, useCodeStore, KeyStroke } from "../state/code-store"; import { useCanType, useGameStore } from "../state/game-store"; +import { useSettingsStore } from "../state/settings-store"; +import { useSound } from "use-sound"; + +// Source: "type writing" by Pixabay - https://pixabay.com/sound-effects/type-writing-6834/ +import correctSfx from "../../../assets/sounds/correct.wav"; +// Source: "Dramatic Guitar" by UNIVERSFIELD - https://pixabay.com/sound-effects/dramatic-guitar-140614/ +import errorSfx from "../../../assets/sounds/error.wav"; + interface HiddenCodeInputProps { hide: boolean; // Used for debugging the input disabled: boolean; @@ -57,6 +65,17 @@ export const HiddenCodeInput = ({ // which gets code.substr(0, correctIndex) const [input, setInput] = useState(""); + const withSound = useSettingsStore((state) => state.sound); + const [playCorrectSfx] = useSound(correctSfx); + const [playErrorSfx] = useSound(errorSfx); + const playSound = (keyStroke: KeyStroke) => { + if (keyStroke.correct) { + playCorrectSfx(); + } else { + playErrorSfx(); + } + }; + function handleOnChange(e: ChangeEvent) { // TODO: use e.isTrusted if (!canType) return; @@ -74,6 +93,9 @@ export const HiddenCodeInput = ({ if (isSkippable(char)) continue; const keyPress = keyPressFactory(char); handleKeyPress(keyPress); + if (withSound) { + playSound(keyPress); + } game.sendKeyStroke(keyPress); } } diff --git a/packages/webapp-next/modules/play2/state/settings-store.ts b/packages/webapp-next/modules/play2/state/settings-store.ts index cfabb0c..e5f17d3 100644 --- a/packages/webapp-next/modules/play2/state/settings-store.ts +++ b/packages/webapp-next/modules/play2/state/settings-store.ts @@ -18,6 +18,7 @@ export interface SettingsState { syntaxHighlighting: boolean; raceIsPublic: boolean; defaultIsPublic: boolean; + sound: boolean; } const SYNTAX_HIGHLIGHTING_KEY = "syntaxHighlighting"; @@ -28,6 +29,8 @@ const DEFAULT_RACE_IS_PUBLIC_KEY = "defaultRaceIsPublic2"; const LANGUAGE_KEY = "language"; +const SOUND_KEY = "sound"; + function getInitialToggleStateFromLocalStorage( key: string, defaultToggleValue: boolean @@ -76,6 +79,7 @@ export const useSettingsStore = create((_set, _get) => ({ false ), languageSelected: getInitialLanguageFromLocalStorage(LANGUAGE_KEY), + sound: getInitialToggleStateFromLocalStorage(SOUND_KEY, false), })); export const setCaretType = (caretType: "smooth" | "block") => { @@ -112,6 +116,14 @@ export const toggleSyntaxHighlightning = () => { useSettingsStore.setState((state) => ({ ...state, syntaxHighlighting })); }; +export const toggleSound = () => { + const soundStr = localStorage.getItem(SOUND_KEY); + let sound = soundStr === "true"; + sound = !sound; + localStorage.setItem(SOUND_KEY, sound.toString()); + useSettingsStore.setState((state) => ({ ...state, sound })); +}; + export const openSettingsModal = () => { if (useSettingsStore.getState().profileModalIsOpen) return; if (useSettingsStore.getState().leaderboardModalIsOpen) return; diff --git a/packages/webapp-next/next.config.js b/packages/webapp-next/next.config.js index 7cfa204..78f8b0e 100644 --- a/packages/webapp-next/next.config.js +++ b/packages/webapp-next/next.config.js @@ -24,6 +24,22 @@ const nextConfig = { ? "https://v3.speedtyper.dev" : "http://localhost:1337", }, + webpack: (config, options) => { + // WAV file + config.module.rules.push({ + test: /\.(wav)$/, + use: { + loader: "file-loader", + options: { + name: "[name].[ext]", + publicPath: "/_next/static/sounds/", + outputPath: `${options.isServer ? "../" : ""}static/sounds/`, + }, + }, + }); + + return config; + } }; module.exports = nextConfig; diff --git a/packages/webapp-next/package.json b/packages/webapp-next/package.json index 4ae9a8a..e2a77b8 100644 --- a/packages/webapp-next/package.json +++ b/packages/webapp-next/package.json @@ -36,6 +36,7 @@ "socket.io-client": "^2", "socketio-latest": "npm:socket.io-client", "swr": "^2.0.3", + "use-sound": "^4.0.1", "zustand": "^4.1.1" }, "devDependencies": { @@ -48,6 +49,7 @@ "eslint-config-next": "12.2.5", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", + "file-loader": "^6.2.0", "postcss": "^8.4.16", "postcss-cli": "^10.0.0", "prettier": "2.7.1", diff --git a/packages/webapp-next/types/assets.d.ts b/packages/webapp-next/types/assets.d.ts new file mode 100644 index 0000000..9e7051b --- /dev/null +++ b/packages/webapp-next/types/assets.d.ts @@ -0,0 +1,4 @@ +declare module "*.wav" { + const src: string; + export default src; +} diff --git a/packages/webapp-next/yarn.lock b/packages/webapp-next/yarn.lock index df96b84..20cc2e2 100644 --- a/packages/webapp-next/yarn.lock +++ b/packages/webapp-next/yarn.lock @@ -281,6 +281,11 @@ dependencies: tslib "^2.4.0" +"@types/json-schema@^7.0.8": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" @@ -412,7 +417,12 @@ after@0.8.2: resolved "https://registry.npmjs.org/after/-/after-0.8.2.tgz" integrity sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA== -ajv@^6.10.0, ajv@^6.12.4: +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -548,6 +558,11 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" @@ -890,6 +905,11 @@ emoji-regex@^9.2.2: resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -1278,6 +1298,14 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" +file-loader@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" @@ -1556,6 +1584,11 @@ highlight.js@^11.6.0: resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-11.6.0.tgz" integrity sha512-ig1eqDzJaB0pqEvlPVIpSSyMaO92bH1N2rJpLMN/nX396wTpDA4Eq0uK+7I/2XG17pFaaKE0kjV/XPeGt7Evjw== +howler@^2.1.3: + version "2.2.4" + resolved "https://registry.yarnpkg.com/howler/-/howler-2.2.4.tgz#bd3df4a4f68a0118a51e4bd84a2bfc2e93e6e5a1" + integrity sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w== + ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -1766,6 +1799,11 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +json5@^2.1.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" @@ -1821,6 +1859,15 @@ lilconfig@^2.0.5, lilconfig@^2.0.6: resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz" integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== +loader-utils@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" @@ -2471,6 +2518,15 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" +schema-utils@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + semver@^6.3.0: version "6.3.0" resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" @@ -2859,6 +2915,13 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-sound@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/use-sound/-/use-sound-4.0.1.tgz#8c8de4badd16902db7762fd9418a4283b9bf7b3e" + integrity sha512-hykJ86kNcu6y/FzlSHcQxhjSGMslZx2WlfLpZNoPbvueakv4OF3xPxEtGV2YmculrIaH0tPp9LtG4jgy17xMWg== + dependencies: + howler "^2.1.3" + use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"