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.
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:
| Key | What it controls |
|---|---|
gameState | Ticks of train, signal, and station simulation per update. Also sets the real-time interval between updates. |
trainCoords | How often train positions are recalculated. Positions always refresh on an update's final tick, so this only matters when it is lower than gameState. |
popMovement | How often pops travelling through the network are advanced. |
popMovementGeojson | How often the pop movement map layer is rebuilt. Keep at or above gameState β rebuilding more than once per update is wasted work. |
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.
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).
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.
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.
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.
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.
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.
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.
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:
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.
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.
| Key | Default | Description |
|---|---|---|
MIN_SINGLE_TRACK_LENGTH | 10 | Minimum length in meters of a track in a track group with only one lane of tracks. |
MIN_TRACK_LENGTH | 10 | Minimum length in meters of a parallel/quad track when auto-crossovers are enabled. |
MIN_ADVANCED_TRACK_LENGTH | 10 | Override for the minimum track length when advanced mode is enabled β power users get the shorter limit on any lane type. |
MAX_TRACK_LENGTH | 10000 | Maximum length in meters of a single track. |
COORD_PRECISION | 6 | Decimal 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_THRESHOLD | 10 | Radius in screen pixels within which drawn track endpoints snap to existing tracks, stations, and endpoints. |
STATION_LENGTH_BUFFER | 4 | Extra meters of platform required on top of the raw car-length times car-count. |
TRACK_WIDTH | 1.435 | Width in meters of a single track. |
SCISSORS_CROSSOVER_LENGTH | 40 | Length in meters of a scissors crossover between parallel tracks. |
OFFSET_TRANSITION_LENGTH | 50 | Maximum 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_MM | 150 | Fallback 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_S | 45 | Maximum 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_MM | 150 | Maximum 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_M | 200 | Length in meters of the approach to a junction or station treated as a possible stopping zone for the cant cap. |
CANT_STATION_MAX_MM | 0 | Maximum applied cant in millimeters through station tracks β platforms must be level. |
CANT_STREET_RUNNING_MAX_MM | 0 | Maximum applied cant in millimeters for street-running track β rail set into a roadway is flat. |
OFFSET_TRANSITION_RADIUS_MULTIPLIER | 10 | How forgivingly curvature inside a spacing-transition zone is judged for speed limits (higher = more forgiving). |
STATION_HEIGHT | 4 | Distance in meters from the station floor to the roof. Used to check if stations are colliding with each other. |
TUNNEL_HEIGHT | 5 | Distance in meters from the bottom to the top of the tunnel. |
V_MERGE_SIGNAL_WINDOW_LENGTH | 200 | Length in meters of the signal window of a v-merge signal. |
DIAMOND_SIGNAL_WINDOW_LENGTH | 10 | Length in meters of the signal window of a diamond signal, for each track where the center is where the diamond meets. |
GRADE_CROSSING_SIGNAL_RADIUS | 200 | Half-width in meters of each grade-crossing signal's track coverage. |
MAX_JERK | 0.3 | Maximum change in acceleration of a train in m/sΒ³. |
ADDITIONAL_WARNING_WINDOW_LENGTH | 2 | Length 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_LENGTH | 100 | Length 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_COST | 3 | Default transit fare in dollars for a new game. |
COMMUTE_INTERVAL_LENGTH | 900 | Commuters are released in intervals of this many seconds. |
SCHEDULE_TRANSITION_WINDOW_MAX_LENGTH | 5400 | Maximum 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_MINUTES | 10 | Granularity in minutes of the train-scheduling analysis/debug simulator. |
MAX_ROUTE_SIMULATION_DURATION | 86400 | Won't simulate a route longer than this many seconds, to prevent infinite loops in route time estimation. |
MAX_WAIT_TIME | 840 | Maximum time in seconds a train waits at a terminal. |
STARTING_MONEY | 3000000000 | Money in dollars the player starts a new game with. |
STARTING_TRAIN_CARS | 30 | Train cars the player starts a new game with. |
FARE_MULTIPLIER | 365 | For every dollar payed in fares, the player earns this many dollars. |
ACCEPTABLE_SPEED_MARGIN | 0.01 | Margin of error for the train's speed when calculating movements. |
MIN_ELEVATION | -100 | Minimum construction elevation in meters. |
MAX_ELEVATION | 20 | Maximum construction elevation in meters. |
TIME_TO_KEEP_COMMUTE_DATA | 86400 | How long in seconds to keep completed commutes data. |
STUCK_TRAIN_TIMEOUT | 900 | If a train hasn't moved for this many seconds, consider it stuck and remove it. |
IS_DELAYED_THRESHOLD | 60 | A 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_DETOUR | 400 | Max 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_INTERVAL | 3600 | Filter completed commutes every this many seconds. |
INFRASTRUCTURE_COST_CHARGE_INTERVAL | 300 | How often in game seconds to charge infrastructure maintenance costs. |
EXTRA_TRACK_STATION_MAINTENANCE_COST_MUTLIPLIER | 0.25 | Maintenance cost multiplier for every extra track lane in a station. |
OPERATIONAL_COST_CHARGE_INTERVAL | 900 | How often in game seconds to charge operational costs. |
TRAIN_SCHEDULE_TRANSITION_WINDOW | 1800 | Transition window in seconds before and after demand level changes. |
BUILDING_FOUNDATION_GAP | 3 | Minimum gap in meters between the bottom of a building foundation and the tunnel. |
WALKING_SPEED | 1 | Walking speed in m/s when using a straight-line approximation instead of the actual path. |
WALKING_SPEED_ACCURATE_PATH | 1.5 | Walking speed in m/s when using accurate path distance. |
GRADE_CROSSING_APPROACH_SECONDS | 20 | Gates start closing this many seconds before a train is predicted to arrive at the crossing. |
GRADE_CROSSING_CLEAR_SECONDS | 15 | Gates remain closed for this many seconds after a train's tail clears the crossing (post-pass hold). |
MAX_ROUTE_NAME_LENGTH | 16 | Maximum number of characters in a route bullet name. |
MAX_ROUTE_FULL_NAME_LENGTH | 48 | Maximum number of characters in a route's optional full name. |
MAX_TICKS_PER_UPDATE | 1000 | Ceiling 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.
| Key | Default | Description |
|---|---|---|
CONSTRUCTION_COSTS.TBM_MIN_SEGMENT_COST | 40000000 | Fixed TBM mobilization charge in dollars applied to every contiguous deep-bore blueprint segment. |
CONSTRUCTION_COSTS.TUNNEL#
Track cost multipliers by lane count.
| Key | Default | Description |
|---|---|---|
CONSTRUCTION_COSTS.TUNNEL.SINGLE_MULTIPLIER | 0.75 | Cost multiplier for single-lane track. |
CONSTRUCTION_COSTS.TUNNEL.QUAD_MULTIPLIER | 1.5 | Cost multiplier for quad-lane track. |
CONSTRUCTION_COSTS.STATION#
Station cost multipliers.
| Key | Default | Description |
|---|---|---|
CONSTRUCTION_COSTS.STATION.SINGLE_MULTIPLIER | 0.75 | Cost multiplier for single-track stations. |
CONSTRUCTION_COSTS.STATION.QUAD_MULTIPLIER | 1.5 | Cost multiplier for quad-track stations. |
CONSTRUCTION_COSTS.STATION.LENGTH_COST_FACTOR | 0.25 | Fraction of station cost that scales with capacity in cars (0 = disabled, 1 = fully linear). |
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS#
Cost multipliers by construction elevation type.
| Key | Default | Description |
|---|---|---|
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.DEEP_BORE | 4.5 | Deep-bore tunnel cost multiplier. |
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.STANDARD_TUNNEL | 2 | Standard tunnel cost multiplier. |
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.CUT_AND_COVER | 1 | Cut-and-cover cost multiplier (baseline). |
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.TRENCHED | 0.5 | Open-top trench cost multiplier. |
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.AT_GRADE | 0.35 | At-grade (surface) cost multiplier. |
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.RAMP | 0.5 | Low open ramp / embankment (1-5 m) cost multiplier. |
CONSTRUCTION_COSTS.ELEVATION_MULTIPLIERS.ELEVATED | 0.8 | Elevated viaduct cost multiplier. |
CONSTRUCTION_COSTS.WATER_MULTIPLIERS#
Multipliers for construction over water, applied on top of the base elevation multiplier.
| Key | Default | Description |
|---|---|---|
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.DEEP_BORE | 1.44444 | Deep-bore over water. |
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.STANDARD_TUNNEL | 1.5 | Standard tunnel over water. |
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.CUT_AND_COVER | 3 | Cut-and-cover over water. |
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.TRENCHED | 100 | Trenched cannot be built over water. |
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.AT_GRADE | 100 | At-grade cannot be built over water. |
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.RAMP | 100 | Low ramp / embankment cannot be built over water. |
CONSTRUCTION_COSTS.WATER_MULTIPLIERS.ELEVATED | 2.5 | Elevated viaduct over water. |
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS#
Elevation boundaries in meters between construction types.
| Key | Default | Description |
|---|---|---|
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.DEEP_BORE | -100 | Below this elevation is deep bore. |
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.STANDARD_TUNNEL | -24 | Standard tunnel threshold. |
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.CUT_AND_COVER | -10 | Cut-and-cover threshold. |
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.TRENCHED | -4 | Trenched threshold. |
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.AT_GRADE | -1 | Boundary between trenched and at-grade. |
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.RAMP | 0 | Boundary between at-grade and the open ramp / embankment. |
CONSTRUCTION_COSTS.ELEVATION_THRESHOLDS.ELEVATED | 5 | At 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.
| Key | Default | Description |
|---|---|---|
CONSTRUCTION_COSTS.GRADE_CROSSING.ROAD_CLASS_MULTIPLIER.minor | 1 | Minor road crossing. |
CONSTRUCTION_COSTS.GRADE_CROSSING.ROAD_CLASS_MULTIPLIER.medium | 1.25 | Medium road crossing. |
CONSTRUCTION_COSTS.GRADE_CROSSING.ROAD_CLASS_MULTIPLIER.major | 1.5 | Major road crossing. |
CONSTRUCTION_COSTS.GRADE_CROSSING.ROAD_CLASS_MULTIPLIER.highway | 2 | Highway crossing. |
CONSTRUCTION_COSTS.GRADE_CROSSING.LANE_COUNT_MULTIPLIER#
Cost multiplier by number of track lanes crossing the road.
| Key | Default | Description |
|---|---|---|
CONSTRUCTION_COSTS.GRADE_CROSSING.LANE_COUNT_MULTIPLIER.1 | 0.75 | Single lane. |
CONSTRUCTION_COSTS.GRADE_CROSSING.LANE_COUNT_MULTIPLIER.2 | 1 | Two lanes. |
CONSTRUCTION_COSTS.GRADE_CROSSING.LANE_COUNT_MULTIPLIER.4 | 1.5 | Four lanes. |
CONSTRUCTION_COSTS.BUILDING_OVERPASS#
Costs and limits for elevated tracks spanning over buildings.
| Key | Default | Description |
|---|---|---|
CONSTRUCTION_COSTS.BUILDING_OVERPASS.VERTICAL_CLEARANCE | 3 | Vertical clearance in meters required between building roof and track deck. |
CONSTRUCTION_COSTS.BUILDING_OVERPASS.COST_PREMIUM | 3 | Cost premium of a special long-span structure vs a standard elevated segment. |
CONSTRUCTION_COSTS.BUILDING_OVERPASS.STANDARD_SPAN | 35 | Reference span in meters; superstructure cost scales with span / STANDARD_SPAN. |
CONSTRUCTION_COSTS.BUILDING_OVERPASS.PIER_COST_PER_METER_HEIGHT | 200000 | Straddle-pier cost per meter of building height, per building crossed. |
CONSTRUCTION_COSTS.BUILDING_OVERPASS.MIN_BUILDING_HEIGHT | 3 | Floor for building height in meters when height data is missing or zero. |
YARDS#
Train yard rules (yards-enabled saves only).
| Key | Default | Description |
|---|---|---|
YARDS.BERTH_OVERHEAD_M | 10 | Clearance in meters added to each train length when packing trains onto berth tracks. |
YARDS.MIN_YARD_TRACK_LENGTH_M | 50 | Minimum length in meters of a drawn yard track. |
YARDS.DEFAULT_YARD_SPEED_LIMIT | 4.47 | Speed 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_MULTIPLIER | 0.5 | Construction 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.
| Key | Default | Description |
|---|---|---|
STREET_RUNNING.ROAD_SNAP_DISTANCE | 15 | Distance in meters within which drawn tracks snap to a road. |
STREET_RUNNING.ROAD_SNAP_BREAKAWAY_DISTANCE | 35 | Distance in meters at which a snapped track breaks away from the road. |
STREET_RUNNING.COST_MULTIPLIER | 3.5 | Construction cost multiplier for street-running track. |
STREET_RUNNING.EXCLUSIVE_ROW_COST_MULTIPLIER | 1.5 | Additional cost multiplier for an exclusive right-of-way. |
STREET_RUNNING.ELEVATION_TOLERANCE | 0.01 | Tracks 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.
| Key | Default | Description |
|---|---|---|
ROAD_SPEED_LIMITS.highway | 29.1 | Highway speed limit. |
ROAD_SPEED_LIMITS.major | 20.1 | Major road speed limit. |
ROAD_SPEED_LIMITS.medium | 15.6 | Medium road speed limit. |
ROAD_SPEED_LIMITS.minor | 11.2 | Minor road speed limit. |
MARKER_ROUTE_BULLETS#
Sizing for the route bullet row shown under station and station-group markers on the map.
| Key | Default | Description |
|---|---|---|
MARKER_ROUTE_BULLETS.TEXT_SIZE_MULTIPLIER | 0.8 | Bullet text size as a fraction of the bullet icon size. |
MARKER_ROUTE_BULLETS.CHAR_WIDTH_RATIO | 0.62 | Approximate width of one bullet character as a fraction of the font size, used to project bullet widths. |
MARKER_ROUTE_BULLETS.ICON_GAP_PX | 2 | Horizontal gap in pixels between bullets in the row. |
MARKER_ROUTE_BULLETS.MAX_ROW_WIDTH_PX | 300 | The entire bullet row is hidden when its projected width exceeds this many pixels. |
SOUND_SETTINGS#
Station crowd ambience settings.
| Key | Default | Description |
|---|---|---|
SOUND_SETTINGS.NEAR_STATIONS_THRESHOLD | 1000 | Distance 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_VOLUME | 0.25 | Maximum 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.
| Key | Default | Description |
|---|---|---|
WARNING_CONSTS.CAPACITY_WARNING_THRESHOLD | 50 | A 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_OLD | 100 | The capacity warning threshold used when the reduced-threshold feature flag is disabled (the default). |
WARNING_CONSTS.CAPACITY_WARNING_DURATION | 900 | How 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%).
| Key | Default | Description |
|---|---|---|
BOND_TYPES.SMALL.displayName | $100M Bond | Display name shown in the financial dashboard. |
BOND_TYPES.SMALL.principal | 100000000 | Amount borrowed in dollars. |
BOND_TYPES.SMALL.interestRate | 0.06 | Daily interest rate on the remaining principal (0.06 = 6% per day), charged hourly at 1/24 of the daily rate. |
BOND_TYPES.SMALL.principalPaymentRate | 0.001 | Fraction of the original principal repaid every game hour. |
BOND_TYPES.SMALL.requiredDailyRevenue | 10000000 | Previous day's revenue in dollars required to issue this bond. |
BOND_TYPES.MEDIUM#
Medium bond ($500M at 4%).
| Key | Default | Description |
|---|---|---|
BOND_TYPES.MEDIUM.displayName | $500M Bond | Display name shown in the financial dashboard. |
BOND_TYPES.MEDIUM.principal | 500000000 | Amount borrowed in dollars. |
BOND_TYPES.MEDIUM.interestRate | 0.04 | Daily interest rate on the remaining principal (0.04 = 4% per day), charged hourly at 1/24 of the daily rate. |
BOND_TYPES.MEDIUM.principalPaymentRate | 0.0008 | Fraction of the original principal repaid every game hour. |
BOND_TYPES.MEDIUM.requiredDailyRevenue | 100000000 | Previous day's revenue in dollars required to issue this bond. |
BOND_TYPES.LARGE#
Large bond ($1B at 2%).
| Key | Default | Description |
|---|---|---|
BOND_TYPES.LARGE.displayName | $1B Bond | Display name shown in the financial dashboard. |
BOND_TYPES.LARGE.principal | 1000000000 | Amount borrowed in dollars. |
BOND_TYPES.LARGE.interestRate | 0.02 | Daily interest rate on the remaining principal (0.02 = 2% per day), charged hourly at 1/24 of the daily rate. |
BOND_TYPES.LARGE.principalPaymentRate | 0.0006 | Fraction of the original principal repaid every game hour. |
BOND_TYPES.LARGE.requiredDailyRevenue | 200000000 | Previous 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.
| Key | Default | Description |
|---|---|---|
GAME_SECONDS_PER_SECOND.slow | 1 | In-game seconds per real second at slow speed (real time). |
GAME_SECONDS_PER_SECOND.normal | 25 | In-game seconds per real second at normal speed. |
GAME_SECONDS_PER_SECOND.fast | 250 | In-game seconds per real second at fast speed. |
GAME_SECONDS_PER_SECOND.ultrafast | 500 | In-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.
| Key | Default | Description |
|---|---|---|
TICKS_PER_UPDATE.slow.gameState | 1 | Ticks 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.trainCoords | 1 | Ticks between train coordinate recalculations at slow speed. Coordinates also refresh on the final tick of every update. |
TICKS_PER_UPDATE.slow.popMovement | 2 | Ticks between updates to pops travelling through the network at slow speed. |
TICKS_PER_UPDATE.slow.popMovementGeojson | 2 | Ticks 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.
| Key | Default | Description |
|---|---|---|
TICKS_PER_UPDATE.normal.gameState | 1 | Ticks 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.trainCoords | 1 | Ticks between train coordinate recalculations at normal speed. Coordinates also refresh on the final tick of every update. |
TICKS_PER_UPDATE.normal.popMovement | 2 | Ticks between updates to pops travelling through the network at normal speed. |
TICKS_PER_UPDATE.normal.popMovementGeojson | 2 | Ticks 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.
| Key | Default | Description |
|---|---|---|
TICKS_PER_UPDATE.fast.gameState | 16 | Ticks 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.trainCoords | 8 | Ticks between train coordinate recalculations at fast speed. Coordinates also refresh on the final tick of every update. |
TICKS_PER_UPDATE.fast.popMovement | 8 | Ticks between updates to pops travelling through the network at fast speed. |
TICKS_PER_UPDATE.fast.popMovementGeojson | 16 | Ticks 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.
| Key | Default | Description |
|---|---|---|
TICKS_PER_UPDATE.ultrafast.gameState | 48 | Ticks 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.trainCoords | 24 | Ticks between train coordinate recalculations at ultrafast speed. Coordinates also refresh on the final tick of every update. |
TICKS_PER_UPDATE.ultrafast.popMovement | 24 | Ticks between updates to pops travelling through the network at ultrafast speed. |
TICKS_PER_UPDATE.ultrafast.popMovementGeojson | 48 | Ticks 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.
| Key | Default | Description |
|---|---|---|
MAX_TRANSFERS | 4 | Maximum 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_STATION | 2700 | Maximum 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_STATION | 420 | Maximum 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_TIME | 600 | Maximum walking time between stations when transferring, in seconds. |
FUTURE_CYCLE_TIME_OFFSET | 7200 | How many seconds in advance train departure times are pre-calculated for pathfinding. |
MIN_FUTURE_CYCLES | 2 | Minimum number of future service cycles to calculate. Ensures loop-around journeys work on very long routes (over 2 hours). |
DRIVING_COST_PER_KM | 0.65 | Perceived cost of driving per kilometer in dollars, not including parking. Based on US BTS data, slightly reduced to nerf cars. |
MIN_SENSIBLE_DRIVING_DISTANCE | 1000 | Minimum sensible driving distance in meters. Shorter drives get an extra penalty for the hassle of using a car on a very short trip. |
PARKING_TIME | 180 | Time spent parking at each end of a car journey in seconds (finding a spot and walking from it). |
PARKING_COST | 5 | Cost of parking in dollars per day (average in urban areas). |
ARRIVAL_GAP | 50 | Pops arrive at the station this many seconds before their train departs. |
RANGE_QUERY_WINDOW | 1800 | rRAPTOR 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_DEPARTURES | 24 | Cap on how many departure times a single range query evaluates. |
INCOME_MEAN | 60000 | Mean of the normal distribution used to generate pop incomes, in dollars per year. |
INCOME_STD_DEV | 25000 | Standard deviation of the pop income distribution, in dollars per year. |
MINIMUM_INCOME | 15000 | Floor for generated pop incomes, in dollars per year. |
MAXIMUM_INCOME | 200000 | Ceiling for generated pop incomes, in dollars per year. |
HOURS_WORKED_PER_YEAR | 1860 | Hours worked per year, used to convert annual income into an hourly value of time for mode choice. |
AIRPORT_INCOME_MULTIPLIER | 1.5 | Income multiplier for airport pops (air travelers skew wealthier). |
AIRPORT_PARKING_COST_MULTIPLIER | 5 | Airport pops pay this many times the normal PARKING_COST. |
COLLEGE_INCOME_MULTIPLIER | 0.6 | Income multiplier for college pops (students earn less). |
MIN_TRANSIT_CHOICE | 10 | If 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).
| Key | Default | Description |
|---|---|---|
PERCEIVED_TIME.WALK_MULTIPLIER | 1.39 | How 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_MULTIPLIER | 1.87 | Walk weight for airport pops, replacing WALK_MULTIPLIER (luggage makes walking feel worse β clock time is unchanged). |
PERCEIVED_TIME.WAIT_MULTIPLIER | 1.37 | How much worse a minute of waiting at a station feels than a minute riding. |
PERCEIVED_TIME.DEPARTURE_SHIFT_MULTIPLIER | 0.4 | Cost per second of departing later than requested (displacement time), e.g. a pop waiting at home for a better train. |
PERCEIVED_TIME.CONGESTED_DRIVING_MULTIPLIER | 1.33 | Perception premium on a minute driven in heavy metropolitan congestion. |
PERCEIVED_TIME.CONGESTION_FULL_AT_MULTIPLIER | 2 | Traffic multiplier at which the whole car trip counts as congested. Keep in sync with DRIVING_TIMES.HIGH_DEMAND. |
PERCEIVED_TIME.PARKING_SEARCH_MULTIPLIER | 1.6 | Perception 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.
| Key | Default | Description |
|---|---|---|
DRIVING_TIMES.VERY_LOW_DEMAND | 0.8 | Overnight traffic multiplier (faster than free-flow baseline). |
DRIVING_TIMES.LOW_DEMAND | 0.9 | Early morning / late evening traffic multiplier. |
DRIVING_TIMES.LOW_MEDIUM_DEMAND | 1 | Midday baseline traffic multiplier. |
DRIVING_TIMES.MEDIUM_DEMAND | 1.25 | Shoulder-peak traffic multiplier. |
DRIVING_TIMES.HIGH_DEMAND | 1.5 | Rush 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.
| Key | Default | Description |
|---|---|---|
MIN_TRACK_BASE_PIXEL_WIDTH | 1.5 | Minimum on-screen width in pixels of track lines when zoomed out. |
MIN_ROAD_PIXEL_WIDTH | 0.5 | Minimum 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_MODIFIER | 1 | The modifier applied to line widths when zoomed out. Output is track width times this value. Currently unused by the game. |
LINE_WIDTH_MODIFIER | 2 | The modifier applied to line widths when zoomed in. Output is track width times this value. Currently unused by the game. |
OFFSET_MULTIPLIER | 1.5 | How much space should be between parallel lines. Currently unused by the game. |
JOURNEY_LINE_WIDTH_MODIFIER | 2 | The modifier applied to journey line widths. Output is track width times this value. Currently unused by the game. |
STATION_MARKER_WIDTH_MODIFIER | 5 | The modifier applied to station marker widths. Output is track width times this value. Currently unused by the game. |
TRAIN_PATH_WIDTH_MODIFIER | 1.25 | The modifier applied to train path widths. Output is track width times this value. Currently unused by the game. |
PICKABLE_TRACK_WIDTH_MODIFIER | 3 | The modifier applied to track widths on the track speeds, elevations, types, and TPH layers to make tracks easier to click. |
SIMPLE_ROUTE_ZOOM | 13 | The zoom level where the map switches from showing routes by track to either simple or interlined routes. |
STATION_GROUP_DISTANCE_THRESHOLD | 150 | Distance in meters within which nearby stations are combined into one station group marker. |
STATION_MARKER_HOVER_RESIZE | 1.15 | The factor by which station markers are resized when hovered. |
RAIL_CROSSING_MARKER_SIZE_PX | 20 | Width 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.
| Key | Default | Description |
|---|---|---|
ROAD_STRUCTURES.TUNNEL_ALPHA | 128 | Alpha (0 to 255) of tunnel road segments, drawn semi-transparent so they read as underground rather than surface roads. |
ROAD_STRUCTURES.BRIDGE_OPACITY | 0.5 | Opacity (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_EXTRA | 4 | Extra 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.
| Key | Default | Description |
|---|---|---|
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.
| Key | Default | Description |
|---|---|---|
TRACK_ARROWS.ENABLED_ZOOM | 15.5 | The zoom at which the arrows appear. Further out they are too small to read. |
TRACK_ARROWS.SEGMENT_INTERVAL | 5 | Draw an arrow every this many subdivided segments. Larger means more spacing between arrows. |
TRACK_ARROWS.LENGTH | 2 | Arrow length as a fraction of a segment. Larger means bigger arrows. |
TRACK_ARROWS.TRACK_WIDTH | 4 | Width in meters of the arrows drawn on tracks. |
TRACK_ARROWS.TRACK_WIDTH_MIN_PIXELS | 4 | Minimum on-screen width in pixels of the arrows drawn on tracks. |
TRACK_ARROWS.ROUTE_WIDTH | 6 | Width in meters of the arrows drawn on routes. |
TRACK_ARROWS.ROUTE_WIDTH_MIN_PIXELS | 6 | Minimum 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.
| Key | Default | Description |
|---|---|---|
PORTOLAN.ROUTE_ZOOM | 15 | Above 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_ZOOM | 11 | The 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.
| Key | Default | Description |
|---|---|---|
PORTOLAN.CATERPILLARS.ENABLED | true | Whether 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_ZOOM | 12 | The 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_CHARS | 1 | Longest 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_HEIGHT | 10 | Bullet 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_ALPHA | 100 | Opacity, 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.
[
{
"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.
[
{
"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.
{
"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.
[
{
"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.
[
{
"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.
[
{
"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.
| Key | Default | Description |
|---|---|---|
STATION_MARKER.DOT_DIAMETER | 8 | Diameter 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_DIAMETER | 10 | Diameter 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_DIAMETER | 8 | Height 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_DIAMETER | 9 | Ceiling 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_WIDTH | 1 | Ring 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_PX | 12 | Two 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_ZOOM | 12 | Below 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_M | 15 | How 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_MS | 250 | How 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_ZOOM | 14 | The 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_STEP | 2 | How 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.
{
"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).
{
"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.
[
{
"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.
[
{
"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.
[
{
"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.
[
{
"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.
[
{
"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.
[
{
"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.
[
{
"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"
}
]