some name change stuff

This commit is contained in:
Alec
2021-12-20 00:28:40 -05:00
parent 133d3a6a03
commit 82ca096600
16 changed files with 534 additions and 125 deletions

View File

@@ -14,7 +14,8 @@ export const globals = {
GET_TIME_REMAINING: 'getTimeRemaining',
KILL_PLAYER: 'killPlayer',
REVEAL_PLAYER: 'revealPlayer',
TRANSFER_MODERATOR: 'transferModerator'
TRANSFER_MODERATOR: 'transferModerator',
CHANGE_NAME: 'changeName'
},
STATUS: {
LOBBY: "lobby",
@@ -24,12 +25,16 @@ export const globals = {
GOOD: "good",
EVIL: "evil"
},
MESSAGES: {
ENTER_NAME: "Client must enter name."
},
EVENTS: {
PLAYER_JOINED: "playerJoined",
SYNC_GAME_STATE: "syncGameState",
START_TIMER: "startTimer",
KILL_PLAYER: "killPlayer",
REVEAL_PLAYER: 'revealPlayer'
REVEAL_PLAYER: 'revealPlayer',
CHANGE_NAME: 'changeName'
},
USER_TYPES: {
MODERATOR: "moderator",

View File

@@ -313,12 +313,13 @@ function updateTracker(step) {
function showButtons(back, forward, forwardHandler, backHandler, builtGame=null) {
document.querySelector("#step-back-button")?.remove();
document.querySelector("#step-forward-button")?.remove();
document.querySelector("#create-game")?.remove();
if (back) {
let backButton = document.createElement("button");
backButton.innerText = "\u2bc7 Back";
backButton.addEventListener('click', backHandler);
backButton.setAttribute("id", "step-back-button");
document.getElementById("creation-step-buttons").appendChild(backButton);
document.getElementById("tracker-container").prepend(backButton);
}
if (forward && builtGame === null) {
@@ -326,7 +327,7 @@ function showButtons(back, forward, forwardHandler, backHandler, builtGame=null)
fwdButton.innerHTML = "Next \u25b6";
fwdButton.addEventListener('click', forwardHandler);
fwdButton.setAttribute("id", "step-forward-button");
document.getElementById("creation-step-buttons").appendChild(fwdButton);
document.getElementById("tracker-container").appendChild(fwdButton);
} else if (forward && builtGame !== null) {
let createButton = document.createElement("button");
createButton.innerText = "Create";
@@ -344,7 +345,7 @@ function showButtons(back, forward, forwardHandler, backHandler, builtGame=null)
toast("You must provide your name.", "error", true);
}
});
document.getElementById("creation-step-buttons").appendChild(createButton);
document.getElementById("tracker-container").appendChild(createButton);
}
}

View File

@@ -16,13 +16,13 @@ export class GameStateRenderer {
renderLobbyPlayers() {
document.querySelectorAll('.lobby-player').forEach((el) => el.remove())
let lobbyPlayersContainer = document.getElementById("lobby-players");
if (this.gameState.client.userType === globals.USER_TYPES.PLAYER) {
if (this.gameState.client.userType === globals.USER_TYPES.PLAYER && this.gameState.moderator.userType === globals.USER_TYPES.MODERATOR) {
lobbyPlayersContainer.appendChild(renderLobbyPerson(this.gameState.moderator.name, this.gameState.moderator.userType))
}
for (let person of this.gameState.people) {
lobbyPlayersContainer.appendChild(renderLobbyPerson(person.name,person.userType))
}
let playerCount = this.gameState.people.filter((person) => person.userType === globals.USER_TYPES.PLAYER).length;
let playerCount = this.gameState.people.length;
document.querySelector("label[for='lobby-players']").innerText =
"People (" + playerCount + "/" + getGameSize(this.gameState.deck) + " Players)";
}
@@ -129,8 +129,23 @@ 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.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);
renderGroupOfPlayers(
teamEvil,
this.killPlayerHandlers,
this.revealRoleHandlers,
this.gameState.accessCode,
globals.ALIGNMENT.EVIL,
this.gameState.moderator.userType,
this.socket
);
renderGroupOfPlayers(
teamGood,
this.killPlayerHandlers,
this.revealRoleHandlers,
this.gameState.accessCode,
globals.ALIGNMENT.GOOD,
this.gameState.moderator.userType,
this.socket);
document.getElementById("players-alive-label").innerText =
'Players: ' + this.gameState.people.filter((person) => !person.out).length + ' / ' + this.gameState.people.length + ' Alive';
@@ -152,10 +167,17 @@ export class GameStateRenderer {
});
}
document.querySelectorAll('.game-player').forEach((el) => el.remove());
this.gameState.people.sort((a, b) => {
return a.name >= b.name ? 1 : -1;
});
renderGroupOfPlayers(this.gameState, this.killPlayerHandlers, this.revealRoleHandlers, this.gameState.accessCode, null, tempMod, this.socket);
sortPeopleByStatus(this.gameState.people);
let modType = tempMod ? this.gameState.moderator.userType : null;
renderGroupOfPlayers(
this.gameState.people,
this.killPlayerHandlers,
this.revealRoleHandlers,
this.gameState.accessCode,
null,
modType,
this.socket
);
document.getElementById("players-alive-label").innerText =
'Players: ' + this.gameState.people.filter((person) => !person.out).length + ' / ' + this.gameState.people.length + ' Alive';
@@ -220,6 +242,19 @@ function renderLobbyPerson(name, userType) {
return el;
}
function sortPeopleByStatus(people) {
people.sort((a, b) => {
if (a.out !== b.out) {
return a.out ? 1 : -1;
} else {
if (a.revealed !== b.revealed) {
return a.revealed? -1 : 1;
}
return a.name >= b.name ? 1 : -1;
}
});
}
function getGameSize(cards) {
let quantity = 0;
for (let card of cards) {
@@ -237,11 +272,11 @@ function removeExistingTitle() {
}
// TODO: refactor to reduce the cyclomatic complexity of this function
function renderGroupOfPlayers(gameState, killPlayerHandlers, revealRoleHandlers, accessCode=null, alignment=null, moderator=false, socket=null) {
for (let player of gameState.people) {
function renderGroupOfPlayers(people, killPlayerHandlers, revealRoleHandlers, accessCode=null, alignment=null, moderatorType, socket=null) {
for (let player of people) {
let container = document.createElement("div");
container.classList.add('game-player');
if (moderator) {
if (moderatorType) {
container.dataset.pointer = player.id;
container.innerHTML = templates.MODERATOR_PLAYER;
} else {
@@ -250,9 +285,9 @@ function renderGroupOfPlayers(gameState, killPlayerHandlers, revealRoleHandlers,
container.querySelector('.game-player-name').innerText = player.name;
let roleElement = container.querySelector('.game-player-role')
if (moderator) {
if (moderatorType) {
roleElement.classList.add(alignment);
if (gameState.moderator.userType === globals.USER_TYPES.MODERATOR) {
if (moderatorType === globals.USER_TYPES.MODERATOR) {
roleElement.innerText = player.gameRole;
document.getElementById("player-list-moderator-team-" + alignment).appendChild(container);
} else {
@@ -275,11 +310,12 @@ function renderGroupOfPlayers(gameState, killPlayerHandlers, revealRoleHandlers,
if (player.out) {
container.classList.add('killed');
if (moderator) {
if (moderatorType) {
container.querySelector('.kill-player-button')?.remove();
insertPlaceholderButton(container, false, "killed");
}
} else {
if (moderator) {
if (moderatorType) {
killPlayerHandlers[player.id] = () => {
if (confirm("KILL " + player.name + "?")) {
socket.emit(globals.COMMANDS.KILL_PLAYER, accessCode, player.id);
@@ -290,11 +326,12 @@ function renderGroupOfPlayers(gameState, killPlayerHandlers, revealRoleHandlers,
}
if (player.revealed) {
if (moderator) {
if (moderatorType) {
container.querySelector('.reveal-role-button')?.remove();
insertPlaceholderButton(container, true, "revealed");
}
} else {
if (moderator) {
if (moderatorType) {
revealRoleHandlers[player.id] = () => {
if (confirm("REVEAL " + player.name + "?")) {
socket.emit(globals.COMMANDS.REVEAL_PLAYER, accessCode, player.id);
@@ -340,3 +377,18 @@ function renderPlayerRole(gameState) {
document.getElementById("game-role").style.display = 'none';
});
}
function insertPlaceholderButton(container, append, type) {
let 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);
}
}

View File

@@ -9,7 +9,7 @@ export const templates = {
"<div id='game-player-count'></div>" +
"</div>" +
"<div>" +
"<div>" +
"<div id='lobby-people-container'>" +
"<label for='lobby-players'>Other People</label>" +
"<div id='lobby-players'></div>" +
"</div>" +
@@ -41,7 +41,7 @@ export const templates = {
"<h4>Click to reveal your role</h4>" +
"<p>(click again to hide)</p>" +
"</div>" +
"<div>" +
"<div id='game-people-container'>" +
"<label id='players-alive-label'></label>" +
"<div id='game-player-list'></div>" +
"</div>",
@@ -121,7 +121,7 @@ export const templates = {
"<h4>Click to reveal your role</h4>" +
"<p>(click again to hide)</p>" +
"</div>" +
"<div>" +
"<div id='game-people-container'>" +
"<label id='players-alive-label'></label>" +
"<div id='game-player-list'></div>" +
"</div>" +
@@ -131,13 +131,52 @@ export const templates = {
"<div class='game-player-name'></div>" +
"<div class='game-player-role'></div>" +
"</div>" +
"<div>" +
"<div class='player-action-buttons'>" +
"<button class='moderator-player-button kill-player-button'>Kill \uD83D\uDC80</button>" +
"<button class='moderator-player-button reveal-role-button'>Reveal Role <img src='../images/eye.svg'/></button>" +
"<button class='moderator-player-button reveal-role-button'>Reveal Role <img src='../images/eye.svg'/></button>" +
"</div>",
GAME_PLAYER:
"<div>" +
"<div class='game-player-name'></div>" +
"<div class='game-player-role'></div>" +
"</div>",
INITIAL_GAME_DOM:
"<div id='game-title'></div>" +
"<div id='client-container'>" +
"<label for='client'>You</label>" +
"<div id='client'>" +
"<div id='client-name'></div>" +
"<div id='client-user-type'></div>" +
"</div>" +
"</div>" +
"<div id='game-state-container'></div>",
// via https://loading.io/css/
SPINNER:
"<div class=\"lds-spinner\">" +
"<div></div>" +
"<div></div>" +
"<div></div>" +
"<div></div>" +
"<div></div>" +
"<div></div>" +
"<div></div>" +
"<div></div>" +
"<div></div>" +
"<div></div>" +
"<div></div>" +
"<div></div>" +
"</div>",
NAME_CHANGE_MODAL:
"<div id='change-name-modal-background' class='modal-background'></div>" +
"<div id='change-name-modal' class='modal'>" +
"<form id='change-name-form'>" +
"<div id='transfer-mod-form-content'>" +
"<label for='player-new-name'>Your name:</label>" +
"<input id='player-new-name' type='text'/>" +
"</div>" +
"<div id='modal-button-container'>" +
"<input type='submit' id='submit-new-name' value='Set Name'/>" +
"</div>" +
"</form>" +
"</div>"
}

View File

@@ -4,6 +4,7 @@ import {templates} from "../modules/Templates.js";
import {GameStateRenderer} from "../modules/GameStateRenderer.js";
import {cancelCurrentToast, toast} from "../modules/Toast.js";
import {GameTimerManager} from "../modules/GameTimerManager.js";
import {ModalManager} from "../modules/ModalManager.js";
export const game = () => {
let timerWorker;
@@ -28,20 +29,43 @@ function prepareGamePage(environment, socket, timerWorker) {
const accessCode = splitUrl[1];
if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) {
socket.emit(globals.COMMANDS.FETCH_GAME_STATE, accessCode, userId, function (gameState) {
let currentGameState = gameState;
document.querySelector('.spinner-container')?.remove();
document.querySelector('.spinner-background')?.remove();
if (gameState === null) {
window.location = '/not-found?reason=' + encodeURIComponent('game-not-found');
} else if (!gameState.client.hasEnteredName) {
userId = gameState.client.cookie;
UserUtility.setAnonymousUserId(userId, environment);
document.getElementById("game-content").innerHTML = templates.NAME_CHANGE_MODAL;
document.getElementById("change-name-form").onsubmit = (e) => {
e.preventDefault();
let name = document.getElementById("player-new-name").value;
if (validateName(name)) {
socket.emit(globals.COMMANDS.CHANGE_NAME, gameState.accessCode, { name: name, personId: gameState.client.id }, (result) => {
switch (result) {
case "taken":
toast('This name is already taken.', 'error', true, true, 8);
break;
case "changed":
ModalManager.dispelModal("change-name-modal", "change-name-modal-background")
toast('Name set.', 'success', true, true, 5);
document.getElementById("game-content").innerHTML = templates.INITIAL_GAME_DOM;
propagateNameChange(currentGameState, name, currentGameState.client.id);
initializeGame(currentGameState, socket, timerWorker, userId);
}
})
} else {
toast("Name must be fewer than 30 characters.", 'error', true, true, 8);
}
}
} else {
toast('You are connected.', 'success', true, true, 3);
document.getElementById("game-content").innerHTML = templates.INITIAL_GAME_DOM;
toast('You are connected.', 'success', true, true, 2);
console.log(gameState);
userId = gameState.client.cookie;
UserUtility.setAnonymousUserId(userId, environment);
let gameStateRenderer = new GameStateRenderer(gameState, socket);
let gameTimerManager;
if (gameState.timerParams) {
gameTimerManager = new GameTimerManager(gameState, socket);
}
setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTimerManager);
processGameState(gameState, userId, socket, gameStateRenderer);
initializeGame(gameState, socket, timerWorker, userId);
}
});
} else {
@@ -49,18 +73,28 @@ function prepareGamePage(environment, socket, timerWorker) {
}
}
function processGameState (gameState, userId, socket, gameStateRenderer) {
displayClientInfo(gameState.client.name, gameState.client.userType);
switch (gameState.status) {
function initializeGame(currentGameState, socket, timerWorker, userId) {
let gameStateRenderer = new GameStateRenderer(currentGameState, socket);
let gameTimerManager;
if (currentGameState.timerParams) {
gameTimerManager = new GameTimerManager(currentGameState, socket);
}
setClientSocketHandlers(currentGameState, gameStateRenderer, socket, timerWorker, gameTimerManager);
processGameState(currentGameState, userId, socket, gameStateRenderer);
}
function processGameState (currentGameState, userId, socket, gameStateRenderer) {
displayClientInfo(currentGameState.client.name, currentGameState.client.userType);
switch (currentGameState.status) {
case globals.STATUS.LOBBY:
document.getElementById("game-state-container").innerHTML = templates.LOBBY;
gameStateRenderer.renderLobbyHeader();
gameStateRenderer.renderLobbyPlayers();
if (
gameState.isFull
currentGameState.isFull
&& (
gameState.client.userType === globals.USER_TYPES.MODERATOR
|| gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
)
) {
displayStartGamePromptForModerators(gameStateRenderer, socket);
@@ -68,7 +102,7 @@ function processGameState (gameState, userId, socket, gameStateRenderer) {
break;
case globals.STATUS.IN_PROGRESS:
gameStateRenderer.renderGameHeader();
switch (gameState.client.userType) {
switch (currentGameState.client.userType) {
case globals.USER_TYPES.PLAYER:
document.getElementById("game-state-container").innerHTML = templates.PLAYER_GAME_VIEW;
gameStateRenderer.renderPlayerView();
@@ -97,7 +131,7 @@ function processGameState (gameState, userId, socket, gameStateRenderer) {
break;
}
socket.emit(globals.COMMANDS.GET_TIME_REMAINING, gameState.accessCode);
socket.emit(globals.COMMANDS.GET_TIME_REMAINING, currentGameState.accessCode);
break;
default:
break;
@@ -110,17 +144,17 @@ function displayClientInfo(name, userType) {
document.getElementById("client-user-type").innerText += globals.USER_TYPE_ICONS[userType];
}
function setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTimerManager) {
function setClientSocketHandlers(currentGameState, gameStateRenderer, socket, timerWorker, gameTimerManager) {
if (!socket.hasListeners(globals.EVENTS.PLAYER_JOINED)) {
socket.on(globals.EVENTS.PLAYER_JOINED, (player, gameIsFull) => {
toast(player.name + " joined!", "success", false);
gameStateRenderer.gameState.people.push(player);
currentGameState.people.push(player);
gameStateRenderer.renderLobbyPlayers();
if (
gameIsFull
&& (
gameStateRenderer.gameState.client.userType === globals.USER_TYPES.MODERATOR
|| gameStateRenderer.gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|| currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
)
) {
displayStartGamePromptForModerators(gameStateRenderer, socket);
@@ -131,12 +165,13 @@ function setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTim
socket.on(globals.EVENTS.SYNC_GAME_STATE, () => {
socket.emit(
globals.COMMANDS.FETCH_GAME_STATE,
gameStateRenderer.gameState.accessCode,
gameStateRenderer.gameState.client.cookie,
currentGameState.accessCode,
currentGameState.client.cookie,
function (gameState) {
gameStateRenderer.gameState = gameState;
gameTimerManager.gameState = gameState;
processGameState(gameState, gameState.client.cookie, socket, gameStateRenderer);
currentGameState = gameState;
gameStateRenderer.gameState = currentGameState;
gameTimerManager.gameState = currentGameState;
processGameState(currentGameState, gameState.client.cookie, socket, gameStateRenderer);
}
);
});
@@ -148,14 +183,14 @@ function setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTim
if (!socket.hasListeners(globals.EVENTS.KILL_PLAYER)) {
socket.on(globals.EVENTS.KILL_PLAYER, (id) => {
let killedPerson = gameStateRenderer.gameState.people.find((person) => person.id === id);
let killedPerson = currentGameState.people.find((person) => person.id === id);
if (killedPerson) {
killedPerson.out = true;
if (gameStateRenderer.gameState.client.userType === globals.USER_TYPES.MODERATOR) {
if (currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
toast(killedPerson.name + ' killed.', 'success', true, true, 6);
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo()
} else {
if (killedPerson.id === gameStateRenderer.gameState.client.id) {
if (killedPerson.id === currentGameState.client.id) {
let clientUserType = document.getElementById("client-user-type");
if (clientUserType) {
clientUserType.innerText = globals.USER_TYPES.KILLED_PLAYER + ' \uD83D\uDC80'
@@ -165,7 +200,7 @@ function setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTim
} else {
toast(killedPerson.name + ' was killed!', 'warning', false, true, 6);
}
if (gameStateRenderer.gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
if (currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
} else {
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
@@ -177,21 +212,21 @@ function setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTim
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);
let revealedPerson = currentGameState.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) {
if (currentGameState.client.userType === globals.USER_TYPES.MODERATOR) {
toast(revealedPerson.name + ' revealed.', 'success', true, true, 6);
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo()
} else {
if (revealedPerson.id === gameStateRenderer.gameState.client.id) {
if (revealedPerson.id === currentGameState.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);
}
if (gameStateRenderer.gameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
if (currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
} else {
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
@@ -200,17 +235,23 @@ function setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTim
}
});
}
if (!socket.hasListeners(globals.EVENTS.CHANGE_NAME)) {
socket.on(globals.EVENTS.CHANGE_NAME, (personId, name) => {
propagateNameChange(currentGameState, name, personId);
processGameState(currentGameState, currentGameState.client.cookie, socket, gameStateRenderer);
});
}
}
function displayStartGamePromptForModerators(gameStateRenderer, socket) {
document.getElementById("lobby-players").setAttribute("style", 'margin-bottom: 130px');
let div = document.createElement("div");
div.innerHTML = templates.START_GAME_PROMPT;
document.body.appendChild(div);
document.getElementById("start-game-button").addEventListener('click', (e) => {
e.preventDefault();
if (confirm("Start the game and deal roles?")) {
socket.emit(globals.COMMANDS.START_GAME, gameStateRenderer.gameState.accessCode, gameStateRenderer.gameState.client.cookie);
socket.emit(globals.COMMANDS.START_GAME, gameStateRenderer.gameState.accessCode,gameStateRenderer.gameState.client.cookie);
}
});
@@ -226,3 +267,24 @@ function runGameTimer (hours, minutes, tickRate, soundManager, timerWorker) {
timerWorker.postMessage({ hours: hours, minutes: minutes, tickInterval: tickRate });
}
}
function validateName(name) {
return typeof name === 'string' && name.length <= 30;
}
function propagateNameChange(gameState, name, personId) {
gameState.client.name = name;
let matchingPerson = gameState.people.find((person) => person.id === personId);
if (matchingPerson) {
matchingPerson.name = name;
}
if (gameState.moderator.id === personId) {
gameState.moderator.name = name;
}
let matchingSpectator = gameState.spectators?.find((spectator) => spectator.id === personId);
if (matchingSpectator) {
matchingSpectator.name = name;
}
}

View File

@@ -97,6 +97,7 @@ button:active, input[type=submit]:active {
justify-content: center;
width: 95%;
margin: 0 auto;
align-items: center;
}
button:hover, input[type="submit"]:hover, #game-link:hover {
@@ -108,6 +109,7 @@ input {
}
.info-message {
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
@@ -295,3 +297,118 @@ input {
font-size: 25px;
}
}
.spinner-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: calc(100% + 100px);
background-color: rgba(0, 0, 0, 0.75);
z-index: 50;
}
/* via https://loading.io/css/ */
.spinner-container {
position: relative;
}
.spinner-container p {
margin: auto;
position: fixed;
top: 0;
left: 0;
bottom: -8em;
font-size: 20px;
z-index: 51;
text-align: center;
right: 0;
color: #d7d7d7;
height: fit-content;
}
.lds-spinner {
margin: auto;
position: fixed;
top: -80px;
left: 0;
bottom: 0;
z-index: 51;
right: 0;
height: fit-content;
display: inline-block;
width: 80px;
}
.lds-spinner div {
transform-origin: 40px 40px;
animation: lds-spinner 1.2s linear infinite;
}
.lds-spinner div:after {
content: " ";
display: block;
position: absolute;
top: 3px;
left: 37px;
width: 6px;
height: 18px;
border-radius: 20%;
background: #d7d7d7;
}
.lds-spinner div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -1.1s;
}
.lds-spinner div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -1s;
}
.lds-spinner div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.9s;
}
.lds-spinner div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.8s;
}
.lds-spinner div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.7s;
}
.lds-spinner div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.6s;
}
.lds-spinner div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.5s;
}
.lds-spinner div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.4s;
}
.lds-spinner div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.3s;
}
.lds-spinner div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.2s;
}
.lds-spinner div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.1s;
}
.lds-spinner div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
}
@keyframes lds-spinner {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@@ -222,14 +222,25 @@ input[type="number"] {
margin-bottom: 8em;
}
#game-creation-container {
width: 95%;
max-width: 60em;
}
#tracker-container {
display: flex;
align-items: center;
margin-top: 2em;
justify-content: center;
width: 100%;
}
#creation-step-tracker {
display: flex;
justify-content: center;
margin-top: 2em;
}
#step-forward-button, #step-back-button, #create-game {
position: absolute;
font-family: sans-serif;
font-size: 20px;
padding: 10px 20px;

View File

@@ -14,6 +14,19 @@
margin: 0.5em 0;
}
#lobby-players {
overflow-y: auto;
max-height: 30em;
overflow-x: hidden;
padding: 0 10px;
border-radius: 3px;
}
#lobby-people-container label {
display: block;
margin-bottom: 0.5em;
}
.lobby-player-client {
border: 2px solid #21ba45;
}
@@ -30,6 +43,9 @@
justify-content: center;
flex-direction: row;
flex-wrap: wrap;
display: flex;
width: 95%;
margin: 0 auto 115px auto;
}
#lobby-header {
@@ -124,7 +140,7 @@ h1 {
display: flex;
align-items: center;
justify-content: center;
background-color: #333243;
background-color: #171522;
border: 5px solid #61606a;
position: relative;
flex-direction: column;
@@ -215,7 +231,7 @@ h1 {
#client-container {
max-width: 35em;
margin-top: 2em;
margin: 1em 0;
}
#client {
@@ -226,12 +242,14 @@ h1 {
justify-content: space-between;
background-color: #333243;
border-radius: 3px;
min-width: 15em;
}
#client-name {
color: whitesmoke;
font-family: 'diavlo', sans-serif;
font-size: 30px;
margin: 0.25em 2em 0.25em 0;
}
#client-user-type {
@@ -267,7 +285,7 @@ label[for='moderator'] {
bottom: 0;
/* width: fit-content; */
font-size: 20px;
height: 120px;
height: 85px;
margin: 0 auto;
animation: fade-in-slide-up 10s ease;
animation-fill-mode: forwards;
@@ -276,16 +294,20 @@ label[for='moderator'] {
background-color: #333243;
}
#end-game-prompt {
box-shadow: 0 -6px 40px black;
}
#start-game-button, #end-game-button {
font-family: 'signika-negative', sans-serif !important;
padding: 10px;
border-radius: 3px;
color: whitesmoke;
font-size: 30px;
cursor: pointer;
border: 2px solid transparent;
transition: background-color, border 0.3s ease-out;
text-shadow: 0 3px 4px rgb(0 0 0 / 85%);
font-size: 25px;
}
#start-game-button {
@@ -358,9 +380,16 @@ label[for='moderator'] {
justify-content: space-between;
margin: 0.5em 0;
position: relative;
box-shadow: 0 1px 1px rgba(0,0,0,0.11),
0 2px 2px rgba(0,0,0,0.11),
0 4px 4px rgba(0,0,0,0.11),
0 8px 8px rgba(0,0,0,0.11),
0 16px 16px rgba(0,0,0,0.11),
0 32px 32px rgba(0,0,0,0.11);
}
.game-player-name {
position: relative;
width: 10em;
overflow: hidden;
white-space: nowrap;
@@ -380,12 +409,27 @@ label[for='moderator'] {
transition: background-color, border 0.3s ease-out;
text-shadow: 0 3px 4px rgb(0 0 0 / 55%);
margin: 5px 0 5px 25px;
min-width: 6em;
width: 117px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.placeholder-button {
font-family: 'signika-negative', sans-serif !important;
padding: 5px;
display: flex;
justify-content: center;
border-radius: 3px;
color: #767676;
font-weight: bold;
font-size: 16px;
border: 2px solid transparent;
text-shadow: 0 3px 4px rgb(0 0 0 / 55%);
margin: 5px 0 5px 25px;
width: 103px;
}
.reveal-role-button {
background-color: #3f5256;
}
@@ -395,11 +439,10 @@ label[for='moderator'] {
margin-left: 5px;
}
.killed::after {
#game-player-list > .game-player.killed::after {
content: '\01F480';
position: absolute;
right: -44px;
font-size: 24px;
margin-left: 1em;
}
.killed, .killed .game-player-role {
@@ -442,6 +485,13 @@ label[for='moderator'] {
flex-wrap: wrap;
}
#game-player-list {
overflow-y: auto;
overflow-x: hidden;
padding: 0 10px;
max-height: 37em;
}
#game-player-list > div {
padding: 2px 10px;
border-radius: 3px;
@@ -462,14 +512,14 @@ label[for='moderator'] {
justify-content: center;
}
@media(max-width: 685px) {
#end-game-button {
font-size: 25px;
}
#change-name-modal-background {
cursor: default;
}
#end-game-prompt {
height: 85px;
}
#lobby-people-container , #game-people-container {
background-color: #333243;
padding: 10px 10px 0 10px;
border-radius: 3px;
}
@keyframes pulse {

View File

@@ -29,7 +29,7 @@
<img src="../images/Werewolf_Small.png"/>
<a href="/">Home</a>
</div>
<div class="container">
<div id="game-creation-container" class="container">
<div id="add-role-modal-background" class="modal-background" style="display: none"></div>
<div id="add-role-modal" class="modal" style="display: none">
<form id="add-role-form">
@@ -55,11 +55,13 @@
</form>
</div>
<h1>Create A Game</h1>
<div id="creation-step-tracker">
<div id="tracker-step-1" class="creation-step creation-step-filled"></div>
<div id="tracker-step-2" class="creation-step"></div>
<div id="tracker-step-3" class="creation-step"></div>
<div id="tracker-step-4" class="creation-step"></div>
<div id="tracker-container">
<div id="creation-step-tracker">
<div id="tracker-step-1" class="creation-step creation-step-filled"></div>
<div id="tracker-step-2" class="creation-step"></div>
<div id="tracker-step-3" class="creation-step"></div>
<div id="tracker-step-4" class="creation-step"></div>
</div>
</div>
<div id="creation-step-container">
<h2 id="step-title">Select your method of moderation:</h2>
@@ -70,7 +72,6 @@
<div class="animated-placeholder animated-placeholder-long"></div>
<div class="animated-placeholder animated-placeholder-long"></div>
</div>
<div id="creation-step-buttons"></div>
<!-- <div id="deck-container">-->
<!-- <label for="deck">Game Deck: 0 Players</label>-->
<!-- <div id="deck"></div>-->

View File

@@ -16,21 +16,33 @@
<link rel="stylesheet" href="/styles/GLOBAL.css">
<link rel="stylesheet" href="/styles/game.css">
<link rel="stylesheet" href="/styles/modal.css">
<link rel="preload" as="font" href="/webfonts/SignikaNegative-Light.woff2" crossorigin/>
<link rel="preload" as="font" href="/webfonts/Diavlo_LIGHT_II_37.woff2" crossorigin/>
</head>
<body>
<div class="spinner-background"></div>
<div class="spinner-container">
<div class="lds-spinner">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<p>Connecting to game...</p>
</div>
<div id="navbar">
<img src="/images/Werewolf_Small.png"/>
<a href="/">Home</a>
</div>
<div id="game-title"></div>
<div id="client-container" class="container">
<label for="client">You</label>
<div id="client">
<div id="client-name"></div>
<div id="client-user-type"></div>
</div>
</div>
<div class="container" id="game-state-container"></div>
<div id="game-content" class="container"></div>
<script type="module">
import { game } from "../scripts/game.js";
game();

View File

@@ -11,7 +11,11 @@ const globals = {
GET_TIME_REMAINING: 'getTimeRemaining',
KILL_PLAYER: 'killPlayer',
REVEAL_PLAYER: 'revealPlayer',
TRANSFER_MODERATOR: 'transferModerator'
TRANSFER_MODERATOR: 'transferModerator',
CHANGE_NAME: 'changeName'
},
MESSAGES: {
ENTER_NAME: "Client must enter name."
},
STATUS: {
LOBBY: "lobby",

View File

@@ -12,6 +12,7 @@ class Person {
this.assigned = assigned;
this.out = false;
this.revealed = false;
this.hasEnteredName = false;
}
}

View File

@@ -22,27 +22,27 @@ class ActiveGameRunner {
//game.status = globals.STATUS.ENDED;
game.timerParams.paused = false;
game.timerParams.timeRemaining = 0;
this.logger.debug('PARENT: END GAME');
this.logger.trace('PARENT: END GAME');
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_GAME, game.accessCode);
break;
case globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER:
game.timerParams.paused = true;
this.logger.trace(msg);
game.timerParams.timeRemaining = msg.timeRemaining;
this.logger.debug('PARENT: PAUSE TIMER');
this.logger.trace('PARENT: PAUSE TIMER');
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, game.timerParams.timeRemaining);
break;
case globals.GAME_PROCESS_COMMANDS.RESUME_TIMER:
game.timerParams.paused = false;
this.logger.trace(msg);
game.timerParams.timeRemaining = msg.timeRemaining;
this.logger.debug('PARENT: RESUME TIMER');
this.logger.trace('PARENT: RESUME TIMER');
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, game.timerParams.timeRemaining);
break;
case globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING:
this.logger.trace(msg);
game.timerParams.timeRemaining = msg.timeRemaining;
this.logger.debug('PARENT: GET TIME REMAINING');
this.logger.trace('PARENT: GET TIME REMAINING');
namespace.to(msg.socketId).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
break;
}

View File

@@ -104,6 +104,10 @@ class GameManager {
this.logger.debug('game ' + accessCode + ': killing player ' + person.name);
person.userType = globals.USER_TYPES.KILLED_PLAYER;
person.out = true;
// temporary moderators will transfer their powers automatically to the first person they kill.
if (game.moderator.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) {
transferModeratorPowers(game, person, namespace, this.logger);
}
namespace.in(accessCode).emit(globals.CLIENT_COMMANDS.KILL_PLAYER, person.id)
}
}
@@ -134,25 +138,26 @@ class GameManager {
if (!person) {
person = game.spectators.find((spectator) => spectator.id === personId)
}
if (person && (person.out || person.userType === globals.USER_TYPES.SPECTATOR)) {
this.logger.debug('game ' + accessCode + ': transferring mod powers to ' + person.name);
if (game.people.includes(game.moderator)) { // the current moderator was at one point a dealt-in player.
game.moderator.userType = globals.USER_TYPES.KILLED_PLAYER; // restore their state from before being made mod.
transferModeratorPowers(game, person, namespace, this.logger);
}
});
socket.on(globals.CLIENT_COMMANDS.CHANGE_NAME, (accessCode, data, ackFn) => {
let game = this.activeGameRunner.activeGames[accessCode];
if (game) {
let person = findPersonById(game, data.personId);
if (person) {
if (!isNameTaken(game, data.name)) {
ackFn("changed");
person.name = data.name.trim();
person.hasEnteredName = true;
socket.to(accessCode).emit(globals.EVENTS.SYNC_GAME_STATE);
} else {
game.moderator.userType = globals.USER_TYPES.SPECTATOR;
if (!game.spectators.includes(game.moderator)) {
game.spectators.push(game.moderator);
}
if (game.spectators.includes(person)) {
game.spectators.splice(game.spectators.indexOf(person), 1);
}
ackFn("taken");
}
person.userType = globals.USER_TYPES.MODERATOR;
game.moderator = person;
namespace.in(accessCode).emit(globals.EVENTS.SYNC_GAME_STATE);
}
}
})
});
}
@@ -216,7 +221,9 @@ function initializeModerator(name, hasDedicatedModerator) {
const userType = hasDedicatedModerator
? globals.USER_TYPES.MODERATOR
: globals.USER_TYPES.TEMPORARY_MODERATOR;
return new Person(createRandomId(), createRandomId(), name, userType)
let moderator = new Person(createRandomId(), createRandomId(), name, userType);
moderator.hasEnteredName = true // they did this when creating the game.
return moderator;
}
function initializePeopleForGame(uniqueCards, moderator) {
@@ -242,7 +249,17 @@ function initializePeopleForGame(uniqueCards, moderator) {
}
while (j < numberOfRoles) {
people.push(new Person(createRandomId(), createRandomId(), UsernameGenerator.generate(), globals.USER_TYPES.PLAYER, cards[j].role, cards[j].description, cards[j].team))
let person = new Person(
createRandomId(),
createRandomId(),
UsernameGenerator.generate(),
globals.USER_TYPES.PLAYER,
cards[j].role,
cards[j].description,
cards[j].team
);
person.hasEnteredName = false;
people.push(person);
j ++;
}
@@ -280,13 +297,33 @@ class Singleton {
}
}
function transferModeratorPowers(game, person, namespace, logger) {
if (person && (person.out || person.userType === globals.USER_TYPES.SPECTATOR)) {
logger.debug('game ' + game.accessCode + ': transferring mod powers to ' + person.name);
if (game.people.includes(game.moderator)) { // the current moderator was at one point a dealt-in player.
game.moderator.userType = globals.USER_TYPES.KILLED_PLAYER; // restore their state from before being made mod.
} else {
game.moderator.userType = globals.USER_TYPES.SPECTATOR;
if (!game.spectators.includes(game.moderator)) {
game.spectators.push(game.moderator);
}
if (game.spectators.includes(person)) {
game.spectators.splice(game.spectators.indexOf(person), 1);
}
}
person.userType = globals.USER_TYPES.MODERATOR;
game.moderator = person;
namespace.in(game.accessCode).emit(globals.EVENTS.SYNC_GAME_STATE);
}
}
/* Since clients are anonymous, we have to rely to some extent on a cookie to identify them. Socket ids
are unique to a client, but they are re-generated if a client disconnects and then reconnects.
Thus, to have the most resilient identification i.e. to let them refresh, navigate away and come back,
get disconnected and reconnect, etc. we should have a combination of the socket id and the cookie. This
will also allow us to reject certain theoretical ways of breaking things, such as copying someone else's
cookie. Though if a client wants to clear their cookie and reset their connection, there's not much we can do.
The best thing in my opinion is to make it hard for clients to _accidentally_ break their experience.
The best thing in my opinion is to make it exceptionally difficult for clients to _accidentally_ break their experience.
*/
function handleRequestForGameState(namespace, logger, gameRunner, accessCode, personCookie, ackFn, socket) {
const game = gameRunner.activeGames[accessCode];
@@ -374,4 +411,19 @@ function isGameFull(game) {
return game.moderator.assigned === true && !game.people.find((person) => person.assigned === false);
}
function findPersonById(game, id) {
let person = game.people.find((person) => person.id === id)
if (!person) {
person = game.spectators.find((spectator) => spectator.id === id)
}
return person;
}
function isNameTaken(game, name) {
let processedName = name.toLowerCase().trim();
return (game.people.find((person) => person.name.toLowerCase().trim() === processedName))
|| (game.moderator.name.toLowerCase().trim() === processedName)
|| (game.spectators.find((spectator) => spectator.name.toLowerCase().trim() === processedName))
}
module.exports = Singleton;

View File

@@ -7,7 +7,7 @@ process.on('message', (msg) => {
const logger = require('./Logger')(msg.logLevel);
switch (msg.command) {
case globals.GAME_PROCESS_COMMANDS.START_TIMER:
logger.debug('CHILD PROCESS ' + msg.accessCode + ': START TIMER');
logger.trace('CHILD PROCESS ' + msg.accessCode + ': START TIMER');
timer = new ServerTimer(
msg.hours,
msg.minutes,
@@ -23,7 +23,7 @@ process.on('message', (msg) => {
break;
case globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER:
timer.stopTimer();
logger.debug('CHILD PROCESS ' + msg.accessCode + ': PAUSE TIMER');
logger.trace('CHILD PROCESS ' + msg.accessCode + ': PAUSE TIMER');
process.send({ command: globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, timeRemaining: timer.currentTimeInMillis});
break;
@@ -34,13 +34,13 @@ process.on('message', (msg) => {
process.send({ command: globals.GAME_PROCESS_COMMANDS.END_GAME });
process.exit(0);
});
logger.debug('CHILD PROCESS ' + msg.accessCode + ': RESUME TIMER');
logger.trace('CHILD PROCESS ' + msg.accessCode + ': RESUME TIMER');
process.send({ command: globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, timeRemaining: timer.currentTimeInMillis});
break;
case globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING:
logger.debug('CHILD PROCESS ' + msg.accessCode + ': GET TIME REMAINING');
logger.trace('CHILD PROCESS ' + msg.accessCode + ': GET TIME REMAINING');
process.send({
command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
timeRemaining: timer.currentTimeInMillis,

View File

@@ -1,7 +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.
/* 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) => {
@@ -11,9 +12,10 @@ const GameStateCurator = {
function getGameStateBasedOnPermissions(game, person, gameRunner) {
let client = game.status === globals.STATUS.LOBBY // people won't be able to know their role until past the lobby stage.
? { name: person.name, cookie: person.cookie, userType: person.userType }
? { name: person.name, hasEnteredName: person.hasEnteredName, id: person.id, cookie: person.cookie, userType: person.userType }
: {
name: person.name,
hasEnteredName: person.hasEnteredName,
id: person.id,
cookie: person.cookie,
userType: person.userType,