summaryrefslogtreecommitdiff
path: root/public
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2022-01-02 17:59:07 +0100
committerTor Andersson <tor@ccxvii.net>2022-01-06 12:40:25 +0100
commit74166074606cdba810b1b42f87af8cbe0bab6519 (patch)
treeb88170560eb9dcd584766c300792fb6457ee91de /public
parent72ebf85bc7f3a5f8fdf37e96a319ccbb8ea5768d (diff)
downloadserver-74166074606cdba810b1b42f87af8cbe0bab6519.tar.gz
Add game replay functionality.
Handle missing replay data. Add replay/rematch/exit buttons on game-over. Set 'player' to active player during replays. Replace space with underscore in role class names. Fix critical undo bug! Set game_over state during replays. Fix jumpy view in battle replays. Nuke undo states from all actions, not just 'undo'. Log play and replay page requests. Clean up client.js and allow selecting replay viewpoint. Add debug mode to replay prompt showing active, state, and next action. Init client roles from HTML structure. Remove unused rematch functions. Drop /info/ prefix on game pages. Update body role classList when replay viewpoint changes.
Diffstat (limited to 'public')
-rw-r--r--public/common/client.js702
-rw-r--r--public/common/grid.css17
-rw-r--r--public/join.js5
3 files changed, 484 insertions, 240 deletions
diff --git a/public/common/client.js b/public/common/client.js
index 01b6c05..d9a1f48 100644
--- a/public/common/client.js
+++ b/public/common/client.js
@@ -1,28 +1,25 @@
"use strict";
-/* global io, on_update */
+/* URL: /$title_id/(re)play:$game_id:$role */
-/* URL: /$title_id/play:$game_id:$role */
-if (!/\/[\w-]+\/play:\d+(:[\w-]+)?/.test(window.location.pathname)) {
+if (!/\/[\w-]+\/(re)?play:\d+(:[\w-]+)?/.test(window.location.pathname)) {
document.getElementById("prompt").textContent = "Invalid game ID.";
throw Error("Invalid game ID.");
}
-const param_title_id = window.location.pathname.split("/")[1];
-const param_game_id = decodeURIComponent(window.location.pathname.split("/")[2]).split(":")[1] | 0;
-const param_role = decodeURIComponent(window.location.pathname.split("/")[2]).split(":")[2] || "Observer";
+let params = {
+ mode: window.location.pathname.split("/")[2].split(":")[0],
+ title_id: window.location.pathname.split("/")[1],
+ game_id: decodeURIComponent(window.location.pathname.split("/")[2]).split(":")[1] | 0,
+ role: decodeURIComponent(window.location.pathname.split("/")[2]).split(":")[2] || "Observer",
+}
-let game = null;
-let game_over = false;
-let player = null;
-let socket = null;
+let roles = Array.from(document.querySelectorAll(".role")).map(x=>({id:x.id,role:x.id.replace(/^role_/,"").replace(/_/g," ")}));
-let chat_is_visible = false;
-let chat_text = null;
-let chat_key = null;
-let chat_last_day = null;
-let chat_log = 0;
-let chat_seen = 0;
+let view = null;
+let player = "Observer";
+let socket = null;
+let chat = null;
function scroll_with_middle_mouse(panel_sel, multiplier) {
let panel = document.querySelector(panel_sel);
@@ -35,8 +32,8 @@ function scroll_with_middle_mouse(panel_sel, multiplier) {
down_y = e.clientY;
scroll_x = panel.scrollLeft;
scroll_y = panel.scrollTop;
- window.addEventListener('mousemove', mm);
- window.addEventListener('mouseup', mu);
+ window.addEventListener("mousemove", mm);
+ window.addEventListener("mouseup", mu);
e.preventDefault();
}
}
@@ -49,12 +46,12 @@ function scroll_with_middle_mouse(panel_sel, multiplier) {
}
function mu(e) {
if (e.button === 1) {
- window.removeEventListener('mousemove', mm);
- window.removeEventListener('mouseup', mu);
+ window.removeEventListener("mousemove", mm);
+ window.removeEventListener("mouseup", mu);
e.preventDefault();
}
}
- panel.addEventListener('mousedown', md);
+ panel.addEventListener("mousedown", md);
}
function drag_element_with_mouse(element_sel, grabber_sel) {
@@ -65,8 +62,8 @@ function drag_element_with_mouse(element_sel, grabber_sel) {
if (e.button === 0) {
save_x = e.clientX;
save_y = e.clientY;
- window.addEventListener('mousemove', mm);
- window.addEventListener('mouseup', mu);
+ window.addEventListener("mousemove", mm);
+ window.addEventListener("mouseup", mu);
e.preventDefault();
}
}
@@ -81,12 +78,12 @@ function drag_element_with_mouse(element_sel, grabber_sel) {
}
function mu(e) {
if (e.button === 0) {
- window.removeEventListener('mousemove', mm);
- window.removeEventListener('mouseup', mu);
+ window.removeEventListener("mousemove", mm);
+ window.removeEventListener("mouseup", mu);
e.preventDefault();
}
}
- grabber.addEventListener('mousedown', md);
+ grabber.addEventListener("mousedown", md);
}
/* TITLE BLINKER */
@@ -115,7 +112,18 @@ function stop_blinker() {
window.addEventListener("focus", stop_blinker);
-function load_chat() {
+/* CHAT */
+
+function init_chat() {
+ // only fetch new messages when we reconnect!
+ if (chat !== null) {
+ console.log("RECONNECT CHAT");
+ socket.emit("getchat", chat.log);
+ return;
+ }
+
+ console.log("CONNECT CHAT");
+
let chat_window = document.createElement("div");
chat_window.id = "chat_window";
chat_window.innerHTML = `
@@ -132,15 +140,50 @@ function load_chat() {
chat_button.addEventListener("click", toggle_chat);
document.querySelector("header").insertBefore(chat_button, document.getElementById("prompt"));
- chat_key = "chat/" + param_game_id;
- chat_text = document.getElementById("chat_text");
- chat_last_day = null;
- chat_log = 0;
- chat_seen = window.localStorage.getItem(chat_key) | 0;
+ chat = {
+ is_visible: false,
+ text_element: document.getElementById("chat_text"),
+ key: "chat/" + params.game_id,
+ last_day: null,
+ log: 0
+ }
+
+ chat.seen = window.localStorage.getItem(chat.key) | 0;
+
+ drag_element_with_mouse("#chat_window", "#chat_header");
+
+ document.getElementById("chat_form").addEventListener("submit", e => {
+ let input = document.getElementById("chat_input");
+ e.preventDefault();
+ if (input.value) {
+ socket.emit("chat", input.value);
+ input.value = "";
+ } else {
+ hide_chat();
+ }
+ });
+
+ document.querySelector("body").addEventListener("keydown", e => {
+ if (e.key === "Escape") {
+ if (chat.is_visible) {
+ e.preventDefault();
+ hide_chat();
+ }
+ }
+ if (e.key === "Enter") {
+ let input = document.getElementById("chat_input");
+ if (document.activeElement !== input) {
+ e.preventDefault();
+ show_chat();
+ }
+ }
+ });
+
+ socket.emit("getchat", 0);
}
function save_chat() {
- window.localStorage.setItem(chat_key, chat_log);
+ window.localStorage.setItem(chat.key, chat.log);
}
function update_chat(chat_id, utc_date, user, message) {
@@ -155,38 +198,70 @@ function update_chat(chat_id, utc_date, user, message) {
let line = document.createElement("div");
line.className = "date";
line.textContent = "~ " + date + " ~";
- chat_text.appendChild(line);
+ chat.text_element.appendChild(line);
}
function add_chat_line(time, user, message) {
let line = document.createElement("div");
line.textContent = "[" + time + "] " + user + " \xbb " + message;
- chat_text.appendChild(line);
- chat_text.scrollTop = chat_text.scrollHeight;
+ chat.text_element.appendChild(line);
+ chat.text_element.scrollTop = chat.text_element.scrollHeight;
}
- if (chat_id > chat_log) {
- chat_log = chat_id;
+ if (chat_id > chat.log) {
+ chat.log = chat_id;
let date = new Date(utc_date + "Z");
let day = date.toDateString();
- if (day !== chat_last_day) {
+ if (day !== chat.last_day) {
add_date_line(day);
- chat_last_day = day;
+ chat.last_day = day;
}
add_chat_line(format_time(date), user, message);
}
- if (chat_id > chat_seen) {
+ if (chat_id > chat.seen) {
let button = document.getElementById("chat_button");
start_blinker("NEW MESSAGE");
- if (!chat_is_visible)
+ if (!chat.is_visible)
button.classList.add("new");
else
save_chat();
}
}
-function init_client(roles) {
- game = null;
- player = null;
+function show_chat() {
+ if (!chat.is_visible) {
+ document.getElementById("chat_button").classList.remove("new");
+ document.getElementById("chat_window").classList.add("show");
+ document.getElementById("chat_input").focus();
+ chat.is_visible = true;
+ save_chat();
+ }
+}
+
+function hide_chat() {
+ if (chat.is_visible) {
+ document.getElementById("chat_window").classList.remove("show");
+ document.getElementById("chat_input").blur();
+ chat.is_visible = false;
+ }
+}
+
+function toggle_chat() {
+ if (chat.is_visible)
+ hide_chat();
+ else
+ show_chat();
+}
+
+/* CONNECT TO GAME SERVER */
+
+function init_player_names(players) {
+ for (let i = 0; i < roles.length; ++i) {
+ let sel = "#" + roles[i].id + " .role_user";
+ let p = players.find(p => p.role === roles[i].role);
+ document.querySelector(sel).textContent = p ? p.name : "NONE";
+ }
+}
+function init_play_client() {
const ROLE_SEL = [
".role.one",
".role.two",
@@ -197,162 +272,119 @@ function init_client(roles) {
".role.seven",
];
- const USER_SEL = [
- ".role.one .role_user",
- ".role.two .role_user",
- ".role.three .role_user",
- ".role.four .role_user",
- ".role.five .role_user",
- ".role.six .role_user",
- ".role.seven .role_user",
- ];
-
- load_chat();
-
- console.log("JOINING", param_title_id + "/" + param_game_id + "/" + param_role);
+ console.log("JOINING", params.title_id + "/" + params.game_id + "/" + params.role);
socket = io({
- transports: ['websocket'],
- query: { title: param_title_id, game: param_game_id, role: param_role },
+ transports: ["websocket"],
+ query: { title: params.title_id, game: params.game_id, role: params.role },
});
- socket.on('connect', () => {
+ socket.on("connect", () => {
console.log("CONNECTED");
- document.querySelector("header").classList.remove('disconnected');
- socket.emit('getchat', chat_log); // only send new messages when we reconnect!
+ document.querySelector("header").classList.remove("disconnected");
});
- socket.on('disconnect', () => {
+ socket.on("disconnect", () => {
console.log("DISCONNECTED");
document.getElementById("prompt").textContent = "Disconnected from server!";
- document.querySelector("header").classList.add('disconnected');
+ document.querySelector("header").classList.add("disconnected");
});
- socket.on('roles', (me, players) => {
- console.log("ROLES", me, JSON.stringify(players));
- player = me.replace(/ /g, '_');
- if (player === "Observer")
- document.getElementById("chat_button").style.display = "none";
- document.querySelector("body").classList.add(player);
- for (let i = 0; i < roles.length; ++i) {
- let pr = players.find(p => p.role === roles[i]);
- document.querySelector(USER_SEL[i]).textContent = pr ? pr.name : "NONE";
- }
+ socket.on("roles", (me, players) => {
+ console.log("PLAYERS", me, JSON.stringify(players));
+ player = me;
+ document.querySelector("body").classList.add(player.replace(/ /g, "_"));
+ if (player !== "Observer")
+ init_chat();
+ init_player_names(players);
});
- socket.on('presence', (presence) => {
+ socket.on("presence", (presence) => {
console.log("PRESENCE", JSON.stringify(presence));
for (let i = 0; i < roles.length; ++i) {
- let elt = document.querySelector(ROLE_SEL[i]);
- if (roles[i] in presence)
- elt.classList.add('present');
+ let elt = document.getElementById(roles[i].id);
+ if (roles[i].role in presence)
+ elt.classList.add("present");
else
- elt.classList.remove('present');
+ elt.classList.remove("present");
}
});
- socket.on('state', (new_game, new_game_over) => {
- console.log("STATE", !!new_game.actions, new_game_over);
- game = new_game;
- game_over = new_game_over;
- on_update_bar();
+ socket.on("state", (new_view) => {
+ console.log("STATE");
+ view = new_view;
+ on_update_header();
on_update();
- on_game_over();
on_update_log();
});
- socket.on('save', (msg) => {
+ socket.on("save", (msg) => {
console.log("SAVE");
- window.localStorage[param_title_id + '/save'] = msg;
+ window.localStorage[params.title_id + "/save"] = msg;
});
- socket.on('error', (msg) => {
+ socket.on("error", (msg) => {
console.log("ERROR", msg);
document.getElementById("prompt").textContent = msg;
});
- socket.on('chat', function (item) {
- console.log("CHAT", JSON.stringify(item));
+ socket.on("chat", function (item) {
update_chat(item[0], item[1], item[2], item[3]);
});
-
- document.getElementById("chat_form").addEventListener("submit", e => {
- let input = document.getElementById("chat_input");
- e.preventDefault();
- if (input.value) {
- socket.emit('chat', input.value);
- input.value = '';
- } else {
- hide_chat();
- }
- });
-
- document.querySelector("body").addEventListener("keydown", e => {
- if (player && player !== "Observer") {
- if (e.key === "Escape") {
- if (chat_is_visible) {
- e.preventDefault();
- hide_chat();
- }
- }
- if (e.key === "Enter") {
- let input = document.getElementById("chat_input");
- if (document.activeElement !== input) {
- e.preventDefault();
- show_chat();
- }
- }
- }
- });
-
- drag_element_with_mouse("#chat_window", "#chat_header");
}
+/* HEADER */
+
let is_your_turn = false;
let old_active = null;
-function on_update_bar() {
- document.getElementById("prompt").textContent = game.prompt;
- if (game.actions) {
+function on_update_header() {
+ document.getElementById("prompt").textContent = view.prompt;
+ if (params.mode === "replay")
+ return;
+ if (view.actions) {
document.querySelector("header").classList.add("your_turn");
- if (!is_your_turn || old_active !== game.active)
+ if (!is_your_turn || old_active !== view.active)
start_blinker("YOUR TURN");
is_your_turn = true;
} else {
document.querySelector("header").classList.remove("your_turn");
is_your_turn = false;
}
- old_active = game.active;
+ old_active = view.active;
}
+/* LOG */
+
let create_log_entry = function (text) {
- let p = document.createElement("div");
- p.textContent = text;
- return p;
+ let div = document.createElement("div");
+ div.textContent = text;
+ return div;
}
-let log_scroller = document.getElementById("log");
-
function on_update_log() {
- let parent = document.getElementById("log");
- let to_delete = parent.children.length - game.log_start;
+ let div = document.getElementById("log");
+ let to_delete = div.children.length - view.log_start;
while (to_delete-- > 0)
- parent.removeChild(parent.lastChild);
- for (let entry of game.log)
- parent.appendChild(create_log_entry(entry));
- log_scroller.scrollTop = log_scroller.scrollHeight;
+ div.removeChild(div.lastChild);
+ for (let entry of view.log)
+ div.appendChild(create_log_entry(entry));
+ scroll_log_to_end();
+}
+
+function scroll_log_to_end() {
+ let div = document.getElementById("log");
+ div.scrollTop = div.scrollHeight;
}
try {
- new ResizeObserver(entries => {
- log_scroller.scrollTop = log_scroller.scrollHeight;
- }).observe(log_scroller);
+ new ResizeObserver(scroll_log_to_end).observe(document.getElementById("log"));
} catch (err) {
- window.addEventListener("resize", evt => {
- log_scroller.scrollTop = log_scroller.scrollHeight;
- });
+ window.addEventListener("resize", scroll_log_to_end);
}
+/* MAP ZOOM */
+
function toggle_fullscreen() {
if (document.fullscreen)
document.exitFullscreen();
@@ -360,78 +392,60 @@ function toggle_fullscreen() {
document.documentElement.requestFullscreen();
}
-function show_chat() {
- if (!chat_is_visible) {
- document.getElementById("chat_button").classList.remove("new");
- document.getElementById("chat_window").classList.add("show");
- document.getElementById("chat_input").focus();
- chat_is_visible = true;
- save_chat();
- }
+function toggle_log() {
+ document.querySelector("aside").classList.toggle("hide");
+ zoom_map();
}
-function hide_chat() {
- if (chat_is_visible) {
- document.getElementById("chat_window").classList.remove("show");
- document.getElementById("chat_input").blur();
- chat_is_visible = false;
+function toggle_zoom() {
+ let mapwrap = document.getElementById("mapwrap");
+ if (mapwrap) {
+ mapwrap.classList.toggle("fit");
+ zoom_map();
}
}
-function toggle_chat() {
- if (chat_is_visible)
- hide_chat();
- else
- show_chat();
-}
-
function zoom_map() {
- let grid = document.querySelector("main");
let mapwrap = document.getElementById("mapwrap");
- let map = document.getElementById("map");
- map.style.transform = null;
- mapwrap.style.width = null;
- mapwrap.style.height = null;
- if (mapwrap.classList.contains("fit")) {
- let { width: gw, height: gh } = grid.getBoundingClientRect();
- let { width: ww, height: wh } = mapwrap.getBoundingClientRect();
- let { width: cw, height: ch } = map.getBoundingClientRect();
- let scale = Math.min(ww / cw, gh / ch);
- if (scale < 1) {
- map.style.transform = "scale(" + scale + ")";
- mapwrap.style.width = (cw * scale) + "px";
- mapwrap.style.height = (ch * scale) + "px";
+ if (mapwrap) {
+ let main = document.querySelector("main");
+ let map = document.getElementById("map");
+ map.style.transform = null;
+ mapwrap.style.width = null;
+ mapwrap.style.height = null;
+ if (mapwrap.classList.contains("fit")) {
+ let { width: gw, height: gh } = main.getBoundingClientRect();
+ let { width: ww, height: wh } = mapwrap.getBoundingClientRect();
+ let { width: cw, height: ch } = map.getBoundingClientRect();
+ let scale = Math.min(ww / cw, gh / ch);
+ if (scale < 1) {
+ map.style.transform = "scale(" + scale + ")";
+ mapwrap.style.width = (cw * scale) + "px";
+ mapwrap.style.height = (ch * scale) + "px";
+ }
}
}
}
-function toggle_zoom() {
- document.getElementById("mapwrap").classList.toggle('fit');
- zoom_map();
-}
+zoom_map();
-function init_map_zoom() {
- window.addEventListener('resize', zoom_map);
- zoom_map();
-}
+window.addEventListener("resize", zoom_map);
-function init_shift_zoom() {
- window.addEventListener("keydown", (evt) => {
- if (evt.key === "Shift")
- document.querySelector("body").classList.add("shift");
- });
- window.addEventListener("keyup", (evt) => {
- if (evt.key === "Shift")
- document.querySelector("body").classList.remove("shift");
- });
-}
+window.addEventListener("keydown", (evt) => {
+ if (evt.key === "Shift")
+ document.querySelector("body").classList.add("shift");
+});
-function toggle_log() {
- document.querySelector("aside").classList.toggle("hide");
- zoom_map();
-}
+window.addEventListener("keyup", (evt) => {
+ if (evt.key === "Shift")
+ document.querySelector("body").classList.remove("shift");
+});
+
+/* ACTIONS */
function action_button(action, label) {
+ if (params.mode === "replay")
+ return;
let id = action + "_button";
let button = document.getElementById(id);
if (!button) {
@@ -441,11 +455,11 @@ function action_button(action, label) {
button.addEventListener("click", evt => send_action(action));
document.getElementById("actions").appendChild(button);
}
- if (game.actions && action in game.actions) {
+ if (view.actions && action in view.actions) {
button.classList.remove("hide");
- if (game.actions[action]) {
+ if (view.actions[action]) {
if (label === undefined)
- button.textContent = game.actions[action];
+ button.textContent = view.actions[action];
button.disabled = false;
} else {
button.disabled = true;
@@ -455,66 +469,282 @@ function action_button(action, label) {
}
}
-function confirm_resign() {
- if (window.confirm("Are you sure that you want to resign?"))
- socket.emit('resign');
-}
-
function send_action(verb, noun) {
+ if (params.mode === "replay")
+ return;
// Reset action list here so we don't send more than one action per server prompt!
if (noun !== undefined) {
- if (game.actions && game.actions[verb] && game.actions[verb].includes(noun)) {
- game.actions = null;
+ if (view.actions && view.actions[verb] && view.actions[verb].includes(noun)) {
+ view.actions = null;
console.log("ACTION", verb, JSON.stringify(noun));
- socket.emit('action', verb, noun);
+ socket.emit("action", verb, noun);
return true;
}
} else {
- if (game.actions && game.actions[verb]) {
- game.actions = null;
+ if (view.actions && view.actions[verb]) {
+ view.actions = null;
console.log("ACTION", verb);
- socket.emit('action', verb);
+ socket.emit("action", verb);
return true;
}
}
return false;
}
+function confirm_resign() {
+ if (window.confirm("Are you sure that you want to resign?"))
+ socket.emit("resign");
+}
+
+/* DEBUGGING */
+
function send_save() {
- socket.emit('save');
+ socket.emit("save");
}
function send_restore() {
- socket.emit('restore', window.localStorage[param_title_id + '/save']);
+ socket.emit("restore", window.localStorage[params.title_id + "/save"]);
}
function send_restart(scenario) {
- socket.emit('restart', scenario);
+ socket.emit("restart", scenario);
}
-function on_game_over() {
- if (player) {
- let exit_button = document.getElementById("exit_button");
- if (exit_button) {
- if (game_over || player === "Observer")
- exit_button.classList.remove("hide");
- else
- exit_button.classList.add("hide");
+/* REPLAY */
+
+function adler32(data) {
+ let a = 1, b = 0;
+ for (let i = 0, n = data.length; i < n; ++i) {
+ a = (a + data.charCodeAt(i)) % 65521;
+ b = (b + a) % 65521;
+ }
+ return (b << 16) | a;
+}
+
+async function require(path) {
+ let cache = {};
+
+ if (!path.endsWith(".js"))
+ path = path + ".js";
+ if (path.startsWith("./"))
+ path = path.substring(2);
+
+ console.log("REQUIRE", path);
+
+ let response = await fetch(path);
+ let source = await response.text();
+
+ for (let [_, subpath] of source.matchAll(/require\(['"]([^)]*)['"]\)/g))
+ if (cache[subpath] === undefined)
+ cache[subpath] = await require(subpath);
+
+ let module = { exports: {} };
+ Function("module", "exports", "require", source)
+ (module, module.exports, path => cache[path]);
+ return module.exports;
+}
+
+let replay = null;
+
+async function init_replay_client() {
+ document.getElementById("prompt").textContent = "Loading replay...";
+ document.querySelector("body").classList.add("replay");
+
+ console.log("LOADING RULES");
+ let rules = await require("rules.js");
+
+ console.log("LOADING REPLAY");
+ let response = await fetch("/replay/" + params.game_id);
+ let body = await response.json();
+ replay = body.replay;
+
+ init_player_names(body.players);
+
+ let viewpoint = "Observer";
+ let log_length = 0;
+ let p = 0;
+ let s = {};
+
+ function eval_action(item) {
+ switch (item.action) {
+ case "setup":
+ s = rules.setup(item.arguments[0], item.arguments[1], item.arguments[2]);
+ break;
+ case "resign":
+ s = rules.resign(s, item.role);
+ break;
+ default:
+ s = rules.action(s, item.role, item.action, item.arguments);
+ break;
}
- let rematch_button = document.getElementById("rematch_button");
- if (rematch_button) {
- if (game_over && player !== "Observer")
- rematch_button.classList.remove("hide");
+ }
+
+ let ss;
+ for (p = 0; p < replay.length; ++p) {
+ replay[p].arguments = JSON.parse(replay[p].arguments);
+
+ if (rules.is_checkpoint) {
+ replay[p].is_checkpoint = (p > 0 && rules.is_checkpoint(ss, s));
+ ss = Object.assign({}, s);
+ }
+
+ eval_action(replay[p]);
+
+ replay[p].digest = adler32(JSON.stringify(s));
+ for (let k = p-1; k > 0; --k) {
+ if (replay[k].digest === replay[p].digest && !replay[k].is_undone) {
+ for (let a = k+1; a <= p; ++a)
+ if (!replay[a].is_undone)
+ replay[a].is_undone = true;
+ break;
+ }
+ }
+ }
+
+ replay = replay.filter(x => !x.is_undone);
+
+ function set_hash(n) {
+ history.replaceState(null, "", window.location.pathname + "#" + n);
+ }
+
+ let timer = 0;
+ function play_pause_replay(evt) {
+ if (timer === 0) {
+ evt.target.textContent = "Stop";
+ timer = setInterval(() => {
+ if (p < replay.length)
+ goto_replay(p+1);
+ else
+ play_pause_replay(evt);
+ }, 1000);
+ } else {
+ evt.target.textContent = "Run";
+ clearInterval(timer);
+ timer = 0;
+ }
+ }
+
+ function prev() {
+ for (let i = p - 1; i > 1; --i)
+ if (replay[i].is_checkpoint)
+ return i;
+ return 1;
+ }
+
+ function next() {
+ for (let i = p + 1; i < replay.length; ++i)
+ if (replay[i].is_checkpoint)
+ return i;
+ return replay.length;
+ }
+
+ function on_hash_change() {
+ goto_replay(parseInt(window.location.hash.slice(1)) || 1);
+ }
+
+ function goto_replay(np) {
+ if (np < 1)
+ np = 1;
+ if (np > replay.length)
+ np = replay.length;
+ set_hash(np);
+ if (p > np)
+ p = 0, s = {};
+ while (p < np)
+ eval_action(replay[p++]);
+ update_replay_view();
+ }
+
+ function update_replay_view() {
+ player = viewpoint;
+
+ if (viewpoint === "Active") {
+ player = s.active;
+ if (player === "All" || player === "Both" || !player)
+ player = "Observer";
+ }
+
+ let body = document.querySelector("body");
+ body.classList.remove("Observer");
+ for (let i = 0; i < roles.length; ++i)
+ body.classList.remove(roles[i].role.replace(/ /g, "_"));
+ body.classList.add(player.replace(/ /g, "_"));
+
+ view = rules.view(s, player);
+ view.actions = null;
+
+ if (viewpoint === "Observer")
+ view.game_over = 1;
+ if (s.state === "game_over")
+ view.game_over = 1;
+
+ if (replay.length > 0) {
+ if (document.querySelector("body").classList.contains("shift"))
+ view.prompt = `[${p}/${replay.length}] ${s.active} / ${s.state} / ${replay[p].action} ${replay[p].arguments}`;
else
- rematch_button.classList.add("hide");
+ view.prompt = "[" + p + "/" + replay.length + "] " + view.prompt;
}
+ if (log_length < view.log.length)
+ view.log_start = log_length;
+ else
+ view.log_start = view.log.length;
+ log_length = view.log.length;
+ view.log = view.log.slice(view.log_start);
+
+ on_update_header();
+ on_update();
+ on_update_log();
}
-}
-function send_rematch() {
- window.location = '/rematch/' + param_game_id + '/' + param_role;
-}
+ function replay_button(div, label, fn) {
+ let button = document.createElement("button");
+ button.addEventListener("click", fn);
+ button.className = "replay_button";
+ button.textContent = label;
+ div.appendChild(button);
+ return button;
+ }
-function send_exit() {
- window.location = '/info/' + param_title_id;
+ function set_viewpoint(vp) {
+ viewpoint = vp;
+ update_replay_view();
+ }
+
+ if (replay.length > 0) {
+ console.log("REPLAY READY");
+
+ let div = document.createElement("div");
+ div.className = "replay";
+ replay_button(div, "Active", () => set_viewpoint("Active"));
+ for (let r of roles)
+ replay_button(div, r.role, () => set_viewpoint(r.role));
+ replay_button(div, "Observer", () => set_viewpoint("Observer"));
+ document.querySelector("header").appendChild(div);
+
+ div = document.createElement("div");
+ div.className = "replay";
+ replay_button(div, "<<<", () => goto_replay(1));
+ replay_button(div, "<<", () => goto_replay(prev()));
+ replay_button(div, "<\xa0", () => goto_replay(p-1));
+ replay_button(div, "\xa0>", () => goto_replay(p+1));
+ replay_button(div, ">>", () => goto_replay(next()));
+ replay_button(div, "Run", play_pause_replay).style.width = "65px";
+ document.querySelector("header").appendChild(div);
+
+ if (window.location.hash === "")
+ set_hash(replay.length);
+
+ on_hash_change();
+
+ window.addEventListener("hashchange", on_hash_change);
+ } else {
+ console.log("REPLAY NOT AVAILABLE");
+ s = JSON.parse(body.state);
+ update_replay_view();
+ }
}
+
+if (params.mode === "play")
+ init_play_client();
+if (params.mode === "replay")
+ init_replay_client();
diff --git a/public/common/grid.css b/public/common/grid.css
index 0532648..228c0aa 100644
--- a/public/common/grid.css
+++ b/public/common/grid.css
@@ -25,14 +25,16 @@ body:not(.shift) .debug {
display: none;
}
-body.Observer .resign {
+body.Observer .resign, body.replay .resign {
display: none;
}
button {
- font-size: 1rem;
+ box-sizing: border-box;
+ font-size: 16px;
+ height: 28px;
margin: 0;
- padding: 1px 12px;
+ padding: 1px 12px 1px 12px;
background-color: gainsboro;
}
button:disabled {
@@ -177,6 +179,15 @@ header button {
margin: 0 10px;
}
+header .replay {
+ margin: 0 10px;
+ display: flex;
+}
+
+header button.replay_button {
+ margin: 0;
+}
+
#prompt {
margin: 0 20px;
font-size: 18px;
diff --git a/public/join.js b/public/join.js
index 24e10ab..eef3833 100644
--- a/public/join.js
+++ b/public/join.js
@@ -68,11 +68,14 @@ function start_event_source() {
});
evtsrc.addEventListener("deleted", function (evt) {
console.log("DELETED");
- window.location.href = '/info/' + game.title_id;
+ window.location.href = '/' + game.title_id;
});
evtsrc.onerror = function (err) {
window.message.innerHTML = "Disconnected from server...";
};
+ window.addEventListener('beforeunload', function (evt) {
+ evtsrc.close();
+ });
}
}