fixed train# specific rules. cleaned up display topline and train display on division map

This commit is contained in:
Jesse
2026-08-09 19:44:18 -04:00
parent 4a800a053a
commit 6ead39c530
14 changed files with 316 additions and 57 deletions
+20 -43
View File
@@ -277,64 +277,41 @@ export function divisionSvg(nodes: DivisionView[]): string {
}
/**
* A TRAIN, DRAWN AS A TRAIN — the same vocabulary the Office Area card uses.
* ON THE DIVISION MAP, A TRAIN IS A CHIP — name, which way it points, how many cars.
*
* The chip was a name and a figure, and the figure was Stages left to cross this card: read
* once as the car count, and once the unit was spelled out, read as redundant beside the
* position the card already draws. What it never said is the thing worth knowing at a glance —
* what the train is carrying, whether those cars are loaded, and which end the engine is on.
* It was drawn as a full consist here, matching the Office Area card, and reported as too large
* and hard to read: a Mainline cell is 152px and a Running Track cell 78px, against a district
* card's 166px, and a four-car train plus its engine has to squeeze into a fifth of that. The
* Office card is where the consist is worth drawing, because that is where the switching
* decisions are made and where there is room to read it.
*
* WEST ON THE LEFT here too. `cars` is nose first, so an east-facing train is drawn reversed and
* its engine lands at the east end, exactly as on the district card.
* So this keeps the two things the Division map is actually for — where a train is and which way
* it is going — and leaves the cars to the tooltip and to the district.
*/
c.trains.forEach((t, k) => {
const kindOf = (car: string): string =>
/caboose/.test(car) ? 'cab' : /hopper/.test(car) ? 'hop' : /boxcar/.test(car) ? 'box'
: /reefer/.test(car) ? 'rfr' : /tank/.test(car) ? 'tnk' : /coach/.test(car) ? 'cch' : '?';
const cars = t.cars ?? [];
const arrow = t.facing === 'w' ? '◀' : t.facing === 'e' ? '▶' : t.facing === 'n' ? '▲' : '▼';
const items: { label: string; kind: string }[] = cars.map((car) => ({
label: kindOf(car),
kind: /^loaded/.test(car) || /caboose/.test(car) ? 'ld' : 'mt',
}));
items.splice(Math.max(0, Math.min(items.length, t.engineAt ?? 0)), 0, { label: arrow, kind: 'eng' });
const laid = t.facing === 'e' ? [...items].reverse() : items;
const arrow = t.facing === 'w' ? '\u25c0' : t.facing === 'e' ? '\u25b6' : t.facing === 'n' ? '\u25b2' : '\u25bc';
const loaded = cars.filter((x) => /^loaded/.test(x) || /caboose/.test(x)).length;
const label = cars.length === 0 ? `${t.label} ${arrow}` : `${t.label} ${arrow}${cars.length}`;
const nameW = t.label.length * 6.5 + 6;
const w = Math.min(c.w - 8, nameW + laid.length * 13 + 6);
/**
* NARROW THE CARS RATHER THAN DROP THEM.
*
* A Running Track cell is 78 wide and a four-car train plus its engine wants more than that.
* Squeezing is honest; leaving cars out is not — a chip showing three of five cars is a chip
* that lies about the train. Below 11px the three-letter type will not fit, so the blocks
* carry the load state alone and the tooltip carries the words.
*/
const CARW = Math.max(5, Math.min(13, (w - nameW - 6) / Math.max(1, laid.length)));
const w = Math.min(c.w - 8, label.length * 6.6 + 12);
// A train on a Mainline card sits in ITS region; anywhere else it just sits on the card.
const inRegion = c.regions > 1 && typeof t.region === 'number';
const slotX = inRegion ? c.x + 6 + RW * (t.region ?? 0) + RW / 2 : c.x + c.w / 2;
const spread = inRegion ? 0 : (k - (c.trains.length - 1) / 2) * (w + 4);
const tx = slotX + spread;
const dir = t.direction === 'west' ? ' ◀ west' : t.direction === 'east' ? ' east ▶' : '';
const dir = t.direction === 'west' ? ' \u25c0 west' : t.direction === 'east' ? ' east \u25b6' : '';
const stages =
typeof t.stagesLeft === 'number'
? ` · ${t.stagesLeft} Stage${t.stagesLeft === 1 ? '' : 's'} still to run across this card` +
? ` \u00b7 ${t.stagesLeft} Stage${t.stagesLeft === 1 ? '' : 's'} still to run across this card` +
' (Stages, not regions: a card is two regions of fixed distance, and how many Stages a' +
' train takes over them depends on the card speed and the train)'
: '';
out += `<g class="bs-train" data-tip="${esc(t.label)} — carrying ${esc(cars.join(', ') || 'no cars')}, nose first${
inRegion ? ` · region ${(t.region ?? 0) + 1} of ${c.regions}${dir}` : ''
}${esc(stages)}">` +
out += `<g class="bs-train" data-tip="${esc(t.label)} \u2014 carrying ${esc(cars.join(', ') || 'no cars')}${
cars.length ? ` (${loaded} loaded)` : ''
}${inRegion ? ` \u00b7 region ${(t.region ?? 0) + 1} of ${c.regions}${dir}` : ''}${esc(stages)}">` +
`<rect x="${tx - w / 2}" y="${c.y + 22}" width="${w}" height="19" rx="3"/>` +
`<text class="bs-tlab" x="${tx - w / 2 + 4}" y="${c.y + 35}">${esc(t.label)}</text>`;
laid.forEach((it, n) => {
const bx = tx - w / 2 + nameW + n * CARW;
out += `<rect class="bs-tcar bs-t-${it.kind}" x="${bx}" y="${c.y + 25}" width="${Math.max(3, CARW - 2)}" height="13" rx="2"/>`;
if (CARW >= 11) {
out += `<text class="bs-tcarlab" x="${bx + (CARW - 2) / 2}" y="${c.y + 35}" text-anchor="middle">${esc(it.label)}</text>`;
}
});
`<text class="bs-tlab" x="${tx}" y="${c.y + 35}" text-anchor="middle">${esc(label)}</text>`;
out += '</g>';
});
out += '</g>';
@@ -716,8 +693,8 @@ export function officeSvg(
t.facing === 'e' ? 'east' : t.facing === 'w' ? 'west' : t.facing === 'n' ? 'north' : 'south';
const consistWords = t.cars.length === 0 ? 'no cars' : t.cars.join(', ');
out += `<g class="bs-crew" data-tip="${esc(
`${t.label} \u2014 engine pointing ${facingWord}, carrying ${consistWords} (nose first). Drawn as it stands: west is on the left.`,
)}"><rect x="${tx}" y="${RAIL - 11}" width="${tw}" height="22" rx="3"/>`;
`${t.label} — engine pointing ${facingWord}, carrying ${consistWords}` + (t.what ? `\n\n${t.what}` : ''),
)}"><rect x="${tx}" y="${RAIL - 11}" width="${tw}" height="22" rx="3"/>`;
out += `<text class="bs-tlab" x="${tx + 4}" y="${RAIL + 4}">${esc(t.label)}</text>`;
laid.forEach((it, n) => {
const bx = tx + 28 + n * cw;