Files
Battle-for-Tismo/shared/data/resources.js
T
adrien da11e1d97d Showed the culture a propaganda campaign drains
The simple economy pays for propaganda out of the nation's culture stock and lapses a campaign it can no longer fund, but the tab quoted money and never named the store. Give culture the clapperboard icon as a non-tradeable resource like research, read the stock and the campaigns' daily drain out on the Propaganda tab, and price every campaign figure in culture when the simple economy runs.
2026-09-28 10:15:22 +02:00

602 lines
24 KiB
JavaScript

// 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;
}