" +
+ "
",
GAME_PLAYER:
"
",
+ INITIAL_GAME_DOM:
+ "
" +
+ "
" +
+ "
",
+ // via https://loading.io/css/
+ SPINNER:
+ "
" +
+ "
" +
+ "
" +
+ "
" +
+ "
" +
+ "
" +
+ "
" +
+ "
" +
+ "
" +
+ "
" +
+ "
" +
+ "
" +
+ "
" +
+ "
",
+ NAME_CHANGE_MODAL:
+ "
" +
+ "
"
}
diff --git a/client/scripts/game.js b/client/scripts/game.js
index 0988a8c..62618a5 100644
--- a/client/scripts/game.js
+++ b/client/scripts/game.js
@@ -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;
+ }
+}
diff --git a/client/styles/GLOBAL.css b/client/styles/GLOBAL.css
index c00c31e..b782f18 100644
--- a/client/styles/GLOBAL.css
+++ b/client/styles/GLOBAL.css
@@ -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;
+ }
+}
diff --git a/client/styles/create.css b/client/styles/create.css
index 5976516..0903409 100644
--- a/client/styles/create.css
+++ b/client/styles/create.css
@@ -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;
diff --git a/client/styles/game.css b/client/styles/game.css
index e703ee9..5ac025a 100644
--- a/client/styles/game.css
+++ b/client/styles/game.css
@@ -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 {
diff --git a/client/views/create.html b/client/views/create.html
index c6f2c0b..4dbbe2d 100644
--- a/client/views/create.html
+++ b/client/views/create.html
@@ -29,7 +29,7 @@
Home