diff --git a/client/config/globals.js b/client/config/globals.js index ffdc08e..421a316 100644 --- a/client/config/globals.js +++ b/client/config/globals.js @@ -12,7 +12,9 @@ export const globals = { PAUSE_TIMER: 'pauseTimer', RESUME_TIMER: 'resumeTimer', GET_TIME_REMAINING: 'getTimeRemaining', - KILL_PLAYER: 'killPlayer' + KILL_PLAYER: 'killPlayer', + REVEAL_PLAYER: 'revealPlayer', + TRANSFER_MODERATOR: 'transferModerator' }, STATUS: { LOBBY: "lobby", @@ -26,12 +28,15 @@ export const globals = { PLAYER_JOINED: "playerJoined", SYNC_GAME_STATE: "syncGameState", START_TIMER: "startTimer", - KILL_PLAYER: "killPlayer" + KILL_PLAYER: "killPlayer", + REVEAL_PLAYER: 'revealPlayer' }, USER_TYPES: { MODERATOR: "moderator", PLAYER: "player", - TEMPORARY_MODERATOR: "player / temp mod" + TEMPORARY_MODERATOR: "player / temp mod", + KILLED_PLAYER: "killed", + SPECTATOR: "spectator" }, ENVIRONMENT: { LOCAL: "local", diff --git a/client/modules/GameStateRenderer.js b/client/modules/GameStateRenderer.js index 949b58f..4d7c22f 100644 --- a/client/modules/GameStateRenderer.js +++ b/client/modules/GameStateRenderer.js @@ -1,12 +1,15 @@ import { globals } from "../config/globals.js"; import { toast } from "./Toast.js"; import {templates} from "./Templates.js"; +import {ModalManager} from "./ModalManager.js"; export class GameStateRenderer { constructor(gameState, socket) { this.gameState = gameState; this.socket = socket; this.killPlayerHandlers = {}; + this.revealRoleHandlers = {}; + this.transferModHandlers = {}; this.cardFlipped = false; } @@ -20,12 +23,6 @@ export class GameStateRenderer { lobbyPlayersContainer.appendChild(renderLobbyPerson(person.name,person.userType)) } let playerCount = this.gameState.people.filter((person) => person.userType === globals.USER_TYPES.PLAYER).length; - if (this.gameState.moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { - playerCount += 1; - } - if (this.gameState.client.userType === globals.USER_TYPES.PLAYER) { - playerCount += 1; - } document.querySelector("label[for='lobby-players']").innerText = "People (" + playerCount + "/" + getGameSize(this.gameState.deck) + " Players)"; } @@ -67,19 +64,37 @@ export class GameStateRenderer { let div = document.createElement("div"); div.innerHTML = templates.END_GAME_PROMPT; document.body.appendChild(div); + + let modTransferButton = document.getElementById("mod-transfer-button"); + modTransferButton.addEventListener( + "click", () => { + this.displayAvailableModerators() + ModalManager.displayModal( + "transfer-mod-modal", + "transfer-mod-modal-background", + "close-modal-button" + ) + } + ) this.renderPlayersWithRoleAndAlignmentInfo(); } - renderPlayerView() { + renderPlayerView(isKilled=false) { + if (isKilled) { + let clientUserType = document.getElementById("client-user-type"); + if (clientUserType) { + clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80' + } + } renderPlayerRole(this.gameState); - this.renderPlayersWithNoRoleInformation(); + this.renderPlayersWithNoRoleInformationUnlessRevealed(); } refreshPlayerList(isModerator) { if (isModerator) { this.renderPlayersWithRoleAndAlignmentInfo() } else { - this.renderPlayersWithNoRoleInformation(); + this.renderPlayersWithNoRoleInformationUnlessRevealed(); } } @@ -87,7 +102,12 @@ export class GameStateRenderer { document.querySelectorAll('.game-player').forEach((el) => { let pointer = el.dataset.pointer; if (pointer && this.killPlayerHandlers[pointer]) { - el.removeEventListener('click', 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(); }); @@ -96,14 +116,14 @@ export class GameStateRenderer { }); let teamGood = this.gameState.people.filter((person) => person.alignment === globals.ALIGNMENT.GOOD); let teamEvil = this.gameState.people.filter((person) => person.alignment === globals.ALIGNMENT.EVIL); - renderGroupOfPlayers(teamEvil, this.killPlayerHandlers, this.gameState.accessCode, globals.ALIGNMENT.EVIL, true, this.socket); - renderGroupOfPlayers(teamGood, this.killPlayerHandlers, this.gameState.accessCode, globals.ALIGNMENT.GOOD, true, this.socket); + renderGroupOfPlayers(teamEvil, this.killPlayerHandlers, this.revealRoleHandlers, this.gameState.accessCode, globals.ALIGNMENT.EVIL, true, this.socket); + renderGroupOfPlayers(teamGood, this.killPlayerHandlers, this.revealRoleHandlers, this.gameState.accessCode, globals.ALIGNMENT.GOOD, true, this.socket); document.getElementById("players-alive-label").innerText = 'Players: ' + this.gameState.people.filter((person) => !person.out).length + ' / ' + this.gameState.people.length + ' Alive'; } - renderPlayersWithNoRoleInformation() { + renderPlayersWithNoRoleInformationUnlessRevealed() { document.querySelectorAll('.game-player').forEach((el) => el.remove()); this.gameState.people.sort((a, b) => { return a.name >= b.name ? 1 : -1; @@ -114,6 +134,44 @@ export class GameStateRenderer { } + updatePlayerCardToKilledState() { + document.querySelector('#role-image').classList.add("killed-card"); + document.getElementById("role-image").setAttribute( + 'src', + '../images/tombstone.png' + ); + } + + displayAvailableModerators() { + document.querySelectorAll('.potential-moderator').forEach((el) => { + let pointer = el.dataset.pointer; + if (pointer && this.transferModHandlers[pointer]) { + el.removeEventListener('click', this.transferModHandlers[pointer]); + delete this.transferModHandlers[pointer]; + } + el.remove(); + }); + 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); + } + } + } + } + } function renderLobbyPerson(name, userType) { @@ -146,7 +204,7 @@ function removeExistingTitle() { } } -function renderGroupOfPlayers(players, handlers, accessCode=null, alignment=null, moderator=false, socket=null) { +function renderGroupOfPlayers(players, killPlayerHandlers, revealRoleHandlers, accessCode=null, alignment=null, moderator=false, socket=null) { for (let player of players) { let container = document.createElement("div"); container.classList.add('game-player'); @@ -159,10 +217,14 @@ function renderGroupOfPlayers(players, handlers, accessCode=null, alignment=null container.querySelector('.game-player-name').innerText = player.name; let roleElement = container.querySelector('.game-player-role') - if (alignment) { + if (moderator) { roleElement.classList.add(alignment); roleElement.innerText = player.gameRole; document.getElementById("player-list-moderator-team-" + alignment).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" document.getElementById("game-player-list").appendChild(container); @@ -175,12 +237,27 @@ function renderGroupOfPlayers(players, handlers, accessCode=null, alignment=null } } else { if (moderator) { - handlers[player.id] = () => { + killPlayerHandlers[player.id] = () => { if (confirm("KILL " + player.name + "?")) { socket.emit(globals.COMMANDS.KILL_PLAYER, accessCode, player.id); } } - container.querySelector('.kill-player-button').addEventListener('click', handlers[player.id]); + container.querySelector('.kill-player-button').addEventListener('click', killPlayerHandlers[player.id]); + } + } + + if (player.revealed) { + if (moderator) { + container.querySelector('.reveal-role-button')?.remove(); + } + } else { + if (moderator) { + revealRoleHandlers[player.id] = () => { + if (confirm("REVEAL " + player.name + "?")) { + socket.emit(globals.COMMANDS.REVEAL_PLAYER, accessCode, player.id); + } + } + container.querySelector('.reveal-role-button').addEventListener('click', revealRoleHandlers[player.id]); } } } @@ -196,19 +273,20 @@ function renderPlayerRole(gameState) { } name.setAttribute("title", gameState.client.gameRole); if (gameState.client.out) { - document.querySelector('#role-description').innerText = "You have been killed."; + document.querySelector('#role-image').classList.add("killed-card"); document.getElementById("role-image").setAttribute( 'src', '../images/tombstone.png' ); } else { - document.querySelector('#role-description').innerText = gameState.client.gameRoleDescription; document.getElementById("role-image").setAttribute( 'src', '../images/roles/' + gameState.client.gameRole.replaceAll(' ', '') + '.png' ); } + document.querySelector('#role-description').innerText = gameState.client.gameRoleDescription; + document.getElementById("game-role-back").addEventListener('click', () => { document.getElementById("game-role").style.display = 'flex'; document.getElementById("game-role-back").style.display = 'none'; diff --git a/client/modules/GameTimerManager.js b/client/modules/GameTimerManager.js index a16cade..b350e51 100644 --- a/client/modules/GameTimerManager.js +++ b/client/modules/GameTimerManager.js @@ -11,21 +11,12 @@ export class GameTimerManager { } } - // startGameTimer (hours, minutes, tickRate, soundManager, timerWorker) { - // if (window.Worker) { - // timerWorker.onmessage = function (e) { - // if (e.data.hasOwnProperty('timeRemainingInMilliseconds') && e.data.timeRemainingInMilliseconds > 0) { - // document.getElementById('game-timer').innerText = e.data.displayTime; - // } - // }; - // const totalTime = convertFromHoursToMilliseconds(hours) + convertFromMinutesToMilliseconds(minutes); - // timerWorker.postMessage({ totalTime: totalTime, tickInterval: tickRate }); - // } - // } - resumeGameTimer(totalTime, tickRate, soundManager, timerWorker) { if (window.Worker) { - if (this.gameState.client.userType !== globals.USER_TYPES.PLAYER) { + if ( + this.gameState.client.userType === globals.USER_TYPES.MODERATOR + || this.gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + ) { this.swapToPauseButton(); } let instance = this; @@ -49,7 +40,10 @@ export class GameTimerManager { pauseGameTimer(timerWorker, timeRemaining) { if (window.Worker) { - if (this.gameState.client.userType !== globals.USER_TYPES.PLAYER) { + if ( + this.gameState.client.userType === globals.USER_TYPES.MODERATOR + || this.gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + ) { this.swapToPlayButton(); } @@ -63,7 +57,10 @@ export class GameTimerManager { } displayPausedTime(time) { - if (this.gameState.client.userType !== globals.USER_TYPES.PLAYER) { + if ( + this.gameState.client.userType === globals.USER_TYPES.MODERATOR + || this.gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + ) { this.swapToPlayButton(); } @@ -87,18 +84,6 @@ export class GameTimerManager { } attachTimerSocketListeners(socket, timerWorker, gameStateRenderer) { - // if (!socket.hasListeners(globals.EVENTS.START_TIMER)) { - // socket.on(globals.EVENTS.START_TIMER, () => { - // this.startGameTimer( - // gameStateRenderer.gameState.timerParams.hours, - // gameStateRenderer.gameState.timerParams.minutes, - // globals.CLOCK_TICK_INTERVAL_MILLIS, - // null, - // timerWorker - // ) - // }); - // } - if(!socket.hasListeners(globals.COMMANDS.PAUSE_TIMER)) { socket.on(globals.COMMANDS.PAUSE_TIMER, (timeRemaining) => { this.pauseGameTimer(timerWorker, timeRemaining) @@ -152,15 +137,6 @@ export class GameTimerManager { } } - -function convertFromMinutesToMilliseconds(minutes) { - return minutes * 60 * 1000; -} - -function convertFromHoursToMilliseconds(hours) { - return hours * 60 * 60 * 1000; -} - function returnHumanReadableTime(milliseconds, tenthsOfSeconds=false) { let tenths = Math.floor((milliseconds / 100) % 10); diff --git a/client/modules/Templates.js b/client/modules/Templates.js index 4a38c95..42242f2 100644 --- a/client/modules/Templates.js +++ b/client/modules/Templates.js @@ -46,6 +46,15 @@ export const templates = { "
" + "", MODERATOR_GAME_VIEW: + "" + + "" + "
" + "
" + "
" + @@ -54,7 +63,7 @@ export const templates = { "
" + "
" + "
" + "
" + - "" + + "" + "
" + "
" + "" + diff --git a/client/scripts/game.js b/client/scripts/game.js index b04735f..9e59192 100644 --- a/client/scripts/game.js +++ b/client/scripts/game.js @@ -74,6 +74,10 @@ function processGameState (gameState, userId, socket, gameStateRenderer) { document.getElementById("game-state-container").innerHTML = templates.PLAYER_GAME_VIEW; gameStateRenderer.renderPlayerView(); break; + case globals.USER_TYPES.KILLED_PLAYER: + document.getElementById("game-state-container").innerHTML = templates.PLAYER_GAME_VIEW; + gameStateRenderer.renderPlayerView(true); + break; case globals.USER_TYPES.MODERATOR: document.querySelector("#start-game-prompt")?.remove(); document.getElementById("game-state-container").innerHTML = templates.MODERATOR_GAME_VIEW; @@ -142,8 +146,39 @@ function setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTim toast(killedPerson.name + ' killed.', 'success', true, true, 6); gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo() } else { - toast(killedPerson.name + ' was killed!', 'warning', false, true, 6); - gameStateRenderer.renderPlayersWithNoRoleInformation(); + if (killedPerson.id === gameStateRenderer.gameState.client.id) { + let clientUserType = document.getElementById("client-user-type"); + if (clientUserType) { + clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80' + } + gameStateRenderer.updatePlayerCardToKilledState(); + toast('You have been killed!', 'warning', false, true, 6); + } else { + toast(killedPerson.name + ' was killed!', 'warning', false, true, 6); + } + gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(); + } + } + }); + } + + if (!socket.hasListeners(globals.EVENTS.REVEAL_PLAYER)) { + socket.on(globals.EVENTS.REVEAL_PLAYER, (revealData) => { + let revealedPerson = gameStateRenderer.gameState.people.find((person) => person.id === revealData.id); + if (revealedPerson) { + revealedPerson.revealed = true; + revealedPerson.gameRole = revealData.gameRole; + revealedPerson.alignment = revealData.alignment; + if (gameStateRenderer.gameState.client.userType === globals.USER_TYPES.MODERATOR) { + toast(revealedPerson.name + ' revealed.', 'success', true, true, 6); + gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo() + } else { + if (revealedPerson.id === gameStateRenderer.gameState.client.id) { + toast('Your role has been revealed!', 'warning', false, true, 6); + } else { + toast(revealedPerson.name + ' was revealed as a ' + revealedPerson.gameRole + '!', 'warning', false, true, 6); + } + gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(); } } }); diff --git a/client/styles/game.css b/client/styles/game.css index e2bf9ee..fa53cc7 100644 --- a/client/styles/game.css +++ b/client/styles/game.css @@ -41,10 +41,33 @@ h1 { margin: 0.5em auto; } -#game-state-container > div { +#game-state-container > div:not(#transfer-mod-modal-background):not(#transfer-mod-modal){ margin: 1em; } +.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; +} + +.potential-moderator:active { + border: 2px solid #21ba45; + transition: border 0.2s ease-out; +} + #game-link { user-select: none; -ms-user-select: none; @@ -234,7 +257,7 @@ label[for='moderator'] { align-items: center; justify-content: center; position: fixed; - z-index: 1000; + z-index: 3; border-radius: 3px; font-family: 'signika-negative', sans-serif; font-weight: 100; @@ -326,7 +349,7 @@ label[for='moderator'] { } .game-player { - border-left: 2px solid gray; + border-left: 3px solid #21ba45; display: flex; color: #d7d7d7; background-color: black; @@ -380,7 +403,11 @@ label[for='moderator'] { } .killed, .killed .game-player-role { - color: gray !important; + /*color: gray !important;*/ +} + +.game-player.killed { + border-left: 3px solid #444444; } .reveal-role-button { @@ -406,6 +433,10 @@ label[for='moderator'] { background-color: #9f4747; } +.killed-card { + width: 55% !important; +} + .game-player > div:nth-child(2) { display: flex; flex-wrap: wrap; diff --git a/client/styles/modal.css b/client/styles/modal.css index f6d6d8a..aa600dd 100644 --- a/client/styles/modal.css +++ b/client/styles/modal.css @@ -3,7 +3,6 @@ text-align: center; position: fixed; width: 100%; - height: 100%; z-index: 100; top: 50%; left: 50%; @@ -11,10 +10,10 @@ background-color: #23282b; align-items: center; justify-content: center; - max-width: 17em; - max-height: 24em; + max-width: 19em; + max-height: 80%; + height: fit-content; font-family: sans-serif; - font-size: 22px; flex-direction: column; padding: 1em; } @@ -25,7 +24,7 @@ left: 0; width: 100%; height: calc(100% + 100px); - background-color: rgba(0, 0, 0, 0.55); + background-color: rgba(0, 0, 0, 0.75); z-index: 50; cursor: pointer; } diff --git a/server/config/globals.js b/server/config/globals.js index dc21ae8..53f24ba 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -9,7 +9,8 @@ const globals = { PAUSE_TIMER: 'pauseTimer', RESUME_TIMER: 'resumeTimer', GET_TIME_REMAINING: 'getTimeRemaining', - KILL_PLAYER: 'killPlayer' + KILL_PLAYER: 'killPlayer', + REVEAL_PLAYER: 'revealPlayer' }, STATUS: { LOBBY: "lobby", @@ -20,7 +21,9 @@ const globals = { USER_TYPES: { MODERATOR: "moderator", PLAYER: "player", - TEMPORARY_MODERATOR: "player / temp mod" + TEMPORARY_MODERATOR: "player / temp mod", + KILLED_PLAYER: "killed", + SPECTATOR: "spectator" }, ERROR_MESSAGE: { GAME_IS_FULL: "This game is full" diff --git a/server/model/Person.js b/server/model/Person.js index 55dc404..a33a69c 100644 --- a/server/model/Person.js +++ b/server/model/Person.js @@ -11,6 +11,7 @@ class Person { this.alignment = alignment; this.assigned = assigned; this.out = false; + this.revealed = false; } } diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js index 3a50d63..9dd1078 100644 --- a/server/modules/GameManager.js +++ b/server/modules/GameManager.js @@ -102,10 +102,29 @@ class GameManager { let person = game.people.find((person) => person.id === personId) if (person && !person.out) { this.logger.debug('game ' + accessCode + ': killing player ' + person.name); + person.userType = globals.USER_TYPES.KILLED_PLAYER; person.out = true; namespace.in(accessCode).emit(globals.CLIENT_COMMANDS.KILL_PLAYER, person.id) } } + }); + + socket.on(globals.CLIENT_COMMANDS.REVEAL_PLAYER, (accessCode, personId) => { + let game = this.activeGameRunner.activeGames[accessCode]; + if (game) { + let person = game.people.find((person) => person.id === personId) + if (person && !person.revealed) { + this.logger.debug('game ' + accessCode + ': revealing player ' + person.name); + person.revealed = true; + namespace.in(accessCode).emit( + globals.CLIENT_COMMANDS.REVEAL_PLAYER, + { + id: person.id, + gameRole: person.gameRole, + alignment: person.alignment + }) + } + } }) } diff --git a/server/modules/GameStateCurator.js b/server/modules/GameStateCurator.js index 946cb9a..44e6722 100644 --- a/server/modules/GameStateCurator.js +++ b/server/modules/GameStateCurator.js @@ -1,5 +1,8 @@ const globals = require("../config/globals") +/* The purpose of this component is to only return the game state information that is necessary. For example, we only want to return player role information + to moderators. This avoids any possibility of a player having access to information that they shouldn't. + */ const GameStateCurator = { getGameStateFromPerspectiveOfPerson: (game, person, gameRunner, socket, logger) => { return getGameStateBasedOnPermissions(game, person, gameRunner); @@ -21,6 +24,7 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) { } switch (person.userType) { case globals.USER_TYPES.PLAYER: + case globals.USER_TYPES.KILLED_PLAYER: return { accessCode: game.accessCode, status: game.status, @@ -32,7 +36,7 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) { return person.assigned === true && (person.userType !== globals.USER_TYPES.MODERATOR && person.userType !== globals.USER_TYPES.TEMPORARY_MODERATOR) }) - .map((filteredPerson) => ({ name: filteredPerson.name, id: filteredPerson.id, userType: filteredPerson.userType, out: filteredPerson.out })), + .map((filteredPerson) => mapPerson(filteredPerson)), timerParams: game.timerParams, isFull: game.isFull, } @@ -75,7 +79,8 @@ function mapPeopleForModerator(people, client) { gameRole: person.gameRole, gameRoleDescription: person.gameRoleDescription, alignment: person.alignment, - out: person.out + out: person.out, + revealed: person.revealed })); } @@ -88,12 +93,25 @@ function mapPeopleForTempModerator(people, client) { name: person.name, id: person.id, userType: person.userType, - out: person.out + out: person.out, + revealed: person.revealed })); } function mapPerson(person) { - return { name: person.name, id: person.id, userType: person.userType, out: person.out }; + if (person.revealed) { + return { + name: person.name, + id: person.id, + userType: person.userType, + out: person.out, + revealed: person.revealed, + gameRole: person.gameRole, + alignment: person.alignment + }; + } else { + return { name: person.name, id: person.id, userType: person.userType, out: person.out, revealed: person.revealed }; + } } module.exports = GameStateCurator;