// Undercooked — client shell entry point (phase 01). // // There is no game logic here on purpose. Phase 05 shipped the server-side // room shell (supervisor, room FSM, session store); phase 06 adds the WebSocket // transport and phase 07 replaces the placeholder button handlers with the lobby // flow and adds render/ (Canvas 2D). What this file owns today: // * the anonymous per-tab session token (PLAN.md A16), // * the last room code typed in this tab, so a reload can rejoin fast, // * mounting the lobby screen and announcing state into #status. // It is a plain ES module served from /static/ — no bundler, no framework, no // remote import (PLAN.md A10/A12). const TOKEN_KEY = "uc.token"; const LAST_CODE_KEY = "uc.lastCode"; const CODE_LENGTH = 5; const NOT_IMPLEMENTED = "Not implemented until phase 07."; const READY_MESSAGE = "Pick a kitchen to start."; /** Base64url without padding, so the token is safe in a query string or header. */ function base64url(bytes) { let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } /** A random, unguessable id for this browser tab (identity, not an account). */ export function randomToken() { const bytes = new Uint8Array(24); globalThis.crypto.getRandomValues(bytes); return base64url(bytes); } function read(storage, key) { try { return storage.getItem(key); } catch { return null; // private browsing / storage disabled: stay functional for this tab only } } function write(storage, key, value) { try { storage.setItem(key, value); } catch { /* nothing to persist to; the in-memory value still works */ } } /** The tab's session token, created once per tab and kept in sessionStorage. */ export function getToken(storage = globalThis.sessionStorage) { const existing = read(storage, TOKEN_KEY); if (typeof existing === "string" && existing.length > 0) return existing; const token = randomToken(); write(storage, TOKEN_KEY, token); return token; } /** Uppercase what the user typed and keep only characters a code can contain. */ export function normalizeCode(value) { return String(value ?? "") .toUpperCase() .replace(/[^0-9A-Z]/g, "") .slice(0, CODE_LENGTH); } export function lastCode(storage = globalThis.sessionStorage) { return read(storage, LAST_CODE_KEY) || ""; } export function rememberCode(code, storage = globalThis.sessionStorage) { write(storage, LAST_CODE_KEY, normalizeCode(code)); } /** Write the live region. `kind` is "info" | "error" and only changes styling. */ export function announce(message, root = document, kind = "info") { const region = root.getElementById("status"); if (!region) return; region.textContent = message; region.dataset.kind = kind; } /** * Wire the lobby screen. Returns the tab identity so later phases can reuse it. */ export function mount(root = document) { const token = getToken(); const createButton = root.getElementById("create-lobby"); const joinForm = root.getElementById("join-form"); const joinInput = root.getElementById("join-code"); const remembered = lastCode(); if (joinInput && remembered) joinInput.value = remembered; if (createButton) { createButton.addEventListener("click", () => { // Phase 07 (on the phase-06 transport): POST to the room supervisor, // open /ws/{code}. announce(NOT_IMPLEMENTED, root); }); } if (joinForm) { joinForm.addEventListener("submit", (event) => { event.preventDefault(); const code = normalizeCode(joinInput ? joinInput.value : ""); if (joinInput) joinInput.value = code; if (code.length !== CODE_LENGTH) { announce(`A room code is ${CODE_LENGTH} characters.`, root, "error"); if (joinInput) joinInput.focus(); return; } rememberCode(code); // Phase 07 (on the phase-06 transport): join the room over the // WebSocket. announce(NOT_IMPLEMENTED, root); }); } if (!lastCode() && root.getElementById("status")) { announce(READY_MESSAGE, root); } return { token, code: lastCode() }; } // Auto-mount when loaded as the page's module, but stay importable (and // therefore testable) without a lobby in the document. if (typeof document !== "undefined" && document.getElementById("screen-lobby")) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => mount(), { once: true }); } else { mount(); } }