/** The splash page. Draws ties on the rule so the line reads as track rather than a border. */ const ties = document.getElementById('ties'); if (ties) { let out = ''; for (let x = 4; x < 700; x += 9) { out += ``; } ties.innerHTML = out; } /** Clicking the box art opens it full size, since the thumbnail is too small to read. */ const heroImage = document.getElementById('hero-image'); const lightbox = document.getElementById('lightbox'); if (heroImage && lightbox) { const open = () => { lightbox.hidden = false; }; const close = () => { lightbox.hidden = true; }; heroImage.addEventListener('click', open); heroImage.addEventListener('keydown', (e) => { if (e instanceof KeyboardEvent && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); open(); } }); lightbox.addEventListener('click', close); document.addEventListener('keydown', (e) => { if (e instanceof KeyboardEvent && e.key === 'Escape') close(); }); } /** * Is a Station Master server actually behind this page? * * The same `dist/` ships two ways — served by `src/server/`, or uploaded as flat files with no * server at all (`scripts/deploy-web.ts`) — and the bundle is identical in both, so the only * honest way to answer is to ask. `/api/health` is the one route that exists to be failed. * * THE DOOR STARTS OPEN AND ONLY EVER CLOSES. Getting this wrong in the "no server" direction is * the bug this whole change exists to fix: a disabled door is invisible and leaves a player who * DOES have a server with no way in, and nothing on screen to explain it. Getting it wrong the * other way costs a click and a lobby that says it cannot reach a server — which is legible, and * recoverable. So a slow or flaky probe leaves the door alone; only a definite answer closes it. */ /** * CARRY `?pace=` THROUGH THE DOORS — v0.8.0.3. * * Both doors are static hrefs that REPLACE the query string (`play.html?lobby`, * `play.html?solitaire`), so a `pace` typed on this page was silently dropped on the way in: Jesse * played a whole game believing he was at 7× when the play page had only ever seen `?lobby`. The * durable answer is the speed control on the play screen, which persists per viewer — this keeps the * URL lever honest for handing two playtesters different speeds, which is the only thing it was ever * for. */ try { const pace = new URLSearchParams(location.search).get('pace'); if (pace !== null) { for (const door of Array.from(document.querySelectorAll('a.door'))) { const href = door.getAttribute('href'); // Only the doors into the game, and only ones that have not been disabled above. if (href?.startsWith('./play.html?')) door.setAttribute('href', `${href}&pace=${encodeURIComponent(pace)}`); } } } catch { // A door that keeps its own href is the status quo, not a broken page. } const mpDoor = document.getElementById('door-multiplayer'); if (mpDoor) { const close = (): void => { mpDoor.classList.add('disabled'); mpDoor.removeAttribute('href'); const go = document.getElementById('door-multiplayer-go'); if (go) go.textContent = 'Requires a Station Master server'; const blurb = document.getElementById('door-multiplayer-blurb'); if (blurb) { blurb.textContent = 'Play with your friends across the internet, each running your own office area within the ' + 'entire division. This copy of the game is a plain website with no server behind it, so ' + 'there is nowhere to host a table — multiplayer needs a Station Master server.'; } }; // Relative, never absolute: the page must work at whatever address it is reached by, and the // server is always the origin that served it (D16). void fetch('./api/health') .then((r) => (r.ok ? (r.json() as Promise) : null)) .then((body) => { // A static host that answers unknown paths with 200 and its own index page would sail past // an `r.ok` check, so the body has to name itself before the door is believed. const named = typeof body === 'object' && body !== null && (body as { service?: unknown }).service === 'station-master'; if (!named) close(); }) .catch(() => close()); }