Say who is playing, what you can press, and where you are in the Stage
Mostly presentation, plus one rules fix found on the way.
This commit is contained in:
+94
-7
@@ -38,6 +38,76 @@ const esc = (s: string): string =>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* THE TURN CHART — the five phases of a Stage, in order, with the current one lit.
|
||||
*
|
||||
* The title bar used to say `phase: Mainline`, which names the moment and nothing around it: a
|
||||
* player could not see what had already happened this Stage or what was coming. The printed game
|
||||
* has a turn chart for exactly this reason.
|
||||
*
|
||||
* Each phase carries a tooltip describing what actually happens in it — the first three are the
|
||||
* rules text; Cargo and Supervisor Shift are written from what the engine does, since the recovered
|
||||
* sheet does not spell them out.
|
||||
*/
|
||||
const PHASES: { key: string; label: string; tip: string; icon: string }[] = [
|
||||
{
|
||||
key: 'localOps',
|
||||
label: 'Local Operations',
|
||||
tip: 'Switch with a train, Freight Agent operations, or draw a card. One of the three, once per Stage — this is the whole of your turn.',
|
||||
// a turnout: the choice of three roads
|
||||
icon: '<path class="ic" d="M1 11h6l6-6h4M7 11h10"/>',
|
||||
},
|
||||
{
|
||||
key: 'newTrain',
|
||||
label: 'New Train',
|
||||
tip: 'Timetabled trains for this Stage are built. New timetabled trains are randomly placed on the timetable. Held trains are built. Extra trains are built.',
|
||||
// a locomotive being made up
|
||||
icon: '<rect class="ic" x="2" y="6" width="9" height="7" rx="1"/><path class="ic" d="M11 9h4v4h-4"/><circle class="icf" cx="5" cy="15" r="1.5"/><circle class="icf" cx="13" cy="15" r="1.5"/>',
|
||||
},
|
||||
{
|
||||
key: 'mainline',
|
||||
label: 'Mainline',
|
||||
tip: 'All trains holding at an Office or Division Point must attempt to move. This phase is automatic — you make no decisions in it, except a clearance ruling if one is called for.',
|
||||
// open road
|
||||
icon: '<path class="ic" d="M2 7h16M2 13h16"/><path class="ic" d="M6 7v6M12 7v6"/>',
|
||||
},
|
||||
{
|
||||
key: 'loadUnload',
|
||||
label: 'Cargo',
|
||||
tip: 'Your workers act, once each per Stage. A Laborer moves a load one square across MEN | AT | WORK, or starts one, or begins an unload. A Porter boards or detrains passengers, which earns Revenue in a single action.',
|
||||
// a loaded box on a flat
|
||||
icon: '<rect class="ic" x="3" y="7" width="8" height="6"/><path class="ic" d="M3 10h8M7 7v6"/><path class="ic" d="M13 13h4"/>',
|
||||
},
|
||||
{
|
||||
key: 'shiftChange',
|
||||
label: 'Supervisor Shift',
|
||||
tip: 'End of the Stage. Every Laborer and Porter is refreshed for the next one, and every third Stage — 3, 6, 9 and 12 — the Superintendent\'s hat passes to the next player.',
|
||||
// a pocket watch
|
||||
icon: '<circle class="ic" cx="10" cy="11" r="6"/><path class="ic" d="M10 8v3l2 2M8 2h4"/>',
|
||||
},
|
||||
];
|
||||
|
||||
function renderTurnChart(f: ReturnType<typeof view>): void {
|
||||
$('tc-day').textContent = `Day ${f.day}`;
|
||||
$('tc-stage').textContent = `Stage ${f.stage} of 12`;
|
||||
$('tc-clock').textContent = f.clock;
|
||||
$('tc-phase').textContent = f.phase;
|
||||
// WHO THE TABLE IS WAITING ON. Solitaire has one seat, but the question is the same one a
|
||||
// multiplayer table asks constantly, and an automatic phase is waiting on nobody.
|
||||
const actorName = f.actor === null ? null : (game.state.players[f.actor]?.name ?? null);
|
||||
$('tc-who').textContent = actorName ?? 'nobody — the Division is running itself';
|
||||
|
||||
const at = PHASES.findIndex((p) => p.key === f.phaseKey);
|
||||
$('tc-phases').innerHTML = PHASES.map((p, i) => {
|
||||
const state = p.key === f.phaseKey ? 'now' : i < at ? 'done' : '';
|
||||
return (
|
||||
`<li class="tc-phase ${state}" data-tip="${esc(p.label)} — ${esc(p.tip)}" tabindex="0">` +
|
||||
`<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true">${p.icon}</svg>` +
|
||||
`<span>${esc(p.label)}</span></li>`
|
||||
);
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function start(): void {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const requested = params.get('seed');
|
||||
@@ -66,8 +136,7 @@ function render(): void {
|
||||
spotsAt.set(key, [...(spotsAt.get(key) ?? []), sp]);
|
||||
}
|
||||
|
||||
$('clock').textContent = `Day ${f.day} · Stage ${f.stage} — ${f.clock}`;
|
||||
$('phase').textContent = f.phase;
|
||||
renderTurnChart(f);
|
||||
$('revenue').textContent = String(f.revenue);
|
||||
const obj = $('objective');
|
||||
obj.textContent = f.objective.note;
|
||||
@@ -150,16 +219,19 @@ function render(): void {
|
||||
|
||||
// Name AND effect. A hand of names alone tells a player nothing about what they can do.
|
||||
// Name and status stay on the page; what the card DOES is reference detail, so it hovers.
|
||||
// The crosshatch says "not yet playable"; it used to be a `not yet` caption, which had to be read
|
||||
// rather than seen. The tooltip still explains WHY, which a hatch cannot.
|
||||
const cardRow = (name: string, why: string, playable: boolean | null): string =>
|
||||
`<div class="handcard${playable === false ? ' unplayable' : ''}"${why ? ` data-tip="${esc(why)}"` : ''} tabindex="0">` +
|
||||
`<b>${esc(name)}</b>` +
|
||||
(playable === false ? ' <span class="tag">not yet</span>' : '') +
|
||||
`</div>`;
|
||||
`<b>${esc(name)}</b></div>`;
|
||||
|
||||
const canPlay = handPlayable(game);
|
||||
$('hand').innerHTML = f.hand.length
|
||||
? f.hand.map((h, i) => cardRow(h, f.handWhat[i] ?? '', canPlay[i] ?? null)).join('')
|
||||
: '<span class="dim">empty</span>';
|
||||
// Reference, not choices: the Department cards are three FACE-UP CARDS, not three decks — there
|
||||
// is nothing underneath them. Drawn like the hand so they read as cards, dashed and unlit so they
|
||||
// do not read as clickable.
|
||||
$('depts').innerHTML = f.departments.length
|
||||
? f.departments.map((d, i) => cardRow(d, f.departmentsWhat[i] ?? '', null)).join('')
|
||||
: '<span class="dim">none</span>';
|
||||
@@ -243,11 +315,26 @@ function renderActions(menu: ReturnType<typeof actionMenu>): void {
|
||||
);
|
||||
};
|
||||
|
||||
let html = menu.direct
|
||||
// A heading over the buttons, so the panel says what it is before it says what is in it. The
|
||||
// list below is already phase-specific: it comes from `legalActions`, so in the Cargo phase with
|
||||
// no worker able to act, the only thing offered is "End my Cargo phase".
|
||||
let html = '<h3 class="actions-hd">Actions</h3>';
|
||||
|
||||
html += menu.direct
|
||||
.map(
|
||||
(g) =>
|
||||
`<div class="grp"><h3>${esc(g.title)}</h3>` +
|
||||
g.actions.map((a) => actionButton(a.label, a.index)).join('') +
|
||||
g.actions
|
||||
.map((a) => {
|
||||
// §6.2 — a drawn card has to be played or discarded before the turn can end. Keyed on
|
||||
// the INTENT, not the label: matching button text would break the moment the wording
|
||||
// changed, and would have caught `switch.end` too.
|
||||
const blocked = menu.options[a.index]?.type === 'draw.end' && game.mustPlayCard;
|
||||
return blocked
|
||||
? `<button class="act blocked" disabled data-tip="You have drawn a card. Play it or discard it to a Department slot before ending your turn — §6.2 holds you to three cards.">${esc(a.label)} — play or discard first</button>`
|
||||
: actionButton(a.label, a.index);
|
||||
})
|
||||
.join('') +
|
||||
`</div>`,
|
||||
)
|
||||
.join('');
|
||||
|
||||
Reference in New Issue
Block a user