From 0d82227824093cdb06cf1dec2d54dbb3b2099216 Mon Sep 17 00:00:00 2001 From: AlecM33 Date: Wed, 2 Aug 2023 23:23:01 -0400 Subject: [PATCH] allow moderators to kick players and spectators --- client/src/config/globals.js | 6 +- .../front_end_components/HTMLFragments.js | 9 +++ .../modules/game_state/states/InProgress.js | 14 +++- client/src/modules/game_state/states/Lobby.js | 52 ++++++++++++--- .../states/shared/SharedStateUtil.js | 65 ++++++++++++++----- client/src/scripts/home.js | 6 ++ client/src/styles/game.css | 27 +++++++- client/src/styles/modal.css | 8 +++ client/src/view_templates/GameTemplate.js | 1 + client/src/views/home.html | 1 + server/api/GamesAPI.js | 2 - server/config/globals.js | 6 +- server/modules/Events.js | 35 ++++++++++ server/modules/singletons/GameManager.js | 1 - 14 files changed, 195 insertions(+), 38 deletions(-) diff --git a/client/src/config/globals.js b/client/src/config/globals.js index df5f28f..3d502bd 100644 --- a/client/src/config/globals.js +++ b/client/src/config/globals.js @@ -53,7 +53,8 @@ export const globals = { ADD_SPECTATOR: 'addSpectator', UPDATE_SPECTATORS: 'updateSpectators', RESTART_GAME: 'restartGame', - ASSIGN_DEDICATED_MOD: 'assignDedicatedMod' + ASSIGN_DEDICATED_MOD: 'assignDedicatedMod', + KICK_PERSON: 'kickPerson' }, TIMER_EVENTS: function () { return [ @@ -66,7 +67,8 @@ export const globals = { LOBBY_EVENTS: function () { return [ this.EVENT_IDS.PLAYER_JOINED, - this.EVENT_IDS.ADD_SPECTATOR + this.EVENT_IDS.ADD_SPECTATOR, + this.EVENT_IDS.KICK_PERSON ]; }, IN_PROGRESS_EVENTS: function () { diff --git a/client/src/modules/front_end_components/HTMLFragments.js b/client/src/modules/front_end_components/HTMLFragments.js index adf2542..83d89aa 100644 --- a/client/src/modules/front_end_components/HTMLFragments.js +++ b/client/src/modules/front_end_components/HTMLFragments.js @@ -139,6 +139,15 @@ export const HTMLFragments = { `, + PLAYER_OPTIONS_MODAL: + ` + `, MODERATOR_GAME_VIEW: `
diff --git a/client/src/modules/game_state/states/InProgress.js b/client/src/modules/game_state/states/InProgress.js index 60eb3b9..5172e90 100644 --- a/client/src/modules/game_state/states/InProgress.js +++ b/client/src/modules/game_state/states/InProgress.js @@ -51,7 +51,7 @@ export class InProgress { this.stateBucket.currentGameState.accessCode ); setTimeout(() => { - if (this.socket.hasListeners(globals.EVENT_IDS.GET_TIME_REMAINING)) { + if (this.socket.hasListeners(globals.EVENT_IDS.GET_TIME_REMAINING) && document.getElementById('game-timer') !== null) { document.getElementById('game-timer').innerText = 'Timer not found.'; document.getElementById('game-timer').classList.add('timer-error'); } @@ -65,8 +65,16 @@ export class InProgress { const spectatorCount = this.container.querySelector('#spectator-count'); const spectatorHandler = (e) => { if (e.type === 'click' || e.code === 'Enter') { - Confirmation(SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.people - .filter(p => p.userType === globals.USER_TYPES.SPECTATOR)), null, true); + Confirmation( + SharedStateUtil.buildSpectatorList( + this.stateBucket.currentGameState.people + .filter(p => p.userType === globals.USER_TYPES.SPECTATOR), + this.stateBucket.currentGameState.client, + this.socket, + this.stateBucket.currentGameState), + null, + true + ); } }; diff --git a/client/src/modules/game_state/states/Lobby.js b/client/src/modules/game_state/states/Lobby.js index 4ef53c8..14cbe0f 100644 --- a/client/src/modules/game_state/states/Lobby.js +++ b/client/src/modules/game_state/states/Lobby.js @@ -61,8 +61,14 @@ export class Lobby { const spectatorHandler = (e) => { if (e.type === 'click' || e.code === 'Enter') { - Confirmation(SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.people - .filter(p => p.userType === globals.USER_TYPES.SPECTATOR), this.stateBucket.currentGameState.client), null, true); + Confirmation( + SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.people + .filter(p => p.userType === globals.USER_TYPES.SPECTATOR), + this.stateBucket.currentGameState.client, + this.socket, + this.stateBucket.currentGameState), + null, true + ); } }; @@ -95,7 +101,7 @@ export class Lobby { } ); for (const person of sorted.filter(p => p.userType !== globals.USER_TYPES.SPECTATOR)) { - lobbyPlayersContainer.appendChild(renderLobbyPerson(person.name, person.userType, this.stateBucket.currentGameState.client)); + lobbyPlayersContainer.appendChild(renderLobbyPerson(person, this.stateBucket.currentGameState, this.socket)); } const playerCount = this.stateBucket.currentGameState.people.filter( p => p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR @@ -126,6 +132,32 @@ export class Lobby { document.getElementById('spectator-count') ); }); + + this.socket.on(globals.EVENT_IDS.KICK_PERSON, (kickedId, gameIsFull) => { + if (kickedId === this.stateBucket.currentGameState.client.id) { + window.location = '/?message=' + encodeURIComponent('You were kicked by the moderator.'); + } else { + const kickedIndex = this.stateBucket.currentGameState.people.findIndex(person => person.id === kickedId); + if (kickedIndex >= 0) { + this.stateBucket.currentGameState.people + .splice(kickedIndex, 1); + } + this.stateBucket.currentGameState.isFull = gameIsFull; + SharedStateUtil.setNumberOfSpectators( + this.stateBucket.currentGameState.people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR).length, + document.getElementById('spectator-count') + ); + this.populatePlayers(); + if (( + this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR + || this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR + ) + ) { + toast('player kicked.', 'success', true, true, 'short'); + this.displayStartGamePromptForModerators(); + } + } + }); } displayStartGamePromptForModerators () { @@ -192,23 +224,25 @@ function getTimeString (gameState) { } } -function renderLobbyPerson (name, userType, client) { +function renderLobbyPerson (person, gameState, socket) { const el = document.createElement('div'); const personNameEl = document.createElement('div'); personNameEl.classList.add('lobby-player-name'); const personTypeEl = document.createElement('div'); - personNameEl.innerText = name; - personTypeEl.innerText = userType + globals.USER_TYPE_ICONS[userType]; + personNameEl.innerText = person.name; + personTypeEl.innerText = person.userType + globals.USER_TYPE_ICONS[person.userType]; el.classList.add('lobby-player'); - if (userType === globals.USER_TYPES.MODERATOR || userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + if (person.userType === globals.USER_TYPES.MODERATOR || person.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { el.classList.add('moderator'); } el.appendChild(personNameEl); el.appendChild(personTypeEl); - if (client.userType === globals.USER_TYPES.MODERATOR || client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { - SharedStateUtil.addPlayerOptions(el); + if ((gameState.client.userType === globals.USER_TYPES.MODERATOR || gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) + && person.userType !== globals.USER_TYPES.MODERATOR && person.userType !== globals.USER_TYPES.TEMPORARY_MODERATOR) { + SharedStateUtil.addPlayerOptions(el, person, socket, gameState); + el.dataset.pointer = person.id; } return el; diff --git a/client/src/modules/game_state/states/shared/SharedStateUtil.js b/client/src/modules/game_state/states/shared/SharedStateUtil.js index ddaea3d..0d8c60c 100644 --- a/client/src/modules/game_state/states/shared/SharedStateUtil.js +++ b/client/src/modules/game_state/states/shared/SharedStateUtil.js @@ -24,7 +24,7 @@ export const SharedStateUtil = { }, restartHandler: (stateBucket, status = globals.STATUS.IN_PROGRESS) => { - console.log("HEY") + console.log('HEY'); XHRUtility.xhr( '/api/games/' + stateBucket.currentGameState.accessCode + '/restart?status=' + status, 'PATCH', @@ -175,17 +175,46 @@ export const SharedStateUtil = { } }, - addPlayerOptions: (personEl) => { - const kickButton = document.createElement('img'); - kickButton.setAttribute('tabIndex', '0'); - kickButton.setAttribute('className', 'role-remove'); - kickButton.setAttribute('src', '../images/3-vertical-dots-icon.svg'); - kickButton.setAttribute('title', 'Kick Player'); - kickButton.setAttribute('alt', 'Kick Player'); - personEl.appendChild(kickButton); + addPlayerOptions: (personEl, person, socket, gameState) => { + const optionsButton = document.createElement('img'); + const optionsHandler = (e) => { + if (e.type === 'click' || e.code === 'Enter') { + document.getElementById('player-options-modal-content').innerHTML = ''; + const kickOption = document.createElement('button'); + kickOption.setAttribute('class', 'player-option'); + kickOption.innerText = 'Kick Person'; + kickOption.addEventListener('click', () => { + ModalManager.dispelModal('player-options-modal', 'player-options-modal-background'); + Confirmation('Kick \'' + person.name + '\'?', () => { + socket.emit( + globals.SOCKET_EVENTS.IN_GAME_MESSAGE, + globals.EVENT_IDS.KICK_PERSON, + gameState.accessCode, + { personId: person.id } + ); + }); + }); + document.getElementById('player-options-modal-content').appendChild(kickOption); + ModalManager.displayModal( + 'player-options-modal', + 'player-options-modal-background', + 'close-player-options-modal-button' + ); + } + }; + + optionsButton.addEventListener('click', optionsHandler); + optionsButton.addEventListener('keyup', optionsHandler); + optionsButton.setAttribute('tabIndex', '0'); + optionsButton.setAttribute('className', 'role-remove'); + optionsButton.setAttribute('src', '../images/3-vertical-dots-icon.svg'); + optionsButton.setAttribute('title', 'Player Options'); + optionsButton.setAttribute('alt', 'Player Options'); + + personEl.appendChild(optionsButton); }, - buildSpectatorList (people, client) { + buildSpectatorList (people, client, socket, gameState) { const list = document.createElement('div'); const spectators = people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR); if (spectators.length === 0) { @@ -200,7 +229,8 @@ export const SharedStateUtil = { list.appendChild(spectatorEl); if (client.userType === globals.USER_TYPES.MODERATOR || client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { - this.addPlayerOptions(spectatorEl); + this.addPlayerOptions(spectatorEl, spectator, socket, gameState); + spectatorEl.dataset.pointer = spectator.id; } } } @@ -259,13 +289,12 @@ function processGameState ( lobby.populatePlayers(); globals.LOBBY_EVENTS().forEach(e => socket.removeAllListeners(e)); lobby.setSocketHandlers(); - if (( - currentGameState.client.userType === globals.USER_TYPES.MODERATOR - || currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR - ) - && refreshPrompt - ) { - lobby.displayStartGamePromptForModerators(); + if (currentGameState.client.userType === globals.USER_TYPES.MODERATOR + || currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + if (refreshPrompt) { + lobby.displayStartGamePromptForModerators(); + } + document.getElementById('player-options-prompt').innerHTML = HTMLFragments.PLAYER_OPTIONS_MODAL; } break; case globals.STATUS.IN_PROGRESS: diff --git a/client/src/scripts/home.js b/client/src/scripts/home.js index f6f85d0..0060664 100644 --- a/client/src/scripts/home.js +++ b/client/src/scripts/home.js @@ -1,9 +1,15 @@ import { XHRUtility } from '../modules/utility/XHRUtility.js'; import { toast } from '../modules/front_end_components/Toast.js'; import { injectNavbar } from '../modules/front_end_components/Navbar.js'; +import { Confirmation } from '../modules/front_end_components/Confirmation.js'; const home = () => { injectNavbar(); + const urlParams = new URLSearchParams(window.location.search); + const message = urlParams.get('message'); + if (message && message.length > 0) { + Confirmation(message); + } document.getElementById('join-form').addEventListener('submit', attemptToJoinGame); }; diff --git a/client/src/styles/game.css b/client/src/styles/game.css index b196860..36c7f4f 100644 --- a/client/src/styles/game.css +++ b/client/src/styles/game.css @@ -273,11 +273,36 @@ h1 { width: 90%; } -#role-info-modal .modal-button-container { +#role-info-modal .modal-button-container, #player-options-modal .modal-button-container { margin-top: 1em; justify-content: center; } +#player-options-modal-content { + margin: 1em 0; +} + +.player-option { + background-color: #4645525c; + border: 2px solid #46455299; + color: #d7d7d7; + display: flex; + font-family: 'signika-negative', sans-serif !important; + padding: 10px 30px; + border-radius: 5px; + font-size: 16px; + flex-direction: column; + align-items: flex-start; + text-align: left; + margin: 10px 0; +} + +.player-option:hover, .player-option:active { + border: 2px solid #e7e7e7; + background-color: #33343c; + cursor: pointer; +} + #game-role-info-container .role-info-name { border-radius: 5px; font-size: 20px; diff --git a/client/src/styles/modal.css b/client/src/styles/modal.css index fba45d8..c508e79 100644 --- a/client/src/styles/modal.css +++ b/client/src/styles/modal.css @@ -76,6 +76,14 @@ border: 1px solid #46455299; } +#player-options-modal { + z-index: 200001; +} + +#player-options-modal-background { + z-index: 200000; +} + #custom-role-info-modal-name { font-family: 'signika-negative', sans-serif; font-size: 23px; diff --git a/client/src/view_templates/GameTemplate.js b/client/src/view_templates/GameTemplate.js index 9c16664..2d6b90f 100644 --- a/client/src/view_templates/GameTemplate.js +++ b/client/src/view_templates/GameTemplate.js @@ -1,6 +1,7 @@ const template = `
+
diff --git a/client/src/views/home.html b/client/src/views/home.html index df9865b..9d96ef6 100644 --- a/client/src/views/home.html +++ b/client/src/views/home.html @@ -19,6 +19,7 @@ +
diff --git a/server/api/GamesAPI.js b/server/api/GamesAPI.js index 6616ca2..95429b8 100644 --- a/server/api/GamesAPI.js +++ b/server/api/GamesAPI.js @@ -103,8 +103,6 @@ router.patch('/:code/restart', async function (req, res) { const game = await gameManager.getActiveGame(req.body.accessCode); if (game) { gameManager.restartGame(game, gameManager.namespace, req.query.status).then((data) => { - console.log(req.query.status); - console.log(req.query.toLobby); res.status(200).send(); }).catch((code) => { res.status(code).send(); diff --git a/server/config/globals.js b/server/config/globals.js index 11af3fa..af4bced 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -55,7 +55,8 @@ const globals = { SYNC_GAME_STATE: 'syncGameState', UPDATE_SOCKET: 'updateSocket', ASSIGN_DEDICATED_MOD: 'assignDedicatedMod', - TIMER_EVENT: 'timerEvent' + TIMER_EVENT: 'timerEvent', + KICK_PERSON: 'kickPerson' }, SYNCABLE_EVENTS: function () { return [ @@ -74,7 +75,8 @@ const globals = { this.EVENT_IDS.ASSIGN_DEDICATED_MOD, this.EVENT_IDS.RESUME_TIMER, this.EVENT_IDS.PAUSE_TIMER, - this.EVENT_IDS.END_TIMER + this.EVENT_IDS.END_TIMER, + this.EVENT_IDS.KICK_PERSON ]; }, TIMER_EVENTS: function () { diff --git a/server/modules/Events.js b/server/modules/Events.js index 7770357..eb1cd8f 100644 --- a/server/modules/Events.js +++ b/server/modules/Events.js @@ -1,5 +1,6 @@ const globals = require('../config/globals'); const GameStateCurator = require('./GameStateCurator'); +const UsernameGenerator = require('./UsernameGenerator'); const EVENT_IDS = globals.EVENT_IDS; const Events = [ @@ -22,6 +23,40 @@ const Events = [ ); } }, + { + id: EVENT_IDS.KICK_PERSON, + stateChange: async (game, socketArgs, vars) => { + const toBeClearedIndex = game.people.findIndex( + (person) => person.id === socketArgs.personId && person.assigned === true + ); + if (toBeClearedIndex >= 0) { + const toBeCleared = game.people[toBeClearedIndex]; + if (toBeCleared.userType === globals.USER_TYPES.SPECTATOR) { + game.people.splice(toBeClearedIndex, 1); + } else { + toBeCleared.assigned = false; + toBeCleared.socketId = null; + toBeCleared.cookie = (() => { + let id = ''; + for (let i = 0; i < globals.INSTANCE_ID_LENGTH; i ++) { + id += globals.INSTANCE_ID_CHAR_POOL[Math.floor(Math.random() * globals.INSTANCE_ID_CHAR_POOL.length)]; + } + return id; + })(); + toBeCleared.hasEnteredName = false; + toBeCleared.name = UsernameGenerator.generate(); + game.isFull = vars.gameManager.isGameFull(game); + } + } + }, + communicate: async (game, socketArgs, vars) => { + vars.gameManager.namespace.in(game.accessCode).emit( + EVENT_IDS.KICK_PERSON, + socketArgs.personId, + game.isFull + ); + } + }, { id: EVENT_IDS.ADD_SPECTATOR, stateChange: async (game, socketArgs, vars) => { diff --git a/server/modules/singletons/GameManager.js b/server/modules/singletons/GameManager.js index f7b329e..3149ae7 100644 --- a/server/modules/singletons/GameManager.js +++ b/server/modules/singletons/GameManager.js @@ -272,7 +272,6 @@ class GameManager { game.status = globals.STATUS.LOBBY; } - await this.refreshGame(game); await this.eventManager.publisher?.publish( globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,