diff --git a/client/config/globals.js b/client/config/globals.js index 8eec0a7..ffdc08e 100644 --- a/client/config/globals.js +++ b/client/config/globals.js @@ -25,7 +25,8 @@ export const globals = { EVENTS: { PLAYER_JOINED: "playerJoined", SYNC_GAME_STATE: "syncGameState", - START_TIMER: "startTimer" + START_TIMER: "startTimer", + KILL_PLAYER: "killPlayer" }, USER_TYPES: { MODERATOR: "moderator", diff --git a/client/images/eye.svg b/client/images/eye.svg new file mode 100644 index 0000000..5add930 --- /dev/null +++ b/client/images/eye.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/images/tombstone.png b/client/images/tombstone.png new file mode 100644 index 0000000..49a91b6 Binary files /dev/null and b/client/images/tombstone.png differ diff --git a/client/modules/GameStateRenderer.js b/client/modules/GameStateRenderer.js index 46fb670..949b58f 100644 --- a/client/modules/GameStateRenderer.js +++ b/client/modules/GameStateRenderer.js @@ -27,7 +27,7 @@ export class GameStateRenderer { playerCount += 1; } document.querySelector("label[for='lobby-players']").innerText = - "Other People (" + playerCount + "/" + getGameSize(this.gameState.deck) + " Players)"; + "People (" + playerCount + "/" + getGameSize(this.gameState.deck) + " Players)"; } renderLobbyHeader() { @@ -67,22 +67,53 @@ export class GameStateRenderer { let div = document.createElement("div"); div.innerHTML = templates.END_GAME_PROMPT; document.body.appendChild(div); - renderPlayersWithRoleAndAlignmentInfo(this.gameState.people, this.socket, this.gameState.accessCode, this.killPlayerHandlers); + this.renderPlayersWithRoleAndAlignmentInfo(); } renderPlayerView() { renderPlayerRole(this.gameState); - renderPlayersWithNoRoleInformation(this.gameState.people, this.killPlayerHandlers); + this.renderPlayersWithNoRoleInformation(); } refreshPlayerList(isModerator) { if (isModerator) { - renderPlayersWithRoleAndAlignmentInfo(this.gameState.people, this.socket, this.gameState.accessCode, this.killPlayerHandlers) + this.renderPlayersWithRoleAndAlignmentInfo() } else { - renderPlayersWithNoRoleInformation(this.gameState.people, this.killPlayerHandlers); + this.renderPlayersWithNoRoleInformation(); } } + renderPlayersWithRoleAndAlignmentInfo() { + document.querySelectorAll('.game-player').forEach((el) => { + let pointer = el.dataset.pointer; + if (pointer && this.killPlayerHandlers[pointer]) { + el.removeEventListener('click', this.killPlayerHandlers[pointer]) + } + el.remove(); + }); + this.gameState.people.sort((a, b) => { + return a.name >= b.name ? 1 : -1; + }); + 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); + document.getElementById("players-alive-label").innerText = + 'Players: ' + this.gameState.people.filter((person) => !person.out).length + ' / ' + this.gameState.people.length + ' Alive'; + + } + + renderPlayersWithNoRoleInformation() { + 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); + document.getElementById("players-alive-label").innerText = + 'Players: ' + this.gameState.people.filter((person) => !person.out).length + ' / ' + this.gameState.people.length + ' Alive'; + + } + } function renderLobbyPerson(name, userType) { @@ -115,37 +146,6 @@ function removeExistingTitle() { } } -function renderPlayersWithRoleAndAlignmentInfo(people, socket, accessCode, handlers) { - document.querySelectorAll('.game-player').forEach((el) => { - let pointer = el.dataset.pointer; - if (pointer && handlers[pointer]) { - el.removeEventListener('click', handlers[pointer]) - } - el.remove(); - }); - people.sort((a, b) => { - return a.name >= b.name ? 1 : -1; - }); - let teamGood = people.filter((person) => person.alignment === globals.ALIGNMENT.GOOD); - let teamEvil = people.filter((person) => person.alignment === globals.ALIGNMENT.EVIL); - renderGroupOfPlayers(teamEvil, handlers, accessCode, globals.ALIGNMENT.EVIL, true, socket); - renderGroupOfPlayers(teamGood, handlers, accessCode, globals.ALIGNMENT.GOOD, true, socket); - document.getElementById("players-alive-label").innerText = - 'Players: ' + people.filter((person) => !person.out).length + ' / ' + people.length + ' Alive'; - -} - -function renderPlayersWithNoRoleInformation(people, handlers) { - document.querySelectorAll('.game-player').forEach((el) => el.remove()); - people.sort((a, b) => { - return a.name >= b.name ? 1 : -1; - }); - renderGroupOfPlayers(people, handlers); - document.getElementById("players-alive-label").innerText = - 'Players: ' + people.filter((person) => !person.out).length + ' / ' + people.length + ' Alive'; - -} - function renderGroupOfPlayers(players, handlers, accessCode=null, alignment=null, moderator=false, socket=null) { for (let player of players) { let container = document.createElement("div"); @@ -168,14 +168,18 @@ function renderGroupOfPlayers(players, handlers, accessCode=null, alignment=null document.getElementById("game-player-list").appendChild(container); } - if (moderator) { - handlers[player.id] = () => { - socket.emit(globals.COMMANDS.KILL_PLAYER, accessCode, player.id); + if (player.out) { + container.classList.add('killed'); + if (moderator) { + container.querySelector('.kill-player-button')?.remove(); } - if (player.out) { - container.classList.add('killed'); - container.querySelector('.kill-player-button').remove(); - } else { + } else { + if (moderator) { + handlers[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]); } } @@ -191,11 +195,19 @@ function renderPlayerRole(gameState) { name.classList.add('evil'); } name.setAttribute("title", gameState.client.gameRole); - document.querySelector('#role-description').innerText = gameState.client.gameRoleDescription; - document.getElementById("role-image").setAttribute( - 'src', - '../images/roles/' + gameState.client.gameRole.replaceAll(' ', '') + '.png' - ); + if (gameState.client.out) { + document.querySelector('#role-description').innerText = "You have been killed."; + 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.getElementById("game-role-back").addEventListener('click', () => { document.getElementById("game-role").style.display = 'flex'; diff --git a/client/modules/GameTimerManager.js b/client/modules/GameTimerManager.js index 3c1dcc9..a16cade 100644 --- a/client/modules/GameTimerManager.js +++ b/client/modules/GameTimerManager.js @@ -28,15 +28,19 @@ export class GameTimerManager { if (this.gameState.client.userType !== globals.USER_TYPES.PLAYER) { this.swapToPauseButton(); } - + let instance = this; let timer = document.getElementById('game-timer'); timer.classList.remove('paused'); timer.innerText = totalTime < 60000 ? returnHumanReadableTime(totalTime, true) : returnHumanReadableTime(totalTime); timerWorker.onmessage = function (e) { - if (e.data.hasOwnProperty('timeRemainingInMilliseconds') && e.data.timeRemainingInMilliseconds > 0) { - timer.innerText = e.data.displayTime; + if (e.data.hasOwnProperty('timeRemainingInMilliseconds') && e.data.timeRemainingInMilliseconds >= 0) { + if (e.data.timeRemainingInMilliseconds === 0) { + instance.displayExpiredTime(); + } else { + timer.innerText = e.data.displayTime; + } } }; timerWorker.postMessage({ totalTime: totalTime, tickInterval: tickRate }); @@ -70,6 +74,18 @@ export class GameTimerManager { timer.classList.add('paused'); } + displayExpiredTime() { + let currentBtn = document.querySelector('#play-pause img'); + if (currentBtn) { + currentBtn.removeEventListener('click', this.pauseListener); + currentBtn.removeEventListener('click', this.playListener); + currentBtn.remove(); + } + + let timer = document.getElementById('game-timer'); + timer.innerText = returnHumanReadableTime(0, true); + } + attachTimerSocketListeners(socket, timerWorker, gameStateRenderer) { // if (!socket.hasListeners(globals.EVENTS.START_TIMER)) { // socket.on(globals.EVENTS.START_TIMER, () => { @@ -100,6 +116,8 @@ export class GameTimerManager { console.log('received time remaining from server'); if (paused) { this.displayPausedTime(timeRemaining); + } else if (timeRemaining === 0) { + this.displayExpiredTime(); } else { this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker); } diff --git a/client/modules/Templates.js b/client/modules/Templates.js index 8c1e41f..4a38c95 100644 --- a/client/modules/Templates.js +++ b/client/modules/Templates.js @@ -75,7 +75,8 @@ export const templates = { "
" + "" + "
" + - "" + + "" + + "" + "
", GAME_PLAYER: "
" + diff --git a/client/modules/Timer.js b/client/modules/Timer.js index 8d581b7..abc81cf 100644 --- a/client/modules/Timer.js +++ b/client/modules/Timer.js @@ -30,6 +30,10 @@ onmessage = function (e) { function stepFn (expected, interval, start, totalTime) { const now = Date.now(); if (now - start >= totalTime) { + postMessage({ + timeRemainingInMilliseconds: 0, + displayTime: returnHumanReadableTime(0, true) + }); return; } const delta = now - expected; diff --git a/client/scripts/game.js b/client/scripts/game.js index 288e5ff..b04735f 100644 --- a/client/scripts/game.js +++ b/client/scripts/game.js @@ -132,6 +132,22 @@ function setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTim if (timerWorker && gameTimerManager) { gameTimerManager.attachTimerSocketListeners(socket, timerWorker, gameStateRenderer); } + + if (!socket.hasListeners(globals.EVENTS.KILL_PLAYER)) { + socket.on(globals.EVENTS.KILL_PLAYER, (id) => { + let killedPerson = gameStateRenderer.gameState.people.find((person) => person.id === id); + if (killedPerson) { + killedPerson.out = true; + if (gameStateRenderer.gameState.client.userType === globals.USER_TYPES.MODERATOR) { + toast(killedPerson.name + ' killed.', 'success', true, true, 6); + gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo() + } else { + toast(killedPerson.name + ' was killed!', 'warning', false, true, 6); + gameStateRenderer.renderPlayersWithNoRoleInformation(); + } + } + }); + } } function displayStartGamePromptForModerators(gameStateRenderer, socket) { diff --git a/client/styles/GLOBAL.css b/client/styles/GLOBAL.css index f43b683..6a51085 100644 --- a/client/styles/GLOBAL.css +++ b/client/styles/GLOBAL.css @@ -214,11 +214,11 @@ input { } .good, .compact-card.good .card-role { - color: #4b6bfa !important; + color: #4b6bfa; } .evil, .compact-card.evil .card-role { - color: #e73333 !important + color: #e73333; } @keyframes placeholder { diff --git a/client/styles/game.css b/client/styles/game.css index fbb53e8..e2bf9ee 100644 --- a/client/styles/game.css +++ b/client/styles/game.css @@ -346,7 +346,7 @@ label[for='moderator'] { text-overflow: ellipsis; } -.kill-player-button, .make-mod-button { +.kill-player-button, .reveal-role-button { font-family: 'signika-negative', sans-serif !important; padding: 5px; border-radius: 3px; @@ -358,6 +358,18 @@ label[for='moderator'] { text-shadow: 0 3px 4px rgb(0 0 0 / 55%); margin: 5px 0 5px 25px; min-width: 6em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.reveal-role-button { + background-color: #3f5256; +} + +.reveal-role-button img { + width: 18px; + margin-left: 5px; } .killed::after { @@ -367,12 +379,13 @@ label[for='moderator'] { font-size: 24px; } -.killed { - filter: grayscale(1); - opacity: 0.6; - pointer-events: none; +.killed, .killed .game-player-role { + color: gray !important; } +.reveal-role-button { + display: flex; + align-items: center; } .make-mod-button { @@ -390,7 +403,12 @@ label[for='moderator'] { } .kill-player-button { - background-color: #3f5256; + background-color: #9f4747; +} + +.game-player > div:nth-child(2) { + display: flex; + flex-wrap: wrap; } #game-player-list > div { diff --git a/client/views/404.html b/client/views/404.html index 210c111..a543e48 100644 --- a/client/views/404.html +++ b/client/views/404.html @@ -13,20 +13,16 @@ - - - - - - - - - - + + + + + + diff --git a/client/views/game.html b/client/views/game.html index 9cfbb4e..3a12de1 100644 --- a/client/views/game.html +++ b/client/views/game.html @@ -13,13 +13,13 @@ - - - + + +
diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js index 8909eea..3a50d63 100644 --- a/server/modules/GameManager.js +++ b/server/modules/GameManager.js @@ -88,6 +88,10 @@ class GameManager { socketId: socket.id, logLevel: this.logger.logLevel }); + } else { + 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); + } } } }); @@ -96,10 +100,10 @@ class GameManager { let game = this.activeGameRunner.activeGames[accessCode]; if (game) { let person = game.people.find((person) => person.id === personId) - if (person) { + if (person && !person.out) { this.logger.debug('game ' + accessCode + ': killing player ' + person.name); person.out = true; - namespace.in(accessCode).emit(globals.CLIENT_COMMANDS.KILL_PLAYER, ) + namespace.in(accessCode).emit(globals.CLIENT_COMMANDS.KILL_PLAYER, person.id) } } }) diff --git a/server/modules/GameStateCurator.js b/server/modules/GameStateCurator.js index e707ba1..946cb9a 100644 --- a/server/modules/GameStateCurator.js +++ b/server/modules/GameStateCurator.js @@ -11,11 +11,13 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) { ? { name: person.name, cookie: person.cookie, userType: person.userType } : { name: person.name, + id: person.id, cookie: person.cookie, userType: person.userType, gameRole: person.gameRole, gameRoleDescription: person.gameRoleDescription, - alignment: person.alignment + alignment: person.alignment, + out: person.out } switch (person.userType) { case globals.USER_TYPES.PLAYER: @@ -27,10 +29,10 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) { deck: game.deck, people: game.people .filter((person) => { - return person.assigned === true && person.cookie !== client.cookie + return person.assigned === true && (person.userType !== globals.USER_TYPES.MODERATOR && person.userType !== globals.USER_TYPES.TEMPORARY_MODERATOR) }) - .map((filteredPerson) => ({ name: filteredPerson.name, userType: filteredPerson.userType })), + .map((filteredPerson) => ({ name: filteredPerson.name, id: filteredPerson.id, userType: filteredPerson.userType, out: filteredPerson.out })), timerParams: game.timerParams, isFull: game.isFull, } @@ -91,7 +93,7 @@ function mapPeopleForTempModerator(people, client) { } function mapPerson(person) { - return { name: person.name, userType: person.userType, out: person.out }; + return { name: person.name, id: person.id, userType: person.userType, out: person.out }; } module.exports = GameStateCurator;