diff --git a/client/src/config/globals.js b/client/src/config/globals.js index 0c9cf28..98dc8da 100644 --- a/client/src/config/globals.js +++ b/client/src/config/globals.js @@ -8,6 +8,8 @@ export const PRIMITIVES = { MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 1000, TOAST_DURATION_DEFAULT: 6, ACCESS_CODE_LENGTH: 4, + MAX_MINUTES: 59, + MAX_HOURS: 5, PLAYER_ID_COOKIE_KEY: 'play-werewolf-anon-id' }; @@ -75,6 +77,7 @@ export const EVENT_IDS = { ASSIGN_DEDICATED_MOD: 'assignDedicatedMod', KICK_PERSON: 'kickPerson', UPDATE_GAME_ROLES: 'updateGameRoles', + UPDATE_GAME_TIMER: 'updateGameTimer', LEAVE_ROOM: 'leaveRoom' }; diff --git a/client/src/modules/front_end_components/HTMLFragments.js b/client/src/modules/front_end_components/HTMLFragments.js index 09f9c2e..2f6ff18 100644 --- a/client/src/modules/front_end_components/HTMLFragments.js +++ b/client/src/modules/front_end_components/HTMLFragments.js @@ -46,6 +46,7 @@ export const HTMLFragments = { `, START_GAME_PROMPT: ` + `, LEAVE_GAME_PROMPT: '', @@ -58,6 +59,11 @@ export const HTMLFragments = { `, + TIMER_EDIT_BUTTONS: + ` + `, PLAYER_GAME_VIEW: `
diff --git a/client/src/modules/game_creation/GameCreationStepManager.js b/client/src/modules/game_creation/GameCreationStepManager.js index ec2e473..2de6cec 100644 --- a/client/src/modules/game_creation/GameCreationStepManager.js +++ b/client/src/modules/game_creation/GameCreationStepManager.js @@ -53,16 +53,10 @@ export class GameCreationStepManager { if (e.type === 'click' || e.code === 'Enter') { let hours = parseInt(document.getElementById('game-hours').value); let minutes = parseInt(document.getElementById('game-minutes').value); - hours = isNaN(hours) ? null : hours; - minutes = isNaN(minutes) ? null : minutes; - if ((hours === null && minutes === null) - || (hours === null && minutes > 0 && minutes < 60) - || (minutes === null && hours > 0 && hours < 6) - || (hours === 0 && minutes > 0 && minutes < 60) - || (minutes === 0 && hours > 0 && hours < 6) - || (hours > 0 && hours < 6 && minutes >= 0 && minutes < 60) - ) { - if (hasTimer(hours, minutes)) { + hours = this.standardizeNumberInput(hours); + minutes = this.standardizeNumberInput(minutes) + if (this.timerIsValid(hours, minutes)) { + if (this.hasTimer(hours, minutes)) { this.currentGame.hasTimer = true; this.currentGame.timerParams = { hours: hours, @@ -77,11 +71,7 @@ export class GameCreationStepManager { this.incrementStep(); this.renderStep('creation-step-container', this.step); } else { - if (hours === 0 && minutes === 0) { - toast('You must enter a non-zero amount of time.', 'error', true); - } else { - toast('Invalid timer options. Hours can be a max of 5, Minutes a max of 59.', 'error', true); - } + toast('Invalid timer options. Hours can be a max of 5, Minutes a max of 59.', 'error', true); } } }, @@ -198,7 +188,7 @@ export class GameCreationStepManager { showButtons(true, true, this.steps[step].forwardHandler, this.steps[step].backHandler); break; case 3: - renderTimerStep(containerId, step, this.currentGame, this.steps); + this.renderTimerStep(containerId, step, this.currentGame, this.steps); showButtons(true, true, this.steps[step].forwardHandler, this.steps[step].backHandler); break; case 4: @@ -297,6 +287,68 @@ export class GameCreationStepManager { initializeRemainingEventListeners(this.deckManager, this.roleBox); }; + + renderTimerStep (containerId, stepNumber, game, steps) { + const div = document.createElement('div'); + div.setAttribute('id', 'step-' + stepNumber); + div.classList.add('step'); + + const timeContainer = document.createElement('div'); + timeContainer.setAttribute('id', 'game-time'); + + const hoursDiv = document.createElement('div'); + const hoursLabel = document.createElement('label'); + hoursLabel.setAttribute('for', 'game-hours'); + hoursLabel.innerText = 'Hours'; + const hours = document.createElement('input'); + hours.addEventListener('keyup', steps[stepNumber].forwardHandler); + setAttributes(hours, { type: 'number', id: 'game-hours', name: 'game-hours', min: '0', max: '5', value: game.timerParams?.hours }); + + const minutesDiv = document.createElement('div'); + const minsLabel = document.createElement('label'); + minsLabel.setAttribute('for', 'game-minutes'); + minsLabel.innerText = 'Minutes'; + const minutes = document.createElement('input'); + minutes.addEventListener('keyup', steps[stepNumber].forwardHandler); + setAttributes(minutes, { type: 'number', id: 'game-minutes', name: 'game-minutes', min: '1', max: '60', value: game.timerParams?.minutes }); + + hoursDiv.appendChild(hoursLabel); + hoursDiv.appendChild(hours); + minutesDiv.appendChild(minsLabel); + minutesDiv.appendChild(minutes); + timeContainer.appendChild(hoursDiv); + timeContainer.appendChild(minutesDiv); + div.appendChild(timeContainer); + + document.getElementById(containerId).appendChild(div); + } + + timerIsValid(hours, minutes) { + let valid = true; + + if (hours === null && minutes === null) { + return valid; + } + + if (hours !== null) { + valid = hours <= PRIMITIVES.MAX_HOURS; + } + + if (minutes !== null) { + valid = minutes <= PRIMITIVES.MAX_MINUTES; + } + + + return valid; + } + + hasTimer(hours, minutes) { + return hours !== null || minutes !== null; + } + + standardizeNumberInput(input) { + return (isNaN(input) || input === 0) ? null : input; + } } function renderNameStep (containerId, step, game, steps) { @@ -358,41 +410,6 @@ function renderModerationTypeStep (game, containerId, stepNumber) { document.getElementById(containerId).appendChild(stepContainer); } -function renderTimerStep (containerId, stepNumber, game, steps) { - const div = document.createElement('div'); - div.setAttribute('id', 'step-' + stepNumber); - div.classList.add('step'); - - const timeContainer = document.createElement('div'); - timeContainer.setAttribute('id', 'game-time'); - - const hoursDiv = document.createElement('div'); - const hoursLabel = document.createElement('label'); - hoursLabel.setAttribute('for', 'game-hours'); - hoursLabel.innerText = 'Hours'; - const hours = document.createElement('input'); - hours.addEventListener('keyup', steps[stepNumber].forwardHandler); - setAttributes(hours, { type: 'number', id: 'game-hours', name: 'game-hours', min: '0', max: '5', value: game.timerParams?.hours }); - - const minutesDiv = document.createElement('div'); - const minsLabel = document.createElement('label'); - minsLabel.setAttribute('for', 'game-minutes'); - minsLabel.innerText = 'Minutes'; - const minutes = document.createElement('input'); - minutes.addEventListener('keyup', steps[stepNumber].forwardHandler); - setAttributes(minutes, { type: 'number', id: 'game-minutes', name: 'game-minutes', min: '1', max: '60', value: game.timerParams?.minutes }); - - hoursDiv.appendChild(hoursLabel); - hoursDiv.appendChild(hours); - minutesDiv.appendChild(minsLabel); - minutesDiv.appendChild(minutes); - timeContainer.appendChild(hoursDiv); - timeContainer.appendChild(minutesDiv); - div.appendChild(timeContainer); - - document.getElementById(containerId).appendChild(div); -} - function renderReviewAndCreateStep (containerId, stepNumber, game, deckManager) { const div = document.createElement('div'); div.setAttribute('id', 'step-' + stepNumber); @@ -591,10 +608,6 @@ function processNewCustomRoleSubmission (name, description, team, deckManager, i } } -function hasTimer (hours, minutes) { - return hours !== null || minutes !== null; -} - function validateName (name) { return typeof name === 'string' && name.length > 0 && name.length <= PRIMITIVES.MAX_PERSON_NAME_LENGTH; } diff --git a/client/src/modules/game_state/states/Lobby.js b/client/src/modules/game_state/states/Lobby.js index e6dabbd..fada5c6 100644 --- a/client/src/modules/game_state/states/Lobby.js +++ b/client/src/modules/game_state/states/Lobby.js @@ -1,5 +1,5 @@ import { QRCode } from '../../third_party/qrcode.js'; -import { toast } from '../../front_end_components/Toast.js'; +import {cancelCurrentToast, toast} from '../../front_end_components/Toast.js'; import { EVENT_IDS, PRIMITIVES, SOCKET_EVENTS, USER_TYPE_ICONS, USER_TYPES } from '../../../config/globals.js'; import { HTMLFragments } from '../../front_end_components/HTMLFragments.js'; import { Confirmation } from '../../front_end_components/Confirmation.js'; @@ -60,6 +60,64 @@ export class Lobby { }); }; + this.editTimerHandler = (e) => { + e.preventDefault(); + document.querySelector('#mid-game-timer-editor')?.remove(); + const timerEditContainer = document.createElement('div'); + const timerEditContainerBackground = document.createElement('div'); + timerEditContainerBackground.setAttribute('id', 'timer-edit-container-background'); + timerEditContainer.setAttribute('id', 'mid-game-timer-editor'); + document.getElementById('game-content').style.display = 'none'; + document.body.appendChild(timerEditContainer); + document.body.appendChild(timerEditContainerBackground); + this.gameCreationStepManager + .renderTimerStep(this.stateBucket.currentGameState, '2', this.stateBucket.currentGameState, this.gameCreationStepManager.steps); + const timerEditPrompt = document.createElement('div'); + timerEditPrompt.setAttribute('id', 'timer-edit-prompt'); + timerEditPrompt.innerHTML = HTMLFragments.TIMER_EDIT_BUTTONS; + timerEditPrompt.querySelector('#save-timer-changes-button').addEventListener('click', () => { + let hours = parseInt(document.getElementById('game-hours').value); + let minutes = parseInt(document.getElementById('game-minutes').value); + hours = this.gameCreationStepManager.standardizeNumberInput(hours); + minutes = this.gameCreationStepManager.standardizeNumberInput(minutes) + if (this.gameCreationStepManager.timerIsValid(hours, minutes)) { + let hasTimer, timerParams; + if (this.gameCreationStepManager.hasTimer(hours, minutes)) { + hasTimer = true; + timerParams = { + hours: hours, + minutes: minutes + }; + } else { + hasTimer = false; + timerParams = null; + } + document.querySelector('#mid-game-timer-editor')?.remove(); + document.querySelector('#timer-edit-container-background')?.remove(); + document.getElementById('game-content').style.display = 'flex'; + this.socket.emit( + SOCKET_EVENTS.IN_GAME_MESSAGE, + EVENT_IDS.UPDATE_GAME_TIMER, + stateBucket.currentGameState.accessCode, + { hasTimer: hasTimer, timerParams: timerParams }, + () => { + toast('Timer updated successfully!', 'success'); + } + ); + } else { + toast('Invalid timer options. Hours can be a max of 5, Minutes a max of 59.', 'error', true); + } + }); + + timerEditPrompt.querySelector('#cancel-timer-changes-button').addEventListener('click', () => { + document.querySelector('#mid-game-timer-editor')?.remove(); + document.querySelector('#timer-edit-container-background')?.remove(); + document.getElementById('game-content').style.display = 'flex'; + }); + + timerEditContainer.appendChild(timerEditPrompt); + } + this.editRolesHandler = (e) => { e.preventDefault(); document.querySelector('#mid-game-role-editor')?.remove(); diff --git a/client/src/modules/page_handlers/gameHandler.js b/client/src/modules/page_handlers/gameHandler.js index a2445af..ff9d5eb 100644 --- a/client/src/modules/page_handlers/gameHandler.js +++ b/client/src/modules/page_handlers/gameHandler.js @@ -20,6 +20,17 @@ import { Ended } from '../game_state/states/Ended.js'; export const gameHandler = (socket, window, gameDOM) => { document.body.innerHTML = gameDOM + document.body.innerHTML; injectNavbar(); + const connectionHandler = () => { + if (stateBucket.timerWorker) { + stateBucket.timerWorker.terminate(); + stateBucket.timerWorker = null; + } + syncWithGame( + socket, + UserUtility.validateAnonUserSignature(stateBucket.environment), + window + ); + } return new Promise((resolve, reject) => { window.fetch( '/api/games/environment', @@ -30,22 +41,18 @@ export const gameHandler = (socket, window, gameDOM) => { ).catch(() => { reject(new Error('There was a problem connecting to the room.')); }).then((response) => { - if (!response.ok) { - reject(new Error('HTTP ' + response.status + ': Could not connect to the room')); + if (!response.ok && !(response.status === 304)) { + console.log('too many requests! returning...'); + reject(new Error('Could not connect to the room: HTTP ' + response.status + ': ' + response.statusText)); return; } response.text().then((text) => { stateBucket.environment = text; + if (socket.connected) { + connectionHandler(); + } socket.on('connect', () => { - if (stateBucket.timerWorker) { - stateBucket.timerWorker.terminate(); - stateBucket.timerWorker = null; - } - syncWithGame( - socket, - UserUtility.validateAnonUserSignature(stateBucket.environment), - window - ); + connectionHandler(); }); socket.on('connect_error', (err) => { toast(err, 'error', true, false); @@ -72,6 +79,7 @@ function syncWithGame (socket, cookie, window) { { personId: cookie }, (err, gameState) => { if (err) { + console.log(err); retrySync(accessCode, socket, cookie); } else { handleGameState(gameState, cookie, socket); diff --git a/client/src/scripts/home.js b/client/src/scripts/home.js index 3736cd0..3839f23 100644 --- a/client/src/scripts/home.js +++ b/client/src/scripts/home.js @@ -31,7 +31,7 @@ function attemptToJoinGame (event) { mode: 'cors' } ).then((res) => { - if (!res.ok) { + if (!res.ok && !(res.status === 304)) { switch (res.status) { case 404: toast('Game not found', 'error', true); diff --git a/client/src/scripts/join.js b/client/src/scripts/join.js index fd9afa7..b9326ae 100644 --- a/client/src/scripts/join.js +++ b/client/src/scripts/join.js @@ -28,7 +28,7 @@ const joinHandler = (e) => { if (validateName(name)) { sendJoinRequest(e, name, accessCode) .then((res) => { - if (!res.ok) { + if (!res.ok && !(res.status === 304)) { switch (res.status) { case 404: toast('Game not found', 'error', true); diff --git a/client/src/styles/game.css b/client/src/styles/game.css index c200c22..c5d4c85 100644 --- a/client/src/styles/game.css +++ b/client/src/styles/game.css @@ -136,7 +136,8 @@ #end-of-game-buttons #return-to-lobby-button, #mod-transfer-button, #edit-roles-button, -#save-role-changes-button { +#save-role-changes-button, +#edit-timer-button { background-color: #045ea6; border: 2px solid #024070; } @@ -145,7 +146,8 @@ #end-of-game-buttons #return-to-lobby-button:hover, #mod-transfer-button:hover, #edit-roles-button:hover, -#save-role-changes-button:hover { +#save-role-changes-button:hover, +#edit-timer-button:hover { background-color: rgba(0, 120, 215, 0.45); border: 2px solid #045EA6; } @@ -678,16 +680,21 @@ label[for='moderator'] { box-shadow: 0 -6px 40px black; } -#start-game-button, #end-game-button, #return-to-lobby-button, #edit-roles-button, #leave-game-button { +#start-game-button, +#end-game-button, +#return-to-lobby-button, +#edit-roles-button, +#leave-game-button, +#edit-timer-button { font-family: 'signika-negative', sans-serif !important; - padding: 10px; + padding: 7px; border-radius: 5px; color: #e7e7e7; cursor: pointer; border: 2px solid transparent; transition: background-color, border 0.3s ease-out; text-shadow: 0 3px 4px rgb(0 0 0 / 85%); - font-size: 25px; + font-size: 22px; user-select: none; -ms-user-select: none; -webkit-user-select: none; @@ -997,6 +1004,10 @@ canvas { } @media(max-width: 500px) { + #game-control-prompt button, #start-game-prompt button, #leave-game-prompt button { + margin: 0 10px; + } + #client-container button img { width: 18px; pointer-events: none; @@ -1068,7 +1079,12 @@ canvas { height: 65px; } - #start-game-button, #end-game-button, #return-to-lobby-button, #edit-roles-button, #leave-game-button { + #start-game-button, + #end-game-button, + #return-to-lobby-button, + #edit-roles-button, + #leave-game-button, + #edit-timer-button { font-size: 20px; padding: 5px; } diff --git a/client/src/view_templates/GameTemplate.js b/client/src/view_templates/GameTemplate.js index acd83c1..f2a914d 100644 --- a/client/src/view_templates/GameTemplate.js +++ b/client/src/view_templates/GameTemplate.js @@ -18,7 +18,7 @@ const template =
-

Connecting to the Room...

+

Waiting for Connection to Room...