/** Physical dimensions used when deriving how many wagons a locomotive can pull. */ export type WagonTypeDimensions = { lengthMeters: number; capacityTons: number; }; export type LocomotiveLimits = { maxPullWeightTons: number; maxTrainLengthMeters: number; }; export type DerivedTrainCapacity = { maxWeightTons: number; maxLengthMeters: number; maxWagonSlots: number; }; const DEFAULT_WAGON_LENGTH_M = 14; const DEFAULT_WAGON_CAPACITY_T = 70; /** * Derive train capacity from locomotive pull weight and train length. * Wagon count is NOT a fixed 53 — it is the minimum of: * - floor(maxLength / shortest wagon type length) * - floor(maxWeight / lightest wagon type capacity) */ export function deriveTrainCapacityFromLocomotive( locomotive: LocomotiveLimits, wagonTypes: WagonTypeDimensions[], ruleCaps?: { maxTrainWeightTons?: number; maxTrainLengthMeters?: number }, ): DerivedTrainCapacity { const maxWeightTons = Math.min( Number(locomotive.maxPullWeightTons) || Infinity, ruleCaps?.maxTrainWeightTons ?? Infinity, ); const maxLengthMeters = Math.min( Number(locomotive.maxTrainLengthMeters) || Infinity, ruleCaps?.maxTrainLengthMeters ?? Infinity, ); const types = wagonTypes.length > 0 ? wagonTypes : [{ lengthMeters: DEFAULT_WAGON_LENGTH_M, capacityTons: DEFAULT_WAGON_CAPACITY_T }]; const minLength = Math.min(...types.map((w) => Number(w.lengthMeters) || DEFAULT_WAGON_LENGTH_M)); const minCapacity = Math.min( ...types.map((w) => Number(w.capacityTons) || DEFAULT_WAGON_CAPACITY_T), ); const byLength = minLength > 0 && Number.isFinite(maxLengthMeters) ? Math.floor(maxLengthMeters / minLength) : 0; const byWeight = minCapacity > 0 && Number.isFinite(maxWeightTons) ? Math.floor(maxWeightTons / minCapacity) : byLength; const maxWagonSlots = Math.max(0, Math.min(byLength, byWeight)); return { maxWeightTons: Number.isFinite(maxWeightTons) ? maxWeightTons : MAX_FALLBACK_WEIGHT, maxLengthMeters: Number.isFinite(maxLengthMeters) ? maxLengthMeters : MAX_FALLBACK_LENGTH, maxWagonSlots, }; } export const MAX_FALLBACK_WEIGHT = 3500; export const MAX_FALLBACK_LENGTH = 760; /** * Effective pull limits for a train set with multiple locomotives: the weakest * locomotive caps the train, so take the minimum pull weight and minimum length * across all assigned locomotives. Returns null when no locomotives are given. */ export function minLocomotiveLimits( locomotives: Array>, ): LocomotiveLimits | null { if (!locomotives.length) return null; return { maxPullWeightTons: Math.min( ...locomotives.map((l) => Number(l.maxPullWeightTons) || Infinity), ), maxTrainLengthMeters: Math.min( ...locomotives.map((l) => Number(l.maxTrainLengthMeters) || Infinity), ), }; } /** Per-booking train length from wagon count and freight-specific wagon type length. */ export function bookingTrainLengthMeters( freightType: string | null | undefined, wagonCount: number, lengths: { container: number; bulk: number }, ): number { const perWagon = freightType === 'BULK' ? lengths.bulk : lengths.container; return wagonCount * perWagon; } export function wagonTypeDimensionsFromEntity(wt: { lengthMeters?: number | string | null; capacityTons?: number | string | null; }): WagonTypeDimensions { return { lengthMeters: Number(wt.lengthMeters) || DEFAULT_WAGON_LENGTH_M, capacityTons: Number(wt.capacityTons) || DEFAULT_WAGON_CAPACITY_T, }; }