Files
Battle-for-Tismo/shared/game_state/resources.js
T
adrien c2f131da5c Turned tile improvements into construction sites with their own budgets
Every road, railway and tile work now rises as an economic construction site instead of a queue entry. A site gathers its materials over days from reachable regions, refusing any supplier quoting above the price it was budgeted at (a 1% tolerance); a day that buys nothing stalls the site, raising a red mark and letting the nation raise the budget to current prices or cancel it. The upfront budget covers the build cost plus the market value of its steel and high-tech, energy stays a grid draw, and any money left over stays with the finished work as a maintenance reserve.

That reserve now pays the work's own daily steel and high-tech wear before the treasury is billed, so a well funded work spares the state until it runs dry; the spent money is paid into the nation's regions. Building upgrades run through the same gather-build-maintain machinery, sharing the site's kind flag and target level. The legacy tile-work queue and the market building are gone.

Bundles the in-progress chart sizing for the central bank panel.
2026-09-22 18:08:31 +02:00

2585 lines
109 KiB
JavaScript

// Resources: the five material groups nations produce, store, trade and
// consume. Storage lives on the map -- a city tile or a resource-building tile
// holds its own stock -- while energy is a flow: power plants feed the grid of
// their connected region, and a grid in deficit imports power from a reachable
// surplus.
//
// The daily tick is the heart of it: produce, satisfy electric demand, consume
// what the people need, buy the rest from reachable stores -- the region first,
// home regions next, foreign ones last -- then let shortages bite and prices
// drift. There is no global market to fall back on.
import { key, parseKey } from "../hex.js";
import {
RESOURCE_IDS,
STORABLE_RESOURCE_IDS,
RESOURCE_RULES,
RESOURCE_MARKET_BASE,
RESOURCE_TRADE_RADIUS,
MIDDLEMAN,
FAMINE,
tileImprovementById,
isResourceTileBuilding,
resourceById,
MONEY,
} from "../data.js";
import {
cityEnergyPerDay,
foodNeedPerDay,
tileFoodOutput,
steelNeedPerDay,
luxuryNeedPerDay,
foodMonthlyTarget,
producerEnergyPerDay,
foodSynthesisEnergy,
synthesisedFoodFor,
deliveryEnergyForResource,
nextMarketPrice,
protoUpkeep,
constructionResourceCost as sharedConstructionResourceCost,
} from "../resources.js";
import { buildingBuildCost } from "../rules.js";
import { Random } from "../rng.js";
import { TILE_IMPROVEMENT_HP, HOURS_PER_DAY } from "./constants.js";
import { EFFECT_RESEARCH } from "../data/effects.js";
import { NEWS_FAMINE } from "../data/relations.js";
// A city keeps at least this many days of a material in stock before it will
// sell any of it on; the rest is surplus a neighbour may buy.
const SUPPLIER_RESERVE_DAYS = 2;
// The producers a fresh nation may be seeded with. Energy uses the large
// fossil plants so a nation needs few works to light its industry; the luxury
// pick is randomised between the two mines. High-tech is deliberately absent:
// a chip foundry is not covered by these passes but placed by hand for the two
// nations that open with one (see `_seedResourceBuildings`). Fusion, locked
// behind research, never appears either.
const SEED_PRODUCERS = {
steel: ["steel_mill"],
luxury: ["diamond_mine", "gold_mine"],
energy: ["coal_power_plant", "natural_gas_power_plant", "oil_power_plant"],
};
// How many top-up passes the start-of-game seeding makes before it gives up on
// the tiles that are left. Each pass places everything a resource is short by,
// so three or four passes settle a nation.
const SEED_MAX_PASSES = 10;
// A little headroom, so the opening economy is not balanced on a knife edge as
// prices and population drift in the first days. Power gets the larger cushion
// because it scales with GDP, which the market moves.
const SEED_MATERIAL_MARGIN = 1.05;
const SEED_ENERGY_MARGIN = 1.15;
function emptyStock() {
return { steel: 0, food: 0, luxury: 0, hightech: 0 };
}
export const resourceMethods = {
// -------------------------------------------------------- setup/state --
_initResources() {
this.resourceStock = new Map();
this.resourcePrices = new Map();
this.resourceMarketStats = new Map();
for (const id of RESOURCE_IDS) {
this.resourcePrices.set(id, RESOURCE_MARKET_BASE[id]);
this.resourceMarketStats.set(id, { supply: 0, demand: 0, price: RESOURCE_MARKET_BASE[id] });
}
// The world price of every commodity at the start of each month, oldest
// first, so the central bank can chart an index from 100 in January 2000 and
// read month-on-month and year-on-year inflation. The first sample is the
// opening price, the index's base.
this.resourcePriceHistory = new Map();
for (const id of RESOURCE_IDS) this.resourcePriceHistory.set(id, [RESOURCE_MARKET_BASE[id]]);
this.resourceLinks = [];
this._resourceShortages = new Map();
// The food each region has bought to relay on to its neighbours, so only
// that stock carries the middleman margin when it is resold.
this._relayStock = new Map();
// The regions whose food store has run dry and cannot feed their people.
this._famine = new Set();
this._resourceVersion = 0;
// What each nation imported and exported over the last simulated day, by
// resource, from cross-nation deliveries only.
this.resourceTrade = new Map();
// What each nation bought from outside its border over the day, with the
// quantity and the money it moved, per resource.
this.resourceExternal = new Map();
// The day's resource-shortage notices, per nation: a producing building that
// could not get the material or power it needs. Cleared each day and shipped
// to that nation's own viewer only.
this._resourceAlerts = new Map();
// What each nation spent on resources during the current day, split into
// electric imports, bought goods, transport, construction and upkeep.
this.resourceSpend = new Map();
// Repair state. City building levels knocked down by bombardment wait here
// to be rebuilt once their materials are paid; a damaged tile work carries a
// steel debt that must clear before it heals or its GDP recovers.
this.cityRepairQueue = new Map();
this.tileRepairDebt = new Map();
for (const city of this.cities) this._seedCityResources(city);
},
// Records money spent on resources. `resource` may name the one commodity the
// bill was for, or be a `{ id: money }` map when several were bought at once,
// so the budget panel can show a per-resource breakdown.
_recordResourceSpend(civ, kind, amount, resource = null) {
if (!(amount > 0)) return;
const record = this._resourceRecord(civ);
record[kind] = (record[kind] || 0) + amount;
if (typeof resource === "string") {
record.buy[resource] = (record.buy[resource] || 0) + amount;
this._recordBudgetResource(civ, resource, amount, 0);
} else if (resource) {
for (const [id, value] of Object.entries(resource)) {
if (value > 0) {
record.buy[id] = (record.buy[id] || 0) + value;
this._recordBudgetResource(civ, id, value, 0);
}
}
}
},
// Records money earned selling a resource to another country.
_recordResourceEarn(civ, resource, amount) {
if (!(amount > 0)) return;
const record = this._resourceRecord(civ);
record.sell[resource] = (record.sell[resource] || 0) + amount;
this._recordBudgetResource(civ, resource, 0, amount);
},
// Records that a nation bought a resource from outside its border: a foreign
// neighbour or the world market. `value` is the money paid, in the buyer's
// national currency. Same-nation purchases are not external and are skipped
// by the callers.
_recordExternalBuy(civ, id, qty, value) {
if (civ < 0 || !(qty > 0)) return;
this._externalRecord(civ, "buy", id, qty, value);
},
// Records that a nation sold a resource across its border, and the money it
// was paid, in the seller's national currency.
_recordExternalSell(civ, id, qty, value) {
if (civ < 0 || !(qty > 0)) return;
this._externalRecord(civ, "sell", id, qty, value);
},
_externalRecord(civ, side, id, qty, value) {
if (!this.resourceExternal) this.resourceExternal = new Map();
let record = this.resourceExternal.get(civ);
if (!record) {
record = { buy: {}, sell: {} };
this.resourceExternal.set(civ, record);
}
const entry = record[side][id] || (record[side][id] = { qty: 0, value: 0 });
entry.qty += qty;
if (value > 0) entry.value += value;
},
// One nation's cross-border buying and selling over the last day, as plain
// per-resource { qty, value } objects for the Economy tab's trade table.
_civExternalTrade(civ) {
const record = this.resourceExternal ? this.resourceExternal.get(civ) : null;
const buy = {};
const sell = {};
for (const id of RESOURCE_IDS) {
const b = record && record.buy[id];
const s = record && record.sell[id];
buy[id] = { qty: b ? b.qty : 0, value: b ? b.value : 0 };
sell[id] = { qty: s ? s.qty : 0, value: s ? s.value : 0 };
}
return { buy, sell };
},
_resourceRecord(civ) {
let record = this.resourceSpend.get(civ);
if (!record) {
record = {
energy: 0, goods: 0, transport: 0, construction: 0, upkeep: 0, combat: 0,
buy: {}, sell: {},
};
this.resourceSpend.set(civ, record);
}
if (!record.buy) record.buy = {};
if (!record.sell) record.sell = {};
return record;
},
// Every city opens with most of a month of food, a little steel and luxury,
// and some high-tech, so an early game with no producer yet can still build.
_seedCityResources(city) {
const k = key(city.coords.x, city.coords.y);
const stock = emptyStock();
stock.food = foodMonthlyTarget(city.population) * 0.75;
stock.steel = steelNeedPerDay(city.population) * 45;
stock.luxury = luxuryNeedPerDay(city.population) * 45;
stock.hightech = 500;
this.resourceStock.set(k, stock);
},
_ensureResourceStock(k) {
let stock = this.resourceStock.get(k);
if (!stock) {
stock = emptyStock();
this.resourceStock.set(k, stock);
}
return stock;
},
// ------------------------------------------------------ starting industry --
// Lays the resource works a nation opens the game with, so a fresh economy
// already makes what its people and its industry need. Called during world
// generation, after the world and its balanced economy exist but before the
// budgets are founded, so the opening treasury reflects the works. It draws
// from the world seed, so a game replayed from the same seed lays out the
// same map. A nation short of land (a one-city island) gets as many works as
// fit and trades for the rest.
_seedResourceBuildings() {
const rng = new Random((this.seed ^ 0x27d4eb2f) >>> 0);
const components = this._resourceLandComponents();
// A work only counts when its land component holds one of the nation's
// cities: otherwise its grid has no demand and its output no store to feed.
const componentCivs = new Map();
for (const city of this.cities) {
const component = components.get(key(city.coords.x, city.coords.y));
if (component === undefined) continue;
if (!componentCivs.has(component)) componentCivs.set(component, new Set());
componentCivs.get(component).add(city.civ);
}
// The food the land grows is fixed for the whole pass, so charge its energy
// once. Ports lift it, so the region multiplier is folded in here.
const foodEnergy = this._foodEnergyByComponent(components);
const isNetwork = (k) => this.roads.has(k) || this.railways.has(k);
const touchesNetwork = (coords) => {
for (const neighbour of this._neighbours(coords)) {
if (isNetwork(key(neighbour.x, neighbour.y))) return true;
}
return false;
};
const claims = [];
for (let civ = 0; civ < this.civilisations.length; civ++) {
// Tiles are ranked so the works land where the transport network can
// reach them: on the network first, then beside it (a one-tile spur
// connects them), and only then off it.
const tiers = [[], [], []];
const tiersByComponent = new Map();
for (const coords of this._territoryByCiv.get(civ) || []) {
const k = key(coords.x, coords.y);
if (this.cityAt(coords) || this.tileImprovements.has(k)) continue;
const component = components.get(k);
if (component === undefined || !componentCivs.get(component)?.has(civ)) continue;
const tier = isNetwork(k) ? 0 : touchesNetwork(coords) ? 1 : 2;
tiers[tier].push(coords);
if (!tiersByComponent.has(component)) tiersByComponent.set(component, [[], [], []]);
tiersByComponent.get(component)[tier].push(coords);
}
for (const list of tiers) rng.shuffle(list);
for (const lists of tiersByComponent.values()) for (const list of lists) rng.shuffle(list);
const spots = tiers[0].concat(tiers[1], tiers[2]);
const byComponent = new Map();
for (const [component, lists] of tiersByComponent) {
byComponent.set(component, lists[0].concat(lists[1], lists[2]));
}
claims.push({ civ, spots, byComponent });
}
// Two nations open with a chip foundry -- the only high-tech the opening
// world makes -- so early research is a prize two powers share. The pair is
// drawn from the world seed and never the same nation twice; a nation with
// no land to host one simply goes without.
const fabCount = Math.max(0, this.mapConfig?.startingChipFabs ?? 2);
const fabCandidates = fabCount > 0 ? claims.filter((claim) => claim.spots.length > 0) : [];
rng.shuffle(fabCandidates);
const fabCivs = new Set(fabCandidates.slice(0, fabCount).map((claim) => claim.civ));
for (const claim of claims) {
this._layStartingIndustry(rng, components, foodEnergy, touchesNetwork, claim, fabCivs.has(claim.civ));
}
this._gdpPerCapitaCache.clear();
},
// The greedy heart of the seeding: each pass covers every resource the nation
// is still short of, converters first so the power pass sees their draw. It
// stops as soon as a pass changes nothing. A work off the transport network
// gets a one-tile road spur where it can, so its output can actually reach a
// city.
_layStartingIndustry(rng, components, foodEnergy, touchesNetwork, { civ, spots, byComponent }, chipFab = false) {
const used = new Set();
let cursor = 0;
const takeSpot = (component) => {
if (component !== undefined && byComponent.has(component)) {
const list = byComponent.get(component);
while (list.length > 0) {
const coords = list.shift();
const k = key(coords.x, coords.y);
if (this.tileImprovements.has(k) || used.has(k)) continue;
used.add(k);
return coords;
}
}
while (cursor < spots.length) {
const coords = spots[cursor];
cursor += 1;
const k = key(coords.x, coords.y);
if (this.tileImprovements.has(k) || used.has(k)) continue;
used.add(k);
return coords;
}
return null;
};
const place = (proto, component, count) => {
let laid = 0;
for (let i = 0; i < count; i++) {
const coords = takeSpot(component);
if (!coords) break;
const k = key(coords.x, coords.y);
this.tileImprovements.set(k, proto.id);
this.tileImprovementOwner.set(k, civ);
this.tileImprovementHp.set(k, TILE_IMPROVEMENT_HP[proto.id] || 1000);
if (this._isProductionAgent(proto)) this._ensureBuildingAgent(k);
// A work beside the network gets a spur; one already on it needs none.
if (!this.roads.has(k) && !this.railways.has(k) && touchesNetwork(coords)) {
this.roads.add(k);
}
laid += 1;
}
return laid;
};
// A chip foundry is laid before the covering passes so the power seeding
// sees its draw and builds the grid needed to run it.
if (chipFab) place(tileImprovementById("chip_foundry"), undefined, 1);
for (let pass = 0; pass < SEED_MAX_PASSES; pass++) {
let changed = false;
const deficits = this._resourceSeedDeficits(civ);
for (const id of ["steel", "luxury"]) {
const target = deficits[id] * SEED_MATERIAL_MARGIN;
if (target <= 0) continue;
const proto = this._pickSeedProducer(rng, id);
const count = Math.max(1, Math.ceil(target / proto.outputPerDay));
if (place(proto, undefined, count) > 0) changed = true;
}
// Power is settled per connected grid, after the converters just placed
// have added their draw, and the plants themselves add fuel and upkeep.
// The GDP figure the ledger reads must reflect those converters, so the
// per-tile cache is dropped before it is consulted.
this._gdpPerCapitaCache.clear();
const energy = this._resourceSeedEnergy(civ, components, foodEnergy);
for (const entry of energy) {
const target = entry.deficit * SEED_ENERGY_MARGIN;
if (target <= 0) continue;
const proto = this._pickSeedProducer(rng, "energy");
const count = Math.max(1, Math.ceil(target / this._netEnergyOutput(proto)));
if (place(proto, entry.component, count) > 0) changed = true;
}
if (!changed) break;
const settled = this._resourceSeedDeficits(civ);
const energyLeft = this._resourceSeedEnergy(civ, components, foodEnergy);
if (settled.steel <= 1 && settled.luxury <= 1 && settled.hightech <= 1 &&
energyLeft.every((entry) => entry.deficit <= 1e-6)) break;
}
},
// What a nation's lands still make less than they need, in tonnes or carats a
// day: the people's appetite plus the works' own wear against what the
// resource buildings produce. Energy is settled separately, per grid.
_resourceSeedDeficits(civ) {
const population = this.getPlayerPopulation(civ);
const upkeep = this.getCivUpkeepResources(civ);
const made = this._civResourceProduction(civ);
return {
steel: steelNeedPerDay(population) + (upkeep.steel || 0) - made.steel,
luxury: luxuryNeedPerDay(population) - made.luxury,
hightech: (upkeep.hightech || 0) - made.hightech,
};
},
// The daily output of a nation's material buildings, by resource. Energy is
// not a stock, so it is left out and handled by the grid ledger.
_civResourceProduction(civ) {
const made = { steel: 0, luxury: 0, hightech: 0 };
for (const [k, id] of this.tileImprovements) {
if (this.tileImprovementOwner.get(k) !== civ) continue;
const proto = tileImprovementById(id);
if (!isResourceTileBuilding(proto) || proto.resource === "energy") continue;
made[proto.resource] = (made[proto.resource] || 0) + this.resourceBuildingOutputAt(k, proto);
}
return made;
},
// Each of a nation's connected grids that is still short of power: the cities
// and industry it draws, plus the energy the land's food takes, against what
// the plants on it make. Returns one entry per short grid.
_resourceSeedEnergy(civ, components, foodEnergy) {
const ledger = this._buildEnergyLedger(components);
const result = [];
for (const [component, entry] of ledger) {
if (entry.civ !== civ) continue;
const food = foodEnergy.get(component) || 0;
const deficit = entry.cityEnergy + entry.converterEnergy + food - entry.supply;
if (deficit > 0) result.push({ component, deficit });
}
return result;
},
// The energy the land's food costs each connected grid, in kWh a day, ports
// included. Fixed for the whole seeding pass.
_foodEnergyByComponent(components) {
const result = new Map();
const regionMultipliers = new Map();
for (const k in this.tiles) {
const coords = parseKey(k);
if (!this._isLand(coords)) continue;
const population = this.tilePopulation.get(k) || 0;
if (population <= 0) continue;
const component = components.get(k);
if (component === undefined) continue;
const city = this.regionCityAt(coords);
let multiplier = 1;
if (city) {
multiplier = regionMultipliers.get(city.id);
if (multiplier === undefined) {
multiplier = this._regionFoodMultiplier(city);
regionMultipliers.set(city.id, multiplier);
}
}
const produced = tileFoodOutput(this.tiles[k]) * multiplier;
result.set(component, (result.get(component) || 0) + producerEnergyPerDay("food", produced));
}
return result;
},
// What a plant actually adds to its grid: its output less the fuel it burns
// and the power its own upkeep draws.
_netEnergyOutput(proto) {
return (proto.outputPerDay || 0) -
(proto.fuelEnergyPerDay || 0) -
protoUpkeep(proto, proto.buildCost || 0).energy;
},
// A random producer of one resource, chosen from the seeded catalogue.
_pickSeedProducer(rng, resource) {
const ids = SEED_PRODUCERS[resource];
return tileImprovementById(ids[rng.range(0, ids.length - 1)]);
},
getResourcePrice(id) {
const price = this.resourcePrices ? this.resourcePrices.get(id) : undefined;
return price !== undefined ? price : (RESOURCE_MARKET_BASE[id] || 1);
},
setResourcePrice(id, value) {
this.resourcePrices.set(id, value);
},
// The price a resource carried at the last daily update, for the day-on-day
// arrows. Falls back to today's price before the first update.
getResourcePrevPrice(id) {
const previous = this._prevResourcePrices ? this._prevResourcePrices.get(id) : undefined;
return previous === undefined ? this.getResourcePrice(id) : previous;
},
// The consumption-weighted move in the world's commodity prices over the last
// day, a positive fraction when they rose overall. Each commodity is weighted
// by what the world consumed of it, valued at today's price, so a dear
// commodity the world uses little of does not swamp the index.
_recordCommodityInflation(market, previousPrices) {
this._prevResourcePrices = previousPrices;
let now = 0;
let before = 0;
for (const id of RESOURCE_IDS) {
const stats = market.get(id);
const weight = Math.max(0, stats ? stats.demand : 0);
if (weight <= 0) continue;
const price = this.getResourcePrice(id);
const previous = previousPrices.get(id) || price;
now += weight * price;
before += weight * previous;
}
this.commodityInflation = before > 0 ? now / before - 1 : 0;
},
// Snapshots every commodity's world price at the start of a new month. Called
// from the monthly rollover, so the series holds one opening price per month.
_sampleResourcePrices() {
if (!this.resourcePriceHistory) return;
for (const id of RESOURCE_IDS) {
const series = this.resourcePriceHistory.get(id) || [];
series.push(this.getResourcePrice(id));
this.resourcePriceHistory.set(id, series);
}
},
// A commodity's price index and its month-on-month and year-on-year inflation,
// from the monthly series. The index is 100 at the opening price in January
// 2000. Monthly compares with last month's opening, yearly with the same month
// a year earlier; through 2000 there is no earlier year, so the year's first
// month stands in and yearly reads as growth since the start of the game.
resourceInflation(id) {
const series = this.resourcePriceHistory ? this.resourcePriceHistory.get(id) : null;
const price = this.getResourcePrice(id);
if (!series || series.length === 0) {
return { index: 100, monthly: 0, yearly: 0 };
}
const base = series[0] || price;
const index = base > 0 ? (price / base) * 100 : 100;
// No previous month before February 2000, so monthly reads zero there.
const previous = series.length >= 2 ? series[series.length - 2] : null;
const yearAgo = series.length >= 13 ? series[series.length - 13] : base;
return {
index,
monthly: previous > 0 ? price / previous - 1 : 0,
yearly: yearAgo > 0 ? price / yearAgo - 1 : 0,
};
},
// The storage a tile has, or null when it is neither a city nor a resource
// building. Used both by the simulation and the delivery graph.
_resourceNodeAt(coords) {
const city = this.cityAt(coords);
if (city) return { kind: "city", civ: city.civ, city };
const k = key(coords.x, coords.y);
const id = this.tileImprovements.get(k);
if (!id) return null;
const proto = tileImprovementById(id);
if (!isResourceTileBuilding(proto)) return null;
return { kind: "building", civ: this.tileImprovementOwner.get(k), proto };
},
getCityResourceStock(city) {
return this._ensureResourceStock(key(city.coords.x, city.coords.y));
},
// What one city's people need in a day, and how that compares with its store.
getCityResourceInfo(city) {
const economy = this.getCityEconomy(city);
const population = economy.population;
const stock = this.getCityResourceStock(city);
const needs = {
food: foodNeedPerDay(population),
steel: steelNeedPerDay(population),
luxury: luxuryNeedPerDay(population),
};
const shortage = this._resourceShortages.get(city.id) || {};
return {
cityId: city.id,
population: Math.round(population),
energyPerDay: cityEnergyPerDay(economy.gdp),
stock: { ...stock },
needs,
foodTarget: foodMonthlyTarget(population),
shortage: { ...shortage },
};
},
// What one region holds and draws per day, for its city panel: the reserve of
// every storable resource, that reserve's market value, and the private
// sector's daily consumption -- the people's staples, the region's building
// upkeep and the grid power its industry pulls.
getRegionResourceInfo(city) {
const economy = this.getCityEconomy(city);
const population = economy.population;
const store = this.getCityResourceStock(city);
const consumption = {
food: foodNeedPerDay(population),
steel: steelNeedPerDay(population),
luxury: luxuryNeedPerDay(population),
hightech: 0,
energy: cityEnergyPerDay(economy.gdp),
};
for (const [index, level] of Object.entries(city.buildings || {})) {
if (level <= 0) continue;
const proto = this.protoBuildings[Number(index)];
if (!proto) continue;
const costs = this.buildingUpkeepResources(proto, level);
consumption.steel += costs.steel || 0;
consumption.energy += costs.energy || 0;
consumption.hightech += costs.hightech || 0;
}
const stock = {};
let value = 0;
for (const id of STORABLE_RESOURCE_IDS) {
const amount = store[id] || 0;
stock[id] = amount;
value += amount * this.getResourcePrice(id);
}
return { stock, consumption, value };
},
// The region's unmet resource needs over the last day: for each resource the
// share of the day's need it could not buy, and the amount that stands for,
// so the People tab can say what ran short and by how much.
getRegionShortages(city) {
const fractions = (this._resourceShortages && this._resourceShortages.get(city.id)) || {};
const population = this.getCityEconomy(city).population;
const needs = {
food: foodNeedPerDay(population),
steel: steelNeedPerDay(population),
luxury: luxuryNeedPerDay(population),
};
const shortages = {};
for (const id of ["food", "steel", "luxury"]) {
const fraction = fractions[id] || 0;
if (!(fraction > 0)) continue;
shortages[id] = { fraction, amount: fraction * needs[id] };
}
return shortages;
},
// Every resource figure one nation's cities hold, for the summary panel: the
// summed stock, the summed daily need and the summed production. Energy has
// no store, so it travels as a daily demand and the plants' daily supply.
getCivResourceSummary(civ) {
const stock = emptyStock();
const need = emptyStock();
let energyNeed = 0;
for (const city of this.cities) {
if (city.civ !== civ) continue;
const store = this.getCityResourceStock(city);
for (const id of STORABLE_RESOURCE_IDS) stock[id] += store[id] || 0;
const economy = this.getCityEconomy(city);
const population = economy.population;
need.food += foodNeedPerDay(population);
need.steel += steelNeedPerDay(population);
need.luxury += luxuryNeedPerDay(population);
energyNeed += cityEnergyPerDay(economy.gdp);
}
const production = emptyStock();
let energySupply = 0;
for (const [k, id] of this.tileImprovements) {
if (this.tileImprovementOwner.get(k) !== civ) continue;
const proto = tileImprovementById(id);
if (!isResourceTileBuilding(proto)) continue;
if (proto.resource === "energy") {
energySupply += this.resourceBuildingOutputAt(k, proto);
} else {
// A converter makes only what it can sell, so report the throttled
// figure the daily tick used rather than the nameplate capacity.
const throttle = this._converterScales ? (this._converterScales.get(k) ?? 1) : 1;
production[proto.resource] += this.resourceBuildingOutputAt(k, proto) * throttle;
}
}
// The land's harvest is part of what the nation produces, and so is the
// extra food its regions force from energy when the harvest falls short.
const foodMultipliers = new Map();
for (const coords of this._territoryByCiv.get(civ) || []) {
const k = key(coords.x, coords.y);
if ((this.tilePopulation.get(k) || 0) <= 0) continue;
const city = this.regionCityAt(coords);
let multiplier = 1;
if (city) {
multiplier = foodMultipliers.get(city.id);
if (multiplier === undefined) {
multiplier = this._regionFoodMultiplier(city);
foodMultipliers.set(city.id, multiplier);
}
}
production.food += tileFoodOutput(this.tiles[k]) * multiplier;
}
if (this._foodSynthesis) {
for (const city of this.cities) {
if (city.civ !== civ) continue;
production.food += this._foodSynthesis.get(city.id) || 0;
}
}
const upkeep = this.getCivUpkeepResources(civ);
// What the nation actually consumes in a day: the people's needs plus the
// materials its buildings, units, works and transport wear out. Energy is a
// flow on both sides, so its row combines the grid demand and upkeep.
const consumed = {
food: need.food + (upkeep.food || 0),
steel: need.steel + (upkeep.steel || 0),
luxury: need.luxury + (upkeep.luxury || 0),
hightech: upkeep.hightech || 0,
energy: energyNeed + (upkeep.energy || 0),
};
return {
stock,
need,
production,
energy: { need: energyNeed, supply: energySupply },
// What the nation's buildings, units, works and transport wear out daily.
upkeep,
consumed,
// Yesterday's cross-nation deliveries: what the nation imported and sold.
trade: this._civResourceTrade(civ),
// What the nation bought from and sold outside its border over the day,
// with the money it moved, for the Economy tab's consumption table.
external: this._civExternalTrade(civ),
prices: Object.fromEntries(RESOURCE_IDS.map((id) => [id, this.getResourcePrice(id)])),
};
},
// One nation's imports and exports of every resource over the last simulated
// day, as plain per-resource objects. Cross-nation deliveries only; the
// global market is not attributed to a partner.
_civResourceTrade(civ) {
const record = this.resourceTrade ? this.resourceTrade.get(civ) : null;
const imports = {};
const exports = {};
for (const id of RESOURCE_IDS) {
imports[id] = record ? record.imports[id] || 0 : 0;
exports[id] = record ? record.exports[id] || 0 : 0;
}
return { imports, exports };
},
// Records a cross-nation delivery: the buyer gained `amount` of `id` and the
// seller shipped it. Both nations' daily trade ledgers are updated.
_recordResourceTrade(importer, exporter, id, amount) {
if (!(amount > 0)) return;
if (!this.resourceTrade) this.resourceTrade = new Map();
const record = (civ, kind) => {
if (civ < 0) return;
let entry = this.resourceTrade.get(civ);
if (!entry) {
entry = { imports: {}, exports: {} };
this.resourceTrade.set(civ, entry);
}
entry[kind][id] = (entry[kind][id] || 0) + amount;
};
record(importer, "imports");
if (exporter !== importer) record(exporter, "exports");
},
// --------------------------------------------------- construction steel --
// The steel and high-tech a construction spends. Every item needs steel
// proportional to its money cost; an advanced building (science, air, radar,
// chips) also needs high-tech, more with every level.
constructionResourceCost(proto, level = 0, buildCost = null) {
return sharedConstructionResourceCost(proto, level, buildCost);
},
// Pays a construction's material bill. The stock of the nation's own cities
// is bought first (the region, then its neighbours), each region paid for the
// material into its private cash; the rest is bought from reachable foreign
// regions. There is no global market, so a bill the reachable stores cannot
// cover refuses the construction. Energy is a flow rather than a store, so it
// is not procured here.
_payConstructionResources(civ, coords, resources, category = "construction", source = null) {
const steel = resources.steel || 0;
const hightech = resources.hightech || 0;
const energy = resources.energy || 0;
if (steel <= 0 && hightech <= 0 && energy <= 0) return true;
const payer = this._treasuryPayer(civ);
const fromStock = this._drawFromNearest(civ, coords, { steel, hightech }, payer);
const shortfall = {
steel: steel - fromStock.steel,
hightech: hightech - fromStock.hightech,
};
const v = this.currencyValue(civ);
// What the nation's own stores supplied is paid to the owning regions.
let spentSteel = fromStock.steel * this.getResourcePrice("steel") / v;
let spentTech = fromStock.hightech * this.getResourcePrice("hightech") / v;
// The whole bill, paid or drawn from stores, is the construction's material
// cost for the month's budget report.
this._recordBudgetEconomic(
civ,
category,
steel * this.getResourcePrice("steel") +
hightech * this.getResourcePrice("hightech") +
energy * this.getResourcePrice("energy")
);
let covered = shortfall.steel <= 0 && shortfall.hightech <= 0;
if (!covered) {
const city = this._nearestCity(civ, coords);
if (city) {
const nodes = this._resourceNodes();
const haul = { steel: 0, hightech: 0 };
let gotSteel = 0;
let gotTech = 0;
if (shortfall.steel > 0) {
const before = payer.total;
gotSteel = this._procureFromNeighbours(city, "steel", shortfall.steel, haul, nodes, { payer });
spentSteel += payer.total - before;
}
if (shortfall.hightech > 0) {
const before = payer.total;
gotTech = this._procureFromNeighbours(city, "hightech", shortfall.hightech, haul, nodes, { payer });
spentTech += payer.total - before;
}
covered = gotSteel + 1e-6 >= shortfall.steel && gotTech + 1e-6 >= shortfall.hightech;
}
}
if (spentSteel > 0) {
this._recordBudgetCash(civ, category, -spentSteel);
this._recordBudgetCategoryResource(civ, category, "steel", spentSteel, source);
}
if (spentTech > 0) {
this._recordBudgetCash(civ, category, -spentTech);
this._recordBudgetCategoryResource(civ, category, "hightech", spentTech, source);
}
if (spentSteel + spentTech > 0) {
this._recordResourceSpend(civ, "construction", spentSteel + spentTech, {
steel: spentSteel,
hightech: spentTech,
});
}
return covered;
},
// Pulls each material from the nearest of the nation's cities to `coords`,
// capped by what that city holds, and buys it: with a `payer` the state pays
// the owning region the material's value into its private cash, so the store
// is never taken for free. Returns how much of each was taken.
_drawFromNearest(civ, coords, wanted, payer = null) {
const taken = { steel: 0, hightech: 0 };
const remaining = { steel: wanted.steel || 0, hightech: wanted.hightech || 0 };
const cities = this.cities
.filter((city) => city.civ === civ)
.sort((a, b) =>
this.topology.tileDistance(a.coords, coords) -
this.topology.tileDistance(b.coords, coords));
for (const city of cities) {
const store = this.getCityResourceStock(city);
for (const id of ["steel", "hightech"]) {
if (remaining[id] <= 0) continue;
const available = store[id] || 0;
const take = Math.min(available, remaining[id]);
if (take <= 0) continue;
store[id] = available - take;
remaining[id] -= take;
taken[id] += take;
this._payRegionForMaterial(city, id, take, payer);
}
if (remaining.steel <= 0 && remaining.hightech <= 0) break;
}
return taken;
},
// The state buys a material out of a region's store: the region is paid the
// material's value into its private cash and the payer (the treasury) carries
// the bill. Without a payer there is nothing to settle.
_payRegionForMaterial(city, id, amount, payer) {
if (!(amount > 0) || !payer) return;
const cost = amount * this.getResourcePrice(id) / this.currencyValue(city.civ);
payer.charge(cost);
this._creditRegionCash(city, this.currencyOf(city.civ).code, cost);
},
// ---------------------------------------------------------- upkeep --
// What one city building level wears out in a day. A level's upkeep is priced
// off the cost of raising that level, so it grows with the level.
buildingUpkeepResources(proto, level) {
return protoUpkeep(proto, buildingBuildCost(proto, level - 1));
},
unitUpkeepResources(proto) {
return protoUpkeep(proto, proto.cost);
},
// Everything a nation's buildings, units, tile works and transport network
// wear out in a day, split by the budget category that owns it, so the monthly
// report can attribute the cash and the market value. Energy is the exception
// units do not pay: it is fed to the grid through the city/improvement a thing
// stands in, so it is settled by `_buildEnergyLedger` instead.
getCivUpkeepBreakdown(civ) {
const categories = {
buildings: { steel: 0, energy: 0, hightech: 0, parts: [] },
units: { steel: 0, energy: 0, hightech: 0, parts: [] },
works: { steel: 0, energy: 0, hightech: 0, parts: [] },
transport: { steel: 0, energy: 0, hightech: 0, parts: [] },
};
// `label` names the thing that wore out -- a building, unit, work or
// transport type -- so the budget row can show what consumed each resource.
const add = (category, costs, label = null) => {
const steel = costs.steel || 0;
const energy = costs.energy || 0;
const hightech = costs.hightech || 0;
category.steel += steel;
category.energy += energy;
category.hightech += hightech;
if (label) category.parts.push({ label, steel, energy, hightech });
};
for (const city of this.cities) {
if (city.civ !== civ) continue;
for (const [index, level] of Object.entries(city.buildings || {})) {
if (level <= 0) continue;
const proto = this.protoBuildings[Number(index)];
if (proto) add(categories.buildings, this.buildingUpkeepResources(proto, level), proto.name);
}
}
const battle = this._battleUnitIds();
for (const unit of this.units) {
if (unit.civ !== civ) continue;
const proto = this.unitProto(unit);
if (!proto) continue;
// A unit in battle burns through its stores faster.
const statuses = this._effectiveStatuses(unit, battle.has(unit.id));
const multiplier = this._statusUpkeepMultiplier(statuses);
const costs = this.unitUpkeepResources(proto);
add(categories.units, {
steel: costs.steel * multiplier,
hightech: costs.hightech * multiplier,
}, proto.name);
}
for (const [k, id] of this.tileImprovements) {
if (this.tileImprovementOwner.get(k) !== civ) continue;
const proto = tileImprovementById(id);
if (!proto) continue;
const costs = protoUpkeep(proto, proto.buildCost || 0);
if (proto.fuelEnergyPerDay) costs.energy = (costs.energy || 0) + proto.fuelEnergyPerDay;
add(categories.works, costs, proto.name);
}
const counts = this._transportTiles(civ);
if (counts.road > 0) {
add(categories.transport, {
steel: counts.road * RESOURCE_RULES.transportUpkeepSteel.road,
}, "Roads");
}
if (counts.railway > 0) {
add(categories.transport, {
steel: counts.railway * RESOURCE_RULES.transportUpkeepSteel.railway,
}, "Railways");
}
// Merge the per-thing parts into one entry per label, so a building type
// spread over several cities is a single line in the budget.
for (const category of Object.values(categories)) {
const byLabel = new Map();
for (const part of category.parts) {
const merged = byLabel.get(part.label)
|| { label: part.label, steel: 0, energy: 0, hightech: 0 };
merged.steel += part.steel;
merged.energy += part.energy;
merged.hightech += part.hightech;
byLabel.set(part.label, merged);
}
category.sources = Array.from(byLabel.values());
delete category.parts;
}
return categories;
},
// The same daily wear as one materials demand, for the summary panels and the
// market-value forecast.
getCivUpkeepResources(civ) {
const categories = this.getCivUpkeepBreakdown(civ);
const total = { steel: 0, energy: 0, hightech: 0, food: 0 };
for (const costs of Object.values(categories)) {
total.steel += costs.steel;
total.energy += costs.energy;
total.hightech += costs.hightech;
}
return total;
},
// An infantry unit's soldiers eat like civilians, doubled in combat and
// tripled while wounded and resting in one of their own cities. Used by the
// supply tick to decide how fast a unit's carried food runs down.
_unitFoodRate(unit, proto, inBattle = false) {
const population = proto && proto.infantry ? (proto.population || RESOURCE_RULES.unitPopulation) : 0;
if (population <= 0) return 0;
const city = this.cityAt(unit.coords);
const resting = !!(city && city.civ === unit.civ && unit.hp < unit.maxHp);
const multiplier = resting
? RESOURCE_RULES.infantryRestingFoodMultiplier
: inBattle
? RESOURCE_RULES.infantryCombatFoodMultiplier
: 1;
return foodNeedPerDay(population) * multiplier;
},
// Pulls a material from the nation's cities and, with a `payer`, buys it from
// each owning region into its private cash. Returns what is still missing.
_drawFromCities(civ, id, amount, payer = null) {
let remaining = Math.max(0, amount);
for (const city of this.cities) {
if (remaining <= 0) break;
if (city.civ !== civ) continue;
const store = this.getCityResourceStock(city);
const take = Math.min(store[id] || 0, remaining);
if (take <= 0) continue;
store[id] = (store[id] || 0) - take;
remaining -= take;
this._payRegionForMaterial(city, id, take, payer);
}
return remaining;
},
// A tile improvement that was raised with money to spare keeps that reserve
// for its own upkeep. Each day, before the state is billed, every work with a
// reserve covers as much of its own steel and high-tech wear as the money
// reaches; the reserve is spent and the nation's `works` and `transport`
// demand is cut by what it covered, so a well funded work spares the treasury
// until it runs dry. The spent money is paid into the nation's regions, since
// it is the private sector the state would otherwise have paid.
_applyWorksMaintenance(civ, categories) {
if (!this.improvementCash || !categories) return;
const steelUnit = this._marketUnitCost("steel");
const techUnit = this._marketUnitCost("hightech");
let covered = 0;
// Spend one work's reserve against its own steel/high-tech wear, trimming
// the matching budget category by whatever the reserve could cover.
const apply = (k, steel, hightech, category) => {
const reserve = this.improvementCash.get(k) || 0;
if (!(reserve > 0)) return;
const value = steel * steelUnit + hightech * techUnit;
if (!(value > 0)) return;
const fraction = Math.min(1, reserve / value);
const spent = value * fraction;
covered += spent;
const after = reserve - spent;
if (after > 0) this.improvementCash.set(k, after);
else this.improvementCash.delete(k);
if (category) {
category.steel = Math.max(0, (category.steel || 0) - steel * fraction);
category.hightech = Math.max(0, (category.hightech || 0) - hightech * fraction);
}
};
for (const [k, id] of this.tileImprovements) {
if (this.tileImprovementOwner.get(k) !== civ) continue;
const proto = tileImprovementById(id);
if (!proto) continue;
const costs = protoUpkeep(proto, proto.buildCost || 0);
apply(k, costs.steel || 0, costs.hightech || 0, categories.works);
}
for (const [tiles, id] of [[this.roads, "road"], [this.railways, "railway"]]) {
const steel = RESOURCE_RULES.transportUpkeepSteel[id] || 0;
for (const k of tiles) {
if (this.civAt(parseKey(k)) !== civ) continue;
apply(k, steel, 0, categories.transport);
}
}
// The reserves paid the private sector the state would otherwise have paid.
this._creditRegions(civ, covered);
},
// Settles one nation's daily upkeep materials: buy the stores from the regions
// that hold them, buy the rest from reachable regions, and return the fraction
// of the bill the nation could actually settle. With no global market, a bill
// the reachable stores cannot cover is left unmet, and the rest is unrest.
_settleUpkeep(civ, categories) {
const ids = ["buildings", "units", "works", "transport"];
const steelUnit = this._marketUnitCost("steel");
const techUnit = this._marketUnitCost("hightech");
const payer = this._treasuryPayer(civ);
let totalSteel = 0;
let totalTech = 0;
let drawnSteel = 0;
let drawnTech = 0;
let drawnSteelCost = 0;
let drawnTechCost = 0;
const shortfalls = {};
// Draw each category's demand from the shared city stores, paying the owning
// region. The totals do not depend on the order, only which category gets
// the stock first does.
for (const id of ids) {
const demand = categories[id] || {};
const steel = demand.steel || 0;
const hightech = demand.hightech || 0;
const beforeSteel = payer.total;
const steelShort = steel > 0 ? this._drawFromCities(civ, "steel", steel, payer) : 0;
drawnSteelCost += payer.total - beforeSteel;
const beforeTech = payer.total;
const techShort = hightech > 0 ? this._drawFromCities(civ, "hightech", hightech, payer) : 0;
drawnTechCost += payer.total - beforeTech;
shortfalls[id] = { steel: steelShort, hightech: techShort };
totalSteel += steel;
totalTech += hightech;
drawnSteel += steel - steelShort;
drawnTech += hightech - techShort;
}
const steelShortTotal = totalSteel - drawnSteel;
const techShortTotal = totalTech - drawnTech;
// The market value of a category's wear is an economic cost whether the
// stores covered it or the treasury had to buy it.
for (const id of ids) {
const demand = categories[id] || {};
this._recordBudgetEconomic(
civ,
id,
(demand.steel || 0) * steelUnit + (demand.hightech || 0) * techUnit
);
}
if (totalSteel <= 0 && totalTech <= 0) return 1;
// The shortfall is bought region by region, each city taking its share, from
// the nearest reachable stores -- home regions first, then foreign ones.
const cities = this.cities.filter((city) => city.civ === civ);
const shares = this._populationShares(cities);
const nodes = this._resourceNodes();
let gotSteel = 0;
let gotTech = 0;
let spentSteel = drawnSteelCost;
let spentTech = drawnTechCost;
for (let i = 0; i < cities.length; i++) {
const city = cities[i];
const allowSea = this._cityMarketAccess(city);
const haul = { steel: 0, hightech: 0 };
if (steelShortTotal > 0) {
const before = payer.total;
gotSteel += this._procureFromNeighbours(
city, "steel", steelShortTotal * shares[i], haul, nodes, { payer, allowSea }
);
spentSteel += payer.total - before;
}
if (techShortTotal > 0) {
const before = payer.total;
gotTech += this._procureFromNeighbours(
city, "hightech", techShortTotal * shares[i], haul, nodes, { payer, allowSea }
);
spentTech += payer.total - before;
}
}
if (spentSteel + spentTech > 0) {
this._recordResourceSpend(civ, "upkeep", spentSteel + spentTech, {
steel: spentSteel,
hightech: spentTech,
});
// Split the cash across the categories that wore the material, then across
// the buildings, units, works and transport that consumed it.
for (const id of ids) {
const demand = categories[id] || {};
const steelCash = totalSteel > 0 ? spentSteel * ((demand.steel || 0) / totalSteel) : 0;
const techCash = totalTech > 0 ? spentTech * ((demand.hightech || 0) / totalTech) : 0;
const cash = steelCash + techCash;
if (cash > 0) this._recordBudgetCash(civ, id, -cash);
const sources = (categories[id] && categories[id].sources) || [];
if (steelCash > 0) {
this._recordResourceBySource(civ, id, "steel", steelCash, sources, "steel");
}
if (techCash > 0) {
this._recordResourceBySource(civ, id, "hightech", techCash, sources, "hightech");
}
}
}
const Psteel = this.getResourcePrice("steel");
const Ptech = this.getResourcePrice("hightech");
const fullValue = totalSteel * Psteel + totalTech * Ptech;
if (!(fullValue > 0)) return 1;
const settled = (drawnSteel + gotSteel) * Psteel + (drawnTech + gotTech) * Ptech;
return Math.min(1, settled / fullValue);
},
// Files a category's market purchase of one resource under the sources that
// consumed it -- the building, unit, work or transport type -- split in
// proportion to how much of that resource each source wore out.
_recordResourceBySource(civ, category, resource, cash, sources, field) {
let total = 0;
for (const source of sources) total += source[field] || 0;
if (!(total > 0)) {
this._recordBudgetCategoryResource(civ, category, resource, cash);
return;
}
let remaining = cash;
for (let i = 0; i < sources.length; i++) {
const source = sources[i];
const amount = i === sources.length - 1
? remaining
: cash * ((source[field] || 0) / total);
remaining -= amount;
if (amount > 0) {
this._recordBudgetCategoryResource(civ, category, resource, amount, source.label);
}
}
},
// ------------------------------------------------------ combat use --
// What one use of a weapon spends. Guns and aircraft are cheap parked and
// costly to fire: the state draws its stores and buys the rest from reachable
// regions. The payment is atomic -- if the reachable stores cannot cover it,
// the drawn stock is put back and nothing is spent. Returns whether the shot
// could be paid for.
payCombatResources(civ, kind, count = 1) {
const consumption = RESOURCE_RULES.combatConsumption[kind];
if (!consumption) return true;
const wanted = {
steel: (consumption.steel || 0) * count,
hightech: (consumption.hightech || 0) * count,
};
if (wanted.steel <= 0 && wanted.hightech <= 0) return true;
const coords = this._anyCityCoords(civ);
const payer = this._treasuryPayer(civ);
const drawn = this._drawFromNearest(civ, coords, wanted, payer);
const missing = {
steel: wanted.steel - drawn.steel,
hightech: wanted.hightech - drawn.hightech,
};
const v = this.currencyValue(civ);
let gotSteel = 0;
let gotTech = 0;
let spentSteel = drawn.steel * this.getResourcePrice("steel") / v;
let spentTech = drawn.hightech * this.getResourcePrice("hightech") / v;
let covered = missing.steel <= 0 && missing.hightech <= 0;
if (!covered) {
const city = this._nearestCity(civ, coords);
if (city) {
const nodes = this._resourceNodes();
const haul = { steel: 0, hightech: 0 };
if (missing.steel > 0) {
const before = payer.total;
gotSteel = this._procureFromNeighbours(city, "steel", missing.steel, haul, nodes, { payer });
spentSteel += payer.total - before;
}
if (missing.hightech > 0) {
const before = payer.total;
gotTech = this._procureFromNeighbours(city, "hightech", missing.hightech, haul, nodes, { payer });
spentTech += payer.total - before;
}
covered = gotSteel + 1e-6 >= missing.steel && gotTech + 1e-6 >= missing.hightech;
}
}
if (spentSteel > 0) {
this._recordBudgetCash(civ, "military", -spentSteel);
this._recordBudgetCategoryResource(civ, "military", "steel", spentSteel);
}
if (spentTech > 0) {
this._recordBudgetCash(civ, "military", -spentTech);
this._recordBudgetCategoryResource(civ, "military", "hightech", spentTech);
}
if (spentSteel + spentTech > 0) {
this._recordResourceSpend(civ, "combat", spentSteel + spentTech, {
steel: spentSteel,
hightech: spentTech,
});
}
return covered;
},
_anyCityCoords(civ) {
for (const city of this.cities) if (city.civ === civ) return city.coords;
return { x: 0, y: 0 };
},
// ------------------------------------------------------- market access --
// Which landmass a tile sits on. Continents never change, so they are built
// once and cached.
_continentOf(coords) {
if (!this._continentByTile) this._buildContinents();
return this._continentByTile.get(key(coords.x, coords.y));
},
_buildContinents() {
this._continentByTile = new Map();
let id = 0;
for (const k in this.tiles) {
if (this.tiles[k].terrainClass !== "Land") continue;
if (this._continentByTile.has(k)) continue;
const queue = [parseKey(k)];
this._continentByTile.set(k, id);
while (queue.length > 0) {
const coords = queue.pop();
for (const neighbour of this._neighbours(coords)) {
const nk = key(neighbour.x, neighbour.y);
if (this._continentByTile.has(nk)) continue;
const tile = this.tiles[nk];
if (!tile || tile.terrainClass !== "Land") continue;
this._continentByTile.set(nk, id);
queue.push(neighbour);
}
}
id += 1;
}
},
// Whether a city can reach another continent's markets: its own continent
// holds a working friendly port. No working port, no foreign resources.
_cityMarketAccess(city) {
const continent = this._continentOf(city.coords);
for (const port of this.cities) {
if (port.civ !== city.civ) continue;
if (this._continentOf(port.coords) !== continent) continue;
if (this._hasSeaSupply(port.coords, port.civ)) return true;
}
return false;
},
_civMarketAccess(civ) {
for (const city of this.cities) {
if (city.civ !== civ) continue;
if (this._cityMarketAccess(city)) return true;
}
return false;
},
// The grid power a region's land harvest draws, after its food-efficiency
// research has cut the energy each tonne costs.
_foodHarvestEnergy(civ, produced) {
return producerEnergyPerDay("food", produced) /
(1 + this.resourceEfficiency(civ, "food"));
},
// ------------------------------------------------------------ daily tick --
_tickResources() {
if (!this.resourceStock) return;
this._resourceVersion += 1;
this.resourceLinks = [];
this.resourceSpend = new Map();
this.resourceTrade = new Map();
this.resourceExternal = new Map();
this._resourceShortages = new Map();
this._resetTaxLedger();
const market = new Map();
for (const id of RESOURCE_IDS) market.set(id, { supply: 0, demand: 0, price: this.getResourcePrice(id) });
this.resourceMarketStats = market;
const components = this._resourceLandComponents();
const ledger = this._buildEnergyLedger(components);
// Food is grown by every worked tile, from the land and how fertile it is
// rather than from the headcount, and stored by the region's city. Ports
// bring in extra, so a coastal region's multiplier is looked up once per
// region and reused for every tile it covers. The energy it takes is
// charged to the tile's grid component.
const pendingFood = [];
const foodMultipliers = new Map();
// The land's harvest per region, so the synthesis pass below knows who is
// short of what its people eat.
const regionFood = new Map();
for (const k in this.tiles) {
const coords = parseKey(k);
if (!this._isLand(coords)) continue;
const population = this.tilePopulation.get(k) || 0;
if (population <= 0) continue;
const city = this.regionCityAt(coords);
if (!city) continue;
let foodMultiplier = foodMultipliers.get(city.id);
if (foodMultiplier === undefined) {
foodMultiplier = this._regionFoodMultiplier(city);
foodMultipliers.set(city.id, foodMultiplier);
}
const produced = tileFoodOutput(this.tiles[k]) * foodMultiplier;
if (produced <= 0) continue;
regionFood.set(city.id, (regionFood.get(city.id) || 0) + produced);
pendingFood.push({
cityId: city.id,
coords: city.coords,
component: components.get(k),
amount: produced,
});
const component = ledger.get(components.get(k));
if (component) {
const foodPower = this._foodHarvestEnergy(component.civ, produced);
component.foodEnergy += foodPower;
component.foodConsumers.push({ city, amount: foodPower });
}
}
// A region whose own harvest falls short of what its people eat can force
// extra food from energy -- greenhouses and synthesis. Growing is cheapest
// for the first tonnes, so the region grows while the marginal energy for
// another tonne beats buying, then completes with the dearer option: what
// the reachable market cannot actually supply, it grows itself.
const foodPrice = this.getResourcePrice("food");
const energyPrice = this.getResourcePrice("energy");
const nodes = this._resourceNodes();
this._foodSynthesis = new Map();
for (const city of this.cities) {
const produced = regionFood.get(city.id) || 0;
const need = foodNeedPerDay(this.getCityEconomy(city).population);
const shortfall = need - produced;
if (!(shortfall > 0)) continue;
const allowSea = this._cityMarketAccess(city) &&
!this._isEncircled(city.coords, city.civ);
// The food the region can actually buy: the reachable surplus. A distant
// island reaches none, so it must grow every tonne itself.
let buyable = 0;
for (const supplier of this._nearbySuppliers(city, "food", nodes, allowSea)) {
buyable += this._nodeSurplus(supplier, "food");
}
// Food efficiency cheapens synthesis, so it is worth growing more before
// the market wins; the energy it actually spends falls by the same share.
const foodEfficiency = 1 + this.resourceEfficiency(city.civ, "food");
const extra = synthesisedFoodFor(
foodPrice, energyPrice / foodEfficiency, shortfall, buyable
);
if (!(extra > 0)) continue;
const componentId = components.get(key(city.coords.x, city.coords.y));
const component = ledger.get(componentId);
if (!component) continue;
const energy = foodSynthesisEnergy(extra) / foodEfficiency;
component.foodEnergy += energy;
component.foodConsumers.push({ city, amount: energy });
pendingFood.push({
cityId: city.id,
coords: city.coords,
component: componentId,
amount: extra,
});
this._foodSynthesis.set(city.id, extra);
}
// Settle every connected grid: what the plants make against what the cities
// and industry draw. A grid that runs short imports power from the reachable
// grids that have a surplus -- there is no global market to fall back on.
for (const component of ledger.values()) {
if (component.civ < 0) continue;
component.demand = component.cityEnergy + component.converterEnergy + component.foodEnergy;
component.imports = 0;
}
this._tradeGridPower(ledger, components);
for (const component of ledger.values()) {
if (component.civ < 0) continue;
component.available = component.supply + component.imports;
// Cities are served first, then the food the region grows, then the
// converters: a famine is worse than an idle factory, so the power that
// feeds people is never throttled away by industry.
const cityServed = Math.min(component.cityEnergy, component.available);
component.cityShortage = component.cityEnergy > 0
? Math.max(0, Math.min(1, (component.cityEnergy - cityServed) / component.cityEnergy))
: 0;
const afterCity = Math.max(0, component.available - cityServed);
const foodServed = Math.min(component.foodEnergy, afterCity);
component.foodScale = component.foodEnergy > 0 ? foodServed / component.foodEnergy : 1;
const afterFood = afterCity - foodServed;
component.industrialScale = component.converterEnergy > 0
? Math.max(0, Math.min(1, afterFood / component.converterEnergy))
: 1;
market.get("energy").supply += component.supply;
market.get("energy").demand += component.demand;
}
// A grid that cannot power its industry names the works left idle.
this._noteGridPowerShortages(ledger, components);
// Food lands in the region's store, scaled by the power the grid could give
// it. Food is served before the converters, so the harvest survives a
// power squeeze that idles the factories.
const grownFood = new Map();
for (const entry of pendingFood) {
const component = ledger.get(entry.component);
const scale = component ? component.foodScale : 1;
const produced = entry.amount * scale;
if (produced <= 0) continue;
this._ensureResourceStock(key(entry.coords.x, entry.coords.y)).food += produced;
market.get("food").supply += produced;
grownFood.set(entry.cityId, (grownFood.get(entry.cityId) || 0) + produced);
}
// Middlemen top up for neighbours whose own power-limited harvest fell short
// before anyone buys, so the relay stock is on hand in the same day.
this._relayFood(grownFood, nodes);
// Converters turn the power they were allocated into their material, held
// at the building's own tile.
for (const [k, id] of this.tileImprovements) {
const proto = tileImprovementById(id);
if (!isResourceTileBuilding(proto) || proto.resource === "energy") continue;
const component = ledger.get(components.get(k));
const scale = component ? component.industrialScale : 1;
const throttle = this._converterScales ? (this._converterScales.get(k) ?? 1) : 1;
const produced = this.resourceBuildingOutputAt(k, proto) * throttle * scale;
if (produced <= 0) continue;
this._ensureResourceStock(k)[proto.resource] += produced;
market.get(proto.resource).supply += produced;
}
// The people eat, wear and spend. Shortfalls are matched against reachable
// stores -- home regions first, then foreign ones.
this._consumeCityResources(components, market, nodes);
// Power is a paid private flow: the grid's consumers settle with its plants,
// after the day's sales so an agent can pay from what it just earned.
this._settleGridPower(ledger);
// A region that could not feed its people is in famine: it tells the world,
// and its people start dying.
this._tickFamine();
// Every nation's buildings, units and works then wear out: the state draws
// its stores and buys the rest, and an unpaid bill becomes unrest.
for (let civ = 0; civ < this.civilisations.length; civ++) {
const categories = this.getCivUpkeepBreakdown(civ);
// Works that finished with money to spare keep it against their own wear;
// that reserve is spent before the state is asked to pay.
this._applyWorksMaintenance(civ, categories);
const paid = this._settleUpkeep(civ, categories);
this._civilianUpkeepShortage(civ, 1 - paid);
}
// Once a day the world's imbalance sets the next price. The prices it
// replaces are kept, so the panel can show each commodity's day-on-day move
// and the consumption-weighted inflation it adds up to.
const previousPrices = new Map();
for (const id of RESOURCE_IDS) previousPrices.set(id, this.getResourcePrice(id));
for (const id of RESOURCE_IDS) {
const stats = market.get(id);
const price = nextMarketPrice(
RESOURCE_MARKET_BASE[id],
this.getResourcePrice(id),
stats.supply,
stats.demand
);
this.setResourcePrice(id, price);
stats.price = price;
}
this._recordCommodityInflation(market, previousPrices);
this._applyResourceShortages(ledger);
this._pruneResourceStock();
},
// Every land tile assigned to the connected component of owned land it sits
// on. Two cities of a nation share a grid only when their land is contiguous.
_resourceLandComponents() {
const component = new Map();
let next = 0;
for (const [startKey, owner] of this.territory) {
if (component.has(startKey)) continue;
const queue = [parseKey(startKey)];
component.set(startKey, next);
while (queue.length > 0) {
const coords = queue.pop();
for (const neighbour of this._neighbours(coords)) {
const nk = key(neighbour.x, neighbour.y);
if (component.has(nk)) continue;
if (this.territory.get(nk) !== owner) continue;
component.set(nk, next);
queue.push(neighbour);
}
}
next += 1;
}
return component;
},
// A per-component energy ledger, keyed by the component id from
// `_resourceLandComponents`. Each entry carries the owner and the day's draw and
// supply, filled in as the world is walked.
_buildEnergyLedger(components) {
const ledger = new Map();
// Each converter's production scale for the day, so its power draw matches
// what it actually intends to make. Read again by the production loop.
this._converterScales = new Map();
for (const [k, id] of components) {
if (ledger.has(id)) continue;
ledger.set(id, {
civ: this.territory.get(k),
supply: 0,
// The draw that makes up the physical balance: cities and their
// buildings, the converters and the land's food, plus the works' wear
// and the plants' own fuel as internal losses.
cityEnergy: 0,
converterEnergy: 0,
foodEnergy: 0,
fuelEnergy: 0,
// The part of `cityEnergy` the regions actually pay for, so the works'
// and plants' internal losses are not billed to them.
billableCityEnergy: 0,
demand: 0,
available: 0,
industrialScale: 1,
foodScale: 1,
cityShortage: 0,
// Who makes the power and who draws it, so the grid's bill can be
// settled between them: the plants sell, the converters pay from their
// own cash and the regions pay for their cities and their food.
producers: [],
cities: [],
converters: [],
foodConsumers: [],
imports: 0,
exports: 0,
importCost: 0,
});
}
for (const city of this.cities) {
const component = ledger.get(components.get(key(city.coords.x, city.coords.y)));
if (!component) continue;
let amount = cityEnergyPerDay(this.getCityEconomy(city).gdp);
// The city's buildings run on the grid too, so their operating power is
// charged to the same component.
for (const [index, level] of Object.entries(city.buildings || {})) {
if (level <= 0) continue;
const proto = this.protoBuildings[Number(index)];
if (proto) amount += this.buildingUpkeepResources(proto, level).energy;
}
component.cityEnergy += amount;
component.billableCityEnergy += amount;
component.cities.push({ city, amount });
}
for (const [k, id] of this.tileImprovements) {
const proto = tileImprovementById(id);
if (!proto) continue;
const component = ledger.get(components.get(k));
if (!component) continue;
if (isResourceTileBuilding(proto)) {
// Efficiency research cuts the power each commodity costs: the draw is
// divided by `1 + bonus`, so the same output needs less energy.
const efficiency = 1 + this.resourceEfficiency(component.civ, proto.resource);
if (proto.resource === "energy") {
const output = this.resourceBuildingOutputAt(k, proto);
component.supply += output;
component.producers.push({ k, output });
} else {
const agent = this.buildingAgents ? this.buildingAgents.get(k) : null;
const scale = this._converterProductionScale(k, proto, agent);
this._converterScales.set(k, scale);
const amount = this.resourceBuildingInputAt(k, proto, "energyPerDay") * scale / efficiency;
component.converterEnergy += amount;
component.converters.push({ k, amount });
}
if (proto.fuelEnergyPerDay) {
const fuel = this.resourceBuildingInputAt(k, proto, "fuelEnergyPerDay") / efficiency;
component.fuelEnergy += fuel;
component.cityEnergy += fuel;
}
}
// Every work's operating power is a draw on the grid; it rides with the
// cities and is not billed back to a region.
component.cityEnergy += protoUpkeep(proto, proto.buildCost || 0).energy;
}
return ledger;
},
// Moves power between grids. A grid in deficit buys from the reachable grids
// that hold a surplus, cheapest route first; power is a flow with no store, so
// it flows the same day. The seller's plants are paid for the power, and the
// buyer's consumers carry the cost. No government is involved.
_tradeGridPower(ledger, components) {
const price = this.getResourcePrice("energy");
const surplus = new Map();
for (const [id, component] of ledger) {
component.imports = 0;
component.exports = 0;
component.importCost = 0;
surplus.set(id, Math.max(0, component.supply - component.demand));
}
for (const [id, component] of ledger) {
if (component.civ < 0) continue;
const deficit = Math.max(0, component.demand - component.supply);
if (deficit <= 0) continue;
const origin = this._componentCity(id, components);
if (!origin) continue;
const reach = this._reachableComponents(origin, components, this._cityMarketAccess(origin));
const suppliers = Array.from(reach.entries())
.filter(([sid]) => sid !== id && (surplus.get(sid) || 0) > 0)
.sort((a, b) => a[1] - b[1]);
let needed = deficit;
for (const [sid, routeEnergy] of suppliers) {
if (needed <= 0) break;
const available = surplus.get(sid) || 0;
const take = Math.min(needed, available);
if (!(take > 0)) continue;
// The power costs its price plus the energy burned carrying it. A unit
// of energy weighs `tonnesPerUnit`, so the freight is charged on the
// real tonnage moved, not as if every kWh were a tonne.
const tonnes = take * (resourceById("energy")?.tonnesPerUnit || 1);
const cost = take * price + tonnes * routeEnergy * price;
const seller = ledger.get(sid);
this._payGridProducers(seller, cost, take);
if (seller.civ !== component.civ) {
this._recordExternalBuy(component.civ, "energy", take, cost);
this._recordExternalSell(seller.civ, "energy", take, cost);
}
surplus.set(sid, available - take);
component.imports += take;
component.importCost += cost;
seller.exports += take;
needed -= take;
}
}
},
// Settles one grid's power bill: the regions pay for their cities' and their
// food's draw, the converters pay for their own from their cash, and the
// plants are paid for what was actually delivered. The works' wear and the
// plants' fuel are the grid's internal losses, folded into the price.
_settleGridPower(ledger) {
const price = this.getResourcePrice("energy");
for (const component of ledger.values()) {
if (component.civ < 0) continue;
const cityServed = component.cityEnergy * (1 - (component.cityShortage || 0));
const billableCityServed = Math.min(cityServed, component.billableCityEnergy);
const foodServed = component.foodEnergy * (component.foodScale ?? 1);
const converterServed = component.converterEnergy * (component.industrialScale ?? 1);
const billableServed = billableCityServed + foodServed + converterServed;
if (!(billableServed > 0)) continue;
const totalConsumed = cityServed + foodServed + converterServed;
const ownUsed = Math.min(component.supply, totalConsumed);
const bill = ownUsed * price + (component.importCost || 0);
const unitPrice = bill / billableServed;
const code = this.currencyOf(component.civ).code;
const cityFactor = component.billableCityEnergy > 0
? billableCityServed / component.billableCityEnergy
: 0;
const foodFactor = component.foodEnergy > 0 ? foodServed / component.foodEnergy : 0;
const converterFactor = component.converterEnergy > 0
? converterServed / component.converterEnergy
: 0;
for (const entry of component.cities || []) {
const cost = entry.amount * cityFactor * unitPrice;
if (cost > 0) this._chargeRegionCash(entry.city, code, cost);
}
for (const entry of component.foodConsumers || []) {
const cost = entry.amount * foodFactor * unitPrice;
if (cost > 0) this._chargeRegionCash(entry.city, code, cost);
}
for (const entry of component.converters || []) {
const cost = entry.amount * converterFactor * unitPrice;
if (cost > 0) this._chargeBuildingCash(entry.k, code, cost);
}
this._payGridProducers(component, ownUsed * price, ownUsed);
}
},
// One city that sits in grid component `id`, the origin for a power trade.
_componentCity(id, components) {
for (const city of this.cities) {
if (components.get(key(city.coords.x, city.coords.y)) === id) return city;
}
return null;
},
// The tiles held by hostile troops: an army camped on a road or a fleet in a
// sea lane severs the route while it stands there. Trade, grid power and unit
// supply searches all route around these tiles, so moving a unit in forces
// the next recalculation to find another way -- or none.
_hostileOccupied(civ) {
const blocked = new Set();
for (const unit of this.units) {
if (unit.civ === civ) continue;
if (!this.isAtWar(civ, unit.civ)) continue;
if (!this._isMilitary(unit) || this._isAirUnit(unit)) continue;
blocked.add(key(unit.coords.x, unit.coords.y));
}
return blocked;
},
// The grid components reachable from a city over the transport network, mapped
// to the least energy it takes to carry a unit there. A sea leg needs a
// working port; a land leg needs road or railway; goods never cross the land
// of a nation you are at war with.
_reachableComponents(city, components, allowSea = true) {
const reach = new Map();
const origin = key(city.coords.x, city.coords.y);
const originComponent = components.get(origin);
const blocked = this._hostileOccupied(city.civ);
const visited = new Set([origin]);
const queue = [{ coords: city.coords, energy: 0 }];
while (queue.length > 0) {
const current = queue.shift();
for (const neighbour of this._neighbours(current.coords)) {
const nk = key(neighbour.x, neighbour.y);
if (visited.has(nk) || blocked.has(nk)) continue;
const tile = this.tiles[nk];
if (!tile || !(tile.terrainClass === "Land" || tile.terrainClass === "Sea" ||
tile.terrainClass === "Ice")) continue;
const sea = tile.terrainClass === "Sea";
let mode;
if (sea) {
if (!allowSea) continue;
mode = "ship";
} else {
if (this.railways.has(nk)) mode = "train";
else if (this.roads.has(nk)) mode = "road";
else continue;
const owner = this.civAt(neighbour);
if (owner >= 0 && owner !== city.civ && this.isAtWar(city.civ, owner)) continue;
}
const stepEnergy = deliveryEnergyForResource(
"food", 1, 1, mode, tile.movementCostMultiplier || 1
);
visited.add(nk);
const energy = current.energy + stepEnergy;
const component = components.get(nk);
if (component !== undefined && component !== originComponent) {
const best = reach.get(component);
if (best === undefined || energy < best) reach.set(component, energy);
}
queue.push({ coords: neighbour, energy });
}
}
return reach;
},
// Middlemen: a region that can reach a food surplus it does not itself need
// buys extra to resell to nearby regions that have no reach of their own. It
// charges a margin on that resale, which covers its transport and turns a
// profit, so food hops past a region that is itself a net importer.
_relayFood(grownFood, nodes) {
const need = new Map();
const stock = new Map();
for (const city of this.cities) {
need.set(city.id, foodNeedPerDay(this.getCityEconomy(city).population));
stock.set(city.id, this.getCityResourceStock(city).food || 0);
}
// What each region actually grew today, and whether it can reach any food
// surplus on its own.
const sources = new Map();
const allowSea = new Map();
for (const city of this.cities) {
const sea = this._cityMarketAccess(city) && !this._isEncircled(city.coords, city.civ);
allowSea.set(city.id, sea);
sources.set(city.id, this._nearbySuppliers(city, "food", nodes, sea));
}
for (const city of this.cities) {
const suppliers = sources.get(city.id);
if (!suppliers || suppliers.length === 0) continue;
let relayDemand = 0;
for (const other of this.cities) {
if (other.id === city.id) continue;
if ((sources.get(other.id) || []).length > 0) continue;
const shortfall = Math.max(0, need.get(other.id) - (grownFood.get(other.id) || 0));
if (shortfall <= 0) continue;
if (stock.get(other.id) >= need.get(other.id) * 2) continue;
// The buyer must be able to reach this region's store.
if (this.topology.tileDistance(city.coords, other.coords) > RESOURCE_TRADE_RADIUS) {
continue;
}
relayDemand += shortfall;
}
if (!(relayDemand > 0)) continue;
const wanted = relayDemand * MIDDLEMAN.coverDays;
const store = this.getCityResourceStock(city);
const bought = this._procureFromNeighbours(city, "food", wanted, store, nodes, {
allowSea: allowSea.get(city.id),
});
if (bought > 0) {
this._relayStock.set(city.id, (this._relayStock.get(city.id) || 0) + bought);
}
}
},
_consumeCityResources(components, market, nodes = null) {
nodes = nodes || this._resourceNodes();
// How much of each supplier's surplus earlier buyers have already claimed
// today, so a contested surplus goes to whoever bids highest.
this._nodeClaims = new Map();
// Starving regions buy first: a city topping up its month-long buffer must
// not strip a neighbour whose store is empty of the last reachable surplus.
const urgency = (city) => {
const store = this.getCityResourceStock(city);
const need = Math.max(1e-9, foodNeedPerDay(this.getCityEconomy(city).population));
return (store.food || 0) / need;
};
const order = this.cities.slice().sort((a, b) => urgency(a) - urgency(b));
for (const city of order) {
const population = this.getCityEconomy(city).population;
const needs = {
food: foodNeedPerDay(population),
steel: steelNeedPerDay(population),
luxury: luxuryNeedPerDay(population),
};
const store = this.getCityResourceStock(city);
const shortage = {};
const foodTarget = foodMonthlyTarget(population);
// An encircled city is cut off from neighbours and the foreign market:
// it lives off its own reserves until the ring is broken. A portless city
// can still trade over land on its own continent, but not across water.
const cutOff = this._isEncircled(city.coords, city.civ);
// A shipping lane needs a working port; an encircled city is cut off from
// every neighbour, home or foreign.
const allowSea = !cutOff && this._cityMarketAccess(city);
for (const id of ["food", "steel", "luxury"]) {
const need = needs[id];
market.get(id).demand += need;
const own = Math.min(store[id] || 0, need);
store[id] = (store[id] || 0) - own;
const dailyShortfall = need - own;
// Food is stocked ahead: the city tops its store up to 130% of the
// coming month's needs, not just today's. Steel and luxury are bought
// as they are eaten.
const wanted = id === "food"
? Math.max(dailyShortfall, foodTarget - store[id])
: dailyShortfall;
let bought = 0;
if (wanted > 0 && !cutOff) {
bought = this._procureFromNeighbours(city, id, wanted, store, nodes, { allowSea });
}
// What was bought against today's need is eaten today; only the food
// buffer left over stays in the store.
const consumed = Math.min(bought, dailyShortfall);
if (consumed > 0) store[id] -= consumed;
if (dailyShortfall > bought + 0.0001 && need > 0) {
shortage[id] = (dailyShortfall - bought) / need;
}
}
if (Object.keys(shortage).length > 0) this._resourceShortages.set(city.id, shortage);
}
},
// A region whose food ran out and could not be resupplied is in famine. The
// world is told the day it begins, once per famine rather than daily, and the
// region loses a share of its people every day it lasts. Food coming back ends
// it silently.
_tickFamine() {
for (const city of this.cities) {
const shortage = (this._resourceShortages && this._resourceShortages.get(city.id)) || {};
const starving = (shortage.food || 0) > 0;
const was = this._famine.has(city.id);
if (starving && !was) {
this._famine.add(city.id);
this._addNews({ type: NEWS_FAMINE, civ: city.civ, region: city.name });
} else if (!starving && was) {
this._famine.delete(city.id);
}
if (starving) this._applyFamineDeaths(city);
}
},
// Famine kills a fixed share of a region's people each day, spread over its
// tiles in proportion to where they live.
_applyFamineDeaths(city) {
let total = 0;
const tiles = [];
for (const coords of this.regionTiles(city)) {
const k = key(coords.x, coords.y);
const population = this.tilePopulation.get(k) || 0;
if (population <= 0) continue;
tiles.push({ k, population });
total += population;
}
if (total <= 0) return;
for (const tile of tiles) {
this.tilePopulation.set(tile.k, tile.population * (1 - FAMINE.dailyMortality));
}
this._populationVersion += 1;
},
// Whether a region is currently in famine, for its city panel.
isFamine(city) {
return !!(this._famine && this._famine.has(city.id));
},
// A city buys a material from the stores it can reach over the transport
// network. There is no global market: if no reachable store holds enough, the
// rest is simply not bought. Suppliers in the buyer's own nation come first;
// foreign regions are considered only to cover what home cannot. Within a
// tier the cheapest delivered price wins, and a surplus several buyers chase
// is bid up.
//
// The region pays from its own private cash by default; when `payer` is given
// (a government buy) the treasury pays instead. A foreign seller is paid in
// its national currency, acquired from that nation's central bank. Returns the
// amount bought and records each leg on the delivery graph.
_procureFromNeighbours(city, id, amount, cityStore, nodes, options = {}) {
const { allowSea = true, payer = null, foreignOnly = false, maxUnitCost = Infinity } = options;
let remaining = amount;
const price = this.getResourcePrice(id);
const energyPrice = this.getResourcePrice("energy");
if (!this._nodeClaims) this._nodeClaims = new Map();
const buyerCode = this.currencyOf(city.civ).code;
const quotes = [];
for (const supplier of this._nearbySuppliers(city, id, nodes, allowSea)) {
const surplus = this._nodeSurplus(supplier, id);
if (surplus <= 0) continue;
const foreign = supplier.civ >= 0 && supplier.civ !== city.civ;
if (foreignOnly && !foreign) continue;
const claimed = this._nodeClaims.get(key(supplier.coords.x, supplier.coords.y)) || 0;
const available = Math.max(0, surplus - claimed);
if (available <= 0) continue;
const premium = Math.min(
MONEY.maxBidMultiplier,
1 + MONEY.bidPremium * (claimed / Math.max(1e-9, surplus))
);
const goods = price * premium;
const freight = (supplier.routeEnergyPerTonne || 0) * energyPrice;
// A buyer with a locked price refuses a supplier quoting above it.
if (goods + freight > maxUnitCost) continue;
quotes.push({ supplier, available, foreign, goods, freight, unitCost: goods + freight });
}
// Home regions before foreign ones; then by delivered price.
quotes.sort((a, b) => (a.foreign - b.foreign) || (a.unitCost - b.unitCost));
for (const quote of quotes) {
if (remaining <= 0) break;
const { supplier, foreign, goods, freight } = quote;
const bought = Math.min(quote.available, remaining);
if (!(bought > 0)) continue;
// A region selling food it bought to relay charges its middleman margin on
// that stock, so the resale covers its transport and turns a profit.
const markup = this._middlemanMarkup(supplier, id, bought, price);
const value = (goods + freight) * bought + markup;
const vBuyer = this.currencyValue(city.civ);
const charge = payer
? (cost) => payer.charge(cost)
: (cost) => this._chargeRegionCash(city, buyerCode, cost);
// The seller is paid the goods' value in its own currency -- a home
// neighbour in the shared national currency, a foreigner after a trip to
// its central bank. Freight rides with the goods.
if (foreign) {
// The seller's government charges the foreign buyer an export tariff,
// in the seller's currency, on top of the goods.
const exportTax = value * this.getTaxRate(supplier.civ, "export", id);
// The central bank pays part of the bill from its reserves, so a region
// or converter imports food and energy more cheaply than the market.
const subsidy = this._centralBankImportSubsidy(city.civ, id, value + exportTax);
const acquired = this._regionAcquireCurrency(
city, supplier.civ, value + exportTax, charge, !payer, subsidy
);
if (!acquired) continue;
if (acquired.subsidised > 0) this._recordImportSubsidy(city.civ, acquired.subsidised);
const total = value + exportTax;
const sellerShare = total > 0 ? value / total : 1;
const sellerAmount = acquired.amount * sellerShare;
this._paySupplier(supplier, acquired.code, sellerAmount);
if (acquired.amount > sellerAmount) {
const sellerCity = supplier.city || this._regionCityNear(supplier.civ, supplier.coords);
this._recordTax(
supplier.civ, "export", id, acquired.amount - sellerAmount, sellerCity
);
}
// The buyer's own government taxes the national currency it had to spend
// to buy the foreign currency (nothing held means nothing spent).
if (!payer && acquired.spent > 0) {
const importRate = this.getTaxRate(city.civ, "import", id);
if (importRate > 0) {
const importTax = acquired.spent * importRate;
charge(importTax);
this._recordTax(city.civ, "import", id, importTax, city);
}
}
this._recordResourceTrade(city.civ, supplier.civ, id, bought);
this._recordExternalBuy(city.civ, id, bought, value / vBuyer);
this._recordExternalSell(supplier.civ, id, bought, value / this.currencyValue(supplier.civ));
this._recordCurrencyDemand(supplier.civ, value);
this._recordCurrencyDemand(city.civ, -value);
} else {
// A region buying from another region of its own country pays a sales
// tax on top of the price; the seller still gets the full value, and the
// government keeps the tax. Government purchases are not taxed.
const homeCost = value / vBuyer;
const salesTax = payer ? 0 : homeCost * this.getTaxRate(city.civ, "sales", id);
charge(homeCost + salesTax);
this._paySupplier(supplier, buyerCode, homeCost);
if (salesTax > 0) this._recordTax(city.civ, "sales", id, salesTax, city);
}
this._nodeClaims.set(
key(supplier.coords.x, supplier.coords.y),
(this._nodeClaims.get(key(supplier.coords.x, supplier.coords.y)) || 0) + bought
);
supplier.stock[id] -= bought;
cityStore[id] += bought;
remaining -= bought;
// A production building's sale feeds the demand trend it grows on.
if (supplier.kind === "building") {
this._recordBuildingSale(key(supplier.coords.x, supplier.coords.y), bought);
}
this.resourceLinks.push({
from: [supplier.coords.x, supplier.coords.y],
to: [city.coords.x, city.coords.y],
resource: id,
amount: bought,
});
}
return amount - remaining;
},
// Pays the producer that owns a surplus. A resource building is an
// independent private-sector agent, so its goods are paid into its own cash;
// a city's surplus is paid to the region that stores it.
_paySupplier(node, code, amount) {
if (!(amount > 0)) return;
if (node.kind === "building") {
this._creditBuildingCash(key(node.coords.x, node.coords.y), code, amount);
return;
}
const city = node.city || this._regionCityNear(node.civ, node.coords);
if (city) this._creditRegionCash(city, code, amount);
},
// A payer that draws on a nation's treasury, so a government purchase settles
// with the supplier while the treasury carries the bill (and may go into
// overdraft). `total` accumulates what was spent.
_treasuryPayer(civ) {
const payer = { total: 0 };
payer.charge = (cost) => {
if (!(cost > 0)) return;
this.budgets.set(civ, this.getBudget(civ) - cost);
payer.total += cost;
};
return payer;
},
// The nation's city nearest `coords`, the region that buys on its behalf when
// the government procures away from a specific city.
_nearestCity(civ, coords) {
let best = null;
let bestDistance = Infinity;
for (const city of this.cities) {
if (city.civ !== civ) continue;
const distance = this.topology.tileDistance(city.coords, coords);
if (distance < bestDistance) {
bestDistance = distance;
best = city;
}
}
return best;
},
// The market's delivered price per unit: the good plus the energy a nominal
// two-tile road delivery burns. Used to value materials, not to source them.
_marketUnitCost(id) {
const energyPerUnit = deliveryEnergyForResource(id, 1, 2, "road", 1);
return this.getResourcePrice(id) + energyPerUnit * this.getResourcePrice("energy");
},
_resourceNodes() {
const nodes = [];
for (const [k, stock] of this.resourceStock) {
const coords = parseKey(k);
const city = this.cityAt(coords);
if (city) {
nodes.push({ coords, civ: city.civ, city, stock, kind: "city" });
continue;
}
const id = this.tileImprovements.get(k);
const proto = id ? tileImprovementById(id) : null;
if (isResourceTileBuilding(proto)) {
nodes.push({
coords,
civ: this.tileImprovementOwner.get(k),
stock,
kind: "building",
proto,
});
}
}
return nodes;
},
// The storage nodes within reach of a city that hold a surplus of `id`, in
// order of transport energy. A breadth-first search over land and sea, capped
// at RESOURCE_TRADE_RADIUS tiles. `routeEnergyPerTonne` is the accumulated
// kWh to move one tonne from the city to that node.
_nearbySuppliers(city, id, nodes, allowSea = true) {
const results = [];
const byKey = new Map(nodes.map((node) => [key(node.coords.x, node.coords.y), node]));
const origin = key(city.coords.x, city.coords.y);
const blocked = this._hostileOccupied(city.civ);
const visited = new Set([origin]);
// Every delivery runs over the transport network: railways and roads on
// land, shipping lanes across water (which need a working port).
const queue = [{ coords: city.coords, tiles: 0, energy: 0 }];
while (queue.length > 0) {
const current = queue.shift();
const here = byKey.get(key(current.coords.x, current.coords.y));
if (here && key(here.coords.x, here.coords.y) !== origin) {
const hostile = here.civ >= 0 && here.civ !== city.civ && this.isAtWar(city.civ, here.civ);
if (!hostile && this._nodeSurplus(here, id) > 0) {
results.push({
...here,
tiles: current.tiles,
routeEnergyPerTonne: current.energy,
});
}
}
if (current.tiles >= RESOURCE_TRADE_RADIUS) continue;
for (const neighbour of this._neighbours(current.coords)) {
const nk = key(neighbour.x, neighbour.y);
if (visited.has(nk) || blocked.has(nk)) continue;
const tile = this.tiles[nk];
if (!tile || !(tile.terrainClass === "Land" || tile.terrainClass === "Sea" ||
tile.terrainClass === "Ice")) continue;
const sea = tile.terrainClass === "Sea";
let mode;
if (sea) {
// A shipping lane needs a working port to load and unload.
if (!allowSea) continue;
mode = "ship";
} else {
// Ground legs must run on a road or railway; there is no off-road
// trucking in the trade network.
if (this.railways.has(nk)) mode = "train";
else if (this.roads.has(nk)) mode = "road";
else continue;
// Goods never cross the land of a nation you are at war with.
const owner = this.civAt(neighbour);
if (owner >= 0 && owner !== city.civ && this.isAtWar(city.civ, owner)) continue;
}
const stepEnergy = deliveryEnergyForResource(id, 1, 1, mode, tile.movementCostMultiplier || 1);
visited.add(nk);
queue.push({
coords: neighbour,
tiles: current.tiles + 1,
energy: current.energy + stepEnergy,
});
}
}
results.sort((a, b) => a.routeEnergyPerTonne - b.routeEnergyPerTonne);
return results;
},
// The middleman margin a region charges when it sells food it bought to relay.
// Only the stock it actually bought for resale carries the margin, and the
// stock is drawn down as it sells.
_middlemanMarkup(supplier, id, bought, price) {
if (id !== "food" || !this._relayStock || supplier.kind !== "city") return 0;
const cityId = supplier.city.id;
const held = this._relayStock.get(cityId) || 0;
if (!(held > 0)) return 0;
const sold = Math.min(held, bought);
this._relayStock.set(cityId, held - sold);
return sold * price * MIDDLEMAN.markup;
},
_nodeSurplus(node, id) {
if (id === "hightech") return Math.max(0, node.stock.hightech || 0);
const population = node.city ? this.getCityEconomy(node.city).population : 0;
const perDay = id === "food"
? foodNeedPerDay(population)
: id === "steel"
? steelNeedPerDay(population)
: id === "luxury"
? luxuryNeedPerDay(population)
: 0;
return Math.max(0, (node.stock[id] || 0) - perDay * SUPPLIER_RESERVE_DAYS);
},
// A shortage of food, steel or luxury costs popularity; a grid starved of
// power costs popularity too. Each city is charged once per day.
_applyResourceShortages(ledger) {
for (const [cityId, shortage] of this._resourceShortages) {
const city = this._cityById.get(cityId);
if (!city) continue;
let penalty = 0;
for (const id of ["food", "steel", "luxury"]) {
penalty += (shortage[id] || 0) * RESOURCE_RULES.shortagePopularityPenalty;
}
if (penalty > 0) this._shiftOwnPopularity(city.civ, -penalty);
}
const civShortage = new Map();
for (const component of ledger.values()) {
if (component.civ < 0 || component.cityShortage <= 0) continue;
civShortage.set(
component.civ,
Math.max(civShortage.get(component.civ) || 0, component.cityShortage)
);
}
for (const [civ, shortage] of civShortage) {
this._shiftOwnPopularity(civ, -shortage * RESOURCE_RULES.shortagePopularityPenalty);
}
},
// An unpaid materials bill for the nation's upkeep means things are not being
// maintained; the people notice.
_civilianUpkeepShortage(civ, fraction) {
if (!(fraction > 0)) return;
this._shiftOwnPopularity(civ, -fraction * RESOURCE_RULES.shortagePopularityPenalty);
},
// ---------------------------------------------------------- repairs --
// Damage to a tile's structures leaves a steel debt; until it is paid down
// the scar does not heal and the tile's GDP recovery waits.
_recordRepairDebt(coords, damage) {
if (!(damage > 0)) return;
const k = key(coords.x, coords.y);
const debt = (this.tileRepairDebt.get(k) || 0) + damage * RESOURCE_RULES.repair.steelPerDamage;
this.tileRepairDebt.set(k, debt);
},
// A building level knocked down by bombardment waits to be rebuilt once its
// materials are paid for.
_queueCityRepair(city, index, level) {
const queue = this.cityRepairQueue.get(city.id) || [];
queue.push({ index, targetLevel: level });
this.cityRepairQueue.set(city.id, queue);
},
_tickRepairs() {
// Rebuild one lost city level per city per day, if the materials are there.
for (const [cityId, queue] of Array.from(this.cityRepairQueue)) {
const city = this._cityById.get(cityId);
if (!city || queue.length === 0) {
this.cityRepairQueue.delete(cityId);
continue;
}
const job = queue[0];
const proto = this.protoBuildings[job.index];
if (!proto) {
queue.shift();
continue;
}
const level = Math.max(1, job.targetLevel);
const buildCost = buildingBuildCost(proto, level - 1);
const cost = this.constructionResourceCost(proto, level - 1, buildCost);
if (!this._payConstructionResources(city.civ, city.coords, cost)) continue;
city.buildings[job.index] = level;
this._gdpPerCapitaCache.clear();
this._touchModifiers();
queue.shift();
if (queue.length === 0) this.cityRepairQueue.delete(cityId);
}
// Pay down battle damage, then let the tile work heal.
for (const [k, debt] of Array.from(this.tileRepairDebt)) {
const coords = parseKey(k);
const civ = this.civAt(coords);
if (civ < 0) {
this.tileRepairDebt.delete(k);
continue;
}
const paid = this._payTileDebt(civ, coords, debt);
const remaining = debt - paid;
if (remaining <= 0.0001) this.tileRepairDebt.delete(k);
else this.tileRepairDebt.set(k, remaining);
}
for (const k of Array.from(this.tileImprovements.keys())) {
if (this.tileRepairDebt.has(k)) continue;
this._repairTileWork(parseKey(k));
}
},
// Spends what steel it can on a tile's repair: the owner's stores first, then
// reachable regions. Returns the amount actually paid.
_payTileDebt(civ, coords, amount) {
if (!(amount > 0)) return 0;
const payer = this._treasuryPayer(civ);
const fromStock = this._drawFromNearest(civ, coords, { steel: amount, hightech: 0 }, payer);
const shortfall = amount - fromStock.steel;
let paid = fromStock.steel;
let spent = fromStock.steel * this.getResourcePrice("steel") / this.currencyValue(civ);
if (shortfall > 0) {
const city = this._nearestCity(civ, coords);
if (city) {
const nodes = this._resourceNodes();
const haul = { steel: 0, hightech: 0 };
const before = payer.total;
paid += this._procureFromNeighbours(city, "steel", shortfall, haul, nodes, { payer });
spent += payer.total - before;
}
}
if (spent > 0) {
this._recordBudgetCash(civ, "works", -spent);
this._recordBudgetCategoryResource(civ, "works", "steel", spent);
this._recordResourceSpend(civ, "upkeep", spent, "steel");
}
return paid;
},
// Gradually restores a damaged tile work's hit points, at a steel cost.
_repairTileWork(coords) {
const k = key(coords.x, coords.y);
const id = this.tileImprovements.get(k);
if (!id) return;
const max = TILE_IMPROVEMENT_HP[id] || 1000;
const hp = this.tileImprovementHp.get(k) || 0;
if (hp >= max) return;
const civ = this.tileImprovementOwner.get(k);
if (civ < 0) return;
const wanted = Math.min(RESOURCE_RULES.repair.tileHpPerDay, max - hp);
const cost = wanted * RESOURCE_RULES.repair.steelPerTileHp;
const paid = this._payTileDebt(civ, coords, cost);
const gained = Math.min(wanted, paid / RESOURCE_RULES.repair.steelPerTileHp);
if (gained > 0) this.tileImprovementHp.set(k, hp + gained);
},
// ------------------------------------------------------- unit supply --
// Land units only: aircraft have endurance and ships their own range.
_isLandUnitProto(proto) {
if (!proto || proto.air || proto.naval) return false;
return (proto.traversableTerrains || []).includes("Land");
},
_nearestAlliedCity(civ, coords) {
let best = null;
let bestDistance = Infinity;
for (const city of this.cities) {
if (city.civ !== civ) continue;
const distance = this.topology.tileDistance(city.coords, coords);
if (distance < bestDistance) {
bestDistance = distance;
best = city;
}
}
return best;
},
// The allied city a unit may draw food from, following the transport network:
// a unit at home or on a working port is fed at once; otherwise the nearest
// allied city reachable along roads, railways and shipping lanes (a port
// needed for the sea) supplies it. An encircled unit is cut off entirely.
_unitSupplyCity(unit) {
const civ = unit.civ;
if (this._isEncircled(unit.coords, civ)) return null;
const here = this.cityAt(unit.coords);
if (here && here.civ === civ) return here;
if (this._hasSeaSupply(unit.coords, civ)) return this._nearestAlliedCity(civ, unit.coords);
const blocked = this._hostileOccupied(civ);
const visited = new Set([key(unit.coords.x, unit.coords.y)]);
const queue = [unit.coords];
while (queue.length > 0) {
const coords = queue.shift();
for (const neighbour of this._neighbours(coords)) {
const nk = key(neighbour.x, neighbour.y);
if (visited.has(nk) || blocked.has(nk)) continue;
const tile = this.tiles[nk];
if (!tile) continue;
const sea = tile.terrainClass === "Sea";
if (sea) {
// A shipping lane needs a working port at the nation's end.
if (!this._civMarketAccess(civ)) continue;
} else if (!(this.railways.has(nk) || this.roads.has(nk))) {
continue;
}
const city = this.cityAt(neighbour);
if (city && city.civ === civ) return city;
visited.add(nk);
queue.push(neighbour);
}
}
return null;
},
// Feeds a unit for a day from its supply city: the city's stores first, then
// the market on the treasury. Atomic: if neither can cover the ration, the
// city is not charged and the unit goes unfed.
_provisionUnit(unit, city, tonnes) {
if (!(tonnes > 0)) return true;
const store = this.getCityResourceStock(city);
const take = Math.min(store.food || 0, tonnes);
const remaining = tonnes - take;
const payer = this._treasuryPayer(unit.civ);
let procuredSpent = 0;
if (remaining > 0.0001) {
// The rest comes from reachable regions, bought by the treasury.
const nodes = this._resourceNodes();
const haul = { food: 0 };
const before = payer.total;
const got = this._procureFromNeighbours(city, "food", remaining, haul, nodes, { payer });
procuredSpent = payer.total - before;
if (got + 1e-6 < remaining) {
// Not enough food reachable: the unit goes without, and what was bought
// stays in the city's store.
store.food = (store.food || 0) + got;
this._recordProvisionSpend(unit.civ, 0, procuredSpent);
return false;
}
}
store.food = (store.food || 0) - take;
// The city's own food is bought from the region that stored it.
const takeValue = take * this.getResourcePrice("food") / this.currencyValue(unit.civ);
this._payRegionForMaterial(city, "food", take, payer);
this._recordProvisionSpend(unit.civ, takeValue, procuredSpent);
return true;
},
// Books the food a unit ate, whether it came from a region's store or from a
// foreign one.
_recordProvisionSpend(civ, takeValue, procuredSpent) {
const spent = takeValue + procuredSpent;
if (!(spent > 0)) return;
this._recordBudgetCash(civ, "units", -spent);
this._recordBudgetCategoryResource(civ, "units", "food", spent);
this._recordResourceSpend(civ, "upkeep", spent, "food");
},
// Every land unit eats a day from its carried stores. At home, or in foreign
// territory with a clear route to an allied city, it is fed and topped back
// up to a full pack; cut off, the days run down and then the soldiers start
// dying.
_tickUnitSupply() {
if (!this.resourceStock) return;
const battle = this._battleUnitIds();
const destroyed = [];
for (const unit of this.units) {
const proto = this.unitProto(unit);
if (!proto || !this._isLandUnitProto(proto)) continue;
const soldiers = proto.population || RESOURCE_RULES.unitPopulation;
const ration = foodNeedPerDay(soldiers);
const here = this.cityAt(unit.coords);
const resting = !!(here && here.civ === unit.civ && unit.hp < unit.maxHp);
const multiplier = resting
? RESOURCE_RULES.infantryRestingFoodMultiplier
: battle.has(unit.id)
? RESOURCE_RULES.infantryCombatFoodMultiplier
: 1;
const city = this._unitSupplyCity(unit);
const fed = city ? this._provisionUnit(unit, city, ration * multiplier) : false;
if (fed) {
unit.foodDays = RESOURCE_RULES.unitSupplyDays;
} else {
unit.foodDays -= multiplier;
}
if (unit.foodDays <= 0) {
unit.hp -= unit.maxHp * RESOURCE_RULES.starvationHpFractionPerDay;
if (unit.hp <= 0) {
unit.hp = 0;
destroyed.push(unit);
}
}
}
if (destroyed.length > 0) {
this._recordMilitaryCasualties(destroyed);
for (const unit of destroyed) this._destroyUnit(unit);
this._visibilityDirty = true;
this._emitChanged();
}
},
_pruneResourceStock() {
for (const k of Array.from(this.resourceStock.keys())) {
const coords = parseKey(k);
if (!this._resourceNodeAt(coords)) this.resourceStock.delete(k);
}
},
// ------------------------------------------------------- research link --
// One city's science buildings spend a unit of high-tech for every research
// point they make, up to what the city holds; the rest of the points are not
// produced. A city that cannot cover its buildings' need raises a shortage
// notice. Returns the effective points.
_spendCityHighTechForResearch(civ, city, points) {
if (points <= 0) return 0;
const needed = points * RESOURCE_RULES.highTechPerResearchPoint;
const store = this.getCityResourceStock(city);
const have = store.hightech || 0;
if (needed <= 0) return points;
if (have + 1e-9 >= needed) {
store.hightech = have - needed;
return points;
}
this._noteResourceShortage(civ, this._cityResearchBuilding(city), city.name, "hightech");
if (have <= 0) return 0;
const ratio = have / needed;
store.hightech = 0;
return points * ratio;
},
// The science building(s) a city runs, named for a shortage notice.
_cityResearchBuilding(city) {
const names = [];
for (const [indexStr, level] of Object.entries(city.buildings || {})) {
if (level <= 0) continue;
const proto = this.protoBuildings[Number(indexStr)];
if (!proto) continue;
if ((proto.effects || []).some((e) => e.stat === EFFECT_RESEARCH)) names.push(proto.name);
}
return names.length > 0 ? names.join(" & ") : "Science buildings";
},
// ----------------------------------------------------- shortage notices --
// Records that a producing building in a region went without the material or
// power it needs, for the day's notice to that nation's own viewer. Notes are
// deduplicated per building, region and resource.
_noteResourceShortage(civ, building, region, resource) {
if (civ < 0 || !building || !region || !resource) return;
let set = this._resourceAlerts.get(civ);
if (!set) {
set = new Map();
this._resourceAlerts.set(civ, set);
}
const key = `${region}|${building}|${resource}`;
if (!set.has(key)) set.set(key, { building, region, resource });
},
// A grid short of power leaves its resource works idle: the owner is told
// which work in which region is missing energy.
_noteGridPowerShortages(ledger, components) {
for (const [id, component] of ledger) {
if (component.civ < 0 || component.industrialScale >= 0.999) continue;
const city = this._componentCity(id, components);
if (!city) continue;
for (const [k, imprId] of this.tileImprovements) {
if (components.get(k) !== id) continue;
const proto = tileImprovementById(imprId);
if (!isResourceTileBuilding(proto) || proto.resource === "energy") continue;
this._noteResourceShortage(component.civ, proto.name, city.name, "energy");
}
}
},
// The day's shortage notices for one nation, for its own viewer. Each carries
// the day it was raised so the viewer re-announces it once per day.
_resourceAlertsFor(civ) {
const set = this._resourceAlerts ? this._resourceAlerts.get(civ) : null;
if (!set) return [];
const day = Math.floor(this.totalHours / HOURS_PER_DAY);
return Array.from(set.values()).map((entry) => ({ ...entry, day }));
},
};