// Pure resource maths shared by the server (which simulates the economy) and // the browser (which explains it). Everything here is framework-free and takes // plain numbers, so both sides derive identical figures from the same rules in // shared/data/resources.js. import { RESOURCE_IDS, RESOURCE_RULES, RESOURCE_MARKET, RESOURCE_MARKET_BASE, formatEnergy, formatResourceAmount, } from "./data/resources.js"; import { EFFECT_RESEARCH } from "./data/effects.js"; const DAYS_PER_YEAR = 365; // The amount formatters live with the resource catalogue so its descriptions // can quote the same figures; re-exported here for the existing callers. export { formatEnergy, formatResourceAmount }; // --------------------------------------------------------------- per-person -- export function foodNeedPerDay(population) { return Math.max(0, population) * RESOURCE_RULES.foodPerPersonPerDay; } // The food a unit eats in a day from its carried stores. Aircraft and ships do // not: they live on endurance and range instead. Every land unit fields a // company of `population` (or the rule's default), fed like the civilians of // that size. Mirrors GameState._tickUnitSupply so the card and the tick agree. export function unitFoodPerDay(proto) { if (!proto || proto.air || proto.naval) return 0; if (!(proto.traversableTerrains || []).includes("Land")) return 0; return foodNeedPerDay(proto.population || RESOURCE_RULES.unitPopulation); } export function steelNeedPerDay(population) { return Math.max(0, population) * RESOURCE_RULES.steelPerPersonPerDay; } export function luxuryNeedPerDay(population) { return Math.max(0, population) * RESOURCE_RULES.luxuryPerPersonPerDay; } // What one worked land tile grows in a day. Food comes from the land, not from // how many people live on it: a farm feeds whoever works it, so moving people // between regions changes only the income per head, never the harvest. A tile // feeds roughly `ECONOMY.basePopulation` people at factor 1. export function tileFoodOutput(tile) { const factor = RESOURCE_RULES.tileFoodFactor[tile.terrainType] ?? 1; return RESOURCE_RULES.foodPerTilePerDay * factor; } // The reserve a region wants to hold of a material it consumes: a month at the // current appetite. Every storable good is buffered to the same horizon, so the // same figure sizes food, steel, luxury and high-tech reserves alike. export function stockpileTarget(needPerDay) { return Math.max(0, needPerDay) * RESOURCE_RULES.stockpileDays; } // How much food a city wants in store before the coming month: a month at the // current appetite. export function foodMonthlyTarget(population) { return stockpileTarget(foodNeedPerDay(population)); } // The energy a region spends to force `extraFood` tonnes a day from the land // beyond its harvest. Quadratic in the extra food, so the marginal tonne gets // dearer the harder the land is pushed. export function foodSynthesisEnergy(extraFood) { const x = Math.max(0, extraFood); return RESOURCE_RULES.foodSynthesisQuadratic * x * x; } // The marginal energy for one more tonne of synthesised food, the slope of // `foodSynthesisEnergy`. export function foodSynthesisMarginalEnergy(extraFood) { return 2 * RESOURCE_RULES.foodSynthesisQuadratic * Math.max(0, extraFood); } // How much extra food a region grows for a `shortfall` it can cover. Growing is // always cheapest for the first tonnes (its marginal energy starts near zero) // and buying is a flat price, so the region grows while the marginal energy is // cheaper than the market, up to the break-even. Then it completes with the // dearer option: what the reachable market cannot actually supply -- `buyable` // tonnes -- it must grow itself, even past the break-even. With an unreachable // or bottomless market the default `buyable = Infinity` gives the plain // break-even result. // // `shortfall` is how much the region would *like* to grow -- the day's meal // plus whatever it must add to refill an empty reserve. `mustGrow` is the part // it cannot avoid: the ration it must force because it cannot buy that much. It // defaults to the whole shortfall, which is the old single-target behaviour. export function synthesisedFoodFor( foodPrice, energyPrice, shortfall, buyable = Infinity, mustGrow = shortfall ) { const gap = Math.max(0, shortfall); if (!(gap > 0)) return 0; const forced = Math.max(0, Math.max(0, mustGrow) - Math.max(0, buyable)); const quadratic = RESOURCE_RULES.foodSynthesisQuadratic; if (!(energyPrice > 0) || !(quadratic > 0) || !(foodPrice > 0)) { return Math.min(gap, forced); } const breakEven = foodPrice / (2 * quadratic * energyPrice); if (!(breakEven > 0)) return Math.min(gap, forced); return Math.min(gap, Math.max(breakEven, forced)); } // A city's baseline electric appetite, kWh a day, from its yearly GDP. export function cityEnergyPerDay(gdp) { return Math.max(0, gdp) * RESOURCE_RULES.gdpEnergyIntensity / DAYS_PER_YEAR; } // What one building level, unit or tile work wears out in a day, from its money // value. Advanced things also burn high-tech. Buildings pass their level's cost // (so upkeep grows with the level), units their training cost. export function upkeepResources(value, advanced = false) { const amount = Math.max(0, value); return { steel: amount * RESOURCE_RULES.upkeepSteelPerEuro, energy: amount * RESOURCE_RULES.upkeepEnergyPerEuro, hightech: advanced ? amount * RESOURCE_RULES.upkeepHighTechPerEuro : 0, }; } // The daily upkeep of a catalogue entry. An explicit `materialUpkeep` override // wins -- guns and aircraft are cheap parked and costly when fired, so their // standing bill is set by hand -- otherwise it scales with the entry's value. export function protoUpkeep(proto, value) { if (proto && proto.materialUpkeep) { return { steel: proto.materialUpkeep.steel || 0, energy: proto.materialUpkeep.energy || 0, hightech: proto.materialUpkeep.hightech || 0, }; } return upkeepResources(value, !!(proto && proto.advanced)); } // The materials and power a tile improvement spends to be built: steel in // proportion to its money cost, the electricity that steel embodies, and // high-tech for an advanced work. Shared so the server's bill and the browser's // cost buttons agree on the same figures. export function tileImprovementResourceCost(proto, buildCost = null) { const cost = Math.max(0, buildCost !== null ? buildCost : ((proto && proto.buildCost) || 0)); return { steel: cost * RESOURCE_RULES.steelPerBudgetEuro, energy: cost * RESOURCE_RULES.constructionEnergyPerBudgetEuro, hightech: proto && proto.advanced ? RESOURCE_RULES.constructionHighTechPerLevel : 0, }; } // The steel and high-tech a building or unit spends to be built: steel in // proportion to its money cost, high-tech for an advanced item (more with every // level). Mirrors the server's construction bill so the browser can quote it at // today's prices. export function constructionResourceCost(proto, level = 0, buildCost = null) { const cost = buildCost !== null ? buildCost : ((proto && (proto.buildCost || proto.baseCost)) || 0); const advanced = !!(proto && (proto.advanced || (proto.effects || []).some((e) => e.stat === EFFECT_RESEARCH))); return { steel: Math.max(0, cost) * RESOURCE_RULES.steelPerBudgetEuro, hightech: advanced ? RESOURCE_RULES.constructionHighTechPerLevel * (level + 1) : 0, }; } // Today's global price index: the average ratio of each resource's current // price to its base. One figure lets a money cost with no material bill of its // own be quoted at the market. export function marketPriceIndex(prices) { let total = 0; let count = 0; for (const id of RESOURCE_IDS) { const base = RESOURCE_MARKET_BASE[id]; if (!(base > 0)) continue; const price = prices && prices[id] !== undefined ? prices[id] : base; total += price / base; count += 1; } return count > 0 ? total / count : 1; } // The market value of a resource bill at the given prices, a resource at a time. export function resourceMarketValue(resources, prices) { let total = 0; for (const id of RESOURCE_IDS) { const amount = (resources && resources[id]) || 0; if (!(amount > 0)) continue; const price = prices && prices[id] !== undefined ? prices[id] : RESOURCE_MARKET_BASE[id]; total += amount * (price || 0); } return total; } // The energy a material producer burns to make its output: the energy-per-unit // ratio from the rules, whichever resource it makes. export function producerEnergyPerDay(id, outputPerDay) { if (id === "steel") return outputPerDay * RESOURCE_RULES.energyPerSteelTonne; if (id === "food") return outputPerDay * RESOURCE_RULES.energyPerFoodTonne; if (id === "hightech") return outputPerDay * RESOURCE_RULES.energyPerHighTechUnit; return 0; } // ----------------------------------------------------------------- market -- // Eases a price toward the equilibrium the day's world-wide supply and demand // imply: the price that clears supply against demand is `base * demand/supply`, // and the price travels a fixed share of the way there each day. A sustained // shortage therefore lifts a good above its base and a glut cheapens it, but the // price settles at the equilibrium instead of compounding without bound, so no // price band is needed to keep it sane. A day with no supply at all is treated as // the sharpest shortage the ratio may read, and the ratio is capped so a // rounding-error supply cannot run the equilibrium away. export function nextMarketPrice(basePrice, price, supply, demand) { if (!(demand > 0)) return basePrice; const ratio = supply > 0 ? demand / supply : RESOURCE_MARKET.maxRatio; const equilibrium = basePrice * Math.min(ratio, RESOURCE_MARKET.maxRatio); return price + (equilibrium - price) * RESOURCE_MARKET.adjustment; }