diff options
author | Tor Andersson <tor@ccxvii.net> | 2021-06-25 17:41:05 +0200 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2021-06-25 23:53:13 +0200 |
commit | ad4a4ea757cce61cde102be2aea2d07fe55aaa2c (patch) | |
tree | 2d1878b5e9b2a1a887a73bdcd793a6a1025786d3 | |
parent | 3009c951931002c9021d76a1c221e3013f0cd046 (diff) | |
download | server-ad4a4ea757cce61cde102be2aea2d07fe55aaa2c.tar.gz |
New and improved join page with server sent events.
-rw-r--r-- | public/join.js | 109 | ||||
-rw-r--r-- | public/style.css | 1 | ||||
-rw-r--r-- | server.js | 121 | ||||
-rw-r--r-- | views/create.ejs | 2 | ||||
-rw-r--r-- | views/join.ejs | 93 |
5 files changed, 231 insertions, 95 deletions
diff --git a/public/join.js b/public/join.js new file mode 100644 index 0000000..96ca321 --- /dev/null +++ b/public/join.js @@ -0,0 +1,109 @@ +"use strict"; + +let evtsrc = null; + +function confirm_delete(status) { + let warning = "Are you sure you want to DELETE this game?"; + if (window.confirm(warning)) + window.open("/delete/" + game.game_id); +} + +function send(url) { + fetch(url) + .then(r => r.text()) + .then(t => window.error.textContent = (t === "SUCCESS") ? "" : t) + .catch(e => window.error.textContent = e ); + start_event_source(); + return void 0; +} + +function start_event_source() { + if (!evtsrc || evtsrc.readyState === 2) { + console.log("STARTING EVENT SOURCE"); + evtsrc = new EventSource("/join-events/" + game.game_id); + evtsrc.addEventListener("players", function (evt) { + console.log("PLAYERS:", evt.data); + players = JSON.parse(evt.data); + update(); + }); + evtsrc.addEventListener("ready", function (evt) { + console.log("READY:", evt.data); + ready = JSON.parse(evt.data); + update(); + }); + evtsrc.addEventListener("game", function (evt) { + console.log("GAME:", evt.data); + game = JSON.parse(evt.data); + if (game.status > 1) + evtsrc.close(); + update(); + }); + evtsrc.onerror = function (err) { + window.message.innerHTML = "Disconnected from server..."; + }; + } +} + +function update() { + window.game_status.textContent = ["Open","Active","Finished","Abandoned"][game.status]; + window.game_result.textContent = game.result || "\u2014"; + + for (let i = 0; i < roles.length; ++i) { + let role = roles[i]; + let role_id = "role_" + role.replace(/ /g, "_"); + if (game.random && game.status === 0) + role = "Random " + (i+1); + document.getElementById(role_id + "_name").textContent = role; + let player = players.find(p => p.role === role); + let element = document.getElementById(role_id); + if (player) { + if (game.status > 0) { + if (game.active === role || game.active === "Both" || game.active === "All") + element.className = "your_turn"; + else + element.className = ""; + if (player.user_id === user_id) + element.innerHTML = `<a href="/play/${game.game_id}/${role}">Play</a>`; + else + element.innerHTML = player.name; + } else { + if ((player.user_id === user_id) || (game.owner_id === user_id)) + element.innerHTML = `<a class="red" href="javascript:send('/part/${game.game_id}/${role}')">\u274c</a> ${player.name}`; + else + element.innerHTML = player.name; + } + } else { + if (game.status === 0) + //element.innerHTML = `<a class="join" href="javascript:send('/join/${game.game_id}/${role}')">Join</a>`; + element.innerHTML = `<a class="join" onclick="send('/join/${game.game_id}/${role}')" href="javascript:void 0">Join</a>`; + else + element.innerHTML = "<i>Empty</i>"; + } + } + + let message = window.message; + if (game.status === 0) { + if (ready && (game.owner_id === user_id)) + message.innerHTML = "Ready to start..."; + else if (ready) + message.innerHTML = "Waiting for game to start..."; + else + message.innerHTML = "Waiting for players to join..."; + } else { + message.innerHTML = `<a class="play" href="/play/${game.game_id}/Observer">Observe</a>`; + } + + if (game.owner_id === user_id) { + window.start_button.disabled = !ready; + window.start_button.classList = (game.status === 0) ? "" : "hide"; + window.delete_button.classList = (game.status === 0 || solo) ? "" : "hide"; + } +} + +window.onload = function () { + update(); + if (game.status < 2) { + start_event_source(); + setInterval(start_event_source, 15000); + } +} diff --git a/public/style.css b/public/style.css index 157661b..0503574 100644 --- a/public/style.css +++ b/public/style.css @@ -44,6 +44,7 @@ button, select { margin: 5px 10px 5px 0; padding: 1px 10px; background-color: gainsboro; + vertical-align: top; } button:disabled { color: gray; @@ -632,7 +632,7 @@ const QUERY_ROLE_FROM_GAME_AND_USER = db.prepare("SELECT role FROM players WHERE const QUERY_IS_SOLO = db.prepare("SELECT COUNT(DISTINCT user_id) = 1 FROM players WHERE game_id = ?").pluck(); const QUERY_JOIN_GAME = db.prepare("INSERT INTO players (user_id, game_id, role) VALUES (?,?,?)"); -const QUERY_PART_GAME = db.prepare("DELETE FROM players WHERE game_id = ? AND user_id = ? AND role = ?"); +const QUERY_PART_GAME = db.prepare("DELETE FROM players WHERE game_id = ? AND role = ?"); const QUERY_START_GAME = db.prepare("UPDATE games SET status = 1, state = ?, active = ? WHERE game_id = ?"); const QUERY_CREATE_GAME = db.prepare(` INSERT INTO games @@ -829,6 +829,39 @@ app.get('/rematch/:old_game_id', must_be_logged_in, function (req, res) { } }); +let join_clients = {}; + +function update_join_clients_players(game_id) { + let list = join_clients[game_id]; + if (list) { + console.log("UPDATE JOIN PLAYERS", game_id, list.length) + let players = QUERY_PLAYERS.all(game_id); + let ready = RULES[list.title_id].ready(list.scenario, players); + for (let res of list) { + console.log("PUSH JOIN PLAYERS", game_id); + res.write("retry: 10000\n"); + res.write("event: players\n"); + res.write("data: " + JSON.stringify(players) + "\n\n"); + res.write("event: ready\n"); + res.write("data: " + ready + "\n\n"); + } + } +} + +function update_join_clients_game(game_id) { + let list = join_clients[game_id]; + if (list && list.length > 0) { + console.log("UPDATE JOIN GAME", game_id, list.length) + let game = QUERY_GAME.get(game_id); + for (let res of list) { + console.log("PUSH JOIN GAME", game_id); + res.write('retry: 10000\n'); + res.write('event: game\n'); + res.write('data: ' + JSON.stringify(game) + '\n\n'); + } + } +} + app.get('/join/:game_id', must_be_logged_in, function (req, res) { LOG(req, "GET /join/" + req.params.game_id); let game_id = req.params.game_id | 0; @@ -838,10 +871,8 @@ app.get('/join/:game_id', must_be_logged_in, function (req, res) { return res.redirect('/'); } let roles = QUERY_ROLES.all(game.title_id); - if (game.random && game.status == 0) - for (let i = 0; i < roles.length; ++i) - roles[i] = "Random " + (i+1); let players = QUERY_PLAYERS.all(game_id); + let ready = (game.status == 0) && RULES[game.title_id].ready(game.scenario, players); res.set("Cache-Control", "no-store"); res.render('join.ejs', { user: req.user, @@ -849,34 +880,70 @@ app.get('/join/:game_id', must_be_logged_in, function (req, res) { roles: roles, players: players, solo: players.every(p => p.user_id == req.user.user_id), + ready: players.length == roles.length, message: req.flash('message') }); }); +app.get('/join-events/:game_id', must_be_logged_in, function (req, res) { + LOG(req, "GET /join-events/" + req.params.game_id); + let game_id = req.params.game_id | 0; + let players = QUERY_PLAYERS.all(game_id); + let game = QUERY_GAME.get(game_id); + + res.setHeader("Cache-Control", "no-store"); + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Connection", "keep-alive"); + + if (!game) + return res.end(); + if (!(game_id in join_clients)) { + join_clients[game_id] = []; + join_clients[game_id].title_id = game.title_id; + join_clients[game_id].scenario = game.scenario; + } + join_clients[game_id].push(res); + + res.on('close', err => { + console.log("CLOSE JOIN EVENTS", err); + let list = join_clients[game_id]; + let i = list.indexOf(res); + if (i >= 0) + list.splice(i, 1); + }); + + res.write("retry: 15000\n\n"); + res.write("event: game\n"); + res.write("data: " + JSON.stringify(game) + "\n\n"); + res.write("event: players\n"); + res.write("data: " + JSON.stringify(players) + "\n\n"); +}); + app.get('/join/:game_id/:role', must_be_logged_in, function (req, res) { LOG(req, "GET /join/" + req.params.game_id + "/" + req.params.role); let game_id = req.params.game_id | 0; let role = req.params.role; try { QUERY_JOIN_GAME.run(req.user.user_id, game_id, role); - return res.redirect('/join/'+game_id); + update_join_clients_players(game_id); + res.send("SUCCESS"); } catch (err) { - req.flash('message', err.toString()); - return res.redirect('/join/'+game_id); + console.log(err); + res.send(err.toString()); } }); -app.get('/part/:game_id/:part_id/:role', must_be_logged_in, function (req, res) { - LOG(req, "GET /part/" + req.params.game_id + "/" + req.params.part_id + "/" + req.params.role); +app.get('/part/:game_id/:role', must_be_logged_in, function (req, res) { + LOG(req, "GET /part/" + req.params.game_id + "/" + req.params.role); let game_id = req.params.game_id | 0; - let part_id = req.params.part_id | 0; let role = req.params.role; try { - QUERY_PART_GAME.run(game_id, part_id, role); - return res.redirect('/join/'+game_id); + QUERY_PART_GAME.run(game_id, role); + update_join_clients_players(game_id); + res.send("SUCCESS"); } catch (err) { - req.flash('message', err.toString()); - return res.redirect('/join/'+game_id); + console.log(err); + res.send(err.toString()); } }); @@ -900,26 +967,27 @@ app.get('/start/:game_id', must_be_logged_in, function (req, res) { let game_id = req.params.game_id | 0; try { let game = QUERY_GAME_OWNER.get(game_id, req.user.user_id); - if (!game) { - req.flash('message', "Only the game owner can start the game!"); - return res.redirect('/join/'+game_id); - } - if (game.status != 0) { - req.flash('message', "The game is already started!"); - return res.redirect('/join/'+game_id); - } + if (!game) + return res.send("Only the game owner can start the game!"); + if (game.status != 0) + return res.send("The game is already started!"); let players = QUERY_PLAYERS.all(game_id); - if (game.random) + if (!RULES[game.title_id].ready(game.scenario, players)) + return res.send("Invalid player configuration!"); + if (game.random) { assign_random_roles(game, players); + update_join_clients_players(game_id); + } let state = RULES[game.title_id].setup(game.scenario, players); QUERY_START_GAME.run(JSON.stringify(state), state.active, game_id); let is_solo = players.every(p => p.user_id == players[0].user_id); if (is_solo) QUERY_UPDATE_GAME_SET_PRIVATE.run(game_id); - return res.redirect('/join/'+game_id); + update_join_clients_game(game_id); + res.send("SUCCESS"); } catch (err) { - req.flash('message', err.toString()); - return res.redirect('/join/'+game_id); + console.log(err); + res.send(err.toString()); } }); @@ -1131,6 +1199,7 @@ function put_game_state(game_id, state, old_active) { QUERY_UPDATE_GAME_STATE.run(JSON.stringify(state), state.active, status, result, game_id); for (let other of clients[game_id]) send_state(other, state); + update_join_clients_game(game_id); mail_your_turn_notification_to_offline_users(game_id, old_active, state.active); } diff --git a/views/create.ejs b/views/create.ejs index 5db8fe6..b995639 100644 --- a/views/create.ejs +++ b/views/create.ejs @@ -1,5 +1,5 @@ <%- include('header', { title: title.title_name }) %> -<style>form{display:block;margin-left:200px;}</style> +<style>form{display:block;margin-left:170px;}</style> <a href="/info/<%= title.title_id %>"><img class="logo" src="/<%= title.title_id %>/cover.jpg"></a> <form action="/create/<%= title.title_id %>" method="post"> <p> diff --git a/views/join.ejs b/views/join.ejs index 2da5f03..46c1e56 100644 --- a/views/join.ejs +++ b/views/join.ejs @@ -1,14 +1,23 @@ -<%- include('header', { title: game.title_name, refresh: game.status == 0 ? 15 : 0 }) %> +<%- include('header', { title: game.title_name }) -%> +<style> +th, td { min-width: 10em; font-size: 16px; } +a.red { text-decoration: none; color: brown; font-size: 15px; } +.hide { display: none; } +</style> <script> -function confirm_delete(status) { - let warning = "Are you sure you want to DELETE this game?"; - if (window.confirm(warning)) - window.open("/delete/<%= game.game_id %>"); -} +let game = <%- JSON.stringify(game) %>; +let roles = <%- JSON.stringify(roles) %>; +let players = <%- JSON.stringify(players) %>; +let user_id = <%- user.user_id %>; +let solo = <%- solo %>; +let ready = <%- ready %>; </script> +<script src="/join.js"></script> +<p id="error" class="error"></p> <a href="/info/<%= game.title_id %>"><img class="logo" src="/<%= game.title_id %>/cover.jpg"></a> +<div class="info"> <p> Owner: <%= game.owner_name %> <br> @@ -17,74 +26,22 @@ Private: <%= game.private ? "yes" : "no" %> Scenario: <%= game.scenario %> <br> Description: <%= game.description || "No description." %> +<br> +Status: <span id="game_status"></span> +<br> +Result: <span id="game_result"></span> <br clear=left> -<p> <table> <tr> -<% - roles.forEach((role) => { - %><th><%= role %><% - }); -%> +<% roles.forEach((role) => { %><th id="role_<%= role.replace(/ /g, '_') %>_name"><%= role %></th><% }); %> +<tr> +<% roles.forEach((role) => { %><td id="role_<%= role.replace(/ /g, '_') %>">-</td><% }); %> <tr> -<% - roles.forEach((role) => { - if (game.active == role || game.active == "Both" || game.active == "All") { - %><td style="min-width:9em" class="your_turn"><% - } else { - %><td style="min-width:9em"><% - } - let p = players.find(p => p.role == role); - if (game.status == 0) { - if (p) { - if ((p.user_id == user.user_id) || (game.owner_id == user.user_id)) { - %><a style="color:red;text-decoration:none" href="/part/<%= game.game_id %>/<%= p.user_id %>/<%= p.role %>">❌</a> <% - %><%= p.name %><% - } else { - %><%= p.name %><% - } - } else { - %><a href="/join/<%= game.game_id %>/<%= role %>">Join</a><% - } - } else { - if (p) { - if (p.user_id == user.user_id) { - %><a href="/play/<%= game.game_id %>/<%= p.role %>">Play</a><% - } else { - %><%= p.name %><% - } - } else { - %><i>Empty</i><% - } - } - }); - if (game.status > 0 && !players.some(p => p.user_id == user.user_id)) { - %> - <tr><td colspan="<%= roles.length %>"><a href="/play/<%= game.game_id %>/Observer">View</a> - <% - } -%> +<td id="message" colspan="<%= roles.length %>">-</td> </table> <p> -<% - if (game.status == 0) { - if (players.length == roles.length) { - if (game.owner_id == user.user_id) { - %><form action="/start/<%= game.game_id %>"><button type="submit">Start!</button></form><% - } else { - %>Waiting for <%= game.owner_name %> to start the game.<% - } - } else { - %>Waiting for players to join the game.<% - } - } - if (game.status == 2 && players.some(p => p.user_id == user.user_id)) { - %><form action="/rematch/<%= game.game_id %>"><button type="submit">Rematch</button></form><% - } - if (game.owner_id == user.user_id && (game.status == 0 || solo)) { - %><p><br><button onclick="confirm_delete()">Delete</button><% - } -%> +<button class="hide" id="delete_button" onclick="confirm_delete()">Delete</button> +<button class="hide" id="start_button" onclick="javascript:send('/start/<%= game.game_id %>')" disabled>Start!</button> |