diff --git a/apps/client/src/assets/sounds/stop/LICENSE.txt b/apps/client/src/assets/sounds/stop/LICENSE.txt new file mode 100644 index 00000000..5c8d9a2d --- /dev/null +++ b/apps/client/src/assets/sounds/stop/LICENSE.txt @@ -0,0 +1,2 @@ +Sound Effect by irinairinafomicheva from Pixabay +License - https://pixabay.com/service/license-summary/ \ No newline at end of file diff --git a/apps/client/src/assets/sounds/stop/stop-sound.mp3 b/apps/client/src/assets/sounds/stop/stop-sound.mp3 new file mode 100644 index 00000000..26c2ed12 Binary files /dev/null and b/apps/client/src/assets/sounds/stop/stop-sound.mp3 differ diff --git a/apps/client/src/components/atoms/cursor/Cursor.component.tsx b/apps/client/src/components/atoms/cursor/Cursor.component.tsx index d01493c7..a6cb2b59 100644 --- a/apps/client/src/components/atoms/cursor/Cursor.component.tsx +++ b/apps/client/src/components/atoms/cursor/Cursor.component.tsx @@ -1,27 +1,55 @@ import { ReactElement } from 'react'; import cs from 'classnames'; +import { ReactComponent as LockIcon } from 'pixelarticons/svg/lock.svg'; export interface CursorProps { isDebug?: boolean; isAtSpace?: boolean; + isInvalidCursor?: boolean; + isLocked?: boolean; } export function Cursor({ isDebug = false, isAtSpace = false, + isInvalidCursor = false, + isLocked = false, }: CursorProps): ReactElement { return ( - -
+ + + {isLocked && ( +
+ +
+ )} +
+ ); } diff --git a/apps/client/src/constants/index.ts b/apps/client/src/constants/index.ts index 99062daa..8de60957 100644 --- a/apps/client/src/constants/index.ts +++ b/apps/client/src/constants/index.ts @@ -1,4 +1,4 @@ export * as Connection from './connection'; -export * as Race from './race'; +export * as RaceConstants from './race'; export * as TextStyles from './text-styles'; export * as Time from './time'; diff --git a/apps/client/src/constants/race.ts b/apps/client/src/constants/race.ts index 5e355e9e..1cbe4e00 100644 --- a/apps/client/src/constants/race.ts +++ b/apps/client/src/constants/race.ts @@ -1 +1,2 @@ export const MAX_INVALID_CHARS_ALLOWED = 5; +export const MIN_RACE_TRACKS = 5; diff --git a/apps/client/src/main.tsx b/apps/client/src/main.tsx index 3913089d..8dab29c3 100644 --- a/apps/client/src/main.tsx +++ b/apps/client/src/main.tsx @@ -1,16 +1,14 @@ import { StrictMode } from 'react'; import * as ReactDOM from 'react-dom/client'; import { Provider } from 'react-redux'; -import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { store } from '@razor/store'; import './services/socket-communication'; import './i18n'; import './controllers'; -import { NotFound } from './pages/NotFound'; -import { Home, Layout, Leaderboard, Race, Room } from './pages'; import { ToastContextProvider } from './providers'; +import { Router } from './router'; import './styles.css'; @@ -22,20 +20,7 @@ root.render( - - - }> - } /> - - } /> - } /> - } /> - } /> - - - } /> - - + , diff --git a/apps/client/src/pages/race/Race.page.tsx b/apps/client/src/pages/race/Race.page.tsx index a6e60b7d..ec18437d 100644 --- a/apps/client/src/pages/race/Race.page.tsx +++ b/apps/client/src/pages/race/Race.page.tsx @@ -31,6 +31,7 @@ export function Race(): ReactElement { const [raceReadyTime, setRaceReadyTime] = useState(5); const [raceTime, setRaceTime] = useState(0); const selfPlayerId = useRef(getSavedPlayerId()); + const [isTypeLocked, setIsTypeLocked] = useState(true); useEffect(() => { const tournamentId: AppTournamentId = `T:${roomId}`; @@ -102,6 +103,7 @@ export function Race(): ReactElement { const raceTime = game.racesModel[raceId]?.timeoutDuration; setRaceTime(raceTime); sendInitialTypeLog(raceId); + setIsTypeLocked(false); } }, [raceReadyTime, raceId]); @@ -113,7 +115,7 @@ export function Race(): ReactElement { )}> {raceId ? ( -
+ <> {raceReadyTime > 0 ? (
) : null}
0, - })}> -
- + className={cs( + 'flex flex-col items-center justify-center gap-4', + 'scale-50 lg:scale-75 xl:scale-90 2xl:scale-95 ', + { + 'opacity-20': raceReadyTime > 0, + }, + )}> +
+
console.log('time end')} + onTimeEnd={(): void => setIsTypeLocked(true)} />
sendTypeLog(charIndex + 1, raceId) } @@ -154,7 +161,7 @@ export function Race(): ReactElement {
-
+ ) : (

Race not found

)} diff --git a/apps/client/src/pages/race/templates/race-text/RaceText.stories.tsx b/apps/client/src/pages/race/templates/race-text/RaceText.stories.tsx index 23ff2427..d9612a56 100644 --- a/apps/client/src/pages/race/templates/race-text/RaceText.stories.tsx +++ b/apps/client/src/pages/race/templates/race-text/RaceText.stories.tsx @@ -34,6 +34,7 @@ export default { component: RaceText, args: { raceId: testRaceId, + isLocked: false, debug: { enableLetterCount: false, enableSpaceCount: false, diff --git a/apps/client/src/pages/race/templates/race-text/RaceText.template.tsx b/apps/client/src/pages/race/templates/race-text/RaceText.template.tsx index 4ab7f9fe..c98c160f 100644 --- a/apps/client/src/pages/race/templates/race-text/RaceText.template.tsx +++ b/apps/client/src/pages/race/templates/race-text/RaceText.template.tsx @@ -20,10 +20,13 @@ import { Cursor, ToastType, UnderlineCursor } from 'apps/client/src/components'; import { AvatarArray } from 'apps/client/src/components/molecules/avatar-array/AvatarArray.component'; import { MAX_INVALID_CHARS_ALLOWED } from 'apps/client/src/constants/race'; import { useToastContext } from 'apps/client/src/hooks/useToastContext'; +import { AudioManager } from 'apps/client/src/services'; import { getSavedPlayerId } from 'apps/client/src/utils/save-player-id'; import cs from 'classnames'; import { ReactComponent as GamePad } from 'pixelarticons/svg/gamepad.svg'; +import stopSound from '../../../../assets/sounds/stop/stop-sound.mp3'; + import { computeCursorsPerLines, getCursorPositionsWithPlayerAvatars, @@ -35,6 +38,7 @@ import { export interface RaceTextProps { raceId: AppRaceId; + isLocked?: boolean; onValidType: (charIndex: number) => void; debug?: { enableLetterCount?: boolean; @@ -46,6 +50,7 @@ export interface RaceTextProps { export function RaceText({ raceId, + isLocked = false, // eslint-disable-next-line @typescript-eslint/no-empty-function onValidType = (): void => {}, debug = {}, @@ -127,6 +132,12 @@ export function RaceText({ const [otherPlayerCursorsPerLines, updateOtherPlayerCursorsPerLines] = useState([]); + useEffect((): void => { + if (isLocked) { + updateNoOfInvalidChars(0); + } + }, [isLocked]); + useEffect((): void => { if (!raceData || !selfPlayerId.current || !playerIds) { return addToast({ @@ -189,7 +200,14 @@ export function RaceText({ t, ]); + const stopAudioManager = useMemo(() => new AudioManager(stopSound), []); + const handleKeyPressFunction = (char: string): void => { + // When is locked or player finished the race stop accepting input. + if (isLocked || raceText.length === playerCursorAt) { + return; + } + const inputStatus = inputHandler(char, raceText[playerCursorAt]); if (!selfPlayerId.current) { return; @@ -199,10 +217,11 @@ export function RaceText({ if (noOfInvalidChars > 0) { return; } - updatePlayerCursorAt(playerCursorAt + 1); onValidType(playerCursorAt); } else if (inputStatus === InputStatus.INCORRECT) { + stopAudioManager.playAudio(); + updateNoOfInvalidChars(prev => { if (prev === MAX_INVALID_CHARS_ALLOWED) { return MAX_INVALID_CHARS_ALLOWED; @@ -212,6 +231,7 @@ export function RaceText({ } else if (inputStatus === InputStatus.BACKSPACE) { updateNoOfInvalidChars(prev => { if (prev === 0) { + stopAudioManager.playAudio(); return 0; } return prev - 1; @@ -280,6 +300,10 @@ export function RaceText({ charIndex, { cursorAt: playerCursorAt }, ); + const isCursorAtInvalidCursor = indexConverter.isCursorAtChar( + charIndex, + { cursorAt: invalidCursorAt }, + ); const isLetterBehindCursor = indexConverter.isCharBehindCursor( charIndex, @@ -295,16 +319,32 @@ export function RaceText({ indexConverter.isCursorAtChar(charIndex, { cursorsAt: otherPlayerCursors, }); + /* Show normal cursor if no invalid chars */ + const isVisibleRegularCursor = + (noOfInvalidChars === 0 || isLocked) && isCursorAtLetter; + /* Show invalid cursor if invalid chars */ + const isVisibleInvalidCursor = + noOfInvalidChars > 0 && + !isLocked && + isCursorAtInvalidCursor; return ( - {isCursorAtLetter ? : null} + {isVisibleRegularCursor ? ( + + ) : null} + {isVisibleInvalidCursor ? ( + + ) : null} {isOtherPlayerCursorsOnLetter ? ( ) : null} diff --git a/apps/client/src/pages/race/templates/race-text/utils/key-press-listener.ts b/apps/client/src/pages/race/templates/race-text/utils/key-press-listener.ts index 791db3b5..6f364fd3 100644 --- a/apps/client/src/pages/race/templates/race-text/utils/key-press-listener.ts +++ b/apps/client/src/pages/race/templates/race-text/utils/key-press-listener.ts @@ -10,8 +10,6 @@ export function useKeyPress( const simpleLetters = 'abcdefghijklmnopqrstuvwxyz'.split(''); const capitalLetters = simpleLetters.map(letter => letter.toUpperCase()); - // Keycodes: KeyA, KeyB,... - const keyCodes = capitalLetters.map(letter => `Key${letter}`); const handleKeyPress = (e: KeyboardEvent): void => { // If event target on a input, don't do anything; else, prevent default @@ -19,48 +17,43 @@ export function useKeyPress( if (e.target instanceof HTMLInputElement) { return; } + + if (e.ctrlKey) { + return; + } e.preventDefault(); - if (e.code === 'Space') { + if (e.key === ' ') { return inputHandler('SPACE'); } - if (e.code === 'Backspace') { + if (e.key === 'Backspace') { return inputHandler('BACKSPACE'); } // Special chars - if (e.code === 'Comma') { + if (e.key === ',') { return inputHandler(','); } - - if (e.code === 'Period') { + if (e.key === '.') { return inputHandler('.'); } - - if (e.code === 'Quote') { + if (e.key === "'") { return inputHandler("'"); } - if (e.key === '?') { return inputHandler('?'); } - if (e.key === '!') { return inputHandler('!'); } - // t f => t - // f t => t - // f f => f - // t t => f - // Not equal act as XOR here - const isCapital = - e.getModifierState('CapsLock') !== e.getModifierState('Shift'); - - // If key code between 65 and 90, it's a letter, input correct letter based on caps lock - for (let i = 0; i < keyCodes.length; i++) { - if (e.code === keyCodes[i]) { - return inputHandler(isCapital ? capitalLetters[i] : simpleLetters[i]); + // If key is a letter, return the letter + for (let i = 0; i < simpleLetters.length; i++) { + if (e.key === simpleLetters[i]) { + return inputHandler(simpleLetters[i]); + } + if (e.key === capitalLetters[i]) { + return inputHandler(capitalLetters[i]); } } }; diff --git a/apps/client/src/pages/race/templates/race-view/RaceTrack.template.tsx b/apps/client/src/pages/race/templates/race-view/RaceTrack.template.tsx index a97af843..e2524362 100644 --- a/apps/client/src/pages/race/templates/race-view/RaceTrack.template.tsx +++ b/apps/client/src/pages/race/templates/race-view/RaceTrack.template.tsx @@ -1,8 +1,9 @@ -import { ReactElement } from 'react'; +import { ReactElement, useRef } from 'react'; import { useSelector } from 'react-redux'; import { AppPlayerId, AppRaceId } from '@razor/models'; import { RootState } from '@razor/store'; -import { extractId, ExtractIdType } from '@razor/util'; +import { useComputeMaxRaceTracks } from 'apps/client/src/utils/custom-hooks/compute-max-race-tracks'; +import { getSavedPlayerId } from 'apps/client/src/utils/save-player-id'; import cs from 'classnames'; import { @@ -20,34 +21,57 @@ export interface RaceTrackProps { export function RaceTrack({ raceId, className }: RaceTrackProps): ReactElement { const game = useSelector((store: RootState) => store.game); - const tournamentId = extractId( - raceId, - ExtractIdType.Race, - ExtractIdType.Tournament, - ); + const selfPlayerId = useRef(getSavedPlayerId()); - // FIXME: should take player ids from the race not the tournament - const playerIds = game.tournamentsModel[tournamentId]?.playerIds; + const racePlayers = game.racesModel[raceId]?.players; + const playerIds = (Object.keys(racePlayers) || []) as AppPlayerId[]; const textLength = game.racesModel[raceId].text.length; const lineHeight = getRaceTrackRowColumnSizes(); - const pavementHeight = - getRaceTrackPavementRows(playerIds.length) * lineHeight; + const maxRaceTracksCount = useComputeMaxRaceTracks(); + const tracksCount = Math.min(maxRaceTracksCount, playerIds.length); + const pavementHeight = getRaceTrackPavementRows(tracksCount) * lineHeight; return (
+ {/* Self player line at top */} + {selfPlayerId.current ? ( +
+ +
+ ) : null}
{playerIds.map((playerId: AppPlayerId) => { const color = carColors[playerIds.indexOf(playerId) % carColors.length]; + + // Skip self player + if (playerId === selfPlayerId.current) { + return null; + } + + // Skip overflowing player lines + if (playerIds.indexOf(playerId) >= tracksCount) { + return null; + } + return (
; + return ( +
+ +
+ ); } diff --git a/apps/client/src/pages/race/templates/race-view/race-line/RaceLine.template.tsx b/apps/client/src/pages/race/templates/race-view/race-line/RaceLine.template.tsx index c755daf0..3099d64d 100644 --- a/apps/client/src/pages/race/templates/race-view/race-line/RaceLine.template.tsx +++ b/apps/client/src/pages/race/templates/race-view/race-line/RaceLine.template.tsx @@ -2,6 +2,10 @@ import { ReactElement } from 'react'; import { useSelector } from 'react-redux'; import { AppPlayerId, AppPlayerLogId, AppRaceId } from '@razor/models'; import { RootState } from '@razor/store'; +import { Text } from 'apps/client/src/components'; +import { TextSize, TextType } from 'apps/client/src/models'; +import { useComputePlayerRanks } from 'apps/client/src/utils/custom-hooks/compute-player-rank'; +import cs from 'classnames'; import { getCarComponentSize, @@ -28,6 +32,9 @@ export function RaceLine({ }: RaceLineProps): ReactElement | null { const playerLogId: AppPlayerLogId = `${raceId}-${playerId}`; const game = useSelector((store: RootState) => store.game); + const playersModel = game.playersModel; + const playerName = playersModel[playerId]?.name || 'Unknown'; + const [playerRanks] = useComputePlayerRanks(raceId); const playerLogs = game.playerLogsModel[playerLogId]; if (!playerLogs) { @@ -57,13 +64,30 @@ export function RaceLine({ className='relative mx-auto' style={{ width: `${raceTrackWidth}px`, height: `${lineHeight}px` }}>
- + + {`${playerRanks[playerId]}`} + +
+ + + {playerName} + +
); diff --git a/apps/client/src/router.tsx b/apps/client/src/router.tsx new file mode 100644 index 00000000..db51397c --- /dev/null +++ b/apps/client/src/router.tsx @@ -0,0 +1,36 @@ +import { ReactElement } from 'react'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; + +import { NotFound } from './pages/NotFound'; +import { GuardedRoute } from './utils/guardedRoute'; +import { Home, Layout, Leaderboard, Race, Room } from './pages'; + +export function Router(): ReactElement { + return ( + + + }> + } /> + + } /> + } + /> + } + /> + + } + /> + + + } /> + + + ); +} diff --git a/apps/client/src/services/audio-manager.ts b/apps/client/src/services/audio-manager.ts new file mode 100644 index 00000000..47286bad --- /dev/null +++ b/apps/client/src/services/audio-manager.ts @@ -0,0 +1,74 @@ +interface AudioInstance { + instance: HTMLAudioElement; + lastPlayedTimestamp: number; +} + +export class AudioManager { + /** Link of the audio */ + private audioLink: string; + /** Circular dynamic array containing audios */ + private audioCircularArray: AudioInstance[] = []; + /** Pointer to the next audio to be played */ + private nextPointer = 0; + /** Number of initial audio instances to be created */ + private initialAudioCount = 2; + + constructor(link: string) { + this.audioLink = link; + this.initializeAudioInstances(); + } + + /** Update the pointer to the next audio instance in the circular array */ + private updatePointer(): void { + this.nextPointer = (this.nextPointer + 1) % this.audioCircularArray.length; + } + + /** Update pointer to oldest instance */ + private updatePointerToOldest(): void { + const oldestAudio = this.audioCircularArray.reduce((prev, curr) => + prev.lastPlayedTimestamp < curr.lastPlayedTimestamp ? prev : curr, + ); + this.nextPointer = this.audioCircularArray.indexOf(oldestAudio); + } + + /** Create a new audio instance and push it to the list */ + private createNewAudio(): HTMLAudioElement { + const audio = new Audio(this.audioLink); + this.audioCircularArray.push({ + instance: audio, + lastPlayedTimestamp: Date.now(), + }); + return audio; + } + + /** Create few Audio instances at the beginning and push them to the list */ + private initializeAudioInstances(): void { + for (let i = 0; i < this.initialAudioCount; i++) { + this.createNewAudio(); + } + } + + /** Get the next audio instance to be played. + * If the audio instance is already playing, + * create a new one and set the pointer to oldest audio instance. + */ + private getNextAudioInstance(): HTMLAudioElement { + const audioInstance = this.audioCircularArray[this.nextPointer]; + + // If audio is instance is paused means it is just created or it played completely, + // as we don't pause the audio purposely. + if (audioInstance.instance.paused) { + audioInstance.lastPlayedTimestamp = Date.now(); + this.updatePointer(); + return audioInstance.instance; + } + + this.updatePointerToOldest(); + return this.createNewAudio(); + } + + public playAudio(): void { + const audio = this.getNextAudioInstance(); + audio.play(); + } +} diff --git a/apps/client/src/services/handlers/race-timeout.ts b/apps/client/src/services/handlers/race-timeout.ts new file mode 100644 index 00000000..25117ab1 --- /dev/null +++ b/apps/client/src/services/handlers/race-timeout.ts @@ -0,0 +1,13 @@ +import { AllProtocolToTypeMap, SocketProtocols } from '@razor/models'; + +import { socket } from '../socket-communication'; + +export function raceTimeout(): void { + type TimeoutData = AllProtocolToTypeMap[SocketProtocols.InformTimeout]; + const data: TimeoutData = { + timestamp: Date.now(), + }; + socket.emit(SocketProtocols.InformTimeout, data); + + // store end race should be called after server notifies the timeout +} diff --git a/apps/client/src/services/index.ts b/apps/client/src/services/index.ts index 67b02ff8..7e914125 100644 --- a/apps/client/src/services/index.ts +++ b/apps/client/src/services/index.ts @@ -1 +1,2 @@ +export * from './audio-manager'; export * from './socket-communication'; diff --git a/apps/client/src/utils/custom-hooks/compute-max-race-tracks.ts b/apps/client/src/utils/custom-hooks/compute-max-race-tracks.ts new file mode 100644 index 00000000..e8baa6a4 --- /dev/null +++ b/apps/client/src/utils/custom-hooks/compute-max-race-tracks.ts @@ -0,0 +1,30 @@ +import { useEffect, useState } from 'react'; + +import { RaceConstants } from '../../constants'; + +/** Compute max race tracks count on window height change */ +export function useComputeMaxRaceTracks(): number { + const [windowHeight, setWindowHeight] = useState(window.innerHeight); + + useEffect(() => { + window.addEventListener('resize', () => { + setWindowHeight(window.innerHeight); + }); + + return () => { + window.removeEventListener('resize', () => { + setWindowHeight(window.innerHeight); + }); + }; + }, []); + + const maxRaceTracks = RaceConstants.MIN_RACE_TRACKS; + const approxAllocatableRaceTrackHeight = windowHeight * 0.35; + if (approxAllocatableRaceTrackHeight > 300) { + const freeHeight = approxAllocatableRaceTrackHeight - 300; + const additionalRaceTracks = Math.floor(freeHeight / 15); // Considering one row takes 20px + return additionalRaceTracks + maxRaceTracks; + } + + return maxRaceTracks; +} diff --git a/apps/client/src/utils/custom-hooks/compute-player-rank.ts b/apps/client/src/utils/custom-hooks/compute-player-rank.ts new file mode 100644 index 00000000..65708797 --- /dev/null +++ b/apps/client/src/utils/custom-hooks/compute-player-rank.ts @@ -0,0 +1,52 @@ +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppPlayerId, AppPlayerLogId, AppRaceId } from '@razor/models'; +import { RootState } from '@razor/store'; + +type PlayerCursorPos = Record; +type PlayerRank = Record; + +export function useComputePlayerRanks(raceId: AppRaceId): PlayerRank[] { + const [initialRaceId] = useState(raceId); + const game = useSelector((store: RootState) => store.game); + const race = game.racesModel[initialRaceId]; + const playerLogsModel = game.playerLogsModel; + + const [playerRanks, setPlayerRanks] = useState({}); + + useEffect(() => { + const playersIds = Object.keys(race.players) as AppPlayerId[]; + const playerCursorPos: PlayerCursorPos = {}; + // Extract last recorded char index for each player + playersIds.forEach(playerId => { + const playerLogsId: AppPlayerLogId = `${initialRaceId}-${playerId}`; + const playerLogs = playerLogsModel[playerLogsId]; + const lastRecordedCharIndex = playerLogs?.length + ? playerLogs[playerLogs.length - 1].textLength + : 0; + playerCursorPos[playerId] = lastRecordedCharIndex; + }); + + // Sort players by their last recorded char index + const sortedPlayersIds = playersIds.sort( + (a, b) => playerCursorPos[b] - playerCursorPos[a], + ); + + // Assign ranks to players, same rank for players in the same position + const playerRanks: PlayerRank = {}; + let rank = 1; + let prevPlayerCursorPos = playerCursorPos[sortedPlayersIds[0]]; + sortedPlayersIds.forEach(playerId => { + const cursorPos = playerCursorPos[playerId]; + if (cursorPos < prevPlayerCursorPos) { + rank++; + } + playerRanks[playerId] = rank; + prevPlayerCursorPos = cursorPos; + }); + + setPlayerRanks(playerRanks); + }, [playerLogsModel]); + + return [playerRanks]; +} diff --git a/apps/client/src/utils/custom-hooks/index.ts b/apps/client/src/utils/custom-hooks/index.ts new file mode 100644 index 00000000..f40600e5 --- /dev/null +++ b/apps/client/src/utils/custom-hooks/index.ts @@ -0,0 +1 @@ +export * from './compute-max-race-tracks'; diff --git a/apps/client/src/utils/guardedRoute.tsx b/apps/client/src/utils/guardedRoute.tsx new file mode 100644 index 00000000..20dc3b4c --- /dev/null +++ b/apps/client/src/utils/guardedRoute.tsx @@ -0,0 +1,22 @@ +import { ReactElement } from 'react'; +import { Navigate, useParams } from 'react-router-dom'; + +import { getSavedPlayerId } from './save-player-id'; + +interface GuardedRouteProps { + component: React.ElementType; + path: string; +} + +export function GuardedRoute({ + component: Component, +}: GuardedRouteProps): ReactElement | null { + const playerId = getSavedPlayerId(); + const { roomId } = useParams(); + + if (!playerId) { + return ; + } + + return ; +} diff --git a/apps/client/src/utils/index.ts b/apps/client/src/utils/index.ts new file mode 100644 index 00000000..7d09fbb5 --- /dev/null +++ b/apps/client/src/utils/index.ts @@ -0,0 +1 @@ +export * from './custom-hooks'; diff --git a/libs/constants/src/lib/constants.ts b/libs/constants/src/lib/constants.ts index c4525ba7..50e9aa19 100644 --- a/libs/constants/src/lib/constants.ts +++ b/libs/constants/src/lib/constants.ts @@ -3,7 +3,7 @@ * Use by, * - compute timeout util function */ -export const AVERAGE_WPM = 50; +export const AVERAGE_WPM = 40; /** Number of chars to create the unique part of the general ID. * diff --git a/libs/models/src/lib/sockets/protocol-data.ts b/libs/models/src/lib/sockets/protocol-data.ts index e54a65b8..7562529c 100644 --- a/libs/models/src/lib/sockets/protocol-data.ts +++ b/libs/models/src/lib/sockets/protocol-data.ts @@ -4,6 +4,7 @@ import { AppStateModel } from '../state'; import { PlayerId } from './player'; import { + informTimeoutSchema, initialClientDataSchema, playerJoinSchema, sendTypeLogSchema, @@ -49,3 +50,5 @@ export type StartRaceAcceptData = z.infer; export type SendTypeLogData = z.infer; export type UpdateTypeLogsData = z.infer; + +export type InformTimeoutData = z.infer; diff --git a/libs/models/src/lib/sockets/protocol-schemas.ts b/libs/models/src/lib/sockets/protocol-schemas.ts index 93904e08..ee1833ea 100644 --- a/libs/models/src/lib/sockets/protocol-schemas.ts +++ b/libs/models/src/lib/sockets/protocol-schemas.ts @@ -70,6 +70,13 @@ export const updateTypeLogsSchema = z.object({ playerLogs: playerLogsCollectionSchema, }); +/** + * Related protocol - {@link SocketProtocols.InformTimeout} + */ +export const informTimeoutSchema = z.object({ + timestamp: z.number(), +}); + export type ProtocolSchemaTypes = | typeof authTokenTransferSchema | typeof initialClientDataSchema @@ -78,4 +85,5 @@ export type ProtocolSchemaTypes = | typeof startRaceRequestSchema | typeof startRaceAcceptSchema | typeof sendTypeLogSchema - | typeof updateTypeLogsSchema; + | typeof updateTypeLogsSchema + | typeof informTimeoutSchema; diff --git a/libs/models/src/lib/sockets/protocol-to-schema-map.ts b/libs/models/src/lib/sockets/protocol-to-schema-map.ts index 165511d5..d4d2b593 100644 --- a/libs/models/src/lib/sockets/protocol-to-schema-map.ts +++ b/libs/models/src/lib/sockets/protocol-to-schema-map.ts @@ -1,5 +1,6 @@ import { authTokenTransferSchema, + informTimeoutSchema, initialClientDataSchema, initialServerDataSchema, playerJoinSchema, @@ -26,4 +27,5 @@ export const protocolToSchemaMap = new Map< [SocketProtocols.StartRaceAccept, startRaceAcceptSchema], [SocketProtocols.SendTypeLog, sendTypeLogSchema], [SocketProtocols.UpdateTypeLogs, updateTypeLogsSchema], + [SocketProtocols.InformTimeout, informTimeoutSchema], ]); diff --git a/libs/models/src/lib/sockets/protocol-to-type-map.ts b/libs/models/src/lib/sockets/protocol-to-type-map.ts index 66b87d80..aeb44057 100644 --- a/libs/models/src/lib/sockets/protocol-to-type-map.ts +++ b/libs/models/src/lib/sockets/protocol-to-type-map.ts @@ -1,4 +1,5 @@ import { + InformTimeoutData, InitialClientData, InitialServerData, PlayerJoinData, @@ -23,6 +24,7 @@ export interface OtherProtocolToTypeMap extends Record { [SocketProtocols.StartRaceAccept]: StartRaceAcceptData; [SocketProtocols.SendTypeLog]: SendTypeLogData; [SocketProtocols.UpdateTypeLogs]: UpdateTypeLogsData; + [SocketProtocols.InformTimeout]: InformTimeoutData; } export type AllProtocolToTypeMap = InitialProtocolToTypeMap & diff --git a/libs/util/src/lib/compute-race-duration.spec.ts b/libs/util/src/lib/compute-race-duration.spec.ts index f93f4eed..259dbc43 100644 --- a/libs/util/src/lib/compute-race-duration.spec.ts +++ b/libs/util/src/lib/compute-race-duration.spec.ts @@ -5,8 +5,8 @@ import { computeRaceDuration } from './compute-race-duration'; describe('[Utils] computeRaceDuration', () => { it.each([ - [M_RACE_TEXT0, 159], - [M_RACE_TEXT1, 135], + [M_RACE_TEXT0, 198], + [M_RACE_TEXT1, 168], ])('Calculate timeout timer', (text, time) => { const timeoutDuration = computeRaceDuration(text); expect(timeoutDuration).toEqual(time);