get timer on connect, patch play/pause functionality

This commit is contained in:
Alec
2021-11-30 19:24:08 -05:00
parent 5c869182a2
commit e21ad8493f
13 changed files with 230 additions and 78 deletions

View File

@@ -1,6 +1,6 @@
export const globals = {
USER_SIGNATURE_LENGTH: 25,
CLOCK_TICK_INTERVAL_MILLIS: 100,
CLOCK_TICK_INTERVAL_MILLIS: 10,
ACCESS_CODE_LENGTH: 6,
PLAYER_ID_COOKIE_KEY: 'play-werewolf-anon-id',
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
@@ -9,7 +9,8 @@ export const globals = {
GET_ENVIRONMENT: 'getEnvironment',
START_GAME: 'startGame',
PAUSE_TIMER: 'pauseTimer',
RESUME_TIMER: 'resumeTimer'
RESUME_TIMER: 'resumeTimer',
GET_TIME_REMAINING: 'getTimeRemaining'
},
STATUS: {
LOBBY: "lobby",

View File

@@ -0,0 +1,116 @@
import {globals} from "../config/globals.js";
export class GameTimerManager {
constructor() {
}
startGameTimer (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;
}
};
const totalTime = convertFromHoursToMilliseconds(hours) + convertFromMinutesToMilliseconds(minutes);
timerWorker.postMessage({ totalTime: totalTime, tickInterval: tickRate });
}
}
resumeGameTimer(totalTime, tickRate, soundManager, timerWorker) {
if (window.Worker) {
let timer = document.getElementById('game-timer');
timer.classList.remove('paused');
timer.innerText = totalTime < 60000
? returnHumanReadableTime(totalTime, true)
: returnHumanReadableTime(totalTime);
timerWorker.onmessage = function (e) {
if (e.data.hasOwnProperty('timeRemainingInMilliseconds') && e.data.timeRemainingInMilliseconds > 0) {
timer.innerText = e.data.displayTime;
}
};
timerWorker.postMessage({ totalTime: totalTime, tickInterval: tickRate });
}
}
pauseGameTimer(timerWorker, timeRemaining) {
if (window.Worker) {
timerWorker.postMessage('stop');
let timer = document.getElementById('game-timer');
timer.innerText = timeRemaining < 60000
? returnHumanReadableTime(timeRemaining, true)
: returnHumanReadableTime(timeRemaining);
timer.classList.add('paused');
}
}
displayPausedTime(time) {
let timer = document.getElementById('game-timer');
timer.innerText = time < 60000
? returnHumanReadableTime(time, true)
: returnHumanReadableTime(time);
timer.classList.add('paused');
}
attachTimerSocketListeners(socket, timerWorker, gameStateRenderer) {
if (!socket.hasListeners(globals.EVENTS.START_TIMER)) {
socket.on(globals.EVENTS.START_TIMER, () => {
this.startGameTimer(
gameStateRenderer.gameState.timerParams.hours,
gameStateRenderer.gameState.timerParams.minutes,
globals.CLOCK_TICK_INTERVAL_MILLIS,
null,
timerWorker
)
});
}
if(!socket.hasListeners(globals.COMMANDS.PAUSE_TIMER)) {
socket.on(globals.COMMANDS.PAUSE_TIMER, (timeRemaining) => {
this.pauseGameTimer(timerWorker, timeRemaining)
});
}
if(!socket.hasListeners(globals.COMMANDS.RESUME_TIMER)) {
socket.on(globals.COMMANDS.RESUME_TIMER, (timeRemaining) => {
this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker);
});
}
if(!socket.hasListeners(globals.COMMANDS.GET_TIME_REMAINING)) {
socket.on(globals.COMMANDS.GET_TIME_REMAINING, (timeRemaining, paused) => {
console.log('received time remaining from server');
if (paused) {
this.displayPausedTime(timeRemaining);
} else {
this.resumeGameTimer(timeRemaining, globals.CLOCK_TICK_INTERVAL_MILLIS, null, timerWorker);
}
});
}
}
}
function convertFromMinutesToMilliseconds(minutes) {
return minutes * 60 * 1000;
}
function convertFromHoursToMilliseconds(hours) {
return hours * 60 * 60 * 1000;
}
function returnHumanReadableTime(milliseconds, tenthsOfSeconds=false) {
let tenths = Math.floor((milliseconds / 100) % 10);
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 tenthsOfSeconds
? hours + ":" + minutes + ":" + seconds + '.' + tenths
: hours + ":" + minutes + ":" + seconds;
}

View File

@@ -10,18 +10,20 @@ See: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
const messageParameters = {
STOP: 'stop',
TICK_INTERVAL: 'tickInterval',
HOURS: 'hours',
MINUTES: 'minutes'
TOTAL_TIME: 'totalTime'
};
let timer;
onmessage = function (e) {
if (typeof e.data === 'object'
&& e.data.hasOwnProperty(messageParameters.HOURS)
&& e.data.hasOwnProperty(messageParameters.MINUTES)
&& e.data.hasOwnProperty(messageParameters.TOTAL_TIME)
&& e.data.hasOwnProperty(messageParameters.TICK_INTERVAL)
) {
const timer = new Singleton(e.data.hours, e.data.minutes, e.data.tickInterval);
timer = new Singleton(e.data.totalTime, e.data.tickInterval);
timer.startTimer();
} else if (e.data === 'stop') {
timer.stopTimer();
}
};
@@ -32,9 +34,12 @@ function stepFn (expected, interval, start, totalTime) {
}
const delta = now - expected;
expected += interval;
let displayTime = (totalTime - (expected - start)) < 60000
? returnHumanReadableTime(totalTime - (expected - start), true)
: returnHumanReadableTime(totalTime - (expected - start));
postMessage({
timeRemainingInMilliseconds: totalTime - (expected - start),
displayTime: returnHumanReadableTime(totalTime - (expected - start))
displayTime: displayTime
});
Singleton.setNewTimeoutReference(setTimeout(() => {
stepFn(expected, interval, start, totalTime);
@@ -43,19 +48,18 @@ function stepFn (expected, interval, start, totalTime) {
}
class Timer {
constructor (hours, minutes, tickInterval) {
constructor (totalTime, tickInterval) {
this.timeoutId = undefined;
this.hours = hours;
this.minutes = minutes;
this.totalTime = totalTime;
this.tickInterval = tickInterval;
}
startTimer () {
if (!isNaN(this.hours) && !isNaN(this.minutes) && !isNaN(this.tickInterval)) {
if (!isNaN(this.tickInterval)) {
const interval = this.tickInterval;
const totalTime = convertFromHoursToMilliseconds(this.hours) + convertFromMinutesToMilliseconds(this.minutes);
const start = Date.now();
const expected = Date.now() + this.tickInterval;
const totalTime = this.totalTime;
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
@@ -64,23 +68,28 @@ class Timer {
}, this.tickInterval);
}
}
stopTimer() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
}
}
class Singleton {
constructor (hours, minutes, tickInterval) {
constructor (totalTime, tickInterval) {
if (!Singleton.instance) {
Singleton.instance = new Timer(hours, minutes, tickInterval);
Singleton.instance = new Timer(totalTime, tickInterval);
} else {
// This allows the same timer to be configured to run for different intervals / at a different granularity.
Singleton.setNewTimerParameters(hours, minutes, tickInterval);
Singleton.setNewTimerParameters(totalTime, tickInterval);
}
return Singleton.instance;
}
static setNewTimerParameters (hours, minutes, tickInterval) {
static setNewTimerParameters (totalTime, tickInterval) {
if (Singleton.instance) {
Singleton.instance.hours = hours;
Singleton.instance.minutes = minutes;
Singleton.instance.totalTime = totalTime;
Singleton.instance.tickInterval = tickInterval;
}
}
@@ -92,16 +101,9 @@ class Singleton {
}
}
function convertFromMinutesToMilliseconds(minutes) {
return minutes * 60 * 1000;
}
function convertFromHoursToMilliseconds(hours) {
return hours * 60 * 60 * 1000;
}
function returnHumanReadableTime(milliseconds) {
function returnHumanReadableTime(milliseconds, tenthsOfSeconds=false) {
let tenths = Math.floor((milliseconds / 100) % 10);
let seconds = Math.floor((milliseconds / 1000) % 60);
let minutes = Math.floor((milliseconds / (1000 * 60)) % 60);
let hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24);
@@ -110,5 +112,7 @@ function returnHumanReadableTime(milliseconds) {
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
return hours + ":" + minutes + ":" + seconds;
return tenthsOfSeconds
? hours + ":" + minutes + ":" + seconds + '.' + tenths
: hours + ":" + minutes + ":" + seconds;
}

View File

@@ -13,13 +13,12 @@ export const create = () => {
gameCreationStepManager.renderStep("creation-step-container", 1);
}
// Display a widget for each default card that allows copies of it to be added/removed. Set initial deck state.
function loadDefaultCards(deckManager) {
defaultCards.sort((a, b) => {
return a.role.localeCompare(b.role);
});
let deck = [];
for (let i = 0; i < defaultCards.length; i ++) { // each dropdown should include every
for (let i = 0; i < defaultCards.length; i ++) {
let card = defaultCards[i];
card.quantity = 0;
deck.push(card);
@@ -27,8 +26,6 @@ function loadDefaultCards(deckManager) {
deckManager.deck = deck;
}
/* Display a dropdown containing all the custom roles. Adding one will add it to the game deck and
create a widget for it */
function loadCustomRoles(deckManager) {
customCards.sort((a, b) => {
return a.role.localeCompare(b.role);

View File

@@ -3,20 +3,23 @@ import { globals } from "../config/globals.js";
import {templates} from "../modules/Templates.js";
import {GameStateRenderer} from "../modules/GameStateRenderer.js";
import {cancelCurrentToast, toast} from "../modules/Toast.js";
import {GameTimerManager} from "../modules/GameTimerManager.js";
export const game = () => {
let timerWorker = new Worker('../modules/Timer.js');
const socket = io('/in-game');
socket.on('disconnect', () => {
timerWorker.terminate();
toast('You are disconnected.', 'error', true);
});
socket.on('connect', () => {
socket.emit(globals.COMMANDS.GET_ENVIRONMENT, function(returnedEnvironment) {
prepareGamePage(returnedEnvironment, socket);
prepareGamePage(returnedEnvironment, socket, timerWorker);
});
})
};
function prepareGamePage(environment, socket, reconnect=false) {
function prepareGamePage(environment, socket, timerWorker) {
let userId = UserUtility.validateAnonUserSignature(environment);
const splitUrl = window.location.href.split('/game/');
const accessCode = splitUrl[1];
@@ -30,9 +33,12 @@ function prepareGamePage(environment, socket, reconnect=false) {
userId = gameState.client.id;
UserUtility.setAnonymousUserId(userId, environment);
let gameStateRenderer = new GameStateRenderer(gameState);
const timerWorker = new Worker('../modules/Timer.js');
setClientSocketHandlers(gameStateRenderer, socket, timerWorker);
processGameState(gameState, userId, socket, gameStateRenderer, timerWorker);
let gameTimerManager;
if (gameState.timerParams) {
gameTimerManager = new GameTimerManager();
}
setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTimerManager);
processGameState(gameState, userId, socket, gameStateRenderer);
}
});
} else {
@@ -40,7 +46,7 @@ function prepareGamePage(environment, socket, reconnect=false) {
}
}
function processGameState (gameState, userId, socket, gameStateRenderer, timerWorker) {
function processGameState (gameState, userId, socket, gameStateRenderer) {
switch (gameState.status) {
case globals.STATUS.LOBBY:
document.getElementById("game-state-container").innerHTML = templates.LOBBY;
@@ -81,7 +87,7 @@ function processGameState (gameState, userId, socket, gameStateRenderer, timerWo
}
}
function setClientSocketHandlers(gameStateRenderer, socket, timerWorker) {
function setClientSocketHandlers(gameStateRenderer, socket, timerWorker, gameTimerManager) {
if (!socket.hasListeners(globals.EVENTS.PLAYER_JOINED)) {
socket.on(globals.EVENTS.PLAYER_JOINED, (player, gameIsFull) => {
toast(player.name + " joined!", "success", false);
@@ -111,28 +117,8 @@ function setClientSocketHandlers(gameStateRenderer, socket, timerWorker) {
});
}
if (!socket.hasListeners(globals.EVENTS.START_TIMER)) {
socket.on(globals.EVENTS.START_TIMER, () => {
runGameTimer(
gameStateRenderer.gameState.timerParams.hours,
gameStateRenderer.gameState.timerParams.minutes,
globals.CLOCK_TICK_INTERVAL_MILLIS,
null,
timerWorker
)
});
}
if(!socket.hasListeners(globals.COMMANDS.PAUSE_TIMER)) {
socket.on(globals.COMMANDS.PAUSE_TIMER, (timeRemaining) => {
console.log(timeRemaining);
});
}
if(!socket.hasListeners(globals.COMMANDS.RESUME_TIMER)) {
socket.on(globals.COMMANDS.RESUME_TIMER, (timeRemaining) => {
console.log(timeRemaining);
});
if (timerWorker && gameTimerManager) {
gameTimerManager.attachTimerSocketListeners(socket, timerWorker, gameStateRenderer);
}
}

View File

@@ -237,6 +237,18 @@ label[for='lobby-players'] {
border: 2px solid #1c8a36;
}
.paused {
animation: pulse 0.75s linear infinite alternate;
}
@keyframes pulse {
from {
color: rgba(255, 255, 255, 0.1);
} to {
color: rgba(255, 255, 255, 1);
}
}
@keyframes fade-in-slide-up {
0% {
opacity: 0;

View File

@@ -71,7 +71,7 @@
}
@media(max-width: 700px) {
h1 {
font-size: 44vw;
font-size: 35vw;
}
}
</style>

View File

@@ -1,7 +1,7 @@
const globals = {
ACCESS_CODE_CHAR_POOL: 'abcdefghijklmnopqrstuvwxyz0123456789',
ACCESS_CODE_LENGTH: 6,
CLOCK_TICK_INTERVAL_MILLIS: 100,
CLOCK_TICK_INTERVAL_MILLIS: 10,
CLIENT_COMMANDS: {
FETCH_GAME_STATE: 'fetchGameState',
GET_ENVIRONMENT: 'getEnvironment',
@@ -43,7 +43,8 @@ const globals = {
START_GAME: "startGame",
START_TIMER: "startTimer",
PAUSE_TIMER: "pauseTimer",
RESUME_TIMER: "resumeTimer"
RESUME_TIMER: "resumeTimer",
GET_TIME_REMAINING: "getTimeRemaining"
}
};

View File

@@ -19,23 +19,31 @@ class ActiveGameRunner {
gameProcess.on('message', (msg) => {
switch (msg.command) {
case globals.GAME_PROCESS_COMMANDS.END_GAME:
game.status = globals.STATUS.ENDED;
//game.status = globals.STATUS.ENDED;
game.timerParams.paused = false;
game.timerParams.timeRemaining = 0;
this.logger.debug('PARENT: END GAME');
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.END_GAME, game.accessCode);
break;
case globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER:
game.timerParams.paused = true;
this.logger.trace(msg);
game.timeRemaining = msg.timeRemaining;
game.timerParams.timeRemaining = msg.timeRemaining;
this.logger.debug('PARENT: PAUSE TIMER');
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, game.timeRemaining);
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.PAUSE_TIMER, game.timerParams.timeRemaining);
break;
case globals.GAME_PROCESS_COMMANDS.RESUME_TIMER:
game.timerParams.paused = false;
this.logger.trace(msg);
game.timeRemaining = msg.timeRemaining;
game.timerParams.timeRemaining = msg.timeRemaining;
this.logger.debug('PARENT: RESUME TIMER');
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, game.timeRemaining);
namespace.in(game.accessCode).emit(globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, game.timerParams.timeRemaining);
break;
case globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING:
this.logger.trace(msg);
game.timerParams.timeRemaining = msg.timeRemaining;
this.logger.debug('PARENT: GET TIME REMAINING');
namespace.to(msg.socketId).emit(globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING, game.timerParams.timeRemaining, game.timerParams.paused);
break;
}
});

View File

@@ -220,13 +220,13 @@ function handleRequestForGameState(namespace, logger, gameRunner, accessCode, pe
if (matchingPerson) {
if (matchingPerson.socketId === socket.id) {
logger.trace("matching person found with an established connection to the room: " + matchingPerson.name);
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson));
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson, gameRunner, socket, logger));
} else {
if (!roomContainsSocketOfMatchingPerson(namespace, matchingPerson, logger, accessCode)) {
logger.trace("matching person found with a new connection to the room: " + matchingPerson.name);
socket.join(accessCode);
matchingPerson.socketId = socket.id;
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson));
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, matchingPerson, gameRunner, socket, logger));
} else {
rejectClientRequestForGameState(ackFn);
}
@@ -235,7 +235,7 @@ function handleRequestForGameState(namespace, logger, gameRunner, accessCode, pe
let personWithMatchingSocketId = findPersonWithMatchingSocketId(game.people, 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));
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, personWithMatchingSocketId, gameRunner, socket, logger));
} else {
let unassignedPerson = game.moderator.assigned === false
? game.moderator
@@ -245,7 +245,7 @@ function handleRequestForGameState(namespace, logger, gameRunner, accessCode, pe
socket.join(accessCode);
unassignedPerson.assigned = true;
unassignedPerson.socketId = socket.id;
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, unassignedPerson));
ackFn(GameStateCurator.getGameStateFromPerspectiveOfPerson(game, unassignedPerson, gameRunner, socket, logger));
let isFull = isGameFull(game);
game.isFull = isFull;
socket.to(accessCode).emit(

View File

@@ -37,6 +37,16 @@ process.on('message', (msg) => {
logger.debug('CHILD PROCESS ' + msg.accessCode + ': RESUME TIMER');
process.send({ command: globals.GAME_PROCESS_COMMANDS.RESUME_TIMER, timeRemaining: timer.currentTimeInMillis});
break;
case globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING:
logger.debug('CHILD PROCESS ' + msg.accessCode + ': GET TIME REMAINING');
process.send({
command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
timeRemaining: timer.currentTimeInMillis,
socketId: msg.socketId
});
break;
}
});

View File

@@ -1,12 +1,15 @@
const globals = require("../config/globals")
const GameStateCurator = {
getGameStateFromPerspectiveOfPerson: (game, person) => {
return getGameStateBasedOnPermissions(game, person);
getGameStateFromPerspectiveOfPerson: (game, person, gameRunner, socket, logger) => {
if (game.timerParams && game.status === globals.STATUS.IN_PROGRESS) {
getTimeRemaining(game.accessCode, gameRunner, socket, logger)
}
return getGameStateBasedOnPermissions(game, person, gameRunner);
}
}
function getGameStateBasedOnPermissions(game, person) {
function getGameStateBasedOnPermissions(game, person, gameRunner) {
let client = game.status === globals.STATUS.LOBBY // people won't be able to know their role until past the lobby stage.
? { name: person.name, id: person.id }
: {
@@ -89,4 +92,16 @@ function mapPerson(person) {
return { name: person.name };
}
function getTimeRemaining(accessCode, gameRunner, socket, logger) {
let thread = gameRunner.timerThreads[accessCode];
if (thread) {
thread.send({
command: globals.GAME_PROCESS_COMMANDS.GET_TIME_REMAINING,
accessCode: accessCode,
socketId: socket.id,
logLevel: logger.logLevel
});
}
}
module.exports = GameStateCurator;

View File

@@ -37,7 +37,9 @@ class ServerTimer {
}
runTimer () {
this.totalTime = convertFromHoursToMilliseconds(this.hours) + convertFromMinutesToMilliseconds(this.minutes);
let total = convertFromHoursToMilliseconds(this.hours) + convertFromMinutesToMilliseconds(this.minutes);
this.totalTime = total;
this.currentTimeInMillis = total;
this.logger.debug('STARTING TIMER FOR ' + this.totalTime + 'ms');
this.start = Date.now();
const expected = Date.now() + this.tickInterval;