Files
Battle-for-Tismo/shared/resources.js
T
adrien e7ee5dbf0e Added the resource economy, its works and their icons
- Five resources (energy, steel, food, luxury, high-tech) with per-city stores, a world market, delivery and material upkeep.
- Resource-producing tile works built outside cities; every nation opens with the works its economy needs.
- Real game-icons.net art replacing the dummy icons for the new resources and works, with the credits updated.
2026-09-22 05:27:11 +02:00

226 lines
8.9 KiB
JavaScript

// 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,
resourceById,
} from "./data/resources.js";
import { EFFECT_RESEARCH } from "./data/effects.js";
const DAYS_PER_YEAR = 365;
// --------------------------------------------------------------- formatting --
// Energy reads in kWh, MWh, GWh or TWh, whichever keeps the figure short.
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`;
}
// A material in its own friendly unit: tonnes step up through kt and Mt,
// carats through kct, high-tech through ku.
export function formatResourceAmount(id, value) {
if (id === "energy") return formatEnergy(value);
const amount = Math.max(0, value);
let unit = resourceById(id) ? resourceById(id).unit : "";
let scaled = amount;
let prefix = "";
if (amount >= 1e9) {
scaled = amount / 1e9;
prefix = "M";
} else if (amount >= 1e6) {
scaled = amount / 1e6;
prefix = "k";
}
return `${trim(scaled)} ${prefix}${unit}`;
}
function trim(value) {
if (value >= 100) return String(Math.round(value));
if (value >= 10) return value.toFixed(1).replace(/\.0$/, "");
return value.toFixed(2).replace(/\.?0+$/, "");
}
// --------------------------------------------------------------- per-person --
export function foodNeedPerDay(population) {
return Math.max(0, population) * RESOURCE_RULES.foodPerPersonPerDay;
}
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;
}
// How much food a city wants in store before the coming month: a month at the
// current appetite, with a 130% cushion.
export function foodMonthlyTarget(population) {
return foodNeedPerDay(population) *
RESOURCE_RULES.daysPerMonth *
RESOURCE_RULES.foodMonthlyBuffer;
}
// 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;
}
// ---------------------------------------------------------------- delivery --
// The kWh of transport energy one material tonne costs over `tiles` tiles in the
// given mode. A material tonne and an energy tonne (10 GWh) share the formula.
export function deliveryEnergyKwh(tonnes, tiles, mode, tileMovementCost = 1) {
const d = RESOURCE_RULES.deliveryEnergy;
const perTile = mode === "train"
? d.train
: mode === "ship"
? d.ship
: mode === "road"
? d.truckRoad
: d.truckOffRoad * Math.max(tileMovementCost, 0);
return tonnes * Math.max(tiles, 0) * perTile;
}
// The energy cost of delivering `amount` of a resource over `tiles`. Every
// resource carries a mass per canonical unit, so a carat or a chip is charged
// for the tonnes it actually weighs rather than as if it were a tonne.
export function deliveryEnergyForResource(id, amount, tiles, mode, tileMovementCost = 1) {
const proto = resourceById(id);
const tonnes = amount * (proto ? proto.tonnesPerUnit : 1);
return deliveryEnergyKwh(tonnes, tiles, mode, tileMovementCost);
}
// The transport mode a tile offers, most efficient first: rail beats road, and
// an unimproved tile falls back to an off-road truck. Sea legs ship.
export function transportModeForTile(tile, road, railway, sea) {
if (sea || (tile && tile.terrainClass === "Sea")) return "ship";
if (railway) return "train";
if (road) return "road";
return "truck";
}
// ----------------------------------------------------------------- market --
// Moves a price toward what the day's world-wide supply and demand imply. A
// glut pushes it down, a shortage up, and the clamp keeps the market sane. A
// zero-supply day is treated as the sharpest shortage.
export function nextMarketPrice(basePrice, price, supply, demand) {
if (!(demand > 0)) return basePrice;
const ratio = supply > 0 ? demand / supply : RESOURCE_MARKET.maxMultiplier;
const pressure = (ratio - 1) * RESOURCE_MARKET.adjustment;
const moved = price * (1 + pressure);
const min = basePrice * RESOURCE_MARKET.minMultiplier;
const max = basePrice * RESOURCE_MARKET.maxMultiplier;
return Math.max(min, Math.min(max, moved));
}