Files
Country-Game---The-Desk/web/js/engine/economy.js
T

92 lines
3.3 KiB
JavaScript

// Public finances: revenue, spending, interest, debt and GDP. All flows are in
// % of GDP per year; debt and gdp are absolute (billions of the scenario currency).
import { TAX_IDS, clamp } from "./state.js";
// Laffer-style curve: revenue peaks at rate == peak, then falls.
export function taxRevenue(id, rate, bal) {
const t = bal.tax[id];
return t.coef * rate * (1 - rate / (2 * t.peak));
}
export function interestRate(debtRatio, bal) {
const d = bal.debt;
const excess = Math.max(0, debtRatio - d.premiumFrom);
return d.baseRate + d.premiumCoef * excess * excess;
}
export function computeFiscal(state, bal) {
const e = state.economy;
let revenue = e.otherRevenue;
for (const id of TAX_IDS) revenue += taxRevenue(id, e.taxes[id], bal);
// Receipts follow the cycle: a slump costs revenue, a boom adds some.
revenue += (bal.fiscal?.growthSens ?? 0) * (e.growth - state.ref.vars.growth);
const debtRatio = (e.debt / e.gdp) * 100;
const rate = interestRate(debtRatio, bal);
const interest = (debtRatio * rate) / 100;
let allocTotal = 0;
for (const s of Object.values(state.sectors)) allocTotal += s.alloc;
const spending = allocTotal + interest + e.extraSpending;
return { revenue, allocTotal, interest, spending, deficit: spending - revenue, debtRatio, rate };
}
// Derived quantities that indicator/variable targets are written against.
// ref values are captured at game creation so the start is an equilibrium.
export function computeDevs(state, bal, fiscal) {
const e = state.economy;
let taxPressure = 0;
let taxGrowthDrag = 0;
for (const id of TAX_IDS) {
const d = e.taxes[id] - state.ref.taxes[id];
taxPressure += bal.tax[id].opinion * d;
taxGrowthDrag += bal.tax[id].growth * d;
}
const start = bal.crackdown.unrestStart;
return {
growth: e.growth,
unemployment: e.unemployment,
inflation: e.inflation,
debt: Math.max(0, fiscal.debtRatio - bal.debt.freeBelow),
deficit: fiscal.deficit,
taxPressure,
taxGrowthDrag,
// police sent in when unrest is high: 0..1
crackdown: clamp((state.indicators.unrest - start) / (100 - start), 0, 1),
};
}
export function advanceEconomy(state, fiscal) {
const e = state.economy;
e.debt = Math.max(0, e.debt + ((fiscal.deficit / 100) * e.gdp) / 365);
e.gdp *= Math.pow(1 + e.growth / 100, 1 / 365);
}
// Player action, only allowed while the quarterly review is open.
export function setBudget(state, bal, { alloc = {}, taxes = {} } = {}) {
if (!state.pending.review) return { ok: false, errors: ["budget_locked"] };
const errors = [];
for (const [id, v] of Object.entries(alloc)) {
const s = state.sectors[id];
if (!s) errors.push(`unknown_sector:${id}`);
else if (!(v >= 0 && v <= s.need * bal.limits.allocMaxRatio)) errors.push(`alloc_out_of_range:${id}`);
}
for (const [id, v] of Object.entries(taxes)) {
if (!TAX_IDS.includes(id)) errors.push(`unknown_tax:${id}`);
else if (!(v >= 0 && v <= bal.tax[id].peak * bal.limits.taxMaxFactorOfPeak)) errors.push(`tax_out_of_range:${id}`);
}
if (errors.length) return { ok: false, errors };
for (const [id, v] of Object.entries(alloc)) state.sectors[id].alloc = v;
for (const [id, v] of Object.entries(taxes)) state.economy.taxes[id] = v;
return { ok: true, errors: [] };
}
export function confirmBudget(state) {
state.pending.review = false;
state.risk.emergency = false;
}