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:
{