mirror of
https://github.com/AlecM33/Werewolf.git
synced 2025-12-26 15:57:50 +01:00
Merge pull request #91 from AlecM33/develop
refactor join to intermediary page
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="119.66505mm"
|
||||
height="109.59733mm"
|
||||
viewBox="0 0 119.66505 109.59733"
|
||||
viewBox="0 -12 125.66505 130.59733"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -1,9 +1,10 @@
|
||||
export class Game {
|
||||
constructor (deck, hasTimer, hasDedicatedModerator, timerParams = null) {
|
||||
constructor (deck, hasTimer, hasDedicatedModerator, moderatorName, timerParams = null) {
|
||||
this.deck = deck;
|
||||
this.hasTimer = hasTimer;
|
||||
this.timerParams = timerParams;
|
||||
this.hasDedicatedModerator = hasDedicatedModerator;
|
||||
this.moderatorName = moderatorName;
|
||||
this.accessCode = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { XHRUtility } from './XHRUtility.js';
|
||||
import { globals } from '../config/globals.js';
|
||||
import { templates } from './Templates.js';
|
||||
import { defaultCards } from '../config/defaultCards';
|
||||
import { UserUtility } from './UserUtility';
|
||||
|
||||
export class GameCreationStepManager {
|
||||
constructor (deckManager) {
|
||||
@@ -50,50 +51,69 @@ export class GameCreationStepManager {
|
||||
},
|
||||
3: {
|
||||
title: 'Set an optional timer:',
|
||||
forwardHandler: () => {
|
||||
const hours = parseInt(document.getElementById('game-hours').value);
|
||||
const minutes = parseInt(document.getElementById('game-minutes').value);
|
||||
if ((isNaN(hours) && isNaN(minutes))
|
||||
|| (isNaN(hours) && minutes > 0 && minutes < 60)
|
||||
|| (isNaN(minutes) && hours > 0 && hours < 6)
|
||||
|| (hours === 0 && minutes > 0 && minutes < 60)
|
||||
|| (minutes === 0 && hours > 0 && hours < 6)
|
||||
|| (hours > 0 && hours < 6 && minutes >= 0 && minutes < 60)
|
||||
) {
|
||||
if (hasTimer(hours, minutes)) {
|
||||
this.currentGame.hasTimer = true;
|
||||
this.currentGame.timerParams = {
|
||||
hours: hours,
|
||||
minutes: minutes
|
||||
};
|
||||
forwardHandler: (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
const hours = parseInt(document.getElementById('game-hours').value);
|
||||
const minutes = parseInt(document.getElementById('game-minutes').value);
|
||||
if ((isNaN(hours) && isNaN(minutes))
|
||||
|| (isNaN(hours) && minutes > 0 && minutes < 60)
|
||||
|| (isNaN(minutes) && hours > 0 && hours < 6)
|
||||
|| (hours === 0 && minutes > 0 && minutes < 60)
|
||||
|| (minutes === 0 && hours > 0 && hours < 6)
|
||||
|| (hours > 0 && hours < 6 && minutes >= 0 && minutes < 60)
|
||||
) {
|
||||
if (hasTimer(hours, minutes)) {
|
||||
this.currentGame.hasTimer = true;
|
||||
this.currentGame.timerParams = {
|
||||
hours: hours,
|
||||
minutes: minutes
|
||||
};
|
||||
} else {
|
||||
this.currentGame.hasTimer = false;
|
||||
this.currentGame.timerParams = null;
|
||||
}
|
||||
cancelCurrentToast();
|
||||
this.removeStepElementsFromDOM(this.step);
|
||||
this.incrementStep();
|
||||
this.renderStep('creation-step-container', this.step);
|
||||
} else {
|
||||
this.currentGame.hasTimer = false;
|
||||
this.currentGame.timerParams = null;
|
||||
}
|
||||
cancelCurrentToast();
|
||||
this.removeStepElementsFromDOM(this.step);
|
||||
this.incrementStep();
|
||||
this.renderStep('creation-step-container', this.step);
|
||||
} else {
|
||||
if (hours === 0 && minutes === 0) {
|
||||
toast('You must enter a non-zero amount of time.', 'error', true);
|
||||
} else {
|
||||
toast('Invalid timer options. Hours can be a max of 5, Minutes a max of 59.', 'error', true);
|
||||
if (hours === 0 && minutes === 0) {
|
||||
toast('You must enter a non-zero amount of time.', 'error', true);
|
||||
} else {
|
||||
toast('Invalid timer options. Hours can be a max of 5, Minutes a max of 59.', 'error', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
backHandler: this.defaultBackHandler
|
||||
},
|
||||
4: {
|
||||
title: 'Enter your name:',
|
||||
forwardHandler: (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
const name = document.getElementById('moderator-name').value;
|
||||
if (validateName(name)) {
|
||||
this.currentGame.moderatorName = name;
|
||||
this.removeStepElementsFromDOM(this.step);
|
||||
this.incrementStep();
|
||||
this.renderStep('creation-step-container', this.step);
|
||||
} else {
|
||||
toast('Name must be between 1 and 30 characters.', 'error', true);
|
||||
}
|
||||
}
|
||||
},
|
||||
backHandler: this.defaultBackHandler
|
||||
},
|
||||
5: {
|
||||
title: 'Review and submit:',
|
||||
backHandler: this.defaultBackHandler,
|
||||
forwardHandler: (deck, hasTimer, hasDedicatedModerator, timerParams) => {
|
||||
forwardHandler: (deck, hasTimer, hasDedicatedModerator, moderatorName, timerParams) => {
|
||||
XHRUtility.xhr(
|
||||
'/api/games/create',
|
||||
'POST',
|
||||
null,
|
||||
JSON.stringify(
|
||||
new Game(deck, hasTimer, hasDedicatedModerator, timerParams)
|
||||
new Game(deck, hasTimer, hasDedicatedModerator, moderatorName, timerParams)
|
||||
)
|
||||
)
|
||||
.then((res) => {
|
||||
@@ -102,7 +122,9 @@ export class GameCreationStepManager {
|
||||
&& Object.prototype.hasOwnProperty.call(res, 'content')
|
||||
&& typeof res.content === 'string'
|
||||
) {
|
||||
window.location = ('/game/' + res.content);
|
||||
const json = JSON.parse(res.content);
|
||||
UserUtility.setAnonymousUserId(json.cookie, json.environment);
|
||||
window.location = ('/game/' + json.accessCode);
|
||||
}
|
||||
}).catch((e) => {
|
||||
const button = document.getElementById('create-game');
|
||||
@@ -144,10 +166,14 @@ export class GameCreationStepManager {
|
||||
showButtons(true, true, this.steps[step].forwardHandler, this.steps[step].backHandler);
|
||||
break;
|
||||
case 3:
|
||||
renderTimerStep(containerId, step, this.currentGame);
|
||||
renderTimerStep(containerId, step, this.currentGame, this.steps);
|
||||
showButtons(true, true, this.steps[step].forwardHandler, this.steps[step].backHandler);
|
||||
break;
|
||||
case 4:
|
||||
renderNameStep(containerId, step, this.currentGame, this.steps);
|
||||
showButtons(true, true, this.steps[step].forwardHandler, this.steps[step].backHandler);
|
||||
break;
|
||||
case 5:
|
||||
renderReviewAndCreateStep(containerId, step, this.currentGame);
|
||||
showButtons(true, true, this.steps[step].forwardHandler, this.steps[step].backHandler, this.currentGame);
|
||||
break;
|
||||
@@ -162,6 +188,18 @@ export class GameCreationStepManager {
|
||||
}
|
||||
}
|
||||
|
||||
function renderNameStep (containerId, step, game, steps) {
|
||||
const stepContainer = document.createElement('div');
|
||||
setAttributes(stepContainer, { id: 'step-' + step, class: 'flex-row-container step' });
|
||||
|
||||
stepContainer.innerHTML = templates.ENTER_NAME_STEP;
|
||||
document.getElementById(containerId).appendChild(stepContainer);
|
||||
const nameInput = document.querySelector('#moderator-name');
|
||||
nameInput.value = game.moderatorName;
|
||||
nameInput.addEventListener('keyup', steps['4'].forwardHandler);
|
||||
nameInput.focus();
|
||||
}
|
||||
|
||||
function renderModerationTypeStep (game, containerId, stepNumber) {
|
||||
const stepContainer = document.createElement('div');
|
||||
setAttributes(stepContainer, { id: 'step-' + stepNumber, class: 'flex-row-container step' });
|
||||
@@ -277,7 +315,7 @@ function renderRoleSelectionStep (game, containerId, step, deckManager) {
|
||||
initializeRemainingEventListeners(deckManager);
|
||||
}
|
||||
|
||||
function renderTimerStep (containerId, stepNumber, game) {
|
||||
function renderTimerStep (containerId, stepNumber, game, steps) {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('id', 'step-' + stepNumber);
|
||||
div.classList.add('step');
|
||||
@@ -290,6 +328,7 @@ function renderTimerStep (containerId, stepNumber, game) {
|
||||
hoursLabel.setAttribute('for', 'game-hours');
|
||||
hoursLabel.innerText = 'Hours (max 5)';
|
||||
const hours = document.createElement('input');
|
||||
hours.addEventListener('keyup', steps[stepNumber].forwardHandler);
|
||||
setAttributes(hours, { type: 'number', id: 'game-hours', name: 'game-hours', min: '0', max: '5', value: game.timerParams?.hours });
|
||||
|
||||
const minutesDiv = document.createElement('div');
|
||||
@@ -297,6 +336,7 @@ function renderTimerStep (containerId, stepNumber, game) {
|
||||
minsLabel.setAttribute('for', 'game-minutes');
|
||||
minsLabel.innerText = 'Minutes';
|
||||
const minutes = document.createElement('input');
|
||||
minutes.addEventListener('keyup', steps[stepNumber].forwardHandler);
|
||||
setAttributes(minutes, { type: 'number', id: 'game-minutes', name: 'game-minutes', min: '1', max: '60', value: game.timerParams?.minutes });
|
||||
|
||||
hoursDiv.appendChild(hoursLabel);
|
||||
@@ -316,6 +356,10 @@ function renderReviewAndCreateStep (containerId, stepNumber, game) {
|
||||
div.classList.add('step');
|
||||
|
||||
div.innerHTML =
|
||||
'<div>' +
|
||||
"<label for='mod-name'>Your name</label>" +
|
||||
"<div id='mod-name' class='review-option'></div>" +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
"<label for='mod-option'>Moderation</label>" +
|
||||
"<div id='mod-option' class='review-option'></div>" +
|
||||
@@ -330,8 +374,8 @@ function renderReviewAndCreateStep (containerId, stepNumber, game) {
|
||||
'</div>';
|
||||
|
||||
div.querySelector('#mod-option').innerText = game.hasDedicatedModerator
|
||||
? "I will be the dedicated mod. Don't deal me a card."
|
||||
: 'The first person out will mod. Deal me into the game.';
|
||||
? "Dedicated Moderator - don't deal me a card."
|
||||
: 'Temporary Moderator - deal me into the game.';
|
||||
|
||||
if (game.hasTimer) {
|
||||
const formattedHours = !isNaN(game.timerParams.hours)
|
||||
@@ -353,6 +397,8 @@ function renderReviewAndCreateStep (containerId, stepNumber, game) {
|
||||
div.querySelector('#roles-option').appendChild(roleEl);
|
||||
}
|
||||
|
||||
div.querySelector('#mod-name').innerText = game.moderatorName;
|
||||
|
||||
document.getElementById(containerId).appendChild(div);
|
||||
}
|
||||
|
||||
@@ -406,6 +452,7 @@ function showButtons (back, forward, forwardHandler, backHandler, builtGame = nu
|
||||
builtGame.deck.filter((card) => card.quantity > 0),
|
||||
builtGame.hasTimer,
|
||||
builtGame.hasDedicatedModerator,
|
||||
builtGame.moderatorName,
|
||||
builtGame.timerParams
|
||||
);
|
||||
});
|
||||
@@ -701,3 +748,7 @@ function updateDeckStatus (deckManager) {
|
||||
function hasTimer (hours, minutes) {
|
||||
return (!isNaN(hours) || !isNaN(minutes));
|
||||
}
|
||||
|
||||
function validateName (name) {
|
||||
return typeof name === 'string' && name.length > 0 && name.length <= 30;
|
||||
}
|
||||
|
||||
@@ -43,18 +43,7 @@ export class GameStateRenderer {
|
||||
title.innerText = 'Lobby';
|
||||
document.getElementById('game-title').appendChild(title);
|
||||
const gameLinkContainer = document.getElementById('game-link');
|
||||
const linkDiv = document.createElement('div');
|
||||
linkDiv.innerText = window.location;
|
||||
gameLinkContainer.prepend(linkDiv);
|
||||
const linkCopyHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
navigator.clipboard.writeText(gameLinkContainer.innerText).then(() => {
|
||||
toast('Link copied!', 'success', true);
|
||||
});
|
||||
}
|
||||
};
|
||||
gameLinkContainer.addEventListener('click', linkCopyHandler);
|
||||
gameLinkContainer.addEventListener('keyup', linkCopyHandler);
|
||||
|
||||
const copyImg = document.createElement('img');
|
||||
copyImg.setAttribute('src', '../images/copy.svg');
|
||||
gameLinkContainer.appendChild(copyImg);
|
||||
@@ -63,8 +52,8 @@ export class GameStateRenderer {
|
||||
const playerCount = document.getElementById('game-player-count');
|
||||
playerCount.innerText = getGameSize(this.stateBucket.currentGameState.deck) + ' Players';
|
||||
|
||||
let timeString = '';
|
||||
if (this.stateBucket.currentGameState.timerParams) {
|
||||
let timeString = '';
|
||||
const hours = this.stateBucket.currentGameState.timerParams.hours;
|
||||
const minutes = this.stateBucket.currentGameState.timerParams.minutes;
|
||||
if (hours) {
|
||||
@@ -79,8 +68,30 @@ export class GameStateRenderer {
|
||||
}
|
||||
time.innerText = timeString;
|
||||
} else {
|
||||
time.innerText = 'untimed';
|
||||
timeString = 'untimed';
|
||||
time.innerText = timeString;
|
||||
}
|
||||
|
||||
const link = window.location.protocol + '//' + window.location.host +
|
||||
'/join/' + this.stateBucket.currentGameState.accessCode +
|
||||
'?playerCount=' + getGameSize(this.stateBucket.currentGameState.deck) +
|
||||
'&timer=' + encodeURIComponent(timeString);
|
||||
|
||||
const linkCopyHandler = (e) => {
|
||||
if (e.type === 'click' || e.code === 'Enter') {
|
||||
navigator.clipboard.writeText(link)
|
||||
.then(() => {
|
||||
toast('Link copied!', 'success', true);
|
||||
});
|
||||
}
|
||||
};
|
||||
gameLinkContainer.addEventListener('click', linkCopyHandler);
|
||||
gameLinkContainer.addEventListener('keyup', linkCopyHandler);
|
||||
|
||||
const linkDiv = document.createElement('div');
|
||||
linkDiv.innerText = link;
|
||||
|
||||
gameLinkContainer.prepend(linkDiv);
|
||||
}
|
||||
|
||||
renderLobbyFooter () {
|
||||
|
||||
@@ -2,7 +2,7 @@ export const templates = {
|
||||
LOBBY:
|
||||
"<div id='lobby-header'>" +
|
||||
'<div>' +
|
||||
"<label for='game-link'>Share Link</label>" +
|
||||
"<label for='game-link'>Copy Share Link</label>" +
|
||||
"<div tabindex='0' id='game-link'></div>" +
|
||||
'</div>' +
|
||||
"<div id='game-parameters'>" +
|
||||
@@ -28,6 +28,12 @@ export const templates = {
|
||||
"<div id='game-deck'></div>" +
|
||||
'</div>' +
|
||||
'</div>',
|
||||
ENTER_NAME_STEP:
|
||||
`<div id='step-4'>
|
||||
<div>
|
||||
<input type="text" id="moderator-name" autocomplete='given-name' placeholder="enter your name...">
|
||||
</div>
|
||||
</div>`,
|
||||
START_GAME_PROMPT:
|
||||
"<div id='start-game-prompt'>" +
|
||||
"<button id='start-game-button'>Start Game</button>" +
|
||||
|
||||
@@ -48,39 +48,15 @@ function syncWithGame (stateBucket, gameTimerManager, gameStateRenderer, timerWo
|
||||
const accessCode = splitUrl[1];
|
||||
if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) {
|
||||
socket.emit(globals.COMMANDS.FETCH_GAME_STATE, accessCode, cookie, function (gameState) {
|
||||
cookie = gameState.client.cookie;
|
||||
UserUtility.setAnonymousUserId(cookie, stateBucket.environment);
|
||||
stateBucket.currentGameState = gameState;
|
||||
document.querySelector('.spinner-container')?.remove();
|
||||
document.querySelector('.spinner-background')?.remove();
|
||||
document.getElementById('game-content').innerHTML = templates.INITIAL_GAME_DOM;
|
||||
toast('You are connected.', 'success', true, true, 2);
|
||||
processGameState(stateBucket.currentGameState, cookie, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
if (!gameState.client.hasEnteredName) {
|
||||
document.getElementById('prompt').innerHTML = templates.NAME_CHANGE_MODAL;
|
||||
document.getElementById('change-name-form').onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('player-new-name').value;
|
||||
if (validateName(name)) {
|
||||
socket.emit(globals.COMMANDS.CHANGE_NAME, gameState.accessCode, {
|
||||
name: name,
|
||||
personId: gameState.client.id
|
||||
}, (result) => {
|
||||
switch (result) {
|
||||
case 'taken':
|
||||
toast('This name is already taken.', 'error', true, true, 8);
|
||||
break;
|
||||
case 'changed':
|
||||
ModalManager.dispelModal('change-name-modal', 'change-name-modal-background');
|
||||
toast('Name set.', 'success', true, true, 5);
|
||||
propagateNameChange(stateBucket.currentGameState, name, stateBucket.currentGameState.client.id);
|
||||
processGameState(stateBucket.currentGameState, cookie, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
toast('Name must be between 1 and 30 characters.', 'error', true, true, 8);
|
||||
}
|
||||
};
|
||||
if (gameState === null) {
|
||||
window.location = '/not-found?reason=' + encodeURIComponent('game-not-found');
|
||||
} else {
|
||||
stateBucket.currentGameState = gameState;
|
||||
document.querySelector('.spinner-container')?.remove();
|
||||
document.querySelector('.spinner-background')?.remove();
|
||||
document.getElementById('game-content').innerHTML = templates.INITIAL_GAME_DOM;
|
||||
toast('You are connected.', 'success', true, true, 2);
|
||||
processGameState(stateBucket.currentGameState, cookie, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -322,10 +298,6 @@ function displayStartGamePromptForModerators (gameState, gameStateRenderer) {
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
function validateName (name) {
|
||||
return typeof name === 'string' && name.length > 0 && name.length <= 30;
|
||||
}
|
||||
|
||||
function removeStartGameFunctionalityIfPresent (gameStateRenderer) {
|
||||
document.querySelector('#start-game-prompt')?.removeEventListener('click', gameStateRenderer.startGameHandler);
|
||||
document.querySelector('#start-game-prompt')?.remove();
|
||||
|
||||
@@ -16,19 +16,23 @@ const home = () => {
|
||||
};
|
||||
|
||||
function roomCodeIsValid (code) {
|
||||
return typeof code === 'string' && /^[a-z0-9]{6}$/.test(code.toLowerCase());
|
||||
return typeof code === 'string' && /^[A-Z0-9]{6}$/.test(code.toUpperCase().trim());
|
||||
}
|
||||
|
||||
function attemptToJoinGame (code) {
|
||||
XHRUtility.xhr(
|
||||
'/api/games/' + code.toLowerCase().trim() + 'availability',
|
||||
'/api/games/' + code.toUpperCase().trim() + '/availability',
|
||||
'GET',
|
||||
null,
|
||||
null
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
window.location = '/game/' + res.content;
|
||||
let json = JSON.parse(res.content);
|
||||
window.location = window.location.protocol + '//' + window.location.host +
|
||||
'/join/' + encodeURIComponent(json.accessCode) +
|
||||
'?playerCount=' + encodeURIComponent(json.playerCount) +
|
||||
'&timer=' + encodeURIComponent(getTimeString(json.timerParams));
|
||||
}
|
||||
}).catch((res) => {
|
||||
if (res.status === 404) {
|
||||
@@ -41,6 +45,29 @@ function attemptToJoinGame (code) {
|
||||
});
|
||||
}
|
||||
|
||||
function getTimeString(timerParams) {
|
||||
let timeString = '';
|
||||
if (timerParams) {
|
||||
const hours = timerParams.hours;
|
||||
const minutes = timerParams.minutes;
|
||||
if (hours) {
|
||||
timeString += hours > 1
|
||||
? hours + ' hours '
|
||||
: hours + ' hour ';
|
||||
}
|
||||
if (minutes) {
|
||||
timeString += minutes > 1
|
||||
? minutes + ' minutes '
|
||||
: minutes + ' minute ';
|
||||
}
|
||||
|
||||
return timeString;
|
||||
} else {
|
||||
timeString = 'untimed';
|
||||
return timeString;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = home;
|
||||
} else {
|
||||
|
||||
73
client/src/scripts/join.js
Normal file
73
client/src/scripts/join.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { injectNavbar } from '../modules/Navbar.js';
|
||||
import { toast } from '../modules/Toast.js';
|
||||
import { XHRUtility } from '../modules/XHRUtility.js';
|
||||
import { UserUtility } from '../modules/UserUtility.js';
|
||||
import { globals } from '../config/globals.js';
|
||||
|
||||
const join = () => {
|
||||
injectNavbar();
|
||||
const splitUrl = window.location.pathname.split('/join/');
|
||||
const accessCode = splitUrl[1];
|
||||
if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) {
|
||||
document.getElementById('game-code').innerText = accessCode;
|
||||
document.getElementById('game-time').innerText =
|
||||
decodeURIComponent((new URL(document.location)).searchParams.get('timer'));
|
||||
document.getElementById('game-player-count').innerText =
|
||||
decodeURIComponent((new URL(document.location)).searchParams.get('playerCount')) + ' Players';
|
||||
const form = document.getElementById('join-game-form');
|
||||
document.getElementById('player-new-name').focus();
|
||||
form.onsubmit = joinHandler;
|
||||
} else {
|
||||
window.location = '/not-found?reason=' + encodeURIComponent('invalid-access-code');
|
||||
}
|
||||
};
|
||||
|
||||
const joinHandler = (e) => {
|
||||
const splitUrl = window.location.pathname.split('/join/');
|
||||
const accessCode = splitUrl[1];
|
||||
e.preventDefault();
|
||||
const name = document.getElementById('player-new-name').value;
|
||||
if (validateName(name)) {
|
||||
document.getElementById('join-game-form').onsubmit = null;
|
||||
document.getElementById('submit-new-name').classList.add('submitted');
|
||||
document.getElementById('submit-new-name').setAttribute('value', 'Joining...');
|
||||
XHRUtility.xhr(
|
||||
'/api/games/' + accessCode + '/players',
|
||||
'PATCH',
|
||||
null,
|
||||
JSON.stringify({ playerName: name, accessCode: accessCode })
|
||||
)
|
||||
.then((res) => {
|
||||
const json = JSON.parse(res.content);
|
||||
UserUtility.setAnonymousUserId(json.cookie, json.environment);
|
||||
window.location = '/game/' + accessCode;
|
||||
}).catch((res) => {
|
||||
document.getElementById('join-game-form').onsubmit = joinHandler;
|
||||
document.getElementById('submit-new-name').classList.remove('submitted');
|
||||
document.getElementById('submit-new-name').setAttribute('value', 'Join Game');
|
||||
if (res.status === 404) {
|
||||
toast('This game was not found.', 'error', true, true, 8);
|
||||
} else if (res.status === 400) {
|
||||
toast('This name is already taken.', 'error', true, true, 8);
|
||||
} else if (res.status >= 500) {
|
||||
toast(
|
||||
'The server is experiencing problems. Please try again later',
|
||||
'error',
|
||||
true
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
toast('Name must be between 1 and 30 characters.', 'error', true, true, 8);
|
||||
}
|
||||
};
|
||||
|
||||
function validateName (name) {
|
||||
return typeof name === 'string' && name.length > 0 && name.length <= 30;
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = join;
|
||||
} else {
|
||||
join();
|
||||
}
|
||||
@@ -186,6 +186,23 @@ button {
|
||||
color: #00a718;
|
||||
}
|
||||
|
||||
#game-parameters {
|
||||
font-family: signika-negative, sans-serif;
|
||||
color: #d7d7d7;
|
||||
font-size: 25px;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
#game-parameters > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#game-parameters img {
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#how-to-use-container img {
|
||||
max-width: 98%;
|
||||
border: 1px solid #57566a;
|
||||
|
||||
@@ -231,7 +231,7 @@ option {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#step-4 > div {
|
||||
#step-5 > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@@ -242,7 +242,19 @@ option {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
#step-4 > div label {
|
||||
#step-4 {
|
||||
width: 95%;
|
||||
max-width: 25em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#step-4 input {
|
||||
padding: 15px 5px;
|
||||
width: 95%;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#step-5 > div label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
min-width: 15em;
|
||||
border: 2px solid transparent;
|
||||
margin: 0.5em 0;
|
||||
box-shadow: 2px 2px 5px rgb(0 0 0 / 40%);
|
||||
}
|
||||
|
||||
#lobby-players {
|
||||
@@ -125,6 +126,7 @@ h1 {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
transition: background-color 0.2s;
|
||||
max-width: 20em;
|
||||
}
|
||||
|
||||
#game-link > div {
|
||||
@@ -624,22 +626,6 @@ label[for='moderator'] {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
#game-parameters {
|
||||
color: #d7d7d7;
|
||||
font-size: 25px;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
#game-parameters > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#game-parameters img {
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#players-alive-label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
|
||||
28
client/src/styles/join.css
Normal file
28
client/src/styles/join.css
Normal file
@@ -0,0 +1,28 @@
|
||||
#join-game-modal {
|
||||
border-left: 5px solid #b1afcd;
|
||||
animation: entrance 0.5s forwards;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
#game-parameters div:nth-child(1) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#game-code {
|
||||
font-family: "Courier New", monospace;
|
||||
}
|
||||
|
||||
@keyframes entrance {
|
||||
0% {
|
||||
transform: translate(-50%, calc(-50% + 40px));
|
||||
opacity: 0;
|
||||
}
|
||||
95% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@
|
||||
<div id="tracker-step-2" class="creation-step"></div>
|
||||
<div id="tracker-step-3" class="creation-step"></div>
|
||||
<div id="tracker-step-4" class="creation-step"></div>
|
||||
<div id="tracker-step-5" class="creation-step"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="creation-step-container">
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Active Game</title>
|
||||
<meta name="description" content="Join this game of werewolf!">
|
||||
<meta name="description" content="Spectate this game of Werewolf">
|
||||
<meta property="og:title" content="Werewolf Utility - Active Game">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:description" content="Join this game of werewolf!">
|
||||
<meta property="og:description" content="Spectate this game of Werewolf">
|
||||
<meta property="og:image" content="image.png">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
|
||||
53
client/src/views/join.html
Normal file
53
client/src/views/join.html
Normal file
@@ -0,0 +1,53 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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:type" content="website">
|
||||
<meta property="og:description" content="Join this game of werewolf!">
|
||||
<meta property="og:image" content="image.png">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/styles/GLOBAL.css">
|
||||
<link rel="stylesheet" href="/styles/join.css">
|
||||
<link rel="stylesheet" href="/styles/modal.css">
|
||||
<link rel="stylesheet" href="/styles/hamburgers.css">
|
||||
<link rel="preload" href="/webfonts/SignikaNegative-Light.woff2" as="font" type="font/woff2" crossorigin>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mobile-menu-background-overlay"></div>
|
||||
<div id="prompt"></div>
|
||||
<div id="navbar"></div>
|
||||
<div id="change-name-modal-background" class="modal-background"></div>
|
||||
<div tabindex="-1" id="join-game-modal" class="modal">
|
||||
<div id='game-parameters'>
|
||||
<div>
|
||||
<div>Game <span id='game-code'></span></div>
|
||||
</div>
|
||||
<div>
|
||||
<img alt='clock' src='/images/clock.svg'/>
|
||||
<div id='game-time'></div>
|
||||
</div>
|
||||
<div>
|
||||
<img alt='person' src='/images/person.svg'/>
|
||||
<div id='game-player-count'></div>
|
||||
</div>
|
||||
</div>
|
||||
<form id="join-game-form">
|
||||
<div id="join-game-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="Join Game"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<script src="/dist/join-bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,7 +6,8 @@ module.exports = {
|
||||
home: './client/src/scripts/home.js',
|
||||
create: './client/src/scripts/create.js',
|
||||
notFound: './client/src/scripts/notFound.js',
|
||||
howToUse: './client/src/scripts/howToUse.js'
|
||||
howToUse: './client/src/scripts/howToUse.js',
|
||||
join: './client/src/scripts/join.js'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../dist'),
|
||||
|
||||
@@ -6,7 +6,9 @@ module.exports = {
|
||||
game: './client/src/scripts/game.js',
|
||||
home: './client/src/scripts/home.js',
|
||||
create: './client/src/scripts/create.js',
|
||||
notFound: './client/src/scripts/notFound.js'
|
||||
notFound: './client/src/scripts/notFound.js',
|
||||
howToUse: './client/src/scripts/howToUse.js',
|
||||
join: './client/src/scripts/join.js'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../dist'),
|
||||
|
||||
@@ -27,7 +27,7 @@ const corsOptions = process.env.NODE_ENV.trim() === 'development'
|
||||
};
|
||||
|
||||
router.use(cors(corsOptions));
|
||||
//router.options('/:code/players', cors(corsOptions));
|
||||
// router.options('/:code/players', cors(corsOptions));
|
||||
|
||||
if (process.env.NODE_ENV.trim() === 'production') { // in prod, limit clients to creating 5 games per 10 minutes.
|
||||
router.use('/create', apiLimiter);
|
||||
@@ -50,13 +50,14 @@ router.post('/create', function (req, res) {
|
||||
});
|
||||
|
||||
router.get('/:code/availability', function (req, res) {
|
||||
console.log(req.params.code);
|
||||
const availabilityPromise = gameManager.checkAvailability(req.params.code);
|
||||
availabilityPromise.then((result) => {
|
||||
if (result === 404) {
|
||||
res.status(404).send();
|
||||
} else if (result instanceof Error) {
|
||||
res.status(400).send(result.message);
|
||||
} else if (typeof result === 'string') {
|
||||
} else if (typeof result === 'object') {
|
||||
logger.debug(result);
|
||||
res.status(200).send(result);
|
||||
} else {
|
||||
@@ -65,24 +66,44 @@ router.get('/:code/availability', function (req, res) {
|
||||
});
|
||||
});
|
||||
|
||||
// router.patch('/:code/players', function (req, res) {
|
||||
// if (
|
||||
// req.body === null
|
||||
// || req.body.cookie === null
|
||||
// || (typeof req.body.cookie !== 'string' && req.body.cookie !== false)
|
||||
// || (req.body.cookie.length !== globals.USER_SIGNATURE_LENGTH && req.body.cookie !== false)
|
||||
// ) {
|
||||
// res.status(400).send();
|
||||
// }
|
||||
// gameManager.joinGame(req.body.cookie, req.params.code).then((data) => {
|
||||
// res.status(200).send(data);
|
||||
// }).catch((code) => {
|
||||
// res.status(code).send();
|
||||
// });
|
||||
// });
|
||||
router.patch('/:code/players', function (req, res) {
|
||||
console.log(req.body);
|
||||
if (
|
||||
req.body === null
|
||||
|| !validateAccessCode(req.body.accessCode)
|
||||
|| !validateName(req.body.playerName)
|
||||
) {
|
||||
res.status(400).send();
|
||||
} else {
|
||||
const game = gameManager.activeGameRunner.activeGames[req.body.accessCode];
|
||||
if (game) {
|
||||
gameManager.joinGame(game, req.body.playerName).then((data) => {
|
||||
res.status(200).send({ cookie: data, environment: gameManager.environment });
|
||||
}).catch((code) => {
|
||||
res.status(code).send();
|
||||
});
|
||||
} else {
|
||||
res.status(404).send();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/environment', function (req, res) {
|
||||
res.status(200).send(gameManager.environment);
|
||||
});
|
||||
|
||||
function validateName (name) {
|
||||
return typeof name === 'string' && name.length > 0 && name.length <= 30;
|
||||
}
|
||||
|
||||
// function validateCookie (cookie) {
|
||||
// return cookie === null
|
||||
// || (typeof cookie !== 'string' && cookie !== false)
|
||||
// || (cookie.length !== globals.USER_SIGNATURE_LENGTH && cookie !== false);
|
||||
// }
|
||||
|
||||
function validateAccessCode (accessCode) {
|
||||
return /^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH;
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const globals = {
|
||||
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
|
||||
ACCESS_CODE_CHAR_POOL: 'BCDFGHJKLMNPQRSTVWXYZ0123456789',
|
||||
ACCESS_CODE_LENGTH: 6,
|
||||
CLOCK_TICK_INTERVAL_MILLIS: 10,
|
||||
STALE_GAME_HOURS: 12,
|
||||
|
||||
@@ -38,7 +38,6 @@ gameManager.namespace = inGameSocketServer;
|
||||
inGameSocketServer.on('connection', function (socket) {
|
||||
socket.on('disconnecting', (reason) => {
|
||||
logger.trace('client socket disconnecting because: ' + reason);
|
||||
gameManager.removeClientFromLobbyIfApplicable(socket);
|
||||
});
|
||||
gameManager.addGameSocketHandlers(inGameSocketServer, socket);
|
||||
});
|
||||
|
||||
@@ -157,7 +157,7 @@ class GameManager {
|
||||
};
|
||||
|
||||
createGame = (gameParams) => {
|
||||
const expectedKeys = ['deck', 'hasTimer', 'timerParams'];
|
||||
const expectedKeys = ['deck', 'hasTimer', 'timerParams', 'moderatorName'];
|
||||
if (typeof gameParams !== 'object'
|
||||
|| expectedKeys.some((key) => !Object.keys(gameParams).includes(key))
|
||||
) {
|
||||
@@ -167,7 +167,8 @@ class GameManager {
|
||||
// to avoid excessive memory build-up, every time a game is created, check for and purge any stale games.
|
||||
pruneStaleGames(this.activeGameRunner.activeGames, this.activeGameRunner.timerThreads, this.logger);
|
||||
const newAccessCode = this.generateAccessCode();
|
||||
const moderator = initializeModerator(UsernameGenerator.generate(), gameParams.hasDedicatedModerator);
|
||||
const moderator = initializeModerator(gameParams.moderatorName, gameParams.hasDedicatedModerator);
|
||||
moderator.assigned = true;
|
||||
if (gameParams.timerParams !== null) {
|
||||
gameParams.timerParams.paused = false;
|
||||
}
|
||||
@@ -181,18 +182,18 @@ class GameManager {
|
||||
gameParams.timerParams
|
||||
);
|
||||
this.activeGameRunner.activeGames[newAccessCode].createTime = new Date().toJSON();
|
||||
return Promise.resolve(newAccessCode);
|
||||
return Promise.resolve({ accessCode: newAccessCode, cookie: moderator.cookie, environment: this.environment });
|
||||
}
|
||||
};
|
||||
|
||||
checkAvailability = (code) => {
|
||||
const game = this.activeGameRunner.activeGames[code];
|
||||
const game = this.activeGameRunner.activeGames[code.toUpperCase()];
|
||||
if (game) {
|
||||
const unassignedPerson = game.people.find((person) => person.assigned === false);
|
||||
if (!unassignedPerson) {
|
||||
return Promise.resolve(new Error(globals.ERROR_MESSAGE.GAME_IS_FULL));
|
||||
} else {
|
||||
return Promise.resolve(code);
|
||||
return Promise.resolve({ accessCode: code, playerCount: getGameSize(game.deck), timerParams: game.timerParams });
|
||||
}
|
||||
} else {
|
||||
return Promise.resolve(404);
|
||||
@@ -253,30 +254,34 @@ class GameManager {
|
||||
}
|
||||
};
|
||||
|
||||
joinGame = (game) => {
|
||||
joinGame = (game, name) => {
|
||||
if (isNameTaken(game, name)) {
|
||||
return Promise.reject(400);
|
||||
}
|
||||
const unassignedPerson = game.moderator.assigned === false
|
||||
? game.moderator
|
||||
: game.people.find((person) => person.assigned === false);
|
||||
if (unassignedPerson) {
|
||||
this.logger.trace('request from client to join game. Assigning: ' + unassignedPerson.name);
|
||||
unassignedPerson.assigned = true;
|
||||
unassignedPerson.name = name;
|
||||
game.isFull = isGameFull(game);
|
||||
this.namespace.in(game.accessCode).emit(
|
||||
globals.EVENTS.PLAYER_JOINED,
|
||||
GameStateCurator.mapPerson(unassignedPerson),
|
||||
game.isFull
|
||||
);
|
||||
return unassignedPerson;
|
||||
return Promise.resolve(unassignedPerson.cookie);
|
||||
} else { // if the game is full, make them a spectator.
|
||||
const spectator = new Person(
|
||||
createRandomId(),
|
||||
createRandomId(),
|
||||
UsernameGenerator.generate(),
|
||||
name,
|
||||
globals.USER_TYPES.SPECTATOR
|
||||
);
|
||||
this.logger.trace('new spectator: ' + spectator.name);
|
||||
game.spectators.push(spectator);
|
||||
return spectator;
|
||||
return Promise.resolve(spectator.cookie);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -295,13 +300,7 @@ class GameManager {
|
||||
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson, gameRunner, clientSocket, logger));
|
||||
}
|
||||
} else {
|
||||
const namespaceSockets = await namespace.in(accessCode).fetchSockets();
|
||||
if (!namespaceSockets.find((namespaceSocket) => namespaceSocket.id === clientSocket.id)) {
|
||||
let newlyAssignedPerson = this.joinGame(game);
|
||||
clientSocket.join(accessCode);
|
||||
newlyAssignedPerson.socketId = clientSocket.id;
|
||||
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, newlyAssignedPerson, gameRunner, clientSocket, logger));
|
||||
}
|
||||
rejectClientRequestForGameState(ackFn);
|
||||
}
|
||||
} else {
|
||||
rejectClientRequestForGameState(ackFn);
|
||||
@@ -456,6 +455,15 @@ function pruneStaleGames (activeGames, timerThreads, logger) {
|
||||
}
|
||||
}
|
||||
|
||||
function getGameSize (cards) {
|
||||
let quantity = 0;
|
||||
for (const card of cards) {
|
||||
quantity += card.quantity;
|
||||
}
|
||||
|
||||
return quantity;
|
||||
}
|
||||
|
||||
class Singleton {
|
||||
constructor (logger, environment) {
|
||||
if (!Singleton.instance) {
|
||||
|
||||
@@ -3,6 +3,7 @@ const http = require('http');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const secure = require('express-force-https');
|
||||
|
||||
const ServerBootstrapper = {
|
||||
processCLIArgs: () => {
|
||||
@@ -55,6 +56,7 @@ const ServerBootstrapper = {
|
||||
}
|
||||
} else {
|
||||
logger.warn('starting main in PRODUCTION mode. This should not be used for local development.');
|
||||
app.use(secure);
|
||||
main = http.createServer(app);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ router.get('/create', function (request, response) {
|
||||
response.sendFile(path.join(__dirname, '../../client/src/views/create.html'));
|
||||
});
|
||||
|
||||
router.get('/join/:code', function (request, response) {
|
||||
response.sendFile(path.join(__dirname, '../../client/src/views/join.html'));
|
||||
});
|
||||
|
||||
router.get('/how-to-use', function (request, response) {
|
||||
response.sendFile(path.join(__dirname, '../../client/src/views/how-to-use.html'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user