mirror of
https://github.com/AlecM33/Werewolf.git
synced 2025-12-26 07:47:50 +01:00
clean up state management
This commit is contained in:
@@ -18,7 +18,8 @@ export const globals = {
|
||||
REVEAL_PLAYER: 'revealPlayer',
|
||||
TRANSFER_MODERATOR: 'transferModerator',
|
||||
CHANGE_NAME: 'changeName',
|
||||
END_GAME: 'endGame'
|
||||
END_GAME: 'endGame',
|
||||
FETCH_IN_PROGRESS_STATE: 'fetchInitialInProgressState'
|
||||
},
|
||||
STATUS: {
|
||||
LOBBY: "lobby",
|
||||
@@ -38,7 +39,9 @@ export const globals = {
|
||||
START_TIMER: "startTimer",
|
||||
KILL_PLAYER: "killPlayer",
|
||||
REVEAL_PLAYER: 'revealPlayer',
|
||||
CHANGE_NAME: 'changeName'
|
||||
CHANGE_NAME: 'changeName',
|
||||
START_GAME: 'startGame',
|
||||
PLAYER_LEFT: 'playerLeft'
|
||||
},
|
||||
USER_TYPES: {
|
||||
MODERATOR: "moderator",
|
||||
|
||||
@@ -10,6 +10,12 @@ export class GameStateRenderer {
|
||||
this.killPlayerHandlers = {};
|
||||
this.revealRoleHandlers = {};
|
||||
this.transferModHandlers = {};
|
||||
this.startGameHandler = (e) => {
|
||||
e.preventDefault();
|
||||
if (confirm("Start the game and deal roles?")) {
|
||||
socket.emit(globals.COMMANDS.START_GAME, this.stateBucket.currentGameState.accessCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderLobbyPlayers() {
|
||||
@@ -465,16 +471,18 @@ function removeExistingPlayerElements(killPlayerHandlers, revealRoleHandlers) {
|
||||
}
|
||||
|
||||
function createEndGamePromptComponent(socket, stateBucket) {
|
||||
let div = document.createElement("div");
|
||||
div.innerHTML = templates.END_GAME_PROMPT;
|
||||
div.querySelector("#end-game-button").addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (confirm("End the game?")) {
|
||||
socket.emit(
|
||||
globals.COMMANDS.END_GAME,
|
||||
stateBucket.currentGameState.accessCode
|
||||
);
|
||||
}
|
||||
});
|
||||
document.getElementById("game-content").appendChild(div);
|
||||
if (document.querySelector("#end-game-prompt") === null) {
|
||||
let div = document.createElement("div");
|
||||
div.innerHTML = templates.END_GAME_PROMPT;
|
||||
div.querySelector("#end-game-button").addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (confirm("End the game?")) {
|
||||
socket.emit(
|
||||
globals.COMMANDS.END_GAME,
|
||||
stateBucket.currentGameState.accessCode
|
||||
);
|
||||
}
|
||||
});
|
||||
document.getElementById("game-content").appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ function getNavbarLinks (page=null, device) {
|
||||
'</a>' +
|
||||
'<a class="' + linkClass + '" href="/">Home</a>' +
|
||||
'<a class="' + linkClass + '" href="/create">Create</a>' +
|
||||
'<a class="' + linkClass + '" href="/">How to Use</a>' +
|
||||
'<a class="' + linkClass + '" href="/how-to-use">How to Use</a>' +
|
||||
'<a class="' + linkClass + ' "href="mailto:play.werewolf.contact@gmail.com?Subject=Werewolf App" target="_top">Contact</a>' +
|
||||
'<a class="' + linkClass + '" href="https://www.buymeacoffee.com/alecm33">Support the App</a>'
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
*/
|
||||
export const stateBucket = {
|
||||
currentGameState: null,
|
||||
timerWorker: null
|
||||
timerWorker: null,
|
||||
gameStateRequestInFlight: false
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export const templates = {
|
||||
"<p id='role-description'></p>" +
|
||||
"</div>" +
|
||||
"<div id='game-role-back'>" +
|
||||
"<h4>Click to reveal your role</h4>" +
|
||||
"<h4>Click to show your role</h4>" +
|
||||
"<p>(click again to hide)</p>" +
|
||||
"</div>" +
|
||||
"<div id='game-people-container'>" +
|
||||
@@ -138,7 +138,7 @@ export const templates = {
|
||||
"<p id='role-description'></p>" +
|
||||
"</div>" +
|
||||
"<div id='game-role-back'>" +
|
||||
"<h4>Click to reveal your role</h4>" +
|
||||
"<h4>Click to show your role</h4>" +
|
||||
"<p>(click again to hide)</p>" +
|
||||
"</div>" +
|
||||
"<div id='game-people-container'>" +
|
||||
|
||||
@@ -13,16 +13,21 @@ const game = () => {
|
||||
injectNavbar();
|
||||
let timerWorker;
|
||||
const socket = io('/in-game');
|
||||
stateBucket.gameStateRequestInFlight = false;
|
||||
socket.on('disconnect', () => {
|
||||
stateBucket.gameStateRequestInFlight = false;
|
||||
if (timerWorker) {
|
||||
timerWorker.terminate();
|
||||
}
|
||||
toast('Disconnected. Attempting reconnect...', 'error', true, false);
|
||||
});
|
||||
socket.on('connect', () => {
|
||||
socket.emit(globals.COMMANDS.GET_ENVIRONMENT, function(returnedEnvironment) {
|
||||
timerWorker = new Worker(new URL('../modules/Timer.js', import.meta.url));
|
||||
prepareGamePage(returnedEnvironment, socket, timerWorker);
|
||||
console.log('fired connect event');
|
||||
socket.emit(globals.COMMANDS.GET_ENVIRONMENT, function (returnedEnvironment) {
|
||||
if (!stateBucket.gameStateRequestInFlight) {
|
||||
timerWorker = new Worker(new URL('../modules/Timer.js', import.meta.url));
|
||||
prepareGamePage(returnedEnvironment, socket, timerWorker);
|
||||
}
|
||||
});
|
||||
})
|
||||
};
|
||||
@@ -32,46 +37,51 @@ function prepareGamePage(environment, socket, timerWorker) {
|
||||
const splitUrl = window.location.href.split('/game/');
|
||||
const accessCode = splitUrl[1];
|
||||
if (/^[a-zA-Z0-9]+$/.test(accessCode) && accessCode.length === globals.ACCESS_CODE_LENGTH) {
|
||||
stateBucket.gameStateRequestInFlight = true;
|
||||
socket.emit(globals.COMMANDS.FETCH_GAME_STATE, accessCode, userId, function (gameState) {
|
||||
stateBucket.gameStateRequestInFlight = false;
|
||||
stateBucket.currentGameState = gameState;
|
||||
document.querySelector('.spinner-container')?.remove();
|
||||
document.querySelector('.spinner-background')?.remove();
|
||||
|
||||
if (gameState === null) {
|
||||
window.location = '/not-found?reason=' + encodeURIComponent('game-not-found');
|
||||
}
|
||||
} else {
|
||||
document.getElementById("game-content").innerHTML = templates.INITIAL_GAME_DOM;
|
||||
toast('You are connected.', 'success', true, true, 2);
|
||||
userId = gameState.client.cookie;
|
||||
UserUtility.setAnonymousUserId(userId, environment);
|
||||
let gameStateRenderer = new GameStateRenderer(stateBucket, socket);
|
||||
let gameTimerManager;
|
||||
if (stateBucket.currentGameState.timerParams) {
|
||||
gameTimerManager = new GameTimerManager(stateBucket, socket);
|
||||
}
|
||||
initializeGame(stateBucket, socket, timerWorker, userId, gameStateRenderer, gameTimerManager);
|
||||
|
||||
document.getElementById("game-content").innerHTML = templates.INITIAL_GAME_DOM;
|
||||
toast('You are connected.', 'success', true, true, 2);
|
||||
userId = gameState.client.cookie;
|
||||
UserUtility.setAnonymousUserId(userId, environment);
|
||||
let gameStateRenderer = new GameStateRenderer(stateBucket, socket);
|
||||
let gameTimerManager;
|
||||
if (stateBucket.currentGameState.timerParams) {
|
||||
gameTimerManager = new GameTimerManager(stateBucket, socket);
|
||||
}
|
||||
initializeGame(stateBucket, socket, timerWorker, userId, gameStateRenderer, gameTimerManager);
|
||||
|
||||
if (!gameState.client.hasEnteredName) {
|
||||
document.getElementById("prompt").innerHTML = templates.NAME_CHANGE_MODAL;
|
||||
document.getElementById("change-name-form").onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
let name = document.getElementById("player-new-name").value;
|
||||
if (validateName(name)) {
|
||||
socket.emit(globals.COMMANDS.CHANGE_NAME, gameState.accessCode, { name: name, personId: gameState.client.id }, (result) => {
|
||||
switch (result) {
|
||||
case "taken":
|
||||
toast('This name is already taken.', 'error', true, true, 8);
|
||||
break;
|
||||
case "changed":
|
||||
ModalManager.dispelModal("change-name-modal", "change-name-modal-background")
|
||||
toast('Name set.', 'success', true, true, 5);
|
||||
propagateNameChange(stateBucket.currentGameState, name, stateBucket.currentGameState.client.id);
|
||||
processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
toast("Name must be between 1 and 30 characters.", 'error', true, true, 8);
|
||||
if (!gameState.client.hasEnteredName) {
|
||||
document.getElementById("prompt").innerHTML = templates.NAME_CHANGE_MODAL;
|
||||
document.getElementById("change-name-form").onsubmit = (e) => {
|
||||
e.preventDefault();
|
||||
let name = document.getElementById("player-new-name").value;
|
||||
if (validateName(name)) {
|
||||
socket.emit(globals.COMMANDS.CHANGE_NAME, gameState.accessCode, {
|
||||
name: name,
|
||||
personId: gameState.client.id
|
||||
}, (result) => {
|
||||
switch (result) {
|
||||
case "taken":
|
||||
toast('This name is already taken.', 'error', true, true, 8);
|
||||
break;
|
||||
case "changed":
|
||||
ModalManager.dispelModal("change-name-modal", "change-name-modal-background")
|
||||
toast('Name set.', 'success', true, true, 5);
|
||||
propagateNameChange(stateBucket.currentGameState, name, stateBucket.currentGameState.client.id);
|
||||
processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
toast("Name must be between 1 and 30 characters.", 'error', true, true, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,10 +96,12 @@ function initializeGame(stateBucket, socket, timerWorker, userId, gameStateRende
|
||||
processGameState(stateBucket.currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
}
|
||||
|
||||
function processGameState (currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker) {
|
||||
function processGameState (currentGameState, userId, socket, gameStateRenderer, gameTimerManager, timerWorker, refreshPrompt=true) {
|
||||
displayClientInfo(currentGameState.client.name, currentGameState.client.userType);
|
||||
document.querySelector("#start-game-prompt")?.remove();
|
||||
document.querySelector("#end-game-prompt")?.remove();
|
||||
if (refreshPrompt) {
|
||||
removeStartGameFunctionalityIfPresent(gameStateRenderer);
|
||||
document.querySelector("#end-game-prompt")?.remove();
|
||||
}
|
||||
switch (currentGameState.status) {
|
||||
case globals.STATUS.LOBBY:
|
||||
document.getElementById("game-state-container").innerHTML = templates.LOBBY;
|
||||
@@ -101,8 +113,9 @@ function processGameState (currentGameState, userId, socket, gameStateRenderer,
|
||||
currentGameState.client.userType === globals.USER_TYPES.MODERATOR
|
||||
|| currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|
||||
)
|
||||
&& refreshPrompt
|
||||
) {
|
||||
displayStartGamePromptForModerators(currentGameState, socket);
|
||||
displayStartGamePromptForModerators(currentGameState, gameStateRenderer);
|
||||
}
|
||||
break;
|
||||
case globals.STATUS.IN_PROGRESS:
|
||||
@@ -174,21 +187,69 @@ function setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWo
|
||||
|| stateBucket.currentGameState.client.userType === globals.USER_TYPES.TEMPORARY_MODERATOR
|
||||
)
|
||||
) {
|
||||
displayStartGamePromptForModerators(stateBucket.currentGameState, socket);
|
||||
displayStartGamePromptForModerators(stateBucket.currentGameState, gameStateRenderer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!socket.hasListeners(globals.EVENTS.PLAYER_LEFT)) {
|
||||
socket.on(globals.EVENTS.PLAYER_LEFT, (player) => {
|
||||
removeStartGameFunctionalityIfPresent(gameStateRenderer);
|
||||
toast(player.name + " has left!", "error", false, true, 3);
|
||||
let index = stateBucket.currentGameState.people.findIndex(person => person.id === player.id);
|
||||
if (index >= 0) {
|
||||
stateBucket.currentGameState.people.splice(
|
||||
index,
|
||||
1
|
||||
);
|
||||
gameStateRenderer.renderLobbyPlayers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!socket.hasListeners(globals.EVENTS.START_GAME)) {
|
||||
socket.on(globals.EVENTS.START_GAME, () => {
|
||||
socket.emit(
|
||||
globals.COMMANDS.FETCH_IN_PROGRESS_STATE,
|
||||
stateBucket.currentGameState.accessCode,
|
||||
stateBucket.currentGameState.client.cookie,
|
||||
function (gameState) {
|
||||
stateBucket.gameStateRequestInFlight = false;
|
||||
stateBucket.currentGameState = gameState;
|
||||
processGameState(
|
||||
stateBucket.currentGameState,
|
||||
gameState.client.cookie,
|
||||
socket,
|
||||
gameStateRenderer,
|
||||
gameTimerManager,
|
||||
timerWorker
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
if (!socket.hasListeners(globals.EVENTS.SYNC_GAME_STATE)) {
|
||||
socket.on(globals.EVENTS.SYNC_GAME_STATE, () => {
|
||||
socket.emit(
|
||||
globals.COMMANDS.FETCH_GAME_STATE,
|
||||
stateBucket.currentGameState.accessCode,
|
||||
stateBucket.currentGameState.client.cookie,
|
||||
function (gameState) {
|
||||
stateBucket.currentGameState = gameState;
|
||||
processGameState(stateBucket.currentGameState, gameState.client.cookie, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
}
|
||||
);
|
||||
if (!stateBucket.gameStateRequestInFlight) {
|
||||
stateBucket.gameStateRequestInFlight = true;
|
||||
socket.emit(
|
||||
globals.COMMANDS.FETCH_IN_PROGRESS_STATE,
|
||||
stateBucket.currentGameState.accessCode,
|
||||
stateBucket.currentGameState.client.cookie,
|
||||
function (gameState) {
|
||||
stateBucket.gameStateRequestInFlight = false;
|
||||
stateBucket.currentGameState = gameState;
|
||||
processGameState(
|
||||
stateBucket.currentGameState,
|
||||
gameState.client.cookie,
|
||||
socket,
|
||||
gameStateRenderer,
|
||||
gameTimerManager,
|
||||
timerWorker
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -255,7 +316,15 @@ function setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWo
|
||||
socket.on(globals.EVENTS.CHANGE_NAME, (personId, name) => {
|
||||
propagateNameChange(stateBucket.currentGameState, name, personId);
|
||||
updateDOMWithNameChange(stateBucket.currentGameState, gameStateRenderer);
|
||||
processGameState(stateBucket.currentGameState, stateBucket.currentGameState.client.cookie, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
processGameState(
|
||||
stateBucket.currentGameState,
|
||||
stateBucket.currentGameState.client.cookie,
|
||||
socket,
|
||||
gameStateRenderer,
|
||||
gameTimerManager,
|
||||
timerWorker,
|
||||
false
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -263,21 +332,22 @@ function setClientSocketHandlers(stateBucket, gameStateRenderer, socket, timerWo
|
||||
socket.on(globals.COMMANDS.END_GAME, (people) => {
|
||||
stateBucket.currentGameState.people = people;
|
||||
stateBucket.currentGameState.status = globals.STATUS.ENDED;
|
||||
processGameState(stateBucket.currentGameState, stateBucket.currentGameState.client.cookie, socket, gameStateRenderer, gameTimerManager, timerWorker);
|
||||
processGameState(
|
||||
stateBucket.currentGameState,
|
||||
stateBucket.currentGameState.client.cookie,
|
||||
socket,
|
||||
gameStateRenderer,
|
||||
gameTimerManager,
|
||||
timerWorker
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function displayStartGamePromptForModerators(gameState, socket) {
|
||||
function displayStartGamePromptForModerators(gameState, gameStateRenderer) {
|
||||
let div = document.createElement("div");
|
||||
div.innerHTML = templates.START_GAME_PROMPT;
|
||||
div.querySelector('#start-game-button').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (confirm("Start the game and deal roles?")) {
|
||||
socket.emit(globals.COMMANDS.START_GAME, gameState.accessCode);
|
||||
}
|
||||
|
||||
});
|
||||
div.querySelector('#start-game-button').addEventListener('click', gameStateRenderer.startGameHandler);
|
||||
document.body.appendChild(div);
|
||||
}
|
||||
|
||||
@@ -296,8 +366,15 @@ 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();
|
||||
}
|
||||
|
||||
function propagateNameChange(gameState, name, personId) {
|
||||
gameState.client.name = name;
|
||||
if (gameState.client.id === personId) {
|
||||
gameState.client.name = name;
|
||||
}
|
||||
let matchingPerson = gameState.people.find((person) => person.id === personId);
|
||||
if (matchingPerson) {
|
||||
matchingPerson.name = name;
|
||||
@@ -314,20 +391,24 @@ function propagateNameChange(gameState, name, personId) {
|
||||
}
|
||||
|
||||
function updateDOMWithNameChange(gameState, gameStateRenderer) {
|
||||
switch (gameState.client.userType) {
|
||||
case globals.USER_TYPES.PLAYER:
|
||||
case globals.USER_TYPES.KILLED_PLAYER:
|
||||
case globals.USER_TYPES.SPECTATOR:
|
||||
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
|
||||
break;
|
||||
case globals.USER_TYPES.MODERATOR:
|
||||
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(gameState.status === globals.STATUS.ENDED);
|
||||
break;
|
||||
case globals.USER_TYPES.TEMPORARY_MODERATOR:
|
||||
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (gameState.status === globals.STATUS.IN_PROGRESS) {
|
||||
switch (gameState.client.userType) {
|
||||
case globals.USER_TYPES.PLAYER:
|
||||
case globals.USER_TYPES.KILLED_PLAYER:
|
||||
case globals.USER_TYPES.SPECTATOR:
|
||||
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(false);
|
||||
break;
|
||||
case globals.USER_TYPES.MODERATOR:
|
||||
gameStateRenderer.renderPlayersWithRoleAndAlignmentInfo(gameState.status === globals.STATUS.ENDED);
|
||||
break;
|
||||
case globals.USER_TYPES.TEMPORARY_MODERATOR:
|
||||
gameStateRenderer.renderPlayersWithNoRoleInformationUnlessRevealed(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
gameStateRenderer.renderLobbyPlayers();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
9
client/src/scripts/howToUse.js
Normal file
9
client/src/scripts/howToUse.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { injectNavbar } from "../modules/Navbar.js";
|
||||
|
||||
const howToUse = () => { injectNavbar(); };
|
||||
|
||||
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
module.exports = howToUse;
|
||||
} else {
|
||||
howToUse();
|
||||
}
|
||||
@@ -221,6 +221,16 @@ input {
|
||||
z-index: 53000;
|
||||
}
|
||||
|
||||
#how-to-use-container {
|
||||
color: #d7d7d7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
width: 95%;
|
||||
max-width: 70em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#desktop-links > a:nth-child(1), #mobile-links a:nth-child(1) {
|
||||
margin: 0 0.5em;
|
||||
width: 50px;
|
||||
|
||||
47
client/src/views/how-to-use.html
Normal file
47
client/src/views/how-to-use.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Create A Game</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:description" content="How to use this app.">
|
||||
<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/create.css">
|
||||
<link rel="stylesheet" href="/styles/modal.css">
|
||||
<link rel="stylesheet" href="/styles/hamburgers.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="mobile-menu-background-overlay"></div>
|
||||
<div id="navbar"></div>
|
||||
<div id="how-to-use-container">
|
||||
<h1 class="how-to-use-header">Purpose of the Application</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 cards with them. You can use a deck of regular
|
||||
playing cards, but it can be difficult for players to remember which card signifies which role, especially if
|
||||
you want to build a crazy game with many different roles. Even when people are together and have cards, there's
|
||||
information that would be great to centralize for everyone - a timer, role descriptions, and the in/out status of
|
||||
players. This app attempts to provide the utilities necessary to run Werewolf with all the different roles you want,
|
||||
wherever you can access the internet.
|
||||
</div>
|
||||
<h1 class="">Creating a Game</h1>
|
||||
<div class="how-to-use-section">
|
||||
Creating a game through the app is a 4-step process:
|
||||
<br>
|
||||
<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 "dedicated
|
||||
moderator" option.
|
||||
</div>
|
||||
</div>
|
||||
<script src="/dist/howToUse-bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,7 +5,8 @@ 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'
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../dist'),
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"start:dev:no-hot-reload": "NODE_ENV=development && node server/main.js",
|
||||
"start:dev:windows": "SET NODE_ENV=development && nodemon server/main.js",
|
||||
"start:dev:windows:no-hot-reload": "SET NODE_ENV=development && node server/main.js",
|
||||
"start": "NODE_ENV=production node server/main.js -- loglevel=debug port=8080",
|
||||
"start": "NODE_ENV=production node server/main.js -- loglevel=trace port=8080",
|
||||
"start:windows": "SET NODE_ENV=production && node server/main.js -- loglevel=warn port=8080",
|
||||
"test": "jasmine"
|
||||
},
|
||||
|
||||
@@ -14,7 +14,8 @@ const globals = {
|
||||
REVEAL_PLAYER: 'revealPlayer',
|
||||
TRANSFER_MODERATOR: 'transferModerator',
|
||||
CHANGE_NAME: 'changeName',
|
||||
END_GAME: 'endGame'
|
||||
END_GAME: 'endGame',
|
||||
FETCH_IN_PROGRESS_STATE: 'fetchInitialInProgressState'
|
||||
},
|
||||
MESSAGES: {
|
||||
ENTER_NAME: "Client must enter name."
|
||||
@@ -38,6 +39,7 @@ const globals = {
|
||||
},
|
||||
EVENTS: {
|
||||
PLAYER_JOINED: "playerJoined",
|
||||
PLAYER_LEFT: "playerLeft",
|
||||
SYNC_GAME_STATE: "syncGameState"
|
||||
},
|
||||
ENVIRONMENT: {
|
||||
|
||||
@@ -23,6 +23,10 @@ app.set('port', args.port);
|
||||
const inGameSocketServer = ServerBootstrapper.createSocketServer(main, app, args.port);
|
||||
|
||||
inGameSocketServer.on('connection', function (socket) {
|
||||
socket.on("disconnecting", (reason) => {
|
||||
logger.trace('client socket disconnecting because: ' + reason)
|
||||
gameManager.removeClientFromLobbyIfApplicable(socket);
|
||||
});
|
||||
gameManager.addGameSocketHandlers(inGameSocketServer, socket);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ class GameManager {
|
||||
this.environment = environment;
|
||||
this.activeGameRunner = new ActiveGameRunner(logger).getInstance();
|
||||
this.namespace = null;
|
||||
//this.gameSocketUtility = GameSocketUtility;
|
||||
}
|
||||
|
||||
addGameSocketHandlers = (namespace, socket) => {
|
||||
@@ -29,19 +28,36 @@ class GameManager {
|
||||
);
|
||||
});
|
||||
|
||||
/* this event handler will call handleRequestForGameState() with the 'handleNoMatch' arg as false - only
|
||||
connections that match a participant in the game at that time will have the game state sent to them.
|
||||
*/
|
||||
socket.on(globals.CLIENT_COMMANDS.FETCH_IN_PROGRESS_STATE, (accessCode, personId, ackFn) => {
|
||||
this.logger.trace('request for game state for accessCode ' + accessCode + ', person ' + personId);
|
||||
this.handleRequestForGameState(
|
||||
this.namespace,
|
||||
this.logger,
|
||||
this.activeGameRunner,
|
||||
accessCode,
|
||||
personId,
|
||||
ackFn,
|
||||
socket,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
socket.on(globals.CLIENT_COMMANDS.GET_ENVIRONMENT, (ackFn) => {
|
||||
ackFn(this.environment);
|
||||
});
|
||||
|
||||
socket.on(globals.CLIENT_COMMANDS.START_GAME, (accessCode) => {
|
||||
let game = this.activeGameRunner.activeGames[accessCode];
|
||||
if (game) {
|
||||
if (game && game.isFull) {
|
||||
game.status = globals.STATUS.IN_PROGRESS;
|
||||
namespace.in(accessCode).emit(globals.EVENTS.SYNC_GAME_STATE);
|
||||
if (game.hasTimer) {
|
||||
game.timerParams.paused = true;
|
||||
this.activeGameRunner.runGame(game, namespace);
|
||||
}
|
||||
namespace.in(accessCode).emit(globals.CLIENT_COMMANDS.START_GAME);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -142,7 +158,7 @@ class GameManager {
|
||||
ackFn("changed");
|
||||
person.name = data.name.trim();
|
||||
person.hasEnteredName = true;
|
||||
socket.to(accessCode).emit(globals.EVENTS.SYNC_GAME_STATE);
|
||||
namespace.in(accessCode).emit(globals.CLIENT_COMMANDS.CHANGE_NAME, person.id, person.name);
|
||||
} else {
|
||||
ackFn("taken");
|
||||
}
|
||||
@@ -261,14 +277,14 @@ class GameManager {
|
||||
}
|
||||
|
||||
|
||||
handleRequestForGameState = (namespace, logger, gameRunner, accessCode, personCookie, ackFn, socket) => {
|
||||
handleRequestForGameState = (namespace, logger, gameRunner, accessCode, personCookie, ackFn, socket, handleNoMatch=true) => {
|
||||
const game = gameRunner.activeGames[accessCode];
|
||||
if (game) {
|
||||
let matchingPerson = game.people.find((person) => person.cookie === personCookie);
|
||||
if (!matchingPerson) {
|
||||
matchingPerson = game.spectators.find((spectator) => spectator.cookie === personCookie);
|
||||
}
|
||||
if (game.moderator.cookie === personCookie) {
|
||||
if (!matchingPerson && game.moderator.cookie === personCookie) {
|
||||
matchingPerson = game.moderator;
|
||||
}
|
||||
if (matchingPerson) {
|
||||
@@ -281,7 +297,7 @@ class GameManager {
|
||||
matchingPerson.socketId = socket.id;
|
||||
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson, gameRunner, socket, logger));
|
||||
}
|
||||
} else {
|
||||
} else if (handleNoMatch) {
|
||||
this.handleRequestFromNonMatchingPerson(game, socket, gameRunner, ackFn, logger);
|
||||
}
|
||||
} else {
|
||||
@@ -291,7 +307,7 @@ class GameManager {
|
||||
}
|
||||
|
||||
handleRequestFromNonMatchingPerson = (game, socket, gameRunner, ackFn, logger) => {
|
||||
let personWithMatchingSocketId = findPersonWithMatchingSocketId(game.people, socket.id);
|
||||
let personWithMatchingSocketId = findPersonWithMatchingSocketId(game, socket.id);
|
||||
if (personWithMatchingSocketId) {
|
||||
logger.trace("matching person found whose cookie got cleared after establishing a connection to the room: " + personWithMatchingSocketId.name);
|
||||
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, personWithMatchingSocketId, gameRunner, socket, logger));
|
||||
@@ -308,7 +324,7 @@ class GameManager {
|
||||
game.isFull = isFull;
|
||||
socket.to(game.accessCode).emit(
|
||||
globals.EVENTS.PLAYER_JOINED,
|
||||
{name: unassignedPerson.name, userType: unassignedPerson.userType},
|
||||
GameStateCurator.mapPerson(unassignedPerson),
|
||||
isFull
|
||||
);
|
||||
} else { // if the game is full, make them a spectator.
|
||||
@@ -326,6 +342,30 @@ class GameManager {
|
||||
}
|
||||
}
|
||||
|
||||
removeClientFromLobbyIfApplicable(socket) {
|
||||
socket.rooms.forEach((room) => {
|
||||
if (this.activeGameRunner.activeGames[room]) {
|
||||
this.logger.trace('disconnected socket is in a game');
|
||||
let game = this.activeGameRunner.activeGames[room];
|
||||
if (game.status === globals.STATUS.LOBBY) {
|
||||
let matchingPlayer = findPlayerBySocketId(game.people, socket.id);
|
||||
if (matchingPlayer) {
|
||||
this.logger.trace("un-assigning disconnected player: " + matchingPlayer.name);
|
||||
matchingPlayer.assigned = false;
|
||||
matchingPlayer.socketId = null;
|
||||
matchingPlayer.cookie = createRandomId();
|
||||
matchingPlayer.hasEnteredName = false;
|
||||
socket.to(game.accessCode).emit(
|
||||
globals.EVENTS.PLAYER_LEFT,
|
||||
GameStateCurator.mapPerson(matchingPlayer)
|
||||
);
|
||||
game.isFull = isGameFull(game);
|
||||
matchingPlayer.name = UsernameGenerator.generate();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getRandomInt (max) {
|
||||
@@ -403,8 +443,19 @@ function rejectClientRequestForGameState(acknowledgementFunction) {
|
||||
return acknowledgementFunction(null);
|
||||
}
|
||||
|
||||
function findPersonWithMatchingSocketId(people, socketId) {
|
||||
return people.find((person) => person.socketId === socketId);
|
||||
function findPersonWithMatchingSocketId(game, socketId) {
|
||||
let person = game.people.find((person) => person.socketId === socketId);
|
||||
if (!person) {
|
||||
person = game.spectators.find((spectator) => spectator.socketId === socketId);
|
||||
}
|
||||
if (!person && game.moderator.socketId === socketId) {
|
||||
person = game.moderator;
|
||||
}
|
||||
return person;
|
||||
}
|
||||
|
||||
function findPlayerBySocketId(people, socketId) {
|
||||
return people.find((person) => person.socketId === socketId && person.userType === globals.USER_TYPES.PLAYER);
|
||||
}
|
||||
|
||||
function isGameFull(game) {
|
||||
|
||||
@@ -24,6 +24,21 @@ const GameStateCurator = {
|
||||
out: person.out,
|
||||
revealed: person.revealed
|
||||
}));
|
||||
},
|
||||
mapPerson: (person) => {
|
||||
if (person.revealed) {
|
||||
return {
|
||||
name: person.name,
|
||||
id: person.id,
|
||||
userType: person.userType,
|
||||
out: person.out,
|
||||
revealed: person.revealed,
|
||||
gameRole: person.gameRole,
|
||||
alignment: person.alignment
|
||||
};
|
||||
} else {
|
||||
return { name: person.name, id: person.id, userType: person.userType, out: person.out, revealed: person.revealed };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +63,7 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) {
|
||||
let state = {
|
||||
accessCode: game.accessCode,
|
||||
status: game.status,
|
||||
moderator: mapPerson(game.moderator),
|
||||
moderator: GameStateCurator.mapPerson(game.moderator),
|
||||
client: client,
|
||||
deck: game.deck,
|
||||
people: game.people
|
||||
@@ -56,7 +71,7 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) {
|
||||
return person.assigned === true
|
||||
})
|
||||
.map((filteredPerson) =>
|
||||
mapPerson(filteredPerson)
|
||||
GameStateCurator.mapPerson(filteredPerson)
|
||||
),
|
||||
timerParams: game.timerParams,
|
||||
isFull: game.isFull,
|
||||
@@ -69,7 +84,7 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) {
|
||||
return {
|
||||
accessCode: game.accessCode,
|
||||
status: game.status,
|
||||
moderator: mapPerson(game.moderator),
|
||||
moderator: GameStateCurator.mapPerson(game.moderator),
|
||||
client: client,
|
||||
deck: game.deck,
|
||||
people: GameStateCurator.mapPeopleForModerator(game.people, client),
|
||||
@@ -81,14 +96,14 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) {
|
||||
return {
|
||||
accessCode: game.accessCode,
|
||||
status: game.status,
|
||||
moderator: mapPerson(game.moderator),
|
||||
moderator: GameStateCurator.mapPerson(game.moderator),
|
||||
client: client,
|
||||
deck: game.deck,
|
||||
people: game.people
|
||||
.filter((person) => {
|
||||
return person.assigned === true
|
||||
})
|
||||
.map((filteredPerson) => mapPerson(filteredPerson)),
|
||||
.map((filteredPerson) => GameStateCurator.mapPerson(filteredPerson)),
|
||||
timerParams: game.timerParams,
|
||||
isFull: game.isFull
|
||||
}
|
||||
@@ -96,14 +111,14 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) {
|
||||
return {
|
||||
accessCode: game.accessCode,
|
||||
status: game.status,
|
||||
moderator: mapPerson(game.moderator),
|
||||
moderator: GameStateCurator.mapPerson(game.moderator),
|
||||
client: client,
|
||||
deck: game.deck,
|
||||
people: game.people
|
||||
.filter((person) => {
|
||||
return person.assigned === true
|
||||
})
|
||||
.map((filteredPerson) => mapPerson(filteredPerson)),
|
||||
.map((filteredPerson) => GameStateCurator.mapPerson(filteredPerson)),
|
||||
timerParams: game.timerParams,
|
||||
isFull: game.isFull,
|
||||
}
|
||||
@@ -112,20 +127,4 @@ function getGameStateBasedOnPermissions(game, person, gameRunner) {
|
||||
}
|
||||
}
|
||||
|
||||
function mapPerson(person) {
|
||||
if (person.revealed) {
|
||||
return {
|
||||
name: person.name,
|
||||
id: person.id,
|
||||
userType: person.userType,
|
||||
out: person.out,
|
||||
revealed: person.revealed,
|
||||
gameRole: person.gameRole,
|
||||
alignment: person.alignment
|
||||
};
|
||||
} else {
|
||||
return { name: person.name, id: person.id, userType: person.userType, out: person.out, revealed: person.revealed };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GameStateCurator;
|
||||
|
||||
@@ -10,6 +10,10 @@ router.get('/create', function (request, response) {
|
||||
response.sendFile(path.join(__dirname, '../../client/src/views/create.html'));
|
||||
});
|
||||
|
||||
router.get('/how-to-use', function (request, response) {
|
||||
response.sendFile(path.join(__dirname, '../../client/src/views/how-to-use.html'));
|
||||
});
|
||||
|
||||
router.get('/game/:code', function (request, response) {
|
||||
response.sendFile(path.join(__dirname, '../../client/src/views/game.html'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user