More bug fixes. Show the timetable, and stop the arrival message contradicting itself. Fixed division point in labels. And highlighted discard as targets.
This commit is contained in:
+12
-1
@@ -91,6 +91,14 @@ export type Game = {
|
||||
* would be guesswork from a rendered frame.
|
||||
*/
|
||||
cues: string[];
|
||||
/**
|
||||
* The timetable slot the last batch of events filled, or null.
|
||||
*
|
||||
* Playing a train card rolls 1D12 for a Stage and the answer landed nowhere the player could see.
|
||||
* Carrying the slot lets the timetable flash the one that just changed — the roll becomes
|
||||
* something you watch land rather than something you are told about afterwards.
|
||||
*/
|
||||
scheduled: number | null;
|
||||
};
|
||||
|
||||
/** How each intent kind is introduced in the action list, in the order they should appear. */
|
||||
@@ -125,7 +133,7 @@ export const SOLO_PLAYER = 'Solitaire';
|
||||
|
||||
export function newGame(seed: number, config: GameConfig = SOLO_CONFIG): Game {
|
||||
const state = createGame({ id: `web-${seed}`, seed, config, playerNames: [SOLO_PLAYER] });
|
||||
const game: Game = { state, seed, history: [], log: [], mustPlayCard: false, cues: [] };
|
||||
const game: Game = { state, seed, history: [], log: [], mustPlayCard: false, cues: [], scheduled: null };
|
||||
// A history that opens mid-Stage reads as though something was missed. Say what the game IS
|
||||
// first, then let the clock take over.
|
||||
game.log.push({ text: 'Game Begins', tone: 'start' });
|
||||
@@ -603,6 +611,9 @@ function record(game: Game, events: GameEvent[], actor: PlayerIndex | null = nul
|
||||
|
||||
}
|
||||
game.cues.push(...cuesFor(events));
|
||||
// Which timetable slot the die just filled, so the panel can flash it. Last one wins: a batch can
|
||||
// schedule more than one train, and the most recent is the one the eye should be sent to.
|
||||
for (const e of events) if (e.type === 'trainScheduled') game.scheduled = e.slot;
|
||||
// Keep the log bounded; the full history lives in `history` and can be replayed.
|
||||
if (game.log.length > 400) game.log.splice(0, game.log.length - 400);
|
||||
}
|
||||
|
||||
+38
-4
@@ -7,7 +7,7 @@
|
||||
|
||||
import { BOARD_CSS, divisionSvg, officeSvg } from '../sim/board-svg.ts';
|
||||
import { TURNCHART_CSS, turnChartHtml } from '../sim/turnchart.ts';
|
||||
import { PANEL_CSS, blockedHtml, facilitiesHtml, pilesHtml, yardHtml } from './panels.ts';
|
||||
import { PANEL_CSS, blockedHtml, facilitiesHtml, pilesHtml, timetableHtml, yardHtml } from './panels.ts';
|
||||
import { TOOLTIP_CSS, installTooltips } from './tooltip.ts';
|
||||
import { playCue } from './sound.ts';
|
||||
import type { Game } from './game.ts';
|
||||
@@ -293,6 +293,9 @@ function render(): void {
|
||||
? menu.hand.find((h) => `hand:${h.cardId}` === selected)
|
||||
: undefined;
|
||||
if (discarding) {
|
||||
// Dim the piles that are not targets, so the three that are read as a choice being offered
|
||||
// rather than as four cards that happen to be there.
|
||||
$('depts').classList.add('aiming');
|
||||
for (const el of Array.from($('depts').querySelectorAll('[data-dept]'))) {
|
||||
const node = el as HTMLElement;
|
||||
const slot = Number(node.dataset['dept']);
|
||||
@@ -309,6 +312,10 @@ function render(): void {
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
$('depts').classList.remove('aiming');
|
||||
}
|
||||
|
||||
// -- making up a train: the Division Yard chip that shows the car IS the button.
|
||||
if (menu.makeUp) {
|
||||
for (const el of Array.from($('divyard').querySelectorAll('[data-car]'))) {
|
||||
@@ -326,6 +333,16 @@ function render(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* THE TIMETABLE, with the slot the die just filled flashed.
|
||||
*
|
||||
* `scheduled` is cleared as it is consumed, so the flash marks the moment rather than the state —
|
||||
* it is gone by the next render, which is what makes it read as "that just happened".
|
||||
*/
|
||||
const justSet = game.scheduled;
|
||||
game.scheduled = null;
|
||||
$('timetable').innerHTML = timetableHtml(f, justSet);
|
||||
|
||||
$('blocked').innerHTML = blockedHtml(f);
|
||||
|
||||
// -- log
|
||||
@@ -354,7 +371,7 @@ function render(): void {
|
||||
lastPhase = f.phaseKey;
|
||||
|
||||
renderDistrict(f);
|
||||
renderActions(menu, f);
|
||||
renderActions(menu, f, justSet);
|
||||
|
||||
// Drain whatever the last batch of events earned. Cleared either way, so turning sound on does
|
||||
// not then play a backlog of everything that happened while it was off.
|
||||
@@ -413,7 +430,11 @@ function renderDistrict(f: ReturnType<typeof view>): void {
|
||||
};
|
||||
}
|
||||
|
||||
function renderActions(menu: ReturnType<typeof actionMenu>, f: ReturnType<typeof view>): void {
|
||||
function renderActions(
|
||||
menu: ReturnType<typeof actionMenu>,
|
||||
f: ReturnType<typeof view>,
|
||||
justSet: number | null,
|
||||
): void {
|
||||
const el = $('actions');
|
||||
|
||||
if (game.state.status !== 'active') {
|
||||
@@ -474,6 +495,19 @@ function renderActions(menu: ReturnType<typeof actionMenu>, f: ReturnType<typeof
|
||||
* the count belongs beside the buttons. It was reported only in the history panel, which is the
|
||||
* one place a player is not looking while switching.
|
||||
*/
|
||||
/**
|
||||
* WHAT THE DIE DID, said where the player is looking.
|
||||
*
|
||||
* Playing a train card rolls 1D12 for its departure Stage, and the card simply left the hand — the
|
||||
* answer arrived only in the history panel, one line among many. The timetable now shows it and
|
||||
* flashes the slot; this says it in words at the same moment.
|
||||
*/
|
||||
const scheduledNote =
|
||||
justSet !== null && f.timetable[justSet] != null
|
||||
? `<div class="scheduled" data-tip="§7 — the Stage is rolled on 1D12 when the card is played; if that Stage is taken the train works down the column to the next free one. From now on it runs at this time every Day.">` +
|
||||
`Train ${f.timetable[justSet]} is scheduled to depart at Stage ${justSet + 1} — see the Timetable</div>`
|
||||
: '';
|
||||
|
||||
const movesNote =
|
||||
f.movesLeft !== null
|
||||
? `<div class="moves${f.movesLeft === 0 ? ' spent' : ''}" data-tip="§6.1 — six Moves a turn. A Move runs any distance in one direction; changing direction costs another, which is why a run-around has to be planned inside the count.">` +
|
||||
@@ -483,7 +517,7 @@ function renderActions(menu: ReturnType<typeof actionMenu>, f: ReturnType<typeof
|
||||
// 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>${movesNote}`;
|
||||
let html = `<h3 class="actions-hd">Actions</h3>${scheduledNote}${movesNote}`;
|
||||
|
||||
/**
|
||||
* WHAT IS LEFT IN THE ACTION LIST.
|
||||
|
||||
+86
-11
@@ -97,6 +97,43 @@ export function yardHtml(rows: { type: string; loaded: number; empty: number }[]
|
||||
.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* THE TIMETABLE: twelve Stages of a Day, and which train is due out at each.
|
||||
*
|
||||
* Playing a train card rolls 1D12 for a slot, and the roll landed nowhere a player could see — the
|
||||
* card was gone and the answer to "so when does it run?" was one line of history. The Frame has
|
||||
* carried the twelve slots all along and only the standalone replay ever drew them.
|
||||
*
|
||||
* `justSet` is the slot filled by the roll being reported this frame, so it can be flashed. It marks
|
||||
* the moment rather than the state: the highlight is gone by the next render, which is what makes it
|
||||
* read as "that just happened" rather than "this is special".
|
||||
*/
|
||||
export function timetableHtml(f: Frame, justSet: number | null): string {
|
||||
const slots = f.timetable
|
||||
.map((t, k) => {
|
||||
const stage = k + 1;
|
||||
const cls = [
|
||||
t !== null ? 'due' : '',
|
||||
stage === f.stage ? 'now' : '',
|
||||
stage < f.stage ? 'past' : '',
|
||||
k === justSet ? 'fresh' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const tip =
|
||||
t !== null
|
||||
? `Train ${t} departs at Stage ${stage}. A train's NUMBER is its seniority and direction — odd runs west, even runs east — and the Stage it leaves is set by the 1D12 roll made when its card was played, so Train 8 at Stage 6 is normal.`
|
||||
: `Stage ${stage} — no train due out.`;
|
||||
return (
|
||||
`<div class="tt-slot ${cls}" data-tip="${esc(tip)}" tabindex="0">` +
|
||||
`<span class="tt-s">${stage}</span>` +
|
||||
`<span class="tt-t">${t !== null ? `T${t}` : '·'}</span></div>`
|
||||
);
|
||||
})
|
||||
.join('');
|
||||
return `<div class="tt">${slots}</div>`;
|
||||
}
|
||||
|
||||
export function blockedHtml(f: Frame): string {
|
||||
return f.blocked.length === 0
|
||||
? '<li class="dim">nothing blocked</li>'
|
||||
@@ -147,18 +184,28 @@ export function facilitiesHtml(f: Frame): string {
|
||||
*/
|
||||
`<div class="caps">` +
|
||||
stat('laborers', x.laborers, x.base.laborers, 'Actions this facility can take each Stage — one Laborer moves one load one square.') +
|
||||
stat('out', String(x.greenCap), x.base.out, 'Green boxes: loads waiting to be worked onto a car.') +
|
||||
stat('in', String(x.redCap), x.base.in, 'Red boxes: loads that have come off an arriving car and are waiting to be cleared.') +
|
||||
(x.allowsOut ? stat('out', String(x.greenCap), x.base.out, 'Green boxes: loads waiting to be worked onto a car.') : '') +
|
||||
(x.allowsIn ? stat('in', String(x.redCap), x.base.in, 'Red boxes: loads cleared off an arriving car.') : '') +
|
||||
(x.modifiers.length
|
||||
? `<span class="mods" data-tip="Modifier cards standing beside this industry, each raising one of the numbers above.">+ ${esc(x.modifiers.join(', '))}</span>`
|
||||
: '') +
|
||||
`</div>` +
|
||||
`<div class="boxes"><span class="dim">green</span>${boxes(x.green, x.greenCap)}</div>` +
|
||||
`<div class="boxes"><span class="dim">MEN AT WORK</span>` +
|
||||
/**
|
||||
* IN THE ORDER THE LOAD TRAVELS. §9.3 loads Green → MEN → AT → WORK → the spotted car,
|
||||
* and unloads the other way: car → WORK → AT → MEN → red. Labelling the rows by colour
|
||||
* alone left a player to work out which direction their industry ran; naming the step
|
||||
* says it outright, and an industry that only receives has no green row to puzzle over.
|
||||
*/
|
||||
(x.allowsOut
|
||||
? `<div class="boxes"><span class="dim" data-tip="Loads waiting to be worked out onto a car. They travel green → MEN → AT → WORK, then onto a spotted empty car.">waiting to load</span>${boxes(x.green, x.greenCap)}</div>`
|
||||
: '') +
|
||||
`<div class="boxes"><span class="dim" data-tip="One physical sign, worked in whichever direction this industry runs. Only one load may sit on each of the three boxes.">MEN AT WORK</span>` +
|
||||
x.maw.map((m) => `<span class="box ${m ? 'm' : 'empty'}">${m ? esc(m) : '·'}</span>`).join('') +
|
||||
`</div>` +
|
||||
`<div class="boxes"><span class="dim">red</span>${boxes(x.red, x.redCap)}</div>` +
|
||||
`<div class="boxes"><span class="dim">siding</span>${boxes(x.track, x.trackCap)}</div>` +
|
||||
(x.allowsIn
|
||||
? `<div class="boxes"><span class="dim" data-tip="Loads that have come off an arriving car and been cleared. They travel car → WORK → AT → MEN, then into a red box — the opposite direction to loading.">cleared inbound</span>${boxes(x.red, x.redCap)}</div>`
|
||||
: '') +
|
||||
`<div class="boxes"><span class="dim" data-tip="Cars spotted on this industry's track. A load leaves the sign onto one of these, or an arriving load starts from one.">siding</span>${boxes(x.track, x.trackCap)}</div>` +
|
||||
`<div class="fstat ${x.jammed ? 'bad' : x.canFinish ? 'good' : 'idle'}" data-tip="${
|
||||
x.jammed
|
||||
? 'A load is sitting on MEN|AT|WORK with no spotted car to receive it. That locks the industry track, which blocks the very car that would clear it (§9.3).'
|
||||
@@ -196,14 +243,42 @@ h3{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:#8b94a3;ma
|
||||
.stock .ld{color:#8fd6a0}
|
||||
.stock .mt{color:#9fb6d8}
|
||||
.stock.none{opacity:.4}
|
||||
/* A pile or a yard chip that is currently a target for the thing you picked. Blue, matching the
|
||||
legal-square outline on the board: the same question is being asked, so it looks the same. */
|
||||
.handcard.target{border-color:#5aa9e6;background:#233246;cursor:pointer;
|
||||
box-shadow:0 0 0 2px rgba(90,169,230,.20)}
|
||||
.handcard.target:hover{background:#2b3d55}
|
||||
/* A PILE THAT IS WAITING TO BE PICKED, drawn like the board's ghost squares because it is the same
|
||||
question: you have chosen a card, now choose where it goes. It was a 2px outline on a small tile
|
||||
in a panel the player might not be looking at, and was reported as "a very tiny highlight". Now it
|
||||
grows, brightens, says DISCARD HERE outright, and pulses once so the eye finds it. */
|
||||
.handcard.target{border:2px dashed #5aa9e6;background:#233246;cursor:pointer;color:#e6e9ee;
|
||||
box-shadow:0 0 0 4px rgba(90,169,230,.22);transform:translateY(-2px);
|
||||
animation:pick 1.1s ease-out 2}
|
||||
.handcard.target:hover{background:#31485f;border-style:solid;box-shadow:0 0 0 6px rgba(90,169,230,.30)}
|
||||
.handcard.target::before{content:"DISCARD HERE";display:block;font-size:9px;letter-spacing:.09em;
|
||||
color:#8fc4ee;font-weight:700;margin-bottom:2px}
|
||||
@keyframes pick{0%{box-shadow:0 0 0 4px rgba(90,169,230,.22)}
|
||||
50%{box-shadow:0 0 0 9px rgba(90,169,230,.10)}
|
||||
100%{box-shadow:0 0 0 4px rgba(90,169,230,.22)}}
|
||||
@media(prefers-reduced-motion:reduce){.handcard.target{animation:none}}
|
||||
/* And the piles that are NOT targets step back while a discard is being aimed, so the three that
|
||||
are stand out from the Salvage Yard beside them. */
|
||||
.cardrow.aiming .handcard:not(.target){opacity:.4}
|
||||
.stock .ld.addable,.stock .mt.addable{cursor:pointer;border-radius:3px;padding:0 4px;
|
||||
outline:1px solid #5aa9e6;background:rgba(90,169,230,.16)}
|
||||
.stock .ld.addable:hover,.stock .mt.addable:hover{background:rgba(90,169,230,.34)}
|
||||
/* Twelve Stages across, so a Day is one glance. The current Stage is lit, Stages already gone are
|
||||
dimmed, and a slot the die has just filled flashes once. */
|
||||
.tt{display:flex;gap:3px;flex-wrap:wrap}
|
||||
.tt-slot{flex:1 1 0;min-width:26px;text-align:center;border:1px solid #2c333d;border-radius:4px;
|
||||
padding:2px 0;background:#1a1f26;font-size:10px;cursor:help;line-height:1.25}
|
||||
.tt-slot .tt-s{display:block;color:#5f6b7a;font-size:9px}
|
||||
.tt-slot .tt-t{display:block;color:#5f6b7a;font-variant-numeric:tabular-nums}
|
||||
.tt-slot.due{background:#222c38;border-color:#4d6fa8}
|
||||
.tt-slot.due .tt-t{color:#cfe0f5;font-weight:700}
|
||||
.tt-slot.past{opacity:.45}
|
||||
.tt-slot.now{border-color:#b98cf0;box-shadow:0 0 0 2px rgba(150,110,230,.20)}
|
||||
.tt-slot.now .tt-s{color:#b98cf0;font-weight:700}
|
||||
.tt-slot.fresh{animation:ttflash 1.5s ease-out 1}
|
||||
@keyframes ttflash{0%{background:#2f6b47;border-color:#8fd6a0;transform:scale(1.18)}
|
||||
100%{background:#222c38;border-color:#4d6fa8;transform:scale(1)}}
|
||||
@media(prefers-reduced-motion:reduce){.tt-slot.fresh{animation:none;outline:2px solid #8fd6a0}}
|
||||
ul.blocked{margin:0;padding-left:18px}
|
||||
.sev-waiting{color:#8b94a3}.sev-risk{color:#e0b060}.sev-stuck{color:#e58080}
|
||||
.fac{border-top:1px solid #2c333d;padding:7px 0}
|
||||
|
||||
@@ -76,6 +76,10 @@ button.cardact.discard{color:#d6b48a}
|
||||
box-shadow:0 6px 22px rgba(0,0,0,.5)}
|
||||
#phasenote.shown{opacity:1;transform:translateX(-50%) translateY(0)}
|
||||
@media(prefers-reduced-motion:reduce){#phasenote{transition:none}}
|
||||
/* The die's answer, said where the player is looking. Green like a revenue gain — it is a good
|
||||
thing that just happened — and it disappears with the next render, as a moment should. */
|
||||
.scheduled{display:block;font-size:11.5px;margin:0 0 7px;padding:3px 9px;border-radius:5px;
|
||||
background:rgba(40,140,60,.22);border:1px solid #2f6b47;color:#bfe8cd;font-weight:600}
|
||||
.makeup-note{font-size:11px;margin:0 0 5px}
|
||||
/* Moves left, beside the moves. Amber like the actions, because it is a budget being spent by the
|
||||
buttons underneath it; struck through in red once it is gone. */
|
||||
@@ -187,6 +191,9 @@ ul.blocked li{padding:2px 0}
|
||||
<div id="clsyard" class="yard"></div>
|
||||
<div class="dim" id="yardnote"></div>
|
||||
</section>
|
||||
<section><h2>Timetable — who is due out, and when</h2><div id="timetable"></div>
|
||||
<div class="dim" style="font-size:11px;margin-top:5px">A train's number is its seniority and direction — odd runs west, even runs east. The Stage it departs is set by a 1D12 roll when its card is played, and it then runs at that time every Day.</div>
|
||||
</section>
|
||||
<section><h2>Blocked — why nothing is moving</h2><ul class="blocked" id="blocked"></ul></section>
|
||||
<section><h2>Facilities</h2><div id="facs"></div></section>
|
||||
</div>
|
||||
|
||||
@@ -116,6 +116,14 @@ export function playCue(cue: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cue === 'schedule') {
|
||||
// A die landing on the despatcher's desk, then the slot taking it: a short clatter and a chime.
|
||||
for (const at of [t, t + 0.05, t + 0.09]) tone(at, 200 + Math.random() * 90, 0.05, 0.07, 'square');
|
||||
tone(t + 0.16, 880, 0.3, 0.09, 'sine');
|
||||
tone(t + 0.16, 1320, 0.28, 0.05, 'sine');
|
||||
return;
|
||||
}
|
||||
|
||||
if (cue === 'day') {
|
||||
// The grade-crossing bell: two strikes. Inharmonic partials separate a bell from a beep.
|
||||
for (const at of [t, t + 0.22]) {
|
||||
|
||||
Reference in New Issue
Block a user