"use strict" /* 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 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?" 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)) post("/api/rewind/" + game.game_id) } 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(`/api/start/${game.game_id}`) } function join(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(`/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(`/api/part/${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) { document.getElementById("invite").close() post(`/api/invite/${game.game_id}/${encodeURIComponent(invite_role)}/${encodeURIComponent(invite_user)}`) } } let blink_title = document.title let blink_timer = 0 function start_blinker(message) { let tick = false if (blink_timer) stop_blinker() if (!document.hasFocus()) { document.title = message blink_timer = setInterval(function () { document.title = tick ? message : blink_title tick = !tick }, 1000) } } function stop_blinker() { document.title = blink_title clearInterval(blink_timer) blink_timer = 0 } 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("hello", function (evt) { console.log("HELLO", evt.data) window.disconnected.textContent = "" }) 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", evt.data) game = null roles = null players = null update() evtsrc.close() }) evtsrc.onerror = function (evt) { console.log("ERROR", evt) window.disconnected.textContent = "Disconnected from server..." } window.addEventListener('beforeunload', function (_evt) { evtsrc.close() }) } } function user_link(user_name) { return `${user_name}` } function player_link(player) { let link = user_link(player.name) if (player.is_invite) link = "" + link + " ?" if (player.user_id === user_id) link = "\xbb " + link return link } 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 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 create_element(parent, tag, classList) { let e = document.createElement(tag) if (classList) e.classList = classList parent.appendChild(e) return e } 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 create_game_list() { let table = create_element(window.game_info, "table") let list = create_element(table, "tbody") if (game.scenario !== "Standard") 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 create_game_list_item(list, "Created", human_date(game.ctime)) create_game_list_item(list, "Moves", game.moves) if (game.status === 1) { create_game_list_item(list, "Last move", human_date(game.mtime)) } if (game.status > 1) { create_game_list_item(list, "Finished", human_date(game.mtime)) create_game_list_item(list, "Result", game.result) } } 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 === 1 && (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 = "seen " + human_date(player.atime) + "" if (user_id) { if (is_blacklist(player)) td_player_name.classList.add("blacklist") if (player.user_id === user_id) { if (player.is_invite) { action_link(td_actions, "Decline", decline, role) action_link(td_actions, "Accept", accept, role) } else { if (may_part()) action_link(td_actions, "Leave", part, role) if (game.status === 1 || game.status === 2) play_link(td_actions, player) } } else { if (may_kick()) action_link(td_actions, "Kick", kick, role) } } } else { td_player_name.innerHTML = "Empty" 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) } } } } 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 = `Login or sign up to join.` } else if (game.status === 1) window.game_enter.innerHTML = `Watch` else if (game.status === 2) window.game_enter.innerHTML = `Review` else window.game_enter.innerHTML = "Archived." create_game_list() for (let i = 0; i < roles.length; ++i) { let role = roles[i] if (game.is_random && game.status === 0) role = "Random " + (i+1) create_player_box(role, players.find(p => p.role === role)) } 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 stop_blinker() } } window.onload = function () { update() if (user_id && game.status <= 1) { start_event_source() setInterval(start_event_source, 15000) setInterval(update, 60000) } }