diff --git a/client/src/config/globals.js b/client/src/config/globals.js
index ebca36d..590c9a7 100644
--- a/client/src/config/globals.js
+++ b/client/src/config/globals.js
@@ -17,7 +17,8 @@ export const globals = {
REVEAL_PLAYER: 'revealPlayer',
TRANSFER_MODERATOR: 'transferModerator',
CHANGE_NAME: 'changeName',
- END_GAME: 'endGame'
+ END_GAME: 'endGame',
+ END_TIMER: 'endTimer'
},
STATUS: {
LOBBY: 'lobby',
@@ -49,7 +50,8 @@ export const globals = {
SYNC_GAME_STATE: 'syncGameState',
START_TIMER: 'startTimer',
PLAYER_LEFT: 'playerLeft',
- NEW_SPECTATOR: 'newSpectator'
+ NEW_SPECTATOR: 'newSpectator',
+ RESTART_GAME: 'restartGame'
},
USER_TYPES: {
MODERATOR: 'moderator',
diff --git a/client/src/images/play-pause-placeholder.svg b/client/src/images/play-pause-placeholder.svg
new file mode 100644
index 0000000..8186e53
--- /dev/null
+++ b/client/src/images/play-pause-placeholder.svg
@@ -0,0 +1,7 @@
+
diff --git a/client/src/images/shuffle.svg b/client/src/images/shuffle.svg
new file mode 100644
index 0000000..54a4a2e
--- /dev/null
+++ b/client/src/images/shuffle.svg
@@ -0,0 +1,52 @@
+
+
+
+
diff --git a/client/src/modules/front_end_components/Confirmation.js b/client/src/modules/front_end_components/Confirmation.js
index 17cb9da..83e8785 100644
--- a/client/src/modules/front_end_components/Confirmation.js
+++ b/client/src/modules/front_end_components/Confirmation.js
@@ -1,16 +1,20 @@
import { toast } from './Toast.js';
-export const Confirmation = (message, onYes) => {
+export const Confirmation = (message, onYes = null) => {
document.querySelector('#confirmation')?.remove();
document.querySelector('#confirmation-background')?.remove();
let confirmation = document.createElement('div');
confirmation.setAttribute('id', 'confirmation');
- confirmation.innerHTML =
- `
All players must join to start.
`,
- END_GAME_PROMPT:
- `
diff --git a/client/src/modules/game_state/states/Ended.js b/client/src/modules/game_state/states/Ended.js
index ea4e9fd..773c184 100644
--- a/client/src/modules/game_state/states/Ended.js
+++ b/client/src/modules/game_state/states/Ended.js
@@ -1,9 +1,6 @@
import { globals } from '../../../config/globals.js';
import { HTMLFragments } from '../../front_end_components/HTMLFragments.js';
-import { XHRUtility } from '../../utility/XHRUtility.js';
-import { UserUtility } from '../../utility/UserUtility.js';
-import { toast } from '../../front_end_components/Toast.js';
-import { Confirmation } from '../../front_end_components/Confirmation.js';
+import { SharedStateUtil } from './shared/SharedStateUtil.js';
export class Ended {
constructor (containerId, stateBucket, socket) {
@@ -11,25 +8,6 @@ export class Ended {
this.socket = socket;
this.container = document.getElementById(containerId);
this.container.innerHTML = HTMLFragments.END_OF_GAME_VIEW;
- this.restartGameHandler = () => {
- XHRUtility.xhr(
- '/api/games/' + this.stateBucket.currentGameState.accessCode + '/restart',
- 'PATCH',
- null,
- JSON.stringify({
- playerName: this.stateBucket.currentGameState.client.name,
- accessCode: this.stateBucket.currentGameState.accessCode,
- sessionCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.LOCAL),
- localCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.PRODUCTION)
- })
- )
- .then((res) => {
- toast('Game restarted!', 'success', true, true, 'medium');
- })
- .catch((res) => {
- toast(res.content, 'error', true, true, 'medium');
- });
- };
}
renderEndOfGame (gameState) {
@@ -37,15 +15,7 @@ export class Ended {
gameState.client.userType === globals.USER_TYPES.MODERATOR
|| gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
) {
- const restartGameContainer = document.createElement('div');
- restartGameContainer.innerHTML = HTMLFragments.RESTART_GAME_BUTTON;
- const button = restartGameContainer.querySelector('#restart-game');
- button.addEventListener('click', () => {
- Confirmation('Restart the game, dealing everyone new roles?', () => {
- this.restartGameHandler();
- });
- });
- document.getElementById('end-of-game-buttons').prepend(restartGameContainer);
+ document.getElementById('end-of-game-buttons').prepend(SharedStateUtil.createRestartButton(this.stateBucket));
}
this.renderPlayersWithRoleInformation();
}
diff --git a/client/src/modules/game_state/states/InProgress.js b/client/src/modules/game_state/states/InProgress.js
index 1bd83d5..f690ca8 100644
--- a/client/src/modules/game_state/states/InProgress.js
+++ b/client/src/modules/game_state/states/InProgress.js
@@ -5,15 +5,13 @@ import { Confirmation } from '../../front_end_components/Confirmation.js';
import { ModalManager } from '../../front_end_components/ModalManager.js';
import { GameTimerManager } from '../../timer/GameTimerManager.js';
import { stateBucket } from '../StateBucket.js';
+import { SharedStateUtil } from './shared/SharedStateUtil.js';
export class InProgress {
constructor (containerId, stateBucket, socket) {
this.stateBucket = stateBucket;
this.socket = socket;
this.container = document.getElementById(containerId);
- this.components = {
-
- };
this.killPlayerHandlers = {};
this.revealRoleHandlers = {};
this.transferModHandlers = {};
@@ -191,6 +189,15 @@ export class InProgress {
}
});
+ if (this.socket.hasListeners(globals.EVENT_IDS.NEW_SPECTATOR)) {
+ this.socket.removeAllListeners(globals.EVENT_IDS.NEW_SPECTATOR);
+ }
+
+ this.socket.on(globals.EVENT_IDS.NEW_SPECTATOR, (spectator) => {
+ stateBucket.currentGameState.spectators.push(spectator);
+ this.displayAvailableModerators();
+ });
+
if (this.stateBucket.currentGameState.timerParams) {
const timerWorker = new Worker(new URL('../../timer/Timer.js', import.meta.url));
const gameTimerManager = new GameTimerManager(stateBucket, this.socket);
@@ -407,9 +414,9 @@ function removeExistingPlayerElements (killPlayerHandlers, revealRoleHandlers) {
}
function createEndGamePromptComponent (socket, stateBucket) {
- if (document.querySelector('#end-game-prompt') === null) {
+ if (document.querySelector('#game-control-prompt') === null) {
const div = document.createElement('div');
- div.innerHTML = HTMLFragments.END_GAME_PROMPT;
+ div.innerHTML = HTMLFragments.GAME_CONTROL_PROMPT;
div.querySelector('#end-game-button').addEventListener('click', (e) => {
e.preventDefault();
Confirmation('End the game?', () => {
@@ -419,11 +426,12 @@ function createEndGamePromptComponent (socket, stateBucket) {
stateBucket.currentGameState.accessCode,
null,
() => {
- document.querySelector('#end-game-prompt')?.remove();
+ document.querySelector('#game-control-prompt')?.remove();
}
);
});
});
+ div.querySelector('#game-control-prompt').prepend(SharedStateUtil.createRestartButton(stateBucket));
document.getElementById('game-content').appendChild(div);
}
}
diff --git a/client/src/modules/game_state/states/shared/SharedStateUtil.js b/client/src/modules/game_state/states/shared/SharedStateUtil.js
new file mode 100644
index 0000000..10f32dd
--- /dev/null
+++ b/client/src/modules/game_state/states/shared/SharedStateUtil.js
@@ -0,0 +1,240 @@
+import { XHRUtility } from '../../../utility/XHRUtility.js';
+import { UserUtility } from '../../../utility/UserUtility.js';
+import { globals } from '../../../../config/globals.js';
+import { toast } from '../../../front_end_components/Toast.js';
+import { Confirmation } from '../../../front_end_components/Confirmation.js';
+import { Lobby } from '../Lobby.js';
+import { stateBucket } from '../../StateBucket.js';
+import { InProgress } from '../InProgress.js';
+import { Ended } from '../Ended.js';
+import { HTMLFragments } from '../../../front_end_components/HTMLFragments.js';
+import { ModalManager } from '../../../front_end_components/ModalManager.js';
+
+// This constant is meant to house logic that is utilized by more than one game state
+export const SharedStateUtil = {
+ restartHandler: (stateBucket) => {
+ XHRUtility.xhr(
+ '/api/games/' + stateBucket.currentGameState.accessCode + '/restart',
+ 'PATCH',
+ null,
+ JSON.stringify({
+ playerName: stateBucket.currentGameState.client.name,
+ accessCode: stateBucket.currentGameState.accessCode,
+ sessionCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.LOCAL),
+ localCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.PRODUCTION)
+ })
+ )
+ .then((res) => {})
+ .catch((res) => {
+ toast(res.content, 'error', true, true, 'medium');
+ });
+ },
+
+ createRestartButton: (stateBucket) => {
+ const restartGameButton = document.createElement('button');
+ restartGameButton.classList.add('app-button');
+ restartGameButton.setAttribute('id', 'restart-game-button');
+ restartGameButton.innerText = 'Restart';
+ restartGameButton.addEventListener('click', () => {
+ Confirmation('Restart the game, dealing everyone new roles?', () => {
+ SharedStateUtil.restartHandler(stateBucket);
+ });
+ });
+
+ return restartGameButton;
+ },
+
+ setClientSocketHandlers: (stateBucket, socket) => {
+ const commonAckLogic = (gameState) => {
+ stateBucket.currentGameState = gameState;
+ processGameState(
+ stateBucket.currentGameState,
+ gameState.client.cookie,
+ socket,
+ true,
+ true
+ );
+ };
+ const startGameStateAckFn = (gameState) => {
+ commonAckLogic(gameState);
+ toast('Game started!', 'success');
+ };
+
+ const restartGameStateAckFn = (gameState) => {
+ commonAckLogic(gameState);
+ toast('Game restarted!', 'success');
+ };
+
+ const fetchGameStateHandler = (ackFn) => {
+ socket.emit(
+ globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
+ globals.EVENT_IDS.FETCH_GAME_STATE,
+ stateBucket.currentGameState.accessCode,
+ { personId: stateBucket.currentGameState.client.cookie },
+ ackFn
+ );
+ };
+
+ socket.on(globals.EVENT_IDS.START_GAME, () => { fetchGameStateHandler(startGameStateAckFn); });
+
+ socket.on(globals.EVENT_IDS.RESTART_GAME, () => { fetchGameStateHandler(restartGameStateAckFn); });
+
+ socket.on(globals.EVENT_IDS.SYNC_GAME_STATE, () => {
+ socket.emit(
+ globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
+ globals.EVENT_IDS.FETCH_GAME_STATE,
+ stateBucket.currentGameState.accessCode,
+ { personId: stateBucket.currentGameState.client.cookie },
+ function (gameState) {
+ stateBucket.currentGameState = gameState;
+ processGameState(
+ stateBucket.currentGameState,
+ gameState.client.cookie,
+ socket,
+ true,
+ true
+ );
+ }
+ );
+ });
+
+ socket.on(globals.COMMANDS.END_GAME, (people) => {
+ stateBucket.currentGameState.people = people;
+ stateBucket.currentGameState.status = globals.STATUS.ENDED;
+ processGameState(
+ stateBucket.currentGameState,
+ stateBucket.currentGameState.client.cookie,
+ socket,
+ true,
+ true
+ );
+ });
+ },
+
+ syncWithGame: (stateBucket, socket, cookie, window) => {
+ const splitUrl = window.location.href.split('/game/');
+ const accessCode = splitUrl[1];
+ if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) {
+ socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.FETCH_GAME_STATE, accessCode, { personId: cookie }, function (gameState) {
+ if (gameState === null) {
+ window.location = '/not-found?reason=' + encodeURIComponent('game-not-found');
+ } else {
+ stateBucket.currentGameState = gameState;
+ document.querySelector('.spinner-container')?.remove();
+ document.querySelector('.spinner-background')?.remove();
+ document.getElementById('game-content').innerHTML = HTMLFragments.INITIAL_GAME_DOM;
+ toast('You are connected.', 'success', true, true, 'short');
+ processGameState(stateBucket.currentGameState, cookie, socket, true, true);
+ }
+ });
+ } else {
+ window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code');
+ }
+ }
+};
+
+function processGameState (
+ currentGameState,
+ userId,
+ socket,
+ refreshPrompt = true,
+ animateContainer = false
+) {
+ const containerAnimation = document.getElementById('game-state-container').animate(
+ [
+ { opacity: '0', transform: 'translateY(10px)' },
+ { opacity: '1', transform: 'translateY(0px)' }
+ ], {
+ duration: 500,
+ easing: 'ease-in-out',
+ fill: 'both'
+ });
+ if (animateContainer) {
+ containerAnimation.play();
+ }
+
+ displayClientInfo(currentGameState.client.name, currentGameState.client.userType);
+
+ switch (currentGameState.status) {
+ case globals.STATUS.LOBBY:
+ const lobby = new Lobby('game-state-container', stateBucket, socket);
+ if (refreshPrompt) {
+ lobby.removeStartGameFunctionalityIfPresent();
+ }
+ lobby.populateHeader();
+ lobby.populatePlayers();
+ lobby.setSocketHandlers();
+ if ((
+ currentGameState.client.userType === globals.USER_TYPES.MODERATOR
+ || currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
+ )
+ && refreshPrompt
+ ) {
+ lobby.displayStartGamePromptForModerators();
+ }
+ break;
+ case globals.STATUS.IN_PROGRESS:
+ if (refreshPrompt) {
+ document.querySelector('#game-control-prompt')?.remove();
+ }
+ const inProgressGame = new InProgress('game-state-container', stateBucket, socket);
+ inProgressGame.setSocketHandlers();
+ inProgressGame.setUserView(currentGameState.client.userType);
+ break;
+ case globals.STATUS.ENDED: {
+ const ended = new Ended('game-state-container', stateBucket, socket);
+ ended.renderEndOfGame(currentGameState);
+ break;
+ }
+ default:
+ break;
+ }
+
+ activateRoleInfoButton(stateBucket.currentGameState.deck);
+}
+
+function activateRoleInfoButton (deck) {
+ deck.sort((a, b) => {
+ return a.team === globals.ALIGNMENT.GOOD ? -1 : 1;
+ });
+ document.getElementById('role-info-button').addEventListener('click', (e) => {
+ e.preventDefault();
+ document.getElementById('role-info-prompt').innerHTML = HTMLFragments.ROLE_INFO_MODAL;
+ const modalContent = document.getElementById('game-role-info-container');
+ for (const card of deck) {
+ const roleDiv = document.createElement('div');
+ const roleNameDiv = document.createElement('div');
+
+ roleNameDiv.classList.add('role-info-name');
+
+ const roleName = document.createElement('h5');
+ const roleQuantity = document.createElement('h5');
+ const roleDescription = document.createElement('p');
+
+ roleDescription.innerText = card.description;
+ roleName.innerText = card.role;
+ roleQuantity.innerText = card.quantity + 'x';
+
+ if (card.team === globals.ALIGNMENT.GOOD) {
+ roleName.classList.add(globals.ALIGNMENT.GOOD);
+ } else {
+ roleName.classList.add(globals.ALIGNMENT.EVIL);
+ }
+
+ roleNameDiv.appendChild(roleQuantity);
+ roleNameDiv.appendChild(roleName);
+
+ roleDiv.appendChild(roleNameDiv);
+ roleDiv.appendChild(roleDescription);
+
+ modalContent.appendChild(roleDiv);
+ }
+ ModalManager.displayModal('role-info-modal', 'role-info-modal-background', 'close-role-info-modal-button');
+ });
+}
+
+function displayClientInfo (name, userType) {
+ document.getElementById('client-name').innerText = name;
+ document.getElementById('client-user-type').innerText = userType;
+ document.getElementById('client-user-type').innerText += globals.USER_TYPE_ICONS[userType];
+}
diff --git a/client/src/modules/page_handlers/gameHandler.js b/client/src/modules/page_handlers/gameHandler.js
index b2421ea..1dcf371 100644
--- a/client/src/modules/page_handlers/gameHandler.js
+++ b/client/src/modules/page_handlers/gameHandler.js
@@ -2,12 +2,7 @@ import { injectNavbar } from '../front_end_components/Navbar.js';
import { stateBucket } from '../game_state/StateBucket.js';
import { UserUtility } from '../utility/UserUtility.js';
import { toast } from '../front_end_components/Toast.js';
-import { globals } from '../../config/globals.js';
-import { HTMLFragments } from '../front_end_components/HTMLFragments.js';
-import { ModalManager } from '../front_end_components/ModalManager.js';
-import { Lobby } from '../game_state/states/Lobby.js';
-import { InProgress } from '../game_state/states/InProgress.js';
-import { Ended } from '../game_state/states/Ended.js';
+import { SharedStateUtil } from '../game_state/states/shared/SharedStateUtil.js';
export const gameHandler = async (socket, XHRUtility, window, gameDOM) => {
document.body.innerHTML = gameDOM + document.body.innerHTML;
@@ -25,7 +20,7 @@ export const gameHandler = async (socket, XHRUtility, window, gameDOM) => {
stateBucket.environment = response.content;
socket.on('connect', function () {
- syncWithGame(
+ SharedStateUtil.syncWithGame(
stateBucket,
socket,
UserUtility.validateAnonUserSignature(response.content),
@@ -41,185 +36,5 @@ export const gameHandler = async (socket, XHRUtility, window, gameDOM) => {
toast('Disconnected. Attempting reconnect...', 'error', true, false);
});
- setClientSocketHandlers(stateBucket, socket);
+ SharedStateUtil.setClientSocketHandlers(stateBucket, socket);
};
-
-function syncWithGame (stateBucket, socket, cookie, window) {
- const splitUrl = window.location.href.split('/game/');
- const accessCode = splitUrl[1];
- if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) {
- socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.FETCH_GAME_STATE, accessCode, { personId: cookie }, function (gameState) {
- if (gameState === null) {
- window.location = '/not-found?reason=' + encodeURIComponent('game-not-found');
- } else {
- stateBucket.currentGameState = gameState;
- document.querySelector('.spinner-container')?.remove();
- document.querySelector('.spinner-background')?.remove();
- document.getElementById('game-content').innerHTML = HTMLFragments.INITIAL_GAME_DOM;
- toast('You are connected.', 'success', true, true, 'short');
- processGameState(stateBucket.currentGameState, cookie, socket, true, true);
- }
- });
- } else {
- window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code');
- }
-}
-
-function processGameState (
- currentGameState,
- userId,
- socket,
- refreshPrompt = true,
- animateContainer = false
-) {
- const containerAnimation = document.getElementById('game-state-container').animate(
- [
- { opacity: '0', transform: 'translateY(10px)' },
- { opacity: '1', transform: 'translateY(0px)' }
- ], {
- duration: 500,
- easing: 'ease-in-out',
- fill: 'both'
- });
- if (animateContainer) {
- containerAnimation.play();
- }
-
- displayClientInfo(currentGameState.client.name, currentGameState.client.userType);
-
- switch (currentGameState.status) {
- case globals.STATUS.LOBBY:
- const lobby = new Lobby('game-state-container', stateBucket, socket);
- if (refreshPrompt) {
- lobby.removeStartGameFunctionalityIfPresent();
- }
- lobby.populateHeader();
- lobby.populatePlayers();
- lobby.setSocketHandlers();
- if ((
- currentGameState.client.userType === globals.USER_TYPES.MODERATOR
- || currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
- )
- && refreshPrompt
- ) {
- lobby.displayStartGamePromptForModerators();
- }
- break;
- case globals.STATUS.IN_PROGRESS:
- if (refreshPrompt) {
- document.querySelector('#end-game-prompt')?.remove();
- }
- const inProgressGame = new InProgress('game-state-container', stateBucket, socket);
- inProgressGame.setSocketHandlers();
- inProgressGame.setUserView(currentGameState.client.userType);
- break;
- case globals.STATUS.ENDED: {
- const ended = new Ended('game-state-container', stateBucket, socket);
- ended.renderEndOfGame(currentGameState);
- break;
- }
- default:
- break;
- }
-
- activateRoleInfoButton(stateBucket.currentGameState.deck);
-}
-
-function activateRoleInfoButton (deck) {
- deck.sort((a, b) => {
- return a.team === globals.ALIGNMENT.GOOD ? -1 : 1;
- });
- document.getElementById('role-info-button').addEventListener('click', (e) => {
- e.preventDefault();
- document.getElementById('role-info-prompt').innerHTML = HTMLFragments.ROLE_INFO_MODAL;
- const modalContent = document.getElementById('game-role-info-container');
- for (const card of deck) {
- const roleDiv = document.createElement('div');
- const roleNameDiv = document.createElement('div');
-
- roleNameDiv.classList.add('role-info-name');
-
- const roleName = document.createElement('h5');
- const roleQuantity = document.createElement('h5');
- const roleDescription = document.createElement('p');
-
- roleDescription.innerText = card.description;
- roleName.innerText = card.role;
- roleQuantity.innerText = card.quantity + 'x';
-
- if (card.team === globals.ALIGNMENT.GOOD) {
- roleName.classList.add(globals.ALIGNMENT.GOOD);
- } else {
- roleName.classList.add(globals.ALIGNMENT.EVIL);
- }
-
- roleNameDiv.appendChild(roleQuantity);
- roleNameDiv.appendChild(roleName);
-
- roleDiv.appendChild(roleNameDiv);
- roleDiv.appendChild(roleDescription);
-
- modalContent.appendChild(roleDiv);
- }
- ModalManager.displayModal('role-info-modal', 'role-info-modal-background', 'close-role-info-modal-button');
- });
-}
-
-function displayClientInfo (name, userType) {
- document.getElementById('client-name').innerText = name;
- document.getElementById('client-user-type').innerText = userType;
- document.getElementById('client-user-type').innerText += globals.USER_TYPE_ICONS[userType];
-}
-
-// Should be reserved for socket events not specific to any one game state (Lobby, In Progress, etc.)
-function setClientSocketHandlers (stateBucket, socket) {
- socket.on(globals.EVENT_IDS.START_GAME, () => {
- socket.emit(
- globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
- globals.EVENT_IDS.FETCH_GAME_STATE,
- stateBucket.currentGameState.accessCode,
- { personId: stateBucket.currentGameState.client.cookie },
- function (gameState) {
- stateBucket.currentGameState = gameState;
- processGameState(
- stateBucket.currentGameState,
- gameState.client.cookie,
- socket,
- true,
- true
- );
- }
- );
- });
-
- socket.on(globals.EVENT_IDS.SYNC_GAME_STATE, () => {
- socket.emit(
- globals.SOCKET_EVENTS.IN_GAME_MESSAGE,
- globals.EVENT_IDS.FETCH_GAME_STATE,
- stateBucket.currentGameState.accessCode,
- { personId: stateBucket.currentGameState.client.cookie },
- function (gameState) {
- stateBucket.currentGameState = gameState;
- processGameState(
- stateBucket.currentGameState,
- gameState.client.cookie,
- socket,
- true,
- true
- );
- }
- );
- });
-
- socket.on(globals.COMMANDS.END_GAME, (people) => {
- stateBucket.currentGameState.people = people;
- stateBucket.currentGameState.status = globals.STATUS.ENDED;
- processGameState(
- stateBucket.currentGameState,
- stateBucket.currentGameState.client.cookie,
- socket,
- true,
- true
- );
- });
-}
diff --git a/client/src/modules/timer/GameTimerManager.js b/client/src/modules/timer/GameTimerManager.js
index ef43c5a..e44d8cd 100644
--- a/client/src/modules/timer/GameTimerManager.js
+++ b/client/src/modules/timer/GameTimerManager.js
@@ -1,4 +1,5 @@
import { globals } from '../../config/globals.js';
+import { Confirmation } from '../front_end_components/Confirmation.js';
export class GameTimerManager {
constructor (stateBucket, socket) {
@@ -88,11 +89,21 @@ export class GameTimerManager {
}
displayExpiredTime () {
- const currentBtn = document.querySelector('#play-pause img');
- if (currentBtn) {
- currentBtn.removeEventListener('click', this.pauseListener);
- currentBtn.removeEventListener('click', this.playListener);
- currentBtn.remove();
+ if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
+ || this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
+ const currentBtn = document.querySelector('#timer-container-moderator #play-pause img');
+ if (currentBtn) {
+ currentBtn.removeEventListener('click', this.pauseListener);
+ currentBtn.removeEventListener('click', this.playListener);
+ currentBtn.classList.add('disabled');
+ currentBtn.setAttribute('src', '/images/play-pause-placeholder.svg');
+ } else {
+ document.querySelector('#play-pause-placeholder')?.remove();
+ const placeholderBtn = document.createElement('img');
+ placeholderBtn.setAttribute('src', '../images/play-pause-placeholder.svg');
+ placeholderBtn.classList.add('disabled');
+ document.getElementById('play-pause').appendChild(placeholderBtn);
+ }
}
const timer = document.getElementById('game-timer');
@@ -123,6 +134,12 @@ export class GameTimerManager {
}
});
}
+
+ if (!socket.hasListeners(globals.COMMANDS.END_TIMER)) {
+ socket.on(globals.COMMANDS.END_TIMER, () => {
+ Confirmation('The timer has expired!');
+ });
+ }
}
swapToPlayButton () {
diff --git a/client/src/styles/confirmation.css b/client/src/styles/confirmation.css
index 512d035..9bf69a2 100644
--- a/client/src/styles/confirmation.css
+++ b/client/src/styles/confirmation.css
@@ -34,7 +34,7 @@
margin: 1em 0 2em 0;
}
-.confirmation-buttons button {
+.confirmation-buttons button, .confirmation-buttons-centered button {
min-width: 5em;
}
@@ -43,6 +43,11 @@
justify-content: space-between;
}
+.confirmation-buttons-centered {
+ display: flex;
+ justify-content: center;
+}
+
#confirmation-cancel-button {
background-color: #762323 !important;
}
diff --git a/client/src/styles/game.css b/client/src/styles/game.css
index 2bcaa95..d68457a 100644
--- a/client/src/styles/game.css
+++ b/client/src/styles/game.css
@@ -58,11 +58,13 @@
max-width: 17em;
}
-#restart-game {
- background-color: #0078D7;
- min-width: 10em;
- margin-bottom: 1em !important;
- animation: shadow-pulse 1.5s infinite ease-out;
+#restart-game-button, #mod-transfer-button {
+ background-color: #045EA6;
+}
+
+#restart-game-button:hover, #mod-transfer-button:hover {
+ background-color: #0078D773;
+ border: 2px solid #045EA6;
}
#play-pause-placeholder {
@@ -130,7 +132,15 @@ h1 {
#end-of-game-header button {
margin: 0.5em;
min-width: 12em;
+ font-size: 18px;
}
+
+#end-of-game-header #restart-game-button {
+ margin-bottom: 1em !important;
+ animation: shadow-pulse 1.5s infinite ease-out;
+ padding: 10px;
+}
+
.potential-moderator {
display: flex;
color: #d7d7d7;
@@ -194,7 +204,7 @@ h1 {
color: #21ba45;
}
-#role-info-button img {
+#role-info-button img, #mod-transfer-button img {
height: 25px;
margin-left: 10px;
}
@@ -443,7 +453,7 @@ label[for='moderator'] {
font-size: 30px;
}
-#start-game-prompt, #end-game-prompt {
+#start-game-prompt, #game-control-prompt {
padding: 0.5em 0;
display: flex;
flex-direction: column;
@@ -466,6 +476,34 @@ label[for='moderator'] {
background-color: #1b1a24;
}
+#game-control-prompt {
+ padding: 0.5em 0;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ position: fixed;
+ z-index: 3;
+ font-family: 'signika-negative', sans-serif;
+ font-weight: 100;
+ box-shadow: 0 -6px 15px rgba(0, 0, 0, 0.5);
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 3px;
+ /* width: fit-content; */
+ font-size: 20px;
+ height: 100px;
+ margin: 0 auto;
+ max-width: 100%;
+ background-color: #1b1a24;
+}
+
+#game-control-prompt button {
+ margin: 0 15px;
+ min-width: 5em;
+}
+
#start-game-prompt p {
color: whitesmoke;
font-size: 15px;
@@ -478,11 +516,11 @@ label[for='moderator'] {
justify-content: space-evenly;
}
-#end-game-prompt {
+#game-control-prompt {
box-shadow: 0 -6px 40px black;
}
-#start-game-button, #end-game-button {
+#start-game-button, #end-game-button, #restart-game-button {
font-family: 'signika-negative', sans-serif !important;
padding: 10px;
border-radius: 3px;
@@ -777,7 +815,7 @@ canvas {
}
@media(max-width: 800px) {
- #start-game-prompt, #end-game-prompt {
+ #start-game-prompt, #game-control-prompt {
border-radius: 0;
width: 100%;
bottom: 0;
@@ -840,11 +878,11 @@ canvas {
font-size: 20px;
}
- #start-game-prompt, #end-game-prompt {
+ #start-game-prompt, #game-control-prompt {
height: 65px;
}
- #start-game-button, #end-game-button {
+ #start-game-button, #end-game-button, #restart-game-button {
font-size: 20px;
padding: 5px;
}
diff --git a/package.json b/package.json
index 928130e..e8f6eab 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"start:dev": "NODE_ENV=development && webpack --config client/webpack/webpack-dev.config.js --mode=development && nodemon index.js",
"start:dev:no-hot-reload": "NODE_ENV=development && webpack --config client/webpack/webpack-dev.config.js --mode=development && node index.js",
"start:dev:windows": "SET NODE_ENV=development && webpack --config client/webpack/webpack-dev.config.js --mode=development && nodemon index.js",
+ "start:dev:windows:no-hot-reload:debug": "SET NODE_ENV=development && webpack --config client/webpack/webpack-dev.config.js --mode=development && node --inspect index.js",
"start:dev:windows:no-hot-reload": "SET NODE_ENV=development && webpack --config client/webpack/webpack-dev.config.js --mode=development && node index.js",
"start": "NODE_ENV=production node index.js -- loglevel=debug",
"start:windows": "SET NODE_ENV=production && node index.js -- loglevel=debug port=8080",
diff --git a/server/config/globals.js b/server/config/globals.js
index 77d0dec..e8972f8 100644
--- a/server/config/globals.js
+++ b/server/config/globals.js
@@ -34,7 +34,8 @@ const globals = {
REVEAL_PLAYER: 'revealPlayer',
TRANSFER_MODERATOR: 'transferModerator',
CHANGE_NAME: 'changeName',
- END_GAME: 'endGame'
+ END_GAME: 'endGame',
+ RESTART_GAME: 'restartGame'
},
MESSAGES: {
ENTER_NAME: 'Client must enter name.'
diff --git a/server/modules/ActiveGameRunner.js b/server/modules/ActiveGameRunner.js
index 1749bee..fc7c2f7 100644
--- a/server/modules/ActiveGameRunner.js
+++ b/server/modules/ActiveGameRunner.js
@@ -21,11 +21,13 @@ class ActiveGameRunner {
this.logger.debug('running game ' + game.accessCode);
const gameProcess = fork(path.join(__dirname, '/GameProcess.js'));
this.timerThreads[game.accessCode] = gameProcess;
+ this.logger.debug('game ' + game.accessCode + ' now associated with subProcess ' + gameProcess.pid);
gameProcess.on('message', (msg) => {
switch (msg.command) {
case globals.GAME_PROCESS_COMMANDS.END_TIMER:
game.timerParams.paused = false;
game.timerParams.timeRemaining = 0;
+ namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_TIMER);
this.logger.trace('PARENT: END TIMER');
break;
case globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER:
@@ -51,9 +53,8 @@ class ActiveGameRunner {
}
});
- gameProcess.on('exit', () => {
- this.logger.debug('Game ' + game.accessCode + ' timer has expired.');
- delete this.timerThreads[game.accessCode];
+ gameProcess.on('exit', (code, signal) => {
+ this.logger.debug('Game timer thread ' + gameProcess.pid + ' exiting with code ' + code + ' - game ' + game.accessCode);
});
gameProcess.send({
command: globals.GAME_PROCESS_COMMANDS.START_TIMER,
diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js
index c10834b..23b0e89 100644
--- a/server/modules/GameManager.js
+++ b/server/modules/GameManager.js
@@ -71,7 +71,7 @@ class GameManager {
pauseTimer = (game, logger) => {
const thread = this.activeGameRunner.timerThreads[game.accessCode];
- if (thread) {
+ if (thread && !thread.killed) {
this.logger.debug('Timer thread found for game ' + game.accessCode);
thread.send({
command: globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER,
@@ -83,7 +83,7 @@ class GameManager {
resumeTimer = (game, logger) => {
const thread = this.activeGameRunner.timerThreads[game.accessCode];
- if (thread) {
+ if (thread && !thread.killed) {
this.logger.debug('Timer thread found for game ' + game.accessCode);
thread.send({
command: globals.GAME_PROCESS_COMMANDS.RESUME_TIMER,
@@ -95,14 +95,14 @@ class GameManager {
getTimeRemaining = (game, socket) => {
const thread = this.activeGameRunner.timerThreads[game.accessCode];
- if (thread) {
+ if (thread && (!thread.killed && thread.exitCode === null)) {
thread.send({
command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
accessCode: game.accessCode,
socketId: socket.id,
logLevel: this.logger.logLevel
});
- } else {
+ } else if (thread) {
if (game.timerParams && game.timerParams.timeRemaining === 0) {
this.namespace.to(socket.id).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
}
@@ -143,7 +143,6 @@ class GameManager {
if (this.activeGameRunner.timerThreads[game.accessCode]) {
this.logger.trace('KILLING TIMER PROCESS FOR ENDED GAME ' + game.accessCode);
this.activeGameRunner.timerThreads[game.accessCode].kill();
- delete this.activeGameRunner.timerThreads[game.accessCode];
}
for (const person of game.people) {
person.revealed = true;
@@ -266,9 +265,13 @@ class GameManager {
restartGame = async (game, namespace) => {
// kill any outstanding timer threads
- if (this.activeGameRunner.timerThreads[game.accessCode]) {
- this.logger.info('KILLING STALE TIMER PROCESS FOR ' + game.accessCode);
- this.activeGameRunner.timerThreads[game.accessCode].kill();
+ const subProcess = this.activeGameRunner.timerThreads[game.accessCode];
+ if (subProcess) {
+ if (!subProcess.killed) {
+ this.logger.info('Killing timer process ' + subProcess.pid + ' for: ' + game.accessCode);
+ this.activeGameRunner.timerThreads[game.accessCode].kill();
+ }
+ this.logger.debug('Deleting reference to subprocess ' + subProcess.pid);
delete this.activeGameRunner.timerThreads[game.accessCode];
}
@@ -296,23 +299,11 @@ class GameManager {
game.people[i].alignment = cards[i].team;
}
- /* If the game was originally set up with a TEMP mod and the game has gone far enough to establish
- a DEDICATED mod, make the current mod a TEMP mod for the restart. */
- if (!game.hasDedicatedModerator && game.moderator.userType === globals.USER_TYPES.MODERATOR) {
- game.moderator.userType = globals.USER_TYPES.TEMPORARY_MODERATOR;
- }
-
- /* If the game was originally set up with a DEDICATED moderator and the current mod is DIFFERENT from that mod
- (i.e. they transferred their powers at some point), check if the current mod was once a player (i.e. they have
- a game role). If they were once a player, make them a temp mod for the restart. Otherwise, they were a
- spectator, and we want to leave them as a dedicated moderator.
+ /* If there is currently a dedicated mod, and that person was once a player (i.e. they have a game role), make
+ them a temporary mod for the restarted game.
*/
- if (game.hasDedicatedModerator && game.moderator.id !== game.originalModeratorId) {
- if (game.moderator.gameRole) {
- game.moderator.userType = globals.USER_TYPES.TEMPORARY_MODERATOR;
- } else {
- game.moderator.userType = globals.USER_TYPES.MODERATOR;
- }
+ if (game.moderator.gameRole && game.moderator.userType === globals.USER_TYPES.MODERATOR) {
+ game.moderator.userType = globals.USER_TYPES.TEMPORARY_MODERATOR;
}
// start the new game
@@ -322,7 +313,7 @@ class GameManager {
this.activeGameRunner.runGame(game, namespace);
}
- namespace.in(game.accessCode).emit(globals.EVENT_IDS.START_GAME);
+ namespace.in(game.accessCode).emit(globals.EVENT_IDS.RESTART_GAME);
};
handleRequestForGameState = async (game, namespace, logger, gameRunner, accessCode, personCookie, ackFn, clientSocket) => {
@@ -330,12 +321,12 @@ class GameManager {
if (matchingPerson) {
if (matchingPerson.socketId === clientSocket.id) {
logger.trace('matching person found with an established connection to the room: ' + matchingPerson.name);
- ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson, gameRunner, clientSocket, logger));
+ ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson));
} else {
logger.trace('matching person found with a new connection to the room: ' + matchingPerson.name);
clientSocket.join(accessCode);
matchingPerson.socketId = clientSocket.id;
- ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson, gameRunner, clientSocket, logger));
+ ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson));
}
} else {
rejectClientRequestForGameState(ackFn);
diff --git a/server/modules/GameStateCurator.js b/server/modules/GameStateCurator.js
index 2089af0..317bb5c 100644
--- a/server/modules/GameStateCurator.js
+++ b/server/modules/GameStateCurator.js
@@ -5,8 +5,8 @@ const globals = require('../config/globals');
information that they shouldn't.
*/
const GameStateCurator = {
- getGameStateFromPerspectiveOfPerson: (game, person, gameRunner) => {
- return getGameStateBasedOnPermissions(game, person, gameRunner);
+ getGameStateFromPerspectiveOfPerson: (game, person) => {
+ return getGameStateBasedOnPermissions(game, person);
},
mapPeopleForModerator: (people) => {
@@ -42,7 +42,7 @@ const GameStateCurator = {
}
};
-function getGameStateBasedOnPermissions (game, person, gameRunner) {
+function getGameStateBasedOnPermissions (game, person) {
const client = game.status === globals.STATUS.LOBBY // people won't be able to know their role until past the lobby stage.
? { name: person.name, hasEnteredName: person.hasEnteredName, id: person.id, cookie: person.cookie, userType: person.userType }
: {
diff --git a/spec/e2e/game_spec.js b/spec/e2e/game_spec.js
index eed051d..012bd0f 100644
--- a/spec/e2e/game_spec.js
+++ b/spec/e2e/game_spec.js
@@ -290,7 +290,7 @@ describe('game page', () => {
}
]);
expect(document.getElementById('end-of-game-header')).not.toBeNull();
- expect(document.getElementById('restart-game')).not.toBeNull();
+ expect(document.getElementById('restart-game-button')).not.toBeNull();
});
afterAll(() => {
diff --git a/spec/unit/server/modules/GameManager_Spec.js b/spec/unit/server/modules/GameManager_Spec.js
index b142c12..6fc94a5 100644
--- a/spec/unit/server/modules/GameManager_Spec.js
+++ b/spec/unit/server/modules/GameManager_Spec.js
@@ -222,7 +222,7 @@ describe('GameManager', () => {
);
expect(GameStateCurator.getGameStateFromPerspectiveOfPerson)
- .toHaveBeenCalledWith(gameRunner.activeGames.get('abc'), player, gameRunner, socket, logger);
+ .toHaveBeenCalledWith(gameRunner.activeGames.get('abc'), player);
});
it('should send the game state to a matching person who reset their connection', () => {
@@ -243,7 +243,7 @@ describe('GameManager', () => {
);
expect(GameStateCurator.getGameStateFromPerspectiveOfPerson)
- .toHaveBeenCalledWith(gameRunner.activeGames.get('abc'), player, gameRunner, socket, logger);
+ .toHaveBeenCalledWith(gameRunner.activeGames.get('abc'), player);
expect(player.socketId).toEqual(socket.id);
expect(socket.join).toHaveBeenCalled();
});
@@ -360,7 +360,7 @@ describe('GameManager', () => {
expect(person.gameRole).toBeDefined();
}
expect(shuffleSpy).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
+ expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.RESTART_GAME);
});
it('should reset all relevant game parameters, including when the game has a timer', async () => {
@@ -387,7 +387,7 @@ describe('GameManager', () => {
expect(runGameSpy).toHaveBeenCalled();
expect(Object.keys(gameManager.activeGameRunner.timerThreads).length).toEqual(0);
expect(shuffleSpy).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
+ expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.RESTART_GAME);
});
it('should reset all relevant game parameters and preserve temporary moderator', async () => {
@@ -408,7 +408,7 @@ describe('GameManager', () => {
expect(person.gameRole).toBeDefined();
}
expect(shuffleSpy).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
+ expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.RESTART_GAME);
});
it('should reset all relevant game parameters and restore a temporary moderator from a dedicated moderator', async () => {
@@ -429,7 +429,7 @@ describe('GameManager', () => {
expect(person.gameRole).toBeDefined();
}
expect(shuffleSpy).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
+ expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.RESTART_GAME);
});
it('should reset all relevant game parameters and create a temporary mod if a dedicated mod transferred to a killed player', async () => {
@@ -450,7 +450,7 @@ describe('GameManager', () => {
expect(person.gameRole).toBeDefined();
}
expect(shuffleSpy).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.START_GAME);
+ expect(emitSpy).toHaveBeenCalledWith(globals.EVENT_IDS.RESTART_GAME);
});
});