v0.8.0.2 — the speed control that was only ever a URL parameter, and a Day-end
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
This commit is contained in:
co-authored by
Claude Opus 5
parent
0cfeb4c496
commit
c10f52791e
@@ -26,6 +26,7 @@ import { createLocalSession, createRemoteSession } from './session.ts';
|
||||
import type { PlayerIndex } from '../engine/state.ts';
|
||||
import type { PublicDistrict } from '../sim/view.ts';
|
||||
import { createStepQueue } from './step-queue.ts';
|
||||
import { PACE_LEVELS } from '../sim/pacing.ts';
|
||||
import { notice, prefillCode, runLobby } from './lobby.ts';
|
||||
import type { LobbyReady } from './lobby.ts';
|
||||
import {
|
||||
@@ -53,6 +54,7 @@ const REMOTE_KEY = 'station-master.remote.v1';
|
||||
/** Preset board zoom levels — a fraction applied to the rendered SVG's own pixel dimensions. */
|
||||
const ZOOM_LEVELS = [0.75, 1, 1.25, 1.5] as const;
|
||||
|
||||
|
||||
/**
|
||||
* Small persisted preferences, kept in a `localStorage` key of their own — separate from
|
||||
* `SAVE_KEY`. A save is the seed plus the intents and has to stay portable; none of this belongs
|
||||
@@ -2603,6 +2605,52 @@ function runSolitaireSetup(params: URLSearchParams, hasSave = false, live: Frame
|
||||
dealBtn.onclick = () => commitNewGame(ss, seedField?.value ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* PLAYBACK SPEED — v0.8.0.3, TODO #13.
|
||||
*
|
||||
* Persisted per viewer in `Settings`, so it survives the navigation that was eating `?pace=`. The
|
||||
* queue reads `settings.pace` through a closure on every step, so a change here takes effect on the
|
||||
* very next move rather than the next game.
|
||||
*/
|
||||
const paceSlowerBtn = document.getElementById('paceslower') as HTMLButtonElement | null;
|
||||
const paceFasterBtn = document.getElementById('pacefaster') as HTMLButtonElement | null;
|
||||
const paceLabel = document.getElementById('pacelabel');
|
||||
if (paceSlowerBtn && paceFasterBtn && paceLabel) {
|
||||
const nearestPace = (): number => {
|
||||
// A saved or URL value need not be on the ladder — `?pace=7` and a hand-edited setting are both
|
||||
// legitimate — so the buttons step from whichever preset is closest rather than refusing to move.
|
||||
const want = PACE_OVERRIDE ?? settings.pace;
|
||||
return PACE_LEVELS.reduce((best, p) => (Math.abs(p - want) < Math.abs(best - want) ? p : best), PACE_LEVELS[0]);
|
||||
};
|
||||
const paintPace = (): void => {
|
||||
const p = PACE_OVERRIDE ?? settings.pace;
|
||||
paceLabel.textContent = p === 0 ? 'off' : `${p}×`;
|
||||
const i = PACE_LEVELS.indexOf(nearestPace() as (typeof PACE_LEVELS)[number]);
|
||||
paceSlowerBtn.disabled = i >= PACE_LEVELS.length - 1;
|
||||
paceFasterBtn.disabled = i <= 0;
|
||||
// A `?pace=` in the URL wins over the setting, so say so rather than showing dead buttons.
|
||||
if (PACE_OVERRIDE !== null) {
|
||||
paceSlowerBtn.disabled = true;
|
||||
paceFasterBtn.disabled = true;
|
||||
paceLabel.textContent = `${PACE_OVERRIDE}× (URL)`;
|
||||
}
|
||||
};
|
||||
const stepPace = (by: number): void => {
|
||||
const i = PACE_LEVELS.indexOf(nearestPace() as (typeof PACE_LEVELS)[number]);
|
||||
const next = PACE_LEVELS[Math.min(PACE_LEVELS.length - 1, Math.max(0, i + by))];
|
||||
if (next === undefined) return;
|
||||
saveSettings({ pace: next });
|
||||
paintPace();
|
||||
// The row's countdown is measured in steps that will dwell, so a change to 0 empties it at once.
|
||||
renderWatching();
|
||||
};
|
||||
// Slower is a BIGGER multiplier, so "−" walks up the ladder. Labelled by what it does to the game,
|
||||
// not to the number: a player pressing "slower" wants to watch for longer.
|
||||
paceSlowerBtn.onclick = () => stepPace(1);
|
||||
paceFasterBtn.onclick = () => stepPace(-1);
|
||||
paintPace();
|
||||
}
|
||||
|
||||
const zoomOutBtn = document.getElementById('zoomout') as HTMLButtonElement | null;
|
||||
const zoomInBtn = document.getElementById('zoomin') as HTMLButtonElement | null;
|
||||
const zoomLabel = document.getElementById('zoomlabel');
|
||||
|
||||
Reference in New Issue
Block a user