/**
* The side panels a game is read from — cards, yards, blockers, facilities.
*
* SHARED between the playable page and the site's replay viewer, for the same reason the board
* renderers are: a replay is the game being WATCHED rather than played, so it should look like the
* game. The viewer had three panels against the play page's eight — no facilities, no blockers, no
* cards, no yards — which meant a replay could not answer "why is nothing moving?", the question a
* replay mostly exists to answer.
*
* Each function returns HTML rather than writing to the DOM, so the two pages can keep their own
* element ids and their own layout while drawing the same contents.
*/
import type { Frame } from '../sim/view.ts';
const esc = (s: string): string =>
String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] ?? c);
/**
* A card, drawn as a card.
*
* `playable === false` crosshatches it: "not yet playable" had to be read, where a hatch is seen.
* The tooltip still explains WHY, which a hatch cannot. A replay passes null throughout — nothing
* there is playable, because nothing there is being played.
*/
export function cardRow(name: string, why: string, playable: boolean | null): string {
return (
`
` +
`
${esc(x.name)} ${esc(x.commodity)}
` +
/**
* THE WORKERS AND THE CAPACITIES, ON THE PANEL RATHER THAN IN A TOOLTIP.
*
* Laborers were only ever in the hover text, so "how many actions can I take here this
* Stage?" — the question that decides every Cargo phase — had to be hunted for. And a
* Modifier's whole effect is one of these numbers going up, so showing the number alone
* made an Ice House look like it had done nothing. Each now says what the card prints
* and what the Modifiers beside it added.
*/
`
` +
/**
* PORTERS TOO, NOT JUST LABORERS.
*
* `porters` has been on the view-model all along and no renderer ever printed it — so a
* Waiting Area, Restaurant or Hotel beside the Office, whose whole effect is +1 porter
* and +1 passenger slot, showed exactly half of what it did. Reported: "it gave me an
* extra porter that I see no indication of anywhere."
*
* A facility has one kind of worker or the other: Laborers work freight, Porters work
* passengers. Showing the row that is always 0/0 would be noise, so each shows its own.
*/
(Number(x.laborers.split('/').pop()) > 0 || x.base.laborers > 0
? stat('laborers', x.laborers, x.base.laborers, 'Actions this facility can take each Stage — one Laborer moves one load one square. Left of the slash is how many are still free this Stage.')
: '') +
(Number(x.porters.split('/').pop()) > 0 || x.base.porters > 0
? stat('porters', x.porters, x.base.porters, 'Passenger work, one action each per Stage: a Porter boards or detrains passengers, and earns the Revenue in a single action. Left of the slash is how many are still free this Stage.')
: '') +
(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
? `+ ${esc(x.modifiers.join(', '))}`
: '') +
/**
* A GRANT THIS FACILITY CANNOT USE, SAID OUT LOUD.
*
* An industry's printed flow is absolute, so an Ice House beside a Grocer's Warehouse
* gives its Laborer and nothing else. Showing only the numbers made that read as a bug —
* reported from playtesting as "it added the laborer but not the outbound slot". The
* number genuinely does not move; what was missing was the reason.
*/
(x.suppressed.length
? `` +
`${esc(String(x.suppressed.length))} printed bonus${x.suppressed.length === 1 ? '' : 'es'} unused`
: '') +
`
` +
/**
* 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
? `
${x.kind === 'passenger' ? 'waiting to board' : 'waiting to load'}${boxes(x.green, x.greenCap, 'g')}
`
: '') +
/**
* A PASSENGER FACILITY HAS NO SIGN AND NO SIDING, so neither row is drawn for one.
*
* `maw` comes back empty for a Depot, Station or Terminal — the pipeline is a Freight
* Facility fitting (§9.2 is Porters, with no MEN | AT | WORK step). The row LABEL printed
* ahead of the loop regardless, so a Depot showed the caption and three empty boxes it
* has no Laborer to work. Same for the siding: an Office has no industry track at all.
*/
(x.maw.length > 0
? `
MEN AT WORK` +
x.maw.map((m) => `${m ? esc(m) : '·'}`).join('') +
`
`
: '') +
(x.allowsIn
? `
${x.kind === 'passenger' ? 'arrived' : 'cleared inbound'}${boxes(x.red, x.redCap, 'r')}
`
: '') +
(x.trackCap > 0 || x.track.length > 0
? `
siding${boxes(x.track, x.trackCap, 's')}
`
: '') +
`
` +
(x.jammed ? 'JAMMED' : x.canFinish ? 'ready' : 'no car spotted') +
`
`,
)
.join('');
}
/** Styling for all of the above, so the two pages cannot drift apart visually. */
export const PANEL_CSS = `
h3{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:#8b94a3;margin:9px 0 4px}
.cardrow{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 2px}
.handcard{background:#242c36;border:1px solid #2c333d;border-radius:5px;
padding:5px 8px;font-size:11px;min-width:96px;position:relative}
.handcard:focus{outline:2px solid #4d6fa8;outline-offset:1px}
.cardrow.ref .handcard{background:#1c2129;border-style:dashed;border-color:#39424e;color:#b6bec9}
.handcard.unplayable{color:#7d8794;border-color:#39424e}
.handcard.unplayable::after{content:"";position:absolute;inset:0;border-radius:5px;pointer-events:none;
background:repeating-linear-gradient(45deg,transparent 0 5px,rgba(150,160,175,.20) 5px 6px)}
/* THE CARD JUST DRAWN. It sits first in the row, and this says which one that is — three cards that
look alike otherwise, with nothing to distinguish the one you turned over. Green is the page's
"a good thing just happened" colour, as on the timetable. A static badge rather than a flash: the
hand is rebuilt by innerHTML on every render, which would restart a keyframe each time, and the
question being answered is "which of these is new" rather than "did something happen". */
.handcard.fresh{border-color:#8fd6a0;box-shadow:0 0 0 2px rgba(143,214,160,.18)}
.handcard.fresh::before{content:"NEW";display:block;font-size:9px;letter-spacing:.09em;
color:#8fd6a0;font-weight:700;margin-bottom:2px}
/* A pile shows two things: which card is face up on top, and how many are under it. */
.pilehd{display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin-bottom:2px;
font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:#8b94a3}
.pilehd .depth{font-variant-numeric:tabular-nums;background:#2a3038;border-radius:8px;
padding:0 6px;color:#cfe0f5}
/* Rolling stock is finite and the Classification Yard only returns it when the Division Yard is
bare, so watching the Division Yard run down is real information. */
.yard{display:flex;flex-wrap:wrap;gap:5px;margin:0 0 6px}
.stock{background:#1c2129;border:1px solid #39424e;border-radius:4px;padding:2px 7px;font-size:11px}
.stock b{color:#e6e9ee}
.stock .ld{color:#8fd6a0}
.stock .mt{color:#9fb6d8}
.stock.none{opacity:.4}
/* 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}}
/* THE KEY TO THE FOUR STATES ABOVE. A slot can be lit blue, ringed violet, dimmed, or flashing
green, and nothing said which was which — reported as "what do the colours mean?" The green one
in particular is a MOMENT, not a state: it marks the slot the die just filled and is gone by the
next render, so without a key it reads as a category of train. */
.tt-key{display:flex;gap:10px;flex-wrap:wrap;margin-top:6px;font-size:10px;color:#8b94a3}
.tt-key span{display:inline-flex;align-items:center;gap:4px}
.tt-key span::before{content:"";width:9px;height:9px;border-radius:2px;border:1px solid #2c333d;
background:#1a1f26}
.tt-key .k-due::before{background:#222c38;border-color:#4d6fa8}
.tt-key .k-now::before{border-color:#b98cf0;box-shadow:0 0 0 1px rgba(150,110,230,.35)}
.tt-key .k-past::before{opacity:.45}
.tt-key .k-fresh::before{background:#2f6b47;border-color:#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}
.fac:first-child{border-top:0}
.fac .nm{font-size:12px;font-weight:600}
.caps{display:flex;flex-wrap:wrap;gap:9px;margin-top:3px;font-size:10px;align-items:baseline}
.cap b{color:#e6e9ee;font-variant-numeric:tabular-nums}
.cap .added{color:#8fd6a0;margin-left:2px;font-weight:700}
.caps .mods{color:#c8a04a}
/* Amber-grey: a fact about the card, not a fault the player caused. */
.caps .dead{color:#8b94a3;border-bottom:1px dotted #6c7480}
.boxes{display:flex;gap:3px;align-items:center;margin-top:3px;flex-wrap:wrap}
.box{display:inline-block;min-width:22px;text-align:center;border-radius:3px;padding:1px 4px;font-size:10px}
.box.empty{background:#242a32;color:#5a6472}
/* Green OUT, red IN, matching the board SVG's palette exactly — the panel and the map are the same
card seen twice, so they may not disagree about which colour means which way. Grey for the siding,
which is a place rather than a direction. */
.box.g{background:#2f6b3d}
.box.r{background:#8a4a4a}
.box.s{background:#3a4450}
.box.m{background:#8a6d1f}
.fstat{margin-top:3px;font-size:10px;padding:1px 6px;border-radius:3px;display:inline-block}
.fstat.good{background:rgba(40,140,60,.28)}
.fstat.bad{background:rgba(190,50,50,.38);font-weight:700}
.fstat.idle{opacity:.6}
`;