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: + "" + + "" + + "
" + + "
" + + "
" + + "" + + "
" + + "
" + + "
" + "
" + + "
" + + "
" + + "" + + "
" + + "

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 {