diff --git a/client/src/modules/game_state/states/Ended.js b/client/src/modules/game_state/states/Ended.js new file mode 100644 index 0000000..d1c27e0 --- /dev/null +++ b/client/src/modules/game_state/states/Ended.js @@ -0,0 +1,102 @@ +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'; + +export class Ended { + constructor (containerId, stateBucket, socket) { + this.stateBucket = stateBucket; + this.socket = socket; + this.container = document.getElementById(containerId); + this.container.innerHTML = HTMLFragments.END_OF_GAME_VIEW; + this.restartGameHandler = (e) => { + e.preventDefault(); + const button = document.getElementById('restart-game'); + button.removeEventListener('click', this.restartGameHandler); + button.classList.add('submitted'); + button.innerText = 'Restarting...'; + 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) => { + const button = document.getElementById('restart-game'); + button.innerText = 'Restart Game 🔄'; + button.classList.remove('submitted'); + button.addEventListener('click', this.restartGameHandler); + toast(res.content, 'error', true, true, 'medium'); + }); + }; + } + + renderEndOfGame (gameState) { + if ( + 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', this.restartGameHandler); + document.getElementById('end-of-game-buttons').prepend(restartGameContainer); + } + this.renderPlayersWithRoleInformation(); + } + + renderPlayersWithRoleInformation (tempMod = false) { + document.querySelectorAll('.game-player').forEach((el) => el.remove()); + /* TODO: UX issue - it's easier to parse visually when players are sorted this way, + but shifting players around when they are killed or revealed is bad UX for the moderator. */ + // sortPeopleByStatus(this.stateBucket.currentGameState.people); + const modType = tempMod ? this.stateBucket.currentGameState.moderator.userType : null; + renderGroupOfPlayers( + this.stateBucket.currentGameState.people, + this.stateBucket.currentGameState.accessCode, + null, + modType, + this.socket + ); + document.getElementById('players-alive-label').innerText = + 'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' + + this.stateBucket.currentGameState.people.length + ' Alive'; + } +} + +function renderGroupOfPlayers ( + people, + accessCode = null, + alignment = null +) { + for (const player of people) { + const playerEl = document.createElement('div'); + playerEl.classList.add('game-player'); + playerEl.innerHTML = HTMLFragments.GAME_PLAYER; + + playerEl.querySelector('.game-player-name').innerText = player.name; + const roleElement = playerEl.querySelector('.game-player-role'); + + if (alignment === null) { + roleElement.classList.add(player.alignment); + } else { + roleElement.classList.add(alignment); + } + roleElement.innerText = player.gameRole; + + if (player.out) { + playerEl.classList.add('killed'); + } + + document.getElementById('game-player-list').appendChild(playerEl); + } +} diff --git a/client/src/modules/game_state/GameStateRenderer.js b/client/src/modules/game_state/states/InProgress.js similarity index 65% rename from client/src/modules/game_state/GameStateRenderer.js rename to client/src/modules/game_state/states/InProgress.js index 97935e0..1bd83d5 100644 --- a/client/src/modules/game_state/GameStateRenderer.js +++ b/client/src/modules/game_state/states/InProgress.js @@ -1,148 +1,114 @@ -import { globals } from '../../config/globals.js'; -import { toast } from '../front_end_components/Toast.js'; -import { HTMLFragments } from '../front_end_components/HTMLFragments.js'; -import { ModalManager } from '../front_end_components/ModalManager.js'; -import { XHRUtility } from '../utility/XHRUtility.js'; -import { UserUtility } from '../utility/UserUtility.js'; -// QRCode module via: https://github.com/soldair/node-qrcode -import { QRCode } from '../third_party/qrcode.js'; -import { Confirmation } from '../front_end_components/Confirmation.js'; +import { toast } from '../../front_end_components/Toast.js'; +import { globals } from '../../../config/globals.js'; +import { HTMLFragments } from '../../front_end_components/HTMLFragments.js'; +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'; -export class GameStateRenderer { - constructor (stateBucket, socket) { +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 = {}; - this.startGameHandler = (e) => { // TODO: prevent multiple emissions of this event (recommend converting to XHR) - e.preventDefault(); - Confirmation('Start game and deal roles?', () => { - socket.emit(globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.START_GAME, stateBucket.currentGameState.accessCode); - }); - }; - this.restartGameHandler = (e) => { - e.preventDefault(); - const button = document.getElementById('restart-game'); - button.removeEventListener('click', this.restartGameHandler); - button.classList.add('submitted'); - button.innerText = 'Restarting...'; - 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) => { - const button = document.getElementById('restart-game'); - button.innerText = 'Restart Game 🔄'; - button.classList.remove('submitted'); - button.addEventListener('click', this.restartGameHandler); - toast(res.content, 'error', true, true, 'medium'); - }); - }; } - renderLobbyPlayers () { - document.querySelectorAll('.lobby-player').forEach((el) => el.remove()); - const lobbyPlayersContainer = document.getElementById('lobby-players'); - if (this.stateBucket.currentGameState.moderator.userType === globals.USER_TYPES.MODERATOR) { - lobbyPlayersContainer.appendChild( - renderLobbyPerson( - this.stateBucket.currentGameState.moderator.name, - this.stateBucket.currentGameState.moderator.userType - ) - ); + setUserView (userType) { + switch (userType) { + case globals.USER_TYPES.PLAYER: + this.container.innerHTML = HTMLFragments.PLAYER_GAME_VIEW; + this.renderPlayerView(); + break; + case globals.USER_TYPES.KILLED_PLAYER: + this.container.innerHTML = HTMLFragments.PLAYER_GAME_VIEW; + this.renderPlayerView(true); + break; + case globals.USER_TYPES.MODERATOR: + document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL; + this.container.innerHTML = HTMLFragments.MODERATOR_GAME_VIEW; + this.renderModeratorView(); + break; + case globals.USER_TYPES.TEMPORARY_MODERATOR: + document.getElementById('transfer-mod-prompt').innerHTML = HTMLFragments.TRANSFER_MOD_MODAL; + this.container.innerHTML = HTMLFragments.TEMP_MOD_GAME_VIEW; + this.renderTempModView(); + break; + case globals.USER_TYPES.SPECTATOR: + this.container.innerHTML = HTMLFragments.SPECTATOR_GAME_VIEW; + this.renderSpectatorView(); + break; + default: + break; } - for (const person of this.stateBucket.currentGameState.people) { - lobbyPlayersContainer.appendChild(renderLobbyPerson(person.name, person.userType)); - } - const playerCount = this.stateBucket.currentGameState.people.length; - document.querySelector("label[for='lobby-players']").innerText = - 'Participants (' + playerCount + '/' + getGameSize(this.stateBucket.currentGameState.deck) + ' Players)'; - } - renderLobbyHeader () { - removeExistingTitle(); - const gameLinkContainer = document.getElementById('game-link'); - - const copyImg = document.createElement('img'); - copyImg.setAttribute('src', '../images/copy.svg'); - gameLinkContainer.appendChild(copyImg); - - const time = document.getElementById('game-time'); - const playerCount = document.getElementById('game-player-count'); - const gameCode = document.getElementById('game-code'); - playerCount.innerText = getGameSize(this.stateBucket.currentGameState.deck) + ' Players'; - gameCode.innerHTML = 'Or enter this code on the homepage: ' + this.stateBucket.currentGameState.accessCode + ''; - - let timeString = ''; if (this.stateBucket.currentGameState.timerParams) { - const hours = this.stateBucket.currentGameState.timerParams.hours; - const minutes = this.stateBucket.currentGameState.timerParams.minutes; - if (hours) { - timeString += hours > 1 - ? hours + ' hours ' - : hours + ' hour '; - } - if (minutes) { - timeString += minutes > 1 - ? minutes + ' minutes ' - : minutes + ' minute '; - } - time.innerText = timeString; + this.socket.emit( + globals.SOCKET_EVENTS.IN_GAME_MESSAGE, + globals.EVENT_IDS.GET_TIME_REMAINING, + this.stateBucket.currentGameState.accessCode + ); } else { - timeString = 'untimed'; - time.innerText = timeString; + document.querySelector('#game-timer')?.remove(); + document.querySelector('#timer-container-moderator')?.remove(); + document.querySelector('label[for="game-timer"]')?.remove(); } + } - const link = window.location.protocol + '//' + window.location.host + - '/join/' + this.stateBucket.currentGameState.accessCode + - '?playerCount=' + getGameSize(this.stateBucket.currentGameState.deck) + - '&timer=' + encodeURIComponent(timeString); - - QRCode.toCanvas(document.getElementById('canvas'), link, { scale: 2 }, function (error) { - if (error) console.error(error); - }); - - const linkCopyHandler = (e) => { - if (e.type === 'click' || e.code === 'Enter') { - navigator.clipboard.writeText(link) - .then(() => { - toast('Link copied!', 'success', true); - }); + renderPlayerView (isKilled = false) { + if (isKilled) { + const clientUserType = document.getElementById('client-user-type'); + if (clientUserType) { + clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80'; } - }; - gameLinkContainer.addEventListener('click', linkCopyHandler); - gameLinkContainer.addEventListener('keyup', linkCopyHandler); - - const linkDiv = document.createElement('div'); - linkDiv.innerText = link; - - gameLinkContainer.prepend(linkDiv); - } - - renderLobbyFooter () { - for (const card of this.stateBucket.currentGameState.deck) { - const cardEl = document.createElement('div'); - cardEl.innerText = card.quantity + 'x ' + card.role; - cardEl.classList.add('lobby-card'); } + renderPlayerRole(this.stateBucket.currentGameState); + this.renderPlayersWithNoRoleInformationUnlessRevealed(false); } - renderGameHeader () { - removeExistingTitle(); - // let title = document.createElement("h1"); - // title.innerText = "Game"; - // document.getElementById("game-title").appendChild(title); + renderPlayersWithNoRoleInformationUnlessRevealed (tempMod = false) { + if (tempMod) { + this.removePlayerListEventListeners(); + } + document.querySelectorAll('.game-player').forEach((el) => el.remove()); + /* TODO: UX issue - it's easier to parse visually when players are sorted this way, + but shifting players around when they are killed or revealed is bad UX for the moderator. */ + // sortPeopleByStatus(this.stateBucket.currentGameState.people); + const modType = tempMod ? this.stateBucket.currentGameState.moderator.userType : null; + this.renderGroupOfPlayers( + this.stateBucket.currentGameState.people, + this.killPlayerHandlers, + this.revealRoleHandlers, + this.stateBucket.currentGameState.accessCode, + null, + modType, + this.socket + ); + document.getElementById('players-alive-label').innerText = + 'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' + + this.stateBucket.currentGameState.people.length + ' Alive'; + } + + removePlayerListEventListeners (removeEl = true) { + document.querySelectorAll('.game-player').forEach((el) => { + const 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]; + } + if (removeEl) { + el.remove(); + } + }); } renderModeratorView () { @@ -169,26 +135,66 @@ export class GameStateRenderer { this.renderPlayersWithNoRoleInformationUnlessRevealed(true); } - renderPlayerView (isKilled = false) { - if (isKilled) { - const clientUserType = document.getElementById('client-user-type'); - if (clientUserType) { - clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80'; - } - } - renderPlayerRole(this.stateBucket.currentGameState); - this.renderPlayersWithNoRoleInformationUnlessRevealed(false); - } - renderSpectatorView () { this.renderPlayersWithNoRoleInformationUnlessRevealed(); } - refreshPlayerList (isModerator) { - if (isModerator) { - this.renderPlayersWithRoleAndAlignmentInfo(); - } else { - this.renderPlayersWithNoRoleInformationUnlessRevealed(); + setSocketHandlers () { + this.socket.on(globals.EVENT_IDS.KILL_PLAYER, (id) => { + const killedPerson = this.stateBucket.currentGameState.people.find((person) => person.id === id); + if (killedPerson) { + killedPerson.out = true; + if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) { + toast(killedPerson.name + ' killed.', 'success', true, true, 'medium'); + this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === globals.STATUS.ENDED); + } else { + if (killedPerson.id === this.stateBucket.currentGameState.client.id) { + const clientUserType = document.getElementById('client-user-type'); + if (clientUserType) { + clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80'; + } + this.updatePlayerCardToKilledState(); + toast('You have been killed!', 'warning', true, true, 'medium'); + } else { + toast(killedPerson.name + ' was killed!', 'warning', true, true, 'medium'); + } + if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + this.removePlayerListEventListeners(false); + } else { + this.renderPlayersWithNoRoleInformationUnlessRevealed(false); + } + } + } + }); + + this.socket.on(globals.EVENT_IDS.REVEAL_PLAYER, (revealData) => { + const revealedPerson = this.stateBucket.currentGameState.people.find((person) => person.id === revealData.id); + if (revealedPerson) { + revealedPerson.revealed = true; + revealedPerson.gameRole = revealData.gameRole; + revealedPerson.alignment = revealData.alignment; + if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR) { + toast(revealedPerson.name + ' revealed.', 'success', true, true, 'medium'); + this.renderPlayersWithRoleAndAlignmentInfo(this.stateBucket.currentGameState.status === globals.STATUS.ENDED); + } else { + if (revealedPerson.id === this.stateBucket.currentGameState.client.id) { + toast('Your role has been revealed!', 'warning', true, true, 'medium'); + } else { + toast(revealedPerson.name + ' was revealed as a ' + revealedPerson.gameRole + '!', 'warning', true, true, 'medium'); + } + if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + this.renderPlayersWithNoRoleInformationUnlessRevealed(true); + } else { + this.renderPlayersWithNoRoleInformationUnlessRevealed(false); + } + } + } + }); + + if (this.stateBucket.currentGameState.timerParams) { + const timerWorker = new Worker(new URL('../../timer/Timer.js', import.meta.url)); + const gameTimerManager = new GameTimerManager(stateBucket, this.socket); + gameTimerManager.attachTimerSocketListeners(this.socket, timerWorker); } } @@ -222,96 +228,6 @@ export class GameStateRenderer { this.stateBucket.currentGameState.people.length + ' Alive'; } - removePlayerListEventListeners (removeEl = true) { - document.querySelectorAll('.game-player').forEach((el) => { - const 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]; - } - if (removeEl) { - el.remove(); - } - }); - } - - renderPlayersWithNoRoleInformationUnlessRevealed (tempMod = false) { - if (tempMod) { - this.removePlayerListEventListeners(); - } - document.querySelectorAll('.game-player').forEach((el) => el.remove()); - /* TODO: UX issue - it's easier to parse visually when players are sorted this way, - but shifting players around when they are killed or revealed is bad UX for the moderator. */ - // sortPeopleByStatus(this.stateBucket.currentGameState.people); - const modType = tempMod ? this.stateBucket.currentGameState.moderator.userType : null; - this.renderGroupOfPlayers( - this.stateBucket.currentGameState.people, - this.killPlayerHandlers, - this.revealRoleHandlers, - this.stateBucket.currentGameState.accessCode, - null, - modType, - this.socket - ); - document.getElementById('players-alive-label').innerText = - 'Players: ' + this.stateBucket.currentGameState.people.filter((person) => !person.out).length + ' / ' + - this.stateBucket.currentGameState.people.length + ' Alive'; - } - - updatePlayerCardToKilledState () { - document.querySelector('#role-image').classList.add('killed-card'); - document.getElementById('role-image').setAttribute( - 'src', - '../images/tombstone.png' - ); - } - - displayAvailableModerators () { - document.getElementById('transfer-mod-modal-content').innerText = ''; - document.querySelectorAll('.potential-moderator').forEach((el) => { - const pointer = el.dataset.pointer; - if (pointer && this.transferModHandlers[pointer]) { - el.removeEventListener('click', this.transferModHandlers[pointer]); - delete this.transferModHandlers[pointer]; - } - el.remove(); - }); - renderPotentialMods( - this.stateBucket.currentGameState, - this.stateBucket.currentGameState.people, - this.transferModHandlers, - this.socket - ); - renderPotentialMods( // spectators can also be made mods. - this.stateBucket.currentGameState, - this.stateBucket.currentGameState.spectators, - this.transferModHandlers, - this.socket - ); - - if (document.querySelectorAll('.potential-moderator').length === 0) { - document.getElementById('transfer-mod-modal-content').innerText = 'There is nobody available to transfer to.'; - } - } - - renderEndOfGame (gameState) { - if ( - 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', this.restartGameHandler); - document.getElementById('end-of-game-buttons').prepend(restartGameContainer); - } - this.renderPlayersWithNoRoleInformationUnlessRevealed(); - } - renderGroupOfPlayers ( people, killPlayerHandlers, @@ -337,7 +253,7 @@ export class GameStateRenderer { const roleElement = playerEl.querySelector('.game-player-role'); // Add role/alignment indicators if necessary - if (moderatorType === globals.USER_TYPES.MODERATOR || player.revealed || this.stateBucket.currentGameState.status === globals.STATUS.ENDED) { + if (moderatorType === globals.USER_TYPES.MODERATOR || player.revealed) { if (alignment === null) { roleElement.classList.add(player.alignment); } else { @@ -386,69 +302,41 @@ export class GameStateRenderer { document.getElementById(playerListContainerId).appendChild(playerEl); } } -} -function renderPotentialMods (gameState, group, transferModHandlers, socket) { - const modalContent = document.getElementById('transfer-mod-modal-content'); - for (const member of group) { - if ((member.out || member.userType === globals.USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) { - const container = document.createElement('div'); - container.classList.add('potential-moderator'); - container.setAttribute('tabindex', '0'); - container.dataset.pointer = member.id; - container.innerText = member.name; - transferModHandlers[member.id] = (e) => { - if (e.type === 'click' || e.code === 'Enter') { - ModalManager.dispelModal('transfer-mod-modal', 'transfer-mod-modal-background'); - Confirmation('Transfer moderator powers to \'' + member.name + '\'?', () => { - const transferPrompt = document.getElementById('transfer-mod-prompt'); - if (transferPrompt !== null) { - transferPrompt.innerHTML = ''; - } - socket.emit( - globals.SOCKET_EVENTS.IN_GAME_MESSAGE, - globals.EVENT_IDS.TRANSFER_MODERATOR, - gameState.accessCode, - { personId: member.id } - ); - }); - } - }; + displayAvailableModerators () { + document.getElementById('transfer-mod-modal-content').innerText = ''; + document.querySelectorAll('.potential-moderator').forEach((el) => { + const pointer = el.dataset.pointer; + if (pointer && this.transferModHandlers[pointer]) { + el.removeEventListener('click', this.transferModHandlers[pointer]); + delete this.transferModHandlers[pointer]; + } + el.remove(); + }); + renderPotentialMods( + this.stateBucket.currentGameState, + this.stateBucket.currentGameState.people, + this.transferModHandlers, + this.socket + ); + renderPotentialMods( // spectators can also be made mods. + this.stateBucket.currentGameState, + this.stateBucket.currentGameState.spectators, + this.transferModHandlers, + this.socket + ); - container.addEventListener('click', transferModHandlers[member.id]); - container.addEventListener('keyup', transferModHandlers[member.id]); - modalContent.appendChild(container); + if (document.querySelectorAll('.potential-moderator').length === 0) { + document.getElementById('transfer-mod-modal-content').innerText = 'There is nobody available to transfer to.'; } } -} -function renderLobbyPerson (name, userType) { - const el = document.createElement('div'); - const personNameEl = document.createElement('div'); - const personTypeEl = document.createElement('div'); - personNameEl.innerText = name; - personTypeEl.innerText = userType + globals.USER_TYPE_ICONS[userType]; - el.classList.add('lobby-player'); - - el.appendChild(personNameEl); - el.appendChild(personTypeEl); - - return el; -} - -function getGameSize (cards) { - let quantity = 0; - for (const card of cards) { - quantity += card.quantity; - } - - return quantity; -} - -function removeExistingTitle () { - const existingTitle = document.querySelector('#game-title h1'); - if (existingTitle) { - existingTitle.remove(); + updatePlayerCardToKilledState () { + document.querySelector('#role-image').classList.add('killed-card'); + document.getElementById('role-image').setAttribute( + 'src', + '../images/tombstone.png' + ); } } @@ -503,21 +391,6 @@ function renderPlayerRole (gameState) { }); } -function insertPlaceholderButton (container, append, type) { - const button = document.createElement('div'); - button.classList.add('placeholder-button'); - if (type === 'killed') { - button.innerText = 'Killed'; - } else { - button.innerText = 'Revealed'; - } - if (append) { - container.querySelector('.player-action-buttons').appendChild(button); - } else { - container.querySelector('.player-action-buttons').prepend(button); - } -} - function removeExistingPlayerElements (killPlayerHandlers, revealRoleHandlers) { document.querySelectorAll('.game-player').forEach((el) => { const pointer = el.dataset.pointer; @@ -543,10 +416,63 @@ function createEndGamePromptComponent (socket, stateBucket) { socket.emit( globals.SOCKET_EVENTS.IN_GAME_MESSAGE, globals.EVENT_IDS.END_GAME, - stateBucket.currentGameState.accessCode + stateBucket.currentGameState.accessCode, + null, + () => { + document.querySelector('#end-game-prompt')?.remove(); + } ); }); }); document.getElementById('game-content').appendChild(div); } } + +function insertPlaceholderButton (container, append, type) { + const button = document.createElement('div'); + button.classList.add('placeholder-button'); + if (type === 'killed') { + button.innerText = 'Killed'; + } else { + button.innerText = 'Revealed'; + } + if (append) { + container.querySelector('.player-action-buttons').appendChild(button); + } else { + container.querySelector('.player-action-buttons').prepend(button); + } +} + +function renderPotentialMods (gameState, group, transferModHandlers, socket) { + const modalContent = document.getElementById('transfer-mod-modal-content'); + for (const member of group) { + if ((member.out || member.userType === globals.USER_TYPES.SPECTATOR) && !(member.id === gameState.client.id)) { + const container = document.createElement('div'); + container.classList.add('potential-moderator'); + container.setAttribute('tabindex', '0'); + container.dataset.pointer = member.id; + container.innerText = member.name; + transferModHandlers[member.id] = (e) => { + if (e.type === 'click' || e.code === 'Enter') { + ModalManager.dispelModal('transfer-mod-modal', 'transfer-mod-modal-background'); + Confirmation('Transfer moderator powers to \'' + member.name + '\'?', () => { + const transferPrompt = document.getElementById('transfer-mod-prompt'); + if (transferPrompt !== null) { + transferPrompt.innerHTML = ''; + } + socket.emit( + globals.SOCKET_EVENTS.IN_GAME_MESSAGE, + globals.EVENT_IDS.TRANSFER_MODERATOR, + gameState.accessCode, + { personId: member.id } + ); + }); + } + }; + + container.addEventListener('click', transferModHandlers[member.id]); + container.addEventListener('keyup', transferModHandlers[member.id]); + modalContent.appendChild(container); + } + } +} diff --git a/client/src/modules/game_state/states/Lobby.js b/client/src/modules/game_state/states/Lobby.js new file mode 100644 index 0000000..0c61820 --- /dev/null +++ b/client/src/modules/game_state/states/Lobby.js @@ -0,0 +1,222 @@ +import { QRCode } from '../../third_party/qrcode.js'; +import { toast } from '../../front_end_components/Toast.js'; +import { globals } from '../../../config/globals.js'; +import { HTMLFragments } from '../../front_end_components/HTMLFragments.js'; +import { Confirmation } from '../../front_end_components/Confirmation.js'; + +export class Lobby { + constructor (containerId, stateBucket, socket) { + this.stateBucket = stateBucket; + this.socket = socket; + this.container = document.getElementById(containerId); + this.components = { + HTML: + `