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
+39 -22
View File
@@ -132,7 +132,7 @@ export function divisionSvg(nodes: DivisionView[]): string {
trains: n.trains.flat(),
cap: n.capacity,
tip: dp
? 'A Division Point — trains enter and leave the Division here, and queue without limit'
? 'A Division Point — the end of the line. Trains both enter and leave the Division here (odd numbers run west, even run east), and queue without limit'
: `${n.label} — Mainline${n.gradeUp ? `, climbs ${n.gradeUp === 'east' ? 'east' : 'west'}` : ''}${n.modifiers.length ? ` · ${n.modifiers.join(' · ')}` : ''}`,
owner: null,
// A Division Point is one region — the queue trains enter and leave the Division through.
@@ -286,12 +286,16 @@ export function divisionSvg(nodes: DivisionView[]): string {
if (first) {
out += `<g class="bs-stop"><line x1="${first.x - 9}" y1="${first.y + 16}" x2="${first.x - 9}" y2="${first.y + CH - 16}"/>` +
`<line x1="${first.x - 9}" y1="${first.y + CH / 2}" x2="${first.x - 2}" y2="${first.y + CH / 2}"/></g>`;
out += `<text class="bs-end" x="${first.x - 12}" y="${first.y + CH + 12}" text-anchor="end">trains enter</text>`;
// BOTH ENDS DO BOTH. Odd trains run west and even trains run east (content.ts `pair`), so each
// Division Point is a way on AND a way off. "trains enter" here and "trains leave" there said
// the Division ran one way, which it does not — the point of the buffer stops is only that it is
// a LINE and not a loop.
out += `<text class="bs-end" x="${first.x - 12}" y="${first.y + CH + 12}" text-anchor="end">west end · in and out</text>`;
}
if (last && last !== first) {
out += `<g class="bs-stop"><line x1="${last.x + last.w + 9}" y1="${last.y + 16}" x2="${last.x + last.w + 9}" y2="${last.y + CH - 16}"/>` +
`<line x1="${last.x + last.w + 2}" y1="${last.y + CH / 2}" x2="${last.x + last.w + 9}" y2="${last.y + CH / 2}"/></g>`;
out += `<text class="bs-end" x="${last.x + last.w + 12}" y="${last.y + CH + 12}">trains leave</text>`;
out += `<text class="bs-end" x="${last.x + last.w + 12}" y="${last.y + CH + 12}">east end · in and out</text>`;
}
out += '</svg>';
@@ -443,12 +447,19 @@ export function officeSvg(
out += `<text class="bs-coord" x="${W - 5}" y="12" text-anchor="end">${cell.row},${cell.col}</text>`;
/**
* THE LOAD PIPELINE, on the card itself.
* THE LOAD PIPELINE, on the card itself, DRAWN THE WAY IT FLOWS.
*
* A load crosses green -> MEN | AT | WORK -> the spotted car, and that journey is the whole of
* freight. It was drawn only in the side panel, so advancing a load changed nothing on the card
* the player was looking at. The printed cards put these squares under the track for exactly
* this reason.
* §9.3 runs loading Green → MEN → AT → WORK → the spotted car, and unloading the other way
* entirely: car → WORK → AT → MEN → red. So green and red BOTH belong beside MEN, and the car
* belongs beside WORK.
*
* This drew green, then MEN|AT|WORK, then red — putting the red box at the far right, which is
* exactly where the car is. An unload therefore appeared to run backwards across the whole row
* and then land on the end it had just come from. Reported after unloading at a warehouse:
* "it went W, A, M, and then to the red box at the far right".
*
* Only the boxes an industry actually uses are drawn, and the arrows point the way its freight
* really moves.
*/
const f = cell.facility;
if (f) {
@@ -459,25 +470,31 @@ export function officeSvg(
let bx = 7;
const by = H - 42;
// Green: what is waiting to be worked.
for (let i = 0; i < Math.max(1, f.greenCap); i++) {
out += box(bx, by, i < f.green.length, 'bs-green');
bx += cellW + 2;
// Green feeds INTO the sign, red comes OUT of it, so both sit on this side and their arrows
// point opposite ways. An industry that only receives has no green boxes at all.
if (f.allowsOut) {
for (let i = 0; i < Math.max(1, f.greenCap); i++) {
out += box(bx, by, i < f.green.length, 'bs-green');
bx += cellW + 2;
}
out += `<text class="bs-arrow" x="${bx + 1}" y="${by + 9}">›</text>`;
bx += 9;
}
out += `<text class="bs-arrow" x="${bx + 1}" y="${by + 9}">›</text>`;
bx += 8;
// MEN | AT | WORK — a load sits in one of these while it is being worked.
if (f.allowsIn) {
for (let i = 0; i < Math.max(1, f.redCap); i++) {
out += box(bx, by, i < f.red.length, 'bs-red');
bx += cellW + 2;
}
out += `<text class="bs-arrow" x="${bx + 1}" y="${by + 9}">‹</text>`;
bx += 9;
}
// MEN | AT | WORK — one physical sign, worked left to right loading and right to left
// unloading. The spotted car sits past WORK, on the siding drawn below.
for (let i = 0; i < f.maw.length; i++) {
out += box(bx, by, f.maw[i] !== null, 'bs-maw', ['M', 'A', 'W'][i]);
bx += cellW + 2;
}
out += `<text class="bs-arrow" x="${bx + 1}" y="${by + 9}">›</text>`;
bx += 8;
// Red: what has come off an arriving car and is waiting to be cleared.
for (let i = 0; i < Math.max(1, f.redCap); i++) {
out += box(bx, by, i < f.red.length, 'bs-red');
bx += cellW + 2;
}
out += `<text class="bs-arrow" x="${bx + 1}" y="${by + 9}">${f.allowsOut && f.allowsIn ? '\u21c4' : f.allowsOut ? '›' : '‹'}</text>`;
}
// Standing room, drawn as the printed squares: filled means occupied.
+14 -1
View File
@@ -239,9 +239,19 @@ export function narrate(e: GameEvent, ctx: NarrateContext = {}): Narration {
text: `Train ${e.trainNumber} HIGHBALLED — departed ${e.from} onto ${e.to}`,
};
case 'trainArrived':
/**
* AN EXPEDITED TRAIN IS NOT COMING BACK ROUND.
*
* This said "it will highball again next Mainline Phase, so any work must happen now" to every
* arrival. For an expedited train that is exactly backwards — Q3 departs it in the SAME Stage
* it arrived, so there is no turn in which to work it — and the log then contradicted itself
* two lines later when the train left. Reported after Train 4, the Express, did precisely that.
*/
return {
tone: 'plain',
text: `Train ${e.trainNumber} ARRIVED at the ${e.office} carrying ${carsLabel(e.consist)} — it will highball again next Mainline Phase, so any work must happen now`,
text: e.expedited
? `Train ${e.trainNumber} ARRIVED at the ${e.office} carrying ${carsLabel(e.consist)} — and EXPEDITES, so it leaves again this same Mainline Phase; there is no turn in which to work it`
: `Train ${e.trainNumber} ARRIVED at the ${e.office} carrying ${carsLabel(e.consist)} — it stands until the next Mainline Phase, so any work must happen before then`,
};
case 'trainDiverted':
return {
@@ -551,6 +561,9 @@ export function cuesFor(events: readonly GameEvent[]): string[] {
// board with only a line of history to say where it went is the thing that most needs a noise.
if (e.type === 'carsCoupled') out.push('couple');
if (e.type === 'carsDropped' || e.type === 'flyingSwitch') out.push('drop');
// The 1D12 that sets a train's departure Stage. It is the one roll a player makes and it landed
// silently, so the card was gone and the answer to "when does it run?" was a line of history.
if (e.type === 'trainScheduled') out.push('schedule');
if (e.type === 'stageBegan') {
// A Stage BEGINNING is the previous one ending — except the first, which is the game opening
// and has nothing behind it. A Day boundary rings the bell only: sounding both would collide,
+34 -3
View File
@@ -109,6 +109,16 @@ export type FacilityView = {
* anywhere. Keeping the base lets the panel say "2 (1 + 1 from a Modifier)".
*/
base: { out: number; in: number; laborers: number };
/**
* Which way freight actually flows here, so the pipeline can be DRAWN in that direction.
*
* §9.3 runs loading Green → MEN → AT → WORK → car, and unloading the other way: car → WORK → AT →
* MEN → red. So green and red both sit beside MEN, and the car sits beside WORK. Drawing red at
* the far right — where the car is — made an unload look like it ran backwards across the whole
* row and then landed on the wrong end.
*/
allowsOut: boolean;
allowsIn: boolean;
/** The Modifier cards standing beside it, by name. */
modifiers: string[];
/**
@@ -322,6 +332,8 @@ function facilityView(
porters: `${portersLeft(f)}/${f.porters}`,
canFinish: canFinishHere(f),
jammed: f.menAtWork.some((l) => l !== null) && !canFinishHere(f),
allowsOut: f.allows.outbound,
allowsIn: f.allows.inbound,
base: baseOf(card),
modifiers: (card.modifiers ?? []).map((m) => MODIFIER_NAMES[m] ?? prettyKey(m)),
};
@@ -468,8 +480,20 @@ export function describeIntent(s: GameState, i: Intent): string {
}
case 'switch.sortConsist':
return `re-order consist [${i.order.join(',')}]`;
case 'freightAgent.stockOutbound':
return `stock a ${i.carType} at ${at(i.at)}`;
case 'freightAgent.stockOutbound': {
/**
* "stock a coach at (0, 0)" reads as putting a CAR on the track, and was reported as exactly
* that confusion: no train at the Depot, so how is a coach being stocked there? It is not a
* car on the track — it is a load taken from the Division Yard into the green Loading box,
* waiting for a train that can carry it. For a passenger facility that load is passengers on
* the platform.
*/
const f = areaOf(s, 0).grid.get(`${i.at.row},${i.at.col}`)?.facility ?? null;
const where = f?.kind === 'passenger' ? 'onto the platform' : 'into the green Loading box';
return i.carType === 'coach' && f?.kind === 'passenger'
? `bring passengers ${where} at ${at(i.at)} — they wait there for a train with an empty coach`
: `bring a ${i.carType} load ${where} at ${at(i.at)} — it waits there for a car to be spotted`;
}
case 'freightAgent.unjam':
return `unjam ${i.from} at ${at(i.at)}`;
case 'freightAgent.clearInbound':
@@ -683,7 +707,14 @@ export function snapshot(
const chip = trainChip(s, t.tray);
return {
...chip,
label: `${chip.label} (${t.stagesRemaining})`,
/**
* SAY WHAT THE NUMBER IS.
*
* This read "TX14 (2)", a bare figure next to a train's name — and it was read as the
* car count twice, by the same player, because that is the obvious guess. It is Stages
* left to cross this Mainline card. Naming the unit costs three characters.
*/
label: `${chip.label} · ${t.stagesRemaining}⧗`,
region: place(t),
direction: t.direction,
};