adding classification and division yard updates, train collisions, engine and car updates, turnout improvements.
This commit is contained in:
+51
-2
@@ -12,6 +12,8 @@
|
||||
*/
|
||||
|
||||
import { BOARD_CSS, divisionSvg, officeSvg } from '../sim/board-svg.ts';
|
||||
import { cuesFor } from '../sim/narrate.ts';
|
||||
import { playCue } from './sound.ts';
|
||||
import { TOOLTIP_CSS, installTooltips } from './tooltip.ts';
|
||||
import { narrate } from '../sim/narrate.ts';
|
||||
import { cardName, trainName } from '../sim/view.ts';
|
||||
@@ -35,7 +37,12 @@ const esc = (s: string): string =>
|
||||
String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] ?? c);
|
||||
|
||||
/** Every position the save passes through, with the lines narrated along the way. */
|
||||
type Step = { frame: Frame; lines: { text: string; tone: string }[] };
|
||||
type Step = {
|
||||
frame: Frame;
|
||||
lines: { text: string; tone: string }[];
|
||||
/** What this step should sound like — from `cuesFor`, the same function the live game uses. */
|
||||
cues: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Rebuild a game from a save, keeping a snapshot at each step.
|
||||
@@ -54,7 +61,7 @@ function rebuild(save: Save): { steps: Step[]; stoppedEarly: boolean } {
|
||||
const n = narrate(e, ctx);
|
||||
return { text: n.text, tone: n.tone };
|
||||
});
|
||||
steps.push({ frame: snapshot(s, [], null), lines });
|
||||
steps.push({ frame: snapshot(s, [], null), lines, cues: cuesFor(events) });
|
||||
};
|
||||
|
||||
push(pump(s));
|
||||
@@ -80,13 +87,28 @@ function rebuild(save: Save): { steps: Step[]; stoppedEarly: boolean } {
|
||||
let steps: Step[] = [];
|
||||
let at = 0;
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
/**
|
||||
* Sound and auto-hide, matching the playable page and the standalone replay.
|
||||
*
|
||||
* This viewer is a third place a game gets drawn, and it had drifted: three speeds where the
|
||||
* standalone had five, and neither of these. Anything a player learns on one screen should hold on
|
||||
* the others.
|
||||
*/
|
||||
let soundOn = false;
|
||||
let districtMode: 'auto' | 'open' | 'closed' = 'auto';
|
||||
const FOCUS_PHASES = new Set(['localOps', 'loadUnload']);
|
||||
|
||||
function show(i: number): void {
|
||||
const from = at;
|
||||
at = Math.max(0, Math.min(steps.length - 1, i));
|
||||
const step = steps[at];
|
||||
if (!step) return;
|
||||
const f = step.frame;
|
||||
|
||||
// Only a single step FORWARD sounds. Scrubbing across a hundred steps would fire a hundred
|
||||
// whistles, and stepping back would sound a Stage ending that is being un-done.
|
||||
if (soundOn && at === from + 1) for (const c of step.cues) playCue(c);
|
||||
|
||||
$('vclock').textContent = `Day ${f.day} · Stage ${f.stage} — ${f.clock}`;
|
||||
$('vphase').textContent = f.phase;
|
||||
$('vrev').textContent = String(f.revenue);
|
||||
@@ -95,6 +117,21 @@ function show(i: number): void {
|
||||
$('vdivision').innerHTML = divisionSvg(f.division);
|
||||
$('vgrid').innerHTML = officeSvg(f.cells, f.runningRow);
|
||||
|
||||
// Auto-hide: the district is worth its space during the phases that change it.
|
||||
const open = districtMode === 'auto' ? FOCUS_PHASES.has(f.phaseKey) : districtMode === 'open';
|
||||
const sec = $('vdistrict');
|
||||
if (open) sec.classList.remove('folded');
|
||||
else sec.classList.add('folded');
|
||||
const cars = f.cells.reduce((n, c) => n + c.cars.length, 0);
|
||||
$('vdistrictsummary').textContent =
|
||||
`${f.cells.length} cards · ${f.facilities.length} facilities · ${cars} cars standing`;
|
||||
$('vdistricttoggle').textContent =
|
||||
districtMode === 'auto'
|
||||
? (open ? 'auto-hide: on — click to keep open' : 'auto-hide: on — click to show')
|
||||
: districtMode === 'open'
|
||||
? 'always showing — click for auto-hide'
|
||||
: 'always hidden — click for auto-hide';
|
||||
|
||||
// A window of history rather than only this step, so a jump lands in context.
|
||||
let log = '';
|
||||
for (let k = Math.max(0, at - 18); k <= at; k++) {
|
||||
@@ -215,6 +252,18 @@ function wire(): void {
|
||||
show(at + 1);
|
||||
}, Number(($('vspeed') as HTMLSelectElement).value));
|
||||
};
|
||||
$('vdistricttoggle').onclick = () => {
|
||||
const f = steps[at]?.frame;
|
||||
const open = districtMode === 'auto' ? (f ? FOCUS_PHASES.has(f.phaseKey) : true) : districtMode === 'open';
|
||||
districtMode = districtMode === 'auto' ? (open ? 'closed' : 'open') : 'auto';
|
||||
show(at);
|
||||
};
|
||||
$('vsound').onclick = () => {
|
||||
soundOn = !soundOn;
|
||||
$('vsound').textContent = soundOn ? '\u{1F50A} sound' : '\u{1F507} muted';
|
||||
// Confirms the change, and gives the browser the gesture it needs before audio may start.
|
||||
if (soundOn) playCue('stage');
|
||||
};
|
||||
$('vclose').onclick = () => {
|
||||
if (timer) clearInterval(timer);
|
||||
timer = null;
|
||||
|
||||
Reference in New Issue
Block a user