diff --git a/client/src/config/globals.js b/client/src/config/globals.js index dbceaf9..05b859c 100644 --- a/client/src/config/globals.js +++ b/client/src/config/globals.js @@ -3,7 +3,7 @@ export const globals = { USER_SIGNATURE_LENGTH: 25, CLOCK_TICK_INTERVAL_MILLIS: 100, MAX_CUSTOM_ROLE_NAME_LENGTH: 50, - MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 500, + MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 1000, TOAST_DURATION_DEFAULT: 6, ACCESS_CODE_LENGTH: 4, PLAYER_ID_COOKIE_KEY: 'play-werewolf-anon-id', diff --git a/client/src/images/screenshots/dedicated_mod_inprogress_mobile.png b/client/src/images/screenshots/dedicated_mod_inprogress_mobile.png new file mode 100644 index 0000000..87fbc1e Binary files /dev/null and b/client/src/images/screenshots/dedicated_mod_inprogress_mobile.png differ diff --git a/client/src/images/screenshots/dedicated_mod_lobby_mobile.png b/client/src/images/screenshots/dedicated_mod_lobby_mobile.png new file mode 100644 index 0000000..9d2ad70 Binary files /dev/null and b/client/src/images/screenshots/dedicated_mod_lobby_mobile.png differ diff --git a/client/src/images/screenshots/localhost_5000_game_NJ36(Samsung Galaxy S8+).png b/client/src/images/screenshots/localhost_5000_game_NJ36(Samsung Galaxy S8+).png deleted file mode 100644 index ab4cd3a..0000000 Binary files a/client/src/images/screenshots/localhost_5000_game_NJ36(Samsung Galaxy S8+).png and /dev/null differ diff --git a/client/src/images/screenshots/player.PNG b/client/src/images/screenshots/player.PNG deleted file mode 100644 index 45054c1..0000000 Binary files a/client/src/images/screenshots/player.PNG and /dev/null differ diff --git a/client/src/images/screenshots/player_inprogress_mobile.png b/client/src/images/screenshots/player_inprogress_mobile.png new file mode 100644 index 0000000..e5960e7 Binary files /dev/null and b/client/src/images/screenshots/player_inprogress_mobile.png differ diff --git a/client/src/images/screenshots/temp_mod_inprogress_mobile.png b/client/src/images/screenshots/temp_mod_inprogress_mobile.png new file mode 100644 index 0000000..581c877 Binary files /dev/null and b/client/src/images/screenshots/temp_mod_inprogress_mobile.png differ diff --git a/client/src/images/tutorial/mod-transfer.gif b/client/src/images/tutorial/mod-transfer.gif new file mode 100644 index 0000000..7018c1c Binary files /dev/null and b/client/src/images/tutorial/mod-transfer.gif differ diff --git a/client/src/images/tutorial/transfer-mod.gif b/client/src/images/tutorial/transfer-mod.gif deleted file mode 100644 index d43ffe6..0000000 Binary files a/client/src/images/tutorial/transfer-mod.gif and /dev/null differ diff --git a/client/src/modules/game_state/states/InProgress.js b/client/src/modules/game_state/states/InProgress.js index aa59362..fa43c26 100644 --- a/client/src/modules/game_state/states/InProgress.js +++ b/client/src/modules/game_state/states/InProgress.js @@ -209,7 +209,10 @@ export class InProgress { this.socket.on(globals.EVENT_IDS.NEW_SPECTATOR, (spectator) => { stateBucket.currentGameState.spectators.push(spectator); - this.displayAvailableModerators(); + if (this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.MODERATOR + || this.stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR) { + this.displayAvailableModerators(); + } }); if (this.stateBucket.currentGameState.timerParams) { diff --git a/client/src/scripts/join.js b/client/src/scripts/join.js index b6c9aa4..e0d816f 100644 --- a/client/src/scripts/join.js +++ b/client/src/scripts/join.js @@ -42,13 +42,9 @@ const joinHandler = (e) => { function sendJoinRequest (e, name, accessCode) { document.getElementById('join-game-form').onsubmit = null; - if (e.submitter.getAttribute('id') === 'submit-join-as-player') { - document.getElementById('submit-join-as-player').classList.add('submitted'); - document.getElementById('submit-join-as-player').setAttribute('value', '...'); - } else { - document.getElementById('submit-join-as-spectator').classList.add('submitted'); - document.getElementById('submit-join-as-spectator').setAttribute('value', '...'); - } + document.getElementById('join-submit').classList.add('submitted'); + document.getElementById('join-submit').setAttribute('value', '...'); + return XHRUtility.xhr( '/api/games/' + accessCode + '/players', 'PATCH', @@ -58,7 +54,7 @@ function sendJoinRequest (e, name, accessCode) { accessCode: accessCode, sessionCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.LOCAL), localCookie: UserUtility.validateAnonUserSignature(globals.ENVIRONMENT.PRODUCTION), - joinAsSpectator: e.submitter.getAttribute('id') === 'submit-join-as-spectator' + joinAsSpectator: document.getElementById('join-as-spectator').checked }) ); } @@ -66,11 +62,8 @@ function sendJoinRequest (e, name, accessCode) { function handleJoinError (e, res, joinHandler) { document.getElementById('join-game-form').onsubmit = joinHandler; e.submitter.classList.remove('submitted'); - if (e.submitter.getAttribute('id') === 'submit-join-as-player') { - e.submitter.setAttribute('value', 'Join'); - } else { - e.submitter.setAttribute('value', 'Spectate'); - } + e.submitter.setAttribute('value', 'Join'); + if (res.status === 404) { toast('This game was not found.', 'error', true, true, 'long'); } else if (res.status === 400) { diff --git a/client/src/styles/GLOBAL.css b/client/src/styles/GLOBAL.css index 5e73dc0..ed3377b 100644 --- a/client/src/styles/GLOBAL.css +++ b/client/src/styles/GLOBAL.css @@ -34,7 +34,7 @@ body { .bmc-btn { height: 40px !important; - border-radius: 3px !important; + border-radius: 5px !important; font-size: 18px !important; min-width: 180px !important; padding: 0 17px !important; @@ -202,7 +202,7 @@ label { input, textarea { background-color: transparent; border: 1px solid white; - border-radius: 3px; + border-radius: 5px; color: #d7d7d7; } @@ -225,7 +225,7 @@ button { font-family: 'signika-negative', sans-serif !important; padding: 10px; background-color: #13762b; - border-radius: 3px; + border-radius: 5px; color: #e7e7e7; font-size: 18px; cursor: pointer; @@ -259,7 +259,7 @@ button { .container { padding: 5px; - border-radius: 3px; + border-radius: 5px; display: flex; flex-direction: column; justify-content: center; @@ -298,7 +298,7 @@ button { #how-to-use-container img { max-width: 98%; border: 1px solid #57566a; - border-radius: 3px; + border-radius: 5px; width: 45em; margin: 0 auto; /* justify-self: center; */ @@ -312,7 +312,7 @@ button { background-color: #1e1b26; width: fit-content; padding: 0 5px; - border-radius: 3px; + border-radius: 5px; } input { @@ -327,7 +327,7 @@ input { position: fixed; z-index: 1000000; padding: 10px; - border-radius: 3px; + border-radius: 5px; font-family: 'signika-negative', sans-serif; font-weight: 100; box-shadow: 0 1px 1px rgba(0,0,0,0.11), @@ -400,7 +400,7 @@ input { } .tutorial-image-small-portrait { - width: 20em !important; + width: 16em !important; } #desktop-links > a:nth-child(1), #mobile-links a:nth-child(1) { @@ -536,7 +536,7 @@ input { position: relative; overflow: hidden; height: 20px; - border-radius: 3px; + border-radius: 5px; opacity: 0.15; margin: 0.5em 0; } @@ -786,7 +786,7 @@ input { display:inline-flex !important; align-items: center !important; color:#ffffff !important; - background-color:#2d2c3a !important; + background-color:#3b3a4a !important; border-radius: 5px !important; border: 1px solid transparent !important; padding: 7px 15px 7px 10px !important; diff --git a/client/src/styles/confirmation.css b/client/src/styles/confirmation.css index cd6d66f..83e44b7 100644 --- a/client/src/styles/confirmation.css +++ b/client/src/styles/confirmation.css @@ -1,5 +1,5 @@ #confirmation { - border-radius: 2px; + border-radius: 5px; text-align: center; position: fixed; width: 85%; @@ -7,7 +7,7 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - background-color: #292834; + background-color: #2d2c38; align-items: center; justify-content: center; max-width: 25em; diff --git a/client/src/styles/create.css b/client/src/styles/create.css index 13c6755..1813ef2 100644 --- a/client/src/styles/create.css +++ b/client/src/styles/create.css @@ -7,7 +7,7 @@ background-color: #191920; color: gray; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.4); - border-radius: 3px; + border-radius: 5px; user-select: none; display: flex; height: 55px; @@ -106,9 +106,9 @@ display: flex; justify-content: space-between; background-color: #0f0f10; - border: 2px solid #2d2c3a; + border: 2px solid #3b3a4a; padding: 5px; - border-radius: 3px; + border-radius: 5px; font-size: 16px; flex-direction: column; align-items: flex-start; @@ -144,7 +144,7 @@ } .deck-role { - border-radius: 3px; + border-radius: 5px; margin: 0.25em 0; padding: 0 5px; font-size: 18px; @@ -158,8 +158,8 @@ margin: 1em 0.5em; background-color: #191920; padding: 10px; - border-radius: 3px; - border: 2px solid #2d2c3a; + border-radius: 5px; + border: 2px solid #3b3a4a; position: relative; } @@ -173,10 +173,10 @@ #deck-count { font-size: 30px; - background-color: #2d2c3a; + background-color: #3b3a4a; width: fit-content; padding: 0 5px; - border-radius: 3px; + border-radius: 5px; } #deck-status-header { @@ -236,8 +236,8 @@ z-index: 25; top: 38px; right: 29px; - background-color: #2d2c3a; - border-radius: 3px; + background-color: #3b3a4a; + border-radius: 5px; box-shadow: -3px -3px 6px rgb(0 0 0 / 60%); } @@ -245,7 +245,7 @@ display: flex; width: 100%; padding: 10px; - background-color: #2d2c3a; + background-color: #3b3a4a; text-align: center; justify-content: center; align-items: center; @@ -259,7 +259,7 @@ #deck-good, #deck-evil { padding: 0; - border-radius: 3px; + border-radius: 5px; margin: 0.5em; display: flex; flex-wrap: wrap; @@ -288,7 +288,7 @@ justify-content: center; align-items: center; width: fit-content; - border-radius: 3px; + border-radius: 5px; margin: 0 auto; } @@ -339,7 +339,7 @@ select { font-family: 'signika-negative', sans-serif; background-color: transparent; color: #d7d7d7; - border-radius: 3px; + border-radius: 5px; min-width: 10em; cursor: pointer; } @@ -349,7 +349,7 @@ select { display: flex; flex-direction: column; padding: 10px; - border-radius: 3px; + border-radius: 5px; width: fit-content; margin: 1em 0; } @@ -362,8 +362,8 @@ select { display: flex; flex-wrap: wrap; background-color: #191920; - border: 2px solid #2d2c3a; - border-radius: 3px; + border: 2px solid #3b3a4a; + border-radius: 5px; } .step { @@ -400,7 +400,7 @@ select { label[for="game-time"], label[for="add-card-to-deck-form"], label[for="deck"] { color: #e7e7e7; font-size: 20px; - border-radius: 3px; + border-radius: 5px; margin-bottom: 10px; font-weight: bold; } @@ -440,7 +440,7 @@ input[type="number"] { align-items: center; padding: 5px; margin: 0.25em 0; - border-radius: 3px; + border-radius: 5px; border: 1px solid transparent; font-size: 16px; } @@ -466,7 +466,7 @@ input[type="number"] { margin: 0 8px; cursor: pointer; padding: 5px; - border-radius: 3px; + border-radius: 5px; border: 1px solid transparent; } @@ -593,8 +593,8 @@ input[type="number"] { max-width: 20em; margin: 0.5em; cursor: pointer; - border: 2px solid #2d2c3a; - border-radius: 3px; + border: 2px solid #3b3a4a; + border-radius: 5px; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.6); } @@ -613,12 +613,12 @@ input[type="number"] { .review-option { background-color: #191920; - border: 2px solid #2d2c3a; + border: 2px solid #3b3a4a; color: #e7e7e7; padding: 10px; font-size: 18px; width: fit-content; - border-radius: 3px; + border-radius: 5px; margin: 0.5em 0; align-self: flex-start; } diff --git a/client/src/styles/game.css b/client/src/styles/game.css index d668378..1b80a87 100644 --- a/client/src/styles/game.css +++ b/client/src/styles/game.css @@ -8,7 +8,7 @@ padding: 5px; border-radius: 3px; font-size: 17px; - width: 17em; + width: 16em; border: 2px solid transparent; margin: 0 auto 0.25em auto; } @@ -22,7 +22,7 @@ max-height: 30em; overflow-x: hidden; padding: 0 25px 0 10px; - border-radius: 3px; + border-radius: 5px; } #lobby-people-container label { @@ -33,7 +33,7 @@ color: #b1afcd; text-decoration: underline; font-size: 17px; - margin: 5px 0; + margin: 5px 0 10px 0; cursor: pointer; } @@ -128,7 +128,7 @@ h1 { } #end-of-game-header h2 { - border: 1px solid #2d2c3a; + border: 1px solid #3b3a4a; border-radius: 5px; background-color: #1a1726; padding: 7px; @@ -161,9 +161,9 @@ h1 { cursor: pointer; margin-top: 10px; padding: 7px; - border-radius: 3px; + border-radius: 5px; background-color: #121314; - border: 2px solid #2d2c3a; + border: 2px solid #3b3a4a; color: #e7e7e7; align-items: center; display: flex; @@ -232,7 +232,7 @@ h1 { #game-role-info-container .role-info-name { padding: 5px; - border-radius: 3px; + border-radius: 5px; font-size: 20px; font-family: signika-negative, sans-serif; margin: 0.5em 0; @@ -265,7 +265,7 @@ h1 { cursor: pointer; justify-content: space-between; max-width: 17em; - border-radius: 3px; + border-radius: 5px; height: 23em; margin: 10px 20px; width: 100%; @@ -308,7 +308,7 @@ h1 { flex-direction: column; cursor: pointer; max-width: 17em; - border-radius: 3px; + border-radius: 5px; height: 23em; margin: 10px 20px; width: 100%; @@ -341,7 +341,7 @@ h1 { margin-top: 5px; background-color: #262626; color: #e7e7e7; - border-radius: 3px; + border-radius: 5px; font-size: 35px; text-shadow: 0 3px 4px rgb(0 0 0 / 85%); border: 1px solid #747474; @@ -416,8 +416,8 @@ h1 { flex-wrap: wrap; align-items: center; justify-content: space-between; - background-color: #2d2c3a; - border-radius: 3px; + background-color: #3b3a4a; + border-radius: 5px; min-width: 15em; } @@ -433,7 +433,7 @@ h1 { font-family: 'signika-negative', sans-serif; font-size: 25px; background-color: #171522; - border-radius: 3px; + border-radius: 5px; display: block; padding: 0 5px; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.4); @@ -460,7 +460,7 @@ label[for='moderator'] { left: 0; right: 0; bottom: 0; - border-radius: 3px; + border-radius: 5px; /* width: fit-content; */ font-size: 20px; height: 100px; @@ -483,7 +483,7 @@ label[for='moderator'] { left: 0; right: 0; bottom: 0; - border-radius: 3px; + border-radius: 5px; /* width: fit-content; */ font-size: 20px; height: 100px; @@ -516,7 +516,7 @@ label[for='moderator'] { #start-game-button, #end-game-button, #restart-game-button { font-family: 'signika-negative', sans-serif !important; padding: 10px; - border-radius: 3px; + border-radius: 5px; color: #e7e7e7; cursor: pointer; border: 2px solid transparent; @@ -600,12 +600,12 @@ label[for='moderator'] { justify-content: center; margin-bottom: 1em; padding: 0.5em; - border-radius: 3px; - background-color: #2d2c3a; + border-radius: 5px; + background-color: #3b3a4a; } canvas { - border-radius: 3px; + border-radius: 5px; margin: 1em; } @@ -641,7 +641,7 @@ canvas { .kill-player-button, .reveal-role-button { background-color: #434156; font-family: 'signika-negative', sans-serif !important; - border-radius: 3px; + border-radius: 5px; color: #e7e7e7; height: 25px; font-size: 16px; @@ -670,7 +670,7 @@ canvas { display: flex; justify-content: center; align-items: center; - border-radius: 3px; + border-radius: 5px; color: #767676; font-weight: bold; font-size: 16px; @@ -721,7 +721,7 @@ canvas { /* font-size: 18px;*/ /* padding: 10px;*/ /* border: 2px transparent;*/ -/* border-radius: 3px;*/ +/* border-radius: 5px;*/ /* color: #d7d7d7;*/ /* font-family: signika-negative, sans-serif;*/ /*}*/ @@ -744,8 +744,8 @@ canvas { } #game-parameters { - background-color: #2d2c3a; - border-radius: 3px; + background-color: #3b3a4a; + border-radius: 5px; padding: 5px 20px; } @@ -789,9 +789,9 @@ canvas { #lobby-people-container , #game-people-container { padding: 10px; - border-radius: 3px; + border-radius: 5px; min-height: 25em; - background-color: #292834; + background-color: #3b3a4a; max-width: 35em; min-width: 17em; margin-top: 1em; diff --git a/client/src/styles/home.css b/client/src/styles/home.css index 179b7c0..35ea41d 100644 --- a/client/src/styles/home.css +++ b/client/src/styles/home.css @@ -68,7 +68,7 @@ form { display: flex; flex-wrap: wrap; margin: 10px 0; - border-radius: 3px; + border-radius: 5px; justify-content: center; align-items: center; } @@ -127,7 +127,7 @@ form > div { max-width: 90%; background-color: #0f0f10; padding: 0.5em; - border-radius: 3px; + border-radius: 5px; margin: 20px; } diff --git a/client/src/styles/join.css b/client/src/styles/join.css index 399a459..4274d5a 100644 --- a/client/src/styles/join.css +++ b/client/src/styles/join.css @@ -4,6 +4,7 @@ transform-origin: center; display: block; z-index: 1 !important; + background-color: #2d2c38; } #join-game-form .modal-button-container { @@ -11,18 +12,27 @@ margin-top: 2em; } -#join-game-form .modal-button-container input { +#join-game-form .modal-button-container #join-submit { width: 5em; margin: 0 10px; } -#join-game-form .modal-button-container #submit-join-as-spectator { - background-color: #045EA6; +.modal-button-container > div { + display: flex; + align-items: center; + justify-content: center; + padding: 5px; + border-radius: 5px; + background-color: #1e1b26; } -#join-game-form .modal-button-container #submit-join-as-spectator:hover { - background-color: #0078D773; - border: 2px solid #045EA6; +.modal-button-container > div > label { + margin: 0 10px; + cursor: pointer; +} + +.modal-button-container > div > input { + cursor: pointer; } #player-new-name { @@ -39,12 +49,10 @@ @keyframes entrance { 0% { - transform: translate(-50%, calc(-50% + 40px)); opacity: 0; } - 95% { + 90% { opacity: 1; - transform: translate(-50%, -50%); } 100% { opacity: 1; diff --git a/client/src/styles/modal.css b/client/src/styles/modal.css index 5d105a5..7cf65f6 100644 --- a/client/src/styles/modal.css +++ b/client/src/styles/modal.css @@ -1,5 +1,5 @@ .modal { - border-radius: 2px; + border-radius: 5px; text-align: center; position: fixed; width: 85%; @@ -7,7 +7,7 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - background-color: #292834; + background-color: #2d2c38; align-items: center; justify-content: center; max-width: 25em; @@ -63,7 +63,7 @@ } #custom-role-info-modal-description { - border-radius: 3px; + border-radius: 5px; background-color: black; max-height: 10em; overflow: auto; @@ -81,7 +81,7 @@ } #custom-role-info-modal-alignment { - border-radius: 3px; + border-radius: 5px; background-color: black; font-size: 18px; font-weight: bold; diff --git a/client/src/views/404.html b/client/src/views/404.html index f7ba5e4..3b39f04 100644 --- a/client/src/views/404.html +++ b/client/src/views/404.html @@ -7,7 +7,7 @@ - + diff --git a/client/src/views/create.html b/client/src/views/create.html index c5d701a..08b6a84 100644 --- a/client/src/views/create.html +++ b/client/src/views/create.html @@ -5,9 +5,9 @@ Create A Game - + - + diff --git a/client/src/views/game.html b/client/src/views/game.html index 6f48ab3..1dd3246 100644 --- a/client/src/views/game.html +++ b/client/src/views/game.html @@ -5,7 +5,7 @@ Active Game - + diff --git a/client/src/views/how-to-use.html b/client/src/views/how-to-use.html index beb9faa..8f28bb4 100644 --- a/client/src/views/how-to-use.html +++ b/client/src/views/how-to-use.html @@ -3,11 +3,11 @@ - Create A Game + How to Use - + @@ -55,22 +55,20 @@

Step One: Choosing method of moderation


You have two options for moderation during the game. If the moderator isn't playing, you can choose the dedicated - moderator option. Dedicated Moderators are not dealt into the game. Once they start the game, they will know + moderator option. Dedicated Moderators are not dealt into the game. Once they start the game, they will know everyone's role. At that point, they can kill players, reveal players' roles to everyone else, transfer their moderator powers, play/pause the timer (if there is one), and end the game when team good or evil wins.

- Similarly, you can also choose the temporary moderator option. The key differences - here are that you are dealt a role. You have the same powers as the dedicated - moderator, with the exception of game knowledge - you know the same information that a regular player does. - When you remove the first player from the game, your powers will be automatically transferred to them - they become - the dedicated moderator, and you become a regular player. + Similarly, you can also choose the temporary moderator option. You are dealt a role, + and you have the same powers as the dedicated moderator, with the exception of game knowledge - you know the same + information that a regular player does. When you remove the first player from the game (which can be yourself), + they will automatically become the dedicated moderator, and the temporary moderator--if they are a different person--will become + a killed player.

Dedicated moderators can transfer their moderator powers to a player that is out, or to a spectator. That way, if the current dedicated moderator has to leave, or simply does not want to moderate anymore, they can easily delegate.

- moderation-option -

Step Two: Build your deck


There is a role box on this page that includes a list of default roles and a list of custom roles, which can be @@ -92,35 +90,40 @@
If you don't fill in these fields, the game will be untimed. If you do, you can use a time between 1 minute and 5 hours. The timer can be played and paused by the current moderator. Importantly, when the timer expires, - nothing automatically happens. The timer will display 0s, but the game will not - end. Whether or not the game ends immediately after that or continues longer is up to the moderator. -

+ nothing automatically happens. Users will be notified that it has expired, and + the timer will display 0s, but the game will not end. Moderators always choose to end or restart the game manually.

Being the Moderator

This is an example of what a dedicated moderator sees during the game:

- +

They can see who is on which team and who has each role. The moderator kills and reveals players. They are separate actions - note the two buttons for each player. So if you want to play a game where people's roles are not revealed during the day or night, you can use the "kill" option but not the reveal option. Or, if you happen to have a role that reveals but is not immediately removed from the game, you can use the "reveal" option but not the - "kill" option. + "kill" option. You of course don't have to utilize either of these options. If you just want to use the app to deal + cards, you are free to do that.

- The temporary moderator view looks like this. They have + The moderator also has permissions to play and pause the timer, and can end the game (revealing everyone's role) + or restart the game, which will reset the timer, give everyone new roles, and reset the status of anyone that was + killed or revealed. +

+ The temporary moderator view looks like the below image. They have much the same abilities as a dedicated moderator, except they don't know role or alignment information and cannot - transfer their powers. Their powers will be transferred automatically to the first person they remove from the game: + transfer their powers. Their powers will be transferred automatically to the first person they remove from the game + (which can be themselves).

- +

Transferring your moderator powers


You can transfer your moderator abilities to anyone that has been removed from the game, or to anyone that happens - to be spectating. After selecting them from the list, they will then inherit the moderator's view, and you will - become a spectator: + to be spectating. Here we select a spectator and transfer our powers to them, and then we inherit the view + of what we were before, which is a player that was removed from the game:

- +

Being a Player

diff --git a/client/src/views/join.html b/client/src/views/join.html index 242a5fa..7557730 100644 --- a/client/src/views/join.html +++ b/client/src/views/join.html @@ -5,7 +5,7 @@ Active Game - + @@ -43,8 +43,11 @@
diff --git a/server/config/globals.js b/server/config/globals.js index 1bdb0d3..6126c80 100644 --- a/server/config/globals.js +++ b/server/config/globals.js @@ -4,7 +4,7 @@ const globals = { ACCESS_CODE_GENERATION_ATTEMPTS: 50, CLOCK_TICK_INTERVAL_MILLIS: 100, MAX_CUSTOM_ROLE_NAME_LENGTH: 50, - MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 500, + MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 1000, ALIGNMENT: { GOOD: 'good', EVIL: 'evil' diff --git a/server/model/GameCreationRequest.js b/server/model/GameCreationRequest.js new file mode 100644 index 0000000..4c8b3a2 --- /dev/null +++ b/server/model/GameCreationRequest.js @@ -0,0 +1,85 @@ +const globals = require("../config/globals"); + +class GameCreationRequest { + constructor ( + deck, + hasTimer, + timerParams, + moderatorName, + hasDedicatedModerator + ) { + this.deck = deck; + this.hasTimer = hasTimer; + this.timerParams = timerParams; + this.moderatorName = moderatorName; + this.hasDedicatedModerator = hasDedicatedModerator; + } + + static validate = (gameParams) => { + const expectedKeys = ['deck', 'hasTimer', 'timerParams', 'moderatorName', 'hasDedicatedModerator']; + if (gameParams === null + || typeof gameParams !== 'object' + || expectedKeys.some((key) => !Object.keys(gameParams).includes(key)) + || !valid(gameParams) + ) { + return Promise.reject(globals.ERROR_MESSAGE.BAD_CREATE_REQUEST); + } else { + return Promise.resolve(); + } + } +} + +function valid (gameParams) { + return typeof gameParams.hasTimer === 'boolean' + && typeof gameParams.hasDedicatedModerator === 'boolean' + && typeof gameParams.moderatorName === 'string' + && gameParams.moderatorName.length > 0 + && gameParams.moderatorName.length <= 30 + && timerParamsAreValid(gameParams.hasTimer, gameParams.timerParams) + && deckIsValid(gameParams.deck); +} + +function timerParamsAreValid (hasTimer, timerParams) { + if (hasTimer === false) { + return timerParams === null; + } else { + if (timerParams === null || typeof timerParams !== 'object') { + return false; + } + + return (timerParams.hours === null && timerParams.minutes > 0 && timerParams.minutes < 60) + || (timerParams.minutes === null && timerParams.hours > 0 && timerParams.hours < 6) + || (timerParams.hours === 0 && timerParams.minutes > 0 && timerParams.minutes < 60) + || (timerParams.minutes === 0 && timerParams.hours > 0 && timerParams.hours < 6) + || (timerParams.hours > 0 && timerParams.hours < 6 && timerParams.minutes >= 0 && timerParams.minutes < 60); + } +} + +function deckIsValid (deck) { + if (Array.isArray(deck) && deck.length > 0) { + for (const entry of deck) { + if (entry !== null + && typeof entry === 'object' + && typeof entry.role === 'string' + && entry.role.length > 0 + && entry.role.length <= globals.MAX_CUSTOM_ROLE_NAME_LENGTH + && typeof entry.team === 'string' + && (entry.team === globals.ALIGNMENT.GOOD || entry.team === globals.ALIGNMENT.EVIL) + && typeof entry.description === 'string' + && entry.description.length > 0 + && entry.description.length <= globals.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH + && (!entry.custom || typeof entry.custom === 'boolean') + && typeof entry.quantity === 'number' + && entry.quantity >= 1 + && entry.quantity <= 50 + ) { + continue; + } + return false; + } + return true; + } + return false; +} + +module.exports = GameCreationRequest; diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js index 608d749..75a063b 100644 --- a/server/modules/GameManager.js +++ b/server/modules/GameManager.js @@ -3,6 +3,7 @@ const Game = require('../model/Game'); const Person = require('../model/Person'); const GameStateCurator = require('./GameStateCurator'); const UsernameGenerator = require('./UsernameGenerator'); +const GameCreationRequest = require("../model/GameCreationRequest"); class GameManager { constructor (logger, environment, activeGameRunner) { @@ -22,41 +23,43 @@ class GameManager { }; createGame = (gameParams) => { - const expectedKeys = ['deck', 'hasTimer', 'timerParams', 'moderatorName', 'hasDedicatedModerator']; - if (typeof gameParams !== 'object' - || expectedKeys.some((key) => !Object.keys(gameParams).includes(key)) - || !valid(gameParams) - ) { - this.logger.error('Tried to create game with invalid options.'); - return Promise.reject(globals.ERROR_MESSAGE.BAD_CREATE_REQUEST); - } else { + return GameCreationRequest.validate(gameParams).then(() => { + let req = new GameCreationRequest( + gameParams.deck, + gameParams.hasTimer, + gameParams.timerParams, + gameParams.moderatorName, + gameParams.hasDedicatedModerator + ); this.pruneStaleGames(); const newAccessCode = this.generateAccessCode(globals.ACCESS_CODE_CHAR_POOL); if (newAccessCode === null) { return Promise.reject(globals.ERROR_MESSAGE.NO_UNIQUE_ACCESS_CODE); } - const moderator = initializeModerator(gameParams.moderatorName, gameParams.hasDedicatedModerator); + const moderator = initializeModerator(req.moderatorName, req.hasDedicatedModerator); moderator.assigned = true; - if (gameParams.timerParams !== null) { - gameParams.timerParams.paused = false; + if (req.timerParams !== null) { + req.timerParams.paused = false; } const newGame = new Game( newAccessCode, globals.STATUS.LOBBY, - initializePeopleForGame(gameParams.deck, moderator, this.shuffle), - gameParams.deck, - gameParams.hasTimer, + initializePeopleForGame(req.deck, moderator, this.shuffle), + req.deck, + req.hasTimer, moderator, - gameParams.hasDedicatedModerator, + req.hasDedicatedModerator, moderator.id, new Date().toJSON(), - gameParams.timerParams + req.timerParams ); this.activeGameRunner.activeGames.set(newAccessCode, newGame); return Promise.resolve({ accessCode: newAccessCode, cookie: moderator.cookie, environment: this.environment }); - } + }).catch((message) => { + return Promise.reject(message); + }); }; startGame = (game, namespace) => { @@ -494,54 +497,6 @@ function getGameSize (cards) { return quantity; } -function valid (gameParams) { - return typeof gameParams.hasTimer === 'boolean' - && typeof gameParams.hasDedicatedModerator === 'boolean' - && typeof gameParams.moderatorName === 'string' - && gameParams.moderatorName.length > 0 - && gameParams.moderatorName.length <= 30 - && timerParamsAreValid(gameParams.hasTimer, gameParams.timerParams) - && deckIsValid(gameParams.deck); -} - -function timerParamsAreValid (hasTimer, timerParams) { - if (hasTimer === false) { - return timerParams === null; - } else { - if (timerParams === null || typeof timerParams !== 'object') { - return false; - } - - return (timerParams.hours === null && timerParams.minutes > 0 && timerParams.minutes < 60) - || (timerParams.minutes === null && timerParams.hours > 0 && timerParams.hours < 6) - || (timerParams.hours === 0 && timerParams.minutes > 0 && timerParams.minutes < 60) - || (timerParams.minutes === 0 && timerParams.hours > 0 && timerParams.hours < 6) - || (timerParams.hours > 0 && timerParams.hours < 6 && timerParams.minutes >= 0 && timerParams.minutes < 60); - } -} - -function deckIsValid (deck) { - if (Array.isArray(deck) && deck.length > 0) { - for (const entry of deck) { - if (entry !== null && typeof entry === 'object') { - if (typeof entry.role !== 'string' || entry.role.length > globals.MAX_CUSTOM_ROLE_NAME_LENGTH - || typeof entry.team !== 'string' || (entry.team !== globals.ALIGNMENT.GOOD && entry.team !== globals.ALIGNMENT.EVIL) - || typeof entry.description !== 'string' || entry.description.length > globals.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH - || (entry.custom && typeof entry.custom !== 'boolean') - || typeof entry.quantity !== 'number' || entry.quantity < 1 || entry.quantity > 50 - ) { - return false; - } - } else { - return false; - } - } - return true; - } - - return false; -} - function addSpectator (game, name, logger, namespace) { const spectator = new Person( createRandomId(),