allow moderators to kick players and spectators

This commit is contained in:
AlecM33
2023-08-02 23:23:01 -04:00
parent d00f3d630a
commit 0d82227824
14 changed files with 195 additions and 38 deletions

View File

@@ -53,7 +53,8 @@ export const globals = {
ADD_SPECTATOR: 'addSpectator', ADD_SPECTATOR: 'addSpectator',
UPDATE_SPECTATORS: 'updateSpectators', UPDATE_SPECTATORS: 'updateSpectators',
RESTART_GAME: 'restartGame', RESTART_GAME: 'restartGame',
ASSIGN_DEDICATED_MOD: 'assignDedicatedMod' ASSIGN_DEDICATED_MOD: 'assignDedicatedMod',
KICK_PERSON: 'kickPerson'
}, },
TIMER_EVENTS: function () { TIMER_EVENTS: function () {
return [ return [
@@ -66,7 +67,8 @@ export const globals = {
LOBBY_EVENTS: function () { LOBBY_EVENTS: function () {
return [ return [
this.EVENT_IDS.PLAYER_JOINED, this.EVENT_IDS.PLAYER_JOINED,
this.EVENT_IDS.ADD_SPECTATOR this.EVENT_IDS.ADD_SPECTATOR,
this.EVENT_IDS.KICK_PERSON
]; ];
}, },
IN_PROGRESS_EVENTS: function () { IN_PROGRESS_EVENTS: function () {

View File

@@ -139,6 +139,15 @@ export const HTMLFragments = {
<button id='close-mod-transfer-modal-button' class='app-button cancel'>Cancel</button> <button id='close-mod-transfer-modal-button' class='app-button cancel'>Cancel</button>
</div> </div>
</div>`, </div>`,
PLAYER_OPTIONS_MODAL:
`<div id='player-options-modal-background' class='modal-background'></div>
<div tabindex='-1' id='player-options-modal' class='modal'>
<h2>Person Options:</h2>
<div id='player-options-modal-content'></div>
<div class='modal-button-container'>
<button id='close-player-options-modal-button' class='app-button cancel'>Close</button>
</div>
</div>`,
MODERATOR_GAME_VIEW: MODERATOR_GAME_VIEW:
`<div id='game-header'> `<div id='game-header'>
<div id='timer-container-moderator'> <div id='timer-container-moderator'>

View File

@@ -51,7 +51,7 @@ export class InProgress {
this.stateBucket.currentGameState.accessCode this.stateBucket.currentGameState.accessCode
); );
setTimeout(() => { 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').innerText = 'Timer not found.';
document.getElementById('game-timer').classList.add('timer-error'); document.getElementById('game-timer').classList.add('timer-error');
} }
@@ -65,8 +65,16 @@ export class InProgress {
const spectatorCount = this.container.querySelector('#spectator-count'); const spectatorCount = this.container.querySelector('#spectator-count');
const spectatorHandler = (e) => { const spectatorHandler = (e) => {
if (e.type === 'click' || e.code === 'Enter') { if (e.type === 'click' || e.code === 'Enter') {
Confirmation(SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.people Confirmation(
.filter(p => p.userType === globals.USER_TYPES.SPECTATOR)), null, true); 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
);
} }
}; };

View File

@@ -61,8 +61,14 @@ export class Lobby {
const spectatorHandler = (e) => { const spectatorHandler = (e) => {
if (e.type === 'click' || e.code === 'Enter') { if (e.type === 'click' || e.code === 'Enter') {
Confirmation(SharedStateUtil.buildSpectatorList(this.stateBucket.currentGameState.people Confirmation(
.filter(p => p.userType === globals.USER_TYPES.SPECTATOR), this.stateBucket.currentGameState.client), null, true); 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)) { 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( const playerCount = this.stateBucket.currentGameState.people.filter(
p => p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR p => p.userType !== globals.USER_TYPES.MODERATOR && p.userType !== globals.USER_TYPES.SPECTATOR
@@ -126,6 +132,32 @@ export class Lobby {
document.getElementById('spectator-count') 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 () { displayStartGamePromptForModerators () {
@@ -192,23 +224,25 @@ function getTimeString (gameState) {
} }
} }
function renderLobbyPerson (name, userType, client) { function renderLobbyPerson (person, gameState, socket) {
const el = document.createElement('div'); const el = document.createElement('div');
const personNameEl = document.createElement('div'); const personNameEl = document.createElement('div');
personNameEl.classList.add('lobby-player-name'); personNameEl.classList.add('lobby-player-name');
const personTypeEl = document.createElement('div'); const personTypeEl = document.createElement('div');
personNameEl.innerText = name; personNameEl.innerText = person.name;
personTypeEl.innerText = userType + globals.USER_TYPE_ICONS[userType]; personTypeEl.innerText = person.userType + globals.USER_TYPE_ICONS[person.userType];
el.classList.add('lobby-player'); 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.classList.add('moderator');
} }
el.appendChild(personNameEl); el.appendChild(personNameEl);
el.appendChild(personTypeEl); el.appendChild(personTypeEl);
if (client.userType === globals.USER_TYPES.MODERATOR || client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { if ((gameState.client.userType === globals.USER_TYPES.MODERATOR || gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR)
SharedStateUtil.addPlayerOptions(el); && 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; return el;

View File

@@ -24,7 +24,7 @@ export const SharedStateUtil = {
}, },
restartHandler: (stateBucket, status = globals.STATUS.IN_PROGRESS) => { restartHandler: (stateBucket, status = globals.STATUS.IN_PROGRESS) => {
console.log("HEY") console.log('HEY');
XHRUtility.xhr( XHRUtility.xhr(
'/api/games/' + stateBucket.currentGameState.accessCode + '/restart?status=' + status, '/api/games/' + stateBucket.currentGameState.accessCode + '/restart?status=' + status,
'PATCH', 'PATCH',
@@ -175,17 +175,46 @@ export const SharedStateUtil = {
} }
}, },
addPlayerOptions: (personEl) => { addPlayerOptions: (personEl, person, socket, gameState) => {
const kickButton = document.createElement('img'); const optionsButton = document.createElement('img');
kickButton.setAttribute('tabIndex', '0'); const optionsHandler = (e) => {
kickButton.setAttribute('className', 'role-remove'); if (e.type === 'click' || e.code === 'Enter') {
kickButton.setAttribute('src', '../images/3-vertical-dots-icon.svg'); document.getElementById('player-options-modal-content').innerHTML = '';
kickButton.setAttribute('title', 'Kick Player'); const kickOption = document.createElement('button');
kickButton.setAttribute('alt', 'Kick Player'); kickOption.setAttribute('class', 'player-option');
personEl.appendChild(kickButton); 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 list = document.createElement('div');
const spectators = people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR); const spectators = people.filter(p => p.userType === globals.USER_TYPES.SPECTATOR);
if (spectators.length === 0) { if (spectators.length === 0) {
@@ -200,7 +229,8 @@ export const SharedStateUtil = {
list.appendChild(spectatorEl); list.appendChild(spectatorEl);
if (client.userType === globals.USER_TYPES.MODERATOR || client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { 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(); lobby.populatePlayers();
globals.LOBBY_EVENTS().forEach(e => socket.removeAllListeners(e)); globals.LOBBY_EVENTS().forEach(e => socket.removeAllListeners(e));
lobby.setSocketHandlers(); lobby.setSocketHandlers();
if (( if (currentGameState.client.userType === globals.USER_TYPES.MODERATOR
currentGameState.client.userType === globals.USER_TYPES.MODERATOR || currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
|| currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR if (refreshPrompt) {
) lobby.displayStartGamePromptForModerators();
&& refreshPrompt }
) { document.getElementById('player-options-prompt').innerHTML = HTMLFragments.PLAYER_OPTIONS_MODAL;
lobby.displayStartGamePromptForModerators();
} }
break; break;
case globals.STATUS.IN_PROGRESS: case globals.STATUS.IN_PROGRESS:

View File

@@ -1,9 +1,15 @@
import { XHRUtility } from '../modules/utility/XHRUtility.js'; import { XHRUtility } from '../modules/utility/XHRUtility.js';
import { toast } from '../modules/front_end_components/Toast.js'; import { toast } from '../modules/front_end_components/Toast.js';
import { injectNavbar } from '../modules/front_end_components/Navbar.js'; import { injectNavbar } from '../modules/front_end_components/Navbar.js';
import { Confirmation } from '../modules/front_end_components/Confirmation.js';
const home = () => { const home = () => {
injectNavbar(); 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); document.getElementById('join-form').addEventListener('submit', attemptToJoinGame);
}; };

View File

@@ -273,11 +273,36 @@ h1 {
width: 90%; width: 90%;
} }
#role-info-modal .modal-button-container { #role-info-modal .modal-button-container, #player-options-modal .modal-button-container {
margin-top: 1em; margin-top: 1em;
justify-content: center; 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 { #game-role-info-container .role-info-name {
border-radius: 5px; border-radius: 5px;
font-size: 20px; font-size: 20px;

View File

@@ -76,6 +76,14 @@
border: 1px solid #46455299; border: 1px solid #46455299;
} }
#player-options-modal {
z-index: 200001;
}
#player-options-modal-background {
z-index: 200000;
}
#custom-role-info-modal-name { #custom-role-info-modal-name {
font-family: 'signika-negative', sans-serif; font-family: 'signika-negative', sans-serif;
font-size: 23px; font-size: 23px;

View File

@@ -1,6 +1,7 @@
const template = const template =
`<div id="role-info-prompt"></div> `<div id="role-info-prompt"></div>
<div id="transfer-mod-prompt"></div> <div id="transfer-mod-prompt"></div>
<div id="player-options-prompt"></div>
<div class="spinner-background"></div> <div class="spinner-background"></div>
<div class="spinner-container"> <div class="spinner-container">
<div class="lds-spinner"> <div class="lds-spinner">

View File

@@ -19,6 +19,7 @@
<link rel="stylesheet" href="./styles/GLOBAL.css"> <link rel="stylesheet" href="./styles/GLOBAL.css">
<link rel="stylesheet" href="./styles/home.css"> <link rel="stylesheet" href="./styles/home.css">
<link rel="stylesheet" href="/styles/hamburgers.css"> <link rel="stylesheet" href="/styles/hamburgers.css">
<link rel="stylesheet" href="./styles/confirmation.css">
</head> </head>
<body> <body>
<div id="mobile-menu-background-overlay"></div> <div id="mobile-menu-background-overlay"></div>

View File

@@ -103,8 +103,6 @@ router.patch('/:code/restart', async function (req, res) {
const game = await gameManager.getActiveGame(req.body.accessCode); const game = await gameManager.getActiveGame(req.body.accessCode);
if (game) { if (game) {
gameManager.restartGame(game, gameManager.namespace, req.query.status).then((data) => { gameManager.restartGame(game, gameManager.namespace, req.query.status).then((data) => {
console.log(req.query.status);
console.log(req.query.toLobby);
res.status(200).send(); res.status(200).send();
}).catch((code) => { }).catch((code) => {
res.status(code).send(); res.status(code).send();

View File

@@ -55,7 +55,8 @@ const globals = {
SYNC_GAME_STATE: 'syncGameState', SYNC_GAME_STATE: 'syncGameState',
UPDATE_SOCKET: 'updateSocket', UPDATE_SOCKET: 'updateSocket',
ASSIGN_DEDICATED_MOD: 'assignDedicatedMod', ASSIGN_DEDICATED_MOD: 'assignDedicatedMod',
TIMER_EVENT: 'timerEvent' TIMER_EVENT: 'timerEvent',
KICK_PERSON: 'kickPerson'
}, },
SYNCABLE_EVENTS: function () { SYNCABLE_EVENTS: function () {
return [ return [
@@ -74,7 +75,8 @@ const globals = {
this.EVENT_IDS.ASSIGN_DEDICATED_MOD, this.EVENT_IDS.ASSIGN_DEDICATED_MOD,
this.EVENT_IDS.RESUME_TIMER, this.EVENT_IDS.RESUME_TIMER,
this.EVENT_IDS.PAUSE_TIMER, this.EVENT_IDS.PAUSE_TIMER,
this.EVENT_IDS.END_TIMER this.EVENT_IDS.END_TIMER,
this.EVENT_IDS.KICK_PERSON
]; ];
}, },
TIMER_EVENTS: function () { TIMER_EVENTS: function () {

View File

@@ -1,5 +1,6 @@
const globals = require('../config/globals'); const globals = require('../config/globals');
const GameStateCurator = require('./GameStateCurator'); const GameStateCurator = require('./GameStateCurator');
const UsernameGenerator = require('./UsernameGenerator');
const EVENT_IDS = globals.EVENT_IDS; const EVENT_IDS = globals.EVENT_IDS;
const Events = [ 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, id: EVENT_IDS.ADD_SPECTATOR,
stateChange: async (game, socketArgs, vars) => { stateChange: async (game, socketArgs, vars) => {

View File

@@ -272,7 +272,6 @@ class GameManager {
game.status = globals.STATUS.LOBBY; game.status = globals.STATUS.LOBBY;
} }
await this.refreshGame(game); await this.refreshGame(game);
await this.eventManager.publisher?.publish( await this.eventManager.publisher?.publish(
globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM, globals.REDIS_CHANNELS.ACTIVE_GAME_STREAM,