diff --git a/common/GameCommon.js b/common/GameCommon.js index 7b055a3..109f170 100644 --- a/common/GameCommon.js +++ b/common/GameCommon.js @@ -98,7 +98,14 @@ function removeSocket(gameId, socket) { } function getAllGames() { - return Object.values(GAMES) + return Object.values(GAMES).sort((a, b) => { + // when both have same finished state, sort by started + if (isFinished(a.id) === isFinished(b.id)) { + return b.puzzle.data.started - a.puzzle.data.started + } + // otherwise, sort: unfinished, finished + return isFinished(a.id) ? 1 : -1 + }) } function getAllPlayers(gameId) { @@ -119,6 +126,10 @@ function getImageUrl(gameId) { return GAMES[gameId].puzzle.info.imageUrl } +function isFinished(gameId) { + return getFinishedTileCount(gameId) === getTileCount(gameId) +} + function getFinishedTileCount(gameId) { let count = 0 for (let t of GAMES[gameId].puzzle.tiles) { @@ -527,6 +538,11 @@ function handleInput(gameId, playerId, input) { finishTiles(gameId, tileIdxs) changePlayer(gameId, playerId, { points: getPlayerPoints(gameId, playerId) + tileIdxs.length }) _tileChanges(tileIdxs) + // check if the puzzle is finished + if (getFinishedTileCount(gameId) === getTileCount(gameId)) { + changeData(gameId, { finished: Util.timestamp() }) + _dataChange() + } } else { // Snap to other tiles const check = (gameId, tileIdx, otherTileIdx, off) => { diff --git a/game/game.js b/game/game.js index 9a05d2c..ca851b4 100644 --- a/game/game.js +++ b/game/game.js @@ -76,10 +76,20 @@ function addMenuToDom(previewImageUrl) { ) const settingsEl = document.createElement('table') - settingsEl.classList.add('layer', 'settings', 'closed') + settingsEl.classList.add('settings') settingsEl.appendChild(bgColorPickerRow) settingsEl.appendChild(playerColorPickerRow) settingsEl.appendChild(nameChangeRow) + settingsEl.addEventListener('click', (e) => { + e.stopPropagation() + }) + + const settingsOverlay = document.createElement('div') + settingsOverlay.classList.add('overlay', 'transparent', 'closed') + settingsOverlay.appendChild(settingsEl) + settingsOverlay.addEventListener('click', () => { + settingsOverlay.classList.toggle('closed') + }) const previewEl = document.createElement('div') previewEl.classList.add('preview') @@ -100,7 +110,7 @@ function addMenuToDom(previewImageUrl) { settingsOpenerEl.classList.add('opener') settingsOpenerEl.appendChild(document.createTextNode('🛠️ Settings')) settingsOpenerEl.addEventListener('click', () => { - settingsEl.classList.toggle('closed') + settingsOverlay.classList.toggle('closed') }) const homeEl = document.createElement('a') @@ -153,7 +163,7 @@ function addMenuToDom(previewImageUrl) { scoresEl.appendChild(scoresTitleEl) scoresEl.appendChild(scoresListEl) - document.body.appendChild(settingsEl) + document.body.appendChild(settingsOverlay) document.body.appendChild(previewOverlay) document.body.appendChild(menuEl) document.body.appendChild(scoresEl) @@ -284,7 +294,7 @@ async function main() { bgColorPickerEl.addEventListener('change', () => { evts.addEvent(['bg_color', bgColorPickerEl.value]) }) - playerColorPickerEl.value = Game.getPlayerBgColor(gameId, CLIENT_ID) + playerColorPickerEl.value = Game.getPlayerColor(gameId, CLIENT_ID) playerColorPickerEl.addEventListener('change', () => { evts.addEvent(['player_color', playerColorPickerEl.value]) }) diff --git a/game/index.js b/game/index.js index ec42324..f9ef63e 100644 --- a/game/index.js +++ b/game/index.js @@ -26,6 +26,20 @@ const Upload = { } } + +export const timestamp = () => { + const d = new Date(); + return Date.UTC( + d.getUTCFullYear(), + d.getUTCMonth(), + d.getUTCDate(), + d.getUTCHours(), + d.getUTCMinutes(), + d.getUTCSeconds(), + d.getUTCMilliseconds(), + ) +} + export default { components: { Upload, @@ -37,12 +51,16 @@ export default { template: `

Running games

-
- - -
- {{g.tilesFinished}}/{{g.tilesTotal}} pieces, {{g.players}} players -
+

New game

@@ -81,6 +99,32 @@ export default { } }, methods: { + time(start, end) { + const icon = end ? '🏁' : '⏳' + + const from = start; + const to = end || timestamp() + + const MS = 1 + const SEC = MS * 1000 + const MIN = SEC * 60 + const HOUR = MIN * 60 + const DAY = HOUR * 24 + + let diff = to - from + const d = Math.floor(diff / DAY) + diff = diff % DAY + + const h = Math.floor(diff / HOUR) + diff = diff % HOUR + + const m = Math.floor(diff / MIN) + diff = diff % MIN + + const s = Math.floor(diff / SEC) + + return `${icon} ${d}d ${h}h ${m}m ${s}s` + }, mediaImgUploaded(j) { this.image = j.image }, diff --git a/game/style.css b/game/style.css index 3a72a81..5cab9d8 100644 --- a/game/style.css +++ b/game/style.css @@ -44,7 +44,21 @@ a:hover { color: var(--link-hover-color); } display: none; } -.layer { +.overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10; + background: var(--bg-color); +} + +.overlay.transparent { + background: transparent; +} + +.settings { position: absolute; left: 50%; top: 50%; @@ -56,16 +70,6 @@ a:hover { color: var(--link-hover-color); } z-index: 1; } -.overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 10; - background: var(--bg-color); -} - .preview { position: absolute; top: 20px; @@ -143,3 +147,38 @@ input:focus { box-shadow: 0 0 1px rgba(150,150,150,.4) inset; cursor: not-allowed; } + +.game-teaser-wrap { + display: inline-block; + width: 20%; + padding: 5px; + box-sizing: border-box; +} + +.game-teaser { + display: block; + background-repeat: no-repeat; + background-position: center; + background-size: contain; + position: relative; + padding-top: 56.25%; + width: 100%; + background-color: #222222; +} + +.game-info { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; +} + +.game-info-text { + position: absolute; + top: 0; + background: var(--bg-color); + padding: 5px; +} diff --git a/server/Game.js b/server/Game.js index 1368036..6e0ea21 100644 --- a/server/Game.js +++ b/server/Game.js @@ -12,17 +12,19 @@ async function createGame(gameId, targetTiles, image) { }) } +const DATA_DIR = './../data' + function loadAllGames() { - const files = fs.readdirSync('./../data/') + const files = fs.readdirSync(DATA_DIR) for (const f of files) { if (!f.match(/\.json$/)) { continue } - const gameId = f.replace(/\.json$/, '') - const contents = fs.readFileSync(`./../data/${f}`, 'utf-8') + const file = `${DATA_DIR}/${f}` + const contents = fs.readFileSync(file, 'utf-8') const game = JSON.parse(contents) GameCommon.newGame({ - id: gameId, + id: game.id, puzzle: game.puzzle, players: game.players, sockets: [], @@ -33,7 +35,7 @@ function loadAllGames() { function persistAll() { for (const game of GameCommon.getAllGames()) { - fs.writeFileSync('./../data/' + game.id + '.json', JSON.stringify({ + fs.writeFileSync(`${DATA_DIR}/${game.id}.json`, JSON.stringify({ id: game.id, puzzle: game.puzzle, players: game.players, diff --git a/server/Puzzle.js b/server/Puzzle.js index c671069..ad89014 100644 --- a/server/Puzzle.js +++ b/server/Puzzle.js @@ -101,6 +101,8 @@ async function createPuzzle(targetTiles, image) { // TODO: maybe calculate this each time? maxZ: 0, // max z of all pieces maxGroup: 0, // max group of all pieces + started: Util.timestamp(), // start timestamp + finished: 0, // finish timestamp }, // static puzzle information. stays same for complete duration of // the game diff --git a/server/index.js b/server/index.js index 73ba731..c93f6ac 100644 --- a/server/index.js +++ b/server/index.js @@ -78,6 +78,8 @@ app.use('/', async (req, res, next) => { const games = [ ...Game.getAllGames().map(game => ({ id: game.id, + started: game.puzzle.data.started, + finished: game.puzzle.data.finished, tilesFinished: Game.getFinishedTileCount(game.id), tilesTotal: Game.getTileCount(game.id), players: Game.getActivePlayers(game.id).length,