Game Actions
Modify game state with action methods
Actions let your mod modify the game state. Use these to add money, control game flow, and more.
Important: Most actions use deferred execution (queueMicrotask) so they are safe to call from inside hooks like onDayChange. The action returns immediately, but the actual state change happens on the next microtask. This means you cannot read the updated state on the very next line — use a hook like onMoneyChanged to react to the change instead.
Exception: issueBond, payBond, getBonds, and getTicketPrice execute synchronously because they return values. Avoid calling these from inside hooks if possible.
Money Actions#
setMoney#
Set the player's money to an exact amount. Triggers the onMoneyChanged hook if the value differs from the current balance.
window.SubwayBuilderAPI.actions.setMoney(10_000_000);addMoney#
Add money to the player's balance. The second parameter is optional and currently unused (reserved for future categorization).
window.SubwayBuilderAPI.actions.addMoney(1_000_000);
window.SubwayBuilderAPI.actions.addMoney(500_000);subtractMoney#
Subtract money from the player's balance. The second parameter is optional and currently unused (reserved for future categorization).
window.SubwayBuilderAPI.actions.subtractMoney(500_000);Game Flow Actions#
setPause#
Pause or resume the game.
// Pause the game
window.SubwayBuilderAPI.actions.setPause(true);
// Resume the game
window.SubwayBuilderAPI.actions.setPause(false);setSpeed#
Set the game speed.
// Available speeds: 'slow', 'normal', 'fast', 'ultrafast'
window.SubwayBuilderAPI.actions.setSpeed('fast');
window.SubwayBuilderAPI.actions.setSpeed('ultrafast');setTicketPrice#
Set the ticket price (fare). Also updates the default cost for new games.
window.SubwayBuilderAPI.actions.setTicketPrice(5);getTicketPrice#
Get the current ticket price. Returns number.
const price = window.SubwayBuilderAPI.actions.getTicketPrice();Fare Groups#
Routes are organized into fare groups — each group prices its journeys with one fare system ('flat', 'route', 'distance', or 'zone') and a transfer policy. See gameState.getFareGroups for reading them and the FareGroup type for all fields. These actions execute synchronously and return a result; a change fires the onFareGroupsChanged hook.
addFareGroup#
Append a new, empty flat fare group. Returns a copy of the created group (grab its id), or null on failure.
const group = window.SubwayBuilderAPI.actions.addFareGroup();updateFareGroup#
Update one group's settings: name, fareSystem, flatFare, routeFares (per-route prices keyed by route id), transferPolicy, boardingCharge, perKmRate, fareCap, zonePricingMode, zoneBaseFare, zonePerZoneFare, zones. Returns true on success. Distance and zone fares always round to the nearest $0.05.
// Turn the group into a distance-based system: $1.50 flag fall + $0.15/km, capped at $6
window.SubwayBuilderAPI.actions.updateFareGroup(group.id, {
fareSystem: 'distance',
boardingCharge: 1.5,
perKmRate: 0.15,
fareCap: 6
});
// Or a zone-based system: $2.50 for one zone + $1 per extra zone,
// counted Berlin-style (nested-ring span)
window.SubwayBuilderAPI.actions.updateFareGroup(group.id, {
fareSystem: 'zone',
zonePricingMode: 'combination',
zoneBaseFare: 2.5,
zonePerZoneFare: 1
});For zones, prefer the dedicated zone actions below — they validate and clip shapes the same way the in-game drawing tool does. Writing zones directly through updateFareGroup is allowed but skips those checks (overlapping zones resolve to the earlier zone in the list).
addFareZone#
Append a fare zone to a zone-system group from a polygon outline ([lng, lat][], 3+ points, unclosed — the ring is closed for you). The outline is rejected if it self-intersects or is smaller than ~0.01 km², and it is clipped against the group's existing zones — zones in a group never overlap, so an outline drawn around an existing zone becomes a concentric ring. Returns a copy of the created FareZone, or null.
Zone order matters: a zone's position in group.zones is its displayed number and, under 'combination' pricing, its ring order (index 0 = innermost).
const zone = window.SubwayBuilderAPI.actions.addFareZone(group.id, [
[-122.45, 37.75],
[-122.39, 37.75],
[-122.39, 37.80],
[-122.45, 37.80]
]);updateFareZone#
Replace one zone's shape with a new outline (same validation and clipping as addFareZone, ignoring the zone's own old shape). Returns true on success.
window.SubwayBuilderAPI.actions.updateFareZone(group.id, zone.id, newOutline);deleteFareZone#
Delete a fare zone from its group. Later zones shift down one number. Returns true on success.
window.SubwayBuilderAPI.actions.deleteFareZone(group.id, zone.id);deleteFareGroup#
Delete a fare group; its routes move to the first remaining group. Fails (returns false) on the last group — every route must be in a fare group.
window.SubwayBuilderAPI.actions.deleteFareGroup(group.id);moveRouteToFareGroup#
Move a route into another fare group (appended at the end). Returns true on success.
const routes = window.SubwayBuilderAPI.gameState.getRoutes({ includeTempRoutes: false });
window.SubwayBuilderAPI.actions.moveRouteToFareGroup(routes[0].id, group.id);setSpeedMultiplier#
Modify how many game-seconds pass per real second for a given speed tier. Multipliers are always applied to the original base value, so calling this multiple times or across hot reloads will not compound — it safely replaces the previous multiplier.
This writes to RULES.GAME_SECONDS_PER_SECOND, which can also be set to absolute values with `modifyConstants`.
// Make "fast" speed 2x faster than default
window.SubwayBuilderAPI.actions.setSpeedMultiplier('fast', 2.0);
// Reset to default
window.SubwayBuilderAPI.actions.setSpeedMultiplier('fast', 1.0);Bonds#
issueBond#
Issue a new bond to raise capital. Returns { success: boolean, message?: string }.
const result = window.SubwayBuilderAPI.actions.issueBond('SMALL');
if (result.success) {
console.log('Bond issued!');
} else {
console.log('Failed:', result.message);
}payBond#
Pay down an existing bond. Returns { success: boolean, message?: string }.
const result = window.SubwayBuilderAPI.actions.payBond('bond-id', 50_000_000);getBonds#
Get all current bonds. Returns Bond[].
const bonds = window.SubwayBuilderAPI.actions.getBonds();Stations & Station Groups#
renameStation#
Rename a station. Returns true on success, false if the station id doesn't exist or the name is empty. The new name propagates to the map labels and panels automatically.
const ok = window.SubwayBuilderAPI.actions.renameStation('station-id', 'Fulton Center');renameStationGroup#
Set a station group's custom name. Passing null clears the custom name and restores the derived one (based on the member stations). Returns true on success, false if the group id doesn't exist.
Use gameState.getStationGroups() to find group ids, and hooks.onStationGroupSelected to react to the player opening one.
const api = window.SubwayBuilderAPI;
api.actions.renameStationGroup('group-id', 'Downtown Interchange');
// Later: restore the automatic name
api.actions.renameStationGroup('group-id', null);Game Constants#
Game rules and pathfinding rules (modifyConstants, modifyPathfindingRules) have their own page — see Game Constants.
Action Summary#
| Action | Parameters | Returns | Description | |||
|---|---|---|---|---|---|---|
setMoney | amount: number | void | Set exact money amount (triggers onMoneyChanged) | |||
addMoney | amount: number | void | Add money (revenue) | |||
subtractMoney | amount: number | void | Subtract money (expense) | |||
setPause | paused: boolean | void | Pause or resume game | |||
setSpeed | `speed: 'slow' \ | 'normal' \ | 'fast' \ | 'ultrafast'` | void | Set game speed |
setTicketPrice | price: number | void | Set ticket price | |||
getTicketPrice | none | number | Get current ticket price | |||
addFareGroup | none | `FareGroup \ | null` | Create a new fare group | ||
updateFareGroup | groupId: string, updates: Partial<FareGroup> | boolean | Edit a fare group's settings | |||
deleteFareGroup | groupId: string | boolean | Delete a fare group (routes move to first group) | |||
moveRouteToFareGroup | routeId: string, groupId: string | boolean | Move a route between fare groups | |||
addFareZone | groupId: string, outline: [number, number][] | `FareZone \ | null` | Add a fare zone (validated + clipped) | ||
updateFareZone | groupId: string, zoneId: string, outline: [number, number][] | boolean | Replace a fare zone's shape | |||
deleteFareZone | groupId: string, zoneId: string | boolean | Delete a fare zone | |||
setSpeedMultiplier | `speed: 'slow' \ | 'normal' \ | 'fast' \ | 'ultrafast', multiplier: number` | void | Modify tick rate (non-compounding) |
issueBond | bondTypeId: string | { success, message? } | Issue a new bond | |||
payBond | bondId: string, amount: number | { success, message? } | Pay down a bond | |||
getBonds | none | Bond[] | Get all current bonds | |||
renameStation | stationId: string, newName: string | boolean | Rename a station | |||
renameStationGroup | `groupId: string, name: string \ | null` | boolean | Set (or clear with null) a group's custom name |
For modifyConstants and modifyPathfindingRules, see Game Constants.
Example: Day/Night Economy Mod#
(function () {
const api = window.SubwayBuilderAPI;
// Track daily expenses
let dailyExpenses = 0;
api.hooks.onMoneyChanged((balance, change, type, category) => {
if (type === 'expense') {
dailyExpenses += Math.abs(change);
}
});
api.hooks.onDayChange((day) => {
// End of day report
if (dailyExpenses > 1_000_000) {
api.ui.showNotification(`High spending day! $${dailyExpenses.toLocaleString()}`, 'warning');
}
// Reset for next day
dailyExpenses = 0;
// Weekly bonus
if (day % 7 === 0) {
api.actions.addMoney(100_000);
api.ui.showNotification('Weekly bonus: $100,000!', 'success');
}
});
})();