diff --git a/client/config/globals.js b/client/config/globals.js
index 421a316..333dc99 100644
--- a/client/config/globals.js
+++ b/client/config/globals.js
@@ -45,6 +45,7 @@ export const globals = {
USER_TYPE_ICONS: {
player: ' \uD83C\uDFAE',
moderator: ' \uD83D\uDC51',
- 'player / temp mod': ' \uD83C\uDFAE\uD83D\uDC51'
+ 'player / temp mod': ' \uD83C\uDFAE\uD83D\uDC51',
+ spectator: ' \uD83D\uDC7B'
}
};
diff --git a/client/modules/GameStateRenderer.js b/client/modules/GameStateRenderer.js
index 4d7c22f..3200bb5 100644
--- a/client/modules/GameStateRenderer.js
+++ b/client/modules/GameStateRenderer.js
@@ -68,7 +68,7 @@ export class GameStateRenderer {
let modTransferButton = document.getElementById("mod-transfer-button");
modTransferButton.addEventListener(
"click", () => {
- this.displayAvailableModerators()
+ this.displayAvailableModerators();
ModalManager.displayModal(
"transfer-mod-modal",
"transfer-mod-modal-background",
@@ -79,6 +79,15 @@ export class GameStateRenderer {
this.renderPlayersWithRoleAndAlignmentInfo();
}
+ renderTempModView() {
+ let div = document.createElement("div");
+ div.innerHTML = templates.END_GAME_PROMPT;
+ document.body.appendChild(div);
+
+ renderPlayerRole(this.gameState);
+ this.renderPlayersWithNoRoleInformationUnlessRevealed(true);
+ }
+
renderPlayerView(isKilled=false) {
if (isKilled) {
let clientUserType = document.getElementById("client-user-type");
@@ -87,6 +96,10 @@ export class GameStateRenderer {
}
}
renderPlayerRole(this.gameState);
+ this.renderPlayersWithNoRoleInformationUnlessRevealed(false);
+ }
+
+ renderSpectatorView() {
this.renderPlayersWithNoRoleInformationUnlessRevealed();
}
@@ -123,12 +136,26 @@ export class GameStateRenderer {
}
- renderPlayersWithNoRoleInformationUnlessRevealed() {
+ renderPlayersWithNoRoleInformationUnlessRevealed(tempMod = false) {
+ if (tempMod) {
+ document.querySelectorAll('.game-player').forEach((el) => {
+ let pointer = el.dataset.pointer;
+ if (pointer && this.killPlayerHandlers[pointer]) {
+ el.removeEventListener('click', this.killPlayerHandlers[pointer]);
+ delete this.killPlayerHandlers[pointer];
+ }
+ if (pointer && this.revealRoleHandlers[pointer]) {
+ el.removeEventListener('click', this.revealRoleHandlers[pointer]);
+ delete this.revealRoleHandlers[pointer];
+ }
+ el.remove();
+ });
+ }
document.querySelectorAll('.game-player').forEach((el) => el.remove());
this.gameState.people.sort((a, b) => {
return a.name >= b.name ? 1 : -1;
});
- renderGroupOfPlayers(this.gameState.people, this.killPlayerHandlers);
+ renderGroupOfPlayers(this.gameState, this.killPlayerHandlers, this.revealRoleHandlers, this.gameState.accessCode, null, tempMod, this.socket);
document.getElementById("players-alive-label").innerText =
'Players: ' + this.gameState.people.filter((person) => !person.out).length + ' / ' + this.gameState.people.length + ' Alive';
@@ -153,27 +180,32 @@ export class GameStateRenderer {
});
let modalContent = document.getElementById("transfer-mod-form-content");
if (modalContent) {
- for (let player of this.gameState.people) {
- if (player.out) {
- let container = document.createElement("div");
- container.classList.add('potential-moderator');
- container.dataset.pointer = player.id;
- container.innerText = player.name;
- this.transferModHandlers[player.id] = () => {
- if (confirm("Transfer moderator powers to " + player.name + "?")) {
- socket.emit(globals.COMMANDS.TRANSFER_MODERATOR, this.gameState.accessCode, player.id);
- }
- }
-
- container.addEventListener('click', this.transferModHandlers[player.id]);
- modalContent.appendChild(container);
- }
- }
+ renderPotentialMods(this.gameState, this.gameState.people, this.transferModHandlers, modalContent, this.socket);
+ renderPotentialMods(this.gameState, this.gameState.spectators, this.transferModHandlers, modalContent, this.socket);
}
}
}
+function renderPotentialMods(gameState, group, transferModHandlers, modalContent, socket) {
+ for (let member of group) {
+ if ((member.out || member.userType === globals.USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) {
+ let container = document.createElement("div");
+ container.classList.add('potential-moderator');
+ container.dataset.pointer = member.id;
+ container.innerText = member.name;
+ transferModHandlers[member.id] = () => {
+ if (confirm("Transfer moderator powers to " + member.name + "?")) {
+ socket.emit(globals.COMMANDS.TRANSFER_MODERATOR, gameState.accessCode, member.id);
+ }
+ }
+
+ container.addEventListener('click', transferModHandlers[member.id]);
+ modalContent.appendChild(container);
+ }
+ }
+}
+
function renderLobbyPerson(name, userType) {
let el = document.createElement("div");
let personNameEl = document.createElement("div");
@@ -204,12 +236,13 @@ function removeExistingTitle() {
}
}
-function renderGroupOfPlayers(players, killPlayerHandlers, revealRoleHandlers, accessCode=null, alignment=null, moderator=false, socket=null) {
- for (let player of players) {
+// TODO: refactor to reduce the cyclomatic complexity of this function
+function renderGroupOfPlayers(gameState, killPlayerHandlers, revealRoleHandlers, accessCode=null, alignment=null, moderator=false, socket=null) {
+ for (let player of gameState.people) {
let container = document.createElement("div");
container.classList.add('game-player');
- container.dataset.pointer = player.id;
- if (alignment) {
+ if (moderator) {
+ container.dataset.pointer = player.id;
container.innerHTML = templates.MODERATOR_PLAYER;
} else {
container.innerHTML = templates.GAME_PLAYER;
@@ -219,14 +252,24 @@ function renderGroupOfPlayers(players, killPlayerHandlers, revealRoleHandlers, a
if (moderator) {
roleElement.classList.add(alignment);
- roleElement.innerText = player.gameRole;
- document.getElementById("player-list-moderator-team-" + alignment).appendChild(container);
+ if (gameState.moderator.userType === globals.USER_TYPES.MODERATOR) {
+ roleElement.innerText = player.gameRole;
+ document.getElementById("player-list-moderator-team-" + alignment).appendChild(container);
+ } else {
+ if (player.revealed) {
+ roleElement.innerText = player.gameRole;
+ roleElement.classList.add(player.alignment);
+ } else {
+ roleElement.innerText = "Unknown";
+ }
+ document.getElementById("game-player-list").appendChild(container);
+ }
} else if (player.revealed) {
roleElement.classList.add(player.alignment);
roleElement.innerText = player.gameRole;
document.getElementById("game-player-list").appendChild(container);
} else {
- roleElement.innerText = "Unknown"
+ roleElement.innerText = "Unknown";
document.getElementById("game-player-list").appendChild(container);
}
diff --git a/client/modules/Templates.js b/client/modules/Templates.js
index 42242f2..4a1a09b 100644
--- a/client/modules/Templates.js
+++ b/client/modules/Templates.js
@@ -45,11 +45,24 @@ export const templates = {
"" +
"
" +
"",
+ SPECTATOR_GAME_VIEW:
+ "" +
+ "",
MODERATOR_GAME_VIEW:
"" +
"" +
"" +
"",
+ TEMP_MOD_GAME_VIEW:
+ "" +
+ "" +
+ "" +
+ "" +
+ "
" +
+ "
![role]()
" +
+ "
" +
+ "
" +
+ "" +
+ "
Click to reveal your role
" +
+ "
(click again to hide)
" +
+ "
" +
+ "" +
+ "",
MODERATOR_PLAYER:
"" +
"
" +
diff --git a/client/scripts/game.js b/client/scripts/game.js
index 9e59192..0988a8c 100644
--- a/client/scripts/game.js
+++ b/client/scripts/game.js
@@ -41,7 +41,6 @@ function prepareGamePage(environment, socket, timerWorker) {
gameTimerManager = new GameTimerManager(gameState, socket);
}
setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTimerManager);
- displayClientInfo(gameState.client.name, gameState.client.userType);
processGameState(gameState, userId, socket, gameStateRenderer);
}
});
@@ -51,6 +50,7 @@ function prepareGamePage(environment, socket, timerWorker) {
}
function processGameState (gameState, userId, socket, gameStateRenderer) {
+ displayClientInfo(gameState.client.name, gameState.client.userType);
switch (gameState.status) {
case globals.STATUS.LOBBY:
document.getElementById("game-state-container").innerHTML = templates.LOBBY;
@@ -67,7 +67,6 @@ function processGameState (gameState, userId, socket, gameStateRenderer) {
}
break;
case globals.STATUS.IN_PROGRESS:
- gameStateRenderer.gameState = gameState;
gameStateRenderer.renderGameHeader();
switch (gameState.client.userType) {
case globals.USER_TYPES.PLAYER:
@@ -75,6 +74,7 @@ function processGameState (gameState, userId, socket, gameStateRenderer) {
gameStateRenderer.renderPlayerView();
break;
case globals.USER_TYPES.KILLED_PLAYER:
+ document.querySelector("#end-game-prompt")?.remove();
document.getElementById("game-state-container").innerHTML = templates.PLAYER_GAME_VIEW;
gameStateRenderer.renderPlayerView(true);
break;
@@ -85,6 +85,13 @@ function processGameState (gameState, userId, socket, gameStateRenderer) {
break;
case globals.USER_TYPES.TEMPORARY_MODERATOR:
document.querySelector("#start-game-prompt")?.remove();
+ document.getElementById("game-state-container").innerHTML = templates.TEMP_MOD_GAME_VIEW;
+ gameStateRenderer.renderTempModView();
+ break;
+ case globals.USER_TYPES.SPECTATOR:
+ document.querySelector("#end-game-prompt")?.remove();
+ document.getElementById("game-state-container").innerHTML = templates.SPECTATOR_GAME_VIEW;
+ gameStateRenderer.renderSpectatorView();
break;
default:
break;
@@ -127,6 +134,8 @@ function setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTim
gameStateRenderer.gameState.accessCode,
gameStateRenderer.gameState.client.cookie,
function (gameState) {
+ gameStateRenderer.gameState = gameState;
+ gameTimerManager.gameState = gameState;
processGameState(gameState, gameState.client.cookie, socket, gameStateRenderer);
}
);
@@ -156,7 +165,11 @@ function setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTim
} else {
toast(killedPerson.name + ' was killed!', 'warning', false, true, 6);
}
- gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed();
+ if (gameStateRenderer.gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
+ gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
+ } else {
+ gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
+ }
}
}
});
@@ -178,7 +191,11 @@ function setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTim
} else {
toast(revealedPerson.name + ' was revealed as a ' + revealedPerson.gameRole + '!', 'warning', false, true, 6);
}
- gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed();
+ if (gameStateRenderer.gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
+ gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
+ } else {
+ gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
+ }
}
}
});
diff --git a/client/styles/GLOBAL.css b/client/styles/GLOBAL.css
index 6a51085..c00c31e 100644
--- a/client/styles/GLOBAL.css
+++ b/client/styles/GLOBAL.css
@@ -96,7 +96,6 @@ button:active, input[type=submit]:active {
flex-direction: column;
justify-content: center;
width: 95%;
- max-width: 68em;
margin: 0 auto;
}
diff --git a/client/styles/game.css b/client/styles/game.css
index fa53cc7..e703ee9 100644
--- a/client/styles/game.css
+++ b/client/styles/game.css
@@ -454,6 +454,14 @@ label[for='moderator'] {
font-size: 25px;
}
+#transfer-mod-form {
+ width: 100%;
+}
+
+#transfer-mod-form #modal-button-container {
+ justify-content: center;
+}
+
@media(max-width: 685px) {
#end-game-button {
font-size: 25px;
diff --git a/server/config/globals.js b/server/config/globals.js
index 53f24ba..45d1b7b 100644
--- a/server/config/globals.js
+++ b/server/config/globals.js
@@ -10,7 +10,8 @@ const globals = {
RESUME_TIMER: 'resumeTimer',
GET_TIME_REMAINING: 'getTimeRemaining',
KILL_PLAYER: 'killPlayer',
- REVEAL_PLAYER: 'revealPlayer'
+ REVEAL_PLAYER: 'revealPlayer',
+ TRANSFER_MODERATOR: 'transferModerator'
},
STATUS: {
LOBBY: "lobby",
diff --git a/server/model/Game.js b/server/model/Game.js
index 9768b1f..0e4c6f1 100644
--- a/server/model/Game.js
+++ b/server/model/Game.js
@@ -9,6 +9,7 @@ class Game {
this.timerParams = timerParams;
this.isFull = false;
this.timeRemaining = null;
+ this.spectators = [];
}
}
diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js
index 9dd1078..ae4ae0f 100644
--- a/server/modules/GameManager.js
+++ b/server/modules/GameManager.js
@@ -125,6 +125,33 @@ class GameManager {
})
}
}
+ });
+
+ socket.on(globals.CLIENT_COMMANDS.TRANSFER_MODERATOR, (accessCode, personId) => {
+ let game = this.activeGameRunner.activeGames[accessCode];
+ if (game) {
+ let person = game.people.find((person) => person.id === personId)
+ if (!person) {
+ person = game.spectators.find((spectator) => spectator.id === personId)
+ }
+ if (person && (person.out || person.userType === globals.USER_TYPES.SPECTATOR)) {
+ this.logger.debug('game ' + accessCode + ': transferring mod powers to ' + person.name);
+ if (game.people.includes(game.moderator)) { // the current moderator was at one point a dealt-in player.
+ game.moderator.userType = globals.USER_TYPES.KILLED_PLAYER; // restore their state from before being made mod.
+ } else {
+ game.moderator.userType = globals.USER_TYPES.SPECTATOR;
+ if (!game.spectators.includes(game.moderator)) {
+ game.spectators.push(game.moderator);
+ }
+ if (game.spectators.includes(person)) {
+ game.spectators.splice(game.spectators.indexOf(person), 1);
+ }
+ }
+ person.userType = globals.USER_TYPES.MODERATOR;
+ game.moderator = person;
+ namespace.in(accessCode).emit(globals.EVENTS.SYNC_GAME_STATE);
+ }
+ }
})
}
@@ -265,7 +292,10 @@ function handleRequestForGameState(namespace, logger, gameRunner, accessCode, pe
const game = gameRunner.activeGames[accessCode];
if (game) {
let matchingPerson = game.people.find((person) => person.cookie === personCookie);
- if (!matchingPerson && game.moderator.cookie === personCookie) {
+ if (!matchingPerson) {
+ matchingPerson = game.spectators.find((spectator) => spectator.cookie = personCookie);
+ }
+ if (game.moderator.cookie === personCookie) {
matchingPerson = game.moderator;
}
if (matchingPerson) {
diff --git a/server/modules/GameStateCurator.js b/server/modules/GameStateCurator.js
index 44e6722..09a4b95 100644
--- a/server/modules/GameStateCurator.js
+++ b/server/modules/GameStateCurator.js
@@ -34,7 +34,6 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) {
people: game.people
.filter((person) => {
return person.assigned === true
- && (person.userType !== globals.USER_TYPES.MODERATOR && person.userType !== globals.USER_TYPES.TEMPORARY_MODERATOR)
})
.map((filteredPerson) => mapPerson(filteredPerson)),
timerParams: game.timerParams,
@@ -49,7 +48,8 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) {
deck: game.deck,
people: mapPeopleForModerator(game.people, client),
timerParams: game.timerParams,
- isFull: game.isFull
+ isFull: game.isFull,
+ spectators: game.spectators
}
case globals.USER_TYPES.TEMPORARY_MODERATOR:
return {
@@ -58,19 +58,38 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) {
moderator: mapPerson(game.moderator),
client: client,
deck: game.deck,
- people: mapPeopleForTempModerator(game.people, client),
+ people: game.people
+ .filter((person) => {
+ return person.assigned === true
+ })
+ .map((filteredPerson) => mapPerson(filteredPerson)),
timerParams: game.timerParams,
isFull: game.isFull
}
+ case globals.USER_TYPES.SPECTATOR:
+ return {
+ accessCode: game.accessCode,
+ status: game.status,
+ moderator: mapPerson(game.moderator),
+ client: client,
+ deck: game.deck,
+ people: game.people
+ .filter((person) => {
+ return person.assigned === true
+ })
+ .map((filteredPerson) => mapPerson(filteredPerson)),
+ timerParams: game.timerParams,
+ isFull: game.isFull,
+ }
default:
break;
}
}
-function mapPeopleForModerator(people, client) {
+function mapPeopleForModerator(people) {
return people
.filter((person) => {
- return person.assigned === true && person.cookie !== client.cookie
+ return person.assigned === true
})
.map((person) => ({
name: person.name,
@@ -84,20 +103,6 @@ function mapPeopleForModerator(people, client) {
}));
}
-function mapPeopleForTempModerator(people, client) {
- return people
- .filter((person) => {
- return person.assigned === true && person.cookie !== client.cookie
- })
- .map((person) => ({
- name: person.name,
- id: person.id,
- userType: person.userType,
- out: person.out,
- revealed: person.revealed
- }));
-}
-
function mapPerson(person) {
if (person.revealed) {
return {