diff --git a/client/src/config/globals.js b/client/src/config/globals.js index 98dc8da..041fa74 100644 --- a/client/src/config/globals.js +++ b/client/src/config/globals.js @@ -96,6 +96,7 @@ export const LOBBY_EVENTS = function () { EVENT_IDS.ADD_SPECTATOR, EVENT_IDS.KICK_PERSON, EVENT_IDS.UPDATE_GAME_ROLES, + EVENT_IDS.UPDATE_GAME_TIMER, EVENT_IDS.LEAVE_ROOM ]; }; diff --git a/client/src/modules/game_creation/GameCreationStepManager.js b/client/src/modules/game_creation/GameCreationStepManager.js index 2de6cec..063eb64 100644 --- a/client/src/modules/game_creation/GameCreationStepManager.js +++ b/client/src/modules/game_creation/GameCreationStepManager.js @@ -54,7 +54,7 @@ export class GameCreationStepManager { let hours = parseInt(document.getElementById('game-hours').value); let minutes = parseInt(document.getElementById('game-minutes').value); hours = this.standardizeNumberInput(hours); - minutes = this.standardizeNumberInput(minutes) + minutes = this.standardizeNumberInput(minutes); if (this.timerIsValid(hours, minutes)) { if (this.hasTimer(hours, minutes)) { this.currentGame.hasTimer = true; @@ -323,7 +323,7 @@ export class GameCreationStepManager { document.getElementById(containerId).appendChild(div); } - timerIsValid(hours, minutes) { + timerIsValid (hours, minutes) { let valid = true; if (hours === null && minutes === null) { @@ -338,15 +338,14 @@ export class GameCreationStepManager { valid = minutes <= PRIMITIVES.MAX_MINUTES; } - return valid; } - hasTimer(hours, minutes) { + hasTimer (hours, minutes) { return hours !== null || minutes !== null; } - standardizeNumberInput(input) { + standardizeNumberInput (input) { return (isNaN(input) || input === 0) ? null : input; } } diff --git a/client/src/modules/game_creation/RoleBox.js b/client/src/modules/game_creation/RoleBox.js index e074a5f..7b996f2 100644 --- a/client/src/modules/game_creation/RoleBox.js +++ b/client/src/modules/game_creation/RoleBox.js @@ -166,6 +166,7 @@ export class RoleBox { }; displayDefaultRoles = (selectEl) => { + document.querySelector('#custom-role-placeholder')?.remove(); document.querySelectorAll('#role-select .default-role, #role-select .custom-role').forEach(e => e.remove()); this.categoryTransition.play(); for (let i = 0; i < this.defaultRoles.length; i ++) { @@ -184,9 +185,21 @@ export class RoleBox { this.addRoleEventListeners(selectEl, true, true, false, false, false); }; + displayCustomRolePlaceHolder = () => { + const placeholder = document.createElement('div'); + placeholder.setAttribute('id', 'custom-role-placeholder'); + placeholder.innerText = 'Create a role with the button below.'; + document.getElementById('role-select').appendChild(placeholder); + }; + displayCustomRoles = (selectEl) => { + document.querySelector('#custom-role-placeholder')?.remove(); document.querySelectorAll('#role-select .default-role, #role-select .custom-role').forEach(e => e.remove()); this.categoryTransition.play(); + if (this.customRoles.length === 0) { + this.displayCustomRolePlaceHolder(); + return; + } this.customRoles.sort((a, b) => { if (a.team !== b.team) { return a.team === ALIGNMENT.GOOD ? -1 : 1; diff --git a/client/src/modules/game_state/states/Lobby.js b/client/src/modules/game_state/states/Lobby.js index fada5c6..dc29961 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 {cancelCurrentToast, toast} from '../../front_end_components/Toast.js'; +import { 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'; @@ -70,16 +70,22 @@ export class Lobby { 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; + this.gameCreationStepManager.steps['3'].forwardHandler = (e) => { + e.preventDefault(); + if (e.type === 'click' || e.code === 'Enter') { + timerEditPrompt.querySelector('#save-timer-changes-button')?.click(); + } + }; + this.gameCreationStepManager + .renderTimerStep('mid-game-timer-editor', '3', this.stateBucket.currentGameState, this.gameCreationStepManager.steps); 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) + minutes = this.gameCreationStepManager.standardizeNumberInput(minutes); if (this.gameCreationStepManager.timerIsValid(hours, minutes)) { let hasTimer, timerParams; if (this.gameCreationStepManager.hasTimer(hours, minutes)) { @@ -116,7 +122,7 @@ export class Lobby { }); timerEditContainer.appendChild(timerEditPrompt); - } + }; this.editRolesHandler = (e) => { e.preventDefault(); @@ -194,11 +200,17 @@ export class Lobby { 'Participants (' + inLobbyCount + '/' + this.stateBucket.currentGameState.gameSize + ' Players)'; } - populateHeader () { + setTimer () { const timeString = getTimeString(this.stateBucket.currentGameState); const time = this.container.querySelector('#game-time'); time.innerText = timeString; + return timeString; + } + + populateHeader () { + const timeString = this.setTimer(); + const link = this.setLink(timeString); this.setPlayerCount(); @@ -293,6 +305,13 @@ export class Lobby { this.setPlayerCount(); }); + this.socket.on(EVENT_IDS.UPDATE_GAME_TIMER, (hasTimer, timerParams) => { + this.stateBucket.currentGameState.hasTimer = hasTimer; + this.stateBucket.currentGameState.timerParams = timerParams; + const timeString = this.setTimer(); + this.setLink(timeString); + }); + this.socket.on(EVENT_IDS.LEAVE_ROOM, (leftId, gameIsStartable) => { if (leftId === this.stateBucket.currentGameState.client.id) { window.location = '/?message=' + encodeURIComponent('You left the room.'); @@ -335,6 +354,7 @@ export class Lobby { if (existingPrompt) { enableStartButton(existingPrompt, this.startGameHandler); document.getElementById('edit-roles-button').addEventListener('click', this.editRolesHandler); + document.getElementById('edit-timer-button').addEventListener('click', this.editTimerHandler); } else { const newPrompt = document.createElement('div'); newPrompt.setAttribute('id', 'start-game-prompt'); @@ -343,6 +363,7 @@ export class Lobby { document.body.appendChild(newPrompt); enableStartButton(newPrompt, this.startGameHandler); document.getElementById('edit-roles-button').addEventListener('click', this.editRolesHandler); + document.getElementById('edit-timer-button').addEventListener('click', this.editTimerHandler); } } diff --git a/client/src/modules/page_handlers/gameHandler.js b/client/src/modules/page_handlers/gameHandler.js index 3704fae..c30a495 100644 --- a/client/src/modules/page_handlers/gameHandler.js +++ b/client/src/modules/page_handlers/gameHandler.js @@ -221,7 +221,10 @@ function setClientSocketHandlers (stateBucket, socket) { socket.on(EVENT_IDS.START_GAME, () => { fetchGameStateHandler(startGameStateAckFn); }); - socket.on(EVENT_IDS.RESTART_GAME, () => { fetchGameStateHandler(restartGameStateAckFn); }); + socket.on(EVENT_IDS.RESTART_GAME, () => { + document.querySelector('#game-control-prompt')?.remove(); + fetchGameStateHandler(restartGameStateAckFn); + }); socket.on(EVENT_IDS.SYNC_GAME_STATE, () => { socket.emit( diff --git a/client/src/styles/create.css b/client/src/styles/create.css index 7b4eb94..b34080e 100644 --- a/client/src/styles/create.css +++ b/client/src/styles/create.css @@ -191,14 +191,14 @@ margin-top: 0.5em; } -#deck-list-placeholder { +#deck-list-placeholder, #custom-role-placeholder { margin: auto; position: absolute; top: 0; left: 0; bottom: 0; right: 0; - width: 290px; + width: fit-content; height: 50px; font-size: 20px; text-align: center; diff --git a/client/src/styles/game.css b/client/src/styles/game.css index c5d4c85..5f2894f 100644 --- a/client/src/styles/game.css +++ b/client/src/styles/game.css @@ -106,13 +106,13 @@ max-width: 17em; } -#save-role-changes-button, #cancel-role-changes-button { +#save-role-changes-button, #cancel-role-changes-button, #save-timer-changes-button, #cancel-timer-changes-button { padding: 10px; font-size: 25px; margin: 0.5em 0; } -#role-edit-prompt { +#role-edit-prompt, #timer-edit-prompt { display: flex; margin: 10px 0; padding: 10px 0; @@ -123,11 +123,15 @@ background-color: #16141e; } -#role-edit-prompt button { +#timer-edit-prompt { + margin-top: 2em; +} + +#role-edit-prompt button, #timer-edit-prompt button { margin: 0 20px; } -#save-role-changes-button img { +#save-role-changes-button img, #save-timer-changes-button img { width: 20px; margin-left: 10px; } @@ -137,7 +141,8 @@ #mod-transfer-button, #edit-roles-button, #save-role-changes-button, -#edit-timer-button { +#edit-timer-button, +#save-timer-changes-button { background-color: #045ea6; border: 2px solid #024070; } @@ -147,12 +152,13 @@ #mod-transfer-button:hover, #edit-roles-button:hover, #save-role-changes-button:hover, -#edit-timer-button:hover { +#edit-timer-button:hover, +#save-timer-changes-button:hover { background-color: rgba(0, 120, 215, 0.45); border: 2px solid #045EA6; } -#mid-game-role-editor { +#mid-game-role-editor, #mid-game-timer-editor { display: flex; border-radius: 5px; position: fixed; @@ -166,7 +172,16 @@ overflow: auto; } -#role-edit-container-background { +#mid-game-timer-editor #game-time { + display: flex; + flex-wrap: wrap; + border-radius: 5px; + margin: 1em; + background-color: #16141e; + border: 2px solid #3b3a4a; +} + +#role-edit-container-background, #timer-edit-container-background { position: fixed; top: 0; left: 0; @@ -177,17 +192,15 @@ cursor: pointer; } -#mid-game-role-editor #step-2 { +#mid-game-role-editor #step-2 , #mid-game-timer-editor #step-3 { width: 100%; display: flex; justify-content: center; overflow: auto; align-items: center; + padding: 0; } -#save-button { - -} #mid-game-role-editor #custom-roles-container { height: fit-content; @@ -641,6 +654,7 @@ label[for='moderator'] { display: flex; flex-direction: row; align-items: center; + animation: fade-in-slide-up 0.5s ease-in-out; justify-content: center; position: fixed; z-index: 3; @@ -1167,11 +1181,7 @@ canvas { @keyframes fade-in-slide-up { 0% { opacity: 0; - transform: translateY(20px); - } - 5% { - opacity: 1; - transform: translateY(0px); + transform: translateY(10px); } 100% { opacity: 1; diff --git a/server/config/globals.js b/server/config/globals.js index e49bbe2..ec01331 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -78,6 +78,7 @@ const EVENT_IDS = { TIMER_EVENT: 'timerEvent', KICK_PERSON: 'kickPerson', UPDATE_GAME_ROLES: 'updateGameRoles', + UPDATE_GAME_TIMER: 'updateGameTimer', LEAVE_ROOM: 'leaveRoom', BROADCAST: 'broadcast' }; @@ -103,6 +104,7 @@ const SYNCABLE_EVENTS = function () { EVENT_IDS.END_TIMER, EVENT_IDS.KICK_PERSON, EVENT_IDS.UPDATE_GAME_ROLES, + EVENT_IDS.UPDATE_GAME_TIMER, EVENT_IDS.LEAVE_ROOM ]; }; diff --git a/server/model/GameCreationRequest.js b/server/model/GameCreationRequest.js index 9301c8f..b6ba6cc 100644 --- a/server/model/GameCreationRequest.js +++ b/server/model/GameCreationRequest.js @@ -56,6 +56,22 @@ class GameCreationRequest { } return false; } + + static timerParamsAreValid = (hasTimer, timerParams) => { + if (hasTimer === false) { + return timerParams === null; + } else { + if (timerParams === null || typeof timerParams !== 'object') { + return false; + } + + return (timerParams.hours === null && timerParams.minutes > 0 && timerParams.minutes < 60) + || (timerParams.minutes === null && timerParams.hours > 0 && timerParams.hours < 6) + || (timerParams.hours === 0 && timerParams.minutes > 0 && timerParams.minutes < 60) + || (timerParams.minutes === 0 && timerParams.hours > 0 && timerParams.hours < 6) + || (timerParams.hours > 0 && timerParams.hours < 6 && timerParams.minutes >= 0 && timerParams.minutes < 60); + } + } } function valid (gameParams) { @@ -65,24 +81,8 @@ function valid (gameParams) { && typeof gameParams.moderatorName === 'string' && gameParams.moderatorName.length > 0 && gameParams.moderatorName.length <= 30 - && timerParamsAreValid(gameParams.hasTimer, gameParams.timerParams) + && GameCreationRequest.timerParamsAreValid(gameParams.hasTimer, gameParams.timerParams) && GameCreationRequest.deckIsValid(gameParams.deck); } -function timerParamsAreValid (hasTimer, timerParams) { - if (hasTimer === false) { - return timerParams === null; - } else { - if (timerParams === null || typeof timerParams !== 'object') { - return false; - } - - return (timerParams.hours === null && timerParams.minutes > 0 && timerParams.minutes < 60) - || (timerParams.minutes === null && timerParams.hours > 0 && timerParams.hours < 6) - || (timerParams.hours === 0 && timerParams.minutes > 0 && timerParams.minutes < 60) - || (timerParams.minutes === 0 && timerParams.hours > 0 && timerParams.hours < 6) - || (timerParams.hours > 0 && timerParams.hours < 6 && timerParams.minutes >= 0 && timerParams.minutes < 60); - } -} - module.exports = GameCreationRequest; diff --git a/server/modules/Events.js b/server/modules/Events.js index 8b81797..6648ff7 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -69,7 +69,7 @@ const Events = [ } vars.ackFn({ errorFlag: 1, message: 'This name is taken.' }); } else if (socketArgs.newName.length > PRIMITIVES.MAX_PERSON_NAME_LENGTH) { - vars.ackFn({ errorFlag: 1, message: 'Your new name is too long - the max is' + PRIMITIVES.MAX_PERSON_NAME_LENGTH + ' characters.' }); + vars.ackFn({ errorFlag: 1, message: 'Your new name is too long - the max is ' + PRIMITIVES.MAX_PERSON_NAME_LENGTH + ' characters.' }); vars.hasNameChanged = false; } else if (socketArgs.newName.length === 0) { vars.ackFn({ errorFlag: 1, message: 'Your new name cannot be empty.' }); @@ -381,6 +381,25 @@ const Events = [ } } }, + { + id: EVENT_IDS.UPDATE_GAME_TIMER, + stateChange: async (game, socketArgs, vars) => { + if (GameCreationRequest.timerParamsAreValid(socketArgs.hasTimer, socketArgs.timerParams)) { + game.hasTimer = socketArgs.hasTimer; + game.timerParams = socketArgs.timerParams; + } + }, + communicate: async (game, socketArgs, vars) => { + if (vars.ackFn) { + vars.ackFn(); + } + vars.gameManager.namespace.in(game.accessCode).emit( + EVENT_IDS.UPDATE_GAME_TIMER, + game.hasTimer, + game.timerParams + ); + } + }, { id: EVENT_IDS.END_TIMER, stateChange: async (game, socketArgs, vars) => {