Game Constants

Modify game rules and pathfinding behavior at runtime

The game's tunable numbers live in three objects: RULES (construction, costs, trains, physics), PATHFINDING_RULES (how pops choose between transit, driving, and walking), and GAME_COLORS (map visuals: road rendering, mode colors, map-layer color scales). Mods can override any of them at runtime.

All modifier methods are top-level API methods on window.SubwayBuilderAPI, not under .actions. Overrides are deep-merged, so you only pass the keys you want to change.

modifyConstants#

Modify game rules (RULES) at runtime.

javascript
window.SubwayBuilderAPI.modifyConstants({
    STARTING_MONEY: 10_000_000_000, // 10B instead of 3B
    DEFAULT_TICKET_COST: 5,
    CONSTRUCTION_COSTS: {
        TUNNEL: {
            SINGLE_MULTIPLIER: 0.5 // Half the normal cost
        }
    }
});

Read the current values with window.SubwayBuilderAPI.utils.getConstants().

Simulation cadence: TICKS_PER_UPDATE#

The simulation advances in fixed ticks of 0.5 in-game seconds (GAME_SECONDS_PER_TICK in shared data β€” the whole simulation is built around a fixed tick length, so unlike RULES it is read once at app load and cannot be changed at runtime). Ticks are run in batches β€” one update β€” and the game loop's real-time interval scales with the batch size, so a bigger batch does more work per fire and fires less often. Game time passes at the same rate either way; what changes is how finely the simulation is sliced, and how much CPU it costs.

RULES.TICKS_PER_UPDATE has an entry per speed tier (slow, normal, fast, ultrafast), each with four values:

KeyWhat it controls
gameStateTicks of train, signal, and station simulation per update. Also sets the real-time interval between updates.
trainCoordsHow often train positions are recalculated. Positions always refresh on an update's final tick, so this only matters when it is lower than gameState.
popMovementHow often pops travelling through the network are advanced.
popMovementGeojsonHow often the pop movement map layer is rebuilt. Keep at or above gameState β€” rebuilding more than once per update is wasted work.
javascript
window.SubwayBuilderAPI.modifyConstants({
    TICKS_PER_UPDATE: {
        ultrafast: {
            gameState: 24, // Half the default batch: smoother motion, more CPU
            trainCoords: 12,
            popMovement: 12,
            popMovementGeojson: 24
        }
    }
});

Changes apply on the next update β€” no reload needed, and the loop picks up the new interval without the player touching the speed controls.

Every value must be a whole number of at least 1, and is capped at RULES.MAX_TICKS_PER_UPDATE (default 1000). Anything else (zero, a fraction, a string, NaN) falls back to that key's shipped default with a console warning: a fractional batch breaks the loop's end-of-batch check, and an unbounded one would run the whole batch inside a single frame and freeze the app. The cap itself is moddable β€” raise MAX_TICKS_PER_UPDATE if you genuinely want bigger batches, accepting that each game-loop fire stalls the app for as long as its batch takes.

Smaller batches at fast and ultrafast are the usual reason a mod tanks the frame rate on a large network β€” the simulation does the same amount of work per game-hour, but hands results to the renderer several times more often.

Game speed: GAME_SECONDS_PER_SECOND#

RULES.GAME_SECONDS_PER_SECOND sets how many in-game seconds pass per real second at each speed tier (slow, normal, fast, ultrafast). Unlike TICKS_PER_UPDATE, this actually changes how fast game time passes.

javascript
window.SubwayBuilderAPI.modifyConstants({
    GAME_SECONDS_PER_SECOND: {
        fast: 500, // Double-speed "fast" tier
        ultrafast: 2000 // 1 day = 43 real seconds
    }
});

Values are read live on every game-loop fire, so changes apply immediately without the player touching the speed controls. Every value must be a positive finite number; anything else falls back to that tier's shipped default with a console warning. `actions.setSpeedMultiplier` writes to these same values β€” modifyConstants sets absolute numbers, while setSpeedMultiplier scales the base value non-compoundingly.

Capacity warnings: WARNING_CONSTS#

RULES.WARNING_CONSTS controls train capacity warnings: a train boarding passengers with fewer empty spots than the active threshold triggers a warning, which expires after CAPACITY_WARNING_DURATION game seconds. Which threshold is active depends on the player's "Capacity Warning Threshold" feature flag: CAPACITY_WARNING_THRESHOLD (50) when enabled, CAPACITY_WARNING_THRESHOLD_OLD (100) when disabled (the default).

javascript
window.SubwayBuilderAPI.modifyConstants({
    WARNING_CONSTS: {
        CAPACITY_WARNING_THRESHOLD_OLD: 25, // Only warn when trains are nearly full
        CAPACITY_WARNING_DURATION: 60 * 30 // Keep warnings around for 30 game-minutes
    }
});

Track drawing snap radius: PIXEL_SNAP_THRESHOLD#

RULES.PIXEL_SNAP_THRESHOLD is the radius in screen pixels within which drawn track endpoints snap to existing tracks, stations, and endpoints. It is read live on every pointer move while drawing. The larger radius used during the tutorial is fixed β€” the tutorial depends on predictable snapping.

javascript
window.SubwayBuilderAPI.modifyConstants({
    PIXEL_SNAP_THRESHOLD: 20 // Easier snapping for touchpads / precision building
});

Station marker bullets: MARKER_ROUTE_BULLETS#

RULES.MARKER_ROUTE_BULLETS sizes the row of route bullets shown under station and station-group names on the map. The game projects the row's total width from the bullet text (TEXT_SIZE_MULTIPLIER, CHAR_WIDTH_RATIO) plus ICON_GAP_PX between bullets, and hides the whole row when the projection exceeds MAX_ROW_WIDTH_PX β€” the default cutoff of 300px hides the row at stations served by very many routes.

javascript
window.SubwayBuilderAPI.modifyConstants({
    MARKER_ROUTE_BULLETS: {
        MAX_ROW_WIDTH_PX: 500, // Show the bullet row at busier stations
        ICON_GAP_PX: 4 // More breathing room between bullets
    }
});

The values are read live, so changes apply the next time the markers re-render (panning, zooming, or any game-state change).

Station crowd ambience: SOUND_SETTINGS#

RULES.SOUND_SETTINGS controls the crowd noise heard when the camera is zoomed in near a constructed station with routes. Volume falls off linearly with the distance between the camera and the closest qualifying station (including the camera's height above the ground, so zooming out fades it too), reaching zero at NEAR_STATIONS_THRESHOLD meters and peaking at CROWD_MAX_VOLUME.

javascript
window.SubwayBuilderAPI.modifyConstants({
    SOUND_SETTINGS: {
        CROWD_MAX_VOLUME: 0.5, // Louder platforms
        NEAR_STATIONS_THRESHOLD: 2000 // Audible from further away
    }
});

The values are read live, so changes apply the next time the camera moves.

Bond financing: BOND_TYPES#

RULES.BOND_TYPES holds everything behind the three bond offerings (SMALL, MEDIUM, LARGE): the display name shown in the financial dashboard, the principal, the daily interest rate charged hourly on the remaining principal, the fraction of the principal repaid per game hour, and the previous-day revenue required to qualify.

javascript
window.SubwayBuilderAPI.modifyConstants({
    BOND_TYPES: {
        SMALL: {
            displayName: '$200M Bond', // Rename to match the new principal
            principal: 200_000_000,
            interestRate: 0.02, // Cheaper starter loans
            requiredDailyRevenue: 5_000_000 // Easier to qualify for
        }
    }
});

Changes apply to bonds issued after the override; already-issued bonds keep the terms they were issued with. The bond ids (small, medium, large) are structural β€” they key issued bonds in save data β€” so they cannot be changed, and bonds cannot be added or removed.

Read the current bond types (names included) with window.SubwayBuilderAPI.gameState.getBondTypes().

modifyPathfindingRules#

Modify the pathfinding and mode-choice rules (PATHFINDING_RULES) at runtime. Every value is a number; non-numeric or non-finite values and unknown keys are ignored with a console warning.

Changes take effect on the next commute simulation β€” no reload needed.

javascript
window.SubwayBuilderAPI.modifyPathfindingRules({
    MAX_TRANSFERS: 6, // Allow more transfers per journey
    MAX_WALK_TO_FROM_STATION: 60 * 30, // Shrink walk catchment to 30 min
    PARKING_COST: 10, // Make driving less attractive
    PERCEIVED_TIME: {
        WALK_MULTIPLIER: 1.6, // Pops perceive walking as more costly
        WAIT_MULTIPLIER: 1.5
    },
    DRIVING_TIMES: {
        HIGH_DEMAND: 2.0 // Heavier rush-hour traffic
    }
});

Read the current values (including any overrides already applied) with window.SubwayBuilderAPI.utils.getPathfindingRules().

modifyColors#

Modify the map visual constants (GAME_COLORS) at runtime: per-road-class widths and fade zoom windows, the fade windows and opacities of the flat area layers (buildings, parks, commercial/college areas), travel mode colors, the color scales for the building foundation depth, track speed, track TPH, and track elevation layers, the size and ring of the station marks (STATION_MARKER), and the zoom ramps of the schematic route diagram (PORTOLAN).

The area and building colors themselves are part of the map theme (the in-game custom color presets), not GAME_COLORS.

Objects are deep-merged, but arrays are replaced wholesale β€” to change one entry of TRACK_SPEED_COLORS, pass the whole array.

Call modifyColors at mod load time for everything to pick up the new values. Some values are only read when the map style is built or the city is loaded (e.g. AREA_VISUALS, BUILDING_FOUNDATION_DEPTHS, STATION_GROUP_DISTANCE_THRESHOLD), so if you change them mid-game, the save may need to be reloaded for the changes to take effect. Others (like ROAD_VISUALS and the track color scales) are read as the map redraws and apply without a reload.

javascript
window.SubwayBuilderAPI.modifyColors({
    ROAD_VISUALS: {
        highway: { zoomWindow: [10, 10.5] } // Keep highways visible further out
    },
    AREA_VISUALS: {
        buildings: { zoomWindow: [12, 12.5], opacity: 0.8 }, // Buildings appear earlier and more opaque
        parksSmall: { zoomWindow: null } // Small parks always visible (null = no fade)
    },
    MODE_COLORS: {
        transit: '#8B5CF6' // Purple transit trips in charts and the demand key
    },
    TRACK_SPEED_COLORS: [
        // Arrays are replaced wholesale, not merged
        { speedKmh: 50, hex: '#22C55E' },
        { speedKmh: 130, hex: '#A855F7' }
    ]
});

Read the current values with window.SubwayBuilderAPI.utils.getColors().

The station marks (STATION_MARKER)#

A station is drawn two different ways and both draw the same mark. Most stops are a dot; a stop whose lines fill a bundle of the schematic diagram gets an interchange pill instead, which stands exactly where that dot would have been and hides it. They sit side by side on the map, so what they have in common comes from one place:

javascript
window.SubwayBuilderAPI.modifyColors({
    STATION_MARKER: {
        DOT_DIAMETER: 9, // Fatter interchange pills
        BORDER_WIDTH: 2, // A heavier ring, on the dots and the pills alike
        BORDER_DARK: '#1b1b3a' // The ring on white dots and on every pill
    }
});

DOT_DIAMETER is the mark's full-size diameter in screen pixels, at zoom 14 and closer. It is the pill's height and twice its corner radius β€” a pill is a dot stretched along its bundle, and it overhangs the outermost ribbon at each end by half of this. Wider zooms scale it down by PORTOLAN.MARKER_ICON_SCALE_STOPS. It does not resize the plain station dots, which are sized in rem so they follow the interface scale rather than the camera.

BORDER_WIDTH is in screen pixels and holds at that width however far you zoom out. The dot gets it directly β€” its border does not scale with the camera β€” while the pill's ring is baked into an image that the marker scale ramp then shrinks, so the game draws it into that image already divided by the scale it is about to be shown at.

BORDER_DARK rings anything that has to separate from a pale background β€” every white interchange dot, every diagram pill, and route-colored dots in the dark theme. BORDER_LIGHT rings a route-colored dot in the light theme, where the ring reads as the page showing through rather than as an outline. Any CSS color works.

All four are read as the map redraws, so a change applies without a reload.

The route diagram (PORTOLAN)#

With visual interlining turned on, zooming out swaps the track-accurate route lines for a schematic diagram: routes that share a corridor are merged into one bundle of parallel ribbons, with an interchange marker where a station's lines fill the bundle. Zooming back in past PORTOLAN.ROUTE_ZOOM hands the map back to lines that follow the actual rails.

The diagram's geometry is computed by the game when the network changes, and mods cannot edit it. What GAME_COLORS.PORTOLAN controls is how that geometry is presented as you zoom: where the handoff sits, how wide a ribbon draws, how big a marker gets, and how far a bundle spreads. All of it is read as the map redraws, so it applies without a reload β€” the one exception is BANDS, whose min values decide how the diagram is sliced when it is built.

The interchange marker's own size and ring are not here: they belong to the station marks it replaces, under STATION_MARKER above. MARKER_ICON_SCALE_STOPS scales that mark; STATION_MARKER.DOT_DIAMETER is what it scales.

javascript
window.SubwayBuilderAPI.modifyColors({
    PORTOLAN: {
        ROUTE_ZOOM: 14, // Hand back to track-accurate lines earlier
        MARKER_ZOOM: 12, // Hold the interchange markers back until closer in
        RIBBON_WIDTH_STOPS: [
            // Arrays are replaced wholesale β€” pass every stop, in ascending zoom order
            { zoom: 10, width: 1.5 },
            { zoom: 14, width: 4 }
        ]
    }
});

Values between two stops are interpolated linearly; outside the listed stops the nearest stop's value holds flat, so a two-stop table is a ramp with a floor and a ceiling. A single stop is a constant.

Two things to know before reaching for these. BAND_GAP_SCALE is documentation, not a control β€” the spacing it describes is already baked into the diagram's geometry, and the renderer deliberately does not apply it a second time. And the route lettering that rides the ribbons was laid out against the shipped OFFSET_SCALE_STOPS ramp, so widening or narrowing that ramp slides the lettering off the line it names.

Game Rules Reference#

Every rule in RULES, its default value, and what it does. This reference is generated from the game's own rule definitions, so it always matches the shipped defaults.

KeyDefaultDescription
MIN_SINGLE_TRACK_LENGTH10Minimum length in meters of a track in a track group with only one lane of tracks.
MIN_TRACK_LENGTH10Minimum length in meters of a parallel/quad track when auto-crossovers are enabled.
MIN_ADVANCED_TRACK_LENGTH10Override for the minimum track length when advanced mode is enabled β€” power users get the shorter limit on any lane type.
MAX_TRACK_LENGTH10000Maximum length in meters of a single track.
COORD_PRECISION6Decimal places coordinates are rounded to before being stored (6 is roughly 0.1 meters). Must be a whole number from 0 to 15. Existing geometry is not re-rounded when this changes.
PIXEL_SNAP_THRESHOLD10Radius in screen pixels within which drawn track endpoints snap to existing tracks, stations, and endpoints.
STATION_LENGTH_BUFFER4Extra meters of platform required on top of the raw car-length times car-count.
TRACK_WIDTH1.435Width in meters of a single track.
SCISSORS_CROSSOVER_LENGTH40Length in meters of a scissors crossover between parallel tracks.
OFFSET_TRANSITION_LENGTH50Maximum length in meters of a lateral track-spacing transition (station flare). e.g. quad rails returning from an island-platform express station's widespacing to normal running spacing.
CANT_MAX_APPLIED_MM150Fallback maximum applied cant (superelevation) in millimeters on running track, used when a train type does not define stats.maxCantMm. Real-world mainline practice is ~150mm.
CANT_RAMP_MM_PER_S45Maximum rate of cant change in millimeters per second of travel time. Lower = longer cant transitions and slower curve entries next to flat track.
CANT_MAX_STOPPED_MM150Maximum applied cant in millimeters on track where trains may come to a stop (junction and station approaches). Vehicle cant above this only materializes on open running track.
CANT_STOP_ZONE_LENGTH_M200Length in meters of the approach to a junction or station treated as a possible stopping zone for the cant cap.
CANT_STATION_MAX_MM0Maximum applied cant in millimeters through station tracks β€” platforms must be level.
CANT_STREET_RUNNING_MAX_MM0Maximum applied cant in millimeters for street-running track β€” rail set into a roadway is flat.
OFFSET_TRANSITION_RADIUS_MULTIPLIER10How forgivingly curvature inside a spacing-transition zone is judged for speed limits (higher = more forgiving).
STATION_HEIGHT4Distance in meters from the station floor to the roof. Used to check if stations are colliding with each other.
TUNNEL_HEIGHT5Distance in meters from the bottom to the top of the tunnel.
V_MERGE_SIGNAL_WINDOW_LENGTH200Length in meters of the signal window of a v-merge signal.
DIAMOND_SIGNAL_WINDOW_LENGTH10Length in meters of the signal window of a diamond signal, for each track where the center is where the diamond meets.
GRADE_CROSSING_SIGNAL_RADIUS200Half-width in meters of each grade-crossing signal's track coverage.
MAX_JERK0.3Maximum change in acceleration of a train in m/sΒ³.
ADDITIONAL_WARNING_WINDOW_LENGTH2Length in meters of the first warning window behind a train. This is a gap used to keep trains from crashing into each other.
ADDITIONAL_EXTRA_WARNING_WINDOW_LENGTH100Length in meters of the second warning window used to stop trains from running into each other. This is a gap used to keep trains from crashing into each other.
DEFAULT_TICKET_COST3Default transit fare in dollars for a new game.
COMMUTE_INTERVAL_LENGTH900Commuters are released in intervals of this many seconds.
SCHEDULE_TRANSITION_WINDOW_MAX_LENGTH5400Maximum time in seconds it can take a route to add/remove trains as the demand windows change. This only effects very long routes where it can take a while to add/remove trains.
SCHEDULING_SIMULATION_INTERVAL_MINUTES10Granularity in minutes of the train-scheduling analysis/debug simulator.
MAX_ROUTE_SIMULATION_DURATION86400Won't simulate a route longer than this many seconds, to prevent infinite loops in route time estimation.
MAX_WAIT_TIME840Maximum time in seconds a train waits at a terminal.
STARTING_MONEY3000000000Money in dollars the player starts a new game with.
STARTING_TRAIN_CARS30Train cars the player starts a new game with.
FARE_MULTIPLIER365For every dollar payed in fares, the player earns this many dollars.
ACCEPTABLE_SPEED_MARGIN0.01Margin of error for the train's speed when calculating movements.
MIN_ELEVATION-100Minimum construction elevation in meters.
MAX_ELEVATION20Maximum construction elevation in meters.
TIME_TO_KEEP_COMMUTE_DATA86400How long in seconds to keep completed commutes data.
STUCK_TRAIN_TIMEOUT900If a train hasn't moved for this many seconds, consider it stuck and remove it.
IS_DELAYED_THRESHOLD60A train running this many seconds behind schedule is shown as delayed, or ahead of it as early. Display only, no effect on the simulation.
MAX_TERMINUS_ALTERNATE_DETOUR400Max extra distance in meters (arrival + departure combined) an alternate terminus platform may add over the canonical platform before it is excluded as a turnback target.
COMMUTE_DATA_FILTER_INTERVAL3600Filter completed commutes every this many seconds.
INFRASTRUCTURE_COST_CHARGE_INTERVAL300How often in game seconds to charge infrastructure maintenance costs.
EXTRA_TRACK_STATION_MAINTENANCE_COST_MUTLIPLIER0.25Maintenance cost multiplier for every extra track lane in a station.
OPERATIONAL_COST_CHARGE_INTERVAL900How often in game seconds to charge operational costs.
TRAIN_SCHEDULE_TRANSITION_WINDOW1800Transition window in seconds before and after demand level changes.
BUILDING_FOUNDATION_GAP3Minimum gap in meters between the bottom of a building foundation and the tunnel.
WALKING_SPEED1Walking speed in m/s when using a straight-line approximation instead of the actual path.
WALKING_SPEED_ACCURATE_PATH1.5Walking speed in m/s when using accurate path distance.
GRADE_CROSSING_APPROACH_SECONDS20Gates start closing this many seconds before a train is predicted to arrive at the crossing.
GRADE_CROSSING_CLEAR_SECONDS15Gates remain closed for this many seconds after a train's tail clears the crossing (post-pass hold).
MAX_ROUTE_NAME_LENGTH16Maximum number of characters in a route bullet name.
MAX_ROUTE_FULL_NAME_LENGTH48Maximum number of characters in a route's optional full name.
MAX_TICKS_PER_UPDATE1000Ceiling on every TICKS_PER_UPDATE value. Raise it to allow bigger batches, at the risk of long game-loop stalls. Must be a whole number of at least 1.

CONSTRUCTION_COSTS#

Multipliers and line items for construction and maintenance costs.

KeyDefaultDescription
CONSTRUCTION_COSTS.TBM_MIN_SEGMENT_COST40000000Fixed TBM mobilization charge in dollars applied to every contiguous deep-bore blueprint segment.

CONSTRUCTION_COSTS.TUNNEL#

Track cost multipliers by lane count.

KeyDefaultDescription
CONSTRUCTION_COSTS.TUNNEL.SINGLE_MULTIPLIER0.75Cost multiplier for single-lane track.
CONSTRUCTION_COSTS.TUNNEL.QUAD_MULTIPLIER1.5Cost multiplier for quad-lane track.

CONSTRUCTION_COSTS.STATION#

Station cost multipliers.

KeyDefaultDescription
CONSTRUCTION_COSTS.STATION.SINGLE_MULTIPLIER0.75Cost multiplier for single-track stations.
CONSTRUCTION_COSTS.STATION.QUAD_MULTIPLIER1.5Cost multiplier for quad-track stations.
CONSTRUCTION_COSTS.STATION.LENGTH_COST_FACTOR0.25Fraction of station cost that scales with capacity in cars (0 = disabled, 1 = fully linear).

CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS#

Cost multipliers by construction elevation type.

KeyDefaultDescription
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.DEEP_BORE4.5Deep-bore tunnel cost multiplier.
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.STANDARD_TUNNEL2Standard tunnel cost multiplier.
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.CUT_AND_COVER1Cut-and-cover cost multiplier (baseline).
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.TRENCHED0.5Open-top trench cost multiplier.
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.AT_GRADE0.35At-grade (surface) cost multiplier.
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.RAMP0.5Low open ramp / embankment (1-5 m) cost multiplier.
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.ELEVATED0.8Elevated viaduct cost multiplier.

CONSTRUCTION_COSTS.WATER_MULTIPLIERS#

Multipliers for construction over water, applied on top of the base elevation multiplier.

KeyDefaultDescription
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.DEEP_BORE1.44444Deep-bore over water.
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.STANDARD_TUNNEL1.5Standard tunnel over water.
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.CUT_AND_COVER3Cut-and-cover over water.
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.TRENCHED100Trenched cannot be built over water.
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.AT_GRADE100At-grade cannot be built over water.
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.RAMP100Low ramp / embankment cannot be built over water.
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.ELEVATED2.5Elevated viaduct over water.

CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS#

Elevation boundaries in meters between construction types.

KeyDefaultDescription
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.DEEP_BORE-100Below this elevation is deep bore.
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.STANDARD_TUNNEL-24Standard tunnel threshold.
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.CUT_AND_COVER-10Cut-and-cover threshold.
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.TRENCHED-4Trenched threshold.
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.AT_GRADE-1Boundary between trenched and at-grade.
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.RAMP0Boundary between at-grade and the open ramp / embankment.
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.ELEVATED5At or above this elevation is an elevated viaduct.

CONSTRUCTION_COSTS.GRADE_CROSSING#

Grade crossing cost multipliers.

CONSTRUCTION_COSTS.GRADE_CROSSING.ROAD_CLASS_MULTIPLIER#

Cost multiplier by road class.

KeyDefaultDescription
CONSTRUCTION_COSTS.GRADE_CROSSING.ROAD_CLASS_MULTIPLIER.minor1Minor road crossing.
CONSTRUCTION_COSTS.GRADE_CROSSING.ROAD_CLASS_MULTIPLIER.medium1.25Medium road crossing.
CONSTRUCTION_COSTS.GRADE_CROSSING.ROAD_CLASS_MULTIPLIER.major1.5Major road crossing.
CONSTRUCTION_COSTS.GRADE_CROSSING.ROAD_CLASS_MULTIPLIER.highway2Highway crossing.

CONSTRUCTION_COSTS.GRADE_CROSSING.LANE_COUNT_MULTIPLIER#

Cost multiplier by number of track lanes crossing the road.

KeyDefaultDescription
CONSTRUCTION_COSTS.GRADE_CROSSING.LANE_COUNT_MULTIPLIER.10.75Single lane.
CONSTRUCTION_COSTS.GRADE_CROSSING.LANE_COUNT_MULTIPLIER.21Two lanes.
CONSTRUCTION_COSTS.GRADE_CROSSING.LANE_COUNT_MULTIPLIER.41.5Four lanes.

CONSTRUCTION_COSTS.BUILDING_OVERPASS#

Costs and limits for elevated tracks spanning over buildings.

KeyDefaultDescription
CONSTRUCTION_COSTS.BUILDING_OVERPASS.VERTICAL_CLEARANCE3Vertical clearance in meters required between building roof and track deck.
CONSTRUCTION_COSTS.BUILDING_OVERPASS.COST_PREMIUM3Cost premium of a special long-span structure vs a standard elevated segment.
CONSTRUCTION_COSTS.BUILDING_OVERPASS.STANDARD_SPAN35Reference span in meters; superstructure cost scales with span / STANDARD_SPAN.
CONSTRUCTION_COSTS.BUILDING_OVERPASS.PIER_COST_PER_METER_HEIGHT200000Straddle-pier cost per meter of building height, per building crossed.
CONSTRUCTION_COSTS.BUILDING_OVERPASS.MIN_BUILDING_HEIGHT3Floor for building height in meters when height data is missing or zero.

YARDS#

Train yard rules (yards-enabled saves only).

KeyDefaultDescription
YARDS.BERTH_OVERHEAD_M10Clearance in meters added to each train length when packing trains onto berth tracks.
YARDS.MIN_YARD_TRACK_LENGTH_M50Minimum length in meters of a drawn yard track.
YARDS.DEFAULT_YARD_SPEED_LIMIT4.47Speed cap in m/s on yard trackage (berths, leads, throat connectors) for train types that do not set their own yardSpeedLimit. Defaults to 4.47 m/s (10 mph) - non-revenue yard moves run at walking pace.
YARDS.YARD_TRACK_COST_MULTIPLIER0.5Construction cost multiplier for yard track relative to running track. Yard trackage is priced exactly like running track of the same length, elevation and lane count, then scaled by this β€” non-revenue storage track is cheaper to build than mainline.

STREET_RUNNING#

Rules for street-running (tram) tracks.

KeyDefaultDescription
STREET_RUNNING.ROAD_SNAP_DISTANCE15Distance in meters within which drawn tracks snap to a road.
STREET_RUNNING.ROAD_SNAP_BREAKAWAY_DISTANCE35Distance in meters at which a snapped track breaks away from the road.
STREET_RUNNING.COST_MULTIPLIER3.5Construction cost multiplier for street-running track.
STREET_RUNNING.EXCLUSIVE_ROW_COST_MULTIPLIER1.5Additional cost multiplier for an exclusive right-of-way.
STREET_RUNNING.ELEVATION_TOLERANCE0.01Tracks within this many meters of elevation 0 are considered at road level (street-running).

ROAD_SPEED_LIMITS#

Speed limits in m/s by road class, used only for street-running trains.

KeyDefaultDescription
ROAD_SPEED_LIMITS.highway29.1Highway speed limit.
ROAD_SPEED_LIMITS.major20.1Major road speed limit.
ROAD_SPEED_LIMITS.medium15.6Medium road speed limit.
ROAD_SPEED_LIMITS.minor11.2Minor road speed limit.

MARKER_ROUTE_BULLETS#

Sizing for the route bullet row shown under station and station-group markers on the map.

KeyDefaultDescription
MARKER_ROUTE_BULLETS.TEXT_SIZE_MULTIPLIER0.8Bullet text size as a fraction of the bullet icon size.
MARKER_ROUTE_BULLETS.CHAR_WIDTH_RATIO0.62Approximate width of one bullet character as a fraction of the font size, used to project bullet widths.
MARKER_ROUTE_BULLETS.ICON_GAP_PX2Horizontal gap in pixels between bullets in the row.
MARKER_ROUTE_BULLETS.MAX_ROW_WIDTH_PX300The entire bullet row is hidden when its projected width exceeds this many pixels.

SOUND_SETTINGS#

Station crowd ambience settings.

KeyDefaultDescription
SOUND_SETTINGS.NEAR_STATIONS_THRESHOLD1000Distance in meters around the camera center within which stations are checked for crowd ambience. Volume falls off linearly with distance and reaches zero here.
SOUND_SETTINGS.CROWD_MAX_VOLUME0.25Maximum volume (0-1) of the station crowd ambience, reached when fully zoomed in on a constructed station with routes.

WARNING_CONSTS#

Thresholds and lifetimes for train capacity warnings.

KeyDefaultDescription
WARNING_CONSTS.CAPACITY_WARNING_THRESHOLD50A train with fewer than this many empty spots triggers a capacity warning, when the reduced-threshold feature flag is enabled. Lower = warns only when nearly full.
WARNING_CONSTS.CAPACITY_WARNING_THRESHOLD_OLD100The capacity warning threshold used when the reduced-threshold feature flag is disabled (the default).
WARNING_CONSTS.CAPACITY_WARNING_DURATION900How long in game seconds a capacity warning is kept before it expires.

BOND_TYPES#

The bond offerings players can issue for financing. Interest accrues on the remaining principal. Already-issued bonds keep the terms they were issued with.

BOND_TYPES.SMALL#

Small bond ($100M at 6%).

KeyDefaultDescription
BOND_TYPES.SMALL.displayName$100M BondDisplay name shown in the financial dashboard.
BOND_TYPES.SMALL.principal100000000Amount borrowed in dollars.
BOND_TYPES.SMALL.interestRate0.06Daily interest rate on the remaining principal (0.06 = 6% per day), charged hourly at 1/24 of the daily rate.
BOND_TYPES.SMALL.principalPaymentRate0.001Fraction of the original principal repaid every game hour.
BOND_TYPES.SMALL.requiredDailyRevenue10000000Previous day's revenue in dollars required to issue this bond.

BOND_TYPES.MEDIUM#

Medium bond ($500M at 4%).

KeyDefaultDescription
BOND_TYPES.MEDIUM.displayName$500M BondDisplay name shown in the financial dashboard.
BOND_TYPES.MEDIUM.principal500000000Amount borrowed in dollars.
BOND_TYPES.MEDIUM.interestRate0.04Daily interest rate on the remaining principal (0.04 = 4% per day), charged hourly at 1/24 of the daily rate.
BOND_TYPES.MEDIUM.principalPaymentRate0.0008Fraction of the original principal repaid every game hour.
BOND_TYPES.MEDIUM.requiredDailyRevenue100000000Previous day's revenue in dollars required to issue this bond.

BOND_TYPES.LARGE#

Large bond ($1B at 2%).

KeyDefaultDescription
BOND_TYPES.LARGE.displayName$1B BondDisplay name shown in the financial dashboard.
BOND_TYPES.LARGE.principal1000000000Amount borrowed in dollars.
BOND_TYPES.LARGE.interestRate0.02Daily interest rate on the remaining principal (0.02 = 2% per day), charged hourly at 1/24 of the daily rate.
BOND_TYPES.LARGE.principalPaymentRate0.0006Fraction of the original principal repaid every game hour.
BOND_TYPES.LARGE.requiredDailyRevenue200000000Previous day's revenue in dollars required to issue this bond.

GAME_SECONDS_PER_SECOND#

How many in-game seconds pass per real second at each speed tier. Values must be positive finite numbers; bad values fall back to the shipped defaults.

KeyDefaultDescription
GAME_SECONDS_PER_SECOND.slow1In-game seconds per real second at slow speed (real time).
GAME_SECONDS_PER_SECOND.normal25In-game seconds per real second at normal speed.
GAME_SECONDS_PER_SECOND.fast250In-game seconds per real second at fast speed.
GAME_SECONDS_PER_SECOND.ultrafast500In-game seconds per real second at ultrafast speed.

TICKS_PER_UPDATE#

How many simulation ticks are batched into one game-loop update, per speed tier. Every value must be a whole number of at least 1. Raising them trades smoothness for CPU cost; it does not change how fast game time passes, because the interval between updates scales with gameState.

TICKS_PER_UPDATE.slow#

Tick batching at the slow speed tier.

KeyDefaultDescription
TICKS_PER_UPDATE.slow.gameState1Ticks of train, signal, and station simulation run per update at slow speed. The real-time interval between updates scales with this.
TICKS_PER_UPDATE.slow.trainCoords1Ticks between train coordinate recalculations at slow speed. Coordinates also refresh on the final tick of every update.
TICKS_PER_UPDATE.slow.popMovement2Ticks between updates to pops travelling through the network at slow speed.
TICKS_PER_UPDATE.slow.popMovementGeojson2Ticks between rebuilds of the pop movement map layer at slow speed. Should not be lower than gameState.

TICKS_PER_UPDATE.normal#

Tick batching at the normal speed tier.

KeyDefaultDescription
TICKS_PER_UPDATE.normal.gameState1Ticks of train, signal, and station simulation run per update at normal speed. The real-time interval between updates scales with this.
TICKS_PER_UPDATE.normal.trainCoords1Ticks between train coordinate recalculations at normal speed. Coordinates also refresh on the final tick of every update.
TICKS_PER_UPDATE.normal.popMovement2Ticks between updates to pops travelling through the network at normal speed.
TICKS_PER_UPDATE.normal.popMovementGeojson2Ticks between rebuilds of the pop movement map layer at normal speed. Should not be lower than gameState.

TICKS_PER_UPDATE.fast#

Tick batching at the fast speed tier.

KeyDefaultDescription
TICKS_PER_UPDATE.fast.gameState16Ticks of train, signal, and station simulation run per update at fast speed. The real-time interval between updates scales with this.
TICKS_PER_UPDATE.fast.trainCoords8Ticks between train coordinate recalculations at fast speed. Coordinates also refresh on the final tick of every update.
TICKS_PER_UPDATE.fast.popMovement8Ticks between updates to pops travelling through the network at fast speed.
TICKS_PER_UPDATE.fast.popMovementGeojson16Ticks between rebuilds of the pop movement map layer at fast speed. Should not be lower than gameState.

TICKS_PER_UPDATE.ultrafast#

Tick batching at the ultrafast speed tier.

KeyDefaultDescription
TICKS_PER_UPDATE.ultrafast.gameState48Ticks of train, signal, and station simulation run per update at ultrafast speed. The real-time interval between updates scales with this.
TICKS_PER_UPDATE.ultrafast.trainCoords24Ticks between train coordinate recalculations at ultrafast speed. Coordinates also refresh on the final tick of every update.
TICKS_PER_UPDATE.ultrafast.popMovement24Ticks between updates to pops travelling through the network at ultrafast speed.
TICKS_PER_UPDATE.ultrafast.popMovementGeojson48Ticks between rebuilds of the pop movement map layer at ultrafast speed. Should not be lower than gameState.

Pathfinding Rules Reference#

Every rule in PATHFINDING_RULES, its default value, and what it does. Also generated from the game's rule definitions.

KeyDefaultDescription
MAX_TRANSFERS4Maximum number of transfers per transit journey. Routes can be split into multiple internal RAPTOR routes, so a transfer can also mean two legs on the same route.
MAX_WALK_TO_FROM_STATION2700Maximum walking time from origin/destination to the first/last station, in seconds. Defines the walk catchment area of a station (scaled by station type catchment multipliers).
MAX_DRIVE_TO_FROM_STATION420Maximum driving time from origin to the first station for drive-to-station (park & ride) access, in seconds. Roughly 4-5 km at urban speeds.
MAX_TRANSFER_WALKING_TIME600Maximum walking time between stations when transferring, in seconds.
FUTURE_CYCLE_TIME_OFFSET7200How many seconds in advance train departure times are pre-calculated for pathfinding.
MIN_FUTURE_CYCLES2Minimum number of future service cycles to calculate. Ensures loop-around journeys work on very long routes (over 2 hours).
DRIVING_COST_PER_KM0.65Perceived cost of driving per kilometer in dollars, not including parking. Based on US BTS data, slightly reduced to nerf cars.
MIN_SENSIBLE_DRIVING_DISTANCE1000Minimum sensible driving distance in meters. Shorter drives get an extra penalty for the hassle of using a car on a very short trip.
PARKING_TIME180Time spent parking at each end of a car journey in seconds (finding a spot and walking from it).
PARKING_COST5Cost of parking in dollars per day (average in urban areas).
ARRIVAL_GAP50Pops arrive at the station this many seconds before their train departs.
RANGE_QUERY_WINDOW1800rRAPTOR departure range window in seconds. The pathfinder considers journeys departing up to this much after the requested time, so a later direct train can beat an immediate two-leg trip. Larger windows are more accurate but cost more per query.
MAX_RANGE_DEPARTURES24Cap on how many departure times a single range query evaluates.
INCOME_MEAN60000Mean of the normal distribution used to generate pop incomes, in dollars per year.
INCOME_STD_DEV25000Standard deviation of the pop income distribution, in dollars per year.
MINIMUM_INCOME15000Floor for generated pop incomes, in dollars per year.
MAXIMUM_INCOME200000Ceiling for generated pop incomes, in dollars per year.
HOURS_WORKED_PER_YEAR1860Hours worked per year, used to convert annual income into an hourly value of time for mode choice.
AIRPORT_INCOME_MULTIPLIER1.5Income multiplier for airport pops (air travelers skew wealthier).
AIRPORT_PARKING_COST_MULTIPLIER5Airport pops pay this many times the normal PARKING_COST.
COLLEGE_INCOME_MULTIPLIER0.6Income multiplier for college pops (students earn less).
MIN_TRANSIT_CHOICE10If fewer than this many people at a demand point choose transit, they are all moved to their second choice (driving or walking) to reduce transit pop count and improve performance.

PERCEIVED_TIME#

Weights for how pops perceive each component of a transit journey, relative to a minute riding the train (1.0). Journeys are ranked and priced into mode choice by this perceived ("generalized") time. Values come from the Wardman et al. 2026 meta-analysis (Transportation Research Part A).

KeyDefaultDescription
PERCEIVED_TIME.WALK_MULTIPLIER1.39How much worse a minute of walking feels than a minute riding. Meta-model implied multiplier for urban metro riders in the modern (smartphone) era.
PERCEIVED_TIME.AIRPORT_WALK_MULTIPLIER1.87Walk weight for airport pops, replacing WALK_MULTIPLIER (luggage makes walking feel worse β€” clock time is unchanged).
PERCEIVED_TIME.WAIT_MULTIPLIER1.37How much worse a minute of waiting at a station feels than a minute riding.
PERCEIVED_TIME.DEPARTURE_SHIFT_MULTIPLIER0.4Cost per second of departing later than requested (displacement time), e.g. a pop waiting at home for a better train.
PERCEIVED_TIME.CONGESTED_DRIVING_MULTIPLIER1.33Perception premium on a minute driven in heavy metropolitan congestion.
PERCEIVED_TIME.CONGESTION_FULL_AT_MULTIPLIER2Traffic multiplier at which the whole car trip counts as congested. Keep in sync with DRIVING_TIMES.HIGH_DEMAND.
PERCEIVED_TIME.PARKING_SEARCH_MULTIPLIER1.6Perception premium on time spent parking (finding a spot, walking from it).

DRIVING_TIMES#

Traffic multipliers applied to driving times by time-of-day demand level. A value of 1.5 means drives take 50% longer than free-flow.

KeyDefaultDescription
DRIVING_TIMES.VERY_LOW_DEMAND0.8Overnight traffic multiplier (faster than free-flow baseline).
DRIVING_TIMES.LOW_DEMAND0.9Early morning / late evening traffic multiplier.
DRIVING_TIMES.LOW_MEDIUM_DEMAND1Midday baseline traffic multiplier.
DRIVING_TIMES.MEDIUM_DEMAND1.25Shoulder-peak traffic multiplier.
DRIVING_TIMES.HIGH_DEMAND1.5Rush hour traffic multiplier.

Map Visuals Reference#

Every value in GAME_COLORS, its default, and what it does. Also generated from the game's own definitions.

KeyDefaultDescription
MIN_TRACK_BASE_PIXEL_WIDTH1.5Minimum on-screen width in pixels of track lines when zoomed out.
MIN_ROAD_PIXEL_WIDTH0.5Minimum on-screen width in pixels of road lines, so roads stay visible when zoomed far out instead of shrinking to sub-pixel widths.
MIN_LINE_WIDTH_MODIFIER1The modifier applied to line widths when zoomed out. Output is track width times this value. Currently unused by the game.
LINE_WIDTH_MODIFIER2The modifier applied to line widths when zoomed in. Output is track width times this value. Currently unused by the game.
OFFSET_MULTIPLIER1.5How much space should be between parallel lines. Currently unused by the game.
JOURNEY_LINE_WIDTH_MODIFIER2The modifier applied to journey line widths. Output is track width times this value. Currently unused by the game.
STATION_MARKER_WIDTH_MODIFIER5The modifier applied to station marker widths. Output is track width times this value. Currently unused by the game.
TRAIN_PATH_WIDTH_MODIFIER1.25The modifier applied to train path widths. Output is track width times this value. Currently unused by the game.
PICKABLE_TRACK_WIDTH_MODIFIER3The modifier applied to track widths on the track speeds, elevations, types, and TPH layers to make tracks easier to click.
SIMPLE_ROUTE_ZOOM13The zoom level where the map switches from showing routes by track to either simple or interlined routes.
STATION_GROUP_DISTANCE_THRESHOLD150Distance in meters within which nearby stations are combined into one station group marker.
STATION_MARKER_HOVER_RESIZE1.15The factor by which station markers are resized when hovered.
RAIL_CROSSING_MARKER_SIZE_PX20Width and height in screen pixels of the traffic-light marker drawn where two railways cross at grade. Read as the map redraws.

ROAD_STRUCTURES#

Rendering of road structures: how bridge and tunnel road segments are drawn differently from surface roads. All values are read as the map redraws.

KeyDefaultDescription
ROAD_STRUCTURES.TUNNEL_ALPHA128Alpha (0 to 255) of tunnel road segments, drawn semi-transparent so they read as underground rather than surface roads.
ROAD_STRUCTURES.BRIDGE_OPACITY0.5Opacity (0 to 1) of road bridges (casing and fill), semi-transparent so tracks built parallel underneath a bridge remain visible.
ROAD_STRUCTURES.BRIDGE_CASING_WIDTH_EXTRA4Extra width in meters of the bridge casing beyond the road width, forming the border that makes a bridge read as elevated.

MODE_COLORS#

Colors for travel modes, used in charts and the demand map key.

KeyDefaultDescription
MODE_COLORS.transit"#0000FF"Transit trips.
MODE_COLORS.driving"#FF0000"Driving trips.
MODE_COLORS.walking"#00FF00"Walking trips.
MODE_COLORS.unknown"#6e6e6e"Fallback for trips with an unknown mode.

TRACK_ARROWS#

The directional arrows drawn along tracks and routes when the track arrows layer is on and the camera is zoomed in. Track arrows sit on the rails themselves; route arrows are the bigger set drawn on route lines. All values are read as the map redraws.

KeyDefaultDescription
TRACK_ARROWS.ENABLED_ZOOM15.5The zoom at which the arrows appear. Further out they are too small to read.
TRACK_ARROWS.SEGMENT_INTERVAL5Draw an arrow every this many subdivided segments. Larger means more spacing between arrows.
TRACK_ARROWS.LENGTH2Arrow length as a fraction of a segment. Larger means bigger arrows.
TRACK_ARROWS.TRACK_WIDTH4Width in meters of the arrows drawn on tracks.
TRACK_ARROWS.TRACK_WIDTH_MIN_PIXELS4Minimum on-screen width in pixels of the arrows drawn on tracks.
TRACK_ARROWS.ROUTE_WIDTH6Width in meters of the arrows drawn on routes.
TRACK_ARROWS.ROUTE_WIDTH_MIN_PIXELS6Minimum on-screen width in pixels of the arrows drawn on routes.

PORTOLAN#

The Portolan schematic diagram for visually interlined routes: the merged, offset route ribbons the map draws instead of track-accurate lines when visual interlining is on and the camera is zoomed out. These control how the diagram is presented, not the geometry itself, which comes from the diagram build.

KeyDefaultDescription
PORTOLAN.ROUTE_ZOOM15Above this zoom the map draws its own track-accurate route lines instead of the diagram, and the diagram interchange markers stop drawing. Deliberately higher than SIMPLE_ROUTE_ZOOM: the ribbons stay legible much further in than the plain simplified lines they replace, so the diagram is worth holding onto longer. Read as the map redraws.
PORTOLAN.MARKER_ZOOM11The zoom at which the diagram interchange markers appear. They all appear at once rather than ranking in: a half-drawn set of stops reads as missing data, not as the important ones. They shrink at the outer end instead, via MARKER_ICON_SCALE_STOPS. Read as the map redraws.

PORTOLAN.CATERPILLARS#

The caterpillars: the route bullets chained along the ribbons between stations, and the route-name lettering that runs along a ribbon when a name does not fit in a bullet. These only show when visual interlining is enabled.

KeyDefaultDescription
PORTOLAN.CATERPILLARS.ENABLEDtrueWhether the caterpillars draw at all. Off hides the bullets and the route-name lettering; the ribbons and interchange pills stay. Read as the map redraws.
PORTOLAN.CATERPILLARS.MIN_ZOOM12The zoom at which the caterpillars appear. Further out the strip is too small to read and the bullets would only clutter the ribbons. Read as the map redraws.
PORTOLAN.CATERPILLARS.MAX_BULLET_CHARS1Longest route name, in characters, that still draws as a badge riding the caterpillar chain. One-character names and two-digit codes keep the 1:1 disc; anything else up to this length draws as a word pill just under BULLET_HEIGHT, and longer names become the route-name lettering running along the ribbon. Read as the map redraws.
PORTOLAN.CATERPILLARS.BULLET_HEIGHT10Bullet height in CSS pixels at full zoom. Everything else about a bullet grows from this: non-circular shapes widen from it to hold their glyphs, and a route name too long for a disc becomes a word pill at nine tenths of this height. Drawn bullets are cached per height, so a change applies as bullets rasterise.
PORTOLAN.CATERPILLARS.BULLET_FONT"600 7.5px system-ui, sans-serif"Canvas font shorthand for the glyphs inside the bullets. Sized against the default BULLET_HEIGHT, so scale the pixel size along with it. Drawn bullets are cached per font, so a change applies as bullets rasterise.
PORTOLAN.CATERPILLARS.TEXT_FAMILY"system-ui, sans-serif"Font family of the route-name lettering riding the ribbons. Also the face the glyph advances are measured with, so the lettering is always spaced by the font it draws in.
PORTOLAN.CATERPILLARS.TEXT_WEIGHT"italic 700"CSS font weight, with optional style, of the route-name lettering - italic the way a road map sets a route name apart from the upright station names around it.
PORTOLAN.CATERPILLARS.HOVER_ALPHA100Opacity, 0-255, of a caterpillar under the pointer - every bullet and letter of the hovered route fades to this, the click-me feedback for opening its route details. Read as the map redraws.

PORTOLAN.CATERPILLARS.TEXT_SIZE_STOPS#

Route-name lettering size in pixels by zoom, interpolated linearly and held flat outside the listed stops. Read as the map redraws.

json
[
    {
        "zoom": 12,
        "size": 10
    },
    {
        "zoom": 16,
        "size": 13
    }
]

PORTOLAN.BANDS#

Zoom bands, widest-zoom first. The diagram build emits one full copy of the network per band, progressively less detailed as you zoom out, and exactly one band may draw at a time - two bands drawn together doubles every ribbon. A camera at zoom z draws the first band whose min is at or below z, so min is what selects a band and max is documentation. The keys are fixed by the build format; changing them leaves the renderer asking for band data that was never emitted. Changing min re-slices the diagram, so it takes effect on the next diagram build rather than immediately.

json
[
    {
        "key": 15,
        "min": 15,
        "max": 24
    },
    {
        "key": 14,
        "min": 14,
        "max": 15
    },
    {
        "key": 13,
        "min": 13,
        "max": 14
    },
    {
        "key": 0,
        "min": 0,
        "max": 13
    }
]

PORTOLAN.BAND_GAP_SCALE#

Slot-pitch multiplier per band, keyed by band key: how far apart two routes sharing a corridor sit in that band, relative to the innermost band. Reference values only. The build has already baked them into each vertex offset, so this table is for reasoning and assertions and the renderer must not apply it again - doing so narrows every bundle twice over.

json
{
    "0": 0.75,
    "13": 0.85,
    "14": 0.95,
    "15": 1
}

PORTOLAN.OFFSET_SCALE_STOPS#

How far a ribbon sits from its bundle centre, as a fraction of the offset baked into the build, interpolated linearly by zoom and held flat outside the listed stops. Bundles narrow as you zoom out so they never swamp the map. Note that the route-name lettering rides the ribbons using a z11 to z14 ramp fixed by the build, so moving these stops drifts the lettering off the line it labels. Read as the map redraws.

json
[
    {
        "zoom": 11,
        "scale": 1
    },
    {
        "zoom": 14,
        "scale": 0.8
    }
]

PORTOLAN.RIBBON_WIDTH_STOPS#

On-screen ribbon width in pixels by zoom, interpolated linearly and held flat outside the listed stops. Multiplied by the per-mode width from the diagram build style, so a metro ribbon and a commuter-rail ribbon keep their relative weights. The station dots also ride this curve: their diameter scales by the ribbon width relative to its zoom-14 value, so the marks thicken with the lines. Read as the map redraws.

json
[
    {
        "zoom": 7,
        "width": 1
    },
    {
        "zoom": 10,
        "width": 1.5
    },
    {
        "zoom": 13,
        "width": 2
    },
    {
        "zoom": 15,
        "width": 3
    },
    {
        "zoom": 16,
        "width": 3.6
    }
]

PORTOLAN.MARKER_ICON_SCALE_STOPS#

Size of every station mark by zoom - the diagram interchange pills AND the ordinary station dots, which share the ramp so the two marks never disagree in size - as a fraction of full size, interpolated linearly and held flat outside the listed stops. It dips further at the outer end than OFFSET_SCALE_STOPS does, because a pill has to stop swamping the ribbons it lies across as well as keep reaching them. Read as the map redraws.

json
[
    {
        "zoom": 11,
        "scale": 0.38
    },
    {
        "zoom": 12,
        "scale": 0.5
    },
    {
        "zoom": 14,
        "scale": 1
    }
]

STATION_MARKER#

The station mark: the dots the map draws on stations, and the schematic diagram interchange pills, which replace those dots on an interchange and sit beside them elsewhere. Both marks are their configured diameter at zoom 14 and scale with the camera from there - the dots by the ribbon-width curve, the pills by their own icon ramp - and they share the ring.

KeyDefaultDescription
STATION_MARKER.DOT_DIAMETER8Diameter in screen pixels of the ordinary station dot at zoom 14. At other zooms it follows the route lines: the diameter scales by PORTOLAN.RIBBON_WIDTH_STOPS relative to the ribbon width at zoom 14, so dots thicken and thin with the lines they punctuate. Read as the map redraws.
STATION_MARKER.SELECTED_DOT_DIAMETER10Diameter in screen pixels, at zoom 14, of the enlarged dot on emphasised stops: the open station, the stops of the selected route, and journey stops. Follows the ribbon-width curve at other zooms, the same way as DOT_DIAMETER. Read as the map redraws.
STATION_MARKER.PILL_DIAMETER8Height in screen pixels of the diagram interchange pill at full size (zoom 14 and in), which is also twice its corner radius - a pill is a dot stretched along its bundle, so this is its dot diameter. Scaled down at wider zooms by PORTOLAN.MARKER_ICON_SCALE_STOPS. Read as the map redraws.
STATION_MARKER.MAX_DIAMETER9Ceiling in screen pixels on a station mark as it grows zooming in. The ordinary dot stops growing once the ribbon-width curve would push it past this; the whole curve is clamped there, so the enlarged selected dot stops at the same zoom and keeps its proportion (its ceiling is this times SELECTED_DOT_DIAMETER over DOT_DIAMETER). The pill height is capped at this too, though the shipped pill ramp already tops out at PILL_DIAMETER. Read as the map redraws.
STATION_MARKER.BORDER_WIDTH1Ring thickness in screen pixels, held at this width at every zoom. The diagram pill draws its ring into an image that the zoom ramp then shrinks, so the game thickens the line inside that image by whatever the ramp is about to take off. Read as the map redraws.
STATION_MARKER.BORDER_DARK"black"The ring on a mark that has to separate from a pale background: every white interchange dot and every diagram pill, in both themes, plus route-colored dots in the dark theme. Any CSS color. Read as the map redraws.
STATION_MARKER.BORDER_LIGHT"white"The ring on a route-colored dot in the light theme, where the ring is the page showing through rather than an outline. Any CSS color. Read as the map redraws.
STATION_MARKER.GROUP_DOT_SPLIT_SPREAD_PX12Two stations of one complex share a dot while they sit within this many screen pixels of EACH OTHER at the current zoom (joining transitively, so a chain of close stations is one dot). Because the gap is measured on screen, a close pair holds its merged dot deeper into the zoom than a far pair, and one complex can be part-merged: the close pair one dot, the outlier its own, every dot styled for its members (or replaced by their diagram interchange pill) under the complex’s one merged label. Raise to keep dots merged longer; set very high to never split. Read as the map redraws.
STATION_MARKER.GROUP_DOT_SPLIT_MIN_ZOOM12Below this zoom a complex always draws one merged dot, however far its stations spread - zoomed out, separate dots for one named stop read as clutter. At or past it, GROUP_DOT_SPLIT_SPREAD_PX decides dot by dot. Set above 16 to disable splitting entirely (16 is where the map stops drawing groups at all). Read as the map redraws.
STATION_MARKER.PILL_OFF_CORRIDOR_M15How far, in meters, a station mark may sit off the corridor its diagram interchange pill was measured on before the pill stops drawing and the stop falls back to the ordinary circular dot. A pill is a plate lying across a specific bundle at a specific angle - it may slide ALONG that corridor (the engine itself walks terminus markers along the track), but a mark moved sideways off it (a relocated visual centre, a complex centre between bundles) would show a plate crossing nothing. Read as the map redraws.
STATION_MARKER.FADE_MS250How long station dots, labels and diagram interchange pills take to fade in and out when they appear or disappear (zoom thresholds, label collision, pills standing in for dots), in milliseconds. 0 makes them pop instantly. Read as the map redraws.
STATION_MARKER.LABEL_ZOOM14The zoom past which every station may draw its name, and label space is settled purely by collision: any name that fits without overlapping a higher-priority label shows. At or below this zoom only the top of the importance ranking labels at all (interchanges and complexes, from zoom 12), and past it that ranking still decides who wins contested space - it just never vetoes a label into empty space. Read as the map redraws.
STATION_MARKER.GROUP_DOT_SPLIT_ZOOM_STEP2How many zoom levels apart the dot clustering re-evaluates. The camera zoom is rounded DOWN to the nearest step above GROUP_DOT_SPLIT_MIN_ZOOM before the on-screen gaps are measured, so the merge picture holds still across each whole band instead of a pair of dots merging for only a sliver of zooms and coming apart again. Lower it (down to fractions of a level) to re-cluster more responsively at the cost of more churn while zooming. Read as the map redraws.

ROAD_VISUALS#

Per-road-class rendering config: line width in meters and the zoom window [start, end] over which the road fades in while zooming in. Highways and major roads stay visible further out than smaller roads.

json
{
    "highway": {
        "width": 12,
        "zoomWindow": [
            6.5,
            7
        ]
    },
    "major": {
        "width": 8,
        "zoomWindow": [
            6.5,
            7
        ]
    },
    "medium": {
        "width": 6,
        "zoomWindow": [
            12,
            12.5
        ]
    },
    "minor": {
        "width": 4,
        "zoomWindow": [
            12,
            12.5
        ]
    }
}

AREA_VISUALS#

Zoom fade windows and final opacities for the flat map area layers. zoomWindow is the [start, end] zoom range over which the layer fades in while zooming in (null means always shown at full opacity). parksLarge covers parks of at least 100,000 square meters. commercial covers both commercial and college areas (one tile layer, so they share a fade and opacity; their colors come from the map theme).

json
{
    "buildings": {
        "zoomWindow": [
            12,
            12.5
        ],
        "opacity": 0.65
    },
    "parksLarge": {
        "zoomWindow": null,
        "opacity": 0.8
    },
    "parksSmall": {
        "zoomWindow": [
            12,
            12.5
        ],
        "opacity": 0.8
    },
    "commercial": {
        "zoomWindow": [
            12.25,
            12.75
        ],
        "opacity": 0.7
    }
}

BUILDING_FOUNDATION_DEPTHS#

Depth bands (in meters) and per-theme colors for the building foundation depth map layer. maxDepth Infinity means no upper bound.

json
[
    {
        "minDepth": 10,
        "maxDepth": 14,
        "colors": {
            "light": "#009c27",
            "dark": "#006b1b"
        },
        "label": "10-14m"
    },
    {
        "minDepth": 14,
        "maxDepth": 18,
        "colors": {
            "light": "#ff9124",
            "dark": "#cc7319"
        },
        "label": "14-18m"
    },
    {
        "minDepth": 18,
        "maxDepth": 22,
        "colors": {
            "light": "#ff3d24",
            "dark": "#cc3119"
        },
        "label": "18-22m"
    },
    {
        "minDepth": 22,
        "maxDepth": 26,
        "colors": {
            "light": "#8B0000",
            "dark": "#5c0000"
        },
        "label": "22-26m"
    },
    {
        "minDepth": 26,
        "maxDepth": 30,
        "colors": {
            "light": "#d324ff",
            "dark": "#a91acc"
        },
        "label": "26-30m"
    },
    {
        "minDepth": 30,
        "maxDepth": Infinity,
        "colors": {
            "light": "#CBC3E3",
            "dark": "#9d96b5"
        },
        "label": "30m+"
    }
]

TRACK_SPEED_COLORS#

Track speed color mapping for the track speeds layer: speeds in km/h to colors.

json
[
    {
        "speedKmh": 10,
        "hex": "#3B82F6"
    },
    {
        "speedKmh": 25,
        "hex": "#06B6D4"
    },
    {
        "speedKmh": 50,
        "hex": "#22C55E"
    },
    {
        "speedKmh": 75,
        "hex": "#F97316"
    },
    {
        "speedKmh": 90,
        "hex": "#F43F5E"
    },
    {
        "speedKmh": 130,
        "hex": "#A855F7"
    }
]

TRACK_TPH_COLORS#

Track TPH color mapping for the track TPH layer: total trains-per-hour through a track (summed across all routes) to colors.

json
[
    {
        "tph": 0,
        "hex": "#94a3b8"
    },
    {
        "tph": 10,
        "hex": "#3B82F6"
    },
    {
        "tph": 20,
        "hex": "#22C55E"
    },
    {
        "tph": 30,
        "hex": "#F97316"
    },
    {
        "tph": 40,
        "hex": "#F43F5E"
    },
    {
        "tph": 50,
        "hex": "#A855F7"
    }
]

ROUTE_UTILIZATION_COLORS#

Route utilization color mapping for the route stats utilization view: percent of offered train capacity in use to colors, interpolated between stops.

json
[
    {
        "percent": 0,
        "hex": "#22C55E"
    },
    {
        "percent": 25,
        "hex": "#EAB308"
    },
    {
        "percent": 50,
        "hex": "#F97316"
    },
    {
        "percent": 75,
        "hex": "#cb0025"
    },
    {
        "percent": 100,
        "hex": "#ba00cb"
    }
]

ROUTE_RELIABILITY_COLORS#

Route reliability color mapping for the reliability panel: percent of arrivals that were on time to colors, interpolated between stops.

json
[
    {
        "percent": 0,
        "hex": "#cb0025"
    },
    {
        "percent": 50,
        "hex": "#F97316"
    },
    {
        "percent": 75,
        "hex": "#EAB308"
    },
    {
        "percent": 90,
        "hex": "#84CC16"
    },
    {
        "percent": 100,
        "hex": "#22C55E"
    }
]

SECTION_DELAY_COLORS#

Section delay color mapping for the reliability map mode: seconds of lateness a route section adds to colors, interpolated between stops and clamped past the last one.

json
[
    {
        "seconds": 0,
        "hex": "#22C55E"
    },
    {
        "seconds": 10,
        "hex": "#84CC16"
    },
    {
        "seconds": 20,
        "hex": "#EAB308"
    },
    {
        "seconds": 40,
        "hex": "#F97316"
    },
    {
        "seconds": 60,
        "hex": "#cb0025"
    }
]

TRACK_ELEVATIONS#

Elevation steps (in meters) and per-theme colors for the track elevations layer.

json
[
    {
        "depth": 20,
        "colors": {
            "light": "#0100cb",
            "dark": "#0100cb"
        },
        "label": "20m"
    },
    {
        "depth": 10,
        "colors": {
            "light": "#00b7cb",
            "dark": "#00b7cb"
        },
        "label": "10m"
    },
    {
        "depth": 0,
        "colors": {
            "light": "#14cb00",
            "dark": "#14cb00"
        },
        "label": "0m"
    },
    {
        "depth": -10,
        "colors": {
            "light": "#cbad00",
            "dark": "#cbad00"
        },
        "label": "-10m"
    },
    {
        "depth": -20,
        "colors": {
            "light": "#cb0025",
            "dark": "#cc3119"
        },
        "label": "-20m"
    },
    {
        "depth": -30,
        "colors": {
            "light": "#ba00cb",
            "dark": "#ba00cb"
        },
        "label": "-30m"
    }
]