diff options
-rw-r--r-- | public/common/play.js | 2 | ||||
-rw-r--r-- | public/join.js | 72 | ||||
-rw-r--r-- | public/style.css | 5 | ||||
-rw-r--r-- | schema.sql | 1 | ||||
-rw-r--r-- | server.js | 104 | ||||
-rw-r--r-- | views/join.pug | 15 |
6 files changed, 143 insertions, 56 deletions
diff --git a/public/common/play.js b/public/common/play.js index 054343c..964889b 100644 --- a/public/common/play.js +++ b/public/common/play.js @@ -343,7 +343,7 @@ function remove_resign_menu() { } function goto_rematch() { - window.location = "/rematch/" + params.game_id + "/" + params.role + window.location = "/rematch/" + params.game_id } function goto_replay() { diff --git a/public/join.js b/public/join.js index 5bb8b9c..72da646 100644 --- a/public/join.js +++ b/public/join.js @@ -3,6 +3,7 @@ let start_status = game.status let evtsrc = null let timer = 0 +let invite_role = null function confirm_delete() { let warning = `Are you sure you want to DELETE this game?` @@ -39,6 +40,25 @@ function kick(role) { post(`/part/${game.game_id}/${encodeURIComponent(role)}`) } +function accept(role) { + post(`/accept/${game.game_id}/${encodeURIComponent(role)}`) +} + +function send_invite() { + let invite_user = document.getElementById("invite_user").value + post(`/invite/${game.game_id}/${encodeURIComponent(invite_role)}/${encodeURIComponent(invite_user)}`) + document.getElementById("invite").close() +} + +function show_invite(role) { + invite_role = role + document.getElementById("invite").showModal() +} + +function hide_invite() { + document.getElementById("invite").close() +} + let blink_title = document.title let blink_timer = 0 @@ -115,6 +135,14 @@ function is_solo() { return players.every(p => p.user_id === players[0].user_id) } +function play_link(player) { + return `<a href="/${game.title_id}/play.html?game=${game.game_id}&role=${encodeURIComponent(player.role)}">${player.name}</a>` +} + +function action_link(player, action, color, text) { + return `<a class="${color}" href="javascript:${action}('${player.role}')">${text}</a>` +} + function update() { for (let i = 0; i < roles.length; ++i) { let role = roles[i] @@ -125,29 +153,47 @@ function update() { let player = players.find(p => p.role === role) let element = document.getElementById(role_id) if (player) { + element.classList.remove("is_invite") switch (game.status) { case 2: if (player.user_id === user_id) - element.innerHTML = `<a href="/${game.title_id}/play.html?game=${game.game_id}&role=${encodeURIComponent(role)}">${player.name}</a>` + element.innerHTML = play_link(player) else element.innerHTML = player.name break case 1: element.classList.toggle("is_active", is_active(player, role)) if (player.user_id === user_id) - element.innerHTML = `<a href="/${game.title_id}/play.html?game=${game.game_id}&role=${encodeURIComponent(role)}">${player.name}</a><a class="red" href="javascript:part('${role}')">\u274c</a>` + element.innerHTML = play_link(player) + action_link(player, "part", "red", "\u274c") else if (game.owner_id === user_id) - element.innerHTML = `${player.name}<a class="red" href="javascript:kick('${role}')">\u274c</a>` + element.innerHTML = player.name + action_link(player, "kick", "red", "\u274c") else element.innerHTML = player.name break case 0: - if (player.user_id === user_id) - element.innerHTML = `${player.name}<a class="red" href="javascript:part('${role}')">\u274c</a>` - else if (game.owner_id === user_id) - element.innerHTML = `${player.name}<a class="red" href="javascript:kick('${role}')">\u274c</a>` - else - element.innerHTML = player.name + if (player.is_invite) { + element.classList.add("is_invite") + if (player.user_id === user_id) + element.innerHTML = player.name + " ?" + + action_link(player, "part", "red", "\u274c") + + action_link(player, "accept", "green", "\u2714") + else if (player.user_id === user_id) + element.innerHTML = player.name + " ?" + action_link(player, "part", "red", "\u274c") + else if (game.owner_id === user_id) + element.innerHTML = player.name + " ?" + action_link(player, "kick", "red", "\u274c") + else + element.innerHTML = player.name + " ?" + } else { + element.classList.remove("is_invite") + if (player.user_id === user_id && player.is_invite) + element.innerHTML = player.name + action_link(player, "part", "red", "\u274c") + else if (player.user_id === user_id) + element.innerHTML = player.name + action_link(player, "part", "red", "\u274c") + else if (game.owner_id === user_id) + element.innerHTML = player.name + action_link(player, "kick", "red", "\u274c") + else + element.innerHTML = player.name + } break } element.classList.toggle("friend", is_friend(player)) @@ -157,14 +203,20 @@ function update() { else element.title = "" } else { + element.classList.remove("is_invite") switch (game.status) { case 2: element.innerHTML = `<i>Empty</i>` break case 1: - case 0: element.innerHTML = `<a class="join" href="javascript:join('${role}')">Join</a>` break + case 0: + if (game.owner_id === user_id) + element.innerHTML = `<a class="join" href="javascript:join('${role}')">Join</a><a class="green" href="javascript:show_invite('${role}')">\u{2795}</a>` + else + element.innerHTML = `<a class="join" href="javascript:join('${role}')">Join</a>` + break } element.classList.remove("friend") element.classList.remove("enemy") diff --git a/public/style.css b/public/style.css index c4b0b9e..e530ef3 100644 --- a/public/style.css +++ b/public/style.css @@ -1,9 +1,9 @@ html, input, textarea { - font-family: "Source Serif", "Georgia", "Dingbats", "Noto Emoji", serif; + font-family: "Source Serif", "Georgia", "Noto Emoji", "Dingbats", serif; font-size: 16px; } button, select { - font-family: "Source Sans", "Verdana", "Dingbats", "Noto Emoji", sans-serif; + font-family: "Source Sans", "Verdana", "Noto Emoji", "Dingbats", sans-serif; font-size: 16px; } @@ -156,6 +156,7 @@ article hr + p { font-style: italic; } .game_item a:not(:hover) { text-decoration: none; color: black; } .game_item a.command { text-decoration: underline; font-weight: bold } .game_info .is_active { text-decoration: underline } +.game_info .is_invite { opacity: 0.5 } .game_info div { text-indent: -20px; padding-left: 20px; @@ -322,6 +322,7 @@ create table if not exists players ( game_id integer, role text, user_id integer, + is_invite integer, primary key (game_id, role) ) without rowid; @@ -267,6 +267,7 @@ const SQL_SELECT_LOGIN_BY_NAME = SQL("SELECT * FROM user_login_view WHERE name=? const SQL_SELECT_USER_PROFILE = SQL("SELECT * FROM user_profile_view WHERE name=?") const SQL_SELECT_USER_DYNAMIC = SQL("select * from user_dynamic_view where user_id=?") const SQL_SELECT_USER_NAME = SQL("SELECT name FROM users WHERE user_id=?").pluck() +const SQL_SELECT_USER_ID = SQL("SELECT user_id FROM users WHERE name=?").pluck() const SQL_OFFLINE_USER = SQL("SELECT * FROM user_view NATURAL JOIN user_last_seen WHERE user_id=? AND julianday() > atime + ?") @@ -1054,6 +1055,9 @@ function get_game_roles(title_id, scenario, options) { } function is_game_ready(title_id, scenario, options, players) { + for (let p of players) + if (p.is_invite) + return false return get_game_roles(title_id, scenario, options).length === players.length } @@ -1093,11 +1097,12 @@ const SQL_SELECT_GAME_HAS_TITLE_AND_STATUS = SQL("SELECT 1 FROM games WHERE game const SQL_SELECT_PLAYERS_ID = SQL("SELECT DISTINCT user_id FROM players WHERE game_id=?").pluck() const SQL_SELECT_PLAYERS = SQL("SELECT * FROM players NATURAL JOIN user_view WHERE game_id=?") -const SQL_SELECT_PLAYERS_JOIN = SQL("SELECT role, user_id, name FROM players NATURAL JOIN users WHERE game_id=?") +const SQL_SELECT_PLAYERS_JOIN = SQL("SELECT role, user_id, name, is_invite FROM players NATURAL JOIN users WHERE game_id=?") +const SQL_UPDATE_PLAYER_ACCEPT = SQL("UPDATE players SET is_invite=0 WHERE game_id=? AND role=? AND user_id=?") +const SQL_UPDATE_PLAYER_ROLE = SQL("UPDATE players SET role=? WHERE game_id=? AND role=? AND user_id=?") const SQL_SELECT_PLAYER_ROLE = SQL("SELECT role FROM players WHERE game_id=? AND user_id=?").pluck() -const SQL_INSERT_PLAYER_ROLE = SQL("INSERT OR IGNORE INTO players (game_id,role,user_id) VALUES (?,?,?)") +const SQL_INSERT_PLAYER_ROLE = SQL("INSERT OR IGNORE INTO players (game_id,role,user_id,is_invite) VALUES (?,?,?,?)") const SQL_DELETE_PLAYER_ROLE = SQL("DELETE FROM players WHERE game_id=? AND role=?") -const SQL_UPDATE_PLAYER_ROLE = SQL("UPDATE players SET role=? WHERE game_id=? AND role=? AND user_id=?") const SQL_AUTHORIZE_GAME_ROLE = SQL("SELECT 1 FROM players NATURAL JOIN games WHERE title_id=? AND game_id=? AND role=? AND user_id=?").pluck() @@ -1109,13 +1114,15 @@ const SQL_INSERT_REMATCH = SQL(` INSERT INTO games (owner_id, title_id, scenario, options, is_private, is_random, description) SELECT - $user_id, title_id, scenario, options, is_private, is_random, $magic + $user_id, title_id, scenario, options, is_private, 0, $magic FROM games WHERE game_id = $game_id AND NOT EXISTS ( SELECT * FROM games WHERE description=$magic ) `) +const SQL_INSERT_REMATCH_PLAYERS = SQL("insert into players (game_id, user_id, role, is_invite) select ?, user_id, role, user_id!=? from players where game_id=?") + const QUERY_LIST_PUBLIC_GAMES = SQL(` SELECT * FROM game_view WHERE is_private=0 AND status = ? @@ -1224,10 +1231,14 @@ function annotate_game(game, user_id, unread) { your_count++ if ((p_is_active || p_is_owner) && game.is_ready) game.your_turn = true + if (p.is_invite) + game.your_turn = true } let link - if (p_is_active || p_is_owner) + if (p.is_invite) + link = `<span class="is_invite"><a href="/user/${p.name}">${p.name}?</a></span>` + else if (p_is_active || p_is_owner) link = `<span class="is_active"><a href="/user/${p.name}">${p.name}</a></span>` else link = `<a href="/user/${p.name}">${p.name}</a>` @@ -1403,44 +1414,19 @@ app.get('/delete/:game_id', must_be_logged_in, function (req, res) { res.redirect('/'+title_id) }) -function join_rematch(req, res, game_id, role) { - try { - let is_random = SQL_SELECT_GAME_RANDOM.get(game_id) - if (is_random) { - let role = SQL_SELECT_PLAYER_ROLE.get(game_id, req.user.user_id) - if (!role) { - for (let i = 1; i <= 6; ++i) { - let info = SQL_INSERT_PLAYER_ROLE.run(game_id, 'Random ' + i, req.user.user_id) - if (info.changes === 1) { - update_join_clients_players(game_id) - break - } - } - } - } else { - let info = SQL_INSERT_PLAYER_ROLE.run(game_id, role, req.user.user_id) - if (info.changes === 1) - update_join_clients_players(game_id) - } - } catch (err) { - console.log(err) - } - return res.redirect('/join/'+game_id) -} - -app.get('/rematch/:old_game_id/:role', must_be_logged_in, function (req, res) { +app.get('/rematch/:old_game_id', must_be_logged_in, function (req, res) { let old_game_id = req.params.old_game_id | 0 - let role = req.params.role let magic = "\u{1F503} " + old_game_id let new_game_id = 0 + console.log("FOO", old_game_id, magic) let info = SQL_INSERT_REMATCH.run({user_id: req.user.user_id, game_id: old_game_id, magic: magic}) - if (info.changes === 1) + if (info.changes === 1) { new_game_id = info.lastInsertRowid - else + SQL_INSERT_REMATCH_PLAYERS.run(new_game_id, req.user.user_id, old_game_id) + } else { new_game_id = SQL_SELECT_REMATCH.get(magic) - if (new_game_id) - return join_rematch(req, res, new_game_id, role) - return res.status(404).send("Can't create or find rematch game!") + } + return res.redirect('/join/'+new_game_id) }) var join_clients = {} @@ -1499,9 +1485,12 @@ app.get('/join/:game_id', must_be_logged_in, function (req, res) { let players = SQL_SELECT_PLAYERS_JOIN.all(game_id) let whitelist = SQL_SELECT_CONTACT_WHITELIST.all(req.user.user_id) let blacklist = SQL_SELECT_CONTACT_BLACKLIST.all(req.user.user_id) + let friends = null + if (game.owner_id === req.user.user_id) + friends = SQL_SELECT_CONTACT_FRIEND_NAMES.all(req.user.user_id) let ready = (game.status === 0) && is_game_ready(game.title_id, game.scenario, game.options, players) res.render('join.pug', { - user: req.user, game, roles, players, ready, whitelist, blacklist + user: req.user, game, roles, players, ready, whitelist, blacklist, friends }) }) @@ -1539,9 +1528,7 @@ app.get('/join-events/:game_id', must_be_logged_in, function (req, res) { res.write("data: " + JSON.stringify(players) + "\n\n") }) -app.post('/join/:game_id/:role', must_be_logged_in, function (req, res) { - let game_id = req.params.game_id | 0 - let role = req.params.role +function do_join(res, game_id, role, user_id, is_invite) { let game = SQL_SELECT_GAME.get(game_id) let roles = get_game_roles(game.title_id, game.scenario, game.options) if (game.is_random && game.status === 0) { @@ -1552,12 +1539,43 @@ app.post('/join/:game_id/:role', must_be_logged_in, function (req, res) { if (!roles.includes(role)) return res.status(404).send("Invalid role.") } - let info = SQL_INSERT_PLAYER_ROLE.run(game_id, role, req.user.user_id) + let info = SQL_INSERT_PLAYER_ROLE.run(game_id, role, user_id, is_invite) + if (info.changes === 1) { + update_join_clients_players(game_id) + res.send("SUCCESS") + } else { + if (is_invite) + res.send("Could not invite.") + else + res.send("Could not join game.") + } +} + +app.post('/join/:game_id/:role', must_be_logged_in, function (req, res) { + let game_id = req.params.game_id | 0 + let role = req.params.role + do_join(res, game_id, role, req.user.user_id, 0) +}) + +app.post('/invite/:game_id/:role/:user', must_be_logged_in, function (req, res) { + let game_id = req.params.game_id | 0 + let role = req.params.role + let user_id = SQL_SELECT_USER_ID.get(req.params.user) + if (user_id) + do_join(res, game_id, role, user_id, 1) + else + res.send("User not found.") +}) + +app.post('/accept/:game_id/:role', must_be_logged_in, function (req, res) { + let game_id = req.params.game_id | 0 + let role = req.params.role + let info = SQL_UPDATE_PLAYER_ACCEPT.run(game_id, role, req.user.user_id) if (info.changes === 1) { update_join_clients_players(game_id) res.send("SUCCESS") } else { - res.send("Could not join game.") + res.send("Could not accept invite.") } }) diff --git a/views/join.pug b/views/join.pug index 90f0db7..abbaf2c 100644 --- a/views/join.pug +++ b/views/join.pug @@ -11,11 +11,13 @@ html table { min-width: 0; } th,td { border: 1px solid black; } td a.red { text-decoration: none; color: brown; font-size: 15px; float: right; } + td a.green { text-decoration: none; color: green; font-size: 15px; float: right; } td a { text-decoration: underline; color: blue; } th { white-space: nowrap; background-color: gainsboro; } td { width: 180px; background-color: white; } #message { background-color: whitesmoke; } .hide { display: none; } + td.is_invite { color: gray } td.enemy { background-color: #f66 } td.enemy::before { content: "\1f6ab "; color: #000; font-size: 15px; } script. @@ -25,6 +27,7 @@ html let user_id = !{ user.user_id } let whitelist = !{ JSON.stringify(whitelist) } let blacklist = !{ JSON.stringify(blacklist) } + let friends = !{ JSON.stringify(friends) } let ready = !{ ready } script(src="/join.js") body @@ -50,6 +53,18 @@ html br(clear="left") + dialog(id="invite") + | Invite a friend: + br + input(id="invite_user" type="text" list="friends" onchange="send_invite()") + datalist(id="friends") + if friends + each who in friends + option= who + br + button(onclick="send_invite()") Invite + button(onclick="hide_invite()") Cancel + p table tbody |