// 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; }