92 lines
3.3 KiB
JavaScript
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;
|
|
}
|