mirror of
https://github.com/AlecM33/Werewolf.git
synced 2025-12-26 15:57:50 +01:00
@@ -3,6 +3,8 @@ export const PRIMITIVES = {
|
||||
USER_SIGNATURE_LENGTH: 75,
|
||||
CLOCK_TICK_INTERVAL_MILLIS: 50,
|
||||
MAX_CUSTOM_ROLE_NAME_LENGTH: 50,
|
||||
MAX_PERSON_NAME_LENGTH: 40,
|
||||
MAX_DECK_SIZE: 50,
|
||||
MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH: 1000,
|
||||
TOAST_DURATION_DEFAULT: 6,
|
||||
ACCESS_CODE_LENGTH: 4,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 417 KiB |
@@ -1,6 +1,6 @@
|
||||
import { toast } from './Toast.js';
|
||||
|
||||
export const Confirmation = (message, onYes = null, isDOMNode = false) => {
|
||||
export const Confirmation = (message, onYes = null, isDOMNode = false, confirmButtonText = 'Yes') => {
|
||||
document.querySelector('#confirmation')?.remove();
|
||||
document.querySelector('#confirmation-background')?.remove();
|
||||
|
||||
@@ -11,7 +11,7 @@ export const Confirmation = (message, onYes = null, isDOMNode = false) => {
|
||||
? `<div id="confirmation-message"></div>
|
||||
<div class="confirmation-buttons">
|
||||
<button id="confirmation-cancel-button" class="app-button cancel">Cancel</button>
|
||||
<button id="confirmation-yes-button" class="app-button">Yes</button>
|
||||
<button id="confirmation-yes-button" class="app-button">` + confirmButtonText + `</button>
|
||||
</div>`
|
||||
: `<div id="confirmation-message"></div>
|
||||
<div class="confirmation-buttons-centered">
|
||||
|
||||
@@ -94,7 +94,7 @@ export const HTMLFragments = {
|
||||
</div>
|
||||
<div id='game-people-container'>
|
||||
<div id="current-moderator" class="moderator">
|
||||
<div id="current-moderator-name"></div>
|
||||
<div id="current-moderator-name" class="person-name-element"></div>
|
||||
<div id="current-moderator-type"></div>
|
||||
</div>
|
||||
<label id='players-alive-label'></label>
|
||||
@@ -128,7 +128,7 @@ export const HTMLFragments = {
|
||||
</div>
|
||||
<div id='game-people-container'>
|
||||
<div id="current-moderator" class="moderator">
|
||||
<div id="current-moderator-name"></div>
|
||||
<div id="current-moderator-name" class="person-name-element"></div>
|
||||
<div id="current-moderator-type"></div>
|
||||
</div>
|
||||
<label id='players-alive-label'></label>
|
||||
@@ -245,7 +245,7 @@ export const HTMLFragments = {
|
||||
</div>`,
|
||||
MODERATOR_PLAYER:
|
||||
`<div>
|
||||
<div class='game-player-name'></div>
|
||||
<div class='game-player-name person-name-element'></div>
|
||||
<div class='game-player-role'></div>
|
||||
</div>
|
||||
<div class='player-action-buttons'>
|
||||
@@ -254,7 +254,7 @@ export const HTMLFragments = {
|
||||
</div>`,
|
||||
GAME_PLAYER:
|
||||
`<div>
|
||||
<div class='game-player-name'></div>
|
||||
<div class='game-player-name person-name-element'></div>
|
||||
<div class='game-player-role'></div>
|
||||
</div>`,
|
||||
INITIAL_GAME_DOM:
|
||||
@@ -265,6 +265,9 @@ export const HTMLFragments = {
|
||||
<div id='client-name'></div>
|
||||
<div id='client-user-type'></div>
|
||||
</div>
|
||||
<button id="edit-name-button">
|
||||
<img alt="edit name" src="../../images/pencil.svg"/>
|
||||
</button>
|
||||
</div>
|
||||
<div id='game-state-container'></div>`,
|
||||
// via https://loading.io/css/
|
||||
@@ -283,18 +286,10 @@ export const HTMLFragments = {
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>`,
|
||||
NAME_CHANGE_MODAL:
|
||||
`<div id='change-name-modal-background' class='modal-background'></div>
|
||||
<div tabindex='-1' 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' autocomplete='given-name' type='text'/>
|
||||
</div>
|
||||
<div class='modal-button-container'>
|
||||
<input type='submit' id='submit-new-name' value='Set Name'/>
|
||||
</div>
|
||||
</form>
|
||||
NAME_CHANGE_FORM:
|
||||
`<div id='change-name-form-content'>
|
||||
<label for='client-new-name'>Your name:</label>
|
||||
<input maxlength="40" id='client-new-name' autocomplete='off' type='text'/>
|
||||
</div>`,
|
||||
ROLE_INFO_MODAL:
|
||||
`<div id='role-info-modal-background' class='modal-background'></div>
|
||||
@@ -314,7 +309,7 @@ export const HTMLFragments = {
|
||||
</div>
|
||||
<div id='game-people-container'>
|
||||
<div id="current-moderator" class="moderator">
|
||||
<div id="current-moderator-name"></div>
|
||||
<div id="current-moderator-name" class="person-name-element"></div>
|
||||
<div id="current-moderator-type"></div>
|
||||
</div>
|
||||
<label id='players-alive-label'></label>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Game } from '../../model/Game.js';
|
||||
import { cancelCurrentToast, toast } from '../front_end_components/Toast.js';
|
||||
import { ModalManager } from '../front_end_components/ModalManager.js';
|
||||
import { ALIGNMENT } from '../../config/globals.js';
|
||||
import { ALIGNMENT, PRIMITIVES } from '../../config/globals.js';
|
||||
import { HTMLFragments } from '../front_end_components/HTMLFragments.js';
|
||||
import { UserUtility } from '../utility/UserUtility.js';
|
||||
import { RoleBox } from './RoleBox.js';
|
||||
@@ -35,8 +35,8 @@ export class GameCreationStepManager {
|
||||
2: {
|
||||
title: 'Create your deck (you can edit this later):',
|
||||
forwardHandler: () => {
|
||||
if (this.deckManager.getDeckSize() > 50) {
|
||||
toast('Your deck is too large. The max is 50 cards.', 'error', true);
|
||||
if (this.deckManager.getDeckSize() > PRIMITIVES.MAX_DECK_SIZE) {
|
||||
toast('Your deck is too large. The max is ' + PRIMITIVES.MAX_DECK_SIZE + ' cards.', 'error', true);
|
||||
} else {
|
||||
this.currentGame.deck = this.deckManager.deck.filter((card) => card.quantity > 0);
|
||||
cancelCurrentToast();
|
||||
@@ -98,7 +98,7 @@ export class GameCreationStepManager {
|
||||
this.incrementStep();
|
||||
this.renderStep('creation-step-container', this.step);
|
||||
} else {
|
||||
toast('Name must be between 1 and 30 characters.', 'error', true);
|
||||
toast('Name must be between 1 and ' + PRIMITIVES.MAX_PERSON_NAME_LENGTH + ' characters.', 'error', true);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -569,12 +569,12 @@ function initializeRemainingEventListeners (deckManager, roleBox) {
|
||||
}
|
||||
|
||||
function processNewCustomRoleSubmission (name, description, team, deckManager, isUpdate, roleBox, option = null) {
|
||||
if (name.length > 40) {
|
||||
toast('Your name is too long (max 40 characters).', 'error', true);
|
||||
if (name.length > PRIMITIVES.MAX_CUSTOM_ROLE_NAME_LENGTH) {
|
||||
toast('Your name is too long (max ' + PRIMITIVES.MAX_CUSTOM_ROLE_NAME_LENGTH + ' characters).', 'error', true);
|
||||
return;
|
||||
}
|
||||
if (description.length > 500) {
|
||||
toast('Your description is too long (max 500 characters).', 'error', true);
|
||||
if (description.length > PRIMITIVES.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH) {
|
||||
toast('Your description is too long (max ' + PRIMITIVES.MAX_CUSTOM_ROLE_DESCRIPTION_LENGTH + ' characters).', 'error', true);
|
||||
return;
|
||||
}
|
||||
if (isUpdate) {
|
||||
@@ -596,5 +596,5 @@ function hasTimer (hours, minutes) {
|
||||
}
|
||||
|
||||
function validateName (name) {
|
||||
return typeof name === 'string' && name.length > 0 && name.length <= 30;
|
||||
return typeof name === 'string' && name.length > 0 && name.length <= PRIMITIVES.MAX_PERSON_NAME_LENGTH;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ function renderGroupOfPlayers (
|
||||
for (const player of people) {
|
||||
const playerEl = document.createElement('div');
|
||||
playerEl.classList.add('game-player');
|
||||
playerEl.dataset.pointer = player.id;
|
||||
playerEl.innerHTML = HTMLFragments.GAME_PLAYER;
|
||||
|
||||
playerEl.querySelector('.game-player-name').innerText = player.name;
|
||||
|
||||
@@ -322,6 +322,7 @@ export class InProgress {
|
||||
for (const player of people) {
|
||||
const playerEl = document.createElement('div');
|
||||
playerEl.classList.add('game-player');
|
||||
playerEl.dataset.pointer = player.id;
|
||||
|
||||
// add a reference to the player's id for each corresponding element in the list
|
||||
if (moderatorType) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { QRCode } from '../../third_party/qrcode.js';
|
||||
import { toast } from '../../front_end_components/Toast.js';
|
||||
import { EVENT_IDS, SOCKET_EVENTS, USER_TYPE_ICONS, USER_TYPES } from '../../../config/globals.js';
|
||||
import { EVENT_IDS, PRIMITIVES, SOCKET_EVENTS, USER_TYPE_ICONS, USER_TYPES } from '../../../config/globals.js';
|
||||
import { HTMLFragments } from '../../front_end_components/HTMLFragments.js';
|
||||
import { Confirmation } from '../../front_end_components/Confirmation.js';
|
||||
import { SharedStateUtil } from './shared/SharedStateUtil.js';
|
||||
@@ -79,7 +79,7 @@ export class Lobby {
|
||||
roleEditPrompt.setAttribute('id', 'role-edit-prompt');
|
||||
roleEditPrompt.innerHTML = HTMLFragments.ROLE_EDIT_BUTTONS;
|
||||
roleEditPrompt.querySelector('#save-role-changes-button').addEventListener('click', () => {
|
||||
if (this.gameCreationStepManager.deckManager.getDeckSize() > 50) {
|
||||
if (this.gameCreationStepManager.deckManager.getDeckSize() > PRIMITIVES.MAX_DECK_SIZE) {
|
||||
toast('Your deck is too large. The max is 50 cards.', 'error', true);
|
||||
} else {
|
||||
document.querySelector('#mid-game-role-editor')?.remove();
|
||||
@@ -354,8 +354,9 @@ function getTimeString (gameState) {
|
||||
|
||||
function renderLobbyPerson (person, gameState, socket) {
|
||||
const el = document.createElement('div');
|
||||
el.dataset.pointer = person.id;
|
||||
const personNameEl = document.createElement('div');
|
||||
personNameEl.classList.add('lobby-player-name');
|
||||
personNameEl.classList.add('lobby-player-name', 'person-name-element');
|
||||
const personTypeEl = document.createElement('div');
|
||||
personNameEl.innerText = person.name;
|
||||
personTypeEl.innerText = person.userType + USER_TYPE_ICONS[person.userType];
|
||||
|
||||
@@ -100,8 +100,9 @@ export const SharedStateUtil = {
|
||||
} else {
|
||||
for (const spectator of spectators) {
|
||||
const spectatorEl = document.createElement('div');
|
||||
spectatorEl.dataset.pointer = spectator.id;
|
||||
spectatorEl.classList.add('spectator');
|
||||
spectatorEl.innerHTML = '<div class=\'spectator-name\'></div>' +
|
||||
spectatorEl.innerHTML = '<div class=\'spectator-name person-name-element\'></div>' +
|
||||
'<div>' + 'spectator' + USER_TYPE_ICONS.spectator + '</div>';
|
||||
spectatorEl.querySelector('.spectator-name').innerText = spectator.name;
|
||||
list.appendChild(spectatorEl);
|
||||
@@ -123,6 +124,7 @@ export const SharedStateUtil = {
|
||||
},
|
||||
|
||||
displayCurrentModerator: (moderator) => {
|
||||
document.getElementById('current-moderator').dataset.pointer = moderator.id;
|
||||
document.getElementById('current-moderator-name').innerText = moderator.name;
|
||||
document.getElementById('current-moderator-type').innerText = moderator.userType + USER_TYPE_ICONS[moderator.userType];
|
||||
},
|
||||
@@ -183,9 +185,30 @@ export const SharedStateUtil = {
|
||||
});
|
||||
},
|
||||
|
||||
displayClientInfo: (name, userType) => {
|
||||
document.getElementById('client-name').innerText = name;
|
||||
document.getElementById('client-user-type').innerText = userType;
|
||||
document.getElementById('client-user-type').innerText += USER_TYPE_ICONS[userType];
|
||||
displayClientInfo: (gameState, socket) => {
|
||||
document.getElementById('client-name').innerText = gameState.client.name;
|
||||
document.getElementById('client-user-type').innerText = gameState.client.userType;
|
||||
document.getElementById('client-user-type').innerText += USER_TYPE_ICONS[gameState.client.userType];
|
||||
const nameForm = document.createElement('form');
|
||||
nameForm.setAttribute('id', 'name-change-form');
|
||||
nameForm.onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById('confirmation-yes-button').click();
|
||||
};
|
||||
nameForm.innerHTML = HTMLFragments.NAME_CHANGE_FORM;
|
||||
nameForm.querySelector('#client-new-name').value = gameState.client.name;
|
||||
document.getElementById('edit-name-button').addEventListener('click', () => {
|
||||
Confirmation(nameForm, () => {
|
||||
socket.emit(
|
||||
SOCKET_EVENTS.IN_GAME_MESSAGE,
|
||||
EVENT_IDS.CHANGE_NAME,
|
||||
gameState.accessCode,
|
||||
{ personId: gameState.client.id, newName: document.getElementById('client-new-name').value },
|
||||
(response) => {
|
||||
toast(response.message, response.errorFlag === 1 ? 'error' : 'success', true);
|
||||
}
|
||||
);
|
||||
}, true, 'Update');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,7 +135,7 @@ function processGameState (
|
||||
});
|
||||
}
|
||||
|
||||
SharedStateUtil.displayClientInfo(currentGameState.client.name, currentGameState.client.userType);
|
||||
SharedStateUtil.displayClientInfo(currentGameState, socket);
|
||||
|
||||
switch (currentGameState.status) {
|
||||
case STATUS.LOBBY:
|
||||
@@ -236,6 +236,20 @@ function setClientSocketHandlers (stateBucket, socket) {
|
||||
);
|
||||
});
|
||||
|
||||
socket.on(EVENT_IDS.CHANGE_NAME, (changedId, newName) => {
|
||||
const person = stateBucket.currentGameState.people.find(person => person.id === changedId);
|
||||
if (person) {
|
||||
person.name = newName;
|
||||
if (stateBucket.currentGameState.client.id === changedId) {
|
||||
stateBucket.currentGameState.client.name = newName;
|
||||
SharedStateUtil.displayClientInfo(stateBucket.currentGameState, socket);
|
||||
}
|
||||
document.querySelectorAll('[data-pointer="' + person.id + '"]').forEach((node) => {
|
||||
node.querySelector('.person-name-element').innerText = newName;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on(EVENT_IDS.END_GAME, (people) => {
|
||||
stateBucket.currentGameState.people = people;
|
||||
stateBucket.currentGameState.status = STATUS.ENDED;
|
||||
|
||||
@@ -55,7 +55,7 @@ const joinHandler = (e) => {
|
||||
resetJoinButtonState(e, joinHandler);
|
||||
});
|
||||
} else {
|
||||
toast('Name must be between 1 and 30 characters.', 'error', true, true, 'long');
|
||||
toast('Name must be between 1 and ' + PRIMITIVES.MAX_PERSON_NAME_LENGTH + ' characters.', 'error', true, true, 'long');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,7 +90,7 @@ function resetJoinButtonState (e, joinHandler) {
|
||||
}
|
||||
|
||||
function validateName (name) {
|
||||
return typeof name === 'string' && name.length > 0 && name.length <= 30;
|
||||
return typeof name === 'string' && name.length > 0 && name.length <= PRIMITIVES.MAX_PERSON_NAME_LENGTH;
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
|
||||
@@ -315,7 +315,7 @@ button {
|
||||
}
|
||||
|
||||
#how-to-use-container h1 {
|
||||
color: #d7d7d7;
|
||||
color: #21ba45;
|
||||
font-family: signika-negative, sans-serif;
|
||||
background-color: #1e1b26;
|
||||
width: fit-content;
|
||||
@@ -376,6 +376,11 @@ input {
|
||||
#how-to-use-container h3 {
|
||||
color: #b1afcd;
|
||||
font-weight: bold;
|
||||
font-family: signika-negative, sans-serif;
|
||||
background-color: #1e1b26;
|
||||
width: fit-content;
|
||||
padding: 0 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#tutorial-links {text-align: left;
|
||||
@@ -829,7 +834,7 @@ input {
|
||||
|
||||
@media(max-width: 550px) {
|
||||
.how-to-use-header {
|
||||
font-size: 25px;
|
||||
font-size: 30px;
|
||||
}
|
||||
#how-to-use-container h3 {
|
||||
font-size: 20px;
|
||||
|
||||
@@ -564,8 +564,34 @@ h1 {
|
||||
}
|
||||
|
||||
#client-container {
|
||||
max-width: 35em;
|
||||
max-width: 25em;
|
||||
width: 75%;
|
||||
margin: 1em 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#client-container button {
|
||||
background-color: transparent;
|
||||
height: fit-content;
|
||||
margin: 0 8px;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid transparent;
|
||||
position: absolute;
|
||||
right: -50px;
|
||||
top: 32px;
|
||||
}
|
||||
|
||||
#client-container button:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(1.5);
|
||||
background-color: #8080804d;
|
||||
}
|
||||
|
||||
#client-container button img {
|
||||
width: 22px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#client {
|
||||
@@ -575,16 +601,19 @@ h1 {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 5px;
|
||||
min-width: 15em;
|
||||
border: 1px solid #46455299;
|
||||
background: #4645525c;
|
||||
}
|
||||
|
||||
#client-name {
|
||||
max-width: 13em;
|
||||
color: #e7e7e7;
|
||||
font-family: 'signika-negative', sans-serif;
|
||||
font-size: 25px;
|
||||
margin: 0.25em 2em 0.25em 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#client-user-type {
|
||||
@@ -920,8 +949,22 @@ canvas {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#change-name-modal-background {
|
||||
cursor: default;
|
||||
#change-name-form-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
max-width: 15em;
|
||||
text-align: left;
|
||||
margin: 2px auto;
|
||||
}
|
||||
|
||||
#change-name-form-content input {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#change-name-form-content label {
|
||||
display: flex;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#lobby-people-container , #game-people-container {
|
||||
@@ -954,6 +997,16 @@ canvas {
|
||||
}
|
||||
|
||||
@media(max-width: 500px) {
|
||||
#client-container button img {
|
||||
width: 18px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#client-container button {
|
||||
right: -46px;
|
||||
top: 27px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 18px;
|
||||
}
|
||||
@@ -968,6 +1021,7 @@ canvas {
|
||||
|
||||
#client-name {
|
||||
font-size: 20px;
|
||||
margin: 0.25em 0 0.25em 0;
|
||||
}
|
||||
|
||||
#client-user-type, #game-parameters {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<div id="tutorial-links">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#purpose-of-the-app">Purpose of the App</a>
|
||||
<a href="#purpose-of-the-app">Purpose</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#creating-a-game">Creating a Game</a>
|
||||
@@ -38,7 +38,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h1 class="how-to-use-header" id="purpose-of-the-app">Purpose of the Application</h1>
|
||||
<h1 class="how-to-use-header" id="purpose-of-the-app">Purpose</h1>
|
||||
<div class="how-to-use-section">This app serves as a means of running games in a social setting where a traditional
|
||||
running of the game is hindered. This might be when people are meeting virtually, and thus roles can't be handed
|
||||
out in-person, or when people are in-person but don't have Werewolf/Mafia cards with them. You can use a deck of regular
|
||||
@@ -52,7 +52,7 @@
|
||||
<div class="how-to-use-section">
|
||||
Creating a game through the app has 3 main components:
|
||||
<br>
|
||||
<h3>Step One: Choosing a method of moderation</h3>
|
||||
<h3>- Step One: Choosing a 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 not dealt into the game. Once they start the game, they will know
|
||||
@@ -68,7 +68,7 @@
|
||||
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>
|
||||
<h3>Step Two: Build your deck</h3>
|
||||
<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 displayed by selecting the appropriate button within the box.
|
||||
@@ -79,7 +79,7 @@
|
||||
<br><br>
|
||||
Here I add 3 villagers to the game, and then remove them:
|
||||
<br><br>
|
||||
<img class='tutorial-image-small' src="../images/tutorial/add-role-to-deck.gif"/>
|
||||
<img alt="adding a role to the deck" class='tutorial-image-small' src="../images/tutorial/add-role-to-deck.gif"/>
|
||||
<br><br>
|
||||
You can add, edit, and remove Custom Roles. You can also import and export them via a formatted text file. Click
|
||||
the hamburger menu on the role box to see the import/export options. Here I create a new Custom Role and observe
|
||||
@@ -87,7 +87,7 @@
|
||||
<br><br>
|
||||
<img alt="create-custom-role" class='tutorial-image-small' src="../images/tutorial/create-custom-role.gif"/>
|
||||
<br><br>
|
||||
<h3>Step Three: Set an optional timer</h3>
|
||||
<h3>- Step Three: Set an optional timer</h3>
|
||||
<br>
|
||||
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,
|
||||
@@ -96,7 +96,7 @@
|
||||
</div>
|
||||
<h1 class="how-to-use-header" id="being-the-moderator">Being the Moderator</h1>
|
||||
<div class="how-to-use-section">
|
||||
<h3>In the Lobby</h3>
|
||||
<h3>- In the Lobby</h3>
|
||||
<br>
|
||||
In the Lobby, moderators can manage the people in the room and the cards in the game. By clicking
|
||||
the <span class="emphasized">three vertical dots (AKA the "kebab menu")</span> next to a given player (<span class="emphasized">point A</span>
|
||||
@@ -107,9 +107,9 @@
|
||||
Saving any changes to the roles may affect the player count. If you wish to <span class="emphasized">start the game (point B)</span>, the number
|
||||
of Players in the Lobby must equal the number of cards in the game.
|
||||
<br><br>
|
||||
<img class='tutorial-image-small-portrait' src="../images/tutorial/dedicated-mod-lobby-mobile.webp"/>
|
||||
<img alt="moderator view in the lobby" class='tutorial-image-small-portrait' src="../images/tutorial/dedicated-mod-lobby-mobile.webp"/>
|
||||
<br><br>
|
||||
<h3>During the Game</h3>
|
||||
<h3>- During the Game</h3>
|
||||
<br>
|
||||
<span class="emphasized">Dedicated Moderators</span> can see who is on which team and who is which role. The moderator
|
||||
Kills and Reveals players (<span class="emphasized">Point A</span> below). They are separate actions. So, if you
|
||||
@@ -120,33 +120,33 @@
|
||||
play and pause the Timer (<span class="emphasized">Point B</span>), and can end the game (revealing everyone's role)
|
||||
or return the game to the Lobby (<span class="emphasized">Point C</span>), where it can be started anew with different settings.
|
||||
<br><br>
|
||||
<img class='tutorial-image-small-portrait' src="../images/tutorial/dedicated-mod-in-progress-mobile.webp"/>
|
||||
<img alt="moderator view during the game" class='tutorial-image-small-portrait' src="../images/tutorial/dedicated-mod-in-progress-mobile.webp"/>
|
||||
<br><br>
|
||||
Similarly, 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
|
||||
(which can be themselves).
|
||||
<br><br>
|
||||
<img class='tutorial-image-small-portrait' src="../images/tutorial/temp-mod-in-progress-mobile.webp"/>
|
||||
<img alt="temporary moderator view during the game" class='tutorial-image-small-portrait' src="../images/tutorial/temp-mod-in-progress-mobile.webp"/>
|
||||
<br><br>
|
||||
<h3>Transferring your moderator powers</h3>
|
||||
<h3>- Transferring your moderator powers</h3>
|
||||
<br>
|
||||
You can transfer your moderator abilities to anyone that has been removed from the game, or to anyone that happens
|
||||
to be spectating. Here we select a killed player and transfer our powers to them:
|
||||
<br><br>
|
||||
<img class='tutorial-image-small-portrait' src="../images/tutorial/mod-transfer.gif"/>
|
||||
<img alt="transferring your moderator powers" class='tutorial-image-small-portrait' src="../images/tutorial/mod-transfer.gif"/>
|
||||
<br><br>
|
||||
</div>
|
||||
<h1 class="how-to-use-header" id="being-a-player">Being a Player</h1>
|
||||
<div class="how-to-use-section">
|
||||
This is an example of what a <span class="emphasized">Player</span> is seeing. The timer is running, and they view their
|
||||
role by double-clicking it:
|
||||
This is an example of what a <span class="emphasized">Player</span> is seeing, including the running timer,
|
||||
their role card, and the player list. You can also edit your name for the Room by clicking the pencil next to it.
|
||||
Below, we flip our role card up and down by double-clicking it, and then we bring up the prompt to edit our name:
|
||||
<br><br>
|
||||
<img class='tutorial-image-small-portrait' src="../images/tutorial/player-view.gif"/>
|
||||
<img alt='player-view' class='tutorial-image-small-portrait' src="../images/tutorial/player-view.gif"/>
|
||||
<br><br>
|
||||
There are three main things - the <span class="emphasized">Timer</span>, your <span class="emphasized">Role Card</span>
|
||||
and the <span class="emphasized">Player List</span>. Players can view the timer, but only the current moderator can play and pause it.
|
||||
Your role card starts flipped over - this is useful if you are in-person and don't want someone else accidentally seeing your role as
|
||||
Players can view the timer, but only the current moderator can play and pause it. Your role card starts flipped over
|
||||
- this is useful if you are in-person and don't want someone else accidentally seeing your role as
|
||||
it is dealt. You can view your role at any time by double-clicking/double-tapping it. Requiring a double-click guards against the possibility
|
||||
of accidentally flipping your role when tapping other things. Within the Player List, you can see who is alive or
|
||||
dead and who has had their role revealed. There is also a <span class="emphasized">role info button</span> that,
|
||||
|
||||
@@ -10,7 +10,8 @@ const PRIMITIVES = {
|
||||
USER_SIGNATURE_LENGTH: 25,
|
||||
INSTANCE_ID_LENGTH: 75,
|
||||
MAX_SPECTATORS: 25,
|
||||
MOCK_AUTH: 'mock_auth'
|
||||
MOCK_AUTH: 'mock_auth',
|
||||
MAX_PERSON_NAME_LENGTH: 40
|
||||
};
|
||||
|
||||
const LOG_LEVEL = {
|
||||
@@ -88,6 +89,7 @@ const SYNCABLE_EVENTS = function () {
|
||||
EVENT_IDS.KILL_PLAYER,
|
||||
EVENT_IDS.REVEAL_PLAYER,
|
||||
EVENT_IDS.TRANSFER_MODERATOR,
|
||||
EVENT_IDS.CHANGE_NAME,
|
||||
EVENT_IDS.END_GAME,
|
||||
EVENT_IDS.RESTART_GAME,
|
||||
EVENT_IDS.PLAYER_JOINED,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const GameStateCurator = require('./GameStateCurator');
|
||||
const GameCreationRequest = require('../model/GameCreationRequest');
|
||||
const { EVENT_IDS, STATUS, USER_TYPES, GAME_PROCESS_COMMANDS, REDIS_CHANNELS } = require('../config/globals');
|
||||
const { EVENT_IDS, STATUS, USER_TYPES, GAME_PROCESS_COMMANDS, REDIS_CHANNELS, PRIMITIVES } = require('../config/globals');
|
||||
|
||||
const Events = [
|
||||
{
|
||||
@@ -55,6 +55,42 @@ const Events = [
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: EVENT_IDS.CHANGE_NAME,
|
||||
stateChange: async (game, socketArgs, vars) => {
|
||||
const toChangeIndex = game.people.findIndex(
|
||||
(person) => person.id === socketArgs.personId
|
||||
);
|
||||
if (toChangeIndex >= 0) {
|
||||
if (vars.gameManager.isNameTaken(game, socketArgs.newName)) {
|
||||
vars.hasNameChanged = false;
|
||||
if (game.people[toChangeIndex].name.toLowerCase().trim() === socketArgs.newName.toLowerCase().trim()) {
|
||||
return;
|
||||
}
|
||||
vars.ackFn({ errorFlag: 1, message: 'This name is taken.' });
|
||||
} else if (socketArgs.newName.length > PRIMITIVES.MAX_PERSON_NAME_LENGTH) {
|
||||
vars.ackFn({ errorFlag: 1, message: 'Your new name is too long - the max is' + PRIMITIVES.MAX_PERSON_NAME_LENGTH + ' characters.' });
|
||||
vars.hasNameChanged = false;
|
||||
} else if (socketArgs.newName.length === 0) {
|
||||
vars.ackFn({ errorFlag: 1, message: 'Your new name cannot be empty.' });
|
||||
vars.hasNameChanged = false;
|
||||
} else {
|
||||
game.people[toChangeIndex].name = socketArgs.newName;
|
||||
vars.ackFn({ errorFlag: 0, message: 'Name updated!' });
|
||||
vars.hasNameChanged = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
communicate: async (game, socketArgs, vars) => {
|
||||
if (vars.hasNameChanged) {
|
||||
vars.gameManager.namespace.in(game.accessCode).emit(
|
||||
EVENT_IDS.CHANGE_NAME,
|
||||
socketArgs.personId,
|
||||
socketArgs.newName
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: EVENT_IDS.UPDATE_GAME_ROLES,
|
||||
stateChange: async (game, socketArgs, vars) => {
|
||||
|
||||
@@ -7,25 +7,23 @@ This timer is accurate to within a few ms for any amount of time provided.
|
||||
*/
|
||||
|
||||
function stepFn (serverTimerInstance, expected) {
|
||||
const now = Date.now(); //
|
||||
serverTimerInstance.currentTimeInMillis = serverTimerInstance.totalTime - (now - serverTimerInstance.start);
|
||||
if (now - serverTimerInstance.start >= serverTimerInstance.totalTime) { // check if the time has elapsed
|
||||
serverTimerInstance.currentTimeInMillis = serverTimerInstance.totalTime - (Date.now() - serverTimerInstance.start);
|
||||
if (Date.now() - serverTimerInstance.start >= serverTimerInstance.totalTime) { // check if the time has elapsed
|
||||
serverTimerInstance.logger.debug(
|
||||
'ELAPSED: ' + (now - serverTimerInstance.start) + 'ms (~' +
|
||||
(Math.abs(serverTimerInstance.totalTime - (now - serverTimerInstance.start)) / serverTimerInstance.totalTime).toFixed(3) + '% error).'
|
||||
'ELAPSED: ' + (Date.now() - serverTimerInstance.start) + 'ms (~' +
|
||||
(Math.abs(serverTimerInstance.totalTime - (Date.now() - serverTimerInstance.start)) / serverTimerInstance.totalTime).toFixed(3) + '% error).'
|
||||
);
|
||||
serverTimerInstance.timesUpResolver(); // this is a reference to the callback defined in the construction of the promise in runTimer()
|
||||
clearTimeout(serverTimerInstance.ticking);
|
||||
return;
|
||||
}
|
||||
const delta = now - expected;
|
||||
expected += serverTimerInstance.interval;
|
||||
serverTimerInstance.ticking = setTimeout(function () {
|
||||
stepFn(
|
||||
serverTimerInstance,
|
||||
expected
|
||||
);
|
||||
}, Math.max(0, serverTimerInstance.interval - delta)); // take into account drift
|
||||
}, Math.max(0, serverTimerInstance.interval - (Date.now() - expected))); // take into account drift
|
||||
}
|
||||
|
||||
class ServerTimer {
|
||||
|
||||
@@ -182,7 +182,7 @@ class GameManager {
|
||||
if (matchingPerson) {
|
||||
return Promise.resolve(matchingPerson.cookie);
|
||||
}
|
||||
if (isNameTaken(game, name)) {
|
||||
if (this.isNameTaken(game, name)) {
|
||||
return Promise.reject({ status: 400, reason: 'This name is taken.' });
|
||||
}
|
||||
if (joinAsSpectator
|
||||
@@ -334,6 +334,11 @@ class GameManager {
|
||||
findPersonByField = (game, fieldName, value) => {
|
||||
return game.people.find(person => person[fieldName] === value);
|
||||
}
|
||||
|
||||
isNameTaken (game, name) {
|
||||
const processedName = name.toLowerCase().trim();
|
||||
return game.people.find((person) => person.name.toLowerCase().trim() === processedName);
|
||||
}
|
||||
}
|
||||
|
||||
function getRandomInt (max) {
|
||||
@@ -383,11 +388,6 @@ function createRandomId () {
|
||||
return id;
|
||||
}
|
||||
|
||||
function isNameTaken (game, name) {
|
||||
const processedName = name.toLowerCase().trim();
|
||||
return game.people.find((person) => person.name.toLowerCase().trim() === processedName);
|
||||
}
|
||||
|
||||
async function addSpectator (game, name, logger, namespace, eventManager, instanceId, refreshGame) {
|
||||
const spectator = new Person(
|
||||
createRandomId(),
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('GameManager', () => {
|
||||
const inObj = { emit: () => {} };
|
||||
namespace = { in: () => { return inObj; }, to: () => { return inObj; } };
|
||||
socket = { id: '123', emit: () => {}, to: () => { return { emit: () => {} }; } };
|
||||
gameManager = GameManager.instance ? GameManager.instance : new GameManager(logger, globals.ENVIRONMENT.PRODUCTION, 'test');
|
||||
gameManager = GameManager.instance ? GameManager.instance : new GameManager(logger, globals.ENVIRONMENTS.PRODUCTION, 'test');
|
||||
timerManager = TimerManager.instance ? TimerManager.instance : new TimerManager(logger, 'test');
|
||||
eventManager = EventManager.instance ? EventManager.instance : new EventManager(logger, 'test');
|
||||
eventManager.publisher = { publish: async (...a) => {} };
|
||||
|
||||
Reference in New Issue
Block a user