change spectator option to checkbox, update tutorial images

This commit is contained in:
AlecM33
2022-12-30 13:42:29 -05:00
parent 0a0fca6ed7
commit fc285bc124
26 changed files with 236 additions and 186 deletions

View File

@@ -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',

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 927 KiB

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -7,7 +7,7 @@
<meta name="description" content="This resource was not found.">
<meta property="og:title" content="Not Found">
<meta property="og:type" content="website">
<meta property="og:url" content="https://playwerewolf.uk.r.appspot.com/not-found">
<meta property="og:url" content="https://play-werewolf.app/not-found">
<meta property="og:description" content="The page you are looking for could not be found.">
<meta property="og:image" content="image.png">
<link rel="icon" href="/favicon.ico">

View File

@@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Create A Game</title>
<meta name="description" content="Create a game of Werewolf using your custom set of roles.">
<meta property="og:title" content="Werewolf Utility - Create A Game">
<meta property="og:title" content="Create A Game">
<meta property="og:type" content="website">
<meta property="og:url" content="https://playwerewolf.uk.r.appspot.com/create">
<meta property="og:url" content="https://play-werewolf.app/create">
<meta property="og:description" content="Create a game of Werewolf using your custom set of roles.">
<meta property="og:image" content="image.png">
<link rel="icon" href="/favicon.ico">

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Active Game</title>
<meta name="description" content="View this game of Werewolf">
<meta property="og:title" content="Play Werwolf (Mafia) - Active Game">
<meta property="og:title" content="Active Game">
<meta property="og:type" content="website">
<meta property="og:description" content="Spectate this game of Werewolf">
<meta property="og:image" content="image.png">

View File

@@ -3,11 +3,11 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Create A Game</title>
<title>How to Use</title>
<meta name="description" content="How to use this app.">
<meta property="og:title" content="How to use">
<meta property="og:type" content="website">
<meta property="og:url" content="https://playwerewolf.uk.r.appspot.com/how-to-use">
<meta property="og:url" content="https://play-werewolf.app/how-to-use">
<meta property="og:description" content="How to use this app.">
<meta property="og:image" content="image.png">
<link rel="icon" href="/favicon.ico">
@@ -55,22 +55,20 @@
<h3>Step One: Choosing method of moderation</h3>
<br>
You have two options for moderation during the game. If the moderator isn't playing, you can choose the <span class="emphasized">dedicated
moderator</span> option. Dedicated Moderators are <span class="emphasized">not dealt into the game</span>. Once they start the game, they will know
moderator</span> 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.
<br><br>
Similarly, you can also choose the <span class="emphasized">temporary moderator</span> option. The key differences
here are that you are <span class="emphasized">dealt a role</span>. 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 <span class="emphasized">temporary moderator</span> 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.
<br><br>
<span class="emphasized">Dedicated moderators</span> can <span class="emphasized">transfer their moderator powers</span>
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.
<br><br>
<img src="../images/tutorial/moderation-option.png" class="tutorial-image-small" alt="moderation-option"/>
<br><br>
<h3>Step Two: Build your deck</h3>
<br>
There is a role box on this page that includes a list of <span class="emphasized">default roles</span> and a list of <span class="emphasized">custom roles</span>, which can be
@@ -92,35 +90,40 @@
<br>
If you don't fill in these fields, the game will be untimed. If you do, <span class="emphasized">you can use a time between 1 minute
and 5 hours.</span> The timer can be played and paused by the current moderator. Importantly, when the timer expires,
<span class="emphasized">nothing automatically happens.</span> 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.
<br><br>
<span class="emphasized">nothing automatically happens.</span> 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.<br><br>
</div>
<h1 class="how-to-use-header" id="being-the-moderator">Being the Moderator</h1>
<div class="how-to-use-section">
This is an example of what a <span class="emphasized">dedicated moderator</span> sees during the game:
<br><br>
<img class='tutorial-image-small' src="../images/tutorial/dedicated-mod.PNG"/>
<img class='tutorial-image-small-portrait' src="../images/screenshots/dedicated_mod_inprogress_mobile.png"/>
<br><br>
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.
<br><br>
The <span class="emphasized">temporary moderator view</span> 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.
<br><br>
The <span class="emphasized">temporary moderator view</span> 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).
<br><br>
<img class='tutorial-image-small' src="../images/tutorial/temporary-mod.PNG"/>
<img class='tutorial-image-small-portrait' src="../images/screenshots/temp_mod_inprogress_mobile.png"/>
<br><br>
<h3>Transferring your moderator powers</h3>
<br>
<span class="emphasized">You can transfer your moderator abilities to anyone that has been removed from the game, or to anyone that happens
to be spectating.</span> After selecting them from the list, they will then inherit the moderator's view, and you will
become a spectator:
to be spectating.</span> 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:
<br><br>
<img class='tutorial-image-small' src="../images/tutorial/transfer-mod.gif"/>
<img class='tutorial-image-small-portrait' src="../images/tutorial/mod-transfer.gif"/>
</div>
<h1 class="how-to-use-header" id="being-a-player">Being a Player</h1>
<div class="how-to-use-section">

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Active Game</title>
<meta name="description" content="Join this game of werewolf!">
<meta property="og:title" content="Werewolf Utility - Join Game">
<meta property="og:title" content="Join a Game">
<meta property="og:type" content="website">
<meta property="og:description" content="Join this game of werewolf!">
<meta property="og:image" content="image.png">
@@ -43,8 +43,11 @@
<input id="player-new-name" autocomplete="given-name" type="text"/>
</div>
<div class="modal-button-container">
<input type="submit" id="submit-join-as-spectator" value="Spectate"/>
<input type="submit" id="submit-join-as-player" value="Join"/>
<div>
<input type="checkbox" id="join-as-spectator" name="join as spectator">
<label for="join-as-spectator">Join as spectator</label>
</div>
<input type="submit" id="join-submit" value="Join"/>
</div>
</form>
</div>

View File

@@ -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'

View File

@@ -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;

View File

@@ -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(),