diff --git a/.gitignore b/.gitignore index b2b0d2b..b713e91 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ # Dependency directory node_modules + +# Claude context file +CLAUDE.md diff --git a/README.md b/README.md index 2653e2f..026d333 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Mots.js ==== Un jeu de mots fléchés multijoueur basé sur Node.js ! -Les grilles sont récupérés chez Metro. Vous pouvez choisir de lancer la grille du jour ou jouer sur la grille de votre choix. +Les grilles sont récupérées chez GSO (rcijeux.fr). Vous pouvez choisir de lancer la grille du jour ou jouer sur la grille de votre choix. ![](./illustrations/mots-details1.png) @@ -25,9 +25,34 @@ Quand vous êtes prêt, écrivez `!start` dans le chat, puis amusez vous ! :smil #### Options -~~Par défaut si vous ne rajoutez pas de paramètre lors du lancement du serveur, le jeu ira charger la grille Metro du jour.~~ ⚠️ Le journal Metro aillant arreté de publier quotidiennement de nouvelles grilles, le jeu lance toujours la même grille. +Par défaut, le jeu tente de charger la grille GSO du jour en estimant son numéro à partir de la date actuelle. -Pour forcer le chargement d'une grille, rajouter son numéro à la fin de votre commande ou pendant une partie grâce au bouton prévu. +Vous pouvez spécifier une grille au lancement : + +``` +$ npm start # Charge la grille numérotée +$ npm start default # Charge la grille par défaut (debug) +``` + +Pendant une partie, vous pouvez changer de grille via le chat avec la commande `!grid `. + +#### Commandes chat + +| Commande | Description | +|---|---| +| `!start` | Lance la partie (en salle d'attente uniquement) | +| `!grid ` | Charge une nouvelle grille et relance la partie | + +#### Système de bonus + +Des points bonus sont attribués en plus des lettres trouvées : + +| Bonus | Points | Condition | +|---|---|---| +| Preum's ! | +4 | Premier mot trouvé de la partie | +| Finish him ! | +4 | Dernier mot trouvé (grille complétée) | +| Débloqueur | +5 | Premier mot après 2 minutes d'inactivité | +| Gros mot ! | +3 | Mot de 6 lettres ou plus | ## Crédits diff --git a/conf.json b/conf.json index 46688e3..1f57609 100644 --- a/conf.json +++ b/conf.json @@ -5,10 +5,11 @@ "SOCKET_PORT": 1337, "GRID_PROVIDER": { - "PROVIDER_NAME": "Metro", - "PROVIDER_ADDR": "https://www.rci-jeux.com/ope/metro/mfleches/data/mfl", - "PROVIDER_EXTENSION": ".mfl", - "PROVIDER_DEFAULT_GRID": 1215, - "PROVIDER_DEFAULT_GRID_DATE": 1400277600000 + "PROVIDER_NAME": "GSO", + "PROVIDER_ADDR": "https://www.rcijeux.fr/drupal_game/gso/mfleches/grids/", + "PROVIDER_EXTENSION": ".mfj", + "PROVIDER_DEFAULT_GRID": 2118, + "PROVIDER_DEFAULT_GRID_DATE": 1772323200000, + "PROVIDER_FIRST_GRID": 1292 } } diff --git a/game_files/gridManager.js b/game_files/gridManager.js index 1f0b728..2a96fd7 100644 --- a/game_files/gridManager.js +++ b/game_files/gridManager.js @@ -1,15 +1,17 @@ var https = require('https'), + vm = require('vm'), + fs = require('fs'), + path = require('path'), config = require('../conf.json').GRID_PROVIDER, enums = require('./enums'), Case = require('./case'); -var _grid = null, // the grid itself ! - _wordsPoints = null, - _theme = null, - _gridInfos = null, - _nbLetters = 0, - _lastSearchCase = 0, - _maxPoints = 0; +// Persist downloaded grids to disk so they survive server restarts +// and work offline for tests +var CACHE_DIR = path.join(__dirname, '..', 'cache'); +if (!fs.existsSync(CACHE_DIR)) { + fs.mkdirSync(CACHE_DIR, { recursive: true }); +} var enumCaseParser = { InARow: 1, @@ -24,36 +26,42 @@ var enumArrow = { BottomRight: 3 }; +// Module-level cache: resolved grid number → raw .mfj text +// Shared across all GridManager instances to avoid re-downloading the same grid +var _rawGridCache = {}; + function GridManager() { - // Init grid infos - _gridInfos = { + // Instance state (not module-level variables) + this._grid = null; + this._wordsPoints = null; + this._theme = null; + this._nbLetters = 0; + this._lastSearchCase = 0; + this._maxPoints = 0; + this._gridInfos = { provider: '', id: 0, level: 0, - nbWords: 0 + nbWords: 0, + date: null }; } function getNextCase(grid, kindMove, caseType, lastCase) { var iterator = 0; - // Get the last case position if (lastCase) { iterator = lastCase.pos; - - // Move iterator to the next case according the kind of search we want if ((kindMove == enumCaseParser.InARow) || (kindMove == enumCaseParser.Horizontal)) iterator++; else if (kindMove == enumCaseParser.Vertical) iterator += grid.nbLines; } - // Don't bypass array length ! if (iterator >= grid.cases.length) return (null); - // According to the kind of case we want, continue searching or not if (caseType != grid.cases[iterator].type) return (getNextCase(grid, kindMove, caseType, grid.cases[iterator])); @@ -64,8 +72,6 @@ function insertDescription(grid, desc) { var currentCase = getNextCase(grid, enumCaseParser.InARow, enums.CaseType.Description), assigned = false; - // Try to set description then go to the next available description case - // If the desc has been set, or if we run out of cases, exit the loop while (currentCase !== null && !assigned) { assigned = currentCase.setDescription(desc); currentCase = getNextCase(grid, enumCaseParser.InARow, enums.CaseType.Description, currentCase); @@ -75,27 +81,21 @@ function insertDescription(grid, desc) { function getCaseType(Char) { if (Char == 'z') return (enums.CaseType.Empty); - else if ((Char >= 'A') && (Char <= 'Z')) + else if ((Char >= 'A') && (Char <= 'Z')) return (enums.CaseType.Letter); else return (enums.CaseType.Description); } function onGetGridError(cb, errorMessage) { - // Print error reason console.error('\t[ERROR]: Cannot retreive grid...'); console.error('\t[ERROR]: ' + errorMessage); - - // Raise callback with null parameter cb(null); } -function parseGrid(callback, serverText) { - var stArray, - info, - i, j, - length, - line, +function parseGrid(self, callback, serverText) { + var sandbox = {}, + data, currentCase = 0, type, grid = { @@ -105,97 +105,59 @@ function parseGrid(callback, serverText) { cases: [] }; - // Initial sort. Isolate each "line" by spliting on '&' char - stArray = serverText.split('&'); - - // Then parse each line - length = stArray.length; - for (i = 0; i < length; i++) { - - // Get key / value for this line - info = stArray[i].split('='); - - // If this line describe a grid line, insert new line in our grid - if (info[0].indexOf('lign') > -1) { - - for (j in info[1]) { - type = getCaseType(info[1][j]); + try { + vm.runInNewContext(serverText, sandbox); + } catch (e) { + onGetGridError(callback, 'Failed to parse grid file: ' + e.message); + return; + } - if (type == enums.CaseType.Letter) { - grid.cases.push(new Case.LetterCase(currentCase++, info[1][j])); - _nbLetters++; - } - else if (type == enums.CaseType.Description) - grid.cases.push(new Case.DescriptionCase(currentCase++, info[1][j])); - else - grid.cases.push(new Case.EmptyCase(currentCase++)); - }; + data = sandbox.gamedata; + if (!data || !data.grille) { + onGetGridError(callback, 'Invalid grid format: no gamedata found'); + return; + } - // Update col & line counters - if (grid.nbLines == 0) - grid.nbLines = info[1].length; - grid.nbColumns++; - } - // If this line is a description, add it - else if (info[0].indexOf('tx') > -1) { - insertDescription(grid, info[1]); - } - // If this line set a dotted frame, apply the effect to the right frame - else if (info[0].indexOf('pointillev') > -1 || info[0].indexOf('pointilleh') > -1) { - grid.cases[(parseInt(info[0].substr(10), 10)) - 1].dashed = info[1]; - } - else if (info[0].indexOf('pointille') > -1) { - grid.cases[(parseInt(info[0].substr(9), 10)) - 1].dashed = info[1]; - } - // Else try to deal with the line key - else { - switch (info[0]) { - case 'nomjeu': - case 'coul': - case 'nbphotos': - case 'casephotos': - case 'fin': - case '?': - case '': - // Ignore useless tags - break; + grid.nbLines = data.nbcaseslargeur; + grid.nbColumns = data.nbcaseshauteur; + grid.nbWords = data.definitions.length; - case 'niveau': - _wordsPoints = info[1]; - grid.nbWords = _wordsPoints.length; - _gridInfos.nbWords = _wordsPoints.length; - break; + self._wordsPoints = data.definitions; + self._gridInfos.nbWords = data.definitions.length; + self._gridInfos.level = parseInt(data.force, 10); - case 'themecase': - _theme = info[1]; - break; + data.grille.forEach(function(row) { + for (var j = 0; j < row.length; j++) { + type = getCaseType(row[j]); - case 'legende': - _gridInfos.level = parseInt(info[1][info[1].length - 1], 10); - break; - - default: - console.info('\t[GRIDMANAGER] Unknow grid tag [' + info[0] + ']'); + if (type === enums.CaseType.Letter) { + grid.cases.push(new Case.LetterCase(currentCase++, row[j])); + self._nbLetters++; + } + else if (type === enums.CaseType.Description) { + grid.cases.push(new Case.DescriptionCase(currentCase++, row[j])); + } + else { + grid.cases.push(new Case.EmptyCase(currentCase++)); } } - }; + }); - // Once the entire grid is retreived, place arrows - placeArrows(grid); + data.definitions.forEach(function(defArray) { + var defText = Array.isArray(defArray) ? defArray.join('\n') : String(defArray); + insertDescription(grid, defText); + }); - // Then store the grid - _grid = grid; + placeArrows(grid); + self._grid = grid; } function placeArrows(grid) { var i, gridSize = grid.cases.length; - // Check each cell to find a description frame for (i = 0; i < gridSize; i++) { if (grid.cases[i].type == enums.CaseType.Description) { - - // According the type of description, set the right arrow to the switch (grid.cases[i].value) { case 'a': grid.cases[i].arrow[0] = enumArrow.Right; @@ -209,7 +171,6 @@ function placeArrows(grid) { case 'd': grid.cases[i].arrow[0] = enumArrow.BottomRight; break; - case 'f': case 'g': case 'h': @@ -233,228 +194,244 @@ function placeArrows(grid) { grid.cases[i].arrow[0] = enumArrow.RightBottom; grid.cases[i].arrow[1] = enumArrow.BottomRight; break; - - default: - console.error('[ERROR][gridManager::placeArrows] Unknow arrow type [' + grid.cases[i].value + '] at frame ' + i); + case 's': + case 't': + case 'u': + grid.cases[i].arrow[0] = enumArrow.Bottom; + grid.cases[i].arrow[1] = enumArrow.BottomRight; + break; + default: { + var colIdx = i % grid.nbLines; + var hasRight = (colIdx + 1 < grid.nbLines) && grid.cases[i + 1] && (grid.cases[i + 1].type === enums.CaseType.Letter); + var hasBelow = (i + grid.nbLines < grid.cases.length) && grid.cases[i + grid.nbLines] && (grid.cases[i + grid.nbLines].type === enums.CaseType.Letter); + var arrowIdx = 0; + if (hasRight) { grid.cases[i].arrow[arrowIdx++] = enumArrow.Right; } + if (hasBelow) { grid.cases[i].arrow[arrowIdx] = enumArrow.Bottom; } + console.warn('[WARN][gridManager::placeArrows] Unknown arrow type [' + grid.cases[i].value + '] at frame ' + i + ', inferred arrows from context'); + } } } - }; + } } -function getGridAddress(commandArgv) { +function getGridAddress(self, commandArgv) { var gridNumber, today, gridDefaultDay, dayDiff; switch (commandArgv) { - // No number given, load day grid case 0: console.info('\n\t[GRIDMANAGER] Load day grid'); - // Compare the default date with today. Add this difference to the default grid number. Assume that we have one grid per day ! gridDefaultDay = new Date(config.PROVIDER_DEFAULT_GRID_DATE); today = new Date(); dayDiff = Math.abs(today.getTime() - gridDefaultDay.getTime()); dayDiff = Math.floor(dayDiff / (1000 * 3600 * 24)); - // gridNumber = config.PROVIDER_DEFAULT_GRID + dayDiff; - gridNumber = config.PROVIDER_DEFAULT_GRID; + gridNumber = config.PROVIDER_DEFAULT_GRID + dayDiff; break; - - // Retreive the default grid case -1: console.info('\n\t[GRIDMANAGER] Load default grid'); gridNumber = config.PROVIDER_DEFAULT_GRID; break; - - // Load the specified grid default: console.info('\n\t[GRIDMANAGER] Load specific grid'); gridNumber = commandArgv; break; } - // Set provider name - _gridInfos.provider = config.PROVIDER_NAME; - _gridInfos.id = gridNumber; + self._gridInfos.provider = config.PROVIDER_NAME; + self._gridInfos.id = gridNumber; + self._gridInfos.date = config.PROVIDER_DEFAULT_GRID_DATE + (gridNumber - config.PROVIDER_DEFAULT_GRID) * 86400000; - // Return the grid address return (config.PROVIDER_ADDR + gridNumber.toString() + config.PROVIDER_EXTENSION); } /* PUBLIC METHODS */ -/* -* Check if the player's founded word is the right one -* @param {Object} wordObj Client word object -* @return {Int} Points scored by the player. If return 0, it's just the wrong word :) -*/ GridManager.prototype.checkPlayerWord = function (wordObj) { - var jump = (wordObj.axis == 0) ? 1 : _grid.nbColumns, + var jump = (wordObj.axis == 0) ? 1 : this._grid.nbLines, wordSize = wordObj.word.length, points = 0, index = wordObj.start, i; - // Check each letters for (i = 0; i < wordSize; i++) { - // If the letter doesn't match the grid, return false - if (wordObj.word[i] != _grid.cases[index].value) + if (wordObj.word[i] != this._grid.cases[index].value) return (-1); - if (_grid.cases[index].available == true) + if (this._grid.cases[index].available == true) points++; index += jump; - }; + } + + // All letters already found — reject (no points, no bonus) + if (points === 0) return (-1); - // It's the righ word, so set letters as already founded index = wordObj.start; for (i = 0; i < wordSize; i++) { - if (_grid.cases[index].available == true) - _grid.cases[index].available = false; + if (this._grid.cases[index].available == true) + this._grid.cases[index].available = false; index += jump; - }; + } - // Decrease word counter - _grid.nbWords--; + this._grid.nbWords--; return (points); }; -/* -* Return a complete grid object to send to the clients. -* The "grid" object is composed by the grid itself, grid informations (nb lines, etc...) and provider informations -* @return {Object} A grid object with all informations needed by the clients -*/ GridManager.prototype.getGrid = function () { var clonedGrid, index; - // Clone the grid object by extanded an empty object - clonedGrid = JSON.parse(JSON.stringify(_grid)); + clonedGrid = JSON.parse(JSON.stringify(this._grid)); + clonedGrid.infos = this._gridInfos; - // Adding grid's informations - clonedGrid.infos = _gridInfos; - - // Finally hide letters before send it to the players :) for (index in clonedGrid.cases) { if (clonedGrid.cases[index].type == enums.CaseType.Letter) clonedGrid.cases[index].value = ''; - }; + } return (clonedGrid); }; /* -* Return grid informations -* @return {Object} The grid information object +* Returns a full grid clone including actual letter values (used for solo mode). */ +GridManager.prototype.getFullGrid = function () { + var clonedGrid = JSON.parse(JSON.stringify(this._grid)); + clonedGrid.infos = this._gridInfos; + return (clonedGrid); +}; + GridManager.prototype.getGridInfos = function () { - return (_gridInfos); + return (this._gridInfos); }; -/* -* To retreive the number of words still not found -* @return {Int} The number of words still available -*/ GridManager.prototype.getNbRemainingWords = function () { - return (_grid.nbWords); -} + return (this._grid.nbWords); +}; -/* -* Retreive the accomplishment rate for -* @return {Int} The number of words still available -*/ GridManager.prototype.getAccomplishmentRate = function (playerPoints, nbPlayers) { - // If we have not retreive the maximum of points for this game - if (_maxPoints == 0) { + if (this._maxPoints == 0) { switch (nbPlayers) { - case 1: - // Because of bonus points, we have to give more points than letters available for 1 player game - _maxPoints = Math.floor(_nbLetters * 1.5); - break; - case 2: - // For a regular 2 player game, the maximum of points can be 90% of letters available. That seems fair :) - _maxPoints = Math.floor(_nbLetters * 0.9); - break; - case 3: - // For 3 player, the maximum is 75% of the amount of letters. - _maxPoints = Math.floor(_nbLetters * 0.75); - break; - case 4: - // If you found 66% of all letters in 4 player game, it's really good - _maxPoints = Math.floor(_nbLetters * 0.66); - break; - default: - // In case of error, max points == number of letters to find - _maxPoints = _nbLetters; - break; + case 1: this._maxPoints = Math.floor(this._nbLetters * 1.5); break; + case 2: this._maxPoints = Math.floor(this._nbLetters * 0.9); break; + case 3: this._maxPoints = Math.floor(this._nbLetters * 0.75); break; + case 4: this._maxPoints = Math.floor(this._nbLetters * 0.66); break; + default: this._maxPoints = this._nbLetters; break; } } - return (Math.floor(playerPoints / _maxPoints * 100)); + return (Math.floor(playerPoints / this._maxPoints * 100)); }; /* -* This method will check for the grid, retreive it and parse it. t's the main method of this class. -* @param {Int} gridNumber The grid number ID to request to the provider -* @param {Function} callback The callback to raise either on success or error ! +* Retreive and parse the grid. Checks the raw text cache before making a network request. +* If the requested grid fails, falls back to PROVIDER_FIRST_GRID. */ GridManager.prototype.retreiveAndParseGrid = function (gridNumber, callback) { - var gridAddr = getGridAddress(gridNumber), // Retreive the grid URL, build from provider infos and ID requested - req = https.get(gridAddr, function (res) { // Launch the request ! + var self = this; + var gridAddr = getGridAddress(self, gridNumber); + var resolvedId = self._gridInfos.id; - var bodyChunks = []; + console.info('\n\t[GRIDMANAGER] Try to load ' + gridAddr); - console.info('\n\t[GRIDMANAGER] Try to load ' + gridAddr); + function loadFromText(text, id) { + _rawGridCache[id] = text; + parseGrid(self, callback, text); + callback(self._grid); + } - // If an error occurs, raise failure callback - if (res.statusCode !== 200 && res.statusCode !== 302) { - onGetGridError(callback, 'Wrong statusCode ' + res.statusCode); - } - else { - // Read server response - res.on('data', function (chunk) { - // Buffer the body... - bodyChunks.push(chunk); - }).on('end', function() { + function saveToDisk(id, text) { + fs.writeFile(path.join(CACHE_DIR, id + '.mfj'), text, function(err) { + if (err) console.warn('\t[GRIDMANAGER] Could not write cache file: ' + err.message); + }); + } - console.info('\t[GRIDMANAGER] Grid downloaded, start parsing...\n'); + // 1. Check in-memory cache + if (_rawGridCache[resolvedId]) { + console.info('\t[GRIDMANAGER] Memory cache hit for grid #' + resolvedId); + loadFromText(_rawGridCache[resolvedId], resolvedId); + return; + } - // Parse server response to extract a grid object - parseGrid(callback, Buffer.concat(bodyChunks).toString()); + // 2. Check disk cache + var diskFile = path.join(CACHE_DIR, resolvedId + '.mfj'); + if (fs.existsSync(diskFile)) { + console.info('\t[GRIDMANAGER] Disk cache hit for grid #' + resolvedId); + var diskText = fs.readFileSync(diskFile, 'utf8'); + loadFromText(diskText, resolvedId); + return; + } - console.info('\n\t[GRIDMANAGER] Parsing Done. Now play ' + _gridInfos.provider + ' ' + _gridInfos.id + ' - Level ' + _gridInfos.level); + // 3. Download from network + var req = https.get(gridAddr, function (res) { + // If the grid is not found, try the fallback + if (res.statusCode !== 200) { + res.resume(); // Consume and discard response body + var fallback = config.PROVIDER_FIRST_GRID; + console.warn('\t[GRIDMANAGER] Grid not found (HTTP ' + res.statusCode + '), falling back to #' + fallback); + self._gridInfos.provider = config.PROVIDER_NAME; + self._gridInfos.id = fallback; + + if (_rawGridCache[fallback]) { + loadFromText(_rawGridCache[fallback], fallback); + return; + } + var diskFallback = path.join(CACHE_DIR, fallback + '.mfj'); + if (fs.existsSync(diskFallback)) { + console.info('\t[GRIDMANAGER] Disk cache hit for fallback grid #' + fallback); + loadFromText(fs.readFileSync(diskFallback, 'utf8'), fallback); + return; + } - callback(_grid); + var fallbackAddr = config.PROVIDER_ADDR + fallback + config.PROVIDER_EXTENSION; + var fallbackReq = https.get(fallbackAddr, function (res2) { + if (res2.statusCode !== 200) { + res2.resume(); + onGetGridError(callback, 'Fallback grid also failed (HTTP ' + res2.statusCode + ')'); + return; + } + var chunks = []; + res2.on('data', function (chunk) { chunks.push(chunk); }); + res2.on('end', function () { + var text = Buffer.concat(chunks).toString(); + saveToDisk(fallback, text); + console.info('\n\t[GRIDMANAGER] Fallback grid loaded: ' + config.PROVIDER_NAME + ' #' + fallback); + loadFromText(text, fallback); + }); }); - + fallbackReq.on('error', function (e) { onGetGridError(callback, e.message); }); + return; } + var bodyChunks = []; + res.on('data', function (chunk) { bodyChunks.push(chunk); }); + res.on('end', function () { + console.info('\t[GRIDMANAGER] Grid downloaded, start parsing...\n'); + var text = Buffer.concat(bodyChunks).toString(); + saveToDisk(resolvedId, text); + console.info('\n\t[GRIDMANAGER] Parsing Done. Now play ' + self._gridInfos.provider + ' ' + self._gridInfos.id + ' - Level ' + self._gridInfos.level); + loadFromText(text, resolvedId); + }); }); - req.on('error', function (e) { - // Notify error - onGetGridError(callback, e.message); - }); - + req.on('error', function (e) { onGetGridError(callback, e.message); }); }; -/* -* reset the current and load a new one -* @param {Int} gridNumber The grid number ID to request to the provider -* @param {Function} callback The callback to raise either on success or error ! -*/ GridManager.prototype.resetGrid = function (gridNumber, callback) { + this._grid = null; + this._wordsPoints = null; + this._theme = null; + this._nbLetters = 0; + this._lastSearchCase = 0; + this._maxPoints = 0; + this._gridInfos.id = 0; + this._gridInfos.level = 0; + this._gridInfos.nbWords = 0; + this._gridInfos.date = null; - // Reset important values - _grid = _wordsPoints = _theme = null; - _nbLetters = _lastSearchCase = _maxPoints = 0; - _gridInfos.id = 0; - _gridInfos.level = 0; - _gridInfos.nbWords = 0; - - // Load the grid this.retreiveAndParseGrid(gridNumber, callback); }; diff --git a/game_files/motsFleches.js b/game_files/motsFleches.js index 3700081..5236540 100644 --- a/game_files/motsFleches.js +++ b/game_files/motsFleches.js @@ -3,270 +3,420 @@ var enums = require('./enums'), GridManager = require('./gridManager'), PlayersManager = require('./playersManager'); -// Defines -var MAX_PLAYERS = 4; -var SERVER_CHAT_COLOR = '#c0392b'; -var TIME_BEFORE_START = 5; - -// Parameters -var _playersManager, - _gridManager, - _io, - _gameState, - _lastWordFoudTimestamp; - -function startGame() { - var Grid = _gridManager.getGrid(), - delay; - - delay = (_playersManager.getNumberOfPlayers() > 1) ? TIME_BEFORE_START : 0; - - // Change game state - _gameState = enums.ServerState.OnGame; - - // Send grid to clients - _io.sockets.emit('grid_event', { grid: Grid, timer: delay } ); +var MAX_PLAYERS = 9; +var SERVER_CHAT_COLOR = '#c0392b'; +var TIME_BEFORE_START = 5; +var ROOM_INACTIVITY_MS = 60 * 60 * 1000; // 60 minutes before room cleanup + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function generateRoomId() { + var chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no 0/O/1/I ambiguity + var id = ''; + for (var i = 0; i < 4; i++) id += chars[Math.floor(Math.random() * chars.length)]; + return id; } -function resetGame(gridID) { - var infos; - - // Reset game state - _gameState = enums.ServerState.WaitingForPlayers; +// ─── GameRoom ───────────────────────────────────────────────────────────────── + +function GameRoom(id, io, onInactive) { + this.id = id; + this._io = io; + this.gridManager = new GridManager(); + this.playersManager = new PlayersManager(); + this.gameState = enums.ServerState.WaitingForPlayers; + this.lastWordFoundTs = null; + this.gridReady = false; + this._foundWords = []; // {word, axis, start, color} for replay on rejoin + this._lastActivity = Date.now(); + this._onInactive = onInactive || null; + this._inactivityTimer = null; + this._startInactivityTimer(); +} - // Reset players - _playersManager.resetPlayersForNewGame(); +GameRoom.prototype.touchActivity = function () { + this._lastActivity = Date.now(); +}; - // Reset the grid - _gridManager.resetGrid(gridID, function (grid) { - if (grid == null) { - // If an error occurs, exit - console.error('[ERROR] Cannot retreive requested grid [' + gridID + ']'); - sendChatMessage('Oups, impossible de récupérer la grille ' + gridID + '!'); +GameRoom.prototype._startInactivityTimer = function () { + var self = this; + this._inactivityTimer = setInterval(function () { + if (Date.now() - self._lastActivity >= ROOM_INACTIVITY_MS) { + console.log('[SERVER] Room ' + self.id + ' — closed after 60 min of inactivity'); + self.destroy(); } - else { - infos = _gridManager.getGridInfos(); - sendChatMessage('Grille ' + infos.provider + ' ' + infos.id + ' (Niveau ' + infos.level + ') prête !'); - - // Send reset order to clients - _io.sockets.emit('grid_reset'); - } - }); -} - -function playerLog (socket, nick, monsterId) { - var gridInfos = _gridManager.getGridInfos(); + }, 60 * 1000); // check every minute +}; - // Retreive PlayerInstance - socket.get('PlayerInstance', function (error, player) { +GameRoom.prototype.destroy = function () { + clearInterval(this._inactivityTimer); + this.broadcast('room_closed'); + // Disconnect all sockets in this room + var roomSockets = this._io.sockets.adapter.rooms.get(this.id); + if (roomSockets) { + roomSockets.forEach(function (socketId) { + var s = this._io.sockets.sockets.get(socketId); + if (s) s.disconnect(true); + }.bind(this)); + } + if (this._onInactive) this._onInactive(this.id); +}; - if (error) - console.error(error); - else { +GameRoom.prototype.broadcast = function (event, data) { + this._io.to(this.id).emit(event, data); +}; - // Set new player parameters - player.setNick(nick); - _playersManager.setMonsterToPlayer(player, monsterId); - // Refresh monster list for unready players - _io.sockets.emit('logos', _playersManager.getAvailableMonsters()); +GameRoom.prototype.sendChat = function (message, sender, color, playerList) { + if (sender === undefined) { sender = 'server'; color = SERVER_CHAT_COLOR; } + this.touchActivity(); + this.broadcast('chat', { message: message, from: sender, color: color, players: playerList }); +}; - // Bind found word event - socket.on('wordValidation', function (wordObj) { - checkWord(player, wordObj); - }); +GameRoom.prototype.sendToSocket = function (socket, message) { + socket.emit('chat', { message: message, from: 'server', color: SERVER_CHAT_COLOR }); +}; - // Notify everyone about the new client - sendChatMessage( nick + ' a rejoint la partie !
' + _playersManager.getNumberOfPlayers() + ' joueurs connectés', undefined, undefined, _playersManager.getPlayerList()); +GameRoom.prototype.startGame = function () { + var grid = this.gridManager.getGrid(); + var delay = this.playersManager.getNumberOfPlayers() > 1 ? TIME_BEFORE_START : 0; + this.gameState = enums.ServerState.OnGame; + this.broadcast('grid_event', { grid: grid, timer: delay }); +}; - // Send grid informations to the player - sendPlayerMessage(socket, 'Grille actuelle: ' + gridInfos.provider + ' ' + gridInfos.id + ' (Niveau ' + gridInfos.level + ')'); +GameRoom.prototype.resetGame = function (gridId) { + var self = this; + self.gameState = enums.ServerState.WaitingForPlayers; + self._foundWords = []; + self.lastWordFoundTs = null; + self.playersManager.resetPlayersForNewGame(); + self.gridManager.resetGrid(gridId, function (grid) { + if (!grid) { + console.error('[ERROR] Cannot retreive requested grid [' + gridId + ']'); + self.sendChat('Oups, impossible de récupérer la grille ' + gridId + ' !'); + } else { + var infos = self.gridManager.getGridInfos(); + self.sendChat('Grille ' + infos.provider + ' ' + infos.id + ' (Niveau ' + infos.level + ') prête !'); + self.broadcast('grid_reset'); + self.startGame(); } }); -} +}; -function bonusChecker(playerPoints, nbWordsRemaining) { - var bonus = { - points: 0, - bonusList: [] - }, - now = new Date().getTime(); +GameRoom.prototype.bonusChecker = function (playerPoints, nbWordsRemaining) { + var bonus = { points: 0, bonusList: [] }; + var now = Date.now(); - // If it's the first word, add 4 bonus points - if (_lastWordFoudTimestamp == null) { - bonus.bonusList.push( { title: "Preum's !", points: 4 } ); + if (this.lastWordFoundTs == null) { + bonus.bonusList.push({ title: "Preum's !", points: 4 }); bonus.points += 4; } - - // If it's the last word if (nbWordsRemaining <= 0) { - bonus.bonusList.push( { title: 'Finish him !', points: 4 } ); + bonus.bonusList.push({ title: 'Finish him !', points: 4 }); bonus.points += 4; } - - // If it's the first word since the last 2 minutes, 5 points - if ((now - _lastWordFoudTimestamp) > 120000) { - bonus.bonusList.push( { title: 'Débloqueur', points: 5 } ); + if ((now - this.lastWordFoundTs) > 120000) { + bonus.bonusList.push({ title: 'Débloqueur', points: 5 }); bonus.points += 5; } - - // If it's a big word, add 3 points if (playerPoints >= 6) { - bonus.bonusList.push( { title: 'Gros mot !', points: 3 } ); + bonus.bonusList.push({ title: 'Gros mot !', points: 3 }); bonus.points += 3; } + return bonus; +}; - return (bonus); -} - -function checkWord(player, wordObj) { - var points, - bonuses; - - // Check word - points = _gridManager.checkPlayerWord(wordObj); - - // If the players has some points, it's mean it's the right word ! Notify players about it +GameRoom.prototype.checkWord = function (player, wordObj) { + this.touchActivity(); + var points = this.gridManager.checkPlayerWord(wordObj); if (points >= 0) { - - // Notify all clients about this word wordObj.color = player.getColor(); - _io.sockets.emit('word_founded', wordObj); - - // Check for bonuses - bonuses = bonusChecker(points, _gridManager.getNbRemainingWords()); - - // Remember time this last word had been found - _lastWordFoudTimestamp = new Date().getTime(); - - // Update player score and notify clients + this._foundWords.push({ word: wordObj.word, axis: wordObj.axis, start: wordObj.start, color: wordObj.color }); + this.broadcast('word_founded', wordObj); + var bonuses = this.bonusChecker(points, this.gridManager.getNbRemainingWords()); + this.lastWordFoundTs = Date.now(); player.updateScore(points + bonuses.points); - _io.sockets.emit('score_update', { playerID: player.getID(), score: player.getScore(), words: player.getNbWords(), progress: _gridManager.getAccomplishmentRate(player.getScore(), _playersManager.getNumberOfPlayers()), bonus: bonuses.bonusList } ); - if (_gridManager.getNbRemainingWords() <= 0) { - console.log('[SERVER] Game over ! Sending player\'s notification...'); - _io.sockets.emit('game_over', _playersManager.getWinner().getPlayerObject()); + var chatMsg = '' + player.getNick() + ' a trouvé ' + wordObj.word + ' (+' + points + ' pts)'; + if (bonuses.bonusList.length > 0) { + for (var b = 0; b < bonuses.bonusList.length; b++) { + chatMsg += ' 🏆 ' + bonuses.bonusList[b].title + ' (+' + bonuses.bonusList[b].points + ')'; + } + } + chatMsg += ' !'; + this.sendChat(chatMsg); + + this.broadcast('score_update', { + playerID: player.getID(), + score: player.getScore(), + words: player.getNbWords(), + progress: this.gridManager.getAccomplishmentRate(player.getScore(), this.playersManager.getNumberOfPlayers()), + bonus: bonuses.bonusList + }); + + if (this.gridManager.getNbRemainingWords() <= 0) { + console.log('[SERVER] Room ' + this.id + ' — game over!'); + this.broadcast('game_over', this.playersManager.getWinner().getPlayerObject()); } } -} - -function checkServerCommand(message) { - var number; - - // If it's not a server command - if (message[0] != '!') - return (false); +}; - // Check the start command - if ((_gameState == enums.ServerState.WaitingForPlayers) && (message == '!start')) { - startGame(); - return (true); +// Send full game state to a (re)joining socket: grid, found words, all scores +GameRoom.prototype.sendGameState = function (socket, player) { + socket.emit('grid_event', { grid: this.gridManager.getGrid(), timer: 0 }); + if (this._foundWords.length > 0) { + socket.emit('found_words', this._foundWords); } - - // Check the change grid command - if (message.indexOf('!grid') >= 0) { - // Retreive grid number and reset game parameters - number = parseInt(message.substr(6)); - resetGame(number); - return (true); + // Send score_update for every player so the panel is fully populated + var players = this.playersManager.getPlayerList(); + var nbPlayers = this.playersManager.getNumberOfPlayers(); + for (var i = 0; i < players.length; i++) { + socket.emit('score_update', { + playerID: players[i].id, + score: players[i].score, + words: players[i].nbWords, + progress: this.gridManager.getAccomplishmentRate(players[i].score, nbPlayers), + bonus: [] + }); } +}; - return (false); -} - -function sendChatMessage(Message, sender, color, playerList) { - if (sender === undefined) { - sender = 'server'; - color = SERVER_CHAT_COLOR; +GameRoom.prototype.checkServerCommand = function (message) { + if (message[0] !== '!') return false; + if (this.gameState === enums.ServerState.WaitingForPlayers && message === '!start') { + this.startGame(); + return true; + } + if (message.indexOf('!grid') === 0) { + var number = parseInt(message.substr(6)); + this.resetGame(isNaN(number) ? 0 : number); + return true; } + return false; +}; - _io.sockets.emit('chat', { message: Message, from: sender, color: color, players: playerList } ); -} +GameRoom.prototype.playerLog = function (socket, nick, monsterId) { + var self = this; + var player = socket.playerInstance; + if (!player) { console.error('No PlayerInstance on socket'); return; } -function sendPlayerMessage(socket, Message) { - socket.emit('chat', { message: Message, from: 'server', color: SERVER_CHAT_COLOR }); -} + var gridInfos = this.gridManager.getGridInfos(); + player.setNick(nick); + this.playersManager.setMonsterToPlayer(player, monsterId); + // Refresh available monsters for everyone in this room + this.broadcast('logos', this.playersManager.getAvailableMonsters()); -/** - * Start mfl server. - */ -exports.startMflServer = function (desiredGrid) { - // Instanciiate io module with proper parameters - _io = require('socket.io').listen(config.SOCKET_PORT); - _io.configure(function(){ - _io.set('log level', 2); + // Bind word validation for this player + socket.on('wordValidation', function (wordObj) { + if (!wordObj || typeof wordObj.word !== 'string' || typeof wordObj.start !== 'number') return; + if (wordObj.axis !== 0 && wordObj.axis !== 1) return; + if (wordObj.word.length === 0 || wordObj.word.length > 50) return; + self.checkWord(player, wordObj); }); - // Retreive the grid - _gridManager = new GridManager(); - _gridManager.retreiveAndParseGrid(desiredGrid, function (grid) { - if (grid == null) { - // If an error occurs, exit - console.error('[ERROR] Cannot retreive grid. Abort server.'); - process.exit(1); - } - }); + this.sendChat( + nick + ' a rejoint la partie !
' + this.playersManager.getNumberOfPlayers() + ' joueurs connectés', + undefined, undefined, + this.playersManager.getPlayerList() + ); + this.sendToSocket(socket, 'Grille actuelle : ' + gridInfos.provider + ' ' + gridInfos.id + ' (Niveau ' + gridInfos.level + ')'); +}; - // Create playersManager instance and register events - _playersManager = new PlayersManager(); - _playersManager.on('players-ready', function () { -}); +// ─── Server entry point ─────────────────────────────────────────────────────── - // On new client connection - _io.sockets.on('connection', function (socket) { +exports.startMflServer = function (desiredGrid, httpServer) { + var { Server } = require('socket.io'); + var io = new Server(httpServer, { cors: { origin: '*' } }); - // If it remains slots in the room, add player and bind events - if ((_gameState == enums.ServerState.WaitingForPlayers) && (_playersManager.getNumberOfPlayers() < MAX_PLAYERS)) { - - // Add new player - var player = _playersManager.addNewPlayer(socket); - - // Register to socket events - socket.on('disconnect', function () { - // When a player disconnect, retreive player instance - socket.get('PlayerInstance', function (error, player) { - sendChatMessage( player.getNick() + ' a quitté la partie'); - _playersManager.removePlayer(player); - player = null; - }); + var rooms = new Map(); // roomId → GameRoom + + // ── Helpers ── + + function getRoomList() { + var list = []; + rooms.forEach(function (room) { + list.push({ + id: room.id, + playerCount: room.playersManager.getNumberOfPlayers(), + gameState: room.gameState, + gridReady: room.gridReady, + gridInfo: room.gridReady ? room.gridManager.getGridInfos() : null + }); + }); + return list; + } + + function broadcastRoomList() { + io.emit('roomList', getRoomList()); + } + + // ── Per-socket game logic ── + + function bindChatHandler(socket, room) { + socket.on('chat', function (message) { + if (typeof message !== 'string') return; + message = message.trim().substring(0, 200); + if (!message) return; + if (room.checkServerCommand(message) === false) { + var p = socket.playerInstance; + if (p) room.sendChat(message, p.getNick(), p.getColor()); + } + }); + } + + function bindWordHandler(socket, room, player) { + socket.on('wordValidation', function (wordObj) { + if (!wordObj || typeof wordObj.word !== 'string' || typeof wordObj.start !== 'number') return; + if (wordObj.axis !== 0 && wordObj.axis !== 1) return; + if (wordObj.word.length === 0 || wordObj.word.length > 50) return; + room.checkWord(player, wordObj); + }); + } + + function registerPlayerInRoom(socket, room) { + room.touchActivity(); + var isWaiting = room.gameState === enums.ServerState.WaitingForPlayers; + var hasSlot = room.playersManager.getNumberOfPlayers() < MAX_PLAYERS; + + // ── Normal pre-game join ── + if (isWaiting && hasSlot) { + var player = room.playersManager.addNewPlayer(socket); + socket.playerInstance = player; + socket.on('disconnect', function () { + var p = socket.playerInstance; + if (!p) return; + if (room.gameState === enums.ServerState.WaitingForPlayers) { + room.sendChat(p.getNick() + ' a quitté la partie'); + room.playersManager.removePlayer(p); + if (room.playersManager.getNumberOfPlayers() === 0) { + clearInterval(room._inactivityTimer); + rooms.delete(room.id); + } + } else { + room.sendChat(p.getNick() + ' s\'est déconnecté (peut revenir avec le même pseudo)'); + } + broadcastRoomList(); }); socket.on('userIsReady', function (infos) { - // Log player, bind events and notify everyone - if (_gameState == enums.ServerState.WaitingForPlayers) - playerLog(socket, infos.nick, infos.monster); - else // Notify game has started - socket.disconnect('game_already_started'); + if (!infos || typeof infos.nick !== 'string') return; + var nick = infos.nick.trim().substring(0, 20); + if (!nick) return; + + if (room.gameState === enums.ServerState.WaitingForPlayers) { + room.playerLog(socket, nick, infos.monster); + broadcastRoomList(); + } else { + // Started while player was on login screen + var rejoiner = room.playersManager.findPlayerByNick(nick); + if (rejoiner) { + rejoiner.updateSocket(socket); + socket.playerInstance = rejoiner; + room.sendChat('' + nick + ' a rejoint la partie !', undefined, undefined, room.playersManager.getPlayerList()); + room.sendGameState(socket, rejoiner); + } else { + socket.emit('game_already_started'); + socket.disconnect(true); + } + } }); - socket.on('chat', function (message) { - // If it's a message for the server, treat it - // Else broadcast the message to everyone - if (checkServerCommand(message) == false) { - socket.get('PlayerInstance', function (error, player) { - sendChatMessage(message, player.getNick(), player.getColor()); - }); + bindChatHandler(socket, room); + socket.emit('logos', room.playersManager.getAvailableMonsters()); + + // ── Game already in progress — allow rejoin or late join ── + } else { + socket.emit('logos', hasSlot ? room.playersManager.getAvailableMonsters() : null); + + socket.once('userIsReady', function (infos) { + if (!infos || typeof infos.nick !== 'string') return; + var nick = infos.nick.trim().substring(0, 20); + if (!nick) return; + + var rejoiner = room.playersManager.findPlayerByNick(nick); + + if (rejoiner) { + // Reconnect existing player + rejoiner.updateSocket(socket); + socket.playerInstance = rejoiner; + room.sendChat('' + nick + ' a rejoint la partie !', undefined, undefined, room.playersManager.getPlayerList()); + room.sendGameState(socket, rejoiner); + bindChatHandler(socket, room); + bindWordHandler(socket, room, rejoiner); + + } else if (room.playersManager.getNumberOfPlayers() < MAX_PLAYERS) { + // New player joining a game already underway + var player = room.playersManager.addNewPlayer(socket); + socket.playerInstance = player; + room.playerLog(socket, nick, infos.monster); + room.sendGameState(socket, player); + bindChatHandler(socket, room); + broadcastRoomList(); + + } else { + socket.emit('game_already_started'); } }); + } + } - // Remember PlayerInstance and push it to the player list - socket.set('PlayerInstance', player); + // ── Socket.IO connection handler ── - // Send to the player availables logos - socket.emit('logos', _playersManager.getAvailableMonsters()); - } - // Else notify players he can't play for the moment - else { - // To do it, returns an empty list of available logos == null - socket.emit('logos', null); - } + io.on('connection', function (socket) { + // Immediately send current room list so the lobby can populate + socket.emit('roomList', getRoomList()); + + socket.on('createRoom', function (options) { + var roomId = generateRoomId(); + while (rooms.has(roomId)) roomId = generateRoomId(); + + var room = new GameRoom(roomId, io, function (id) { + rooms.delete(id); + broadcastRoomList(); + }); + rooms.set(roomId, room); + + var gridNum = (options && options.gridNumber !== undefined && !isNaN(parseInt(options.gridNumber))) + ? parseInt(options.gridNumber) : (desiredGrid || 0); + + room.gridManager.retreiveAndParseGrid(gridNum, function (grid) { + if (!grid) { + socket.emit('roomError', 'Impossible de charger la grille'); + rooms.delete(roomId); + broadcastRoomList(); + return; + } + room.gridReady = true; + broadcastRoomList(); + }); + socket.join(roomId); + socket.roomId = roomId; + socket.emit('roomJoined', { roomId: roomId }); + registerPlayerInRoom(socket, room); + broadcastRoomList(); + }); + + socket.on('joinRoom', function (roomId) { + if (typeof roomId !== 'string') return; + roomId = roomId.toUpperCase().trim().substring(0, 8); + var room = rooms.get(roomId); + if (!room) { + socket.emit('roomError', 'Salle "' + roomId + '" introuvable'); + return; + } + socket.join(roomId); + socket.roomId = roomId; + socket.emit('roomJoined', { roomId: roomId }); + registerPlayerInRoom(socket, room); + }); + + socket.on('getRoomList', function () { + socket.emit('roomList', getRoomList()); + }); }); - - // Set game state and print ready message - _gameState = enums.ServerState.WaitingForPlayers; - console.log('Game started and waiting for players on port ' + config.SOCKET_PORT); + console.log('Game server started — waiting for connections.'); }; diff --git a/game_files/player.js b/game_files/player.js index 38f94b8..b575ccc 100644 --- a/game_files/player.js +++ b/game_files/player.js @@ -28,6 +28,10 @@ Player.prototype.updateScore = function (points) { this._playerTinyObject.nbWords++; }; +Player.prototype.updateSocket = function (newSocket) { + this._socket = newSocket; +}; + Player.prototype.resetPlayerInfos = function () { this._playerTinyObject.score = 0; this._playerTinyObject.nbWords = 0; diff --git a/game_files/playersManager.js b/game_files/playersManager.js index ab4dd07..ad402c3 100644 --- a/game_files/playersManager.js +++ b/game_files/playersManager.js @@ -2,85 +2,89 @@ var util = require('util'), EventEmitter = require('events').EventEmitter, Player = require('./player'), enums = require('./enums'), - Monsters = require('./playersLogos').Monsters; - -var _playersList = [] - _currentPlayerId = 0; + PlayersLogos = require('./playersLogos'); function PlayersManager () { EventEmitter.call(this); + // Instance state (not module-level variables) + this._playersList = []; + this._currentPlayerId = 0; + // Each room gets its own copy of the monsters list so rooms don't conflict + this._monsters = PlayersLogos.Monsters.map(function (m) { + return { id: m.id, path: m.path, color: m.color, player: null }; + }); } util.inherits(PlayersManager, EventEmitter); PlayersManager.prototype.addNewPlayer = function (playerSocket) { - var newPlayer; - - // Create new player and add it in the list - newPlayer = new Player(playerSocket, _currentPlayerId++); - _playersList.push(newPlayer); - - console.info('New player connected. There is currently ' + _playersList.length + ' player(s)'); - + var newPlayer = new Player(playerSocket, this._currentPlayerId++); + this._playersList.push(newPlayer); + console.info('New player connected. There is currently ' + this._playersList.length + ' player(s)'); return (newPlayer); }; PlayersManager.prototype.removePlayer = function (player) { - var pos = _playersList.indexOf(player); + var pos = this._playersList.indexOf(player); if (pos < 0) { console.error("[ERROR] Can't find player in playerList"); } else { console.info('Removing player ' + player.getNick()); - _playersList.splice(pos, 1); - console.info('It remains ' + _playersList.length + ' player(s)'); + this._playersList.splice(pos, 1); + console.info('It remains ' + this._playersList.length + ' player(s)'); } }; PlayersManager.prototype.getPlayerList = function () { var players = [], - nbPlayers = _playersList.length, + nbPlayers = this._playersList.length, i; for (i = 0; i < nbPlayers; i++) { - players.push(_playersList[i].getPlayerObject()); - }; + players.push(this._playersList[i].getPlayerObject()); + } return (players); }; PlayersManager.prototype.getNumberOfPlayers = function () { - return (_playersList.length); -} + return (this._playersList.length); +}; PlayersManager.prototype.getAvailableMonsters = function () { var availableMonsters = [], i, - nbLogos = Monsters.length; + nbLogos = this._monsters.length; for (i = 0; i < nbLogos; i++) { - if (Monsters[i].player == null) - availableMonsters.push(Monsters[i]); - }; + if (this._monsters[i].player == null) + availableMonsters.push(this._monsters[i]); + } return (availableMonsters); }; PlayersManager.prototype.setMonsterToPlayer = function (player, monsterId) { - if ((monsterId > (Monsters.length - 1)) || (Monsters[monsterId].player != null)) { + if ((monsterId > (this._monsters.length - 1)) || (this._monsters[monsterId].player != null)) { console.error('[ERROR] Monster ' + monsterId + ' seems to be unavailable'); - - // Set the first available monster to this user monsterId = 0; - while (Monsters[monsterId].player != null) + while (this._monsters[monsterId].player != null) monsterId++; } - // Set monster to this player - player.setMonster(Monsters[monsterId]); - Monsters[monsterId].player = player.getID(); + player.setMonster(this._monsters[monsterId]); + this._monsters[monsterId].player = player.getID(); +}; + +PlayersManager.prototype.findPlayerByNick = function (nick) { + for (var i = 0; i < this._playersList.length; i++) { + if (this._playersList[i].getNick() === nick) + return (this._playersList[i]); + } + return (null); }; PlayersManager.prototype.getWinner = function () { @@ -88,24 +92,27 @@ PlayersManager.prototype.getWinner = function () { bestScore = 0, winnerIndex; - // Look in all players which one has the bigger score - for (i in _playersList) { - if (_playersList[i].getScore() > bestScore) { - bestScore = _playersList[i].getScore(); + for (i in this._playersList) { + if (this._playersList[i].getScore() > bestScore) { + bestScore = this._playersList[i].getScore(); winnerIndex = i; } - }; + } - // Return the high score player - return (_playersList[winnerIndex]); + return (this._playersList[winnerIndex]); }; PlayersManager.prototype.resetPlayersForNewGame = function () { - var index; + var index, i; - for (index in _playersList) { - _playersList[index].resetPlayerInfos(); - }; + for (index in this._playersList) { + this._playersList[index].resetPlayerInfos(); + } + + // Reset all monster assignments so they become available again + for (i = 0; i < this._monsters.length; i++) { + this._monsters[i].player = null; + } }; diff --git a/package-lock.json b/package-lock.json index 5f9df74..da1c554 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,667 +1,1520 @@ { "name": "Mots.js", "version": "0.1.0", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "acorn": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", - "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=" - }, - "acorn-globals": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", - "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", - "requires": { - "acorn": "^2.1.0" - } - }, - "active-x-obfuscator": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/active-x-obfuscator/-/active-x-obfuscator-0.0.1.tgz", - "integrity": "sha1-CJuJs3FF/x2ex0r2UwvlUmyuHxo=", - "requires": { - "zeparser": "0.0.5" - } - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + "packages": { + "": { + "name": "Mots.js", + "version": "0.1.0", + "dependencies": { + "express": "^4.18.2", + "prompts": "^2.1.0", + "pug": "^3.0.2", + "socket.io": "^4.7.2" + } }, - "asap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz", - "integrity": "sha1-sqRdpf36ILBJb8N2jMJ8EvqRan0=" + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "base64id": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-0.1.0.tgz", - "integrity": "sha1-As4P3u4M709ACA4ec+g08LG/zj8=" + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "batch": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.5.0.tgz", - "integrity": "sha1-/S4Fp6XWlrTbkxQBPihdj/NVfsM=" + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } }, - "buffer-crc32": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.1.tgz", - "integrity": "sha1-vj5TgvwCttYySVasGvmKqYsIU0w=" + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } }, - "bytes": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-0.2.1.tgz", - "integrity": "sha1-VVsIq8sGP4l1kFMCUj5M1P/f3zE=" + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } }, - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" } }, - "character-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.1.tgz", - "integrity": "sha1-wN3kqxgnE7kZuXCVmhI+zBow/NY=" - }, - "clean-css": { - "version": "3.4.28", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", - "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", - "requires": { - "commander": "2.8.x", - "source-map": "0.4.x" - }, - "dependencies": { - "commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", - "requires": { - "graceful-readlink": ">= 1.0.0" - } - } + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" } }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" - } + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/assert-never": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "license": "MIT" + }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" } }, - "commander": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-1.3.2.tgz", - "integrity": "sha1-io8w7GcKb91kr1LxkUuQfXnq1bU=", - "requires": { - "keypress": "0.1.x" - } - }, - "connect": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-2.12.0.tgz", - "integrity": "sha1-Mdj6DcrN8ZCNgivSkjvootKn7Zo=", - "requires": { - "batch": "0.5.0", - "buffer-crc32": "0.2.1", - "bytes": "0.2.1", - "cookie": "0.1.0", - "cookie-signature": "1.0.1", - "debug": ">= 0.7.3 < 1", - "fresh": "0.2.0", - "methods": "0.1.0", - "multiparty": "2.2.0", - "negotiator": "0.3.0", - "pause": "0.0.1", - "qs": "0.6.6", - "raw-body": "1.1.2", - "send": "0.1.4", - "uid2": "0.0.3" - } - }, - "constantinople": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.0.2.tgz", - "integrity": "sha1-S5RdmTeQe82Y7ldRIsOBdRZUQUE=", - "requires": { - "acorn": "^2.1.0" - } - }, - "cookie": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz", - "integrity": "sha1-kOtGndzpBchm3mh+/EMTHYgB+dA=" + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } }, - "cookie-signature": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.1.tgz", - "integrity": "sha1-ROByFIrwHm6OJK+/EmkNaK5pjss=" + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "css": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz", - "integrity": "sha1-k4aBHKgrzMnuf7WnMrHioxfIo+c=", - "requires": { - "css-parse": "1.0.4", - "css-stringify": "1.0.5" + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, - "css-parse": { + "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz", - "integrity": "sha1-OLBQP7+dqfVOnB29pg4UXHcRe90=" + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } }, - "css-stringify": { + "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz", - "integrity": "sha1-sNBClG2ylTu50pKQCmy19tASIDE=" + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "debug": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-0.8.1.tgz", - "integrity": "sha1-IP9NJvXkIstoobrLu2EDmtjBwTA=" + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, - "decamelize": { + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "express": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/express/-/express-3.4.8.tgz", - "integrity": "sha1-qnqJht4HBTM39Lxe2aZFPZzI4uE=", - "requires": { - "buffer-crc32": "0.2.1", - "commander": "1.3.2", - "connect": "2.12.0", - "cookie": "0.1.0", - "cookie-signature": "1.0.1", - "debug": ">= 0.7.3 < 1", - "fresh": "0.2.0", - "merge-descriptors": "0.0.1", - "methods": "0.1.0", - "mkdirp": "0.3.5", - "range-parser": "0.0.4", - "send": "0.1.4" - } - }, - "fresh": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.0.tgz", - "integrity": "sha1-v9lALPPfEsSkwxDHn5mj3eE9NKc=" + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "license": "MIT" }, - "graceful-readlink": { + "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "is-buffer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", - "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" + "node_modules/engine.io": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz", + "integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "jade": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/jade/-/jade-1.11.0.tgz", - "integrity": "sha1-nIDlOMEtP7lcjZu5VZ+gzAQEBf0=", - "requires": { - "character-parser": "1.2.1", - "clean-css": "^3.1.9", - "commander": "~2.6.0", - "constantinople": "~3.0.1", - "jstransformer": "0.0.2", - "mkdirp": "~0.5.0", - "transformers": "2.1.0", - "uglify-js": "^2.4.19", - "void-elements": "~2.0.1", - "with": "~4.0.0" - }, - "dependencies": { - "commander": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", - "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "jstransformer": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-0.0.2.tgz", - "integrity": "sha1-eq4pqQPRls+glz2IXT5HlH7Ndqs=", - "requires": { - "is-promise": "^2.0.0", - "promise": "^6.0.1" + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, - "keypress": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.1.0.tgz", - "integrity": "sha1-SjGI1CkbZrT2XtuZ+AaqmuKTWSo=" + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "longest": { + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } }, - "merge-descriptors": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.1.tgz", - "integrity": "sha1-L/CYDJJM+B0LXR+2ARd8uLtWwNA=" + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "methods": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/methods/-/methods-0.1.0.tgz", - "integrity": "sha1-M11Cnu/SG3us8unJIqjSvRSjDk8=" + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "mime": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", - "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=" + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } }, - "mkdirp": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", - "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=" + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, - "multiparty": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-2.2.0.tgz", - "integrity": "sha1-pWfCrwAK0i3I8qZT2Rl4rh9TFvQ=", - "requires": { - "readable-stream": "~1.1.9", - "stream-counter": "~0.2.0" + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "nan": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-1.0.0.tgz", - "integrity": "sha1-riT4hQgY1mL8q1rPfzuVv6oszzg=" + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-expression/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "negotiator": { + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "license": "MIT" + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.3.0.tgz", - "integrity": "sha1-cG1pLv7d9XTVfqn7GriaT6fuj2A=" + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } }, - "optimist": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", - "requires": { - "wordwrap": "~0.0.2" + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "options": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", - "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "pause": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", - "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "policyfile": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/policyfile/-/policyfile-0.0.4.tgz", - "integrity": "sha1-1rgurZiueeviKOLa9ZAzEeyYLk0=" + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } }, - "promise": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-6.1.0.tgz", - "integrity": "sha1-LOcp9rlLRcJoka0GAsXJDgTG7vY=", - "requires": { - "asap": "~1.0.0" + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "prompts": { + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/prompts": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.1.0.tgz", "integrity": "sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg==", - "requires": { + "dependencies": { "kleur": "^3.0.2", "sisteransi": "^1.0.0" + }, + "engines": { + "node": ">= 6" } }, - "qs": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/qs/-/qs-0.6.6.tgz", - "integrity": "sha1-bgFQmP9RlouKPIGQAdXyyJvEsQc=" + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } }, - "range-parser": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-0.0.4.tgz", - "integrity": "sha1-wEJ//vUcEKy6B4KkbJYC50T/Ygs=" + "node_modules/pug": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.4.tgz", + "integrity": "sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg==", + "license": "MIT", + "dependencies": { + "pug-code-gen": "^3.0.4", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } }, - "raw-body": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.2.tgz", - "integrity": "sha1-x0swBN6l3v0WlhcRBqx0DsMdYr4=", - "requires": { - "bytes": "~0.2.1" - } - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "redis": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/redis/-/redis-0.7.3.tgz", - "integrity": "sha1-7le3pE0l7BWU5ENl2BZfp9HUgRo=", - "optional": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "requires": { - "align-text": "^0.1.1" - } - }, - "send": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/send/-/send-0.1.4.tgz", - "integrity": "sha1-vnDY0b4B3mGCGvE3gLUDRaT3Gr0=", - "requires": { - "debug": "*", - "fresh": "0.2.0", - "mime": "~1.2.9", - "range-parser": "0.0.4" - } - }, - "sisteransi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.0.tgz", - "integrity": "sha512-N+z4pHB4AmUv0SjveWRd6q1Nj5w62m5jodv+GD8lvmbY/83T/rpbJGZOnK5T149OldDj4Db07BSv9xY4K6NTPQ==" + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } }, - "socket.io": { - "version": "0.9.19", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-0.9.19.tgz", - "integrity": "sha1-SQu1/Q3FTPAC7gTmf638Q7hIo48=", - "requires": { - "base64id": "0.1.0", - "policyfile": "0.0.4", - "redis": "0.7.3", - "socket.io-client": "0.9.16" - } - }, - "socket.io-client": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-0.9.16.tgz", - "integrity": "sha1-TadRXF53MEHRtCOXBBW8xDDzX8Y=", - "requires": { - "active-x-obfuscator": "0.0.1", - "uglify-js": "1.2.5", - "ws": "0.4.x", - "xmlhttprequest": "1.4.2" - }, - "dependencies": { - "uglify-js": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.2.5.tgz", - "integrity": "sha1-tULCx29477NLIAsgF3Y0Mw/3ArY=" - } + "node_modules/pug-attrs/node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" } }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "requires": { - "amdefine": ">=0.0.4" + "node_modules/pug-code-gen": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.4.tgz", + "integrity": "sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g==", + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" } }, - "stream-counter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/stream-counter/-/stream-counter-0.2.0.tgz", - "integrity": "sha1-3tJmVWMZyLDiIoErnPOyb6fZR94=", - "requires": { - "readable-stream": "~1.1.8" + "node_modules/pug-code-gen/node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" } }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + "node_modules/pug-code-gen/node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "tinycolor": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tinycolor/-/tinycolor-0.0.1.tgz", - "integrity": "sha1-MgtaUtg6u1l42Bo+iH1K77FaYWQ=" + "node_modules/pug-code-gen/node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } }, - "transformers": { + "node_modules/pug-error": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz", - "integrity": "sha1-XSPLNVYd2F3Gf7hIIwm0fVPM6ac=", - "requires": { - "css": "~1.0.8", - "promise": "~2.0", - "uglify-js": "~2.2.5" - }, - "dependencies": { - "is-promise": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", - "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" - }, - "promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz", - "integrity": "sha1-RmSKqdYFr10ucMMCS/WUNtoCuA4=", - "requires": { - "is-promise": "~1" - } + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "license": "MIT" + }, + "node_modules/pug-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "license": "MIT", + "dependencies": { + "constantinople": "^4.0.1", + "jstransformer": "1.0.0", + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" + } + }, + "node_modules/pug-filters/node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/pug-filters/node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, + "node_modules/pug-filters/node_modules/jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "license": "MIT", + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "node_modules/pug-filters/node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/pug-lexer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "license": "MIT", + "dependencies": { + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-lexer/node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "license": "MIT", + "dependencies": { + "is-regex": "^1.0.3" + } + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "license": "MIT" + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "license": "MIT", + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "source-map": { - "version": "0.1.43", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", - "requires": { - "amdefine": ">=0.0.4" - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "uglify-js": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz", - "integrity": "sha1-puAqcNg5eSuXgEiLe4sYTAlcmcc=", - "requires": { - "optimist": "~0.3.5", - "source-map": "~0.1.7" - } + { + "type": "consulting", + "url": "https://feross.org/support" } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" } }, - "uglify-js": { - "version": "2.8.27", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.27.tgz", - "integrity": "sha1-R3h/kSsPJC5bmENDvo416V9pTJw=", - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { - "source-map": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" - } + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "uglify-to-browserify": { + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "optional": true + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "uid2": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", - "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=" + "node_modules/sisteransi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.0.tgz", + "integrity": "sha512-N+z4pHB4AmUv0SjveWRd6q1Nj5w62m5jodv+GD8lvmbY/83T/rpbJGZOnK5T149OldDj4Db07BSv9xY4K6NTPQ==" }, - "void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" - }, - "with": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/with/-/with-4.0.3.tgz", - "integrity": "sha1-7v0VTp550sjTQXtkeo8U2f7M4U4=", - "requires": { - "acorn": "^1.0.1", - "acorn-globals": "^1.0.3" - }, - "dependencies": { - "acorn": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", - "integrity": "sha1-yM4n3grMdtiW0rH6099YjZ6C8BQ=" + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } }, - "ws": { - "version": "0.4.32", - "resolved": "https://registry.npmjs.org/ws/-/ws-0.4.32.tgz", - "integrity": "sha1-eHphVEFPPJntg8V3IVOyD+sM7DI=", - "requires": { - "commander": "~2.1.0", - "nan": "~1.0.0", - "options": ">=0.0.5", - "tinycolor": "0.x" + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "commander": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", - "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "xmlhttprequest": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.4.2.tgz", - "integrity": "sha1-AUU6HZvtHo8XL2SVu/TIxCYyFQA=" + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" } }, - "zeparser": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/zeparser/-/zeparser-0.0.5.tgz", - "integrity": "sha1-A3JlYbwmjy5URPVMZlt/1KjAKeI=" + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 163869d..f9ddb22 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,13 @@ "scripts": { "start": "node server.js" }, + "engines": { + "node": ">=18" + }, "dependencies": { - "express": "3.4.8", - "jade": "*", + "express": "^4.18.2", + "pug": "^3.0.2", "prompts": "^2.1.0", - "socket.io": "~0.9" + "socket.io": "^4.7.2" } } diff --git a/public/javascripts/game/UITools.js b/public/javascripts/game/UITools.js index 098fe64..2db0d76 100644 --- a/public/javascripts/game/UITools.js +++ b/public/javascripts/game/UITools.js @@ -22,14 +22,18 @@ define(function () { }; // Find an available space within empty spaces (fuck the ads !!!) - if (emptyNodes) { + if (emptyNodes.length > 0) { square.x = emptyNodes[0].offsetLeft; square.y = emptyNodes[0].offsetTop; square.width = emptyNodes[emptyNodes.length - 1].offsetLeft + emptyNodes[emptyNodes.length - 1].offsetWidth - square.x; square.height = emptyNodes[emptyNodes.length - 1].offsetTop + emptyNodes[emptyNodes.length - 1].offsetHeight - square.y; // Now put the info panel on the grid - document.getElementById('gs-grid-container').innerHTML += '
'; + var igInfos = document.createElement('div'); + igInfos.id = 'ig-infos'; + igInfos.style.cssText = 'left: ' + square.x + 'px; top: ' + square.y + 'px; width: ' + square.width + 'px; height: ' + square.height + 'px;'; + igInfos.innerHTML = '
'; + document.getElementById('gs-grid-container').appendChild(igInfos); } } @@ -141,7 +145,7 @@ define(function () { if (oldSelection) oldSelection.classList.remove('myMonster'); - event.srcElement.classList.add('myMonster'); + event.target.classList.add('myMonster'); } }; }; @@ -154,9 +158,38 @@ define(function () { var timeNode, time = 0; - // First inject the game info panel + // Display grid infos below the grid + var container = document.getElementById('gs-grid-container'); + var oldBar = document.getElementById('gs-grid-infos'); + if (oldBar) oldBar.parentNode.removeChild(oldBar); + + // Calculate grid bottom from actual frame positions + var frames = container.querySelectorAll('.frame'); + var gridBottom = 0; + for (var f = 0; f < frames.length; f++) { + var bottom = frames[f].offsetTop + frames[f].offsetHeight; + if (bottom > gridBottom) gridBottom = bottom; + } + + var gridDate = infos.date ? new Date(infos.date) : new Date(); + var dateStr = gridDate.toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); + var levelStars = ''; + for (var s = 0; s < infos.level; s++) levelStars += '★'; + + var bar = document.createElement('div'); + bar.id = 'gs-grid-infos'; + bar.style.top = (gridBottom + 6) + 'px'; + bar.innerHTML = '' + dateStr + '' + + 'Grille n°' + infos.id + '' + + 'Difficulté : ' + levelStars + ' (' + infos.level + ')'; + container.appendChild(bar); + + // Inject the in-game info panel (timer, level) inside empty cells injectInGameInfoPanel(); + // If the grid has no empty cells the panel could not be injected — skip timer + if (!document.getElementById('ig-infos')) return; + // Retreive time node and inject timer timeNode = document.querySelector('#ig-infos > time'); timeNode.innerHTML = formatTime(time); @@ -180,9 +213,11 @@ define(function () { if (_gameTimer != null) window.clearInterval(_gameTimer); + if (!document.getElementById('ig-infos')) return; + // Set game over class document.querySelector('#ig-infos > header').classList.add('game-over'); - + // Put winner picture gamePanel.innerHTML += 'winner picture'; window.setTimeout(function() { @@ -196,6 +231,8 @@ define(function () { UITools.prototype.resetGridInformations = function () { if (_gameTimer != null) window.clearInterval(_gameTimer); + var gridInfosBar = document.getElementById('gs-grid-infos'); + if (gridInfosBar) gridInfosBar.parentNode.removeChild(gridInfosBar); }; /* diff --git a/public/javascripts/game/chat.js b/public/javascripts/game/chat.js index a18a10e..57eea9e 100644 --- a/public/javascripts/game/chat.js +++ b/public/javascripts/game/chat.js @@ -6,19 +6,26 @@ define(function () { var _socket = null, _notifyCallback, + _chatHandler = null, _mesNode = document.getElementById('gsc-messages'), _writeNode = document.getElementById('gsc-write'), _serverColor = null; function Chat (socket, notifyPlayerListCallback) { // Store usefull object and callback + _notifyCallback = notifyPlayerListCallback; + + // Remove previous chat listener if any (prevents duplicates on reconnect) + if (_socket && _chatHandler) { + _socket.off('chat', _chatHandler); + } _socket = socket; - _notifyCallback = notifyPlayerListCallback // On init, bind socket to receive messages - _socket.on('chat', function (messageObj) { + _chatHandler = function (messageObj) { treatChatMessage(messageObj); - }); + }; + _socket.on('chat', _chatHandler); // Bind onkeyPress of the textarea node to send messages _writeNode.onkeypress = function (event) { diff --git a/public/javascripts/game/cursor.js b/public/javascripts/game/cursor.js index c7dbf41..3664505 100644 --- a/public/javascripts/game/cursor.js +++ b/public/javascripts/game/cursor.js @@ -1,64 +1,104 @@ /* -* The cursor is used to focus a special frame and write in it +* The cursor is used to focus a special frame and write in it. +* Mobile support: a hidden captures keyboard input so the native +* iOS/Android keyboard opens when a letter cell is tapped. */ define(function () { var enumDirections = { - Left: 37, - Up: 38, + Left: 37, + Up: 38, Right: 39, - Down: 40 + Down: 40 }; var _grid, _letterUpdateCallback, _nbLines, _nbCols, - _focusCell = null; - _focusDirection = null; - + _focusCell = null, + _focusDirection = null, + _mobileInput = null; // hidden for native mobile keyboard + + /* * Constructor - * @param: {Object} gridObj Grid instance given by the server - * @param: {Function} letterUpdateCallback Function to call when we update a letter frame (insert new letter or delete current one) */ function Cursor(gridObj, letterUpdateCallback) { - // Getting Grid - _grid = gridObj.cases; - _nbLines = gridObj.nbLines; - _nbCols = gridObj.nbColumns - - // Retreive callback + _grid = gridObj.cases; + _nbLines = gridObj.nbLines; + _nbCols = gridObj.nbColumns; _letterUpdateCallback = letterUpdateCallback; } - /*------------------------- - - Private functions - - -------------------------*/ + /*----------------------------------------------------------------------- + Private helpers + -----------------------------------------------------------------------*/ /* - * Set the cursor direction - * @param: {Int} direction [Optional] If this parameter is setted, force the direction. Else just toggle the current direction + * Create (once) the hidden used to capture keyboard on mobile. + * autocapitalize="characters" nudges iOS to open the all-caps keyboard. */ + function ensureMobileInput() { + if (_mobileInput) return; + + _mobileInput = document.createElement('input'); + _mobileInput.type = 'text'; + _mobileInput.autocomplete = 'off'; + _mobileInput.autocorrect = 'off'; + _mobileInput.autocapitalize = 'characters'; + _mobileInput.setAttribute('inputmode', 'text'); + _mobileInput.style.cssText = + 'position:fixed;top:-200px;left:0;opacity:0;width:1px;height:1px;' + + 'border:none;padding:0;margin:0;font-size:16px;'; // font-size≥16 avoids iOS zoom + document.body.appendChild(_mobileInput); + + // Character typed on mobile keyboard + _mobileInput.addEventListener('input', function () { + if (!_focusCell) { _mobileInput.value = ''; return; } + var val = _mobileInput.value.toUpperCase(); + _mobileInput.value = ''; + if (!val) return; + // Take only the last character in case of autocorrect inserting multiple + var ch = val.charAt(val.length - 1); + var code = ch.charCodeAt(0); + if (code >= 65 && code <= 90) insertLetter(code); + }); + + // Backspace / delete on mobile keyboard + _mobileInput.addEventListener('keydown', function (e) { + if (!_focusCell) return; + if (e.keyCode === 8 || e.keyCode === 46) { + removeLetter(); + e.preventDefault(); + } + if (e.keyCode >= 37 && e.keyCode <= 40) { + moveCursor(e.keyCode); + e.preventDefault(); + } + }); + } + + function focusMobileInput() { + ensureMobileInput(); + _mobileInput.value = ''; + _mobileInput.focus(); + } + + function setCursorDirection(direction) { - // If no direction given, toggle current direction if (!direction) { if (_focusDirection == enumDirections.Right) { _focusCell.classList.remove('goRight'); _focusCell.classList.add('goDown'); _focusDirection = enumDirections.Down; - } - else { + } else { _focusCell.classList.remove('goDown'); _focusCell.classList.add('goRight'); _focusDirection = enumDirections.Right; } - } - // Else change direction to the one needed then apply right style - else { + } else { _focusDirection = (_focusDirection == enumDirections.Right) ? enumDirections.Down : enumDirections.Right; _focusCell.classList.remove('goRight'); _focusCell.classList.remove('goDown'); @@ -69,167 +109,121 @@ define(function () { } } - /* - * Move the cursor to the next case according to the direction - * @param: {Int} direction [Optional] If this parameter is setted, force the direction. Else just follow the cursor one. - * @return: {Bool} True if the cursor has moved, else false - */ function moveCursor(direction) { var frameNumber = parseInt(_focusCell.getAttribute('data-pos')), - index = 0; + index = 0; - // Retreive direction if not specified - if (direction == undefined) - direction = _focusDirection; + if (direction == undefined) direction = _focusDirection; - // According to the direction, check if the next frame is available switch (direction) { case enumDirections.Left: - // The first frame will always be a description frame index = frameNumber - 1; break; case enumDirections.Right: index = ((frameNumber + 1) >= _grid.length) ? 0 : (frameNumber + 1); break; case enumDirections.Up: - index = (frameNumber > _nbCols) ? (frameNumber - _nbCols) : 0; + index = (frameNumber > _nbLines) ? (frameNumber - _nbLines) : 0; break; case enumDirections.Down: - index = ((frameNumber + _nbCols) >= _grid.length) ? 0 : (frameNumber + _nbCols); + index = ((frameNumber + _nbLines) >= _grid.length) ? 0 : (frameNumber + _nbLines); break; - default: - console.log('[ERROR] [Cursor.moveCursor] Unknow direction ' + direction); + console.log('[ERROR] [Cursor.moveCursor] Unknown direction ' + direction); } - // If the next frame is a letter frame, movo on it if (_grid[index].type == 2) { - // Release old frame _focusCell.classList.remove('goRight'); _focusCell.classList.remove('goDown'); _focusCell.classList.remove('focusCell'); - // Focus new frame _focusCell = document.querySelector('.frame' + index); _focusCell.classList.add('focusCell'); if (direction == enumDirections.Left || direction == enumDirections.Right) { _focusDirection = enumDirections.Right; _focusCell.classList.add('goRight'); - } - else { + } else { _focusDirection = enumDirections.Down; _focusCell.classList.add('goDown'); } - - return (true); + return true; } - // Else do nothing - else - return (false); + return false; } - - function onClickReceived(event) { + function activateCell(target) { if (_focusCell != null) { - // If the player clicked the same frame, just toggle the cursor direction - if (_focusCell == event.target) { + if (_focusCell == target) { setCursorDirection(); + focusMobileInput(); return; } _focusCell.classList.remove('goRight'); _focusCell.classList.remove('goDown'); _focusCell.classList.remove('focusCell'); } - - // Remember the cell, focus it and set default direction - _focusCell = event.target; + _focusCell = target; _focusCell.classList.add('focusCell'); _focusCell.classList.add('goRight'); _focusDirection = enumDirections.Right; + focusMobileInput(); + } + + function onClickReceived(event) { + activateCell(event.target); } - /* - * When a letter is pressed on the grid - */ function onLetterPressed(event) { var key = event.keyCode; - - // If a letter is pressed - if ((key >= 65) && (key <= 90)) { - insertLetter(key); - } - - // If backspace / escape / del is pressed - if ((key == 8) || (key == 27) || (key == 46)) { - removeLetter(); - event.preventDefault(); - } - - // If an arrow is pressed - if ((key >= 37) && (key <= 40)) - moveCursor(key); - + if ((key >= 65) && (key <= 90)) insertLetter(key); + if ((key == 8) || (key == 27) || (key == 46)) { removeLetter(); event.preventDefault(); } + if ((key >= 37) && (key <= 40)) moveCursor(key); } - - /* - * Insert a letter in the grid - */ function insertLetter(letter) { var character = String.fromCharCode(letter), - pos = parseInt(_focusCell.getAttribute('data-pos')); + pos = parseInt(_focusCell.getAttribute('data-pos')); - // Print letter on grid if we can if ((_focusCell != null) && (_grid[pos].available == true)) { _focusCell.innerHTML = character; - - // Notify grid that a new letter is inserted _letterUpdateCallback(pos, character); } - - // Go to the next frame moveCursor(_focusDirection); } function removeLetter() { var pos = parseInt(_focusCell.getAttribute('data-pos')); - if (_grid[pos].available == true) { _focusCell.innerHTML = ''; - - // Notify grid that the letter has been removed _letterUpdateCallback(parseInt(_focusCell.getAttribute('data-pos')), null); } } - - /*------------------------- - + /*----------------------------------------------------------------------- Public methods - - -------------------------*/ + -----------------------------------------------------------------------*/ - /* - * Register to click and keyboard events - */ Cursor.prototype.RegisterEvents = function () { var letterCases = document.querySelectorAll('.letter'), - size, + size = letterCases.length, i; - // For each letter case - size = letterCases.length; for (i = 0; i < size; i++) { - // Register click event for cursor + // Desktop: click + keyboard letterCases[i].addEventListener('click', onClickReceived, false); - // Register keydown event to get letter letterCases[i].addEventListener('keydown', onLetterPressed, false); - }; + // Mobile: touchend activates the cell and opens the native keyboard. + // preventDefault() stops the 300 ms ghost click on iOS. + letterCases[i].addEventListener('touchend', function (e) { + e.preventDefault(); + activateCell(e.currentTarget); + }, { passive: false }); + } }; return (Cursor); - -}); \ No newline at end of file + +}); diff --git a/public/javascripts/game/grid.js b/public/javascripts/game/grid.js index e164e52..eaa8a3c 100644 --- a/public/javascripts/game/grid.js +++ b/public/javascripts/game/grid.js @@ -53,29 +53,12 @@ define(['cursor'], function (Cursor) { frame.setAttribute('data-col', column); frame.setAttribute('data-pos', info.pos); - switch (info.nbLines) { - case 1: - lineHeight = size; - fontSize = Math.floor(lineHeight / 5.4); - break; - - case 2: - lineHeight = Math.floor(size / info.nbLines); - fontSize = Math.floor(lineHeight / 2.6); - break; - - case 3: - lineHeight = Math.floor(size / info.nbLines); - fontSize = Math.floor(lineHeight / 1.8); - break; - - case 4: - lineHeight = Math.floor(size / info.nbLines); - fontSize = Math.round(lineHeight / 1.5); - break; - - default: - console.log('[ERROR][grid.js] Don\'t know how to display ' + info.nbLines + ' lines frame !!!'); + if (info.nbLines === 1) { + lineHeight = size; + fontSize = Math.floor(size / 5.4); + } else { + lineHeight = Math.floor(size / info.nbLines); + fontSize = Math.max(7, Math.floor(size / 5.5)); } frame.style.lineHeight = lineHeight + 'px'; @@ -84,11 +67,14 @@ define(['cursor'], function (Cursor) { // Adding description in frame for (var i = 0; i < info.nbDesc; i++) { descNode = document.createElement('span'); - + // Insert description and arrow descNode.innerHTML = info.desc[i]; - descNode.classList.add('arrow' + info.arrow[i].toString()); - + descNode.innerHTML = info.desc[i]; + if (info.arrow[i] !== null) { + descNode.classList.add('arrow' + info.arrow[i].toString()); + } + frame.appendChild(descNode); }; @@ -147,10 +133,10 @@ define(['cursor'], function (Cursor) { function getFrameAxisNumber(index, axis) { if (axis == AxisType.Horizontal) { - return (Math.floor(index / _grid.nbColumns)); + return (Math.floor(index / _grid.nbLines)); } else { - return (index % _grid.nbColumns); + return (index % _grid.nbLines); } } @@ -163,7 +149,7 @@ define(['cursor'], function (Cursor) { */ function findWord(initialPos, axis) { var word = _grid.cases[initialPos].letter, - jump = (axis == AxisType.Horizontal) ? 1 : _grid.nbColumns, // The axis will define how many frames we have to jump to retreive the next letter + jump = (axis == AxisType.Horizontal) ? 1 : _grid.nbLines, // The axis will define how many frames we have to jump to retreive the next letter i = initialPos - jump, wordAxe = getFrameAxisNumber(initialPos, axis), firstLetterIndex = 0; @@ -237,7 +223,7 @@ define(['cursor'], function (Cursor) { */ Grid.prototype.RevealWord = function (wordObj) { var index = wordObj.start, - jump = (wordObj.axis == AxisType.Horizontal) ? 1 : _grid.nbColumns, + jump = (wordObj.axis == AxisType.Horizontal) ? 1 : _grid.nbLines, size = wordObj.word.length, i, node, @@ -252,6 +238,7 @@ define(['cursor'], function (Cursor) { // Display it node = document.querySelector('.frame' + index); + if (!node) { index += jump; continue; } node.style.cssText += '-webkit-transition-delay: ' + animationDelay + 'ms; transition-delay: ' + animationDelay + 'ms; color: ' + wordObj.color; node.classList.add('reveal' + wordObj.axis); node.innerHTML = _grid.cases[index].letter; @@ -275,20 +262,20 @@ define(['cursor'], function (Cursor) { nbFrames = _grid.cases.length, i; - // First we have to retreive the min size to display the grid + // Compute frame size from the container's smaller dimension. + // On mobile the container may be very narrow so we enforce a minimum of + // 30 px per cell and let the container scroll horizontally if needed. limit = (container.offsetWidth < container.offsetHeight) ? container.offsetWidth : container.offsetHeight; - // console.log('Plus petit cote: ' + limit); - // Determine frame size frameSize = (_grid.nbLines > _grid.nbColumns) ? _grid.nbLines : _grid.nbColumns; - frameSize = Math.floor(limit / frameSize); + frameSize = Math.max(30, Math.floor(limit / frameSize)); // console.log('Taille de case: ' + frameSize); // For each frame for (i = 0; i < nbFrames; i++) { // Get line and col line = Math.floor(i / _grid.nbLines); - col = i % _grid.nbColumns; + col = i % _grid.nbLines; // Insert frame if (_grid.cases[i].type == CaseType.Letter) diff --git a/public/javascripts/game/mflEngine.js b/public/javascripts/game/mflEngine.js index b751d05..d467dbe 100644 --- a/public/javascripts/game/mflEngine.js +++ b/public/javascripts/game/mflEngine.js @@ -1,226 +1,399 @@ /* -* Game Engine +* Game Engine — handles lobby, multiplayer rooms, and solo mode */ require(['../lib/text!../../conf.json', 'UITools', 'grid', 'chat', 'score'], function (Conf, UITools, GridManager, Chat, Score) { var enumState = { - Login: 0, - Waiting: 1, - OnGame: 2 + Lobby: 0, + Login: 1, + Waiting: 2, + OnGame: 3, + Solo: 4 }; var enumPanels = { + Lobby: 'lobby-panel', Login: 'login-panel', - Game: 'game-panel', + Game: 'game-panel', Error: 'error-panel' }; - var _gameState = enumState.Login, + var _gameState = enumState.Lobby, _gridManager, _scoreManager, _ui, _chat, _socket, - _grid; + _soloGrid, + _soloScore = 0, + _countdownTimer = null, + _wordFoundedHandler = null, + _gameListenersReady = false; Conf = JSON.parse(Conf); + // ─── Bootstrap ──────────────────────────────────────────────────────────── - function startClient () { - if (typeof io == 'undefined') { - document.getElementById('ep-text').innerHTML = 'Cannot retreive socket.io file at the address ' + Conf.SOCKET_ADDR + '

Please provide a valid address.'; - _ui.ChangeGameScreen(enumPanels.Error, true); - console.log('Cannot reach socket.io file !'); + _ui = new UITools(); + _scoreManager = new Score(); + + startLobby(); + + // ─── Lobby ──────────────────────────────────────────────────────────────── + + function startLobby() { + if (typeof io === 'undefined') { + showError('Impossible de charger socket.io.
Vérifiez l\'adresse du serveur.'); return; } - // Instanciate usefull classes - _ui = new UITools(); - _scoreManager = new Score(); - - // document.getElementById('gs-loader-text').innerHTML = 'Connecting to the server...'; - _socket = io.connect((Conf.SOCKET_ADDR + ':' + Conf.SOCKET_PORT), { reconnect: false }); - _socket.on('connect', function() { - - console.log('Connection established :)'); - - // Bind server disconnect event - _socket.on('disconnect', function (reason) { - if (reason && reason == 'booted') - document.getElementById('ep-text').innerHTML = 'Désolé, la partie a déjà commencée !'; - else - document.getElementById('ep-text').innerHTML = 'Connection au serveur perdue'; - _ui.ChangeGameScreen(enumPanels.Error, true); - console.log('Connection with the server lost :( '); - }); + var socketUrl = Conf.SOCKET_ADDR + (Conf.SOCKET_PORT !== 80 && Conf.SOCKET_PORT !== 443 ? ':' + Conf.SOCKET_PORT : ''); + _socket = io.connect(socketUrl, { reconnect: false }); - // Bind login event - _socket.on('logos', function (availableLogos) { - if (_gameState == enumState.Login) { - if (availableLogos == null) { - document.getElementById('lp-infos').innerHTML = ''; - _ui.InfoTooltip(true, "Ho non, c'est balot !
Il semblerait qu'il n'y ai plus de place pour le jeu en cours."); - } - else - prepareUserLoginForm(availableLogos); - } + _socket.on('connect', function () { + console.log('Socket connecté'); + // Auto-join if URL contains a room code in the hash (#ABCD) or query (?room=ABCD) + var roomCode = getRoomCodeFromURL(); + if (roomCode) joinRoom(roomCode); + }); + + _socket.on('roomList', updateLobbyRoomList); + + _socket.on('disconnect', function () { + if (_gameState > enumState.Lobby) showError('Connexion au serveur perdue'); + }); + + _socket.on('error', function () { + showError('Impossible de se connecter au serveur.'); + }); + + // Lobby button bindings + document.getElementById('lobby-create-btn').onclick = function () { + localStorage.removeItem('mfl_nick'); + var raw = document.getElementById('lobby-grid-input').value.trim(); + var opts = {}; + if (raw && !isNaN(parseInt(raw))) opts.gridNumber = parseInt(raw); + _socket.emit('createRoom', opts); + _socket.once('roomJoined', function (data) { + setURLRoomCode(data.roomId); + enterLoginPhase(); + }); + _socket.once('roomError', function (msg) { + _ui.InfoTooltip(true, 'Erreur : ' + msg, 4000); }); - - // Display login screen and bind start button + }; + + document.getElementById('lobby-join-btn').onclick = function () { + localStorage.removeItem('mfl_nick'); + var code = document.getElementById('lobby-code-input').value.trim().toUpperCase(); + if (!code) { _ui.InfoTooltip(true, 'Entrez un code de salle', 3000); return; } + joinRoom(code); + }; + + document.getElementById('lobby-code-input').onkeydown = function (e) { + if (e.key === 'Enter') document.getElementById('lobby-join-btn').click(); + }; + + document.getElementById('lobby-solo-btn').onclick = startSoloMode; + } + + function joinRoom(roomId) { + _socket.emit('joinRoom', roomId); + _socket.once('roomJoined', function (data) { + setURLRoomCode(data.roomId); + enterLoginPhase(); + }); + _socket.once('roomError', function (msg) { + setURLRoomCode(''); + _ui.InfoTooltip(true, 'Salle introuvable : ' + msg, 4000); + }); + } + + function updateLobbyRoomList(rooms) { + var container = document.getElementById('lobby-room-list'); + if (!rooms || rooms.length === 0) { + container.innerHTML = '

Aucune salle disponible — créez-en une !

'; + return; + } + var html = ''; + rooms.forEach(function (room) { + var stateCls = room.gameState === 2 ? 'room-in-game' : 'room-waiting'; + var stateLabel = room.gameState === 2 ? 'En cours' : 'En attente'; + var gridLabel = room.gridInfo + ? 'Grille ' + room.gridInfo.id + ' (niv. ' + room.gridInfo.level + ')' + : 'Chargement…'; + html += '
'; + html += '' + room.id + ''; + html += '' + gridLabel + ''; + html += '' + stateLabel + ''; + html += '' + room.playerCount + '/9 joueurs'; + html += ''; + html += '
'; + }); + container.innerHTML = html; + container.querySelectorAll('.room-join-btn').forEach(function (btn) { + btn.onclick = function () { localStorage.removeItem('mfl_nick'); joinRoom(btn.getAttribute('data-id')); }; + }); + } + + // ─── Login phase (after room joined) ────────────────────────────────────── + + function enterLoginPhase() { + _gameState = enumState.Login; + + _socket.on('game_already_started', function () { + localStorage.removeItem('mfl_nick'); + showError('Désolé, la partie a déjà commencée !'); + }); + + _socket.on('room_closed', function () { + showError('La salle a été fermée pour inactivité.'); + }); + + _socket.on('logos', function (availableLogos) { + if (_gameState > enumState.Login) return; // already past login + + var savedNick = localStorage.getItem('mfl_nick'); + + if (availableLogos == null && !savedNick) { + // No slot and no saved nick — show error + document.getElementById('lp-infos').innerHTML = ''; + _ui.InfoTooltip(true, "Ho non, c'est balot !
Il semblerait qu'il n'y ai plus de place pour le jeu en cours."); + return; + } + + // Auto-rejoin if we have a saved nick (refresh / reconnect) + if (savedNick) { + _socket.emit('userIsReady', { nick: savedNick, monster: 0 }); + setupGameListeners(); + _ui.ChangeGameScreen(enumPanels.Game, true); + _gameState = enumState.Waiting; + _ui.bindServerCommandButtons(_socket); + return; + } + + // Normal login form + prepareUserLoginForm(availableLogos); _ui.ChangeGameScreen(enumPanels.Login, true); document.getElementById('lp-start-btn').onclick = sendPlayerReady; - }); + } - _socket.on('error', function() { - document.querySelector('body').innerHTML += 'Fail to connect the WebSocket to the server.

Please check the WS address.'; - _ui.ChangeGameScreen(enumPanels.Error, true); - console.log('Cannot connect the web_socket'); + // ─── Shared game listener setup ─────────────────────────────────────────── + + function setupGameListeners() { + if (_gameListenersReady) return; + _gameListenersReady = true; + + _chat = new Chat(_socket, _scoreManager.UpdatePlayerList); + _socket.on('grid_event', onStartGame); + _socket.on('grid_reset', resetGame); + _socket.on('score_update', _scoreManager.RefreshScore); + _socket.on('game_over', function (winner) { + _ui.displayGameOver(winner); + _chat.congrats(winner); + }); + // Replay previously found words when (re)joining a game in progress. + // found_words arrives right after grid_event; onStartGame creates _gridManager + // synchronously but DisplayGrid may not have run yet — use a short delay. + _socket.on('found_words', function (words) { + function replay() { + if (!_gridManager) return; + for (var i = 0; i < words.length; i++) { + _gridManager.RevealWord(words[i]); + } + } + // Small delay to ensure grid DOM is rendered before revealing + setTimeout(replay, 300); }); - } + // ─── Login form ─────────────────────────────────────────────────────────── + function prepareUserLoginForm(logoList) { var logosNodes = '', i, - nbLogos = logoList.length; + nbLogos = logoList.length; - // Insert all available monster logo in login form for (i = 0; i < nbLogos; i++) { if (logosNodes.player == null) logosNodes += ''; - }; + } document.getElementById('lp-logos').innerHTML = logosNodes; - // Bind select event logosNodes = document.querySelectorAll('.lp-logos-monster'); - nbLogos = logosNodes.length; + nbLogos = logosNodes.length; for (i = 0; i < nbLogos; i++) { logosNodes[i].onclick = function (event) { var oldSelection = document.querySelector('.myMonster'); - - // Unset last selection if any and set the new monster - if (oldSelection) - oldSelection.classList.remove('myMonster'); - event.srcElement.classList.add('myMonster'); - - // Show the color ! - document.getElementById('lp-nick').style.borderColor = event.srcElement.style.borderColor; - } - }; + if (oldSelection) oldSelection.classList.remove('myMonster'); + event.target.classList.add('myMonster'); + document.getElementById('lp-nick').style.borderColor = event.target.style.borderColor; + }; + } } - function sendPlayerReady () { - var nick = document.getElementById('lp-nick').value, + function sendPlayerReady() { + var nick = document.getElementById('lp-nick').value, monsterNode = document.querySelector('.myMonster'), monster; - // If nick is empty or if it has the default value, if ((nick == '') || (monsterNode == null)) { - _ui.InfoTooltip(true, 'Vous devez choisir un pseudo et un petit monstre !', 4000); - return (false); + _ui.InfoTooltip(true, 'Vous devez choisir un pseudo et un petit monstre !', 4000); + return false; } - + monster = parseInt(monsterNode.getAttribute('data-monster-id'), 10); - // Unbind button event to prevent "space click" - document.getElementById('lp-start-btn').onclick = function() { return false; }; + // Prevent double-submit + document.getElementById('lp-start-btn').onclick = function () { return false; }; - // Connect chat - _chat = new Chat(_socket, _scoreManager.UpdatePlayerList); + setupGameListeners(); - // Bind grid event, meaning the game is about to start ! - _socket.on('grid_event', onStartGame); - // Bind also grid reset, to play more than one game :p - _socket.on('grid_reset', resetGame); - - // Send player infos to the server - _socket.emit('userIsReady', { 'nick': nick, 'monster': monster } ); - - // Bind score update - _socket.on('score_update', _scoreManager.RefreshScore); - - // Finally bind game over event - _socket.on('game_over', function (winner) { - _ui.displayGameOver(winner); - _chat.congrats(winner); - }); + localStorage.setItem('mfl_nick', nick); + _socket.emit('userIsReady', { 'nick': nick, 'monster': monster }); - // Show game screen and change state _ui.ChangeGameScreen(enumPanels.Game, true); _gameState = enumState.Waiting; - - // Bind command buttons _ui.bindServerCommandButtons(_socket); - - // Set player's color setPlayerColor(monsterNode.style.borderColor); - return (false); + return false; } + // ─── Multiplayer game ───────────────────────────────────────────────────── + function onStartGame(gridEvent) { - var startTimer; + // Clear any leftover countdown from a previous round + if (_countdownTimer) { window.clearInterval(_countdownTimer); _countdownTimer = null; } - // Instanciate grid manager and provide the validation callback _gridManager = new GridManager(gridEvent.grid, function (wordObj) { _socket.emit('wordValidation', wordObj); }); - // Display timer before game start + // Replace the word_founded listener so it points to the new grid + if (_wordFoundedHandler) _socket.off('word_founded', _wordFoundedHandler); + _wordFoundedHandler = function (wordObj) { _gridManager.RevealWord(wordObj); }; + _socket.on('word_founded', _wordFoundedHandler); + if (gridEvent.timer > 0) { _ui.InfoTooltip(true, 'Tenez-vous prêt !
Début des hostilités dans ' + (gridEvent.timer--) + ''); - startTimer = window.setInterval(function () { + _countdownTimer = window.setInterval(function () { _ui.InfoTooltip(true, 'Tenez-vous prêt !
Début des hostilités dans ' + (gridEvent.timer--) + ''); - - // When the timer is over if (gridEvent.timer < 0) { - // Clear timer - window.clearInterval(startTimer); - - // Display grid !! + window.clearInterval(_countdownTimer); + _countdownTimer = null; _gridManager.DisplayGrid(); _ui.displayGridInformations(gridEvent.grid.infos); - - // Remove tooltip _ui.InfoTooltip(false, 'Bonne chance !'); } }, 1000); - } - else { + } else { _gridManager.DisplayGrid(); _ui.displayGridInformations(gridEvent.grid.infos); } - - // Bind get word event - _socket.on('word_founded', _gridManager.RevealWord); } function resetGame() { + // Stop any running countdown and hide the banner immediately + if (_countdownTimer) { window.clearInterval(_countdownTimer); _countdownTimer = null; } + _ui.InfoTooltip(false); _ui.resetGridInformations(); _scoreManager.resetScores(); - if (_gridManager) - _gridManager.resetGrid(); + if (_gridManager) _gridManager.resetGrid(); } function setPlayerColor(color) { - var color = _ui.getRGBComponents(color), - css = '.focusCell { -moz-box-shadow: inset 0px 0px 30px 4px rgba(' + color + ',0.2);box-shadow: inset 0px 0px 30px 4px rgba(' + color + ',0.2);border-color: rgba(' + color + ',0.4); } .goRight:before, .goDown:before { color: rgb(' + color + '); }', + var rgb = _ui.getRGBComponents(color), + css = '.focusCell { -moz-box-shadow: inset 0px 0px 30px 4px rgba(' + rgb + ',0.2);box-shadow: inset 0px 0px 30px 4px rgba(' + rgb + ',0.2);border-color: rgba(' + rgb + ',0.4); } .goRight:before, .goDown:before { color: rgb(' + rgb + '); }', style = document.createElement('style'); - style.type = 'text/css'; - - if (style.styleSheet) - style.styleSheet.cssText = css; - else - style.appendChild(document.createTextNode(css)); - + if (style.styleSheet) style.styleSheet.cssText = css; + else style.appendChild(document.createTextNode(css)); document.head.appendChild(style); } + // ─── Solo mode ──────────────────────────────────────────────────────────── + + function startSoloMode() { + _gameState = enumState.Solo; + _soloScore = 0; + + _ui.ChangeGameScreen(enumPanels.Game, true); + + // Hide chat panel — not needed in solo + document.getElementById('gs-chat').style.display = 'none'; + document.getElementById('gs-scores').innerHTML = + '
Score0
'; + + fetch('/api/grid') + .then(function (r) { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }) + .then(function (fullGrid) { + _soloGrid = fullGrid; + _gridManager = new GridManager(fullGrid, validateSoloWord); + _gridManager.DisplayGrid(); + _ui.displayGridInformations(fullGrid.infos); + }) + .catch(function (e) { + showError('Impossible de charger la grille solo : ' + e.message); + }); + } + + function validateSoloWord(wordObj) { + // fullGrid has .value on every LetterCase — validate locally + var jump = wordObj.axis === 0 ? 1 : _soloGrid.nbLines; + var index = wordObj.start; + var points = 0; + var i; + + for (i = 0; i < wordObj.word.length; i++) { + if (wordObj.word[i] !== _soloGrid.cases[index].value) return; // wrong letter + if (_soloGrid.cases[index].available) points++; + index += jump; + } + + // Mark as solved in the shared grid object + index = wordObj.start; + for (i = 0; i < wordObj.word.length; i++) { + _soloGrid.cases[index].available = false; + index += jump; + } + _soloGrid.nbWords--; + + wordObj.color = '#27A096'; + _gridManager.RevealWord(wordObj); + + _soloScore += points; + var el = document.querySelector('#solo-score .solo-value'); + if (el) el.textContent = _soloScore; + + if (_soloGrid.nbWords <= 0) { + _ui.displayGameOver({ nick: 'Solo', monster: { path: 'images/logos/monster1.png', color: '#27A096' } }); + } + } + + // ─── URL helpers ────────────────────────────────────────────────────────── + + function getRoomCodeFromURL() { + var hash = window.location.hash.substring(1).toUpperCase(); + if (/^[A-Z0-9]{4}$/.test(hash)) return hash; + var match = window.location.search.match(/[?&]room=([A-Z0-9]{4})/i); + if (match) return match[1].toUpperCase(); + return null; + } + + function setURLRoomCode(roomId) { + if (history.replaceState) { + history.replaceState(null, '', roomId ? '#' + roomId : window.location.pathname + window.location.search); + } + } + + function showError(msg) { + document.getElementById('ep-text').innerHTML = msg; + _ui.ChangeGameScreen(enumPanels.Error, true); + } - // Load ressources and Start the client ! - console.log('Client started'); - startClient(); - -}); \ No newline at end of file +}); diff --git a/public/javascripts/game/score.js b/public/javascripts/game/score.js index df88a2c..9b06fda 100644 --- a/public/javascripts/game/score.js +++ b/public/javascripts/game/score.js @@ -68,11 +68,11 @@ define(function () { // Add event listener on animation end to properly remove the node bonusNode.addEventListener('animationend', function (event) { // Remove node when animation ends - scoreNode.removeChild(event.srcElement); + scoreNode.removeChild(event.target); }, false); bonusNode.addEventListener('webkitAnimationEnd', function (event) { // Remove node when animation ends - scoreNode.removeChild(event.srcElement); + scoreNode.removeChild(event.target); }, false); // Adding bonus in DOM and increase delay before the next bonus diff --git a/public/stylesheets/mfl.css b/public/stylesheets/mfl.css index 4cb49bc..56ce376 100644 --- a/public/stylesheets/mfl.css +++ b/public/stylesheets/mfl.css @@ -263,9 +263,9 @@ html, body { padding: 8px; /*width: 94%;*/ - width: -webkit-calc(100% - 66px); - width: -moz-calc(100% - 66px); - width: calc(100% - 66px); + width: -webkit-calc(100% - 68px); + width: -moz-calc(100% - 68px); + width: calc(100% - 68px); height: 60px; @@ -282,11 +282,11 @@ html, body { right: 0; height: 36px; - width: 50px; + width: 60px; - font-family: 'icomoon'; + font-family: 'abeezeeregular'; color: #444; - font-size: 17px; + font-size: 13px; line-height: 36px; text-align: center; @@ -297,28 +297,13 @@ html, body { overflow: hidden; cursor: pointer; - -webkit-transition: box-shadow 200ms ease, width 200ms ease; - -moz-transition: box-shadow 200ms ease, width 200ms ease; - transition: box-shadow 200ms ease, width 200ms ease; + -webkit-transition: box-shadow 200ms ease; + -moz-transition: box-shadow 200ms ease; + transition: box-shadow 200ms ease; } .gsc-button:hover { box-shadow: 0px 2px 10px 0px rgb(127, 140, 141); } #gsc-command-start { bottom: 38px; } #gsc-command-grid { bottom: 0; } -#gsc-command-start:hover, #gsc-command-grid:hover { - width: 100px; -} -#gsc-command-start:hover:after { - content: 'Lancer'; - font-family: 'abeezeeregular'; - margin-left: 10px; - font-size: 14px; -} -#gsc-command-grid:hover:after { - content: 'Grille'; - font-family: 'abeezeeregular'; - margin-left: 10px; - font-size: 14px; -} /* Game grid part */ #gs-grid-container { @@ -332,6 +317,25 @@ html, body { perspective: 1200; } +/* Grid info bar below the grid */ +#gs-grid-infos { + position: absolute; + left: 0; + display: flex; + justify-content: center; + gap: 18px; + width: 100%; + padding: 6px 0; + font-family: 'abeezeeregular', sans-serif; + font-size: 13px; + color: #333; + background: rgba(255,255,255,0.85); + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); +} +#gs-grid-infos .grid-info-date { text-transform: capitalize; } +#gs-grid-infos .grid-info-level { color: #c0392b; } + /* Score part */ #gs-scores { float: left; @@ -392,7 +396,15 @@ html, body { background-color: rgba(52, 73, 94, 0.4); background: url('../images/grey.png'); } + +/* When the TOP span of a 2-definition cell carries a downward arrow its :after + must be relative to the whole .description cell (not just the span), so we + remove the span from the positioning context with position:static. */ +.description span.arrow-cell-bottom { + position: static; +} .description span { + position: relative; display: inline-block; width: 100%; @@ -401,6 +413,8 @@ html, body { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; border-top: 1px solid rgba(127, 140, 141, 0.2); + word-break: break-word; + hyphens: auto; } .letter { @@ -422,51 +436,55 @@ html, body { } -/* Right arrow */ +/* Right arrow — outside, centered vertically in its span */ .description span.arrow0:after { position: absolute; - right: -16px; - top: 5px; + right: -18px; + top: 50%; + -webkit-transform: translateY(-50%); + transform: translateY(-50%); content: "\e603"; font-family: 'icomoon'; - font-size: 17px; - line-height: 17px; + font-size: 16px; + line-height: 16px; color: #444; } -/* Right Bottom arrow */ +/* Right+Bottom diagonal arrow */ .description span.arrow1:after { position: absolute; - top: 5%; - right: -18px; + top: 10%; + right: -20px; content: "\e600"; font-family: 'icomoon'; - font-size: 21px; - line-height: 21px; + font-size: 20px; + line-height: 20px; color: #444; -webkit-transform: rotateZ(90deg) rotateX(-180deg); + transform: rotateZ(90deg) rotateX(-180deg); } -/* Bottom arrow */ +/* Bottom arrow — outside, centered horizontally in its span */ .description span.arrow2:after { position: absolute; - left: 35%; - bottom: -16px; + bottom: -18px; + left: 50%; content: "\e603"; font-family: 'icomoon'; - font-size: 17px; - line-height: 17px; + font-size: 16px; + line-height: 16px; color: #444; - -webkit-transform: rotateZ(90deg); + -webkit-transform: translateX(-50%) rotate(90deg); + transform: translateX(-50%) rotate(90deg); } -/* Bottom Right arrow */ +/* Bottom+Right diagonal arrow */ .description span.arrow3:after { position: absolute; - left: 5%; - bottom: -19px; + left: 10%; + bottom: -20px; content: "\e600"; font-family: 'icomoon'; - font-size: 24px; + font-size: 22px; color: #444; - line-height: 21px; + line-height: 20px; } .dash1 { border-bottom: 1px dashed rgba(127, 140, 141, 0.2); } @@ -521,12 +539,20 @@ html, body { /* Set container sizes */ .bloc1, .bloc2 { width: 50%; } -.bloc3 { width: 33%; } +.bloc3 { width: 33.33%; } .bloc4 { width: 25%; } +.bloc5 { width: 20%; } +.bloc6 { width: 16.66%; } +.bloc7 { width: 14.28%; } +.bloc8 { width: 12.5%; } +.bloc9 { width: 11.11%; } .bloc1 > .score-bar, .bloc2 > .score-bar { width: 50%; left: 25%; } .bloc3 > .score-bar { width: 66%; left: 17%; } .bloc4 > .score-bar { width: 90%; left: 5%; } +.bloc5 > .score-bar, .bloc6 > .score-bar, +.bloc7 > .score-bar, .bloc8 > .score-bar, +.bloc9 > .score-bar { width: 96%; left: 2%; } .score-bar { @@ -743,3 +769,274 @@ html, body { 0% { -webkit-transform: rotateZ(-10deg); } 100% { -webkit-transform: rotateZ(10deg); } } + + +/*==============*\ +| | +| LOBBY PANEL | +| | +\*==============*/ + +#lobby-panel { + background-image: url('../images/green-bg.png'); + overflow-y: auto; +} + +#lobby-panel > header { + margin: 5% auto; + text-align: center; +} + +#lobby-section { + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; + gap: 40px; + padding: 0 5% 40px; + flex-wrap: wrap; +} + +/* ─ Room list ─────────────────────────────────────────────────────────── */ +#lobby-rooms { + flex: 1 1 400px; + max-width: 600px; +} + +#lobby-room-list { + min-height: 60px; +} + +.lobby-connecting, +.lobby-empty { + color: rgba(236,240,241,0.7); + font-family: 'abeezeeitalic'; + font-size: 0.95em; +} + +.lobby-room-item { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; + padding: 10px 14px; + background: rgba(255,255,255,0.12); + border-radius: 6px; + flex-wrap: wrap; +} + +.room-code { + font-size: 1.3em; + font-weight: bold; + color: #ecf0f1; + letter-spacing: 3px; + min-width: 60px; +} + +.room-grid { + flex: 1; + color: rgba(236,240,241,0.85); + font-size: 0.85em; +} + +.room-status { + font-size: 0.8em; + padding: 3px 8px; + border-radius: 10px; +} +.room-waiting { background: rgba(39,160,150,0.5); color: #ecf0f1; } +.room-in-game { background: rgba(231,76,60,0.5); color: #ecf0f1; } + +.room-players { + font-size: 0.8em; + color: rgba(236,240,241,0.7); +} + +.room-join-btn { + padding: 5px 14px; + font-family: 'abeezeeregular'; + font-size: 0.85em; + color: #444; + border: 2px solid grey; + border-radius: 4px; + background-image: url('../images/grey.png'); + cursor: pointer; +} +.room-join-btn:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.2); } + +/* ─ Action panel ──────────────────────────────────────────────────────── */ +#lobby-actions { + flex: 0 1 320px; + display: flex; + flex-direction: column; + gap: 10px; + align-items: stretch; +} + +.lobby-group { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.lobby-group input[type="text"], +.lobby-group input[type="number"] { + flex: 1; + min-width: 0; + padding: 8px 12px; + font-family: 'abeezeeregular'; + font-size: 0.95em; + color: #ecf0f1; + background: rgba(255,255,255,0.12); + border: 0; + border-bottom: 3px solid rgba(255,255,255,0.4); + outline: none; + border-radius: 3px 3px 0 0; + -webkit-transition: border-color 200ms ease; + transition: border-color 200ms ease; +} +.lobby-group input[type="text"]:focus, +.lobby-group input[type="number"]:focus { + border-bottom-color: #27A096; +} + +.lobby-group input[type="button"], +#lobby-solo-btn { + padding: 9px 18px; + font-family: 'abeezeeregular'; + font-size: 0.95em; + color: #444; + border: 2px solid grey; + border-radius: 4px; + background-image: url('../images/grey.png'); + box-shadow: 0 2px 6px rgba(127,140,141,0.4); + cursor: pointer; + white-space: nowrap; + -webkit-transition: box-shadow 200ms ease; + transition: box-shadow 200ms ease; +} +.lobby-group input[type="button"]:hover, +#lobby-solo-btn:hover { + box-shadow: 0 4px 12px rgba(127,140,141,0.7); +} + +#lobby-solo-btn { + margin-top: 4px; +} + +.lobby-sep { + color: rgba(236,240,241,0.5); + text-align: center; + font-size: 0.85em; + margin: 4px 0 2px; +} + + +/*==========================*\ +| | +| SOLO SCORE | +| | +\*==========================*/ + +#solo-score { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: #ecf0f1; + text-shadow: 2px 2px 5px #2c3e50; +} +.solo-label { + font-size: 1.1em; + opacity: 0.7; +} +.solo-value { + font-size: 3em; + font-weight: bold; +} + + +/*===================*\ +| | +| MOBILE / TABLET | +| | +\*===================*/ + +/* Grid container must scroll when frame size hits the 30px minimum */ +#gs-grid-container { + overflow: auto; + -webkit-overflow-scrolling: touch; +} + +/* Increase tap target sizes on touch devices */ +@media (hover: none) and (pointer: coarse) { + .letter { + min-width: 30px; + min-height: 30px; + } + .gsc-button { + height: 44px; + line-height: 44px; + } + #gsc-command-start { bottom: 46px; } + #gsc-command-grid { bottom: 0; } +} + +/* Stack layout on small screens */ +@media screen and (max-width: 768px) { + /* Game panel: vertical stack */ + #game-panel { + display: flex; + flex-direction: column; + overflow: hidden; + } + + /* Chat: thin strip at the top */ + #gs-chat { + float: none !important; + width: 100% !important; + height: 160px !important; + flex-shrink: 0; + border-bottom: 2px solid rgba(127,140,141,0.2); + } + + #gsc-messages { height: calc(100% - 50px); } + + /* Grid: fills remaining space, scrollable */ + #gs-grid-container { + float: none !important; + width: 100% !important; + flex: 1 1 auto; + overflow: auto; + -webkit-overflow-scrolling: touch; + } + + /* Scores: hidden by default on very small screens to give grid more room */ + #gs-scores { + display: none; + } + + /* Solo score stays visible */ + #solo-score { display: flex; } + + /* Lobby: single column */ + #lobby-section { + flex-direction: column; + align-items: stretch; + gap: 20px; + padding: 0 4% 30px; + } + + #lobby-panel > header { margin: 4% auto 2%; } + + .lp-title { + width: 40px; + height: 40px; + line-height: 40px; + font-size: 30px; + } + + /* Login panel: smaller title on mobile */ + #login-panel > header { margin: 5% auto; } +} diff --git a/routes/index.js b/routes/index.js index bd862e3..9a9874f 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,8 +1,9 @@ -var config = require('../conf.json') - /* * GET home page. */ exports.index = function(req, res) { - res.render('mfl', { title: 'MotsFleches.js', wsAddress: config.SOCKET_ADDR + ':' + config.SOCKET_PORT }); + // Derive the server's public address from the incoming request (works locally and on cloud) + var protocol = req.headers['x-forwarded-proto'] || req.protocol; + var host = req.get('host'); + res.render('mfl', { title: 'MotsFleches.js', wsAddress: protocol + '://' + host }); }; diff --git a/server.js b/server.js index 4d2a431..63214d3 100644 --- a/server.js +++ b/server.js @@ -3,42 +3,53 @@ */ var express = require('express'), routes = require('./routes'), - http = require('http'), - path = require('path'), - os = require('os'), + http = require('http'), + path = require('path'), + os = require('os'), prompts = require('prompts'), - app = express(), + app = express(), config = require('./conf.json'), - mfl = require('./game_files/motsFleches'), + mfl = require('./game_files/motsFleches'), _gridNumber = 0; // all environments -app.set('port', config.SERVER_PORT); +var _port = process.env.PORT || config.SERVER_PORT; +app.set('port', _port); app.set('views', path.join(__dirname, 'views')); -app.set('view engine', 'jade'); +app.set('view engine', 'pug'); -app.use(express.favicon()); -app.use(express.logger('dev')); app.use(express.json()); -app.use(express.urlencoded()); -app.use(express.methodOverride()); -app.use(app.router); +app.use(express.urlencoded({ extended: false })); app.use(express.static(path.join(__dirname, 'public'))); -// development only -if ('development' == app.get('env')) { - app.use(express.errorHandler()); -} - app.get('/', routes.index); + +// Solo mode — returns full grid (letters included) for client-side word validation +app.get('/api/grid/:number?', function (req, res) { + var GridManager = require('./game_files/gridManager'); + var gm = new GridManager(); + var number = req.params.number ? parseInt(req.params.number) : 0; + if (isNaN(number)) number = 0; + gm.retreiveAndParseGrid(number, function (grid) { + if (!grid) return res.status(500).json({ error: 'Impossible de charger la grille' }); + res.json(gm.getFullGrid()); + }); +}); app.get('/conf.json', function(req, res) { - res.json({ SOCKET_ADDR: config.SOCKET_ADDR, SOCKET_PORT: config.SOCKET_PORT }); + // Derive public address from request headers (works locally and on cloud platforms) + var protocol = req.headers['x-forwarded-proto'] || req.protocol; + var host = req.get('host'); // e.g. "myapp.railway.app" or "192.168.1.5:2121" + var parts = host.split(':'); + var hostname = parts[0]; + var port = parts[1] ? parseInt(parts[1]) : (protocol === 'https' ? 443 : 80); + res.json({ SOCKET_ADDR: protocol + '://' + hostname, SOCKET_PORT: port }); }); -// Start server -http.createServer(app).listen(app.get('port'), onServerReady); +// Create HTTP server (Socket.IO will attach to it too — single port) +var _server = http.createServer(app); +_server.listen(_port, onServerReady); // Retreive command line arguments if (process.argv[2]) { @@ -52,11 +63,15 @@ if (process.argv[2]) { /** Call when the express server has started */ async function onServerReady() { - console.log('Express server listening on port ' + app.get('port')); + console.log('Express server listening on port ' + _port); var addresses = getLocalIpAddresses(); - if (addresses.length > 1) { + if (addresses.length === 0) { + // Cloud environment — no local interfaces, URL is derived from request headers + console.log('\n\n\tGame server ready (cloud mode)\n\n'); + } + else if (addresses.length > 1) { var response = await prompts({ type: 'select', name: 'value', @@ -64,18 +79,15 @@ async function onServerReady() { choices: addresses, }); - // Update socket address with the choosen one - config.SOCKET_ADDR = `http://${addresses[response.value]}`; + console.log(`\n\n\tWaiting for players at http://${addresses[response.value]}:${_port}\n\n`); } else { - config.SOCKET_ADDR = `http://${addresses[0]}`; + console.log(`\n\n\tWaiting for players at http://${addresses[0]}:${_port}\n\n`); } - console.log(`\n\n\tWaiting for players at ${config.SOCKET_ADDR}:${config.SERVER_PORT}\n\n`); - // Load desired grid in parameter. // -1 to retreive the day grid, 0 for the default one or any number for a special one - mfl.startMflServer(_gridNumber); + mfl.startMflServer(_gridNumber, _server); } /** Get local ip addresses */ @@ -84,8 +96,6 @@ function getLocalIpAddresses() { var addresses = []; Object.keys(ifaces).map(function (ifname) { - var alias = 0; - return ifaces[ifname].map(function (iface) { if (iface.family !== 'IPv4' || iface.internal !== false) return; addresses.push(iface.address); diff --git a/views/mfl.pug b/views/mfl.pug new file mode 100644 index 0000000..e44cb48 --- /dev/null +++ b/views/mfl.pug @@ -0,0 +1,90 @@ +doctype html +html + head + script(type='text/javascript', src=wsAddress + '/socket.io/socket.io.js'). + + + + + Mots.js + + + + + + body. + + +
+
+
M
O
T
S
.
J
S
+
+ +
+ +
+
Connexion au serveur...
+
+ +
+
+ + +
+ +

— ou rejoindre —

+ +
+ + +
+ +

— ou —

+ + +
+ +
+
+ + + +
+
+
M
O
T
S
.
J
S
+
+ +
+ + +
+ + +
+ +
+ + + +
+ +
+
+ +
Lancer
+
Grille
+
+ +
+ +
+ +
+ + +
+

Ooooops !

+ Something goes wrong +
+ +