// Resources: the five material groups nations produce, store, trade and // consume. Everything is expressed in one canonical unit per resource so the // simulation never has to convert: energy in kWh, steel and food in tonnes, // luxury in carats and high-tech in whole units. Display formatting converts to // the friendly unit (MWh, Mt, kt, ...). The market prices and conversion ratios // here are the main tuning knobs for the whole resource economy; the per-person // needs and the energy-per-material figures come straight from the roadmap. import { EFFECT_ENERGY_EFFICIENCY, EFFECT_STEEL_EFFICIENCY, EFFECT_FOOD_EFFICIENCY, EFFECT_LUXURY_EFFICIENCY, EFFECT_HIGHTECH_EFFICIENCY, } from "./effects.js"; import { groupDigits } from "../text_format.js"; // Energy is stored and moved as kWh. These keep the catalogue readable. export const KWH_PER_MWH = 1_000; export const KWH_PER_GWH = 1_000_000; // One tonne of any material counts as this much energy on the wire, so material // deliveries and energy deliveries share one transport-cost formula. export const ENERGY_PER_MATERIAL_TONNE = 10 * KWH_PER_GWH; // 10 GWh/t export const RESOURCES = [ { id: "energy", name: "Energy", unit: "kWh", icon: "icon_electricity.svg", colour: "#ffd633", // Mass of one canonical unit, so a delivery leg can charge transport per // tonne whatever the good is: 10 GWh of energy weighs a tonne. tonnesPerUnit: 1 / ENERGY_PER_MATERIAL_TONNE, // Energy is a flow, not a stock: it comes from the grid and is spent the // same day. Only cities and resource buildings can hold the other four. storable: false, }, { id: "steel", colour: "#9aa7b8", name: "Steel", unit: "t", icon: "icon_steel.svg", tonnesPerUnit: 1, storable: true, }, { id: "food", colour: "#7ed957", name: "Food", unit: "t", icon: "icon_food.svg", tonnesPerUnit: 1, storable: true, }, { id: "luxury", colour: "#d98cff", name: "Luxury", unit: "ct", icon: "icon_luxury.svg", // A carat is 0.2 g, so 200,000 ct weigh a tonne. tonnesPerUnit: 0.0002 / 1000, storable: true, }, { id: "hightech", colour: "#5cc8ff", name: "High-tech", unit: "u", icon: "icon_hightech.svg", // A high-tech unit is a crate of components, about a kilogram. tonnesPerUnit: 0.001, storable: true, }, ]; export const RESOURCE_IDS = RESOURCES.map((r) => r.id); export const RESOURCE_INDEX = Object.fromEntries(RESOURCES.map((r, i) => [r.id, i])); // The four stockable resources, in catalogue order. Energy is handled as a flow. export const STORABLE_RESOURCE_IDS = RESOURCES.filter((r) => r.storable).map((r) => r.id); // The modifier each resource's efficiency technology writes. The simulation // divides that resource's energy draw by `1 + modifier`, so a higher figure is // more of the commodity produced for the same power. export const RESOURCE_EFFICIENCY_EFFECT = { energy: EFFECT_ENERGY_EFFICIENCY, steel: EFFECT_STEEL_EFFICIENCY, food: EFFECT_FOOD_EFFICIENCY, luxury: EFFECT_LUXURY_EFFICIENCY, hightech: EFFECT_HIGHTECH_EFFICIENCY, }; // Research is the nation's stock of knowledge: produced by its science // buildings and a few governments, and spent on the focused technology. It is a // special kind of resource -- a unit of it cannot be bought, sold, stored or // transported -- so it is deliberately kept out of the tradeable catalogue // above and never appears in the market, the stockpiles or a delivery. Only // science production and the technology tree ever show it, always as this // erlenmeyer icon standing in for the word "research". export const RESEARCH = { id: "research", name: "Research", unit: "", icon: "icon_research.svg", colour: "#a142b6", }; // Culture is the nation's store of soft power: produced by its cultural // buildings and spent, in the simple economy, on propaganda campaigns. Like // research it is a special resource -- it has no market, no stockpile tile and // never moves -- so it is kept out of the tradeable catalogue and shown only // with its clapperboard icon. export const CULTURE = { id: "culture", name: "Culture", unit: "", icon: "icon_culture.svg", colour: "#9a7bff", }; export function resourceById(id) { if (id === RESEARCH.id) return RESEARCH; if (id === CULTURE.id) return CULTURE; return RESOURCES[RESOURCE_INDEX[id]] || null; } // --------------------------------------------------------------- formatting -- // // The canonical rendering of an amount, shared with the browser so a // description quotes exactly what the game shows. Energy reads in kWh, MWh, // GWh or TWh; a material in its own unit, stepping through kt/Mt, kct and ku. export function formatEnergy(kwh) { const value = Math.max(0, kwh); if (value >= 1e9) return `${trim(value / 1e9)} TWh`; if (value >= 1e6) return `${trim(value / 1e6)} GWh`; if (value >= 1e3) return `${trim(value / 1e3)} MWh`; return `${trim(value)} kWh`; } export function formatResourceAmount(id, value) { if (id === "energy") return formatEnergy(value); const amount = Math.max(0, value); const unit = resourceById(id) ? resourceById(id).unit : ""; if (amount >= 1e12) return `${trim(amount / 1e12)} T${unit}`; if (amount >= 1e9) return `${trim(amount / 1e9)} G${unit}`; if (amount >= 1e6) return `${trim(amount / 1e6)} M${unit}`; return `${trim(amount)} ${unit}`; } function trim(value) { if (value >= 100) return groupDigits(Math.round(value)); if (value >= 10) return value.toFixed(1).replace(/\.0$/, ""); return value.toFixed(2).replace(/\.?0+$/, ""); } // Global market prices, in euro per canonical unit. These are what a city pays // when it cannot meet a need from its own store or a neighbour. Energy is // deliberately cheap: the roadmap's energy-per-material ratios are large, so a // high electric price would make every material's energy bill dwarf its value. // // The five figures are the old nominal prices raised roughly seventeen-fold so // the production-based GDP (what a nation makes times these prices) lands in // the same range as the money economy it replaced. The scaling is what makes a // fresh nation's food output alone read as a plausible per-capita GDP rather // than a few hundred euro; construction, upkeep and imports are priced off the // same figures. export const RESOURCE_MARKET_BASE = { energy: 0.051, // per kWh steel: 10_200, // per tonne food: 5_100, // per tonne luxury: 1_020, // per carat hightech: 13_600, // per unit }; // How the world market finds its price each day. The market is a smoothing // device, not a real auction: the day's imbalance sets an equilibrium, the // price that clears supply against demand, and the price eases toward it. There // is no price band, so a sustained shortage lifts a good well past its base // while a glut cheapens it, but the equilibrium keeps the price anchored: it // settles rather than compounding without bound. `maxRatio` bounds how sharp a // shortage the ratio may read, so a rounding-error supply cannot run away, and // is also the ratio a day with no supply at all is treated as. export const RESOURCE_MARKET = { base: RESOURCE_MARKET_BASE, adjustment: 0.2, maxRatio: 10, }; // The central bank charts a rolling window of the world commodity price index: // one sample per day for this many days. The model keeps that many daily // samples and the browser draws the same window. export const PRICE_INDEX_WINDOW_DAYS = 90; // A region whose food store runs dry and cannot feed its people is in famine: // the whole world is told the day it begins, and the region loses this share of // its people every day until food comes back. export const FAMINE = { dailyMortality: 0.01, }; // The physical rules of the resource economy. Every per-person figure and every // energy-per-material ratio below is fixed by the design; the prices above are // what we tune against them. export const RESOURCE_RULES = { // Each person eats 1 kg of food and consumes 1 kg of steel a day. Both are // canonical tonnes, so 1 kg is 0.001 t. foodPerPersonPerDay: 0.001, steelPerPersonPerDay: 0.001, // Luxuries are rarer: 0.01 carat a person a day. luxuryPerPersonPerDay: 0.01, // Cities aim to hold a month of every storable resource -- food, steel, // luxury and high-tech -- before the month begins. stockpileDays: 30, // A city's baseline electric appetite, in kWh per euro of yearly GDP. This is // the "kWh per euro of GDP" economic metric; the top bar does not show it. gdpEnergyIntensity: 0.15, // Energy the material producers consume to make one canonical unit. energyPerFoodTonne: 1 * KWH_PER_MWH, // 1 MWh per tonne // Food a region can force from energy beyond its own harvest, in greenhouses // and synthesis. The energy for `x` extra tonnes a day is this coefficient // times `x^2`, so each further tonne costs more energy than the last and a // region grows only while its marginal energy beats the market price of food. foodSynthesisQuadratic: 20, // kWh per tonne squared energyPerSteelTonne: 20 * KWH_PER_MWH, // 20 MWh per tonne energyPerLuxuryCarat: { gold_mine: 10 * KWH_PER_MWH, // 10 MWh per carat diamond_mine: 5 * KWH_PER_MWH, // 5 MWh per carat }, energyPerHighTechUnit: 10 * KWH_PER_MWH, // 10 MWh per unit // Science buildings spend one high-tech unit for every research point. highTechPerResearchPoint: 1, // Delivering one tonne of material one tile, in kWh of transport energy. // Roads use trucks, railways trains; without a road a truck burns the // off-road figure times the tile's movement cost, and sea legs go by ship. deliveryEnergy: { truckRoad: 10 * KWH_PER_MWH, // 10 MWh per tonne per tile truckOffRoad: 50 * KWH_PER_MWH, // 50 MWh per tonne per tile, times cost train: 1 * KWH_PER_MWH, // 1 MWh per tonne per tile ship: 50, // 50 kWh per tonne per tile }, // Steel every construction spends per euro of its money cost. At 1/1000 a // 100 M building needs 100,000 t of steel. The steel bill is deliberately an // order of magnitude larger than the cash price, so nothing is bought with // money alone: the state pays with materials it has, and the currency is only // the settlement it falls back on. steelPerBudgetEuro: 1 / 1_000, // The electricity a tile work draws as it is built, per euro of its money // cost. It is roughly the power embodied in the steel it spends: steel costs // 20 MWh a tonne and 1 t per 1,000 euro, so building is powered as well as // supplied. Tile improvements pay this up front. constructionEnergyPerBudgetEuro: 2, // kWh per euro // High-tech a science or advanced building needs to be raised, per level. constructionHighTechPerLevel: 100, // Upkeep is materials, not money: every building, unit and improvement wears // out and the state buys what it needs. Each figure scales with the thing's // money value -- a building level's cost, a unit's training cost -- so one set // of coefficients tunes the whole load. `advanced` things (science, air, // radar, chips, fusion) also need high-tech. upkeepSteelPerEuro: 5e-7, // t per euro of value per day upkeepEnergyPerEuro: 1e-3, // kWh per euro of value per day upkeepHighTechPerEuro: 2e-6, // u per euro of value per day (advanced only) // Transport is the exception: its build cost is enormous but its upkeep is a // per-tile wear figure, not a share of that cost. transportUpkeepSteel: { road: 0.02, railway: 0.1 }, // Combat is the other exception: guns and aircraft are cheap to keep parked // and expensive to fire. These are the materials one use spends. Air defense // fires per hostile aircraft pass, artillery per hourly shelling, an aircraft // per sortie. combatConsumption: { airDefenseVolley: { steel: 20, hightech: 5 }, artilleryVolley: { steel: 1, hightech: 0 }, airStrike: { steel: 100, hightech: 20 }, }, // Infantry eat. A unit carries this many soldiers, and each is fed like a // civilian: double rations in combat, triple while wounded and resting. // Training or healing a unit draws its soldiers out of the region's people. unitPopulation: 1_000, infantryCombatFoodMultiplier: 2, infantryRestingFoodMultiplier: 3, // Supply. A unit carries this many days of its own food. At home, or in // foreign territory with a clear route to an allied city, it tops up from // that city; cut off, the days run down and the unit starts to starve. unitSupplyDays: 50, starvationHpFractionPerDay: 0.1, // A region that cannot buy the food it needs bids the price up: every tonne // of unmet demand counts this many times over in the market, so a local famine // shows in the national food price instead of hiding behind a balanced total. famineBidFactor: 3, // Repairs: damage leaves a materials debt the owner pays down, and only then // does the structure recover. A razed city level costs what it cost to build. repair: { steelPerTileHp: 0.05, // t to restore one hit point of a tile work tileHpPerDay: 60, // hit points a funded repair restores each day steelPerDamage: 0.5, // t of debt per point of structure damage unitSteelPerHp: 0.01, // t to heal one hit point of a ground or naval unit unitHighTechPerHp: 0.002, // u to heal one hit point of an advanced unit }, // Fraction of a missing need that turns into a popularity penalty per day, // and the same for a GDP-per-capita shortfall, so shortages are felt. shortagePopularityPenalty: 2, shortageGdpPenalty: 0.05, // Food comes from the land, not from the number of people standing on it: // every worked tile grows `foodPerTilePerDay` tonnes a day scaled by its // terrain, so moving people between regions changes only income per head. foodPerTilePerDay: 100, // How fertile each terrain is. A worked tile grows its base output scaled by // this, so forests and plains feed cities while deserts and ice do not. tileFoodFactor: { Forest: 1.4, Hills: 0.7, Land: 1.2, Desert: 0.1, Mountain: 0.2, Tundra: 0.2, Ice: 0, }, }; // How far a city will look overland for a neighbour's surplus. Sea legs are not // counted: a shipping lane between ports reaches any coast, so a region can // always trade with a sea-connected one, however far, at the freight's energy // cost. The cap only keeps a city from trucking goods right across a continent. export const RESOURCE_TRADE_RADIUS = 8; // A region that can reach a food surplus it does not need buys extra to resell // to nearby regions that cannot, acting as a middleman. The freight a buyer pays // on the longer outbound leg is its margin, so it only relays toward a market // farther than its own source, and stocks this many days of the neighbours' // shortfall. export const MIDDLEMAN = { coverDays: 3, markup: 0.25 }; // Resource-producing buildings. Built only outside cities, they occupy a land // tile like a military improvement and are managed through the same tile-works // queue. A power plant produces energy; every other building consumes energy // and produces its material. `outputPerDay` is in the resource's canonical // unit, `energyPerDay` in kWh (positive means consumed, negative produced). export const RESOURCE_BUILDINGS = [ { id: "coal_power_plant", name: "Coal power plant", get description() { return `Burns coal to make ${formatEnergy(this.outputPerDay)} of electricity a day. Cheap to raise, but the fuel bill never stops.`; }, resource: "energy", outputPerDay: 250 * KWH_PER_GWH, fuelEnergyPerDay: 250 * KWH_PER_GWH * 0.25, energyPerDay: 0, buildCost: 500_000_000, maintenance: 20_000, icon: "icon_coal_power_plant.svg", }, { id: "natural_gas_power_plant", name: "Natural gas power plant", get description() { return `The cleanest fossil plant: ${formatEnergy(this.outputPerDay)} a day for a higher fuel bill.`; }, resource: "energy", outputPerDay: 300 * KWH_PER_GWH, fuelEnergyPerDay: 300 * KWH_PER_GWH * 0.25, energyPerDay: 0, buildCost: 700_000_000, maintenance: 25_000, icon: "icon_gas_power_plant.svg", }, { id: "oil_power_plant", name: "Oil power plant", get description() { return `Burns oil for ${formatEnergy(this.outputPerDay)} of electricity a day.`; }, resource: "energy", outputPerDay: 270 * KWH_PER_GWH, fuelEnergyPerDay: 270 * KWH_PER_GWH * 0.25, energyPerDay: 0, buildCost: 600_000_000, maintenance: 22_000, icon: "icon_oil_power_plant.svg", }, { id: "solar_power_plant", name: "Solar power plant", get description() { return `A field of panels producing a steady ${formatEnergy(this.outputPerDay)} a day with no fuel and no upkeep.`; }, resource: "energy", outputPerDay: 30 * KWH_PER_GWH, energyPerDay: 0, renewable: true, buildCost: 300_000_000, maintenance: 0, icon: "icon_solar_power_plant.svg", }, { id: "geothermal_plant", name: "Geothermal plant", get description() { return `Taps the earth's heat for ${formatEnergy(this.outputPerDay)} a day of free electricity.`; }, resource: "energy", outputPerDay: 60 * KWH_PER_GWH, energyPerDay: 0, renewable: true, buildCost: 800_000_000, maintenance: 0, icon: "icon_geothermal_plant.svg", }, { id: "wind_turbines", name: "Wind turbines", get description() { return `A hilltop wind farm making ${formatEnergy(this.outputPerDay)} a day for free.`; }, resource: "energy", outputPerDay: 25 * KWH_PER_GWH, energyPerDay: 0, renewable: true, buildCost: 200_000_000, maintenance: 0, icon: "icon_wind_turbines.svg", }, { id: "offshore_wind_turbines", name: "Offshore wind turbines", get description() { return `Turbines out at sea making ${formatEnergy(this.outputPerDay)} a day. Needs a coastal tile.`; }, resource: "energy", outputPerDay: 50 * KWH_PER_GWH, energyPerDay: 0, renewable: true, coastal: true, buildCost: 500_000_000, maintenance: 0, icon: "icon_offshore_wind_turbines.svg", }, { id: "fusion_power_plant", advanced: true, name: "Fusion power plant", get description() { return `A late-game reactor producing ${formatEnergy(this.outputPerDay)} of electricity a day.`; }, resource: "energy", outputPerDay: 4_000 * KWH_PER_GWH, energyPerDay: 0, renewable: true, buildCost: 50_000_000_000, maintenance: 50_000, // A wonder of the endgame: only a nation that has mastered fusion may raise // one. Its upkeep is deliberately high but not the raw share of its enormous // build cost. requiresTechnology: "fusion_power", materialUpkeep: { steel: 1_000, energy: 0, hightech: 500 }, icon: "icon_fusion_power_plant.svg", }, { id: "steel_mill", name: "Steel mill", get description() { return `Smelts iron into ${formatResourceAmount(this.resource, this.outputPerDay)} of steel a day, spending ${formatEnergy(RESOURCE_RULES.energyPerSteelTonne)} of energy per tonne.`; }, resource: "steel", outputPerDay: 20_000, energyPerDay: 2_000 * RESOURCE_RULES.energyPerSteelTonne, buildCost: 800_000_000, maintenance: 10_000, icon: "icon_steel_mill.svg", }, { id: "gold_mine", name: "Gold mine", get description() { return `Produces ${formatResourceAmount(this.resource, this.outputPerDay)} of luxury a day, at ${formatEnergy(RESOURCE_RULES.energyPerLuxuryCarat.gold_mine)} a carat.`; }, resource: "luxury", outputPerDay: 20_000, energyPerDay: 20_000 * RESOURCE_RULES.energyPerLuxuryCarat.gold_mine, buildCost: 400_000_000, maintenance: 5_000, icon: "icon_gold_mine.svg", }, { id: "diamond_mine", name: "Diamond mine", get description() { return `Produces ${formatResourceAmount(this.resource, this.outputPerDay)} of luxury a day, at ${formatEnergy(RESOURCE_RULES.energyPerLuxuryCarat.diamond_mine)} a carat.`; }, resource: "luxury", outputPerDay: 20_000, energyPerDay: 20_000 * RESOURCE_RULES.energyPerLuxuryCarat.diamond_mine, buildCost: 500_000_000, maintenance: 5_000, icon: "icon_diamond_mine.svg", }, { id: "chip_foundry", advanced: true, name: "Chip foundry", get description() { return `Fabricates ${formatResourceAmount(this.resource, this.outputPerDay)} of high-tech a day, spending ${formatEnergy(RESOURCE_RULES.energyPerHighTechUnit)} of energy per unit.`; }, resource: "hightech", outputPerDay: 5_000, energyPerDay: 5_000 * RESOURCE_RULES.energyPerHighTechUnit, buildCost: 2_000_000_000, maintenance: 10_000, icon: "icon_chip_foundry.svg", }, ]; // One work stands for a whole industry: its output is scaled up so a nation // needs far fewer of them on the map. The energy and fuel it draws scale with // the output too, so a converter stays exactly as efficient as it was authored. export const RESOURCE_BUILDING_OUTPUT_SCALE = 10; for (const building of RESOURCE_BUILDINGS) { building.outputPerDay = (building.outputPerDay || 0) * RESOURCE_BUILDING_OUTPUT_SCALE; building.energyPerDay = (building.energyPerDay || 0) * RESOURCE_BUILDING_OUTPUT_SCALE; building.fuelEnergyPerDay = (building.fuelEnergyPerDay || 0) * RESOURCE_BUILDING_OUTPUT_SCALE; } // The resource buildings decay in battle like the military improvements do. for (const building of RESOURCE_BUILDINGS) { if (building.takesCombatDamage === undefined) building.takesCombatDamage = true; } // Every resource building -- a material producer or a power plant -- is an // independent private-sector agent. It keeps its own cash from what it sells, // tries to grow when demand for its output has been rising, and spends that // cash to upgrade itself. Power is a paid flow: the plants are paid for the // power the grid's consumers draw, and a plant that keeps selling out expands. // // An upgrade to level u raises throughput exponentially (`outputPerDay` times // `alpha^u`) but the extra output costs more power: the operating input grows // linearly (`1 + inputPerLevel*u`). Each further level costs `costAlpha^u` more // steel and high-tech, so the benefit compounds slower than the bill. export const RESOURCE_BUILDING_UPGRADE = { alpha: 1.1, inputPerLevel: 1, costAlpha: 1.1, // A ceiling on the compounding, so a very old building cannot run away. maxLevel: 12, // How many days of sales the agent watches for the demand trend, and how much // its recent average must beat its older one before it commits to an upgrade. // A long window means only demand that is sustained earns an upgrade, not a // one-day spike. salesWindow: 12, demandGrowth: 0, // A producer that sells at least this share of what it makes is demand-bound // (it sells out), so it grows even when its sales look flat. sellThrough: 0.9, // The cash headroom over the quoted material bill an agent wants before it // starts, so a price swing cannot leave an upgrade half-bought. cashMargin: 1.15, }; // A production building's output per day at upgrade `level`, exponential in the // level. Shared by the simulation and the browser so both quote the same figure. export function resourceBuildingOutputAt(proto, level) { const base = (proto && proto.outputPerDay) || 0; return base * Math.pow(RESOURCE_BUILDING_UPGRADE.alpha, Math.max(0, level || 0)); } // A production building's operating input at upgrade `level`: the plant's fuel // or a converter's energy, growing linearly with the level. `field` is the // proto field to scale (`energyPerDay` or `fuelEnergyPerDay`). export function resourceBuildingInputAt(proto, level, field = "energyPerDay") { const base = (proto && proto[field]) || 0; return base * (1 + RESOURCE_BUILDING_UPGRADE.inputPerLevel * Math.max(0, level || 0)); } // The money an agent pays to reach the next level from `level`: the build cost // compounded by `costAlpha` per level already gained. export function resourceBuildingUpgradeMoneyCost(proto, level) { const base = (proto && proto.buildCost) || 0; return base * Math.pow(RESOURCE_BUILDING_UPGRADE.costAlpha, Math.max(0, level || 0)); } export const RESOURCE_BUILDING_BY_ID = Object.fromEntries( RESOURCE_BUILDINGS.map((building) => [building.id, building]) ); export function resourceBuildingById(id) { return RESOURCE_BUILDING_BY_ID[id] || null; }