From e0dffe17b6d52d6d88b8636f9d7a5213e4374ed9 Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Thu, 29 Dec 2022 15:38:40 -0500 Subject: [PATCH] join purposefully as spectator, various improvements --- client/src/config/globals.js | 4 +- .../front_end_components/Confirmation.js | 8 +- .../front_end_components/HTMLFragments.js | 3 + .../modules/front_end_components/Navbar.js | 3 +- .../game_creation/GameCreationStepManager.js | 5 +- .../modules/game_state/states/InProgress.js | 18 ++++- client/src/modules/game_state/states/Lobby.js | 18 ++--- .../states/shared/SharedStateUtil.js | 21 ++++++ client/src/scripts/join.js | 74 ++++++++++++------- client/src/styles/GLOBAL.css | 4 +- client/src/styles/confirmation.css | 9 ++- client/src/styles/create.css | 16 ++-- client/src/styles/game.css | 65 ++++++++-------- client/src/styles/join.css | 19 +++++ client/src/styles/modal.css | 3 +- client/src/views/join.html | 3 +- index.js | 2 +- server/api/GamesAPI.js | 11 ++- server/config/globals.js | 5 +- server/modules/GameManager.js | 45 +++++++---- spec/e2e/game_spec.js | 2 +- spec/support/MockGames.js | 3 +- 22 files changed, 221 insertions(+), 120 deletions(-) diff --git a/client/src/config/globals.js b/client/src/config/globals.js index 590c9a7..ec51baf 100644 --- a/client/src/config/globals.js +++ b/client/src/config/globals.js @@ -56,7 +56,7 @@ export const globals = { USER_TYPES: { MODERATOR: 'moderator', PLAYER: 'player', - TEMPORARY_MODERATOR: 'player / temp mod', + TEMPORARY_MODERATOR: 'temp mod', KILLED_PLAYER: 'killed', SPECTATOR: 'spectator' }, @@ -67,7 +67,7 @@ export const globals = { USER_TYPE_ICONS: { player: ' \uD83C\uDFAE', moderator: ' \uD83D\uDC51', - 'player / temp mod': ' \uD83C\uDFAE\uD83D\uDC51', + 'temp mod': ' \uD83C\uDFAE\uD83D\uDC51', spectator: ' \uD83D\uDC7B', killed: '\uD83D\uDC80' } diff --git a/client/src/modules/front_end_components/Confirmation.js b/client/src/modules/front_end_components/Confirmation.js index 83e8785..5ed4e44 100644 --- a/client/src/modules/front_end_components/Confirmation.js +++ b/client/src/modules/front_end_components/Confirmation.js @@ -1,6 +1,6 @@ import { toast } from './Toast.js'; -export const Confirmation = (message, onYes = null) => { +export const Confirmation = (message, onYes = null, innerHTML = false) => { document.querySelector('#confirmation')?.remove(); document.querySelector('#confirmation-background')?.remove(); @@ -17,7 +17,11 @@ export const Confirmation = (message, onYes = null) => { `; - confirmation.querySelector('#confirmation-message').innerText = message; + if (innerHTML) { + confirmation.querySelector('#confirmation-message').innerHTML = message; + } else { + confirmation.querySelector('#confirmation-message').innerText = message; + } let background = document.createElement('div'); background.setAttribute('id', 'confirmation-background'); diff --git a/client/src/modules/front_end_components/HTMLFragments.js b/client/src/modules/front_end_components/HTMLFragments.js index 6939d57..c0261f7 100644 --- a/client/src/modules/front_end_components/HTMLFragments.js +++ b/client/src/modules/front_end_components/HTMLFragments.js @@ -69,6 +69,7 @@ export const HTMLFragments = {
+
`, SPECTATOR_GAME_VIEW: @@ -83,6 +84,7 @@ export const HTMLFragments = {
+
`, TRANSFER_MOD_MODAL: @@ -147,6 +149,7 @@ export const HTMLFragments = {
+
`, diff --git a/client/src/modules/front_end_components/Navbar.js b/client/src/modules/front_end_components/Navbar.js index cd14850..c6adb36 100644 --- a/client/src/modules/front_end_components/Navbar.js +++ b/client/src/modules/front_end_components/Navbar.js @@ -44,8 +44,7 @@ function getNavbarLinks (page = null, device) { 'Create' + 'How to Use' + 'Feedback' + - 'Github' + - 'Support the App'; + 'Github'; } function attachHamburgerListener () { diff --git a/client/src/modules/game_creation/GameCreationStepManager.js b/client/src/modules/game_creation/GameCreationStepManager.js index e9a8dc4..26429b6 100644 --- a/client/src/modules/game_creation/GameCreationStepManager.js +++ b/client/src/modules/game_creation/GameCreationStepManager.js @@ -153,9 +153,12 @@ export class GameCreationStepManager { } }).catch((e) => { restoreButton(); - toast(e.content, 'error', true, true, 'medium'); if (e.status === 429) { toast('You\'ve sent this request too many times.', 'error', true, true, 'medium'); + } else if (e.status === 413) { + toast('Your request is too large.', 'error', true, true); + } else { + toast(e.content, 'error', true, true, 'medium'); } }); } diff --git a/client/src/modules/game_state/states/InProgress.js b/client/src/modules/game_state/states/InProgress.js index f690ca8..aa59362 100644 --- a/client/src/modules/game_state/states/InProgress.js +++ b/client/src/modules/game_state/states/InProgress.js @@ -56,6 +56,19 @@ export class InProgress { document.querySelector('#timer-container-moderator')?.remove(); document.querySelector('label[for="game-timer"]')?.remove(); } + + const spectatorCount = this.container.querySelector('#spectator-count'); + + if (spectatorCount) { + spectatorCount?.addEventListener('click', () => { + Confirmation(SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.spectators), null, true); + }); + + SharedStateUtil.setNumberOfSpectators( + this.stateBucket.currentGameState.spectators.length, + spectatorCount + ); + } } renderPlayerView (isKilled = false) { @@ -142,6 +155,7 @@ export class InProgress { const killedPerson = this.stateBucket.currentGameState.people.find((person) => person.id === id); if (killedPerson) { killedPerson.out = true; + killedPerson.userType = globals.USER_TYPES.KILLED_PLAYER; if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) { toast(killedPerson.name + ' killed.', 'success', true, true, 'medium'); this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === globals.STATUS.ENDED); @@ -459,7 +473,9 @@ function renderPotentialMods (gameState, group, transferModHandlers, socket) { container.classList.add('potential-moderator'); container.setAttribute('tabindex', '0'); container.dataset.pointer = member.id; - container.innerText = member.name; + container.innerHTML = + '
' + member.name + '
' + + '
' + member.userType + ' ' + globals.USER_TYPE_ICONS[member.userType] + '
'; transferModHandlers[member.id] = (e) => { if (e.type === 'click' || e.code === 'Enter') { ModalManager.dispelModal('transfer-mod-modal', 'transfer-mod-modal-background'); diff --git a/client/src/modules/game_state/states/Lobby.js b/client/src/modules/game_state/states/Lobby.js index bd64585..5167cd8 100644 --- a/client/src/modules/game_state/states/Lobby.js +++ b/client/src/modules/game_state/states/Lobby.js @@ -3,6 +3,7 @@ import { toast } from '../../front_end_components/Toast.js'; import { globals } from '../../../config/globals.js'; import { HTMLFragments } from '../../front_end_components/HTMLFragments.js'; import { Confirmation } from '../../front_end_components/Confirmation.js'; +import { SharedStateUtil } from './shared/SharedStateUtil.js'; export class Lobby { constructor (containerId, stateBucket, socket) { @@ -46,10 +47,14 @@ export class Lobby { const playerCount = this.container.querySelector('#game-player-count'); playerCount.innerText = this.stateBucket.currentGameState.gameSize + ' Players'; - setNumberOfSpectators( + this.container.querySelector('#spectator-count').addEventListener('click', () => { + Confirmation(SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.spectators), null, true); + }); + + SharedStateUtil.setNumberOfSpectators( this.stateBucket.currentGameState.spectators.length, this.container.querySelector('#spectator-count') - ) + ); const gameCode = this.container.querySelector('#game-code'); gameCode.innerHTML = 'Or enter this code on the homepage: ' + @@ -96,7 +101,7 @@ export class Lobby { this.socket.on(globals.EVENT_IDS.NEW_SPECTATOR, (spectator) => { this.stateBucket.currentGameState.spectators.push(spectator); - setNumberOfSpectators( + SharedStateUtil.setNumberOfSpectators( this.stateBucket.currentGameState.spectators.length, document.getElementById('spectator-count') ); @@ -136,12 +141,6 @@ export class Lobby { } } -function setNumberOfSpectators(number, el) { - el.innerText = '+ ' + (number === 1 - ? number + ' Spectator' - : number + ' Spectators'); -} - function enableOrDisableStartButton (gameState, buttonContainer, handler) { if (gameState.isFull) { buttonContainer.querySelector('#start-game-button').addEventListener('click', handler); @@ -189,6 +188,7 @@ function getTimeString (gameState) { function renderLobbyPerson (name, userType) { const el = document.createElement('div'); const personNameEl = document.createElement('div'); + personNameEl.classList.add('lobby-player-name'); const personTypeEl = document.createElement('div'); personNameEl.innerText = name; personTypeEl.innerText = userType + globals.USER_TYPE_ICONS[userType]; diff --git a/client/src/modules/game_state/states/shared/SharedStateUtil.js b/client/src/modules/game_state/states/shared/SharedStateUtil.js index 10f32dd..6b6cc3e 100644 --- a/client/src/modules/game_state/states/shared/SharedStateUtil.js +++ b/client/src/modules/game_state/states/shared/SharedStateUtil.js @@ -130,6 +130,27 @@ export const SharedStateUtil = { } else { window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code'); } + }, + + buildSpectatorList (spectators) { + if (spectators.length === 0) { + return '
Nobody currently spectating.
'; + } + let html = ''; + for (const spectator of spectators) { + html += `
+
` + spectator.name + '
' + + '
' + 'spectator' + globals.USER_TYPE_ICONS.spectator + `
+
`; + } + + return html; + }, + + setNumberOfSpectators: (number, el) => { + el.innerText = '+ ' + (number === 1 + ? number + ' Spectator' + : number + ' Spectators'); } }; diff --git a/client/src/scripts/join.js b/client/src/scripts/join.js index 72e72f8..b6c9aa4 100644 --- a/client/src/scripts/join.js +++ b/client/src/scripts/join.js @@ -27,45 +27,63 @@ const joinHandler = (e) => { e.preventDefault(); const name = document.getElementById('player-new-name').value; if (validateName(name)) { - document.getElementById('join-game-form').onsubmit = null; - document.getElementById('submit-new-name').classList.add('submitted'); - document.getElementById('submit-new-name').setAttribute('value', 'Joining...'); - XHRUtility.xhr( - '/api/games/' + accessCode + '/players', - 'PATCH', - null, - JSON.stringify({ - playerName: name, - accessCode: accessCode, - sessionCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.LOCAL), - localCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.PRODUCTION) - }) - ) + sendJoinRequest(e, name, accessCode) .then((res) => { const json = JSON.parse(res.content); UserUtility.setAnonymousUserId(json.cookie, json.environment); window.location = '/game/' + accessCode; }).catch((res) => { - document.getElementById('join-game-form').onsubmit = joinHandler; - document.getElementById('submit-new-name').classList.remove('submitted'); - document.getElementById('submit-new-name').setAttribute('value', 'Join Game'); - if (res.status === 404) { - toast('This game was not found.', 'error', true, true, 'long'); - } else if (res.status === 400) { - toast('This name is already taken.', 'error', true, true, 'long'); - } else if (res.status >= 500) { - toast( - 'The server is experiencing problems. Please try again later', - 'error', - true - ); - } + handleJoinError(e, res, joinHandler); }); } else { toast('Name must be between 1 and 30 characters.', 'error', true, true, 'long'); } }; +function sendJoinRequest (e, name, accessCode) { + document.getElementById('join-game-form').onsubmit = null; + if (e.submitter.getAttribute('id') === 'submit-join-as-player') { + document.getElementById('submit-join-as-player').classList.add('submitted'); + document.getElementById('submit-join-as-player').setAttribute('value', '...'); + } else { + document.getElementById('submit-join-as-spectator').classList.add('submitted'); + document.getElementById('submit-join-as-spectator').setAttribute('value', '...'); + } + return XHRUtility.xhr( + '/api/games/' + accessCode + '/players', + 'PATCH', + null, + JSON.stringify({ + playerName: name, + accessCode: accessCode, + sessionCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.LOCAL), + localCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.PRODUCTION), + joinAsSpectator: e.submitter.getAttribute('id') === 'submit-join-as-spectator' + }) + ); +} + +function handleJoinError (e, res, joinHandler) { + document.getElementById('join-game-form').onsubmit = joinHandler; + e.submitter.classList.remove('submitted'); + if (e.submitter.getAttribute('id') === 'submit-join-as-player') { + e.submitter.setAttribute('value', 'Join'); + } else { + e.submitter.setAttribute('value', 'Spectate'); + } + if (res.status === 404) { + toast('This game was not found.', 'error', true, true, 'long'); + } else if (res.status === 400) { + toast(res.content, 'error', true, true, 'long'); + } else if (res.status >= 500) { + toast( + 'The server is experiencing problems. Please try again later', + 'error', + true + ); + } +} + function validateName (name) { return typeof name === 'string' && name.length > 0 && name.length <= 30; } diff --git a/client/src/styles/GLOBAL.css b/client/src/styles/GLOBAL.css index 3284e8f..5e73dc0 100644 --- a/client/src/styles/GLOBAL.css +++ b/client/src/styles/GLOBAL.css @@ -572,7 +572,7 @@ input { } .good, .compact-card.good .card-role { - color: #4b6bfa; + color: #5f7cfb; } .good-players, #deck-good { @@ -786,7 +786,7 @@ input { display:inline-flex !important; align-items: center !important; color:#ffffff !important; - background-color:#333243 !important; + background-color:#2d2c3a !important; border-radius: 5px !important; border: 1px solid transparent !important; padding: 7px 15px 7px 10px !important; diff --git a/client/src/styles/confirmation.css b/client/src/styles/confirmation.css index 9bf69a2..cd6d66f 100644 --- a/client/src/styles/confirmation.css +++ b/client/src/styles/confirmation.css @@ -2,13 +2,12 @@ border-radius: 2px; text-align: center; position: fixed; - border: 2px solid #333243; width: 85%; z-index: 100001; top: 50%; left: 50%; transform: translate(-50%, -50%); - background-color: #191920; + background-color: #292834; align-items: center; justify-content: center; max-width: 25em; @@ -32,6 +31,12 @@ font-size: 20px; color: #e7e7e7; margin: 1em 0 2em 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + max-height: 20em; + overflow-y: auto; } .confirmation-buttons button, .confirmation-buttons-centered button { diff --git a/client/src/styles/create.css b/client/src/styles/create.css index 70ca168..13c6755 100644 --- a/client/src/styles/create.css +++ b/client/src/styles/create.css @@ -106,7 +106,7 @@ display: flex; justify-content: space-between; background-color: #0f0f10; - border: 2px solid #333243; + border: 2px solid #2d2c3a; padding: 5px; border-radius: 3px; font-size: 16px; @@ -159,7 +159,7 @@ background-color: #191920; padding: 10px; border-radius: 3px; - border: 2px solid #333243; + border: 2px solid #2d2c3a; position: relative; } @@ -173,7 +173,7 @@ #deck-count { font-size: 30px; - background-color: #333243; + background-color: #2d2c3a; width: fit-content; padding: 0 5px; border-radius: 3px; @@ -236,7 +236,7 @@ z-index: 25; top: 38px; right: 29px; - background-color: #333243; + background-color: #2d2c3a; border-radius: 3px; box-shadow: -3px -3px 6px rgb(0 0 0 / 60%); } @@ -245,7 +245,7 @@ display: flex; width: 100%; padding: 10px; - background-color: #333243; + background-color: #2d2c3a; text-align: center; justify-content: center; align-items: center; @@ -362,7 +362,7 @@ select { display: flex; flex-wrap: wrap; background-color: #191920; - border: 2px solid #333243; + border: 2px solid #2d2c3a; border-radius: 3px; } @@ -593,7 +593,7 @@ input[type="number"] { max-width: 20em; margin: 0.5em; cursor: pointer; - border: 2px solid #333243; + border: 2px solid #2d2c3a; border-radius: 3px; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.6); } @@ -613,7 +613,7 @@ input[type="number"] { .review-option { background-color: #191920; - border: 2px solid #333243; + border: 2px solid #2d2c3a; color: #e7e7e7; padding: 10px; font-size: 18px; diff --git a/client/src/styles/game.css b/client/src/styles/game.css index d0c441e..d668378 100644 --- a/client/src/styles/game.css +++ b/client/src/styles/game.css @@ -1,18 +1,20 @@ -.lobby-player, #moderator { +.lobby-player, #moderator, .spectator, .potential-moderator { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; - background-color: black; + background-color: #171522; color: #e7e7e7; padding: 5px; border-radius: 3px; font-size: 17px; - width: fit-content; - min-width: 15em; + width: 17em; border: 2px solid transparent; - margin: 0.25em 0; - box-shadow: 2px 2px 5px rgb(0 0 0 / 40%); + margin: 0 auto 0.25em auto; +} + +.potential-moderator { + margin: 0.5em auto; } #lobby-players { @@ -29,13 +31,17 @@ #spectator-count { color: #b1afcd; + text-decoration: underline; + font-size: 17px; + margin: 5px 0; + cursor: pointer; } .lobby-player-client { border: 2px solid #21ba45; } -.lobby-player div:nth-child(2) { +.lobby-player div:nth-child(2), .spectator div:nth-child(2), .potential-moderator div:nth-child(2) { color: #21ba45; } @@ -122,7 +128,7 @@ h1 { } #end-of-game-header h2 { - border: 1px solid #333243; + border: 1px solid #2d2c3a; border-radius: 5px; background-color: #1a1726; padding: 7px; @@ -141,19 +147,6 @@ h1 { padding: 10px; } -.potential-moderator { - display: flex; - color: #d7d7d7; - background-color: black; - border: 2px solid transparent; - align-items: center; - padding: 10px; - border-radius: 3px; - justify-content: space-between; - margin: 0.5em 0; - position: relative; -} - .potential-moderator:hover { border: 2px solid #d7d7d7; cursor: pointer; @@ -170,7 +163,7 @@ h1 { padding: 7px; border-radius: 3px; background-color: #121314; - border: 2px solid #333243; + border: 2px solid #2d2c3a; color: #e7e7e7; align-items: center; display: flex; @@ -243,7 +236,7 @@ h1 { font-size: 20px; font-family: signika-negative, sans-serif; margin: 0.5em 0; - background-color: black; + background-color: #171522; } #role-info-modal h2 { @@ -423,7 +416,7 @@ h1 { flex-wrap: wrap; align-items: center; justify-content: space-between; - background-color: #333243; + background-color: #2d2c3a; border-radius: 3px; min-width: 15em; } @@ -431,7 +424,7 @@ h1 { #client-name { color: #e7e7e7; font-family: 'signika-negative', sans-serif; - font-size: 30px; + font-size: 25px; margin: 0.25em 2em 0.25em 0; } @@ -439,7 +432,7 @@ h1 { color: #21ba45; font-family: 'signika-negative', sans-serif; font-size: 25px; - background-color: black; + background-color: #171522; border-radius: 3px; display: block; padding: 0 5px; @@ -608,7 +601,7 @@ label[for='moderator'] { margin-bottom: 1em; padding: 0.5em; border-radius: 3px; - background-color: #333243; + background-color: #2d2c3a; } canvas { @@ -620,13 +613,12 @@ canvas { border-left: 3px solid #21ba45; display: flex; color: #d7d7d7; - background-color: black; + background-color: #171522; align-items: center; padding: 0 5px; justify-content: space-between; margin: 0.25em 0; position: relative; - box-shadow: 2px 3px 6px rgb(0 0 0 / 50%); border-radius: 3px; } @@ -634,18 +626,20 @@ canvas { justify-content: flex-end; } -.game-player-name { +.game-player-name, .lobby-player-name, .spectator-name, .potential-mod-name { position: relative; min-width: 6em; + max-width: 10em; overflow: hidden; white-space: nowrap; font-weight: bold; - font-size: 18px; + font-size: 16px; text-overflow: ellipsis; + text-align: left; } .kill-player-button, .reveal-role-button { - background-color: #333243; + background-color: #434156; font-family: 'signika-negative', sans-serif !important; border-radius: 3px; color: #e7e7e7; @@ -750,7 +744,7 @@ canvas { } #game-parameters { - background-color: #333243; + background-color: #2d2c3a; border-radius: 3px; padding: 5px 20px; } @@ -765,12 +759,11 @@ canvas { #game-player-list > div { padding: 2px 10px; border-radius: 3px; - margin-bottom: 0.5em; + margin-bottom: 0.25em; } #players-alive-label { display: block; - margin-bottom: 10px; font-size: 25px; } @@ -795,10 +788,10 @@ canvas { } #lobby-people-container , #game-people-container { - background-color: #333243; padding: 10px; border-radius: 3px; min-height: 25em; + background-color: #292834; max-width: 35em; min-width: 17em; margin-top: 1em; diff --git a/client/src/styles/join.css b/client/src/styles/join.css index ddfe421..399a459 100644 --- a/client/src/styles/join.css +++ b/client/src/styles/join.css @@ -6,6 +6,25 @@ z-index: 1 !important; } +#join-game-form .modal-button-container { + justify-content: flex-end; + margin-top: 2em; +} + +#join-game-form .modal-button-container input { + width: 5em; + margin: 0 10px; +} + +#join-game-form .modal-button-container #submit-join-as-spectator { + background-color: #045EA6; +} + +#join-game-form .modal-button-container #submit-join-as-spectator:hover { + background-color: #0078D773; + border: 2px solid #045EA6; +} + #player-new-name { max-width: 17em; } diff --git a/client/src/styles/modal.css b/client/src/styles/modal.css index 1223df0..5d105a5 100644 --- a/client/src/styles/modal.css +++ b/client/src/styles/modal.css @@ -7,7 +7,7 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - background-color: #191920; + background-color: #292834; align-items: center; justify-content: center; max-width: 25em; @@ -15,7 +15,6 @@ flex-direction: column; padding: 1em; display: none; - border: 2px solid #333243; } .modal-background { diff --git a/client/src/views/join.html b/client/src/views/join.html index 2ad4e97..242a5fa 100644 --- a/client/src/views/join.html +++ b/client/src/views/join.html @@ -43,7 +43,8 @@ diff --git a/index.js b/index.js index 6eff3be..28617f6 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,7 @@ const express = require('express'); const app = express(); const ServerBootstrapper = require('./server/modules/ServerBootstrapper'); -app.use(express.json()); +app.use(express.json({ limit: '10kb' })); const args = ServerBootstrapper.processCLIArgs(); diff --git a/server/api/GamesAPI.js b/server/api/GamesAPI.js index 984cbbc..fbbbaa9 100644 --- a/server/api/GamesAPI.js +++ b/server/api/GamesAPI.js @@ -74,16 +74,17 @@ router.patch('/:code/players', function (req, res) { || !validateName(req.body.playerName) || !validateCookie(req.body.localCookie) || !validateCookie(req.body.sessionCookie) + || !validateSpectatorFlag(req.body.joinAsSpectator) ) { res.status(400).send(); } else { const game = gameManager.activeGameRunner.activeGames.get(req.body.accessCode); if (game) { const inUseCookie = gameManager.environment === globals.ENVIRONMENT.PRODUCTION ? req.body.localCookie : req.body.sessionCookie; - gameManager.joinGame(game, req.body.playerName, inUseCookie).then((data) => { + gameManager.joinGame(game, req.body.playerName, inUseCookie, req.body.joinAsSpectator).then((data) => { res.status(200).send({ cookie: data, environment: gameManager.environment }); - }).catch((code) => { - res.status(code).send(); + }).catch((data) => { + res.status(data.status).send(data.reason); }); } else { res.status(404).send(); @@ -130,4 +131,8 @@ function validateAccessCode (accessCode) { return /^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH; } +function validateSpectatorFlag (spectatorFlag) { + return typeof spectatorFlag === 'boolean'; +} + module.exports = router; diff --git a/server/config/globals.js b/server/config/globals.js index e8972f8..ca9f0cc 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -49,7 +49,7 @@ const globals = { USER_TYPES: { MODERATOR: 'moderator', PLAYER: 'player', - TEMPORARY_MODERATOR: 'player / temp mod', + TEMPORARY_MODERATOR: 'temp mod', KILLED_PLAYER: 'killed', SPECTATOR: 'spectator' }, @@ -84,7 +84,8 @@ const globals = { RESUME_TIMER: 'resumeTimer', GET_TIME_REMAINING: 'getTimeRemaining' }, - MOCK_AUTH: 'mock_auth' + MOCK_AUTH: 'mock_auth', + MAX_SPECTATORS: 25 }; module.exports = globals; diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js index 23b0e89..7977cd3 100644 --- a/server/modules/GameManager.js +++ b/server/modules/GameManager.js @@ -199,6 +199,7 @@ class GameManager { if (game.spectators.includes(person)) { game.spectators.splice(game.spectators.indexOf(person), 1); } + namespace.in(game.accessCode).emit(globals.EVENTS.NEW_SPECTATOR); } person.userType = globals.USER_TYPES.MODERATOR; game.moderator = person; @@ -224,13 +225,18 @@ class GameManager { } }; - joinGame = (game, name, cookie) => { + joinGame = (game, name, cookie, joinAsSpectator) => { const matchingPerson = findPersonByField(game, 'cookie', cookie); if (matchingPerson) { return Promise.resolve(matchingPerson.cookie); } if (isNameTaken(game, name)) { - return Promise.reject(400); + return Promise.reject({ status: 400, reason: 'This name is taken.' }); + } + if (joinAsSpectator && game.spectators.length === globals.MAX_SPECTATORS) { + return Promise.reject({ status: 400, reason: 'There are too many people already spectating.' }); + } else if (joinAsSpectator) { + return addSpectator(game, name, this.logger, this.namespace); } const unassignedPerson = game.moderator.assigned === false ? game.moderator @@ -246,20 +252,11 @@ class GameManager { game.isFull ); return Promise.resolve(unassignedPerson.cookie); - } else { // if the game is full, make them a spectator. - const spectator = new Person( - createRandomId(), - createRandomId(), - name, - globals.USER_TYPES.SPECTATOR - ); - this.logger.trace('new spectator: ' + spectator.name); - game.spectators.push(spectator); - this.namespace.in(game.accessCode).emit( - globals.EVENTS.NEW_SPECTATOR, - GameStateCurator.mapPerson(spectator) - ); - return Promise.resolve(spectator.cookie); + } else { + if (game.spectators.length === globals.MAX_SPECTATORS) { + return Promise.reject({ status: 400, reason: 'This game has reached the maximum number of players and spectators.' }); + } + return addSpectator(game, name, this.logger, this.namespace); } }; @@ -496,4 +493,20 @@ function getGameSize (cards) { return quantity; } +function addSpectator (game, name, logger, namespace) { + const spectator = new Person( + createRandomId(), + createRandomId(), + name, + globals.USER_TYPES.SPECTATOR + ); + logger.trace('new spectator: ' + spectator.name); + game.spectators.push(spectator); + namespace.in(game.accessCode).emit( + globals.EVENTS.NEW_SPECTATOR, + GameStateCurator.mapPerson(spectator) + ); + return Promise.resolve(spectator.cookie); +} + module.exports = GameManager; diff --git a/spec/e2e/game_spec.js b/spec/e2e/game_spec.js index 012bd0f..dedaa2f 100644 --- a/spec/e2e/game_spec.js +++ b/spec/e2e/game_spec.js @@ -199,7 +199,7 @@ describe('game page', () => { it('should display the mod transfer modal, with the single spectator available for selection', () => { document.getElementById('mod-transfer-button').click(); expect(document.querySelector('div[data-pointer="MGGVR8KQ7V7HGN3QBLJ5339ZL"].potential-moderator') - .innerText).toEqual('Matt'); + .innerText).toContain('Matt'); document.getElementById('close-mod-transfer-modal-button').click(); }); diff --git a/spec/support/MockGames.js b/spec/support/MockGames.js index a742e45..5e3501a 100644 --- a/spec/support/MockGames.js +++ b/spec/support/MockGames.js @@ -163,7 +163,8 @@ export const mockGames = { paused: true, timeRemaining: 120000 }, - isFull: true + isFull: true, + spectators: [] }, moderatorGame: {