v0.4.2 fixed passenger trains not stopping at office. changeed revenue for train completing run - 1 point to every player when train exits division

This commit is contained in:
Jesse
2026-08-13 21:29:52 -04:00
parent a7221dcf20
commit 393a669418
21 changed files with 1930 additions and 1531 deletions
+71 -26
View File
@@ -269,11 +269,13 @@ export function narrate(e: GameEvent, ctx: NarrateContext = {}): Narration {
text: `Train ${e.trainNumber} DIVERTED to ${e.to} — ${e.reason}`,
};
case 'trainCompleted':
// The one line in the log that is good news for everybody, so it is not 'quiet'.
return {
tone: 'quiet',
tone: 'good',
text:
`Train ${e.trainNumber} finished its run and left the Division carrying ` +
`${carsLabel(e.consist)} — the crew is free again`,
`Train ${e.isExtra ? 'X' : ''}${e.trainNumber} has completed its run, leaving via the ` +
`${e.side === 'east' ? 'Eastern' : 'Western'} Division Point carrying ${carsLabel(e.consist)}. ` +
`All players get 1 Revenue. The crew is free again.`,
};
// -- freight agent
@@ -533,42 +535,82 @@ export function impediments(s: GameState, player: PlayerIndex = 0): Impediment[]
}
/**
* WHY THE PASSENGERS ARE NOT GETTING OFF.
* WHY THE PASSENGERS ARE NOT MOVING — boarding as well as de-training.
*
* Reported: "I can't figure out how to have a train with three passenger coaches unload all three
* at my depot." You cannot — a Depot has ONE Porter and ONE red slot, so it works one coach a
* Stage and then needs a Freight Agent action to clear the box before the next. A Station does two
* and a Terminal three. That is the Office ladder doing its job, and nothing on screen said so.
* Reported first as "I can't figure out how to have a train with three passenger coaches unload
* all three at my depot." You cannot — a Depot has ONE Porter and ONE red slot, so it works one
* coach a Stage and then needs a Freight Agent action to clear the box before the next. A Station
* does two and a Terminal three. That is the Office ladder doing its job, and nothing on screen
* said so.
*
* §9.2 also requires an empty coach in the DIVISION YARD to swap into the train, which is a
* requirement the engine only started enforcing recently and which no panel mentions at all.
* Reported again as "passenger trains arrive at my Office and move on before I can load or unload
* them", which turned out to be three separate silences. This block only looked at trains carrying
* a LOADED coach, so a train arriving to PICK UP said nothing at all. A Whistle Post's Office card
* carries a passenger facility with zero Porters, so the engine answered `RESOURCE_SPENT` — "all
* Porters used" — to a player who had used none. And an expedited train leaves at the end of the
* Stage it arrives, so its one Load/Unload phase is now-or-never and nothing said which trains
* those are.
*
* §9.2 also requires an empty coach in the DIVISION YARD to swap into the train, which no panel
* mentioned at all.
*/
const office = area.grid.get(coordKey(area.officeCoord));
const pf = office?.facility;
if (pf && pf.kind === 'passenger') {
const waiting = area.adOccupancy
const coachTrains = area.adOccupancy
.map((id) => s.trays.get(id))
.filter((t) => t?.consist.some((c) => c.type === 'coach' && c.loaded));
if (waiting.length > 0) {
const reasons: string[] = [];
if (portersLeft(pf) < 1) {
reasons.push(
.filter((t): t is NonNullable<typeof t> => !!t && t.consist.some((c) => c.type === 'coach'));
if (coachTrains.length > 0) {
const label = (t: (typeof coachTrains)[number]): string =>
t.trainNumber === null ? 'the local crew' : `Train ${t.trainIsExtra ? 'X' : ''}${t.trainNumber}`;
const say = (why: string, severity: Impediment['severity'] = 'waiting'): void => {
out.push({ where: 'Office — passengers', why, severity });
};
const boarding = pf.outboundBox.some((c) => c.type === 'coach' && c.loaded);
const emptyOnTrain = coachTrains.some((t) => t.consist.some((c) => c.type === 'coach' && !c.loaded));
const loadedOnTrain = coachTrains.some((t) => t.consist.some((c) => c.type === 'coach' && c.loaded));
if (pf.porters < 1) {
say(
'this Office has NO Porters — a Whistle Post is not a Passenger Facility (§9) and cannot ' +
'work passengers at all. Upgrade it to a Depot or better.',
'stuck',
);
} else if (portersLeft(pf) < 1) {
say(
`all ${pf.porters} Porter${pf.porters === 1 ? '' : 's'} used this Stage — one works one coach, ` +
'and the Office tier is the Porter count (Depot 1, Station 2, Terminal 3)',
);
} else {
if (boarding && !emptyOnTrain) {
say('passengers are waiting to board, but every coach standing here is already full');
}
if (!boarding && !loadedOnTrain) {
say('nobody waiting to travel and no loaded coach to set down — stock the platform with a Freight Agent action');
}
if (loadedOnTrain && pf.inboundBox.length >= pf.capacity.inbound) {
say(
`the red Unloading box is full (${pf.capacity.inbound} slot${pf.capacity.inbound === 1 ? '' : 's'}) — ` +
'a Freight Agent action clears it, which costs a whole Local Operations turn',
);
}
if (loadedOnTrain && !s.yards.divisionYard.some((c) => c.type === 'coach' && !c.loaded)) {
say('no EMPTY coach in the Division Yard to swap into the train (§9.2 requires one)');
}
}
if (pf.inboundBox.length >= pf.capacity.inbound) {
reasons.push(
`the red Unloading box is full (${pf.capacity.inbound} slot${pf.capacity.inbound === 1 ? '' : 's'}) — ` +
'a Freight Agent action clears it, which costs a whole Local Operations turn',
// NOW OR NEVER. An expedited train leaves at the end of this Stage, so this is the only
// Load/Unload phase it will ever stand for — the exact thing the playtest report was about.
for (const t of coachTrains) {
if (!t.departsThisStage) continue;
say(
`${label(t)} is EXPEDITED — it leaves at the END of this Stage, so this is the only ` +
'Load/Unload phase it will stand for. Work it now or not at all.',
'risk',
);
}
if (!s.yards.divisionYard.some((c) => c.type === 'coach' && !c.loaded)) {
reasons.push('no EMPTY coach in the Division Yard to swap into the train (§9.2 requires one)');
}
for (const why of reasons) {
out.push({ where: 'passengers waiting to de-train', why, severity: 'waiting' });
}
}
}
@@ -645,6 +687,9 @@ export function cuesFor(events: readonly GameEvent[]): string[] {
// 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');
// A train running the whole length of the Division pays every player, and it is the one event
// nobody made happen on the turn it lands. It should be heard, not found in the log.
if (e.type === 'trainCompleted') out.push('completed');
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,