(() => {
let etag = "";
const path_match = window.location.pathname.match(
/^\/g\/([0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/);
const game_id = path_match ? path_match[1] : "";
const state_url = game_id
? `/api/games/${game_id}/state` : "";
const lifecycle = document.querySelector("#lifecycle");
const map = document.querySelector("#map");
const status = document.querySelector("#status");
const pending = document.querySelector("#pending");
const messages = document.querySelector("#messages");
const STATUS_ORDER = [
["title", "Character"],
["dungeon_level", "Location"],
["hp", "HP", "max_hp"],
["power", "Power", "max_power"],
["armor_class", "Armor class"],
["experience_level", "Level"],
["experience", "Experience"],
["gold", "Gold"],
["time", "Turn"],
["alignment", "Alignment"],
["hunger", "Hunger"],
["carrying_capacity", "Encumbrance"],
["strength", "Strength"],
["dexterity", "Dexterity"],
["constitution", "Constitution"],
["intelligence", "Intelligence"],
["wisdom", "Wisdom"],
["charisma", "Charisma"],
["score", "Score"],
["weapon", "Weapon"],
["armor", "Armor"],
["terrain", "Terrain"],
["HD", "Hit dice"],
["version", "Version"],
];
function displayText(field) {
if(field && typeof field === "object")
{
return typeof field.text === "string" ? field.text : "";
}
return typeof field === "string" ? field : "";
}
function appendStatusField(label, value) {
const item = document.createElement("div");
item.className = "status-field";
const name = document.createElement("dt");
name.textContent = label;
const text = document.createElement("dd");
text.textContent = value;
item.append(name, text);
status.append(item);
}
function renderStatus(fields) {
status.replaceChildren();
const values = fields && typeof fields === "object" ? fields : {};
const displayed = new Set();
for(const [key, label, maximum] of STATUS_ORDER)
{
if(!Object.hasOwn(values, key)) continue;
const current = displayText(values[key]);
if(current === "") continue;
let text = current;
if(maximum && Object.hasOwn(values, maximum))
{
const maximum_text = displayText(values[maximum]);
if(maximum_text !== "") text += ` / ${maximum_text}`;
displayed.add(maximum);
}
appendStatusField(label, text);
displayed.add(key);
}
const condition = values.condition;
if(condition && Array.isArray(condition.active))
{
appendStatusField("Conditions",
condition.active.length ? condition.active.join(", ") : "None");
displayed.add("condition");
}
for(const [key, field] of Object.entries(values))
{
if(displayed.has(key)) continue;
const text = displayText(field);
if(text === "") continue;
const label = key.replaceAll("_", " ");
appendStatusField(label[0].toUpperCase() + label.slice(1), text);
}
if(!status.children.length)
{
const empty = document.createElement("p");
empty.className = "status-empty";
empty.textContent = "No status available yet.";
status.append(empty);
}
}
function show(value) {
lifecycle.textContent = value.lifecycle || "unknown";
map.textContent = (value.map && value.map.rows || []).join("\n");
renderStatus(value.status);
pending.textContent = value.pending
? JSON.stringify(value.pending, null, 2) : "None";
messages.replaceChildren();
for(const message of value.messages || [])
{
const item = document.createElement("li");
item.textContent = message.text || "";
messages.append(item);
}
}
async function poll() {
let retry_delay = 2000;
if(!game_id)
{
lifecycle.textContent = "Invalid game URL";
return;
}
try
{
const headers = etag ? {"If-None-Match": etag} : {};
const response = await fetch(state_url, {
headers, cache: "no-store",
});
if(response.status === 410)
{
window.location.replace("/");
return;
}
if(response.status === 404)
{
lifecycle.textContent = "Game not found";
return;
}
if(response.status !== 304)
{
if(!response.ok) throw new Error(`HTTP ${response.status}`);
etag = response.headers.get("ETag") || "";
show(await response.json());
}
}
catch(error)
{
lifecycle.textContent = `Disconnected: ${error.message}`;
etag = "";
retry_delay = 5000;
}
setTimeout(poll, retry_delay);
}
poll();
})();