132 lines
6.8 KiB
JavaScript
132 lines
6.8 KiB
JavaScript
// Simple-economy figures, in their own namespace so they cannot collide with
|
||
// the hard economy's ECONOMY, MONEY and RESOURCE_* constants. The simple model
|
||
// is the default from now on; the rich per-region simulation is kept intact as
|
||
// the "hard" model and selected explicitly.
|
||
|
||
// The two models a game may run, and the one used when none is given.
|
||
export const ECONOMY_MODELS = ["simple", "hard"];
|
||
export const DEFAULT_ECONOMY_MODEL = "simple";
|
||
|
||
export const SIMPLE_ECONOMY = {
|
||
// Tax: every inhabitant pays this many euros a day, scaled by twice the
|
||
// region's approval (a 0..2 factor) and any tax-income modifiers.
|
||
taxPerInhabitant: 100,
|
||
approvalTaxFactor: 2,
|
||
// The one shared currency: there is no conversion, every nation quotes prices
|
||
// in euros.
|
||
euro: { code: "EUR", symbol: "€", name: "Euro" },
|
||
// The fixed euro reserve every nation opens with. Money buys only market
|
||
// goods in the simple economy, so this is day-one buying power rather than a
|
||
// working balance the state spends internally.
|
||
startingTreasury: 1_000_000_000,
|
||
// Propaganda is the one thing culture is spent on: a running campaign drains
|
||
// culture at its hourly rate (the opinion shift scales with that rate just as
|
||
// the hard economy's money spend did), and lapses when the nation can no
|
||
// longer fund it. The hard economy's figures are money, so the simple model
|
||
// keeps its own, sized near a culture building's hourly output.
|
||
campaign: {
|
||
defaultCulturePerHour: 8,
|
||
maxCulturePerHour: 40,
|
||
},
|
||
// The comfortable working stock every nation's single resource pool opens
|
||
// with. Unlike the hard economy's year of per-region reserves this is a fixed
|
||
// figure, not derated from consumption: with one pool per player there is
|
||
// nowhere for a surplus region to hoard, so the opening stock only has to
|
||
// bridge the first works coming online. Energy is not pooled -- it stays a
|
||
// per-region flow (see `03-construction.md`) -- so only the four storable
|
||
// goods appear here.
|
||
startingPool: {
|
||
steel: 1_000_000,
|
||
food: 500_000,
|
||
luxury: 500_000,
|
||
hightech: 20_000,
|
||
},
|
||
// Construction in the simple economy starts the moment it is ordered: there
|
||
// is no materials-gathering phase, and the speed comes from the spare power
|
||
// of the region the work stands in rather than from national GDP.
|
||
construction: {
|
||
// The cost/energy exchange at the heart of simple construction: one euro of
|
||
// build cost needs this many kWh a day of spare regional power to be raised
|
||
// in one hour. A work's base hours are `cost * energyPerConstructionHour /
|
||
// max(available, minEnergy)`, then the building/training ratio below and the
|
||
// usual approval and technology multipliers. A tighter grid raises the same
|
||
// work slower, the opposite of the hard economy's richer-nation-builds-faster.
|
||
energyPerConstructionHour: 1_600,
|
||
// The construction power every region is credited with even when its grid
|
||
// has nothing to spare. This is the guarantee behind the opening pace: a
|
||
// modern-infantry unit (cost 100M) always trains in 45 game hours -- 45 real
|
||
// seconds, one hour a second -- in a starter city on any map, however
|
||
// starved its grid. A region with more spare power builds the same work
|
||
// faster; none builds it slower. Sized as `100M * energyPerConstructionHour
|
||
// / 45`, so the reference work lands exactly on 45 hours at the floor.
|
||
minEnergy: (100_000_000 * 1_600) / 45,
|
||
// The hard economy builds ten times faster than it trains a unit of the same
|
||
// cost; the simple economy keeps that ratio.
|
||
buildingTimeMultiplier: 0.1,
|
||
// No work finishes in under an hour, so even a surging grid cannot make a
|
||
// build instant.
|
||
minHours: 1,
|
||
// The fuel-free plants, in preference order, used to guarantee every opening
|
||
// region a power plant. All three are land buildings with no fuel bill, so
|
||
// the simple economy never implies a fuel resource.
|
||
seedPlantIds: ["solar_power_plant", "wind_turbines", "geothermal_plant"],
|
||
},
|
||
};
|
||
|
||
// The base hours a simple-economy work of `cost` takes at `energy` kWh a day of
|
||
// spare regional power. `kind` is "unit" for training, anything else for a
|
||
// building, tile work or upgrade, which the figures in `construction` run
|
||
// faster. Energy is floored at the region's guaranteed construction power, so a
|
||
// blacked-out starter region still raises the reference unit in 45 hours and
|
||
// divides by no zero; more power than the floor only makes a work quicker.
|
||
// Shared by the server's clock and the browser's quoted times, so the two can
|
||
// never disagree.
|
||
export function simpleConstructionHours(cost, energy, kind) {
|
||
const available = Math.max(
|
||
Number.isFinite(energy) ? energy : 0,
|
||
SIMPLE_ECONOMY.construction.minEnergy
|
||
);
|
||
const base = Math.max(0, cost) *
|
||
SIMPLE_ECONOMY.construction.energyPerConstructionHour / available;
|
||
const scaled = kind === "unit"
|
||
? base
|
||
: base * SIMPLE_ECONOMY.construction.buildingTimeMultiplier;
|
||
return Math.max(SIMPLE_ECONOMY.construction.minHours, scaled);
|
||
}
|
||
|
||
// The global market every simple-economy nation buys and sells from. One shared
|
||
// world stock per storable good opens at this figure, and the price is the base
|
||
// price scaled by how far the stock has fallen from it: half the stock doubles
|
||
// the price, a glut cheapens it. Buying drains the stock (raising the price),
|
||
// selling refills it (lowering it), so the two sides of the trade price
|
||
// themselves. A single shared stock, not a per-player order book, matches the
|
||
// formula and the opening figure.
|
||
export const MARKET_START_STOCK = 1_000_000;
|
||
|
||
// The only quantities a market order may name, in canonical units. The client
|
||
// offers exactly these three buttons and the server re-validates the lot.
|
||
export const MARKET_LOTS = [100, 2_000, 50_000];
|
||
|
||
// The stock a price is never allowed to read below, so an emptied market cannot
|
||
// send the price to infinity (or to NaN on a zero). One thousandth of the
|
||
// opening stock caps the dearest a good can get at a thousand times its base.
|
||
export const MARKET_MIN_STOCK = 1_000;
|
||
|
||
// The market price of one good at a given world stock:
|
||
// `basePrice × (stock / MARKET_START_STOCK) ^ -1`, clamped so a low or broken
|
||
// stock stays finite. This is the single source of truth the tick, the order
|
||
// handler and the snapshot all price a good through.
|
||
export function marketPriceFor(basePrice, stock) {
|
||
const available = Number.isFinite(stock) && stock > MARKET_MIN_STOCK
|
||
? stock
|
||
: MARKET_MIN_STOCK;
|
||
return basePrice * Math.pow(available / MARKET_START_STOCK, -1);
|
||
}
|
||
|
||
// Coerces a stored or configured model name to a known one. An unknown value
|
||
// falls back to the default rather than throwing, so a typo or a stale save
|
||
// cannot stop a game from starting.
|
||
export function normalizeEconomyModel(value) {
|
||
return ECONOMY_MODELS.includes(value) ? value : DEFAULT_ECONOMY_MODEL;
|
||
}
|