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:
Jesse
2026-08-05 23:08:03 -04:00
parent 543584a2ca
commit f4c0f49604
13 changed files with 381 additions and 50 deletions
+86 -11
View File
@@ -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}