Show the whole Division mainline. district now has option for auto hiding on phases where not needed. scaffold for multi-player geometry showing division. regions on mainline cards implemented. bug fix on laying rail cards. optimized replay (carried links forward). Add sounds, first pass
This commit is contained in:
+122
-5
@@ -17,6 +17,7 @@ import {
|
||||
MAINLINE_MODIFIER_CARDS,
|
||||
MAINLINE_PROFILES,
|
||||
MANEUVER_CARDS,
|
||||
REGIONS_PER_MAINLINE_CARD,
|
||||
OFFICE_ORDER,
|
||||
SPACE_USE_CARDS,
|
||||
industryProfile,
|
||||
@@ -86,7 +87,35 @@ export type FacilityView = {
|
||||
jammed: boolean;
|
||||
};
|
||||
|
||||
export type TrainChip = { label: string; consist: string[] };
|
||||
export type TrainChip = {
|
||||
label: string;
|
||||
consist: string[];
|
||||
/**
|
||||
* Which region of a Mainline card the train is standing in, and which way it is going. Absent
|
||||
* everywhere else: a Division Point is a single queue, and inside a district a train moves by
|
||||
* Moves rather than by Stages, so it occupies a card outright.
|
||||
*/
|
||||
region?: number;
|
||||
direction?: string;
|
||||
};
|
||||
/**
|
||||
* One card of a player's Running Track, as the Division sees it.
|
||||
*
|
||||
* The through route between the Limits IS the Running Track — everything hanging beneath it is
|
||||
* secondary, and a train crossing the Division never touches it. So the Division map expands an
|
||||
* Office into this row and leaves sidings, industries and the load pipeline to the district view.
|
||||
*/
|
||||
export type RunningCardView = {
|
||||
row: number;
|
||||
col: number;
|
||||
kind: string;
|
||||
label: string;
|
||||
/** Port pairs, from the engine's `connectionsFor`, so the drawn rail cannot invent a join. */
|
||||
links: string[];
|
||||
/** Trains standing ON this card of the Running Track. */
|
||||
trains: TrainChip[];
|
||||
};
|
||||
|
||||
export type DivisionView = {
|
||||
kind: string;
|
||||
label: string;
|
||||
@@ -104,6 +133,19 @@ export type DivisionView = {
|
||||
* on that grade has no visible meaning.
|
||||
*/
|
||||
gradeUp: string | null;
|
||||
/** Mainline cards only: how many regions the card is divided into (§2.1 — two). */
|
||||
regions?: number;
|
||||
/** Office nodes only: the Running Track, Limits to Limits, west to east. */
|
||||
running?: RunningCardView[];
|
||||
/** Office nodes only: whose district this is. */
|
||||
owner?: number;
|
||||
/**
|
||||
* Office nodes only: crews working BELOW the Running Track.
|
||||
*
|
||||
* A crew down a siding has no position on the through route — projecting one would be a lie — so
|
||||
* it is reported against the district as a whole and drawn exactly where it is in the zoomed view.
|
||||
*/
|
||||
switching?: TrainChip[];
|
||||
};
|
||||
|
||||
export type Frame = {
|
||||
@@ -431,12 +473,43 @@ export function snapshot(
|
||||
// with how long each still has to run.
|
||||
const name = MAINLINE_PROFILES.find((m) => m.kind === n.card)?.name ?? n.card;
|
||||
const isGrade = MAINLINE_PROFILES.find((m) => m.kind === n.card)?.speed.kind === 'grade';
|
||||
/**
|
||||
* WHERE ON THE CARD, from what the crossing already cost.
|
||||
*
|
||||
* §2.1 divides a Mainline card into two regions and §8.2 moves a train one region per Stage.
|
||||
* The engine crosses in `crossingStages` Stages instead, which varies by card speed, train
|
||||
* speed, passengers and modifiers — so the printed model is recovered by treating the entry
|
||||
* point as the thing that varies, exactly as the cards do:
|
||||
*
|
||||
* entry = REGIONS - stagesTotal position = entry + elapsed
|
||||
*
|
||||
* A 60 card is one Stage, so the train enters at the second region and is gone — which is
|
||||
* what "Start positions further along the card" means on the printed art. A 30 card is two
|
||||
* Stages, giving one region per Stage, which is §8.2 exactly. A slow train needing three
|
||||
* Stages cannot fit three steps into two regions, so it holds in the first for a Stage: the
|
||||
* card's distance is fixed and the train is simply slow across it.
|
||||
*/
|
||||
const place = (t: { stagesRemaining: number; stagesTotal: number }): number => {
|
||||
// `entry` may be NEGATIVE — a slow train needing three Stages cannot fit three steps into
|
||||
// two regions, so it notionally starts before the card and spends the extra Stage getting
|
||||
// to the first region. Clamping only the final position keeps that Stage at the START,
|
||||
// where being slow shows; clamping `entry` first would have parked it at the exit instead.
|
||||
const entry = REGIONS_PER_MAINLINE_CARD - t.stagesTotal;
|
||||
const elapsed = t.stagesTotal - t.stagesRemaining;
|
||||
return Math.min(REGIONS_PER_MAINLINE_CARD - 1, Math.max(0, entry + elapsed));
|
||||
};
|
||||
return {
|
||||
kind: 'ml',
|
||||
label: name,
|
||||
regions: REGIONS_PER_MAINLINE_CARD,
|
||||
trains: [n.transits.map((t) => {
|
||||
const chip = trainChip(s, t.tray);
|
||||
return { ...chip, label: `${chip.label} (${t.stagesRemaining})` };
|
||||
return {
|
||||
...chip,
|
||||
label: `${chip.label} (${t.stagesRemaining})`,
|
||||
region: place(t),
|
||||
direction: t.direction,
|
||||
};
|
||||
})],
|
||||
capacity: MAINLINE_PROFILES.find((m) => m.kind === n.card)?.trainsMayPass ? 2 : 1,
|
||||
modifiers: [
|
||||
@@ -446,13 +519,57 @@ export function snapshot(
|
||||
gradeUp: isGrade ? (n.gradeUp ?? 'east') : null,
|
||||
};
|
||||
}
|
||||
const oa = areaOf(s, n.owner);
|
||||
|
||||
// Where every crew in this district actually is: on a Running Track card, or below it.
|
||||
const onRunning = new Map<string, TrainChip[]>();
|
||||
const below: TrainChip[] = [];
|
||||
for (const [id, tray] of s.trays) {
|
||||
const pos = tray.position;
|
||||
if (pos.at !== 'grid' || pos.owner !== n.owner) continue;
|
||||
const c = trainChip(s, id);
|
||||
if (pos.coord.row === oa.runningRow) {
|
||||
const k = `${pos.coord.row},${pos.coord.col}`;
|
||||
onRunning.set(k, [...(onRunning.get(k) ?? []), c]);
|
||||
} else {
|
||||
below.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Limits to Limits, west to east. §2.1 — the Running Track runs BETWEEN the Limits, so the
|
||||
// signs are the ends of the through route rather than obstacles on it.
|
||||
const running: RunningCardView[] = [];
|
||||
for (let col = oa.limitsWest.col; col <= oa.limitsEast.col; col++) {
|
||||
const card = oa.grid.get(`${oa.runningRow},${col}`);
|
||||
if (!card) continue;
|
||||
const g = card.geometry;
|
||||
let label: string;
|
||||
if (g.kind === 'office') label = officeProfile(oa.tier).name;
|
||||
else if (g.kind === 'limits') label = 'Limits';
|
||||
else if (g.kind === 'facility') label = FACILITY_NAMES[g.facility] ?? prettyKey(g.facility);
|
||||
else if (g.kind === 'modifier') label = MODIFIER_NAMES[g.modifier] ?? prettyKey(g.modifier);
|
||||
else if (g.kind === 'spaceUse') label = prettyKey(g.key);
|
||||
else label = geometryLabel(g.geometry);
|
||||
running.push({
|
||||
row: oa.runningRow,
|
||||
col,
|
||||
kind: g.kind,
|
||||
label,
|
||||
links: connectionsFor(card).map(([a, b]) => `${a}${b}`),
|
||||
trains: onRunning.get(`${oa.runningRow},${col}`) ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'office',
|
||||
label: officeProfile(areaOf(s, n.owner).tier).name,
|
||||
trains: [areaOf(s, n.owner).adOccupancy.map((id) => trainChip(s, id))],
|
||||
capacity: officeProfile(areaOf(s, n.owner).tier).adTracks,
|
||||
label: officeProfile(oa.tier).name,
|
||||
trains: [oa.adOccupancy.map((id) => trainChip(s, id))],
|
||||
capacity: officeProfile(oa.tier).adTracks,
|
||||
modifiers: [],
|
||||
gradeUp: null,
|
||||
owner: n.owner,
|
||||
running,
|
||||
switching: below,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user