Cities of Stone is a map. Its roads, zones, services, peoples, districts and
seasons all come from one setting in the map file, rules.city, and everything
below works on any map that sets it. You can make your own city builder from
the same parts: a dwarven mountain hold where water is dear, a goblin junk town
where the works smoke and burn, a farming valley, or a volcano town where the
mountain decides what gets built.
Three of the tools on this page work on every map, not just cities: script overlays (your own map layers), runtime terrain (changing the ground during play) and the events and timers that drive them. A tower defence can paint its kill zones; a survival map can flood a valley.
Start from Cities of Stone
Download The Concord of Aethermere, the map the game itself plays, and open it in the Forge with File → Import Map JSON…. Its city rules come with it. Save it as your own and change anything.
To start from a blank map instead, open the Players module in the Forge. Under Rules there is a City panel: switch it on, pick the setting and how cities are founded, set the starting gold, and give the city your own numbers. Switching the city on also turns standard victory and the automatic starting base off, because a city builder founds its own hall and has no last-team-standing ending; turn either back on in Rules if your map wants it.
Switch a city on: rules.city
{
"rules": {
"standardVictory": false,
"noStartingUnits": true,
"city": {
"setting": "golden_age",
"founding": "choose",
"startingGold": 5000,
"startingTaxPercent": 9
}
}
}| Field | What it does |
|---|---|
setting | golden_age (peaceful), frontier (raids, lairs, disasters), rival_crowns (rival cities race to a Capital), grand_concord (co-op: one great city) |
founding | choose: each player founds their city where they like. placed: the map places the town halls |
startingGold, startingTaxPercent | Each city's starting gold and tax rate |
combat | Whether armies exist. Absent: the setting's own choice |
raids, disasters | The Frontier's raids and its storms, floods and quakes. Set false for a quiet frontier |
seasons | false turns the lean season and granaries off |
unlocks, startingLore | Lore items every city holds from the start, and Lore to spend |
cityName | The name a single player's city starts with |
tuning | The map's own numbers (next section) |
Your own numbers: rules.city.tuning
A map may change a chosen set of the city's figures for its own matches. Any field you leave out keeps the game's own number, and nothing you set reaches any other map: the numbers are resolved for your match alone.
{
"city": {
"setting": "golden_age",
"founding": "choose",
"tuning": {
"services": {
"water": { "cost": 400, "radius": 6, "upkeep": 9 },
"fire": { "cost": 150, "buildSteps": 4 }
},
"needs": {
"residential": [[], ["water", "safety"], ["water", "safety", "faith", "food"]]
},
"taxes": { "maxPercent": 30, "perResident": 4 },
"demand": { "baseJobs": 80 },
"tiers": { "village": 120, "town": 500 },
"seasons": { "yearSteps": 480, "leanSteps": 90, "leanFoodPercent": 40 },
"startingHallTier": 2,
"unlocks": { "density_medium": 1, "service_fire": 1 },
"peoples": ["dwarf", "human"]
}
}
}| Group | Keys | Notes |
|---|---|---|
services | per service (water, safety, faith, fire, health, learning, entertainment, market, park, academy): cost (0 to 100,000), upkeep (gold a minute, 0 to 10,000), radius (reach in tiles, 1 to 64), buildSteps (seconds to build, 1 to 600), jobs (0 to 500) | A park's radius is its beauty |
needs | per zone (residential, commercial, industrial, farm): a list of three lists, the needs of levels 1, 2 and 3 | A need is a service's name, or food. A lot short of a need of its level declines; one short of the next level's stays where it is |
taxes | perResident, perCommercialJob, perIndustrialJob (gold a minute at a 100% rate, 0 to 100), maxPercent (the tax ceiling, 1 to 100) | |
demand | workforcePercent, baseJobs, commercialJobsPer100Pop, baseCommercialJobs, industrialJobsPer100Workers, baseIndustrialJobs, neutralTaxPercent, perTaxPercent | baseJobs is the work the outside world offers before the city has any: raise it for a faster start |
tiers | village, town, city, capital, concord: the population each tier starts at | They must still rise |
seasons | yearSteps (seconds in a year, 120 to 36,000), leanSteps (the lean season, at most half the year), leanFoodPercent (what farms yield in it, 0 to 100) | Switch seasons off with rules.city.seasons: false |
startingHallTier | 1 a town hall, 2 a keep, 3 a castle | The hall a city founds with on a choose map. On a placed map, place the hall you want |
unlocks | tool id to hall tier: 0 (always open), 1, 2 or 3 | Only tools the halls open for free (zone_farm, density_medium, service_fire...). An item Lore prices, such as avenues, is handed out with rules.city.unlocks instead |
peoples | the peoples who may come to live: human, dwarf, goblin, ratmen, lizardmen, high_elf, wood_elf, dark_elf, undead | Founders always may. Absent: all of them |
Every number must be a whole number in its range. A value the city cannot use
is ignored and the usual number plays; the Forge's City panel and Validation
(F10) both list each one as invalid-city-tuning, so nothing fails silently.
Only maps read on this build understand tuning; an older build plays the
map with the usual numbers.
Read and drive the city from a script: sos.city
Scripts reach the city through sos.city. Writes are the very commands the
city's own HUD sends, so the city checks them the same way, the seat pays the
same prices, and a refusal is the same refusal: the call returns false (or
{ ok: false, reason }) and nothing happens. Without city rules every read
returns null and every write false.
| Call | Returns |
|---|---|
sos.city.stats(seat) | population, jobs, lots, tier and its name, hall tier, tax, income and upkeep a minute, demand per zone, coverage per service, food, problems (blight, crime, burning, burnt, plague), harmony, wants, goods in stock, Lore |
sos.city.coverageAt({x, y}) | each service's reach at a tile, 0 to 255 |
sos.city.lotsIn(from, to) | the lots in a rectangle: id, seat, position and size, zone or service, level, people, residents, jobs, land value, burning, burnt, sick, abandoned |
sos.city.lot(id), sos.city.districts(seat) | one lot; a seat's districts |
sos.city.zone(seat, from, to, zone, density?) | zone a rectangle: residential, commercial, industrial, farm, or none to clear |
sos.city.placeService(seat, service, {x, y}) | place a service with its top-left at a tile beside the seat's road |
sos.city.setTax(seat, percent) | set the tax, held to the map's ceiling |
sos.city.command(seat, command) | any other city command, such as { type: 'city_road', x0, y0, x1, y1, tier }; returns { ok, reason } |
sos.city.raiseDisaster(lotId, 'fire' or 'plague') | set a built lot alight, or start a plague in a home. The city's rules then run: a fire watch puts the fire out, healers cure the plague |
sos.events.onEvent hears the city: city-lot-built (a lot finished: lotId,
playerId, zone, service), city-lot-levelled, city-lot-grown (a new
plot breaking ground), city-lot-abandoned, city-fire (what: started,
spread, out, burnt), city-plague, city-peoples (a people arrived, rioted or
left), city-tier-up, city-district-founded, city-season (what: warning,
lean, plenty) and city-contest (Rival Crowns or the Grand Concord decided).
Trigger nodes
The same city is open to triggers. Events: cityLotBuilt (event value is the
lot id, event text its zone or service), cityLotLevelled, cityLotBurnt,
cityTierReached and cityLeanSeason. Conditions and values: cityStat
(population, tier, money, demand, food, harmony, problems) and
cityCoverageAt. Actions: cityZone (a region), cityPlaceService,
citySetTax and cityDisaster. Every one is in the
Trigger Reference.
Draw your own layers: overlays
A map can paint its own layer over the ground: a number per tile, drawn through a colour ramp, with a legend line. Players switch layers on and off in the Overlays frame, top right of the screen; it appears as soon as a map defines a layer that player may see. Overlays work on every map.
sos.overlay.define(id, { name, legend, colors, min, max, opacity, smooth, seats, show }): 2 to 6 colours from low to high ('#rrggbb'), the value drawn as the first colour (min, default 0) and the last (max, default 100).seatslimits who may see it;showturns it on for everyone now. Up to 8 layers.sos.overlay.paint(id, {x, y}, width, values): a block of values, row by row. A value that is not a number leaves its tile clear.sos.overlay.fill(id, from, to, value),clear(id),remove(id),show(id, on).- Triggers:
overlayDefine,overlayFill(a region) andoverlayClear.
Overlays are for players to read and nothing in the game reads them back, so they never change a match. They are not saved: repaint them from a timer if your map needs them after a load. They draw in the 3D view.
Change the land: runtime terrain
sos.terrain.set(from, to, kind) and the setTerrain trigger action turn a
rectangle of tiles (at most 4,096) to grass, dirt or shallowWater: a
flood, a drained marsh, a scorched field. Pathing, the minimap and the ground
follow at once, and a city sees the new ground on its next step. It returns
how many tiles changed.
To keep every match whole, some tiles never change: trees, ore, rock, deep water, lava and cliffs. Land and water never swap under a building, a city lot or a ship, or beside lava. Cliff height, ramps and pathing blockers cannot change during play; see Terrain and height.
Example: a volcano town
A city at the foot of a volcano. The script paints a danger layer round the vent, and every 90 seconds the mountain scorches the ground and sets the nearest homes alight. Players learn to keep a fire watch near the vent and to build their best streets out of the red.
{
"rules": {
"standardVictory": false,
"noStartingUnits": true,
"city": {
"setting": "golden_age",
"founding": "choose",
"startingGold": 6000,
"tuning": { "services": { "fire": { "cost": 150, "radius": 18 } } }
}
}
}// Volcano town: a danger layer round the vent, and eruptions every 90 seconds.
var VENT = { x: 40, y: 30 }; // the crater, in tiles
var REACH = 12; // how far its heat reaches
sos.lifecycle.onInit(function () {
sos.overlay.define('danger', {
name: 'Volcano danger',
legend: 'Red: lava bombs land here. Green: safe to build.',
colors: ['#3fa34d', '#f2c14e', '#d7263d'],
min: 0, max: REACH, show: true,
});
// Hotter nearer the vent: REACH at the crater, 0 at the edge of its reach.
var values = [];
for (var dy = -REACH; dy <= REACH; dy++) {
for (var dx = -REACH; dx <= REACH; dx++) {
values.push(REACH - Math.max(Math.abs(dx), Math.abs(dy)));
}
}
sos.overlay.paint('danger', { x: VENT.x - REACH, y: VENT.y - REACH }, REACH * 2 + 1, values);
sos.storage.eruptions = 0;
sos.timers.repeat('eruption', 20 * 90);
});
sos.lifecycle.onTimer(function (event) {
if (event.name !== 'eruption') return;
sos.storage.eruptions += 1;
// The crater's rim scorches to dirt.
sos.terrain.set({ x: VENT.x - 3, y: VENT.y - 3 }, { x: VENT.x + 3, y: VENT.y + 3 }, 'dirt');
// Lava bombs: up to three built lots within reach catch fire.
var lots = sos.city.lotsIn({ x: VENT.x - REACH, y: VENT.y - REACH }, { x: VENT.x + REACH, y: VENT.y + REACH }) || [];
var struck = 0;
for (var i = 0; i < lots.length && struck < 3; i++) {
var lot = lots[i];
if (lot.zone !== 'none' && lot.level >= 1 && !lot.burning && sos.city.raiseDisaster(lot.id, 'fire')) struck += 1;
}
});
sos.events.onEvent(function (event) {
// A city that reaches a town is paid for living with the mountain.
if (event.name === 'city-tier-up' && event.data.tier === 2) {
sos.players.addResource(event.data.playerId, 'gold', 500);
}
});Every call in it is metered like any script call. The danger layer is painted once, so the eruption timer costs only a lot read and up to three fires.
Three more ideas
- A dwarven mountain hold.
peoples: ["dwarf", "human"], water dear and short-reaching (services.water), the fire watch cheap, and stone works unlocked early (unlocks). A mountain map with little flat ground. - A goblin junk town.
peoples: ["goblin", "ratmen"], industry wanted early (demand.baseIndustrialJobs), and a script that sets an unruly block alight whenever crime (sos.city.stats(seat).problems.crime) runs high. - A farming valley. Homes that need food from level 1 (
needs), long lean seasons (seasons), a soil layer painted from the map's rivers, and a script that drains a marsh to farmland withsos.terrain.setonce a city reaches a village.
What a map cannot change yet
The city's kinds of things are the game's own: its zones, services, peoples, goods and building looks, and its four settings. A map can retune all of them, script around them and draw on top of them, but it cannot add a new service or a new good. Cliff heights and pathing blockers stay as authored. The full list of what maps can and cannot do is the Capability map.