summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--public/join.js603
-rw-r--r--public/style.css3
-rw-r--r--schema.sql16
-rw-r--r--server.js240
-rw-r--r--views/create.pug6
-rw-r--r--views/head.pug23
-rw-r--r--views/info.pug4
-rw-r--r--views/join.pug84
8 files changed, 533 insertions, 446 deletions
diff --git a/public/join.js b/public/join.js
index f3279ad..f852b95 100644
--- a/public/join.js
+++ b/public/join.js
@@ -1,66 +1,232 @@
"use strict"
-let start_status = game.status
+/* global game, roles, players, blacklist, user_id */
+
+const pace_text = [
+ "",
+ "Live!",
+ "Fast \u2013 many moves per day",
+ "Slow \u2013 one move per day",
+]
+
+let start_status = 0
let evtsrc = null
-let timer = 0
let invite_role = null
+function is_game_ready() {
+ if (game.player_count !== players.length)
+ return false
+ for (let p of players)
+ if (p.is_invite)
+ return false
+ return true
+}
+
+function is_blacklist(p) {
+ return blacklist && blacklist.includes(p.user_id)
+}
+
+function has_already_joined() {
+ for (let p of players)
+ if (p.user_id === user_id)
+ return true
+ return false
+}
+
+function has_other_players() {
+ for (let p of players)
+ if (p.user_id !== user_id)
+ return true
+ return false
+}
+
+function may_join() {
+ if (game.is_match || game.status > 1)
+ return false
+ if (has_already_joined()) {
+ if (user_id !== game.owner_id)
+ return false
+ if (has_other_players())
+ return false
+ }
+ return true
+}
+
+function may_part() {
+ if (game.is_match || game.status > 1)
+ return false
+ if (game.status > 0) {
+ if (!game.is_private)
+ return false
+ }
+ return true
+}
+
+function may_kick() {
+ if (game.owner_id !== user_id)
+ return false
+ return may_part()
+}
+
+function may_start() {
+ if (game.owner_id !== user_id || game.is_match || game.status !== 0)
+ return false
+ if (!is_game_ready())
+ return false
+ return true
+}
+
+function may_delete() {
+ if (game.owner_id !== user_id || game.is_match || game.status >= 2)
+ return false
+ if (game.status > 0 && game.user_count > 0)
+ return false
+ return true
+}
+
+function may_rewind() {
+ if (game.owner_id !== user_id || game.is_match || game.status !== 1)
+ return false
+ if (!game.is_private)
+ return false
+ return true
+}
+
+function option_to_english(k) {
+ if (k === true || k === 1)
+ return "yes"
+ if (k === false)
+ return "no"
+ if (typeof k === "string")
+ return k.replace(/_/g, " ").replace(/^\w/, (c) => c.toUpperCase())
+ return k
+}
+
+function format_options(options) {
+ if (options === "{}")
+ return ""
+ return Object.entries(JSON.parse(options))
+ .map(([ k, v ]) => {
+ if (k === "players")
+ return v + " Player"
+ if (v === true || v === 1)
+ return option_to_english(k)
+ return option_to_english(k) + "=" + option_to_english(v)
+ })
+ .join(", ")
+}
+
+function format_time_left(time) {
+ if (time <= 0)
+ return "no time left"
+ if (time <= 2 / 24)
+ return Math.floor(time * 24 * 60) + " minutes left"
+ if (time <= 2)
+ return Math.floor(time * 24) + " hours left"
+ return Math.floor(time) + " days left"
+}
+
+function epoch_from_julianday(x) {
+ return (x - 2440587.5) * 86400000
+}
+
+function julianday_from_epoch(x) {
+ return x / 86400000 + 2440587.5
+}
+
+function human_date(date) {
+ if (typeof date === "string")
+ date = julianday_from_epoch(Date.parse(date + "Z"))
+ if (typeof date !== "number")
+ return "never"
+ var days = julianday_from_epoch(Date.now()) - date
+ var seconds = days * 86400
+ if (days < 1) {
+ if (seconds < 60) return "now"
+ if (seconds < 120) return "1 minute ago"
+ if (seconds < 3600) return Math.floor(seconds / 60) + " minutes ago"
+ if (seconds < 7200) return "1 hour ago"
+ if (seconds < 86400) return Math.floor(seconds / 3600) + " hours ago"
+ }
+ if (days < 2) return "yesterday"
+ if (days < 14) return Math.floor(days) + " days ago"
+ if (days < 31) return Math.floor(days / 7) + " weeks ago"
+ return new Date(epoch_from_julianday(date)).toISOString().substring(0,10)
+}
+
function confirm_delete() {
- let warning = `Are you sure you want to DELETE this game?`
+ let warning = "Are you sure you want to DELETE this game?"
+ if (window.confirm(warning))
+ post("/api/delete/" + game.game_id)
+}
+
+function confirm_rewind() {
+ let warning = "Are you sure you want to REWIND this game to the last move?\n\nMake sure you have the consent of all the players."
if (window.confirm(warning))
- window.location.href = "/delete/" + game.game_id
+ post("/api/rewind/" + game.game_id)
}
-function post(url) {
- fetch(url, { method: "POST" })
- .then(r => r.text())
- .then(t => window.error.textContent = (t === "SUCCESS") ? "" : t)
- .catch(e => window.error.textContent = e)
+async function post(url) {
+ window.error.textContent = ""
+ let res = await fetch(url, { method: "POST" })
+ if (!res.ok) {
+ window.error.textContent = res.status + " " + res.statusText
+ return
+ }
+ let text = await res.text()
+ if (text !== "SUCCESS") {
+ window.error.textContent = text
+ return
+ }
start_event_source()
}
function start() {
- post(`/start/${game.game_id}`)
+ post(`/api/start/${game.game_id}`)
}
function join(role) {
- post(`/join/${game.game_id}/${encodeURIComponent(role)}`)
+ post(`/api/join/${game.game_id}/${encodeURIComponent(role)}`)
+}
+
+function accept(role) {
+ post(`/api/accept/${game.game_id}/${encodeURIComponent(role)}`)
+}
+
+function decline(role) {
+ post(`/api/part/${game.game_id}/${encodeURIComponent(role)}`)
}
function part(role) {
let warning = "Are you sure you want to LEAVE this game?"
if (game.status === 0 || window.confirm(warning))
- post(`/part/${game.game_id}/${encodeURIComponent(role)}`)
+ post(`/api/part/${game.game_id}/${encodeURIComponent(role)}`)
}
function kick(role) {
let player = players.find(p => p.role === role)
let warning = `Are you sure you want to KICK player ${player.name} (${role}) from this game?`
if (game.status === 0 || window.confirm(warning))
- post(`/part/${game.game_id}/${encodeURIComponent(role)}`)
+ post(`/api/part/${game.game_id}/${encodeURIComponent(role)}`)
}
-function accept(role) {
- post(`/accept/${game.game_id}/${encodeURIComponent(role)}`)
+function invite(role) {
+ invite_role = role
+ document.getElementById("invite").showModal()
+}
+
+function hide_invite() {
+ document.getElementById("invite").close()
}
function send_invite() {
let invite_user = document.getElementById("invite_user").value
if (invite_user) {
- post(`/invite/${game.game_id}/${encodeURIComponent(invite_role)}/${encodeURIComponent(invite_user)}`)
document.getElementById("invite").close()
+ post(`/api/invite/${game.game_id}/${encodeURIComponent(invite_role)}/${encodeURIComponent(invite_user)}`)
}
}
-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
@@ -86,261 +252,225 @@ function stop_blinker() {
window.addEventListener("focus", stop_blinker)
function start_event_source() {
+ if (!game)
+ return
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("hello", function (evt) {
+ console.log("HELLO", evt.data)
+ window.disconnected.textContent = ""
})
- evtsrc.addEventListener("game", function (evt) {
- console.log("GAME:", evt.data)
- game = JSON.parse(evt.data)
- if (game.status > 1) {
- console.log("CLOSED EVENT SOURCE")
- clearInterval(timer)
- evtsrc.close()
- }
+ evtsrc.addEventListener("updated", function (evt) {
+ console.log("UPDATED", evt.data)
+ let data = JSON.parse(evt.data)
+ game = data.game
+ roles = data.roles
+ players = data.players
update()
})
evtsrc.addEventListener("deleted", function (evt) {
- console.log("DELETED")
- window.location.href = '/' + game.title_id
+ console.log("DELETED", evt.data)
+ game = null
+ roles = null
+ players = null
+ update()
+ evtsrc.close()
})
- evtsrc.onerror = function (err) {
- window.message.innerHTML = "Disconnected from server..."
+ evtsrc.onerror = function (evt) {
+ console.log("ERROR", evt)
+ window.disconnected.textContent = "Disconnected from server..."
}
- window.addEventListener('beforeunload', function (evt) {
+ window.addEventListener('beforeunload', function (_evt) {
evtsrc.close()
})
}
}
-function is_friend(p) {
- return whitelist && whitelist.includes(p.user_id)
+function user_link(user_name) {
+ return `<a class="black" href="/user/${user_name}">${user_name}</a>`
}
-function is_enemy(p) {
- return blacklist && blacklist.includes(p.user_id)
+function player_link(player) {
+ let link = user_link(player.name)
+ if (player.is_invite)
+ link = "<i>" + link + "</i> ?"
+ if (player.user_id === user_id)
+ link = "\xbb " + link
+ return link
}
-function user_link(player) {
- return `<a class="black" href="/user/${player.name}">${player.name}</a>`
+function play_link(parent, player) {
+ let e = document.createElement("a")
+ e.setAttribute("href", `/${game.title_id}/play.html?game=${game.game_id}&role=${encodeURIComponent(player.role)}`)
+ e.textContent = "Play"
+ parent.appendChild(e)
}
-function play_link(player) {
- return `\xbb <a href="/${game.title_id}/play.html?game=${game.game_id}&role=${encodeURIComponent(player.role)}">${player.name}</a>`
+function action_link(parent, text, action, arg) {
+ let e = document.createElement("a")
+ e.setAttribute("href", `javascript:${action.name}('${arg}')`)
+ e.textContent = text
+ parent.appendChild(e)
}
-function action_link(player, action, color, text) {
- return `<a class="${color}" href="javascript:${action}('${player.role}')">${text}</a>`
+function create_element(parent, tag, classList) {
+ let e = document.createElement(tag)
+ if (classList)
+ e.classList = classList
+ parent.appendChild(e)
+ return e
}
-function update() {
- update_common()
- if (user_id)
- update_login()
- else
- update_no_login()
+function create_button(text, action) {
+ let e = create_element(window.game_actions, "button")
+ e.textContent = text
+ e.onclick = action
+}
+
+function create_game_list_item(parent, key, val) {
+ if (val) {
+ let tr = create_element(parent, "tr")
+ let e_key = create_element(tr, "td")
+ let e_val = create_element(tr, "td")
+ e_key.innerHTML = key
+ e_val.innerHTML = val
+ }
}
-function update_common() {
+function create_game_list() {
+ let table = create_element(window.game_info, "table")
+ let list = create_element(table, "tbody")
+
if (game.scenario !== "Standard")
- document.querySelector("h1").textContent = "#" + game.game_id + " - " + game.title_name + " - " + game.scenario
+ create_game_list_item(list, "Scenario", game.scenario)
+ create_game_list_item(list, "Options", format_options(game.options))
+ create_game_list_item(list, "Pace", pace_text[game.pace])
+ create_game_list_item(list, "Notice", game.notice)
+
+ if (game.owner_id)
+ create_game_list_item(list, "Created", human_date(game.ctime) + " by " + user_link(game.owner_name))
else
- document.querySelector("h1").textContent = "#" + game.game_id + " - " + game.title_name
+ create_game_list_item(list, "Created", human_date(game.ctime))
- let message = window.message
- if (game.status === 0) {
- if (ready)
- message.innerHTML = "Waiting to start..."
- else
- message.innerHTML = "Waiting for players to join..."
- } else if (game.status === 1) {
- message.innerHTML = `<a href="/${game.title_id}/play.html?game=${game.game_id}">Observe</a>`
- } else if (game.status === 2) {
- message.innerHTML = `<a href="/${game.title_id}/play.html?game=${game.game_id}">Review</a>`
- } else if (game.status === 3) {
- message.innerHTML = "Archived"
+
+ create_game_list_item(list, "Moves", game.moves)
+
+ if (game.status === 1) {
+ create_game_list_item(list, "Last move", human_date(game.mtime))
}
-}
-function update_no_login() {
- for (let i = 0; i < roles.length; ++i) {
- let role = roles[i]
- let role_id = "role_" + role.replace(/ /g, "_")
- if (game.is_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 (game.is_match) {
- if (player) {
- if (game.status === 1)
- element.classList.toggle("is_active", player.is_active)
- element.innerHTML = user_link(player)
- } else {
- element.innerHTML = `<i>Empty</i>`
- }
- continue
- }
+ if (game.status > 1) {
+ create_game_list_item(list, "Finished", human_date(game.mtime))
+ create_game_list_item(list, "Result", game.result)
+ }
+}
- if (player) {
- element.classList.remove("is_invite")
- switch (game.status) {
- case 3:
- element.innerHTML = player.name
- break
- case 2:
- element.innerHTML = user_link(player)
- break
- case 1:
- if (player.is_invite) {
- element.classList.add("is_invite")
- element.innerHTML = user_link(player) + " ?"
- } else {
- element.classList.toggle("is_active", player.is_active)
- element.innerHTML = user_link(player)
- }
- break
- case 0:
+function create_player_box(role, player) {
+ let box = create_element(window.game_players, "table")
+ let thead = create_element(box, "thead")
+ let tbody = create_element(box, "tbody")
+ let tr_role = create_element(thead, "tr")
+ let tr_player = create_element(tbody, "tr", "p")
+ let tr_actions = create_element(tbody, "tr", "a")
+
+ let td_role_name = create_element(tr_role, "td")
+ let td_role_time = create_element(tr_role, "td", "r")
+ let td_player_name = create_element(tr_player, "td")
+ let td_player_seen = create_element(tr_player, "td", "r")
+ let td_actions = create_element(tr_actions, "td", "a r")
+ td_actions.setAttribute("colspan", 2)
+ td_actions.textContent = "\u200b"
+
+ td_role_name.textContent = role
+
+ if (player) {
+ if (player.is_active && game.status === 1)
+ box.classList = "active"
+ if (player.is_invite)
+ box.classList = "invite"
+
+ if (game.status > 0 && (game.pace > 0 || player.time_left < 3))
+ td_role_time.textContent = format_time_left(player.time_left)
+
+ td_player_name.innerHTML = player_link(player)
+ if (player.user_id !== user_id && game.status <= 1)
+ td_player_seen.innerHTML = "<i>seen " + human_date(player.atime) + "</i>"
+
+ if (user_id) {
+ if (is_blacklist(player))
+ td_player_name.classList.add("blacklist")
+
+ if (player.user_id === user_id) {
if (player.is_invite) {
- element.classList.add("is_invite")
- element.innerHTML = user_link(player) + " ?"
+ action_link(td_actions, "Decline", decline, role)
+ action_link(td_actions, "Accept", accept, role)
} else {
- element.innerHTML = user_link(player)
+ if (may_part())
+ action_link(td_actions, "Leave", part, role)
+ if (game.status === 1 || game.status === 2)
+ play_link(td_actions, player)
}
- break
+ } else {
+ if (may_kick())
+ action_link(td_actions, "Kick", kick, role)
+ }
+ }
+ } else {
+ td_player_name.innerHTML = "<i>Empty</i>"
+
+ if (user_id) {
+ if (!game.is_match) {
+ if (game.owner_id === user_id)
+ action_link(td_actions, "Invite", invite, role)
+ if (may_join())
+ action_link(td_actions, "Join", join, role)
}
- } else {
- element.classList.remove("is_invite")
- element.innerHTML = `<i>Empty</i>`
}
}
}
-function update_login() {
+function update() {
+ window.error.textContent = ""
+ window.game_info.replaceChildren()
+ window.game_players.replaceChildren()
+ window.game_actions.replaceChildren()
+
+ if (!game) {
+ window.game_enter.textContent = "Game deleted!"
+ stop_blinker()
+ return
+ }
+
+ if (game.status === 0) {
+ if (user_id)
+ window.game_enter.textContent = "Waiting for players to join."
+ else
+ window.game_enter.innerHTML = `<a href="/login">Login</a> or <a href="/signup">sign up</a> to join.`
+ } else if (game.status === 1)
+ window.game_enter.innerHTML = `<a href="/${game.title_id}/play.html?game=${game.game_id}">Watch</a>`
+ else if (game.status === 2)
+ window.game_enter.innerHTML = `<a href="/${game.title_id}/play.html?game=${game.game_id}">Review</a>`
+ else
+ window.game_enter.innerHTML = "Archived."
+
+ create_game_list()
+
for (let i = 0; i < roles.length; ++i) {
let role = roles[i]
- let role_id = "role_" + role.replace(/ /g, "_")
if (game.is_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 (game.is_match) {
- if (player) {
- if (game.status === 1)
- element.classList.toggle("is_active", player.is_active)
- if (player.user_id === user_id && (game.status === 1 || game.status === 2))
- element.innerHTML = play_link(player)
- else
- element.innerHTML = user_link(player)
- } else {
- element.innerHTML = `<i>Empty</i>`
- }
- continue
- }
-
- if (player) {
- element.classList.remove("is_invite")
- switch (game.status) {
- case 3:
- element.innerHTML = player.name
- break
- case 2:
- if (player.user_id === user_id)
- element.innerHTML = play_link(player)
- else
- element.innerHTML = user_link(player)
- break
- case 1:
- 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 (game.owner_id === user_id)
- element.innerHTML = user_link(player) + " ?" + action_link(player, "kick", "red", "\u274c")
- else
- element.innerHTML = user_link(player) + " ?"
- } else {
- element.classList.toggle("is_active", player.is_active)
- if (player.user_id === user_id)
- element.innerHTML = play_link(player) + action_link(player, "part", "red", "\u274c")
- else if (game.owner_id === user_id)
- element.innerHTML = user_link(player) + action_link(player, "kick", "red", "\u274c")
- else
- element.innerHTML = user_link(player)
- }
- break
- case 0:
- 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 (game.owner_id === user_id)
- element.innerHTML = user_link(player) + " ?" + action_link(player, "kick", "red", "\u274c")
- else
- element.innerHTML = user_link(player) + " ?"
- } 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 = user_link(player) + action_link(player, "kick", "red", "\u274c")
- else
- element.innerHTML = user_link(player)
- }
- break
- }
- element.classList.toggle("friend", is_friend(player))
- element.classList.toggle("enemy", is_enemy(player))
- if (is_enemy(player))
- element.title = "You have blacklisted this user!"
- else
- element.title = ""
- } else {
- element.classList.remove("is_invite")
- switch (game.status) {
- case 2:
- element.innerHTML = `<i>Empty</i>`
- break
- case 1:
- case 0:
- if (limit)
- element.innerHTML = `<i>Empty</i>`
- else if (game.owner_id === user_id)
- element.innerHTML = `\xbb <a class="join" href="javascript:join('${role}')">Join</a><a class="green" href="javascript:show_invite('${role}')">\u{2795}</a>`
- else
- element.innerHTML = `\xbb <a class="join" href="javascript:join('${role}')">Join</a>`
- break
- }
- element.classList.remove("friend")
- element.classList.remove("enemy")
- element.title = ""
- }
+ create_player_box(role, players.find(p => p.role === role))
}
- 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 || game.user_count <= 1) ? "" : "hide"
- if (game.status === 0 && ready)
- start_blinker("READY TO START")
- else
- stop_blinker()
- } else {
+ if (user_id) {
+ if (may_rewind())
+ create_button("Rewind", confirm_rewind)
+ if (may_delete())
+ create_button("Delete", confirm_delete)
+ if (may_start())
+ create_button("Start", start)
+
if (start_status === 0 && game.status === 1)
start_blinker("STARTED")
else
@@ -350,8 +480,9 @@ function update_login() {
window.onload = function () {
update()
- if (user_id && game.status <= 1 && !game.is_match) {
+ if (user_id && game.status <= 1) {
start_event_source()
- timer = setInterval(start_event_source, 15000)
+ setInterval(start_event_source, 15000)
+ setInterval(update, 60000)
}
}
diff --git a/public/style.css b/public/style.css
index caa2feb..7928f86 100644
--- a/public/style.css
+++ b/public/style.css
@@ -21,7 +21,6 @@ html {
--color-table-active: hsla(55, 100%, 50%, 0.15);
--color-table-invite: hsla(120, 100%, 50%, 0.15);
- --color-table-danger: hsla(350, 100%, 50%, 0.15);
}
/* light gray */
@@ -141,6 +140,8 @@ h1 { margin: 16px 0 16px -1px; font-size: 24px; }
h2 { margin: 16px 0 16px -1px; font-size: 20px; }
h3 { margin: 16px 0 8px -1px; font-size: 16px; }
+h1 .icon { font-weight: normal; }
+
html {
overflow-y: scroll;
}
diff --git a/schema.sql b/schema.sql
index 9fe8a76..1d3641c 100644
--- a/schema.sql
+++ b/schema.sql
@@ -474,6 +474,18 @@ create view game_view as
on owner.user_id = games.owner_id
;
+drop view if exists game_view_public;
+create view game_view_public as
+ select
+ *
+ from
+ game_view
+ where
+ not is_private
+ and join_count > 0
+ and join_count = user_count
+ ;
+
drop view if exists player_view;
create view player_view as
select
@@ -502,11 +514,13 @@ create view player_view as
10.0 - (time_used - time_added)
end
end
- ) as time_left
+ ) as time_left,
+ atime
from
games
join players using(game_id)
join users using(user_id)
+ left join user_last_seen using(user_id)
;
drop view if exists time_control_view;
diff --git a/server.js b/server.js
index 86a4c68..52507e4 100644
--- a/server.js
+++ b/server.js
@@ -176,11 +176,19 @@ app.locals.ENABLE_MAIL = !!mailer
app.locals.ENABLE_WEBHOOKS = !!WEBHOOKS
app.locals.ENABLE_FORUM = process.env.FORUM | 0
+app.locals.EMOJI_PRIVATE = "\u{1F512}" // or 512
app.locals.EMOJI_MATCH = "\u{1f3c6}"
app.locals.EMOJI_LIVE = "\u{1f465}"
app.locals.EMOJI_FAST = "\u{1f3c1}"
app.locals.EMOJI_SLOW = "\u{1f40c}"
+const PACE_ICON = [
+ "",
+ app.locals.EMOJI_LIVE,
+ app.locals.EMOJI_FAST,
+ app.locals.EMOJI_SLOW
+]
+
app.set("x-powered-by", false)
app.set("etag", false)
app.set("view engine", "pug")
@@ -1146,9 +1154,10 @@ function option_to_english(k) {
return k
}
-function format_options(options_json, options) {
+function format_options(options_json) {
if (options_json in HUMAN_OPTIONS_CACHE)
return HUMAN_OPTIONS_CACHE[options_json]
+ let options = parse_game_options(options_json)
let text = Object.entries(options)
.map(([ k, v ]) => {
if (k === "players")
@@ -1163,6 +1172,8 @@ function format_options(options_json, options) {
function get_game_roles(title_id, scenario, options) {
let roles = RULES[title_id].roles
+ if (typeof options === "string")
+ options = parse_game_options(options)
if (typeof roles === "function")
return roles(scenario, options)
return roles
@@ -1198,7 +1209,7 @@ function load_rules(rules_dir, rules_file, title) {
else
setup.setup_name = title.title_name
}
- setup.roles = get_game_roles(setup.title_id, setup.scenario, parse_game_options(setup.options))
+ setup.roles = get_game_roles(setup.title_id, setup.scenario, setup.options)
}
title.about_html = fs.readFileSync(rules_dir + "/about.html")
@@ -1399,54 +1410,49 @@ const SQL_INSERT_REMATCH = SQL(`
`).pluck()
const QUERY_LIST_PUBLIC_GAMES_OPEN = SQL(`
- select * from game_view where status=0 and not is_private and join_count > 0 and join_count < player_count
+ select * from game_view_public where status = 0 and join_count < player_count
and not exists ( select 1 from contacts where me = owner_id and you = ? and relation < 0 )
order by mtime desc, ctime desc
`)
const QUERY_LIST_PUBLIC_GAMES_REPLACEMENT = SQL(`
- select * from game_view where status=1 and not is_private and join_count > 0 and join_count < player_count
+ select * from game_view_public where status = 1 and join_count < player_count
and not exists ( select 1 from contacts where me = owner_id and you = ? and relation < 0 )
order by mtime desc, ctime desc
`)
const QUERY_LIST_PUBLIC_GAMES_ACTIVE = SQL(`
- select * from game_view where status=1 and not is_private and join_count = player_count
+ select * from game_view_public where status = 1 and join_count = player_count
order by mtime desc, ctime desc
limit 12
`)
const QUERY_LIST_PUBLIC_GAMES_FINISHED = SQL(`
- select * from game_view where status=2 and not is_private
+ select * from game_view_public where status = 2
order by mtime desc, ctime desc
limit 12
`)
const QUERY_LIST_GAMES_OF_TITLE_OPEN = SQL(`
- select * from game_view where title_id=? and not is_private and status=0 and join_count > 0 and join_count < player_count
+ select * from game_view_public where title_id=? and status = 0 and join_count < player_count
and not exists ( select 1 from contacts where me = owner_id and you = ? and relation < 0 )
order by mtime desc, ctime desc
`)
-const QUERY_LIST_GAMES_OF_TITLE_READY = SQL(`
- select * from game_view where title_id=? and not is_private and status=0 and join_count = player_count
- order by mtime desc, ctime desc
- `)
-
const QUERY_LIST_GAMES_OF_TITLE_REPLACEMENT = SQL(`
- select * from game_view where title_id=? and not is_private and status=1 and join_count > 0 and join_count < player_count
+ select * from game_view_public where title_id=? and status = 1 and join_count < player_count
and not exists ( select 1 from contacts where me = owner_id and you = ? and relation < 0 )
order by mtime desc, ctime desc
`)
const QUERY_LIST_GAMES_OF_TITLE_ACTIVE = SQL(`
- select * from game_view where title_id=? and not is_private and status=1 and join_count = player_count
+ select * from game_view_public where title_id=? and status = 1 and join_count = player_count
order by mtime desc, ctime desc
limit 12
`)
const QUERY_LIST_GAMES_OF_TITLE_FINISHED = SQL(`
- select * from game_view where title_id=? and not is_private and status=2
+ select * from game_view_public where title_id=? and status = 2
order by mtime desc, ctime desc
limit 12
`)
@@ -1511,8 +1517,7 @@ function check_join_game_limit(user) {
}
function annotate_game_info(game, user_id, unread) {
- let options = parse_game_options(game.options)
- game.human_options = format_options(game.options, options)
+ game.human_options = format_options(game.options)
game.is_unread = set_has(unread, game.game_id)
@@ -1520,7 +1525,7 @@ function annotate_game_info(game, user_id, unread) {
let your_role = null
let time_left = Infinity
- let roles = get_game_roles(game.title_id, game.scenario, options)
+ let roles = get_game_roles(game.title_id, game.scenario, game.options)
game.players = SQL_SELECT_PLAYER_VIEW.all(game.game_id)
for (let p of game.players)
@@ -1676,13 +1681,11 @@ function get_title_page(req, res, title_id) {
let user_id = req.user ? req.user.user_id : 0
let open_games = QUERY_LIST_GAMES_OF_TITLE_OPEN.all(title_id, user_id)
- let ready_games = QUERY_LIST_GAMES_OF_TITLE_READY.all(title_id)
let replacement_games = QUERY_LIST_GAMES_OF_TITLE_REPLACEMENT.all(title_id, user_id)
let active_games = QUERY_LIST_GAMES_OF_TITLE_ACTIVE.all(title_id)
let finished_games = QUERY_LIST_GAMES_OF_TITLE_FINISHED.all(title_id)
annotate_games(open_games, user_id, unread)
- annotate_games(ready_games, user_id, unread)
annotate_games(replacement_games, user_id, unread)
annotate_games(active_games, user_id, unread)
annotate_games(finished_games, user_id, unread)
@@ -1691,7 +1694,6 @@ function get_title_page(req, res, title_id) {
user: req.user,
title: title,
open_games,
- ready_games,
replacement_games,
active_games,
finished_games,
@@ -1761,13 +1763,13 @@ app.post("/create/:title_id", must_be_logged_in, function (req, res) {
if (is_random_scenario(title_id, scenario))
rand = 1
- let player_count = get_game_roles(title_id, scenario, parse_game_options(options)).length
+ let player_count = get_game_roles(title_id, scenario, options).length
let game_id = SQL_INSERT_GAME.get(user_id, title_id, scenario, options, player_count, pace, priv, rand, notice, 0)
res.redirect("/join/" + game_id)
})
-app.get("/delete/:game_id", must_be_logged_in, function (req, res) {
+app.post("/api/delete/:game_id", must_be_logged_in, function (req, res) {
let game_id = req.params.game_id
let title_id = SQL_SELECT_GAME_TITLE.get(game_id)
let info = SQL_DELETE_GAME_BY_OWNER.run(game_id, req.user.user_id)
@@ -1775,13 +1777,13 @@ app.get("/delete/:game_id", must_be_logged_in, function (req, res) {
return res.send("Not authorized to delete that game ID.")
if (info.changes === 1)
update_join_clients_deleted(game_id)
- res.redirect("/" + title_id)
+ res.send("SUCCESS")
})
function insert_rematch_players(old_game_id, new_game_id, req_user_id, order) {
let game = SQL_SELECT_GAME.get(old_game_id)
let players = SQL_SELECT_PLAYERS.all(old_game_id)
- let roles = get_game_roles(game.title_id, game.scenario, parse_game_options(game.options))
+ let roles = get_game_roles(game.title_id, game.scenario, game.options)
let n = roles.length
if (players.length !== n)
@@ -1861,39 +1863,27 @@ app.post("/rematch/:old_game_id", must_be_logged_in, function (req, res) {
function update_join_clients_deleted(game_id) {
let list = join_clients[game_id]
- if (list && list.length > 0) {
- for (let { res } of list) {
- res.write("retry: 15000\n")
- res.write("event: deleted\n")
- res.write("data: The game doesn't exist.\n\n")
- }
- }
+ if (list && list.length > 0)
+ for (let res of list)
+ res.write("event: deleted\ndata: null\n\n")
delete join_clients[game_id]
}
-function update_join_clients_game(game_id) {
+function update_join_clients(game_id) {
let list = join_clients[game_id]
if (list && list.length > 0) {
let game = SQL_SELECT_GAME_VIEW.get(game_id)
- for (let { res } of list) {
- res.write("retry: 15000\n")
- res.write("event: game\n")
- res.write("data: " + JSON.stringify(game) + "\n\n")
- }
- }
-}
-
-function update_join_clients_players(game_id) {
- let list = join_clients[game_id]
- if (list && list.length > 0) {
- let players = SQL_SELECT_PLAYER_VIEW.all(game_id)
- let ready = is_game_ready(list.player_count, players)
- for (let { res } of list) {
- res.write("retry: 15000\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")
+ if (game) {
+ let players = SQL_SELECT_PLAYER_VIEW.all(game_id)
+ let roles = null
+ if (game)
+ roles = get_game_roles(game.title_id, game.scenario, game.options)
+ let data = "event: updated\ndata: " + JSON.stringify({game,roles,players}) + "\n\n"
+ for (let res of list)
+ res.write(data)
+ } else {
+ for (let res of list)
+ res.write("event: deleted\ndata: null\n\n")
}
}
}
@@ -1904,10 +1894,7 @@ app.get("/join/:game_id", function (req, res) {
if (!game)
return res.status(404).send("Invalid game ID.")
- let options = parse_game_options(game.options)
- game.human_options = format_options(game.options, options)
-
- let roles = get_game_roles(game.title_id, game.scenario, options)
+ let roles = get_game_roles(game.title_id, game.scenario, game.options)
let players = SQL_SELECT_PLAYER_VIEW.all(game_id)
let whitelist = null
@@ -1924,15 +1911,20 @@ app.get("/join/:game_id", function (req, res) {
rewind = SQL_SELECT_REWIND.all(game_id)
}
- let ready = (game.status === STATUS_OPEN) && is_game_ready(game.player_count, players)
- game.ctime = human_date(game.ctime)
- game.mtime = human_date(game.mtime)
+ let icon = ""
+ if (game.is_private)
+ icon += app.locals.EMOJI_PRIVATE
+ if (game.is_match)
+ icon += app.locals.EMOJI_MATCH
+ if (game.pace)
+ icon += PACE_ICON[game.pace]
+
res.render("join.pug", {
user: req.user,
+ icon,
game,
roles,
players,
- ready,
whitelist,
blacklist,
friends,
@@ -1950,34 +1942,30 @@ app.get("/join-events/:game_id", must_be_logged_in, function (req, res) {
res.setHeader("Connection", "keep-alive")
res.setHeader("X-Accel-Buffering", "no")
- if (!game) {
- return res.send("event: deleted\ndata: The game doesn't exist.\n\n")
- }
+ if (!game)
+ return res.send("data: null\n\n")
+
if (!(game_id in join_clients)) {
join_clients[game_id] = []
join_clients[game_id].player_count = game.player_count
}
- join_clients[game_id].push({ res: res, user_id: req.user.user_id })
+ join_clients[game_id].push(res)
res.on("close", () => {
let list = join_clients[game_id]
if (list) {
- let i = list.findIndex(item => item.res === res)
+ 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")
+ res.write("retry: 15000\nevent: hello\ndata: null\n\n")
})
function do_join(res, game_id, role, user_id, user_name, is_invite) {
let game = SQL_SELECT_GAME.get(game_id)
- let roles = get_game_roles(game.title_id, game.scenario, parse_game_options(game.options))
+ let roles = get_game_roles(game.title_id, game.scenario, game.options)
if (game.is_random && game.status === STATUS_OPEN) {
let m = role.match(/^Random (\d+)$/)
if (!m || Number(m[1]) < 1 || Number(m[1]) > roles.length)
@@ -1988,7 +1976,7 @@ function do_join(res, game_id, role, user_id, user_name, is_invite) {
}
let info = SQL_INSERT_PLAYER_ROLE.run(game_id, role, user_id, is_invite)
if (info.changes === 1) {
- update_join_clients_players(game_id)
+ update_join_clients(game_id)
res.send("SUCCESS")
// send chat message about player joining a game in progress
@@ -2003,7 +1991,7 @@ function do_join(res, game_id, role, user_id, user_name, is_invite) {
}
}
-app.post("/join/:game_id/:role", must_be_logged_in, function (req, res) {
+app.post("/api/join/:game_id/:role", must_be_logged_in, function (req, res) {
let game_id = req.params.game_id | 0
let role = req.params.role
let limit = check_join_game_limit(req.user)
@@ -2012,24 +2000,26 @@ app.post("/join/:game_id/:role", must_be_logged_in, function (req, res) {
do_join(res, game_id, role, req.user.user_id, req.user.name, 0)
})
-app.post("/invite/:game_id/:role/:user", must_be_logged_in, function (req, res) {
+app.post("/api/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, null, 1)
- else
+ if (!user_id)
res.send("User not found.")
+ else if (user_id === req.user.user_id)
+ res.send("You cannot invite yourself!")
+ else
+ do_join(res, game_id, role, user_id, null, 1)
})
-app.post("/accept/:game_id/:role", must_be_logged_in, function (req, res) {
+app.post("/api/accept/:game_id/:role", must_be_logged_in, function (req, res) {
// TODO: check join game limit if inviting self...
let game_id = req.params.game_id | 0
let game = SQL_SELECT_GAME.get(game_id)
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)
+ update_join_clients(game_id)
res.send("SUCCESS")
// send chat message about player joining a game in progress
@@ -2040,13 +2030,13 @@ app.post("/accept/:game_id/:role", must_be_logged_in, function (req, res) {
}
})
-app.post("/part/:game_id/:role", must_be_logged_in, function (req, res) {
+app.post("/api/part/:game_id/:role", must_be_logged_in, function (req, res) {
let game_id = req.params.game_id | 0
let role = req.params.role
let user_name = SQL_SELECT_PLAYER_NAME.get(game_id, role)
let game = SQL_SELECT_GAME.get(game_id)
SQL_DELETE_PLAYER_ROLE.run(game_id, role)
- update_join_clients_players(game_id)
+ update_join_clients(game_id)
res.send("SUCCESS")
// send chat message about player leaving a game in progress
@@ -2074,7 +2064,7 @@ function assign_random_roles(game, options, players) {
}
}
-app.post("/start/:game_id", must_be_logged_in, function (req, res) {
+app.post("/api/start/:game_id", must_be_logged_in, function (req, res) {
let game_id = req.params.game_id | 0
let game = SQL_SELECT_GAME.get(game_id)
if (game.owner_id !== req.user.user_id)
@@ -2124,35 +2114,12 @@ function start_game(game) {
SQL_ROLLBACK.run()
}
- update_join_clients_players(game.game_id)
- update_join_clients_game(game.game_id)
+ update_join_clients(game.game_id)
send_game_started_notification_to_offline_users(game.game_id)
send_your_turn_notification_to_offline_users(game.game_id, null, state.active)
}
-app.get("/play/:game_id/:role", function (req, res) {
- let game_id = req.params.game_id | 0
- let role = req.params.role
- let title = SQL_SELECT_GAME_TITLE.get(game_id)
- if (!title)
- return res.status(404).send("Invalid game ID.")
- res.redirect(play_url(title, game_id, role))
-})
-
-app.get("/play/:game_id", function (req, res) {
- let game_id = req.params.game_id | 0
- let user_id = req.user ? req.user.user_id : 0
- let title = SQL_SELECT_GAME_TITLE.get(game_id)
- if (!title)
- return res.status(404).send("Invalid game ID.")
- let role = SQL_SELECT_PLAYER_ROLE.get(game_id, user_id)
- if (role)
- res.redirect(play_url(title, game_id, role))
- else
- res.redirect(play_url(title, game_id, "Observer"))
-})
-
app.get("/api/replay/:game_id", function (req, res) {
let game_id = req.params.game_id | 0
let game = SQL_SELECT_GAME.get(game_id)
@@ -2173,9 +2140,7 @@ app.get("/api/export/:game_id", function (req, res) {
return res.type("application/json").send(SQL_SELECT_EXPORT.get(game_id))
})
-app.get("/admin/rewind/:game_id/:snap_id", must_be_administrator, function (req, res) {
- let game_id = req.params.game_id | 0
- let snap_id = req.params.snap_id | 0
+function rewind_game_to_snap(game_id, snap_id, res) {
let snap = SQL_SELECT_SNAP.get(game_id, snap_id)
let game_state = JSON.parse(SQL_SELECT_GAME_STATE.get(game_id))
let snap_state = JSON.parse(snap.state)
@@ -2190,19 +2155,45 @@ app.get("/admin/rewind/:game_id/:snap_id", must_be_administrator, function (req,
SQL_REWIND_GAME.run(snap_id - 1, snap_state.active, game_id)
- update_join_clients_game(game_id)
+ update_join_clients(game_id)
if (game_clients[game_id])
for (let other of game_clients[game_id])
send_state(other, snap_state)
SQL_COMMIT.run()
- } catch (err) {
- return res.send(err.toString())
} finally {
if (db.inTransaction)
SQL_ROLLBACK.run()
}
- res.redirect("/join/" + game_id)
+}
+
+const SQL_SELECT_REWIND_AUTH = SQL("select 1 from games where game_id=? and owner_id=? and is_private").pluck()
+const SQL_SELECT_REWIND_ONCE_1 = SQL("select max(replay_id) from game_replay where game_id=?").pluck()
+const SQL_SELECT_REWIND_ONCE_2 = SQL("select max(snap_id) from game_snap where game_id=? and replay_id<?").pluck()
+
+app.post("/api/rewind/:game_id", must_be_logged_in, function (req, res) {
+ let game_id = req.params.game_id | 0
+ if (!SQL_SELECT_REWIND_AUTH.get(game_id, req.user.user_id))
+ return res.send("Not authorized to rewind that game ID.")
+ let replay_id = SQL_SELECT_REWIND_ONCE_1.get(game_id)
+ if (replay_id) {
+ let snap_id = SQL_SELECT_REWIND_ONCE_2.get(game_id, replay_id)
+ if (snap_id) {
+ try {
+ rewind_game_to_snap(game_id, snap_id, res)
+ send_chat_message(game_id, null, null, `${req.user.name} rewound the game to move ${snap_id}.`)
+ return res.send("SUCCESS")
+ } catch (err) {
+ return res.send(err.toString())
+ }
+ }
+ }
+ res.send("Nothing to rewind!")
+})
+
+app.get("/api/rewind/:game_id/:snap_id", must_be_administrator, function (req, res) {
+ rewind_game_to_snap(req.params.game_id | 0, req.params.snap_id | 0, res)
+ res.redirect("/join/" + req.params.game_id)
})
const SQL_CLONE_1 = SQL(`
@@ -2219,7 +2210,7 @@ const SQL_CLONE_2 = [
SQL(`insert into game_snap(game_id,snap_id,replay_id,state) select $new_game_id,snap_id,replay_id,state from game_snap where game_id=$old_game_id`),
]
-app.get("/admin/clone/:game_id", must_be_administrator, function (req, res) {
+app.get("/api/clone/:game_id", must_be_administrator, function (req, res) {
let old_game_id = req.params.game_id | 0
let new_game_id = 0
@@ -2639,10 +2630,6 @@ function is_player_online(game_id, user_id) {
for (let other of game_clients[game_id])
if (other.user && other.user.user_id === user_id)
return true
- if (join_clients[game_id])
- for (let other of join_clients[game_id])
- if (other.user_id === user_id)
- return true
return false
}
@@ -2745,7 +2732,8 @@ function put_new_state(game_id, state, old_active, role, action, args) {
put_game_state(game_id, state, old_active, role)
- update_join_clients_game(game_id)
+ if (state.active !== old_active)
+ update_join_clients(game_id)
if (game_clients[game_id])
for (let other of game_clients[game_id])
send_state(other, state)
@@ -2808,7 +2796,7 @@ function do_resign(game_id, role, how) {
let result = "None"
- let roles = get_game_roles(game.title_id, game.scenario, parse_game_options(game.options))
+ let roles = get_game_roles(game.title_id, game.scenario, game.options)
if (game.player_count === 2) {
for (let r of roles)
if (r !== role)
@@ -3086,21 +3074,15 @@ wss.on("connection", (socket, req) => {
if (!socket.user)
return socket.close(1000, "You are not logged in!")
- if (socket.role && socket.role !== "undefined" && socket.role !== "null") {
- let me = players.find(p => p.user_id === socket.user.user_id && p.role === socket.role)
- if (!me)
- return socket.close(1000, "You aren't assigned that role!")
- } else {
- let me = players.find(p => p.user_id === socket.user.user_id)
- socket.role = me ? me.role : "Observer"
- }
+ if (!players.find(p => p.user_id === socket.user.user_id && p.role === socket.role))
+ return socket.close(1000, "You aren't assigned that role!")
let new_chat = SQL_SELECT_UNREAD_CHAT.get(socket.user.user_id, socket.game_id)
send_message(socket, "newchat", new_chat)
}
if (socket.seen === 0) {
- let roles = get_game_roles(game.title_id, game.scenario, parse_game_options(game.options))
+ let roles = get_game_roles(game.title_id, game.scenario, game.options)
send_message(socket, "players", [
socket.role,
roles.map(r => ({ role: r, name: players.find(p => p.role === r)?.name }))
diff --git a/views/create.pug b/views/create.pug
index 4e52da4..912156c 100644
--- a/views/create.pug
+++ b/views/create.pug
@@ -13,6 +13,8 @@ html
div.logo
+gamecover(title.title_id)
+ if limit
+ p.error= limit
if !user
p.error You are not logged in!
@@ -77,9 +79,7 @@ html
input(type="checkbox" name="is_private" value="true")
| Private
- if limit
- p.error= limit
- else
+ if !limit
p
button(type="submit") Create
diff --git a/views/head.pug b/views/head.pug
index 3deb614..e4ff857 100644
--- a/views/head.pug
+++ b/views/head.pug
@@ -61,10 +61,11 @@ mixin gamelist(list,hide_title=0)
else if (item.status === 2) className += " finished"
else if (item.status === 3) className += " archived"
if (item.is_unread) chat_icon = "\u{1f4dd}"
- if (item.is_match) pace_icon = EMOJI_MATCH
- else if (item.pace === 1) pace_icon = EMOJI_LIVE, pace_text = "Live!"
- else if (item.pace === 2) pace_icon = EMOJI_FAST, pace_text = "Fast - many moves per day"
- else if (item.pace === 3) pace_icon = EMOJI_SLOW, pace_text = "Slow - one move per day"
+ if (item.is_private) pace_icon += EMOJI_PRIVATE
+ if (item.is_match) pace_icon += EMOJI_MATCH
+ else if (item.pace === 1) pace_icon += EMOJI_LIVE, pace_text = "Live!"
+ else if (item.pace === 2) pace_icon += EMOJI_FAST, pace_text = "Fast - many moves per day"
+ else if (item.pace === 3) pace_icon += EMOJI_SLOW, pace_text = "Slow - one move per day"
div(class=className)
div.game_head
@@ -77,25 +78,25 @@ mixin gamelist(list,hide_title=0)
case item.status
when 0
- a(class="command" href=`/join/${item.game_id}`) Enter
+ a(class="command" href=`/join/${item.game_id}`) Join
when 1
if !item.is_ready
- a(class="command" href="/join/"+item.game_id) Enter
+ a(class="command" href="/join/"+item.game_id) Join
else if item.is_yours
if item.your_role
a(class="command" href=`/${item.title_id}/play.html?game=${item.game_id}&role=${encodeURIComponent(item.your_role)}`) Play
else
- a(class="command" href="/join/"+item.game_id) Enter
+ a(class="command" href="/join/"+item.game_id) Play
else
- a(class="command" href=`/${item.title_id}/play.html?game=${item.game_id}&role=Observer`) View
+ a(class="command" href=`/${item.title_id}/play.html?game=${item.game_id}`) Watch
when 2
if item.is_yours
if item.your_role
- a(class="command" href=`/${item.title_id}/play.html?game=${item.game_id}&role=${encodeURIComponent(item.your_role)}`) Play
+ a(class="command" href=`/${item.title_id}/play.html?game=${item.game_id}&role=${encodeURIComponent(item.your_role)}`) Review
else
- a(class="command" href="/join/"+item.game_id) Enter
+ a(class="command" href="/join/"+item.game_id) Review
else
- a(class="command" href=`/${item.title_id}/play.html?game=${item.game_id}&role=Observer`) View
+ a(class="command" href=`/${item.title_id}/play.html?game=${item.game_id}`) Review
when 3
| Archived
diff --git a/views/info.pug b/views/info.pug
index 369cf8a..5c8346b 100644
--- a/views/info.pug
+++ b/views/info.pug
@@ -30,10 +30,6 @@ html
p
a(href="/create/"+title.title_id) Create a new game
- if ready_games.length > 0
- h2 Open (waiting to start)
- +gamelist(ready_games, true)
-
if active_games.length > 0
h2 Recently active
+gamelist(active_games, true)
diff --git a/views/join.pug b/views/join.pug
index 1f00995..257e4a8 100644
--- a/views/join.pug
+++ b/views/join.pug
@@ -8,71 +8,51 @@ html
game.title_id)
title ##{game.game_id} - #{game.title_name}
style.
- th,td { border: var(--table-border); }
- a.red { text-decoration: none; color: var(--color-red); font-size: 15px; float: right; }
- a.green { text-decoration: none; color: var(--color-green); font-size: 15px; float: right; }
- td, th { border: var(--thin-border); }
- td { width: 180px; }
- .hide { display: none; }
- td.is_invite { background-color: var(--color-table-invite); }
- td.is_active { background-color: var(--color-table-active); }
- td.enemy { background-color: var(--color-table-danger); }
- td.enemy::before { content: "\1f6ab "; font-size: 15px; }
+ table { width: clamp(240px, 100%, 400px); }
+ table.invite tr.p { background-color: var(--color-table-invite) }
+ table.active tr.p { background-color: var(--color-table-active) }
+ #game_info td { padding: 2px 10px }
+ #game_info td:first-child { width: 80px }
+ #game_info tr:first-child td { padding-top: 5px }
+ #game_info tr:last-child td { padding-bottom: 5px }
+ td.r a { margin-left: 18px; }
+ tr.p i { color: #0008; }
+ td.blacklist::before { color: brown; content: "\1f6ab "; font-size: 15px; }
script.
let game = !{ JSON.stringify(game) }
- let roles = !{ JSON.stringify(roles) }
let players = !{ JSON.stringify(players) }
+ let roles = !{ JSON.stringify(roles) }
let user_id = !{ user ? user.user_id : 0 }
- let whitelist = !{ JSON.stringify(whitelist) }
let blacklist = !{ JSON.stringify(blacklist) }
let friends = !{ JSON.stringify(friends) }
- let limit = !{ !!limit }
- let ready = !{ ready }
script(src="/join.js")
body
include header
article
- if game.scenario === "Standard"
- h1 ##{game.game_id} - #{game.title_name}
+ if icon
+ h1 <span class="icon">#{icon}</span> ##{game.game_id} - #{game.title_name}
else
- h1 ##{game.game_id} - #{game.title_name} - #{game.scenario}
+ h1 ##{game.game_id} - #{game.title_name}
div.logo
+gamecover(game.title_id)
+ p.error#error
+
if limit && game.status < 1
p.error= limit
if !user
p.error You are not logged in!
- p.error#error
- if game.owner_id
- if game.is_private
- p Created #{game.ctime} by <a class="black" href="/user/#{game.owner_name}">#{game.owner_name}</a> (private).
- else
- p Created #{game.ctime} by <a class="black" href="/user/#{game.owner_name}">#{game.owner_name}</a>.
- else
- p Created #{game.ctime}.
+ div#game_info
- if game.status > 1
- p Finished #{game.mtime}.
- else if game.status > 0
- p Last move #{game.mtime}.
+ p#game_enter
- unless game.human_options === "None"
- p Options: #{game.human_options}.
+ div#game_players
- case game.pace
- when 1
- p #{EMOJI_LIVE} Live!
- when 2
- p #{EMOJI_FAST} Fast &ndash; many moves per day.
- when 3
- p #{EMOJI_SLOW} Slow &ndash; one move per day.
+ p#game_actions
- if game.notice
- p
- i= game.notice
+ p.error#disconnected
if user
dialog(id="invite")
@@ -87,30 +67,12 @@ html
button(onclick="send_invite()") Invite
button(onclick="hide_invite()") Cancel
- p
- table
- tbody
- each role in roles
- tr
- th.w(id="role_"+role.replace(/ /g, "_")+"_name")= role
- td(id="role_"+role.replace(/ /g, "_")) -
- tfoot
- tr
- td#message(colspan=2) -
-
- p
- button.hide#delete_button(onclick="confirm_delete()") Delete
- button.hide#start_button(onclick="start()" disabled) Start
-
- if !user && !ready
- p <a href="/login">Login</a> or <a href="/signup">sign up</a> to play.
-
if user && user.user_id === 1
hr
div: p
each snap in rewind
- <a href="/admin/rewind/#{game.game_id}/#{snap.snap_id}">REWIND #{snap.snap_id}</a> - #{snap.state} - #{snap.active}
+ <a href="/api/rewind/#{game.game_id}/#{snap.snap_id}">REWIND #{snap.snap_id}</a> - #{snap.state} - #{snap.active}
br
if DEBUG
- <a href="/admin/clone/#{game.game_id}">CLONE</a>
+ <a href="/api/clone/#{game.game_id}">CLONE</a>
br