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 = { "
" + "
" + "
" + - "" + + "" + "
" + "
" + "
" + @@ -37,9 +37,13 @@ export const templates = { "
" + "
" + "
" + - "
" + + "" + + "
" + + "

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;