diff --git a/client/config/globals.js b/client/config/globals.js
index 52f6f4b..6a16d74 100644
--- a/client/config/globals.js
+++ b/client/config/globals.js
@@ -1,5 +1,6 @@
export const globals = {
USER_SIGNATURE_LENGTH: 25,
+ CLOCK_TICK_INTERVAL_MILLIS: 100,
ACCESS_CODE_LENGTH: 6,
PLAYER_ID_COOKIE_KEY: 'play-werewolf-anon-id',
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
@@ -18,7 +19,8 @@ export const globals = {
},
EVENTS: {
PLAYER_JOINED: "playerJoined",
- SYNC_GAME_STATE: "syncGameState"
+ SYNC_GAME_STATE: "syncGameState",
+ START_TIMER: "startTimer"
},
USER_TYPES: {
MODERATOR: "moderator",
diff --git a/client/images/roles/Double-BlindMinion.png b/client/images/roles/Double-BlindMinion.png
new file mode 100644
index 0000000..e48ec91
Binary files /dev/null and b/client/images/roles/Double-BlindMinion.png differ
diff --git a/client/images/roles/DreamWolf.png b/client/images/roles/DreamWolf.png
new file mode 100644
index 0000000..bf5b085
Binary files /dev/null and b/client/images/roles/DreamWolf.png differ
diff --git a/client/images/roles/Hunter.png b/client/images/roles/Hunter.png
new file mode 100644
index 0000000..c4251e3
Binary files /dev/null and b/client/images/roles/Hunter.png differ
diff --git a/client/images/roles/KnowingMinion.png b/client/images/roles/KnowingMinion.png
new file mode 100644
index 0000000..e48ec91
Binary files /dev/null and b/client/images/roles/KnowingMinion.png differ
diff --git a/client/images/roles/Mason.png b/client/images/roles/Mason.png
new file mode 100644
index 0000000..58752a2
Binary files /dev/null and b/client/images/roles/Mason.png differ
diff --git a/client/images/roles/Seer.png b/client/images/roles/Seer.png
new file mode 100644
index 0000000..cc4ed3f
Binary files /dev/null and b/client/images/roles/Seer.png differ
diff --git a/client/images/roles/Shadow.png b/client/images/roles/Shadow.png
new file mode 100644
index 0000000..11f5e64
Binary files /dev/null and b/client/images/roles/Shadow.png differ
diff --git a/client/images/roles/Sorcerer.png b/client/images/roles/Sorcerer.png
new file mode 100644
index 0000000..a85b4da
Binary files /dev/null and b/client/images/roles/Sorcerer.png differ
diff --git a/client/images/roles/Villager.png b/client/images/roles/Villager.png
new file mode 100644
index 0000000..0b21d06
Binary files /dev/null and b/client/images/roles/Villager.png differ
diff --git a/client/images/roles/Werewolf.png b/client/images/roles/Werewolf.png
new file mode 100644
index 0000000..fa2d3db
Binary files /dev/null and b/client/images/roles/Werewolf.png differ
diff --git a/client/modules/GameCreationStepManager.js b/client/modules/GameCreationStepManager.js
index a284ea5..30e0dc7 100644
--- a/client/modules/GameCreationStepManager.js
+++ b/client/modules/GameCreationStepManager.js
@@ -485,6 +485,9 @@ function updateCustomRoleOptionsList(deckManager, selectEl) {
}
function addOptionsToList(options, selectEl) {
+ options.sort((a, b) => {
+ return a.role.localeCompare(b.role);
+ });
for (let i = 0; i < options.length; i ++) {
let optionEl = document.createElement("option");
let alignmentClass = customCards[i].team === globals.ALIGNMENT.GOOD ? globals.ALIGNMENT.GOOD : globals.ALIGNMENT.EVIL
diff --git a/client/modules/GameStateRenderer.js b/client/modules/GameStateRenderer.js
index b7cc8e4..a947711 100644
--- a/client/modules/GameStateRenderer.js
+++ b/client/modules/GameStateRenderer.js
@@ -4,6 +4,7 @@ import { toast } from "./Toast.js";
export class GameStateRenderer {
constructor(gameState) {
this.gameState = gameState;
+ this.cardFlipped = false;
}
renderLobbyPlayers() {
@@ -79,6 +80,20 @@ export class GameStateRenderer {
}
name.setAttribute("title", this.gameState.client.gameRole);
document.querySelector('#role-description').innerText = this.gameState.client.gameRoleDescription;
+ document.getElementById("role-image").setAttribute(
+ 'src',
+ '../images/roles/' + this.gameState.client.gameRole.replaceAll(' ', '') + '.png'
+ );
+
+ document.getElementById("game-role-back").addEventListener('click', () => {
+ document.getElementById("game-role").style.display = 'flex';
+ document.getElementById("game-role-back").style.display = 'none';
+ });
+
+ document.getElementById("game-role").addEventListener('click', () => {
+ document.getElementById("game-role-back").style.display = 'flex';
+ document.getElementById("game-role").style.display = 'none';
+ });
}
}
diff --git a/client/modules/Templates.js b/client/modules/Templates.js
index 51ad6fd..bc9d995 100644
--- a/client/modules/Templates.js
+++ b/client/modules/Templates.js
@@ -29,7 +29,7 @@ export const templates = {
"
" +
+ "
" +
"
" +
"
![role]()
" +
"
" +
+ "
" +
+ "
" +
+ "
Click to reveal your role
" +
+ "
(click again to hide)
" +
"
"
}
diff --git a/client/modules/Timer.js b/client/modules/Timer.js
index 608873a..d2fe55d 100644
--- a/client/modules/Timer.js
+++ b/client/modules/Timer.js
@@ -9,16 +9,18 @@ See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
const messageParameters = {
STOP: 'stop',
- TOTAL_TIME: 'totalTime',
- TICK_INTERVAL: 'tickInterval'
+ TICK_INTERVAL: 'tickInterval',
+ HOURS: 'hours',
+ MINUTES: 'minutes'
};
onmessage = function (e) {
if (typeof e.data === 'object'
- && e.data.hasOwnProperty(messageParameters.TOTAL_TIME)
+ && e.data.hasOwnProperty(messageParameters.HOURS)
+ && e.data.hasOwnProperty(messageParameters.MINUTES)
&& e.data.hasOwnProperty(messageParameters.TICK_INTERVAL)
) {
- const timer = new Singleton(e.data.totalTime, e.data.tickInterval);
+ const timer = new Singleton(e.data.hours, e.data.minutes, e.data.tickInterval);
timer.startTimer();
}
};
@@ -30,7 +32,10 @@ function stepFn (expected, interval, start, totalTime) {
}
const delta = now - expected;
expected += interval;
- postMessage({ timeRemaining: (totalTime - (expected - start)) / 1000 });
+ postMessage({
+ timeRemainingInMilliseconds: totalTime - (expected - start),
+ displayTime: returnHumanReadableTime(totalTime - (expected - start))
+ });
Singleton.setNewTimeoutReference(setTimeout(() => {
stepFn(expected, interval, start, totalTime);
}, Math.max(0, interval - delta)
@@ -38,16 +43,17 @@ function stepFn (expected, interval, start, totalTime) {
}
class Timer {
- constructor (totalTime, tickInterval) {
+ constructor (hours, minutes, tickInterval) {
this.timeoutId = undefined;
- this.totalTime = totalTime;
+ this.hours = hours;
+ this.minutes = minutes;
this.tickInterval = tickInterval;
}
startTimer () {
- if (!isNaN(this.totalTime) && !isNaN(this.tickInterval)) {
+ if (!isNaN(this.hours) && !isNaN(this.minutes) && !isNaN(this.tickInterval)) {
const interval = this.tickInterval;
- const totalTime = this.totalTime;
+ const totalTime = convertFromHoursToMilliseconds(this.hours) + convertFromMinutesToMilliseconds(this.minutes);
const start = Date.now();
const expected = Date.now() + this.tickInterval;
if (this.timeoutId) {
@@ -61,19 +67,20 @@ class Timer {
}
class Singleton {
- constructor (totalTime, tickInterval) {
+ constructor (hours, minutes, tickInterval) {
if (!Singleton.instance) {
- Singleton.instance = new Timer(totalTime, tickInterval);
+ Singleton.instance = new Timer(hours, minutes, tickInterval);
} else {
// This allows the same timer to be configured to run for different intervals / at a different granularity.
- Singleton.setNewTimerParameters(totalTime, tickInterval);
+ Singleton.setNewTimerParameters(hours, minutes, tickInterval);
}
return Singleton.instance;
}
- static setNewTimerParameters (totalTime, tickInterval) {
+ static setNewTimerParameters (hours, minutes, tickInterval) {
if (Singleton.instance) {
- Singleton.instance.totalTime = totalTime;
+ Singleton.instance.hours = hours;
+ Singleton.instance.minutes = minutes;
Singleton.instance.tickInterval = tickInterval;
}
}
@@ -84,3 +91,24 @@ class Singleton {
}
}
}
+
+function convertFromMinutesToMilliseconds(minutes) {
+ return minutes * 60 * 1000;
+}
+
+function convertFromHoursToMilliseconds(hours) {
+ return hours * 60 * 60 * 1000;
+}
+
+function returnHumanReadableTime(milliseconds) {
+
+ let seconds = Math.floor((milliseconds / 1000) % 60);
+ let minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
+ let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
+
+ hours = hours < 10 ? "0" + hours : hours;
+ minutes = minutes < 10 ? "0" + minutes : minutes;
+ seconds = seconds < 10 ? "0" + seconds : seconds;
+
+ return hours + ":" + minutes + ":" + seconds;
+}
diff --git a/client/scripts/game.js b/client/scripts/game.js
index f9a5c52..1e76185 100644
--- a/client/scripts/game.js
+++ b/client/scripts/game.js
@@ -19,8 +19,9 @@ export const game = () => {
userId = gameState.client.id;
UserUtility.setAnonymousUserId(userId, environment);
let gameStateRenderer = new GameStateRenderer(gameState);
- processGameState(gameState, userId, socket, gameStateRenderer); // this socket is initialized via a script tag in the game page HTML.
- setClientSocketHandlers(gameStateRenderer, socket);
+ const timerWorker = new Worker('../modules/Timer.js');
+ processGameState(gameState, userId, socket, gameStateRenderer, timerWorker); // this socket is initialized via a script tag in the game page HTML.
+ setClientSocketHandlers(gameStateRenderer, socket, timerWorker);
}
});
} else {
@@ -29,7 +30,7 @@ export const game = () => {
});
};
-function processGameState (gameState, userId, socket, gameStateRenderer) {
+function processGameState (gameState, userId, socket, gameStateRenderer, timerWorker) {
cancelCurrentToast();
switch (gameState.status) {
case globals.STATUS.LOBBY:
@@ -58,7 +59,7 @@ function processGameState (gameState, userId, socket, gameStateRenderer) {
}
}
-function setClientSocketHandlers(gameStateRenderer, socket) {
+function setClientSocketHandlers(gameStateRenderer, socket, timerWorker) {
socket.on(globals.EVENTS.PLAYER_JOINED, (player, gameIsFull) => {
toast(player.name + " joined!", "success", false);
gameStateRenderer.gameState.people.push(player);
@@ -84,6 +85,16 @@ function setClientSocketHandlers(gameStateRenderer, socket) {
}
);
})
+
+ socket.on(globals.EVENTS.START_TIMER, () => {
+ runGameTimer(
+ gameStateRenderer.gameState.timerParams.hours,
+ gameStateRenderer.gameState.timerParams.minutes,
+ globals.CLOCK_TICK_INTERVAL_MILLIS,
+ null,
+ timerWorker
+ )
+ })
}
function displayStartGamePromptForModerators(gameStateRenderer) {
@@ -99,3 +110,14 @@ function displayStartGamePromptForModerators(gameStateRenderer) {
});
}
+
+function runGameTimer (hours, minutes, tickRate, soundManager, timerWorker) {
+ if (window.Worker) {
+ timerWorker.onmessage = function (e) {
+ if (e.data.hasOwnProperty('timeRemainingInMilliseconds') && e.data.timeRemainingInMilliseconds > 0) {
+ document.getElementById('game-timer').innerText = e.data.displayTime;
+ }
+ };
+ timerWorker.postMessage({ hours: hours, minutes: minutes, tickInterval: tickRate });
+ }
+}
diff --git a/client/styles/game.css b/client/styles/game.css
index 62c8ef3..a101038 100644
--- a/client/styles/game.css
+++ b/client/styles/game.css
@@ -64,7 +64,7 @@ h1 {
#game-role {
position: relative;
- border-bottom: 2px solid gray;
+ border: 5px solid transparent;
background-color: #e7e7e7;
display: flex;
flex-direction: column;
@@ -85,6 +85,56 @@ h1 {
/*transform-style: preserve-3d;*/
}
+#game-role-back {
+ user-select: none;
+ -ms-user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #333243;
+ border: 5px solid #61606a;
+ position: relative;
+ flex-direction: column;
+ cursor: pointer;
+ max-width: 17em;
+ border-radius: 3px;
+ height: 23em;
+ margin: 0 auto 2em auto;
+ width: 100%;
+ box-shadow: 0 1px 1px rgba(0,0,0,0.11),
+ 0 2px 2px rgba(0,0,0,0.11),
+ 0 4px 4px rgba(0,0,0,0.11),
+ 0 8px 8px rgba(0,0,0,0.11),
+ 0 16px 16px rgba(0,0,0,0.11),
+ 0 32px 32px rgba(0,0,0,0.11);
+ /*perspective: 1000px;*/
+ /*transform-style: preserve-3d;*/
+}
+
+#game-role-back h4 {
+ font-size: 24px;
+ padding: 0.5em;
+ text-align: center;
+ color: #e7e7e7;
+}
+
+#game-role-back p {
+ color: #c3c3c3;
+ font-size: 20px;
+}
+
+#game-timer {
+ padding: 1px;
+ background-color: #3c3c3c;
+ color: whitesmoke;
+ border-radius: 3px;
+ font-size: 35px;
+ text-shadow: 0 3px 4px rgb(0 0 0 / 85%);
+ border: 1px solid #747474;
+}
+
#role-name {
position: absolute;
top: 6%;
@@ -100,10 +150,15 @@ h1 {
}
#role-image {
+ user-select: none;
+ -ms-user-select: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
position: absolute;
- top: 34%;
+ top: 37%;
left: 50%;
transform: translate(-50%, -50%);
+ width: 78%;
}
#role-description {
@@ -114,7 +169,7 @@ h1 {
transform: translate(-50%, 0);
font-size: 16px;
width: 78%;
- max-height: 7em;
+ max-height: 6em;
}
#game-link img {
diff --git a/server/config/globals.js b/server/config/globals.js
index 15fa0cc..947f9bb 100644
--- a/server/config/globals.js
+++ b/server/config/globals.js
@@ -1,6 +1,7 @@
const globals = {
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
ACCESS_CODE_LENGTH: 6,
+ CLOCK_TICK_INTERVAL_MILLIS: 100,
CLIENT_COMMANDS: {
FETCH_GAME_STATE: 'fetchGameState',
GET_ENVIRONMENT: 'getEnvironment',
@@ -8,7 +9,8 @@ const globals = {
},
STATUS: {
LOBBY: "lobby",
- IN_PROGRESS: "in progress"
+ IN_PROGRESS: "in progress",
+ ENDED: "ended"
},
USER_SIGNATURE_LENGTH: 25,
USER_TYPES: {
@@ -33,6 +35,11 @@ const globals = {
ERROR: "error",
WARN: "warn",
TRACE: "trace"
+ },
+ GAME_PROCESS_COMMANDS: {
+ END_GAME: "endGame",
+ START_GAME: "startGame",
+ START_TIMER: "startTimer"
}
};
diff --git a/server/model/Game.js b/server/model/Game.js
index efd635c..7c71fa7 100644
--- a/server/model/Game.js
+++ b/server/model/Game.js
@@ -1,5 +1,6 @@
class Game {
- constructor(status, people, deck, hasTimer, moderator, timerParams=null) {
+ constructor(accessCode, status, people, deck, hasTimer, moderator, timerParams=null) {
+ this.accessCode = accessCode
this.status = status;
this.moderator = moderator;
this.people = people;
diff --git a/server/modules/ActiveGameRunner.js b/server/modules/ActiveGameRunner.js
index e3c415f..dc5adb1 100644
--- a/server/modules/ActiveGameRunner.js
+++ b/server/modules/ActiveGameRunner.js
@@ -1,90 +1,49 @@
const { fork } = require('child_process');
const path = require('path');
-
-const logger = require('./logger')(false);
+const globals = require('../config/globals');
class ActiveGameRunner {
- constructor () {
+ constructor (logger) {
this.activeGames = {};
+ this.logger = logger;
}
- // runGame = (game, namespace, gameStateFn) => {
- // logger.debug('running game ' + game.accessCode);
- // const gameProcess = fork(path.join(__dirname, '/GameProcess.js'));
- // gameProcess.on('message', (msg) => {
- // switch (msg.command) {
- // case serverGlobals.COMMAND.END_COUNTDOWN:
- // logger.debug('GAME PARENT PROCESS ' + game.accessCode + ': COMMAND: END COUNTDOWN');
- // namespace.in(game.accessCode).emit(serverGlobals.COMMAND.END_COUNTDOWN);
- // gameProcess.send({
- // command: serverGlobals.COMMAND.START_GAME,
- // cycleNumber: game.words.length - 1,
- // cycleLength: game.timePerWord * 1000,
- // accessCode: game.accessCode
- // });
- // break;
- // case serverGlobals.COMMAND.START_GAME:
- // game.status = serverGlobals.GAME_STATE.STARTED;
- // game.lastCycleTime = new Date().toJSON();
- // logger.debug('GAME PARENT PROCESS ' + game.accessCode + ': COMMAND: START GAME');
- // namespace.in(game.accessCode).emit(serverGlobals.COMMAND.START_GAME, {
- // firstWord: game.words[0].baseword,
- // gameLength: game.words.length,
- // timePerWord: game.timePerWord * 1000
- // });
- // break;
- // case serverGlobals.COMMAND.CYCLE_WORD:
- // game.currentWordIndex += 1;
- // game.lastCycleTime = new Date().toJSON();
- // logger.debug('GAME PARENT PROCESS ' + game.accessCode + ': COMMAND: CYCLE WORD');
- // if (game.currentWordIndex < game.words.length) {
- // namespace.in(game.accessCode).emit(serverGlobals.COMMAND.CYCLE_WORD, {
- // word: game.words[game.currentWordIndex].baseword,
- // index: game.currentWordIndex + 1,
- // totalTime: game.timePerWord * 1000,
- // gameLength: game.words.length
- // });
- // }
- // gameProcess.send({
- // command: serverGlobals.COMMAND.CYCLE_WORD,
- // cycleIndex: game.currentWordIndex,
- // cycleLength: game.timePerWord * 1000,
- // accessCode: game.accessCode,
- // gameLength: game.words.length
- // });
- // break;
- // case serverGlobals.COMMAND.END_GAME:
- // game.status = serverGlobals.GAME_STATE.ENDED;
- // if (!game.posted) {
- // logger.debug('GAME PARENT PROCESS: GAME ' + game.accessCode + ' HAS ENDED...BEGINNING POST TO DATABASE');
- // this.postGameFn(game).then(() => {
- // game.posted = true;
- // logger.debug('GAME ' + game.accessCode + ' SUCCESSFULLY POSTED');
- // namespace.in(game.accessCode).emit(serverGlobals.COMMAND.END_GAME, game.accessCode);
- // });
- // }
- // break;
- // }
- // });
- //
- // gameProcess.on('exit', () => {
- // if (this.activeGames[game.accessCode]) {
- // delete this.activeGames[game.accessCode];
- // logger.debug('GAME ' + game.accessCode + ' REMOVED FROM ACTIVE GAMES.');
- // }
- // });
- // gameProcess.send({ command: serverGlobals.COMMAND.START_COUNTDOWN, accessCode: game.accessCode });
- // game.status = serverGlobals.GAME_STATE.STARTING;
- // game.startCountdownTime = new Date().toJSON();
- // namespace.in(game.accessCode).emit(serverGlobals.COMMAND.START_COUNTDOWN);
- // }
+ /* We're only going to fork a child process for games with a timer. They will report back to the parent process whenever
+ the timer is up.
+ */
+ runGame = (game, namespace) => {
+ this.logger.debug('running game ' + game.accessCode);
+ const gameProcess = fork(path.join(__dirname, '/GameProcess.js'));
+ gameProcess.on('message', (msg) => {
+ switch (msg.command) {
+ case globals.GAME_PROCESS_COMMANDS.END_GAME:
+ game.status = globals.STATUS.ENDED;
+ this.logger.debug('PARENT: END GAME');
+ namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_GAME, game.accessCode);
+ break;
+ }
+ });
+
+ gameProcess.on('exit', () => {
+ this.logger.debug('Game ' + game.accessCode + ' has ended. Elapsed: ' + (new Date() - game.startTime) + 'ms');
+ });
+ gameProcess.send({
+ command: globals.GAME_PROCESS_COMMANDS.START_TIMER,
+ accessCode: game.accessCode,
+ logLevel: this.logger.logLevel,
+ hours: game.timerParams.hours,
+ minutes: game.timerParams.minutes
+ });
+ game.startTime = new Date().toJSON();
+ namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.START_TIMER);
+ }
}
class Singleton {
- constructor () {
+ constructor (logger) {
if (!Singleton.instance) {
logger.log('CREATING SINGLETON ACTIVE GAME RUNNER');
- Singleton.instance = new ActiveGameRunner();
+ Singleton.instance = new ActiveGameRunner(logger);
}
}
diff --git a/server/modules/GameManager.js b/server/modules/GameManager.js
index 42e2d1c..97244bf 100644
--- a/server/modules/GameManager.js
+++ b/server/modules/GameManager.js
@@ -9,7 +9,7 @@ class GameManager {
constructor (logger, environment) {
this.logger = logger;
this.environment = environment;
- this.activeGameRunner = new ActiveGameRunner().getInstance();
+ this.activeGameRunner = new ActiveGameRunner(logger).getInstance();
this.namespace = null;
//this.gameSocketUtility = GameSocketUtility;
}
@@ -37,6 +37,9 @@ class GameManager {
if (game) {
game.status = globals.STATUS.IN_PROGRESS;
namespace.in(accessCode).emit(globals.EVENTS.SYNC_GAME_STATE);
+ if (game.hasTimer) {
+ this.activeGameRunner.runGame(game, namespace);
+ }
}
});
}
@@ -51,6 +54,7 @@ class GameManager {
const newAccessCode = this.generateAccessCode();
let moderator = initializeModerator(gameParams.moderatorName, gameParams.hasDedicatedModerator);
this.activeGameRunner.activeGames[newAccessCode] = new Game(
+ newAccessCode,
globals.STATUS.LOBBY,
initializePeopleForGame(gameParams.deck, moderator),
gameParams.deck,
diff --git a/server/modules/GameProcess.js b/server/modules/GameProcess.js
new file mode 100644
index 0000000..c170a72
--- /dev/null
+++ b/server/modules/GameProcess.js
@@ -0,0 +1,26 @@
+const globals = require('../config/globals.js');
+const ServerTimer = require('./ServerTimer.js');
+
+process.on('message', (msg) => {
+ const logger = require('./Logger')(msg.logLevel);
+ switch (msg.command) {
+ case globals.GAME_PROCESS_COMMANDS.START_TIMER:
+ logger.debug('CHILD PROCESS ' + msg.accessCode + ': START TIMER');
+ runGameTimer(msg.hours, msg.minutes, logger).then(() => {
+ logger.debug('Timer finished for ' + msg.accessCode);
+ process.send({ command: globals.GAME_PROCESS_COMMANDS.END_GAME });
+ process.exit(0);
+ });
+ break;
+ }
+});
+
+function runGameTimer (hours, minutes, logger) {
+ const cycleTimer = new ServerTimer(
+ hours,
+ minutes,
+ globals.CLOCK_TICK_INTERVAL_MILLIS,
+ logger
+ );
+ return cycleTimer.runTimer();
+}
diff --git a/server/modules/Logger.js b/server/modules/Logger.js
index d0d1a5c..01a1db4 100644
--- a/server/modules/Logger.js
+++ b/server/modules/Logger.js
@@ -2,6 +2,7 @@ const globals = require('../config/globals');
module.exports = function (logLevel = globals.LOG_LEVEL.INFO) {
return {
+ logLevel: logLevel,
log (message = '') {
const now = new Date();
console.log('LOG ', now.toGMTString(), ': ', message);
diff --git a/server/modules/ServerTimer.js b/server/modules/ServerTimer.js
new file mode 100644
index 0000000..8cb439b
--- /dev/null
+++ b/server/modules/ServerTimer.js
@@ -0,0 +1,69 @@
+/* ALL TIMES ARE IN MILLIS */
+
+function stepFn (expected, interval, start, totalTime, ticking, timesUpResolver, logger) {
+ const now = Date.now();
+ if (now - start >= totalTime) {
+ clearTimeout(ticking);
+ logger.debug('ELAPSED: ' + (now - start) + 'ms (~' + (Math.abs(totalTime - (now - start)) / totalTime).toFixed(3) + '% error).');
+ timesUpResolver(); // this is a reference to the callback defined in the construction of the promise in runTimer()
+ return;
+ }
+ const delta = now - expected;
+ expected += interval;
+ ticking = setTimeout(function () {
+ stepFn(
+ expected,
+ interval,
+ start,
+ totalTime,
+ ticking,
+ timesUpResolver,
+ logger
+ );
+ }, Math.max(0, interval - delta)); // take into account drift
+}
+
+class ServerTimer {
+ constructor (hours, minutes, tickInterval, logger) {
+ this.hours = hours;
+ this.minutes = minutes;
+ this.tickInterval = tickInterval;
+ this.logger = logger;
+ }
+
+ runTimer () {
+ const interval = this.tickInterval;
+ const totalTime = convertFromHoursToMilliseconds(this.hours) + convertFromMinutesToMilliseconds(this.minutes);
+ const logger = this.logger;
+ logger.debug('STARTING TIMER FOR ' + totalTime + 'ms');
+ const start = Date.now();
+ const expected = Date.now() + this.tickInterval;
+ let timesUpResolver;
+ const timesUpPromise = new Promise((resolve) => {
+ timesUpResolver = resolve;
+ });
+ const ticking = setTimeout(function () {
+ stepFn(
+ expected,
+ interval,
+ start,
+ totalTime,
+ ticking,
+ timesUpResolver,
+ logger
+ );
+ }, this.tickInterval);
+
+ return timesUpPromise;
+ }
+}
+
+function convertFromMinutesToMilliseconds(minutes) {
+ return minutes * 60 * 1000;
+}
+
+function convertFromHoursToMilliseconds(hours) {
+ return hours * 60 * 60 * 1000;
+}
+
+module.exports = ServerTimer;