more info on train moves in history. train info added to tooltip on timetable and division map. support for multiple trains in area switching. more info on train7/8 wrt location of coach on initial train build.

This commit is contained in:
Jesse
2026-08-16 21:45:19 -04:00
parent 98f93e6691
commit 08339effba
14 changed files with 1029 additions and 40 deletions
+44 -5
View File
@@ -507,12 +507,23 @@ function moveTrain(
id: TrayId,
tray: CrewTray,
events: GameEvent[],
/**
* Which pass is asking. The Mainline Phase moves every train once; Supervisor Shift then asks the
* Expedited arrivals to go again. The train cannot tell the difference, but the player must — see
* `trainHighballed.why`.
*/
pass: 'mainline' | 'expedite' = 'mainline',
): MoveOutcome {
const dir = step(tray.direction);
if (tray.position.at === 'divisionPoint') {
// Read BEFORE `enterMainline` overwrites `tray.position`: the departure line named the side the
// train was leaving FROM, and taking it afterwards read the mainline position instead — so every
// train reported leaving the Eastern Division Point, including the eastbound ones that had just
// been made up at the Western one.
const fromSide = (tray.position as { side: Direction }).side;
const dpIndex = s.division.nodes.findIndex(
(n) => n.kind === 'divisionPoint' && n.side === (tray.position as { side: Direction }).side,
(n) => n.kind === 'divisionPoint' && n.side === fromSide,
);
const target = dpIndex + dir;
const node = s.division.nodes[target];
@@ -525,7 +536,15 @@ function moveTrain(
enterMainline(s, node, id, tray, target);
const dp = s.division.nodes[dpIndex];
if (dp?.kind === 'divisionPoint') dp.holding = dp.holding.filter((t) => t !== id);
events.push({ type: 'phaseBegan', phase: `train ${tray.trainNumber} highballed` });
events.push({
type: 'trainHighballed',
trainNumber: tray.trainNumber ?? 0,
from: `the ${fromSide === 'west' ? 'Western' : 'Eastern'} Division Point`,
to: 'the Mainline',
// Narrated as a phase marker until now — "▸ train 8 highballed phase" — which is not a phase
// and told the player nothing about why the train had started its run.
why: 'it was made up and the Subdivision ahead was clear, so its run begins',
});
return 'moved';
}
@@ -580,6 +599,10 @@ function moveTrain(
trainNumber: tray.trainNumber ?? 0,
from: 'the Office',
to: 'the Mainline',
why:
pass === 'expedite'
? 'its card prints EXPEDITE, so it leaves at the end of the Stage it arrived in — you got the Cargo phase with it, but no Local Operations turn to switch it'
: 'it stood a full Stage at the Office, so §8.1 released it this Mainline Phase',
});
return 'moved';
}
@@ -1019,9 +1042,25 @@ function shiftChange(s: GameState, events: GameEvent[]): AdvanceResult {
.sort(([, a], [, b]) => (a.trainNumber ?? 0) - (b.trainNumber ?? 0));
for (const [id, tray] of expedited) {
tray.departsThisStage = false;
// A clearance question cannot be asked here — the Superintendent's phase is over — so an
// expedited train that would need a ruling simply stays, and runs normally next Stage.
if (moveTrain(s, id, tray, events) === 'needsClearance') continue;
/**
* An expedited train that would need a §8.1 ruling STAYS, and runs normally from then on.
*
* Said in the log, because it is the difference between "this train leaves at the end of the
* Stage" and "this train is still here next turn", and a player watching one Expedited train
* vanish and the next one linger has no way to tell why. Measured on one seed: with ordinary
* traffic running, Train 6 was held this way and collected four Local Operations turns.
*/
if (moveTrain(s, id, tray, events, 'expedite') === 'needsClearance') {
events.push({
type: 'trainHeld',
trainNumber: tray.trainNumber ?? 0,
reason:
'it EXPEDITES and was due out at the end of this Stage, but the Subdivision ahead needs a ' +
'clearance ruling and Supervisor Shift has no Superintendent to give one. It stays, and ' +
'runs like an ordinary train from now on — so you DO get a Local Operations turn with it',
});
continue;
}
}
// §5 — the Fedora passes every three Stages: shift changes at Stages 3, 6, 9 and 12.