Files
Battle-for-Tismo/shared/data/resources.js
T
adrien 7ea5373b60 Moved the economy onto resources and surfaced each building's material bill
Rebalanced the resource rules so nothing is priced primarily in money: steel, electricity and high-tech lead every construction bill, chip foundries are seeded everywhere instead of being hand-placed, and cities open with stores of steel and high-tech. City building rows now lead with the resource bill and keep the money as an aside, and the region's resources expand into the buildings that demand them. Pulled the graph and cost maths into a framework-free shared/economy_graph.js, quoted currency through one helper, and gave every data table the same striping and row highlight.
2026-09-22 23:31:36 +02:00

505 lines
20 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";
// 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,
};
export function resourceById(id) {
return RESOURCES[RESOURCE_INDEX[id]] || null;
}
// 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 strongly a day's world-wide imbalance moves the price, and how far it may
// drift from the base. The market is a smoothing device, not a real auction:
// it lets a shortage raise the cost of imports and a glut cheapen them.
export const RESOURCE_MARKET = {
base: RESOURCE_MARKET_BASE,
adjustment: 0.2,
minMultiplier: 0.5,
maxMultiplier: 1.8,
};
// 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 130% of a month's food before the month begins.
foodMonthlyBuffer: 1.3,
daysPerMonth: 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,
// 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 for a neighbour's surplus before falling back to the
// global market.
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",
description:
"Burns coal to make 250 GWh 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",
description:
"The cleanest fossil plant: 300 GWh 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",
description: "Burns oil for 270 GWh 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",
description:
"A field of panels producing a steady 30 GWh 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",
description: "Taps the earth's heat for 60 GWh 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",
description: "A hilltop wind farm making 25 GWh 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",
description: "Turbines out at sea making 50 GWh 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",
description: "A late-game reactor producing 4 TWh 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",
description:
"Smelts iron into 20,000 t of steel a day, spending 20 MWh 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",
description: "Produces 20,000 carats of luxury a day, at 10 MWh 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",
description: "Produces 20,000 carats of luxury a day, at 5 MWh 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",
description:
"Fabricates 5000 units of high-tech a day, spending 100 MWh 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.
salesWindow: 6,
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;
}