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.

javascript
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).

javascript
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).

javascript
window.SubwayBuilderAPI.actions.subtractMoney(500_000);

Game Flow Actions#

setPause#

Pause or resume the game.

javascript
// Pause the game
window.SubwayBuilderAPI.actions.setPause(true);

// Resume the game
window.SubwayBuilderAPI.actions.setPause(false);

setSpeed#

Set the game speed.

javascript
// 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.

javascript
window.SubwayBuilderAPI.actions.setTicketPrice(5);

getTicketPrice#

Get the current ticket price. Returns number.

javascript
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.

javascript
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.

javascript
// 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).

javascript
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.

javascript
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.

javascript
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.

javascript
window.SubwayBuilderAPI.actions.deleteFareGroup(group.id);

moveRouteToFareGroup#

Move a route into another fare group (appended at the end). Returns true on success.

javascript
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`.

javascript
// 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 }.

javascript
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 }.

javascript
const result = window.SubwayBuilderAPI.actions.payBond('bond-id', 50_000_000);

getBonds#

Get all current bonds. Returns Bond[].

javascript
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.

javascript
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.

javascript
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#

ActionParametersReturnsDescription
setMoneyamount: numbervoidSet exact money amount (triggers onMoneyChanged)
addMoneyamount: numbervoidAdd money (revenue)
subtractMoneyamount: numbervoidSubtract money (expense)
setPausepaused: booleanvoidPause or resume game
setSpeed`speed: 'slow' \'normal' \'fast' \'ultrafast'`voidSet game speed
setTicketPriceprice: numbervoidSet ticket price
getTicketPricenonenumberGet current ticket price
addFareGroupnone`FareGroup \null`Create a new fare group
updateFareGroupgroupId: string, updates: Partial<FareGroup>booleanEdit a fare group's settings
deleteFareGroupgroupId: stringbooleanDelete a fare group (routes move to first group)
moveRouteToFareGrouprouteId: string, groupId: stringbooleanMove a route between fare groups
addFareZonegroupId: string, outline: [number, number][]`FareZone \null`Add a fare zone (validated + clipped)
updateFareZonegroupId: string, zoneId: string, outline: [number, number][]booleanReplace a fare zone's shape
deleteFareZonegroupId: string, zoneId: stringbooleanDelete a fare zone
setSpeedMultiplier`speed: 'slow' \'normal' \'fast' \'ultrafast', multiplier: number`voidModify tick rate (non-compounding)
issueBondbondTypeId: string{ success, message? }Issue a new bond
payBondbondId: string, amount: number{ success, message? }Pay down a bond
getBondsnoneBond[]Get all current bonds
renameStationstationId: string, newName: stringbooleanRename a station
renameStationGroup`groupId: string, name: string \null`booleanSet (or clear with null) a group's custom name

For modifyConstants and modifyPathfindingRules, see Game Constants.

Example: Day/Night Economy Mod#

javascript
(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');
        }
    });
})();