contradiction Two things found by playing v0.8.0.1, neither in the mechanism itself. ?pace= never worked. index.html's doors are play.html?lobby and play.html?solitaire, so arriving through the splash replaces the query string and the play page only ever saw ?lobby — a whole game was played at 1x while believing it was at 7x. v0.8.0 shipped that parameter as the only way to change speed and the game's own front door destroyed it. There is a control on the play screen now, beside zoom, persisted per viewer; the doors carry pace through as well, so the URL lever is honest for handing two playtesters different speeds. PACE_LEVELS moved to sim/pacing.ts with DWELL and MAX_PACE — the tuning surface in one file, and testable. The committed default is unchanged: what it should be is a question for a game played at a speed that took effect. And the Day-end dialog said "0 today, 2 in all". advance.ts increments the Day and then zeroes collisionsToday, and noteDayEnd() fires when the Day goes up — so the dialog reporting the Day that just finished was drawn from the very frame in which that Day's count was reset. Reproduced on four of five seeds before changing anything. The count is captured at the rollover now; it is not derivable on the client, because in multiplayer the push announcing the new Day is the same push that carries the reset. And "today" was the wrong word regardless: it names the Day instead — "Collisions: 2 on Day 1, 2 in all". Unrelated to v0.8.0 — that one has been wrong since the dialog was built for Gitea#10, and needed somebody to play a Day with a collision in it and then read the summary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6cF1iYvJ1kNmzYBzu4QX6
95 lines
4.2 KiB
TypeScript
95 lines
4.2 KiB
TypeScript
/** 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 += `<line class="tie" x1="${x}" y1="6" x2="${x}" y2="19"/>`;
|
||
}
|
||
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<unknown>) : 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());
|
||
}
|